@yzj01/llm-router 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2726 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/proxy.ts
4
+ import http from "http";
5
+
6
+ // src/router/rules.ts
7
+ function scoreTokenCount(estimatedTokens, thresholds) {
8
+ if (estimatedTokens < thresholds.simple) {
9
+ return { name: "tokenCount", score: -1, signal: `short (${estimatedTokens} tokens)` };
10
+ }
11
+ if (estimatedTokens > thresholds.complex) {
12
+ return { name: "tokenCount", score: 1, signal: `long (${estimatedTokens} tokens)` };
13
+ }
14
+ return { name: "tokenCount", score: 0, signal: null };
15
+ }
16
+ function scoreKeywordMatch(text, keywords, name, signalLabel, thresholds, scores) {
17
+ const matches = keywords.filter((keyword) => text.includes(keyword.toLowerCase()));
18
+ if (matches.length >= thresholds.high) {
19
+ return { name, score: scores.high, signal: `${signalLabel} (${matches.slice(0, 3).join(", ")})` };
20
+ }
21
+ if (matches.length >= thresholds.low) {
22
+ return { name, score: scores.low, signal: `${signalLabel} (${matches.slice(0, 3).join(", ")})` };
23
+ }
24
+ return { name, score: scores.none, signal: null };
25
+ }
26
+ function scoreMultiStep(text) {
27
+ const patterns = [/first.*then/i, /step \d/i, /\d\.\s/];
28
+ return patterns.some((pattern) => pattern.test(text)) ? { name: "multiStepPatterns", score: 0.5, signal: "multi-step" } : { name: "multiStepPatterns", score: 0, signal: null };
29
+ }
30
+ function scoreQuestionComplexity(prompt) {
31
+ const count = (prompt.match(/\?/g) ?? []).length;
32
+ return count > 3 ? { name: "questionComplexity", score: 0.5, signal: `${count} questions` } : { name: "questionComplexity", score: 0, signal: null };
33
+ }
34
+ function scoreAgenticTask(text, keywords) {
35
+ let matchCount = 0;
36
+ const signals = [];
37
+ for (const keyword of keywords) {
38
+ if (text.includes(keyword.toLowerCase())) {
39
+ matchCount++;
40
+ if (signals.length < 3) signals.push(keyword);
41
+ }
42
+ }
43
+ if (matchCount >= 4) {
44
+ return {
45
+ dimensionScore: { name: "agenticTask", score: 1, signal: `agentic (${signals.join(", ")})` },
46
+ agenticScore: 1
47
+ };
48
+ }
49
+ if (matchCount >= 3) {
50
+ return {
51
+ dimensionScore: { name: "agenticTask", score: 0.6, signal: `agentic (${signals.join(", ")})` },
52
+ agenticScore: 0.6
53
+ };
54
+ }
55
+ if (matchCount >= 1) {
56
+ return {
57
+ dimensionScore: { name: "agenticTask", score: 0.2, signal: `agentic-light (${signals.join(", ")})` },
58
+ agenticScore: 0.2
59
+ };
60
+ }
61
+ return {
62
+ dimensionScore: { name: "agenticTask", score: 0, signal: null },
63
+ agenticScore: 0
64
+ };
65
+ }
66
+ function classifyByRules(prompt, _systemPrompt, estimatedTokens, config) {
67
+ const userText = prompt.toLowerCase();
68
+ const dimensions = [
69
+ scoreTokenCount(estimatedTokens, config.tokenCountThresholds),
70
+ scoreKeywordMatch(userText, config.codeKeywords, "codePresence", "code", { low: 1, high: 2 }, { none: 0, low: 0.5, high: 1 }),
71
+ scoreKeywordMatch(userText, config.reasoningKeywords, "reasoningMarkers", "reasoning", { low: 1, high: 2 }, { none: 0, low: 0.7, high: 1 }),
72
+ scoreKeywordMatch(userText, config.technicalKeywords, "technicalTerms", "technical", { low: 2, high: 4 }, { none: 0, low: 0.5, high: 1 }),
73
+ scoreKeywordMatch(userText, config.creativeKeywords, "creativeMarkers", "creative", { low: 1, high: 2 }, { none: 0, low: 0.5, high: 0.7 }),
74
+ scoreKeywordMatch(userText, config.simpleKeywords, "simpleIndicators", "simple", { low: 1, high: 2 }, { none: 0, low: -1, high: -1 }),
75
+ scoreMultiStep(userText),
76
+ scoreQuestionComplexity(prompt),
77
+ scoreKeywordMatch(userText, config.imperativeVerbs, "imperativeVerbs", "imperative", { low: 1, high: 2 }, { none: 0, low: 0.3, high: 0.5 }),
78
+ scoreKeywordMatch(userText, config.constraintIndicators, "constraintCount", "constraints", { low: 1, high: 3 }, { none: 0, low: 0.3, high: 0.7 }),
79
+ scoreKeywordMatch(userText, config.outputFormatKeywords, "outputFormat", "format", { low: 1, high: 2 }, { none: 0, low: 0.4, high: 0.7 }),
80
+ scoreKeywordMatch(userText, config.referenceKeywords, "referenceComplexity", "references", { low: 1, high: 2 }, { none: 0, low: 0.3, high: 0.5 }),
81
+ scoreKeywordMatch(userText, config.negationKeywords, "negationComplexity", "negation", { low: 2, high: 3 }, { none: 0, low: 0.3, high: 0.5 }),
82
+ scoreKeywordMatch(userText, config.domainSpecificKeywords, "domainSpecificity", "domain-specific", { low: 1, high: 2 }, { none: 0, low: 0.5, high: 0.8 })
83
+ ];
84
+ const agenticResult = scoreAgenticTask(userText, config.agenticTaskKeywords);
85
+ dimensions.push(agenticResult.dimensionScore);
86
+ const signals = dimensions.filter((dimension) => dimension.signal !== null).map((dimension) => dimension.signal);
87
+ const weightedScore = dimensions.reduce(
88
+ (score, dimension) => score + dimension.score * (config.dimensionWeights[dimension.name] ?? 0),
89
+ 0
90
+ );
91
+ const reasoningMatches = config.reasoningKeywords.filter(
92
+ (keyword) => userText.includes(keyword.toLowerCase())
93
+ );
94
+ if (reasoningMatches.length >= 2) {
95
+ return {
96
+ score: weightedScore,
97
+ tier: "REASONING",
98
+ confidence: Math.max(calibrateConfidence(Math.max(weightedScore, 0.3), config.confidenceSteepness), 0.85),
99
+ signals,
100
+ agenticScore: agenticResult.agenticScore,
101
+ dimensions
102
+ };
103
+ }
104
+ const { simpleMedium, mediumComplex, complexReasoning } = config.tierBoundaries;
105
+ let tier;
106
+ let distanceFromBoundary;
107
+ if (weightedScore < simpleMedium) {
108
+ tier = "SIMPLE";
109
+ distanceFromBoundary = simpleMedium - weightedScore;
110
+ } else if (weightedScore < mediumComplex) {
111
+ tier = "MEDIUM";
112
+ distanceFromBoundary = Math.min(weightedScore - simpleMedium, mediumComplex - weightedScore);
113
+ } else if (weightedScore < complexReasoning) {
114
+ tier = "COMPLEX";
115
+ distanceFromBoundary = Math.min(weightedScore - mediumComplex, complexReasoning - weightedScore);
116
+ } else {
117
+ tier = "REASONING";
118
+ distanceFromBoundary = weightedScore - complexReasoning;
119
+ }
120
+ const confidence = calibrateConfidence(distanceFromBoundary, config.confidenceSteepness);
121
+ if (confidence < config.confidenceThreshold) {
122
+ return {
123
+ score: weightedScore,
124
+ tier: null,
125
+ confidence,
126
+ signals,
127
+ agenticScore: agenticResult.agenticScore,
128
+ dimensions
129
+ };
130
+ }
131
+ return {
132
+ score: weightedScore,
133
+ tier,
134
+ confidence,
135
+ signals,
136
+ agenticScore: agenticResult.agenticScore,
137
+ dimensions
138
+ };
139
+ }
140
+ function calibrateConfidence(distance, steepness) {
141
+ return 1 / (1 + Math.exp(-steepness * distance));
142
+ }
143
+
144
+ // src/router/selector.ts
145
+ var DEFAULT_BASELINE_INPUT_PRICE = 0.56;
146
+ var DEFAULT_BASELINE_OUTPUT_PRICE = 1.68;
147
+ function selectModel(tier, confidence, method, reasoning, tierConfigs, modelPricing, estimatedInputTokens, maxOutputTokens, agenticScore, score) {
148
+ const config = tierConfigs[tier];
149
+ if (!config) {
150
+ throw new Error(`Missing tier config for ${tier}`);
151
+ }
152
+ const model = config.primary;
153
+ const baselineModel = tierConfigs["COMPLEX"].primary;
154
+ const costs = calculateModelCost(
155
+ model,
156
+ modelPricing,
157
+ estimatedInputTokens,
158
+ maxOutputTokens,
159
+ baselineModel
160
+ );
161
+ return {
162
+ publicModel: model,
163
+ tier,
164
+ confidence,
165
+ method,
166
+ reasoning,
167
+ ...costs,
168
+ ...agenticScore !== void 0 && { agenticScore },
169
+ ...score !== void 0 && { score }
170
+ };
171
+ }
172
+ function getFallbackChain(tier, tierConfigs) {
173
+ const config = tierConfigs[tier];
174
+ return [config.primary, ...config.fallback];
175
+ }
176
+ function calculateModelCost(model, modelPricing, estimatedInputTokens, maxOutputTokens, baselineModelId) {
177
+ const pricing = modelPricing.get(model);
178
+ const inputCost = estimatedInputTokens / 1e6 * (pricing?.inputPrice ?? 0);
179
+ const outputCost = maxOutputTokens / 1e6 * (pricing?.outputPrice ?? 0);
180
+ const costEstimate = inputCost + outputCost;
181
+ const baselinePricing = baselineModelId ? modelPricing.get(baselineModelId) : void 0;
182
+ const baselineInput = estimatedInputTokens / 1e6 * (baselinePricing?.inputPrice ?? DEFAULT_BASELINE_INPUT_PRICE);
183
+ const baselineOutput = maxOutputTokens / 1e6 * (baselinePricing?.outputPrice ?? DEFAULT_BASELINE_OUTPUT_PRICE);
184
+ const baselineCost = baselineInput + baselineOutput;
185
+ const savings = baselineCost > 0 ? Math.max(0, (baselineCost - costEstimate) / baselineCost) : 0;
186
+ return { costEstimate, baselineCost, savings };
187
+ }
188
+ function filterByExcludeList(models, excludeList) {
189
+ if (excludeList.size === 0) return models;
190
+ const filtered = models.filter((model) => !excludeList.has(model));
191
+ return filtered.length > 0 ? filtered : models;
192
+ }
193
+
194
+ // src/router/strategy.ts
195
+ var RulesStrategy = class {
196
+ name = "rules";
197
+ route(prompt, systemPrompt, maxOutputTokens, options) {
198
+ const { config, modelPricing } = options;
199
+ if (!config) {
200
+ throw new Error("Routing config is required at runtime");
201
+ }
202
+ if (!modelPricing) {
203
+ throw new Error("Model pricing is required at runtime");
204
+ }
205
+ const fullText = `${systemPrompt ?? ""} ${prompt}`;
206
+ const estimatedTokens = Math.ceil(fullText.length / 4);
207
+ const ruleResult = classifyByRules(
208
+ prompt,
209
+ systemPrompt,
210
+ estimatedTokens,
211
+ config.scoring
212
+ );
213
+ const { tierConfigs, profile, profileSuffix } = chooseTierConfigs(options);
214
+ const hasStructuredOutput = systemPrompt ? /json|structured|schema/i.test(systemPrompt) : false;
215
+ let tier;
216
+ let confidence;
217
+ let reasoning = `score=${ruleResult.score.toFixed(2)} | ${ruleResult.signals.join(", ")}`;
218
+ if (ruleResult.tier !== null) {
219
+ tier = ruleResult.tier;
220
+ confidence = ruleResult.confidence;
221
+ } else {
222
+ tier = config.overrides?.ambiguousDefaultTier ?? "MEDIUM";
223
+ confidence = 0.5;
224
+ reasoning += ` | ambiguous -> default: ${tier}`;
225
+ }
226
+ if (hasStructuredOutput) {
227
+ const tierRank = {
228
+ SIMPLE: 0,
229
+ MEDIUM: 1,
230
+ COMPLEX: 2,
231
+ REASONING: 3
232
+ };
233
+ const minTier = config.overrides?.structuredOutputMinTier ?? "MEDIUM";
234
+ if (tierRank[tier] < tierRank[minTier]) {
235
+ reasoning += ` | upgraded to ${minTier} (structured output)`;
236
+ tier = minTier;
237
+ } else {
238
+ reasoning += " | structured output";
239
+ }
240
+ }
241
+ reasoning += profileSuffix;
242
+ const decision = selectModel(
243
+ tier,
244
+ confidence,
245
+ "rules",
246
+ reasoning,
247
+ tierConfigs,
248
+ modelPricing,
249
+ estimatedTokens,
250
+ maxOutputTokens,
251
+ ruleResult.agenticScore,
252
+ ruleResult.score
253
+ );
254
+ return { ...decision, tierConfigs, profile };
255
+ }
256
+ };
257
+ function chooseTierConfigs(options) {
258
+ const config = options?.config;
259
+ if (!config?.tiers) {
260
+ throw new Error("Routing tiers are required at runtime");
261
+ }
262
+ return {
263
+ tierConfigs: config.tiers,
264
+ profile: "default",
265
+ profileSuffix: ""
266
+ };
267
+ }
268
+ var registry = /* @__PURE__ */ new Map();
269
+ registry.set("rules", new RulesStrategy());
270
+ function getStrategy(name) {
271
+ const strategy = registry.get(name);
272
+ if (!strategy) {
273
+ throw new Error(`Unknown routing strategy: ${name}`);
274
+ }
275
+ return strategy;
276
+ }
277
+ function registerStrategy(strategy) {
278
+ registry.set(strategy.name, strategy);
279
+ }
280
+
281
+ // src/router/config.ts
282
+ var DEFAULT_ROUTING_CONFIG = {
283
+ version: "2.0",
284
+ scoring: {
285
+ tokenCountThresholds: { simple: 50, complex: 500 },
286
+ // Multilingual keywords: EN + ZH + JA + RU + DE + ES + PT + KO + AR
287
+ codeKeywords: [
288
+ // English
289
+ "function",
290
+ "class",
291
+ "import",
292
+ "def",
293
+ "SELECT",
294
+ "async",
295
+ "await",
296
+ "const",
297
+ "let",
298
+ "var",
299
+ "return",
300
+ "```",
301
+ // Chinese
302
+ "\u51FD\u6570",
303
+ "\u7C7B",
304
+ "\u5BFC\u5165",
305
+ "\u5B9A\u4E49",
306
+ "\u67E5\u8BE2",
307
+ "\u5F02\u6B65",
308
+ "\u7B49\u5F85",
309
+ "\u5E38\u91CF",
310
+ "\u53D8\u91CF",
311
+ "\u8FD4\u56DE",
312
+ // Japanese
313
+ "\u95A2\u6570",
314
+ "\u30AF\u30E9\u30B9",
315
+ "\u30A4\u30F3\u30DD\u30FC\u30C8",
316
+ "\u975E\u540C\u671F",
317
+ "\u5B9A\u6570",
318
+ "\u5909\u6570",
319
+ // Russian
320
+ "\u0444\u0443\u043D\u043A\u0446\u0438\u044F",
321
+ "\u043A\u043B\u0430\u0441\u0441",
322
+ "\u0438\u043C\u043F\u043E\u0440\u0442",
323
+ "\u043E\u043F\u0440\u0435\u0434\u0435\u043B",
324
+ "\u0437\u0430\u043F\u0440\u043E\u0441",
325
+ "\u0430\u0441\u0438\u043D\u0445\u0440\u043E\u043D\u043D\u044B\u0439",
326
+ "\u043E\u0436\u0438\u0434\u0430\u0442\u044C",
327
+ "\u043A\u043E\u043D\u0441\u0442\u0430\u043D\u0442\u0430",
328
+ "\u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u0430\u044F",
329
+ "\u0432\u0435\u0440\u043D\u0443\u0442\u044C",
330
+ // German
331
+ "funktion",
332
+ "klasse",
333
+ "importieren",
334
+ "definieren",
335
+ "abfrage",
336
+ "asynchron",
337
+ "erwarten",
338
+ "konstante",
339
+ "variable",
340
+ "zur\xFCckgeben",
341
+ // Spanish
342
+ "funci\xF3n",
343
+ "clase",
344
+ "importar",
345
+ "definir",
346
+ "consulta",
347
+ "as\xEDncrono",
348
+ "esperar",
349
+ "constante",
350
+ "variable",
351
+ "retornar",
352
+ // Portuguese
353
+ "fun\xE7\xE3o",
354
+ "classe",
355
+ "importar",
356
+ "definir",
357
+ "consulta",
358
+ "ass\xEDncrono",
359
+ "aguardar",
360
+ "constante",
361
+ "vari\xE1vel",
362
+ "retornar",
363
+ // Korean
364
+ "\uD568\uC218",
365
+ "\uD074\uB798\uC2A4",
366
+ "\uAC00\uC838\uC624\uAE30",
367
+ "\uC815\uC758",
368
+ "\uCFFC\uB9AC",
369
+ "\uBE44\uB3D9\uAE30",
370
+ "\uB300\uAE30",
371
+ "\uC0C1\uC218",
372
+ "\uBCC0\uC218",
373
+ "\uBC18\uD658",
374
+ // Arabic
375
+ "\u062F\u0627\u0644\u0629",
376
+ "\u0641\u0626\u0629",
377
+ "\u0627\u0633\u062A\u064A\u0631\u0627\u062F",
378
+ "\u062A\u0639\u0631\u064A\u0641",
379
+ "\u0627\u0633\u062A\u0639\u0644\u0627\u0645",
380
+ "\u063A\u064A\u0631 \u0645\u062A\u0632\u0627\u0645\u0646",
381
+ "\u0627\u0646\u062A\u0638\u0627\u0631",
382
+ "\u062B\u0627\u0628\u062A",
383
+ "\u0645\u062A\u063A\u064A\u0631",
384
+ "\u0625\u0631\u062C\u0627\u0639"
385
+ ],
386
+ reasoningKeywords: [
387
+ // English
388
+ "prove",
389
+ "theorem",
390
+ "derive",
391
+ "step by step",
392
+ "chain of thought",
393
+ "formally",
394
+ "mathematical",
395
+ "proof",
396
+ "logically",
397
+ // Chinese
398
+ "\u8BC1\u660E",
399
+ "\u5B9A\u7406",
400
+ "\u63A8\u5BFC",
401
+ "\u9010\u6B65",
402
+ "\u601D\u7EF4\u94FE",
403
+ "\u5F62\u5F0F\u5316",
404
+ "\u6570\u5B66",
405
+ "\u903B\u8F91",
406
+ // Japanese
407
+ "\u8A3C\u660E",
408
+ "\u5B9A\u7406",
409
+ "\u5C0E\u51FA",
410
+ "\u30B9\u30C6\u30C3\u30D7\u30D0\u30A4\u30B9\u30C6\u30C3\u30D7",
411
+ "\u8AD6\u7406\u7684",
412
+ // Russian
413
+ "\u0434\u043E\u043A\u0430\u0437\u0430\u0442\u044C",
414
+ "\u0434\u043E\u043A\u0430\u0436\u0438",
415
+ "\u0434\u043E\u043A\u0430\u0437\u0430\u0442\u0435\u043B\u044C\u0441\u0442\u0432",
416
+ "\u0442\u0435\u043E\u0440\u0435\u043C\u0430",
417
+ "\u0432\u044B\u0432\u0435\u0441\u0442\u0438",
418
+ "\u0448\u0430\u0433 \u0437\u0430 \u0448\u0430\u0433\u043E\u043C",
419
+ "\u043F\u043E\u0448\u0430\u0433\u043E\u0432\u043E",
420
+ "\u043F\u043E\u044D\u0442\u0430\u043F\u043D\u043E",
421
+ "\u0446\u0435\u043F\u043E\u0447\u043A\u0430 \u0440\u0430\u0441\u0441\u0443\u0436\u0434\u0435\u043D\u0438\u0439",
422
+ "\u0440\u0430\u0441\u0441\u0443\u0436\u0434\u0435\u043D\u0438",
423
+ "\u0444\u043E\u0440\u043C\u0430\u043B\u044C\u043D\u043E",
424
+ "\u043C\u0430\u0442\u0435\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438",
425
+ "\u043B\u043E\u0433\u0438\u0447\u0435\u0441\u043A\u0438",
426
+ // German
427
+ "beweisen",
428
+ "beweis",
429
+ "theorem",
430
+ "ableiten",
431
+ "schritt f\xFCr schritt",
432
+ "gedankenkette",
433
+ "formal",
434
+ "mathematisch",
435
+ "logisch",
436
+ // Spanish
437
+ "demostrar",
438
+ "teorema",
439
+ "derivar",
440
+ "paso a paso",
441
+ "cadena de pensamiento",
442
+ "formalmente",
443
+ "matem\xE1tico",
444
+ "prueba",
445
+ "l\xF3gicamente",
446
+ // Portuguese
447
+ "provar",
448
+ "teorema",
449
+ "derivar",
450
+ "passo a passo",
451
+ "cadeia de pensamento",
452
+ "formalmente",
453
+ "matem\xE1tico",
454
+ "prova",
455
+ "logicamente",
456
+ // Korean
457
+ "\uC99D\uBA85",
458
+ "\uC815\uB9AC",
459
+ "\uB3C4\uCD9C",
460
+ "\uB2E8\uACC4\uBCC4",
461
+ "\uC0AC\uACE0\uC758 \uC5F0\uC1C4",
462
+ "\uD615\uC2DD\uC801",
463
+ "\uC218\uD559\uC801",
464
+ "\uB17C\uB9AC\uC801",
465
+ // Arabic
466
+ "\u0625\u062B\u0628\u0627\u062A",
467
+ "\u0646\u0638\u0631\u064A\u0629",
468
+ "\u0627\u0634\u062A\u0642\u0627\u0642",
469
+ "\u062E\u0637\u0648\u0629 \u0628\u062E\u0637\u0648\u0629",
470
+ "\u0633\u0644\u0633\u0644\u0629 \u0627\u0644\u062A\u0641\u0643\u064A\u0631",
471
+ "\u0631\u0633\u0645\u064A\u0627\u064B",
472
+ "\u0631\u064A\u0627\u0636\u064A",
473
+ "\u0628\u0631\u0647\u0627\u0646",
474
+ "\u0645\u0646\u0637\u0642\u064A\u0627\u064B"
475
+ ],
476
+ simpleKeywords: [
477
+ // English
478
+ "what is",
479
+ "define",
480
+ "translate",
481
+ "hello",
482
+ "yes or no",
483
+ "capital of",
484
+ "how old",
485
+ "who is",
486
+ "when was",
487
+ // Chinese
488
+ "\u4EC0\u4E48\u662F",
489
+ "\u5B9A\u4E49",
490
+ "\u7FFB\u8BD1",
491
+ "\u4F60\u597D",
492
+ "\u662F\u5426",
493
+ "\u9996\u90FD",
494
+ "\u591A\u5927",
495
+ "\u8C01\u662F",
496
+ "\u4F55\u65F6",
497
+ // Japanese
498
+ "\u3068\u306F",
499
+ "\u5B9A\u7FA9",
500
+ "\u7FFB\u8A33",
501
+ "\u3053\u3093\u306B\u3061\u306F",
502
+ "\u306F\u3044\u304B\u3044\u3044\u3048",
503
+ "\u9996\u90FD",
504
+ "\u8AB0",
505
+ // Russian
506
+ "\u0447\u0442\u043E \u0442\u0430\u043A\u043E\u0435",
507
+ "\u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u0435",
508
+ "\u043F\u0435\u0440\u0435\u0432\u0435\u0441\u0442\u0438",
509
+ "\u043F\u0435\u0440\u0435\u0432\u0435\u0434\u0438",
510
+ "\u043F\u0440\u0438\u0432\u0435\u0442",
511
+ "\u0434\u0430 \u0438\u043B\u0438 \u043D\u0435\u0442",
512
+ "\u0441\u0442\u043E\u043B\u0438\u0446\u0430",
513
+ "\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u043B\u0435\u0442",
514
+ "\u043A\u0442\u043E \u0442\u0430\u043A\u043E\u0439",
515
+ "\u043A\u043E\u0433\u0434\u0430",
516
+ "\u043E\u0431\u044A\u044F\u0441\u043D\u0438",
517
+ // German
518
+ "was ist",
519
+ "definiere",
520
+ "\xFCbersetze",
521
+ "hallo",
522
+ "ja oder nein",
523
+ "hauptstadt",
524
+ "wie alt",
525
+ "wer ist",
526
+ "wann",
527
+ "erkl\xE4re",
528
+ // Spanish
529
+ "qu\xE9 es",
530
+ "definir",
531
+ "traducir",
532
+ "hola",
533
+ "s\xED o no",
534
+ "capital de",
535
+ "cu\xE1ntos a\xF1os",
536
+ "qui\xE9n es",
537
+ "cu\xE1ndo",
538
+ // Portuguese
539
+ "o que \xE9",
540
+ "definir",
541
+ "traduzir",
542
+ "ol\xE1",
543
+ "sim ou n\xE3o",
544
+ "capital de",
545
+ "quantos anos",
546
+ "quem \xE9",
547
+ "quando",
548
+ // Korean
549
+ "\uBB34\uC5C7",
550
+ "\uC815\uC758",
551
+ "\uBC88\uC5ED",
552
+ "\uC548\uB155\uD558\uC138\uC694",
553
+ "\uC608 \uB610\uB294 \uC544\uB2C8\uC624",
554
+ "\uC218\uB3C4",
555
+ "\uB204\uAD6C",
556
+ "\uC5B8\uC81C",
557
+ // Arabic
558
+ "\u0645\u0627 \u0647\u0648",
559
+ "\u062A\u0639\u0631\u064A\u0641",
560
+ "\u062A\u0631\u062C\u0645",
561
+ "\u0645\u0631\u062D\u0628\u0627",
562
+ "\u0646\u0639\u0645 \u0623\u0648 \u0644\u0627",
563
+ "\u0639\u0627\u0635\u0645\u0629",
564
+ "\u0645\u0646 \u0647\u0648",
565
+ "\u0645\u062A\u0649"
566
+ ],
567
+ technicalKeywords: [
568
+ // English
569
+ "algorithm",
570
+ "optimize",
571
+ "architecture",
572
+ "distributed",
573
+ "kubernetes",
574
+ "microservice",
575
+ "database",
576
+ "infrastructure",
577
+ // Chinese
578
+ "\u7B97\u6CD5",
579
+ "\u4F18\u5316",
580
+ "\u67B6\u6784",
581
+ "\u5206\u5E03\u5F0F",
582
+ "\u5FAE\u670D\u52A1",
583
+ "\u6570\u636E\u5E93",
584
+ "\u57FA\u7840\u8BBE\u65BD",
585
+ // Japanese
586
+ "\u30A2\u30EB\u30B4\u30EA\u30BA\u30E0",
587
+ "\u6700\u9069\u5316",
588
+ "\u30A2\u30FC\u30AD\u30C6\u30AF\u30C1\u30E3",
589
+ "\u5206\u6563",
590
+ "\u30DE\u30A4\u30AF\u30ED\u30B5\u30FC\u30D3\u30B9",
591
+ "\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9",
592
+ // Russian
593
+ "\u0430\u043B\u0433\u043E\u0440\u0438\u0442\u043C",
594
+ "\u043E\u043F\u0442\u0438\u043C\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C",
595
+ "\u043E\u043F\u0442\u0438\u043C\u0438\u0437\u0430\u0446\u0438",
596
+ "\u043E\u043F\u0442\u0438\u043C\u0438\u0437\u0438\u0440\u0443\u0439",
597
+ "\u0430\u0440\u0445\u0438\u0442\u0435\u043A\u0442\u0443\u0440\u0430",
598
+ "\u0440\u0430\u0441\u043F\u0440\u0435\u0434\u0435\u043B\u0451\u043D\u043D\u044B\u0439",
599
+ "\u043C\u0438\u043A\u0440\u043E\u0441\u0435\u0440\u0432\u0438\u0441",
600
+ "\u0431\u0430\u0437\u0430 \u0434\u0430\u043D\u043D\u044B\u0445",
601
+ "\u0438\u043D\u0444\u0440\u0430\u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0430",
602
+ // German
603
+ "algorithmus",
604
+ "optimieren",
605
+ "architektur",
606
+ "verteilt",
607
+ "kubernetes",
608
+ "mikroservice",
609
+ "datenbank",
610
+ "infrastruktur",
611
+ // Spanish
612
+ "algoritmo",
613
+ "optimizar",
614
+ "arquitectura",
615
+ "distribuido",
616
+ "microservicio",
617
+ "base de datos",
618
+ "infraestructura",
619
+ // Portuguese
620
+ "algoritmo",
621
+ "otimizar",
622
+ "arquitetura",
623
+ "distribu\xEDdo",
624
+ "microsservi\xE7o",
625
+ "banco de dados",
626
+ "infraestrutura",
627
+ // Korean
628
+ "\uC54C\uACE0\uB9AC\uC998",
629
+ "\uCD5C\uC801\uD654",
630
+ "\uC544\uD0A4\uD14D\uCC98",
631
+ "\uBD84\uC0B0",
632
+ "\uB9C8\uC774\uD06C\uB85C\uC11C\uBE44\uC2A4",
633
+ "\uB370\uC774\uD130\uBCA0\uC774\uC2A4",
634
+ "\uC778\uD504\uB77C",
635
+ // Arabic
636
+ "\u062E\u0648\u0627\u0631\u0632\u0645\u064A\u0629",
637
+ "\u062A\u062D\u0633\u064A\u0646",
638
+ "\u0628\u0646\u064A\u0629",
639
+ "\u0645\u0648\u0632\u0639",
640
+ "\u062E\u062F\u0645\u0629 \u0645\u0635\u063A\u0631\u0629",
641
+ "\u0642\u0627\u0639\u062F\u0629 \u0628\u064A\u0627\u0646\u0627\u062A",
642
+ "\u0628\u0646\u064A\u0629 \u062A\u062D\u062A\u064A\u0629"
643
+ ],
644
+ creativeKeywords: [
645
+ // English
646
+ "story",
647
+ "poem",
648
+ "compose",
649
+ "brainstorm",
650
+ "creative",
651
+ "imagine",
652
+ "write a",
653
+ // Chinese
654
+ "\u6545\u4E8B",
655
+ "\u8BD7",
656
+ "\u521B\u4F5C",
657
+ "\u5934\u8111\u98CE\u66B4",
658
+ "\u521B\u610F",
659
+ "\u60F3\u8C61",
660
+ "\u5199\u4E00\u4E2A",
661
+ // Japanese
662
+ "\u7269\u8A9E",
663
+ "\u8A69",
664
+ "\u4F5C\u66F2",
665
+ "\u30D6\u30EC\u30A4\u30F3\u30B9\u30C8\u30FC\u30E0",
666
+ "\u5275\u9020\u7684",
667
+ "\u60F3\u50CF",
668
+ // Russian
669
+ "\u0438\u0441\u0442\u043E\u0440\u0438\u044F",
670
+ "\u0440\u0430\u0441\u0441\u043A\u0430\u0437",
671
+ "\u0441\u0442\u0438\u0445\u043E\u0442\u0432\u043E\u0440\u0435\u043D\u0438\u0435",
672
+ "\u0441\u043E\u0447\u0438\u043D\u0438\u0442\u044C",
673
+ "\u0441\u043E\u0447\u0438\u043D\u0438",
674
+ "\u043C\u043E\u0437\u0433\u043E\u0432\u043E\u0439 \u0448\u0442\u0443\u0440\u043C",
675
+ "\u0442\u0432\u043E\u0440\u0447\u0435\u0441\u043A\u0438\u0439",
676
+ "\u043F\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044C",
677
+ "\u043F\u0440\u0438\u0434\u0443\u043C\u0430\u0439",
678
+ "\u043D\u0430\u043F\u0438\u0448\u0438",
679
+ // German
680
+ "geschichte",
681
+ "gedicht",
682
+ "komponieren",
683
+ "brainstorming",
684
+ "kreativ",
685
+ "vorstellen",
686
+ "schreibe",
687
+ "erz\xE4hlung",
688
+ // Spanish
689
+ "historia",
690
+ "poema",
691
+ "componer",
692
+ "lluvia de ideas",
693
+ "creativo",
694
+ "imaginar",
695
+ "escribe",
696
+ // Portuguese
697
+ "hist\xF3ria",
698
+ "poema",
699
+ "compor",
700
+ "criativo",
701
+ "imaginar",
702
+ "escreva",
703
+ // Korean
704
+ "\uC774\uC57C\uAE30",
705
+ "\uC2DC",
706
+ "\uC791\uACE1",
707
+ "\uBE0C\uB808\uC778\uC2A4\uD1A0\uBC0D",
708
+ "\uCC3D\uC758\uC801",
709
+ "\uC0C1\uC0C1",
710
+ "\uC791\uC131",
711
+ // Arabic
712
+ "\u0642\u0635\u0629",
713
+ "\u0642\u0635\u064A\u062F\u0629",
714
+ "\u062A\u0623\u0644\u064A\u0641",
715
+ "\u0639\u0635\u0641 \u0630\u0647\u0646\u064A",
716
+ "\u0625\u0628\u062F\u0627\u0639\u064A",
717
+ "\u062A\u062E\u064A\u0644",
718
+ "\u0627\u0643\u062A\u0628"
719
+ ],
720
+ imperativeVerbs: [
721
+ // English
722
+ "build",
723
+ "create",
724
+ "implement",
725
+ "design",
726
+ "develop",
727
+ "construct",
728
+ "generate",
729
+ "deploy",
730
+ "configure",
731
+ "set up",
732
+ // Chinese
733
+ "\u6784\u5EFA",
734
+ "\u521B\u5EFA",
735
+ "\u5B9E\u73B0",
736
+ "\u8BBE\u8BA1",
737
+ "\u5F00\u53D1",
738
+ "\u751F\u6210",
739
+ "\u90E8\u7F72",
740
+ "\u914D\u7F6E",
741
+ "\u8BBE\u7F6E",
742
+ // Japanese
743
+ "\u69CB\u7BC9",
744
+ "\u4F5C\u6210",
745
+ "\u5B9F\u88C5",
746
+ "\u8A2D\u8A08",
747
+ "\u958B\u767A",
748
+ "\u751F\u6210",
749
+ "\u30C7\u30D7\u30ED\u30A4",
750
+ "\u8A2D\u5B9A",
751
+ // Russian
752
+ "\u043F\u043E\u0441\u0442\u0440\u043E\u0438\u0442\u044C",
753
+ "\u043F\u043E\u0441\u0442\u0440\u043E\u0439",
754
+ "\u0441\u043E\u0437\u0434\u0430\u0442\u044C",
755
+ "\u0441\u043E\u0437\u0434\u0430\u0439",
756
+ "\u0440\u0435\u0430\u043B\u0438\u0437\u043E\u0432\u0430\u0442\u044C",
757
+ "\u0440\u0435\u0430\u043B\u0438\u0437\u0443\u0439",
758
+ "\u0441\u043F\u0440\u043E\u0435\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C",
759
+ "\u0440\u0430\u0437\u0440\u0430\u0431\u043E\u0442\u0430\u0442\u044C",
760
+ "\u0440\u0430\u0437\u0440\u0430\u0431\u043E\u0442\u0430\u0439",
761
+ "\u0441\u043A\u043E\u043D\u0441\u0442\u0440\u0443\u0438\u0440\u043E\u0432\u0430\u0442\u044C",
762
+ "\u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C",
763
+ "\u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0439",
764
+ "\u0440\u0430\u0437\u0432\u0435\u0440\u043D\u0443\u0442\u044C",
765
+ "\u0440\u0430\u0437\u0432\u0435\u0440\u043D\u0438",
766
+ "\u043D\u0430\u0441\u0442\u0440\u043E\u0438\u0442\u044C",
767
+ "\u043D\u0430\u0441\u0442\u0440\u043E\u0439",
768
+ // German
769
+ "erstellen",
770
+ "bauen",
771
+ "implementieren",
772
+ "entwerfen",
773
+ "entwickeln",
774
+ "konstruieren",
775
+ "generieren",
776
+ "bereitstellen",
777
+ "konfigurieren",
778
+ "einrichten",
779
+ // Spanish
780
+ "construir",
781
+ "crear",
782
+ "implementar",
783
+ "dise\xF1ar",
784
+ "desarrollar",
785
+ "generar",
786
+ "desplegar",
787
+ "configurar",
788
+ // Portuguese
789
+ "construir",
790
+ "criar",
791
+ "implementar",
792
+ "projetar",
793
+ "desenvolver",
794
+ "gerar",
795
+ "implantar",
796
+ "configurar",
797
+ // Korean
798
+ "\uAD6C\uCD95",
799
+ "\uC0DD\uC131",
800
+ "\uAD6C\uD604",
801
+ "\uC124\uACC4",
802
+ "\uAC1C\uBC1C",
803
+ "\uBC30\uD3EC",
804
+ "\uC124\uC815",
805
+ // Arabic
806
+ "\u0628\u0646\u0627\u0621",
807
+ "\u0625\u0646\u0634\u0627\u0621",
808
+ "\u062A\u0646\u0641\u064A\u0630",
809
+ "\u062A\u0635\u0645\u064A\u0645",
810
+ "\u062A\u0637\u0648\u064A\u0631",
811
+ "\u062A\u0648\u0644\u064A\u062F",
812
+ "\u0646\u0634\u0631",
813
+ "\u0625\u0639\u062F\u0627\u062F"
814
+ ],
815
+ constraintIndicators: [
816
+ // English
817
+ "under",
818
+ "at most",
819
+ "at least",
820
+ "within",
821
+ "no more than",
822
+ "o(",
823
+ "maximum",
824
+ "minimum",
825
+ "limit",
826
+ "budget",
827
+ // Chinese
828
+ "\u4E0D\u8D85\u8FC7",
829
+ "\u81F3\u5C11",
830
+ "\u6700\u591A",
831
+ "\u5728\u5185",
832
+ "\u6700\u5927",
833
+ "\u6700\u5C0F",
834
+ "\u9650\u5236",
835
+ "\u9884\u7B97",
836
+ // Japanese
837
+ "\u4EE5\u4E0B",
838
+ "\u6700\u5927",
839
+ "\u6700\u5C0F",
840
+ "\u5236\u9650",
841
+ "\u4E88\u7B97",
842
+ // Russian
843
+ "\u043D\u0435 \u0431\u043E\u043B\u0435\u0435",
844
+ "\u043D\u0435 \u043C\u0435\u043D\u0435\u0435",
845
+ "\u043A\u0430\u043A \u043C\u0438\u043D\u0438\u043C\u0443\u043C",
846
+ "\u0432 \u043F\u0440\u0435\u0434\u0435\u043B\u0430\u0445",
847
+ "\u043C\u0430\u043A\u0441\u0438\u043C\u0443\u043C",
848
+ "\u043C\u0438\u043D\u0438\u043C\u0443\u043C",
849
+ "\u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u0438\u0435",
850
+ "\u0431\u044E\u0434\u0436\u0435\u0442",
851
+ // German
852
+ "h\xF6chstens",
853
+ "mindestens",
854
+ "innerhalb",
855
+ "nicht mehr als",
856
+ "maximal",
857
+ "minimal",
858
+ "grenze",
859
+ "budget",
860
+ // Spanish
861
+ "como m\xE1ximo",
862
+ "al menos",
863
+ "dentro de",
864
+ "no m\xE1s de",
865
+ "m\xE1ximo",
866
+ "m\xEDnimo",
867
+ "l\xEDmite",
868
+ "presupuesto",
869
+ // Portuguese
870
+ "no m\xE1ximo",
871
+ "pelo menos",
872
+ "dentro de",
873
+ "n\xE3o mais que",
874
+ "m\xE1ximo",
875
+ "m\xEDnimo",
876
+ "limite",
877
+ "or\xE7amento",
878
+ // Korean
879
+ "\uC774\uD558",
880
+ "\uC774\uC0C1",
881
+ "\uCD5C\uB300",
882
+ "\uCD5C\uC18C",
883
+ "\uC81C\uD55C",
884
+ "\uC608\uC0B0",
885
+ // Arabic
886
+ "\u0639\u0644\u0649 \u0627\u0644\u0623\u0643\u062B\u0631",
887
+ "\u0639\u0644\u0649 \u0627\u0644\u0623\u0642\u0644",
888
+ "\u0636\u0645\u0646",
889
+ "\u0644\u0627 \u064A\u0632\u064A\u062F \u0639\u0646",
890
+ "\u0623\u0642\u0635\u0649",
891
+ "\u0623\u062F\u0646\u0649",
892
+ "\u062D\u062F",
893
+ "\u0645\u064A\u0632\u0627\u0646\u064A\u0629"
894
+ ],
895
+ outputFormatKeywords: [
896
+ // English
897
+ "json",
898
+ "yaml",
899
+ "xml",
900
+ "table",
901
+ "csv",
902
+ "markdown",
903
+ "schema",
904
+ "format as",
905
+ "structured",
906
+ // Chinese
907
+ "\u8868\u683C",
908
+ "\u683C\u5F0F\u5316\u4E3A",
909
+ "\u7ED3\u6784\u5316",
910
+ // Japanese
911
+ "\u30C6\u30FC\u30D6\u30EB",
912
+ "\u30D5\u30A9\u30FC\u30DE\u30C3\u30C8",
913
+ "\u69CB\u9020\u5316",
914
+ // Russian
915
+ "\u0442\u0430\u0431\u043B\u0438\u0446\u0430",
916
+ "\u0444\u043E\u0440\u043C\u0430\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043A\u0430\u043A",
917
+ "\u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0439",
918
+ // German
919
+ "tabelle",
920
+ "formatieren als",
921
+ "strukturiert",
922
+ // Spanish
923
+ "tabla",
924
+ "formatear como",
925
+ "estructurado",
926
+ // Portuguese
927
+ "tabela",
928
+ "formatar como",
929
+ "estruturado",
930
+ // Korean
931
+ "\uD14C\uC774\uBE14",
932
+ "\uD615\uC2DD",
933
+ "\uAD6C\uC870\uD654",
934
+ // Arabic
935
+ "\u062C\u062F\u0648\u0644",
936
+ "\u062A\u0646\u0633\u064A\u0642",
937
+ "\u0645\u0646\u0638\u0645"
938
+ ],
939
+ referenceKeywords: [
940
+ // English
941
+ "above",
942
+ "below",
943
+ "previous",
944
+ "following",
945
+ "the docs",
946
+ "the api",
947
+ "the code",
948
+ "earlier",
949
+ "attached",
950
+ // Chinese
951
+ "\u4E0A\u9762",
952
+ "\u4E0B\u9762",
953
+ "\u4E4B\u524D",
954
+ "\u63A5\u4E0B\u6765",
955
+ "\u6587\u6863",
956
+ "\u4EE3\u7801",
957
+ "\u9644\u4EF6",
958
+ // Japanese
959
+ "\u4E0A\u8A18",
960
+ "\u4E0B\u8A18",
961
+ "\u524D\u306E",
962
+ "\u6B21\u306E",
963
+ "\u30C9\u30AD\u30E5\u30E1\u30F3\u30C8",
964
+ "\u30B3\u30FC\u30C9",
965
+ // Russian
966
+ "\u0432\u044B\u0448\u0435",
967
+ "\u043D\u0438\u0436\u0435",
968
+ "\u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0438\u0439",
969
+ "\u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439",
970
+ "\u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0430\u0446\u0438\u044F",
971
+ "\u043A\u043E\u0434",
972
+ "\u0440\u0430\u043D\u0435\u0435",
973
+ "\u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0435",
974
+ // German
975
+ "oben",
976
+ "unten",
977
+ "vorherige",
978
+ "folgende",
979
+ "dokumentation",
980
+ "der code",
981
+ "fr\xFCher",
982
+ "anhang",
983
+ // Spanish
984
+ "arriba",
985
+ "abajo",
986
+ "anterior",
987
+ "siguiente",
988
+ "documentaci\xF3n",
989
+ "el c\xF3digo",
990
+ "adjunto",
991
+ // Portuguese
992
+ "acima",
993
+ "abaixo",
994
+ "anterior",
995
+ "seguinte",
996
+ "documenta\xE7\xE3o",
997
+ "o c\xF3digo",
998
+ "anexo",
999
+ // Korean
1000
+ "\uC704",
1001
+ "\uC544\uB798",
1002
+ "\uC774\uC804",
1003
+ "\uB2E4\uC74C",
1004
+ "\uBB38\uC11C",
1005
+ "\uCF54\uB4DC",
1006
+ "\uCCA8\uBD80",
1007
+ // Arabic
1008
+ "\u0623\u0639\u0644\u0627\u0647",
1009
+ "\u0623\u062F\u0646\u0627\u0647",
1010
+ "\u0627\u0644\u0633\u0627\u0628\u0642",
1011
+ "\u0627\u0644\u062A\u0627\u0644\u064A",
1012
+ "\u0627\u0644\u0648\u062B\u0627\u0626\u0642",
1013
+ "\u0627\u0644\u0643\u0648\u062F",
1014
+ "\u0645\u0631\u0641\u0642"
1015
+ ],
1016
+ negationKeywords: [
1017
+ // English
1018
+ "don't",
1019
+ "do not",
1020
+ "avoid",
1021
+ "never",
1022
+ "without",
1023
+ "except",
1024
+ "exclude",
1025
+ "no longer",
1026
+ // Chinese
1027
+ "\u4E0D\u8981",
1028
+ "\u907F\u514D",
1029
+ "\u4ECE\u4E0D",
1030
+ "\u6CA1\u6709",
1031
+ "\u9664\u4E86",
1032
+ "\u6392\u9664",
1033
+ // Japanese
1034
+ "\u3057\u306A\u3044\u3067",
1035
+ "\u907F\u3051\u308B",
1036
+ "\u6C7A\u3057\u3066",
1037
+ "\u306A\u3057\u3067",
1038
+ "\u9664\u304F",
1039
+ // Russian
1040
+ "\u043D\u0435 \u0434\u0435\u043B\u0430\u0439",
1041
+ "\u043D\u0435 \u043D\u0430\u0434\u043E",
1042
+ "\u043D\u0435\u043B\u044C\u0437\u044F",
1043
+ "\u0438\u0437\u0431\u0435\u0433\u0430\u0442\u044C",
1044
+ "\u043D\u0438\u043A\u043E\u0433\u0434\u0430",
1045
+ "\u0431\u0435\u0437",
1046
+ "\u043A\u0440\u043E\u043C\u0435",
1047
+ "\u0438\u0441\u043A\u043B\u044E\u0447\u0438\u0442\u044C",
1048
+ "\u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435",
1049
+ // German
1050
+ "nicht",
1051
+ "vermeide",
1052
+ "niemals",
1053
+ "ohne",
1054
+ "au\xDFer",
1055
+ "ausschlie\xDFen",
1056
+ "nicht mehr",
1057
+ // Spanish
1058
+ "no hagas",
1059
+ "evitar",
1060
+ "nunca",
1061
+ "sin",
1062
+ "excepto",
1063
+ "excluir",
1064
+ // Portuguese
1065
+ "n\xE3o fa\xE7a",
1066
+ "evitar",
1067
+ "nunca",
1068
+ "sem",
1069
+ "exceto",
1070
+ "excluir",
1071
+ // Korean
1072
+ "\uD558\uC9C0 \uB9C8",
1073
+ "\uD53C\uD558\uB2E4",
1074
+ "\uC808\uB300",
1075
+ "\uC5C6\uC774",
1076
+ "\uC81C\uC678",
1077
+ // Arabic
1078
+ "\u0644\u0627 \u062A\u0641\u0639\u0644",
1079
+ "\u062A\u062C\u0646\u0628",
1080
+ "\u0623\u0628\u062F\u0627\u064B",
1081
+ "\u0628\u062F\u0648\u0646",
1082
+ "\u0628\u0627\u0633\u062A\u062B\u0646\u0627\u0621",
1083
+ "\u0627\u0633\u062A\u0628\u0639\u0627\u062F"
1084
+ ],
1085
+ domainSpecificKeywords: [
1086
+ // English
1087
+ "quantum",
1088
+ "fpga",
1089
+ "vlsi",
1090
+ "risc-v",
1091
+ "asic",
1092
+ "photonics",
1093
+ "genomics",
1094
+ "proteomics",
1095
+ "topological",
1096
+ "homomorphic",
1097
+ "zero-knowledge",
1098
+ "lattice-based",
1099
+ // Chinese
1100
+ "\u91CF\u5B50",
1101
+ "\u5149\u5B50\u5B66",
1102
+ "\u57FA\u56E0\u7EC4\u5B66",
1103
+ "\u86CB\u767D\u8D28\u7EC4\u5B66",
1104
+ "\u62D3\u6251",
1105
+ "\u540C\u6001",
1106
+ "\u96F6\u77E5\u8BC6",
1107
+ "\u683C\u5BC6\u7801",
1108
+ // Japanese
1109
+ "\u91CF\u5B50",
1110
+ "\u30D5\u30A9\u30C8\u30CB\u30AF\u30B9",
1111
+ "\u30B2\u30CE\u30DF\u30AF\u30B9",
1112
+ "\u30C8\u30DD\u30ED\u30B8\u30AB\u30EB",
1113
+ // Russian
1114
+ "\u043A\u0432\u0430\u043D\u0442\u043E\u0432\u044B\u0439",
1115
+ "\u0444\u043E\u0442\u043E\u043D\u0438\u043A\u0430",
1116
+ "\u0433\u0435\u043D\u043E\u043C\u0438\u043A\u0430",
1117
+ "\u043F\u0440\u043E\u0442\u0435\u043E\u043C\u0438\u043A\u0430",
1118
+ "\u0442\u043E\u043F\u043E\u043B\u043E\u0433\u0438\u0447\u0435\u0441\u043A\u0438\u0439",
1119
+ "\u0433\u043E\u043C\u043E\u043C\u043E\u0440\u0444\u043D\u044B\u0439",
1120
+ "\u0441 \u043D\u0443\u043B\u0435\u0432\u044B\u043C \u0440\u0430\u0437\u0433\u043B\u0430\u0448\u0435\u043D\u0438\u0435\u043C",
1121
+ "\u043D\u0430 \u043E\u0441\u043D\u043E\u0432\u0435 \u0440\u0435\u0448\u0451\u0442\u043E\u043A",
1122
+ // German
1123
+ "quanten",
1124
+ "photonik",
1125
+ "genomik",
1126
+ "proteomik",
1127
+ "topologisch",
1128
+ "homomorph",
1129
+ "zero-knowledge",
1130
+ "gitterbasiert",
1131
+ // Spanish
1132
+ "cu\xE1ntico",
1133
+ "fot\xF3nica",
1134
+ "gen\xF3mica",
1135
+ "prote\xF3mica",
1136
+ "topol\xF3gico",
1137
+ "homom\xF3rfico",
1138
+ // Portuguese
1139
+ "qu\xE2ntico",
1140
+ "fot\xF4nica",
1141
+ "gen\xF4mica",
1142
+ "prote\xF4mica",
1143
+ "topol\xF3gico",
1144
+ "homom\xF3rfico",
1145
+ // Korean
1146
+ "\uC591\uC790",
1147
+ "\uD3EC\uD1A0\uB2C9\uC2A4",
1148
+ "\uC720\uC804\uCCB4\uD559",
1149
+ "\uC704\uC0C1",
1150
+ "\uB3D9\uD615",
1151
+ // Arabic
1152
+ "\u0643\u0645\u064A",
1153
+ "\u0636\u0648\u0626\u064A\u0627\u062A",
1154
+ "\u062C\u064A\u0646\u0648\u0645\u064A\u0627\u062A",
1155
+ "\u0637\u0648\u0628\u0648\u0644\u0648\u062C\u064A",
1156
+ "\u062A\u0645\u0627\u062B\u0644\u064A"
1157
+ ],
1158
+ // Agentic task keywords - file ops, execution, multi-step, iterative work
1159
+ // Pruned: removed overly common words like "then", "first", "run", "test", "build"
1160
+ agenticTaskKeywords: [
1161
+ // English - File operations (clearly agentic)
1162
+ "read file",
1163
+ "read the file",
1164
+ "look at",
1165
+ "check the",
1166
+ "open the",
1167
+ "edit",
1168
+ "modify",
1169
+ "update the",
1170
+ "change the",
1171
+ "write to",
1172
+ "create file",
1173
+ // English - Execution (specific commands only)
1174
+ "execute",
1175
+ "deploy",
1176
+ "install",
1177
+ "npm",
1178
+ "pip",
1179
+ "compile",
1180
+ // English - Multi-step patterns (specific only)
1181
+ "after that",
1182
+ "and also",
1183
+ "once done",
1184
+ "step 1",
1185
+ "step 2",
1186
+ // English - Iterative work
1187
+ "fix",
1188
+ "debug",
1189
+ "until it works",
1190
+ "keep trying",
1191
+ "iterate",
1192
+ "make sure",
1193
+ "verify",
1194
+ "confirm",
1195
+ // Chinese (keep specific ones)
1196
+ "\u8BFB\u53D6\u6587\u4EF6",
1197
+ "\u67E5\u770B",
1198
+ "\u6253\u5F00",
1199
+ "\u7F16\u8F91",
1200
+ "\u4FEE\u6539",
1201
+ "\u66F4\u65B0",
1202
+ "\u521B\u5EFA",
1203
+ "\u6267\u884C",
1204
+ "\u90E8\u7F72",
1205
+ "\u5B89\u88C5",
1206
+ "\u7B2C\u4E00\u6B65",
1207
+ "\u7B2C\u4E8C\u6B65",
1208
+ "\u4FEE\u590D",
1209
+ "\u8C03\u8BD5",
1210
+ "\u76F4\u5230",
1211
+ "\u786E\u8BA4",
1212
+ "\u9A8C\u8BC1",
1213
+ // Spanish
1214
+ "leer archivo",
1215
+ "editar",
1216
+ "modificar",
1217
+ "actualizar",
1218
+ "ejecutar",
1219
+ "desplegar",
1220
+ "instalar",
1221
+ "paso 1",
1222
+ "paso 2",
1223
+ "arreglar",
1224
+ "depurar",
1225
+ "verificar",
1226
+ // Portuguese
1227
+ "ler arquivo",
1228
+ "editar",
1229
+ "modificar",
1230
+ "atualizar",
1231
+ "executar",
1232
+ "implantar",
1233
+ "instalar",
1234
+ "passo 1",
1235
+ "passo 2",
1236
+ "corrigir",
1237
+ "depurar",
1238
+ "verificar",
1239
+ // Korean
1240
+ "\uD30C\uC77C \uC77D\uAE30",
1241
+ "\uD3B8\uC9D1",
1242
+ "\uC218\uC815",
1243
+ "\uC5C5\uB370\uC774\uD2B8",
1244
+ "\uC2E4\uD589",
1245
+ "\uBC30\uD3EC",
1246
+ "\uC124\uCE58",
1247
+ "\uB2E8\uACC4 1",
1248
+ "\uB2E8\uACC4 2",
1249
+ "\uB514\uBC84\uADF8",
1250
+ "\uD655\uC778",
1251
+ // Arabic
1252
+ "\u0642\u0631\u0627\u0621\u0629 \u0645\u0644\u0641",
1253
+ "\u062A\u062D\u0631\u064A\u0631",
1254
+ "\u062A\u0639\u062F\u064A\u0644",
1255
+ "\u062A\u062D\u062F\u064A\u062B",
1256
+ "\u062A\u0646\u0641\u064A\u0630",
1257
+ "\u0646\u0634\u0631",
1258
+ "\u062A\u062B\u0628\u064A\u062A",
1259
+ "\u0627\u0644\u062E\u0637\u0648\u0629 1",
1260
+ "\u0627\u0644\u062E\u0637\u0648\u0629 2",
1261
+ "\u0625\u0635\u0644\u0627\u062D",
1262
+ "\u062A\u0635\u062D\u064A\u062D",
1263
+ "\u062A\u062D\u0642\u0642"
1264
+ ],
1265
+ // Dimension weights (sum to 1.0)
1266
+ dimensionWeights: {
1267
+ tokenCount: 0.08,
1268
+ codePresence: 0.15,
1269
+ reasoningMarkers: 0.18,
1270
+ technicalTerms: 0.1,
1271
+ creativeMarkers: 0.05,
1272
+ simpleIndicators: 0.02,
1273
+ // Reduced from 0.12 to make room for agenticTask
1274
+ multiStepPatterns: 0.12,
1275
+ questionComplexity: 0.05,
1276
+ imperativeVerbs: 0.03,
1277
+ constraintCount: 0.04,
1278
+ outputFormat: 0.03,
1279
+ referenceComplexity: 0.02,
1280
+ negationComplexity: 0.01,
1281
+ domainSpecificity: 0.02,
1282
+ agenticTask: 0.04
1283
+ // Reduced - agentic signals influence tier selection, not dominate it
1284
+ },
1285
+ // Tier boundaries on weighted score axis
1286
+ tierBoundaries: {
1287
+ simpleMedium: 0,
1288
+ mediumComplex: 0.3,
1289
+ // Raised from 0.18 - prevent simple tasks from reaching expensive COMPLEX tier
1290
+ complexReasoning: 0.5
1291
+ // Raised from 0.4 - reserve for true reasoning tasks
1292
+ },
1293
+ // Sigmoid steepness for confidence calibration
1294
+ confidenceSteepness: 12,
1295
+ // Below this confidence → ambiguous (null tier)
1296
+ confidenceThreshold: 0.7
1297
+ },
1298
+ overrides: {
1299
+ structuredOutputMinTier: "MEDIUM",
1300
+ ambiguousDefaultTier: "MEDIUM"
1301
+ }
1302
+ };
1303
+
1304
+ // src/router/trace.ts
1305
+ function defaultTraceWriter() {
1306
+ return console.debug.bind(console);
1307
+ }
1308
+ function resolveTraceWriter(logger) {
1309
+ return logger?.debug ?? logger?.info ?? defaultTraceWriter();
1310
+ }
1311
+ function normalizeTraceMode(mode) {
1312
+ if (mode === "summary" || mode === "debug") {
1313
+ return mode;
1314
+ }
1315
+ return "off";
1316
+ }
1317
+ function getPromptPreview(prompt) {
1318
+ const chars = Array.from(prompt);
1319
+ if (chars.length <= 24) {
1320
+ return prompt;
1321
+ }
1322
+ return `${chars.slice(0, 10).join("")}...${chars.slice(-10).join("")}`;
1323
+ }
1324
+ function buildTraceSummary(input) {
1325
+ const requestCode = getRequestCode(input);
1326
+ const profileCode = getProfileOrTierCode(input.profile, input.tier);
1327
+ return [
1328
+ requestCode,
1329
+ profileCode,
1330
+ input.routedModel,
1331
+ input.reason
1332
+ ].join(":");
1333
+ }
1334
+ function emitRouteTrace(mode, detail, writer = defaultTraceWriter()) {
1335
+ if (mode === "off") {
1336
+ return;
1337
+ }
1338
+ if (mode === "summary") {
1339
+ try {
1340
+ writer(
1341
+ `[llm-router] ${detail.trace} model=${detail.actualModel} fallback=${detail.fallback}`
1342
+ );
1343
+ } catch {
1344
+ }
1345
+ return;
1346
+ }
1347
+ try {
1348
+ writer(JSON.stringify(detail));
1349
+ } catch {
1350
+ }
1351
+ }
1352
+ function getRequestCode(input) {
1353
+ if (input.explicit || !input.routed) {
1354
+ return "explicit";
1355
+ }
1356
+ if (input.requestedModel !== "auto") {
1357
+ return input.requestedModel;
1358
+ }
1359
+ return "auto";
1360
+ }
1361
+ function getProfileOrTierCode(profile, tier) {
1362
+ if (profile === "agentic") {
1363
+ return "agentic";
1364
+ }
1365
+ return tier.toLowerCase();
1366
+ }
1367
+
1368
+ // src/router/index.ts
1369
+ function route(prompt, systemPrompt, maxOutputTokens, options) {
1370
+ return getStrategy("rules").route(
1371
+ prompt,
1372
+ systemPrompt,
1373
+ maxOutputTokens,
1374
+ options
1375
+ );
1376
+ }
1377
+
1378
+ // src/config.ts
1379
+ var DEFAULT_BASE_URL = "https://api.deepseek.com";
1380
+ var DEFAULT_PORT = 8402;
1381
+ function normalizeBaseUrl(value) {
1382
+ return value.replace(/\/+$/, "");
1383
+ }
1384
+ function resolveConfig(input = {}) {
1385
+ return {
1386
+ baseUrl: normalizeBaseUrl(input.baseUrl ?? DEFAULT_BASE_URL),
1387
+ apiKey: input.apiKey,
1388
+ headers: input.headers ?? {},
1389
+ port: input.port ?? DEFAULT_PORT,
1390
+ sessionPinning: input.sessionPinning ?? true,
1391
+ traceMode: normalizeTraceMode(input.traceMode),
1392
+ traceLogger: input.traceLogger
1393
+ };
1394
+ }
1395
+
1396
+ // src/model-registry.ts
1397
+ function createModelRegistry(models) {
1398
+ const map = /* @__PURE__ */ new Map();
1399
+ for (const model of models) {
1400
+ if (map.has(model.id)) {
1401
+ throw new Error(`Duplicate model ID: ${model.id}`);
1402
+ }
1403
+ map.set(model.id, { ...model });
1404
+ }
1405
+ return {
1406
+ get(id) {
1407
+ const model = map.get(id);
1408
+ return model ? { ...model } : void 0;
1409
+ },
1410
+ has(id) {
1411
+ return map.has(id);
1412
+ },
1413
+ all() {
1414
+ return Array.from(map.values(), (model) => ({ ...model }));
1415
+ }
1416
+ };
1417
+ }
1418
+
1419
+ // src/public-model-resolver.ts
1420
+ function resolvePublicModel(publicModelId, publicModels, registry2) {
1421
+ return resolvePublicModelCandidate(publicModelId, publicModels, registry2).id;
1422
+ }
1423
+ function resolvePublicModelCandidate(publicModelId, publicModels, registry2) {
1424
+ const pub = publicModels[publicModelId];
1425
+ if (!pub) {
1426
+ throw new Error(`Unknown public model: ${publicModelId}`);
1427
+ }
1428
+ if (pub.kind === "router") {
1429
+ throw new Error("Cannot resolve router model directly");
1430
+ }
1431
+ const candidateId = selectCandidateId(pub.candidates, registry2, pub.selection ?? "cheapest");
1432
+ const candidate = registry2.get(candidateId);
1433
+ if (!candidate) {
1434
+ throw new Error(`Candidate model not found in registry: ${candidateId}`);
1435
+ }
1436
+ return candidate;
1437
+ }
1438
+ function selectCandidateId(candidates, registry2, selection) {
1439
+ if (candidates.length === 0) {
1440
+ throw new Error("No candidates available for public model");
1441
+ }
1442
+ if (selection === "first") {
1443
+ const first = candidates[0];
1444
+ if (!registry2.has(first)) {
1445
+ throw new Error(`Candidate model not found in registry: ${first}`);
1446
+ }
1447
+ return first;
1448
+ }
1449
+ let selectedId;
1450
+ let selectedCost = Number.POSITIVE_INFINITY;
1451
+ for (const candidateId of candidates) {
1452
+ const model = registry2.get(candidateId);
1453
+ if (!model) {
1454
+ throw new Error(`Candidate model not found in registry: ${candidateId}`);
1455
+ }
1456
+ const cost = model.inputPrice + model.outputPrice;
1457
+ if (cost < selectedCost) {
1458
+ selectedId = candidateId;
1459
+ selectedCost = cost;
1460
+ }
1461
+ }
1462
+ if (!selectedId) {
1463
+ throw new Error("No candidates available for public model");
1464
+ }
1465
+ return selectedId;
1466
+ }
1467
+
1468
+ // src/session.ts
1469
+ import { createHash } from "crypto";
1470
+ var DEFAULT_SESSION_CONFIG = {
1471
+ enabled: true,
1472
+ ttlMs: 30 * 60 * 1e3,
1473
+ cleanupIntervalMs: 5 * 60 * 1e3
1474
+ };
1475
+ var SessionStore = class {
1476
+ config;
1477
+ sessions = /* @__PURE__ */ new Map();
1478
+ cleanupTimer;
1479
+ constructor(config = {}) {
1480
+ this.config = { ...DEFAULT_SESSION_CONFIG, ...config };
1481
+ if (this.config.enabled && this.config.cleanupIntervalMs > 0) {
1482
+ this.cleanupTimer = setInterval(() => {
1483
+ this.cleanupExpired();
1484
+ }, this.config.cleanupIntervalMs);
1485
+ this.cleanupTimer.unref?.();
1486
+ }
1487
+ }
1488
+ /**
1489
+ * 读取一个未过期 session;如果已经过期,会顺手删除并返回 undefined。
1490
+ */
1491
+ getSession(sessionId) {
1492
+ if (!this.config.enabled || !sessionId) return void 0;
1493
+ const entry = this.sessions.get(sessionId);
1494
+ if (!entry) return void 0;
1495
+ if (this.isExpired(entry)) {
1496
+ this.sessions.delete(sessionId);
1497
+ return void 0;
1498
+ }
1499
+ return entry;
1500
+ }
1501
+ /**
1502
+ * 创建或更新某个 session 的 pinning 结果,同时保留历史用量累计值。
1503
+ */
1504
+ setSession(sessionId, input) {
1505
+ if (!this.config.enabled || !sessionId) return void 0;
1506
+ const now = Date.now();
1507
+ const existing = this.getSession(sessionId);
1508
+ const entry = {
1509
+ sessionId,
1510
+ physicalModelId: input.physicalModelId,
1511
+ routedPublicModel: input.routedPublicModel,
1512
+ pinnedTier: input.pinnedTier,
1513
+ createdAt: existing?.createdAt ?? now,
1514
+ updatedAt: now,
1515
+ expiresAt: now + this.config.ttlMs,
1516
+ inputTokens: existing?.inputTokens ?? 0,
1517
+ outputTokens: existing?.outputTokens ?? 0,
1518
+ costEstimate: existing?.costEstimate ?? 0
1519
+ };
1520
+ this.sessions.set(sessionId, entry);
1521
+ return entry;
1522
+ }
1523
+ /**
1524
+ * 只刷新 TTL,不改动当前 alias / physical model 选择结果。
1525
+ */
1526
+ touchSession(sessionId) {
1527
+ const entry = this.getSession(sessionId);
1528
+ if (!entry) return false;
1529
+ const now = Date.now();
1530
+ entry.updatedAt = now;
1531
+ entry.expiresAt = now + this.config.ttlMs;
1532
+ return true;
1533
+ }
1534
+ clearSession(sessionId) {
1535
+ if (!sessionId) return false;
1536
+ return this.sessions.delete(sessionId);
1537
+ }
1538
+ clearAll() {
1539
+ this.sessions.clear();
1540
+ }
1541
+ /**
1542
+ * 返回清理过期项后的聚合统计。
1543
+ */
1544
+ getStats() {
1545
+ this.cleanupExpired();
1546
+ let totalInputTokens = 0;
1547
+ let totalOutputTokens = 0;
1548
+ let totalCostEstimate = 0;
1549
+ for (const entry of this.sessions.values()) {
1550
+ totalInputTokens += entry.inputTokens;
1551
+ totalOutputTokens += entry.outputTokens;
1552
+ totalCostEstimate += entry.costEstimate;
1553
+ }
1554
+ return {
1555
+ enabled: this.config.enabled,
1556
+ size: this.sessions.size,
1557
+ totalInputTokens,
1558
+ totalOutputTokens,
1559
+ totalCostEstimate
1560
+ };
1561
+ }
1562
+ /**
1563
+ * 把一次上游调用的 token / cost 增量累计到 session 上。
1564
+ */
1565
+ recordUsage(sessionId, usage) {
1566
+ const entry = this.getSession(sessionId);
1567
+ if (!entry) return;
1568
+ entry.inputTokens += usage.inputTokens ?? 0;
1569
+ entry.outputTokens += usage.outputTokens ?? 0;
1570
+ entry.costEstimate += usage.costEstimate ?? 0;
1571
+ entry.updatedAt = Date.now();
1572
+ }
1573
+ /**
1574
+ * 停止后台清理定时器。
1575
+ */
1576
+ close() {
1577
+ if (this.cleanupTimer) {
1578
+ clearInterval(this.cleanupTimer);
1579
+ }
1580
+ }
1581
+ cleanupExpired() {
1582
+ if (!this.config.enabled) return;
1583
+ for (const [sessionId, entry] of this.sessions) {
1584
+ if (this.isExpired(entry)) {
1585
+ this.sessions.delete(sessionId);
1586
+ }
1587
+ }
1588
+ }
1589
+ isExpired(entry) {
1590
+ return Date.now() >= entry.expiresAt;
1591
+ }
1592
+ };
1593
+ function hashRequestContent(content, toolNames = []) {
1594
+ const normalizedContent = content.trim().replace(/\s+/g, " ");
1595
+ const normalizedTools = [...toolNames].map((tool) => tool.trim()).filter(Boolean).sort().join(",");
1596
+ return hashHex(`${normalizedContent}
1597
+ ${normalizedTools}`, 8);
1598
+ }
1599
+ function deriveSessionId(headers, messages) {
1600
+ const explicit = headers["x-session-id"];
1601
+ const explicitId = pickHeaderValue(explicit);
1602
+ if (explicitId) return explicitId;
1603
+ const firstUserContent = findFirstUserContent(messages);
1604
+ if (!firstUserContent) return void 0;
1605
+ return hashRequestContent(firstUserContent);
1606
+ }
1607
+ function pickHeaderValue(value) {
1608
+ if (typeof value === "string") {
1609
+ const trimmed = value.trim();
1610
+ return trimmed || void 0;
1611
+ }
1612
+ if (Array.isArray(value)) {
1613
+ for (const item of value) {
1614
+ const trimmed = item.trim();
1615
+ if (trimmed) return trimmed;
1616
+ }
1617
+ }
1618
+ return void 0;
1619
+ }
1620
+ function findFirstUserContent(messages) {
1621
+ for (const message of messages) {
1622
+ if (!message || typeof message !== "object") continue;
1623
+ const record = message;
1624
+ if (record.role !== "user") continue;
1625
+ const text = contentToText(record.content);
1626
+ if (text.trim()) return text;
1627
+ }
1628
+ return void 0;
1629
+ }
1630
+ function contentToText(content) {
1631
+ if (typeof content === "string") return content;
1632
+ if (Array.isArray(content)) {
1633
+ return content.map((part) => {
1634
+ if (!part || typeof part !== "object") return "";
1635
+ const record = part;
1636
+ return record.type === "text" && typeof record.text === "string" ? record.text : "";
1637
+ }).filter(Boolean).join(" ");
1638
+ }
1639
+ return "";
1640
+ }
1641
+ function hashHex(value, length) {
1642
+ return createHash("sha256").update(value).digest("hex").slice(0, length);
1643
+ }
1644
+
1645
+ // src/proxy.ts
1646
+ var VERSION = "1.0.0";
1647
+ var HOP_BY_HOP = /* @__PURE__ */ new Set([
1648
+ "connection",
1649
+ "keep-alive",
1650
+ "proxy-authenticate",
1651
+ "proxy-authorization",
1652
+ "te",
1653
+ "trailer",
1654
+ "transfer-encoding",
1655
+ "upgrade",
1656
+ "host",
1657
+ "content-length"
1658
+ ]);
1659
+ var RETRYABLE_STATUS = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
1660
+ var REQUESTABLE_PUBLIC_MODELS = /* @__PURE__ */ new Set(["auto"]);
1661
+ var PUBLIC_HEADER_PREFIXES = ["x-xy-router-"];
1662
+ function readBody(req) {
1663
+ return new Promise((resolve, reject) => {
1664
+ let body = "";
1665
+ req.setEncoding("utf8");
1666
+ req.on("data", (chunk) => {
1667
+ body += chunk;
1668
+ });
1669
+ req.on("end", () => resolve(body));
1670
+ req.on("error", reject);
1671
+ });
1672
+ }
1673
+ var OPENCLAW_CLI_TURN_PATTERN = /(?:^|\n)\[[^\]\n]+?\]\s+([\s\S]*?)(?=(?:\n\[[^\]\n]+?\]\s+)|$)/g;
1674
+ function extractRouteTextFromUserMessage(text) {
1675
+ const matches = [...text.matchAll(OPENCLAW_CLI_TURN_PATTERN)];
1676
+ const last = matches.at(-1)?.[1]?.trim();
1677
+ return last || text;
1678
+ }
1679
+ function extractPrompt(messages) {
1680
+ const parts = [];
1681
+ let system;
1682
+ let openingText = "";
1683
+ let lastUserText = "";
1684
+ for (const msg of messages) {
1685
+ if (!msg || typeof msg !== "object") continue;
1686
+ const role = msg.role;
1687
+ const content = msg.content;
1688
+ let text2 = "";
1689
+ if (typeof content === "string") {
1690
+ text2 = content;
1691
+ } else if (Array.isArray(content)) {
1692
+ text2 = content.filter(
1693
+ (p) => typeof p === "object" && p !== null && p.type === "text"
1694
+ ).map((p) => typeof p.text === "string" ? p.text : "").join(" ");
1695
+ }
1696
+ if (role === "system") {
1697
+ system = system ? `${system}
1698
+ ${text2}` : text2;
1699
+ } else {
1700
+ parts.push(text2);
1701
+ if (role === "user" && text2.trim()) {
1702
+ lastUserText = extractRouteTextFromUserMessage(text2);
1703
+ }
1704
+ if (!openingText && text2.trim()) {
1705
+ openingText = text2;
1706
+ }
1707
+ }
1708
+ }
1709
+ const text = parts.join(" ");
1710
+ return { text, routeText: lastUserText || text, system, openingText };
1711
+ }
1712
+ function buildUpstreamHeaders(req, cfg) {
1713
+ const headers = {};
1714
+ const setHeader = (key, value) => {
1715
+ const existingKey = Object.keys(headers).find(
1716
+ (candidate) => candidate.toLowerCase() === key.toLowerCase()
1717
+ );
1718
+ if (existingKey) {
1719
+ delete headers[existingKey];
1720
+ }
1721
+ headers[key] = value;
1722
+ };
1723
+ for (const [key, value] of Object.entries(req.headers)) {
1724
+ if (HOP_BY_HOP.has(key.toLowerCase())) continue;
1725
+ if (value !== void 0) {
1726
+ setHeader(key, Array.isArray(value) ? value.join(", ") : value);
1727
+ }
1728
+ }
1729
+ for (const [key, value] of Object.entries(cfg.headers)) {
1730
+ setHeader(key, value);
1731
+ }
1732
+ const hasAuth = Object.keys(headers).some(
1733
+ (k) => k.toLowerCase() === "authorization"
1734
+ );
1735
+ if (!hasAuth && cfg.apiKey) {
1736
+ headers.authorization = `Bearer ${cfg.apiKey}`;
1737
+ }
1738
+ if (!Object.keys(headers).some((k) => k.toLowerCase() === "content-type")) {
1739
+ setHeader("content-type", "application/json");
1740
+ }
1741
+ return headers;
1742
+ }
1743
+ function writeJson(res, status, body) {
1744
+ if (!res.headersSent) {
1745
+ res.statusCode = status;
1746
+ res.setHeader("content-type", "application/json");
1747
+ res.end(JSON.stringify(body));
1748
+ }
1749
+ }
1750
+ function copyResponseHeaders(response, extraHeaders) {
1751
+ const headers = {};
1752
+ for (const [key, value] of response.headers.entries()) {
1753
+ const lower = key.toLowerCase();
1754
+ if (HOP_BY_HOP.has(lower)) continue;
1755
+ if (PUBLIC_HEADER_PREFIXES.some((prefix) => lower.startsWith(prefix)))
1756
+ continue;
1757
+ headers[key] = value;
1758
+ }
1759
+ Object.assign(headers, extraHeaders);
1760
+ return headers;
1761
+ }
1762
+ async function streamResponse(response, res) {
1763
+ if (response.body) {
1764
+ for await (const chunk of response.body) {
1765
+ res.write(chunk);
1766
+ }
1767
+ }
1768
+ res.end();
1769
+ }
1770
+ function writeOpenAiError(res, status, message, type = "invalid_request_error", code = null, headers) {
1771
+ if (headers) {
1772
+ for (const [key, value] of Object.entries(headers)) {
1773
+ res.setHeader(key, value);
1774
+ }
1775
+ }
1776
+ writeJson(res, status, {
1777
+ error: {
1778
+ message,
1779
+ type,
1780
+ param: null,
1781
+ code
1782
+ }
1783
+ });
1784
+ }
1785
+ async function fetchUpstream(cfg, req, body, actualModel) {
1786
+ try {
1787
+ const response = await fetch(`${cfg.baseUrl}/chat/completions`, {
1788
+ method: "POST",
1789
+ headers: buildUpstreamHeaders(req, cfg),
1790
+ body: JSON.stringify({ ...body, model: actualModel })
1791
+ });
1792
+ if (RETRYABLE_STATUS.has(response.status)) {
1793
+ return { ok: false, reason: "retryable", response };
1794
+ }
1795
+ return { ok: true, response };
1796
+ } catch (error) {
1797
+ return { ok: false, reason: "network_error", error };
1798
+ }
1799
+ }
1800
+ function getMaxOutputTokens(body) {
1801
+ const maxTokens = body.max_tokens;
1802
+ if (typeof maxTokens === "number" && Number.isFinite(maxTokens) && maxTokens > 0) {
1803
+ return Math.ceil(maxTokens);
1804
+ }
1805
+ const maxCompletionTokens = body.max_completion_tokens;
1806
+ if (typeof maxCompletionTokens === "number" && Number.isFinite(maxCompletionTokens) && maxCompletionTokens > 0) {
1807
+ return Math.ceil(maxCompletionTokens);
1808
+ }
1809
+ return 1024;
1810
+ }
1811
+ function buildRouterOptions(options) {
1812
+ return {
1813
+ config: options.routingConfig,
1814
+ modelPricing: options.modelPricing
1815
+ };
1816
+ }
1817
+ function buildRoutingConfigFromRawConfig(rawConfig) {
1818
+ const scoring = {
1819
+ ...DEFAULT_ROUTING_CONFIG.scoring,
1820
+ tierBoundaries: {
1821
+ ...DEFAULT_ROUTING_CONFIG.scoring.tierBoundaries,
1822
+ ...rawConfig.routing.tierBoundaries
1823
+ },
1824
+ confidenceThreshold: rawConfig.routing.confidenceThreshold ?? DEFAULT_ROUTING_CONFIG.scoring.confidenceThreshold
1825
+ };
1826
+ return {
1827
+ ...DEFAULT_ROUTING_CONFIG,
1828
+ scoring,
1829
+ tiers: mapRawTierEntries(rawConfig.routing.tiers),
1830
+ overrides: {
1831
+ structuredOutputMinTier: rawConfig.routing.structuredOutputMinTier ?? "MEDIUM",
1832
+ ambiguousDefaultTier: rawConfig.routing.ambiguousDefaultTier ?? "MEDIUM"
1833
+ }
1834
+ };
1835
+ }
1836
+ function mapRawTierEntries(entries) {
1837
+ return Object.fromEntries(
1838
+ Object.entries(entries).map(([tier, entry]) => [
1839
+ tier,
1840
+ {
1841
+ primary: entry.publicModel,
1842
+ fallback: entry.fallback ?? []
1843
+ }
1844
+ ])
1845
+ );
1846
+ }
1847
+ function buildModelPricingFromPublicModels(publicModels, registry2) {
1848
+ return new Map(
1849
+ Object.entries(publicModels).map(([publicModelId, config]) => {
1850
+ if (config.kind === "router") {
1851
+ return [
1852
+ publicModelId,
1853
+ {
1854
+ inputPrice: config.metadata.cost.input,
1855
+ outputPrice: config.metadata.cost.output
1856
+ }
1857
+ ];
1858
+ }
1859
+ const physicalModel = resolvePublicModelCandidate(
1860
+ publicModelId,
1861
+ publicModels,
1862
+ registry2
1863
+ );
1864
+ return [
1865
+ publicModelId,
1866
+ {
1867
+ inputPrice: physicalModel.inputPrice,
1868
+ outputPrice: physicalModel.outputPrice
1869
+ }
1870
+ ];
1871
+ })
1872
+ );
1873
+ }
1874
+ var TIER_ORDER = {
1875
+ SIMPLE: 0,
1876
+ MEDIUM: 1,
1877
+ COMPLEX: 2,
1878
+ REASONING: 3
1879
+ };
1880
+ function isLowerTier(nextTier, pinnedTier) {
1881
+ return TIER_ORDER[nextTier] < TIER_ORDER[pinnedTier];
1882
+ }
1883
+ function getExplicitTier(publicModelId, entries) {
1884
+ const matches = Object.entries(entries).filter(([, entry]) => entry.publicModel === publicModelId).map(([tier]) => tier);
1885
+ if (matches.length === 0) {
1886
+ return "MEDIUM";
1887
+ }
1888
+ return matches.reduce(
1889
+ (highest, tier) => TIER_ORDER[tier] > TIER_ORDER[highest] ? tier : highest
1890
+ );
1891
+ }
1892
+ function getTraceReason(selected, failed) {
1893
+ if (selected.explicit) return "user";
1894
+ if (selected.decision?.tier === "REASONING") return "reasoning";
1895
+ if (failed) return "error";
1896
+ return "first-pass";
1897
+ }
1898
+ function buildPublicHeaders(cfg, selected, finalTier, trace) {
1899
+ return {
1900
+ "x-xy-router-model": selected.routedModel,
1901
+ "x-xy-router-actual-model": selected.actualModel,
1902
+ "x-xy-router-tier": finalTier,
1903
+ "x-xy-router-trace": trace,
1904
+ "x-xy-router-routed": String(selected.routed),
1905
+ "x-xy-router-fallback": "false",
1906
+ "x-xy-router-upstream": cfg.baseUrl
1907
+ };
1908
+ }
1909
+ function emitProxyTrace(cfg, selected, finalTier, attempts, sessionAction, failed) {
1910
+ const writer = resolveTraceWriter(cfg.traceLogger);
1911
+ const reason = getTraceReason(selected, failed);
1912
+ const trace = buildTraceSummary({
1913
+ requestedModel: selected.requestedModel,
1914
+ routedModel: selected.routedModel,
1915
+ actualModel: selected.actualModel,
1916
+ tier: finalTier,
1917
+ profile: selected.decision?.profile ?? "default",
1918
+ reason,
1919
+ routed: selected.routed,
1920
+ explicit: selected.explicit,
1921
+ fallback: false
1922
+ });
1923
+ const detail = {
1924
+ trace,
1925
+ requestedModel: selected.requestedModel,
1926
+ routedModel: selected.routedModel,
1927
+ actualModel: selected.actualModel,
1928
+ tier: finalTier,
1929
+ profile: selected.decision?.profile ?? "default",
1930
+ reason,
1931
+ explicit: selected.explicit,
1932
+ routed: selected.routed,
1933
+ fallback: false,
1934
+ attempts,
1935
+ sessionAction,
1936
+ ...selected.decision && {
1937
+ method: selected.decision.method,
1938
+ confidence: selected.decision.confidence,
1939
+ ...selected.decision.score !== void 0 && {
1940
+ score: selected.decision.score
1941
+ },
1942
+ ...selected.decision.agenticScore !== void 0 && {
1943
+ agenticScore: selected.decision.agenticScore
1944
+ }
1945
+ },
1946
+ ...cfg.traceMode === "debug" && {
1947
+ promptPreview: getPromptPreview(selected.routeText)
1948
+ }
1949
+ };
1950
+ emitRouteTrace(cfg.traceMode, detail, writer);
1951
+ return trace;
1952
+ }
1953
+ function chooseModel(requestedModel, body, headers, sessionStore, cfg, tierEntries, publicModels, registry2, routerOptions) {
1954
+ const prompt = extractPrompt(body.messages ?? []);
1955
+ if (requestedModel !== "auto") {
1956
+ const physicalModel2 = resolvePublicModelCandidate(
1957
+ requestedModel,
1958
+ publicModels,
1959
+ registry2
1960
+ );
1961
+ return {
1962
+ routedModel: requestedModel,
1963
+ actualModel: physicalModel2.id,
1964
+ tier: getExplicitTier(requestedModel, tierEntries),
1965
+ routeText: prompt.routeText,
1966
+ requestedModel,
1967
+ routed: false,
1968
+ explicit: true,
1969
+ sessionAction: "none"
1970
+ };
1971
+ }
1972
+ const sessionId = deriveSessionId(headers, body.messages ?? []);
1973
+ const existing = cfg.sessionPinning ? sessionStore.getSession(sessionId) : void 0;
1974
+ const decision = route(
1975
+ prompt.routeText,
1976
+ prompt.system,
1977
+ getMaxOutputTokens(body),
1978
+ routerOptions
1979
+ );
1980
+ const routedModel = decision.publicModel;
1981
+ const physicalModel = resolvePublicModelCandidate(
1982
+ routedModel,
1983
+ publicModels,
1984
+ registry2
1985
+ );
1986
+ if (existing && isLowerTier(decision.tier, existing.pinnedTier)) {
1987
+ sessionStore.touchSession(sessionId);
1988
+ return {
1989
+ routedModel: existing.routedPublicModel,
1990
+ actualModel: existing.physicalModelId,
1991
+ tier: existing.pinnedTier,
1992
+ routeText: prompt.routeText,
1993
+ requestedModel,
1994
+ sessionId,
1995
+ routed: true,
1996
+ explicit: false,
1997
+ sessionAction: "reuse"
1998
+ };
1999
+ }
2000
+ return {
2001
+ routedModel,
2002
+ actualModel: physicalModel.id,
2003
+ tier: decision.tier,
2004
+ decision,
2005
+ routeText: prompt.routeText,
2006
+ requestedModel,
2007
+ sessionId,
2008
+ routed: true,
2009
+ explicit: false,
2010
+ sessionAction: "none"
2011
+ };
2012
+ }
2013
+ async function proxyChat(req, res, cfg, sessionStore, tierEntries, publicModels, registry2, routerOptions) {
2014
+ const rawBody = await readBody(req);
2015
+ let body;
2016
+ try {
2017
+ body = JSON.parse(rawBody);
2018
+ } catch {
2019
+ writeOpenAiError(res, 400, "Invalid JSON body");
2020
+ return;
2021
+ }
2022
+ if (!body || typeof body !== "object") {
2023
+ writeOpenAiError(res, 400, "Body must be a JSON object");
2024
+ return;
2025
+ }
2026
+ const bodyObj = body;
2027
+ const requestedModelId = bodyObj.model;
2028
+ const supportedModels = [...REQUESTABLE_PUBLIC_MODELS].filter((modelId) => publicModels[modelId]).sort().join(", ");
2029
+ if (typeof requestedModelId !== "string" || !publicModels[requestedModelId] || !REQUESTABLE_PUBLIC_MODELS.has(requestedModelId)) {
2030
+ writeOpenAiError(
2031
+ res,
2032
+ 400,
2033
+ `Unknown model "${String(requestedModelId)}". Supported models: ${supportedModels}`,
2034
+ "invalid_request_error",
2035
+ "model_not_found"
2036
+ );
2037
+ return;
2038
+ }
2039
+ const selected = chooseModel(
2040
+ requestedModelId,
2041
+ bodyObj,
2042
+ req.headers,
2043
+ sessionStore,
2044
+ cfg,
2045
+ tierEntries,
2046
+ publicModels,
2047
+ registry2,
2048
+ routerOptions
2049
+ );
2050
+ const physicalModel = registry2.get(selected.actualModel);
2051
+ if (!physicalModel) {
2052
+ writeOpenAiError(
2053
+ res,
2054
+ 500,
2055
+ `Physical model not found in registry: ${selected.actualModel}`
2056
+ );
2057
+ return;
2058
+ }
2059
+ const attempt = await fetchUpstream(cfg, req, bodyObj, physicalModel.id);
2060
+ const attempts = [
2061
+ attempt.ok ? { model: selected.actualModel, status: "success" } : {
2062
+ model: selected.actualModel,
2063
+ status: "error",
2064
+ error: attempt.reason === "network_error" ? "network_error" : `upstream_http_${attempt.response.status}`
2065
+ }
2066
+ ];
2067
+ const finalTier = selected.tier;
2068
+ let sessionAction = selected.sessionAction;
2069
+ if (!attempt.ok && attempt.reason === "network_error") {
2070
+ const trace2 = emitProxyTrace(
2071
+ cfg,
2072
+ selected,
2073
+ finalTier,
2074
+ attempts,
2075
+ sessionAction,
2076
+ true
2077
+ );
2078
+ const headers2 = buildPublicHeaders(cfg, selected, finalTier, trace2);
2079
+ writeOpenAiError(
2080
+ res,
2081
+ 502,
2082
+ attempt.error instanceof Error ? attempt.error.message : "Upstream request failed",
2083
+ "invalid_request_error",
2084
+ null,
2085
+ headers2
2086
+ );
2087
+ return;
2088
+ }
2089
+ if (attempt.ok && selected.sessionId && !selected.explicit) {
2090
+ sessionStore.setSession(selected.sessionId, {
2091
+ physicalModelId: selected.actualModel,
2092
+ routedPublicModel: selected.routedModel,
2093
+ pinnedTier: finalTier
2094
+ });
2095
+ if (sessionAction === "none") {
2096
+ sessionAction = "set";
2097
+ }
2098
+ }
2099
+ const trace = emitProxyTrace(
2100
+ cfg,
2101
+ selected,
2102
+ finalTier,
2103
+ attempts,
2104
+ sessionAction,
2105
+ !attempt.ok
2106
+ );
2107
+ const headers = buildPublicHeaders(cfg, selected, finalTier, trace);
2108
+ const responseHeaders = copyResponseHeaders(attempt.response, headers);
2109
+ res.statusCode = attempt.response.status;
2110
+ for (const [k, v] of Object.entries(responseHeaders)) {
2111
+ res.setHeader(k, v);
2112
+ }
2113
+ await streamResponse(attempt.response, res);
2114
+ }
2115
+ async function startProxy(options) {
2116
+ const cfg = resolveConfig({
2117
+ baseUrl: options.config.proxy.upstreamUrl,
2118
+ apiKey: options.config.proxy.apiKey,
2119
+ headers: options.config.proxy.headers,
2120
+ port: options.config.proxy.port,
2121
+ traceMode: options.config.proxy.trace,
2122
+ traceLogger: options.traceLogger,
2123
+ sessionPinning: options.session?.enabled
2124
+ });
2125
+ const sessionStore = new SessionStore(options.session);
2126
+ const publicModels = options.config.publicModels;
2127
+ const tierEntries = options.config.routing.tiers;
2128
+ const registry2 = createModelRegistry(options.config.models);
2129
+ const routerOptions = buildRouterOptions({
2130
+ routingConfig: buildRoutingConfigFromRawConfig(options.config),
2131
+ modelPricing: buildModelPricingFromPublicModels(publicModels, registry2)
2132
+ });
2133
+ const server = http.createServer((req, res) => {
2134
+ void (async () => {
2135
+ try {
2136
+ const url = req.url ?? "/";
2137
+ if (req.method === "GET" && url === "/health") {
2138
+ res.setHeader("content-type", "application/json");
2139
+ res.end(
2140
+ JSON.stringify({
2141
+ status: "ok",
2142
+ baseUrl: cfg.baseUrl,
2143
+ version: VERSION
2144
+ })
2145
+ );
2146
+ return;
2147
+ }
2148
+ if (req.method === "POST" && url === "/v1/chat/completions") {
2149
+ await proxyChat(
2150
+ req,
2151
+ res,
2152
+ cfg,
2153
+ sessionStore,
2154
+ tierEntries,
2155
+ publicModels,
2156
+ registry2,
2157
+ routerOptions
2158
+ );
2159
+ return;
2160
+ }
2161
+ writeOpenAiError(res, 404, "Not Found");
2162
+ } catch (error) {
2163
+ if (!res.headersSent) {
2164
+ writeOpenAiError(res, 502, String(error));
2165
+ } else {
2166
+ res.destroy();
2167
+ }
2168
+ }
2169
+ })();
2170
+ });
2171
+ const port = await new Promise((resolve, reject) => {
2172
+ server.once("error", reject);
2173
+ server.listen(cfg.port, "127.0.0.1", () => {
2174
+ const address = server.address();
2175
+ if (!address || typeof address === "string") {
2176
+ reject(new Error("Could not determine server port"));
2177
+ return;
2178
+ }
2179
+ resolve(address.port);
2180
+ });
2181
+ });
2182
+ return {
2183
+ port,
2184
+ baseUrl: cfg.baseUrl,
2185
+ close: () => new Promise((resolve, reject) => {
2186
+ server.close((err) => {
2187
+ sessionStore.close();
2188
+ if (err) reject(err);
2189
+ else resolve();
2190
+ });
2191
+ })
2192
+ };
2193
+ }
2194
+
2195
+ // src/config-loader.ts
2196
+ import { readFileSync } from "fs";
2197
+ function hasOwn(value, key) {
2198
+ return Object.prototype.hasOwnProperty.call(value, key);
2199
+ }
2200
+ function isFiniteNumber(value) {
2201
+ return typeof value === "number" && Number.isFinite(value);
2202
+ }
2203
+ function assertFiniteNumber(value, path) {
2204
+ if (!isFiniteNumber(value)) {
2205
+ throw new Error(`${path} must be a finite number`);
2206
+ }
2207
+ }
2208
+ function assertPublicModelMetadata(value, path) {
2209
+ if (!value || typeof value !== "object") {
2210
+ throw new Error(`${path}.metadata is required`);
2211
+ }
2212
+ const metadata = value;
2213
+ const metadataPath = `${path}.metadata`;
2214
+ for (const key of ["name", "reasoning", "contextWindow", "maxTokens", "cost"]) {
2215
+ if (!hasOwn(metadata, key)) {
2216
+ throw new Error(`${metadataPath}.${key} is required`);
2217
+ }
2218
+ }
2219
+ if (typeof metadata.name !== "string") {
2220
+ throw new Error(`${metadataPath}.name must be a string`);
2221
+ }
2222
+ if (typeof metadata.reasoning !== "boolean") {
2223
+ throw new Error(`${metadataPath}.reasoning must be a boolean`);
2224
+ }
2225
+ if (typeof metadata.contextWindow !== "number") {
2226
+ throw new Error(`${metadataPath}.contextWindow must be a number`);
2227
+ }
2228
+ if (typeof metadata.maxTokens !== "number") {
2229
+ throw new Error(`${metadataPath}.maxTokens must be a number`);
2230
+ }
2231
+ if (!metadata.cost || typeof metadata.cost !== "object") {
2232
+ throw new Error(`${metadataPath}.cost must be an object`);
2233
+ }
2234
+ const cost = metadata.cost;
2235
+ const costPath = `${metadataPath}.cost`;
2236
+ for (const key of ["input", "output", "cacheRead", "cacheWrite"]) {
2237
+ if (!hasOwn(cost, key)) {
2238
+ throw new Error(`${costPath}.${key} is required`);
2239
+ }
2240
+ }
2241
+ for (const key of ["input", "output", "cacheRead", "cacheWrite"]) {
2242
+ if (typeof cost[key] !== "number") {
2243
+ throw new Error(`${costPath}.${key} must be a number`);
2244
+ }
2245
+ }
2246
+ }
2247
+ function assertAliasPublicModel(publicModels, id, path) {
2248
+ const publicModel = publicModels[id];
2249
+ if (!publicModel) {
2250
+ throw new Error(`${path} references unknown publicModel: ${id}`);
2251
+ }
2252
+ if (publicModel.kind !== "alias") {
2253
+ throw new Error(`${path} must reference a publicModel with kind: "alias"`);
2254
+ }
2255
+ }
2256
+ function loadConfig(source) {
2257
+ const raw = source.kind === "inline" ? source.config : JSON.parse(readFileSync(source.path, "utf-8"));
2258
+ validateConfig(raw);
2259
+ return raw;
2260
+ }
2261
+ function validateConfig(config) {
2262
+ const modelIds = /* @__PURE__ */ new Set();
2263
+ for (const model of config.models) {
2264
+ if (modelIds.has(model.id)) {
2265
+ throw new Error(`Duplicate model ID: ${model.id}`);
2266
+ }
2267
+ modelIds.add(model.id);
2268
+ }
2269
+ const auto = config.publicModels.auto;
2270
+ if (!auto || auto.kind !== "router") {
2271
+ throw new Error('publicModels must contain "auto" with kind: "router"');
2272
+ }
2273
+ assertPublicModelMetadata(auto.metadata, "publicModels.auto");
2274
+ for (const [publicModelId, publicModel] of Object.entries(config.publicModels)) {
2275
+ if (publicModel.kind === "router") {
2276
+ if (publicModelId !== "auto") {
2277
+ throw new Error(`publicModels.${publicModelId}: only auto may use kind: "router"`);
2278
+ }
2279
+ assertPublicModelMetadata(publicModel.metadata, `publicModels.${publicModelId}`);
2280
+ continue;
2281
+ }
2282
+ if (!Array.isArray(publicModel.candidates) || publicModel.candidates.length === 0) {
2283
+ throw new Error(`publicModels.${publicModelId}.candidates must not be empty`);
2284
+ }
2285
+ for (const candidate of publicModel.candidates) {
2286
+ if (!modelIds.has(candidate)) {
2287
+ throw new Error(`Unknown candidate '${candidate}' in publicModels.${publicModelId}`);
2288
+ }
2289
+ }
2290
+ if (publicModel.metadata != null) {
2291
+ assertPublicModelMetadata(publicModel.metadata, `publicModels.${publicModelId}`);
2292
+ }
2293
+ }
2294
+ for (const tier of ["SIMPLE", "MEDIUM", "COMPLEX", "REASONING"]) {
2295
+ if (!config.routing.tiers[tier]) {
2296
+ throw new Error(`routing.tiers.${tier} is required`);
2297
+ }
2298
+ }
2299
+ for (const [tier, tierConfig] of Object.entries(config.routing.tiers)) {
2300
+ assertAliasPublicModel(config.publicModels, tierConfig.publicModel, `routing.tiers.${tier}.publicModel`);
2301
+ for (const fallbackId of tierConfig.fallback ?? []) {
2302
+ assertAliasPublicModel(config.publicModels, fallbackId, `routing.tiers.${tier}.fallback`);
2303
+ }
2304
+ }
2305
+ if (hasOwn(config.routing, "tierBoundaries")) {
2306
+ const tierBoundaries = config.routing.tierBoundaries;
2307
+ if (!tierBoundaries || typeof tierBoundaries !== "object" || Array.isArray(tierBoundaries)) {
2308
+ throw new Error("routing.tierBoundaries must be an object");
2309
+ }
2310
+ const { simpleMedium, mediumComplex, complexReasoning } = tierBoundaries;
2311
+ assertFiniteNumber(simpleMedium, "routing.tierBoundaries.simpleMedium");
2312
+ assertFiniteNumber(mediumComplex, "routing.tierBoundaries.mediumComplex");
2313
+ assertFiniteNumber(complexReasoning, "routing.tierBoundaries.complexReasoning");
2314
+ if (!(simpleMedium <= mediumComplex && mediumComplex <= complexReasoning)) {
2315
+ throw new Error(
2316
+ "routing.tierBoundaries must satisfy simpleMedium <= mediumComplex <= complexReasoning"
2317
+ );
2318
+ }
2319
+ }
2320
+ if (hasOwn(config.routing, "confidenceThreshold")) {
2321
+ assertFiniteNumber(config.routing.confidenceThreshold, "routing.confidenceThreshold");
2322
+ if (config.routing.confidenceThreshold < 0 || config.routing.confidenceThreshold > 1) {
2323
+ throw new Error("routing.confidenceThreshold must be between 0 and 1");
2324
+ }
2325
+ }
2326
+ if (!Number.isInteger(config.proxy.port) || config.proxy.port < 1 || config.proxy.port > 65535) {
2327
+ throw new Error(`proxy.port must be an integer between 1-65535, got: ${config.proxy.port}`);
2328
+ }
2329
+ if (config.proxy.headers) {
2330
+ for (const [key, value] of Object.entries(config.proxy.headers)) {
2331
+ if (typeof value !== "string") {
2332
+ throw new Error(`proxy.headers['${key}'] must be a string, got: ${typeof value}`);
2333
+ }
2334
+ }
2335
+ }
2336
+ }
2337
+
2338
+ // src/proxy-config-resolver.ts
2339
+ function resolveProxyConfig(baseConfig, overrides = {}) {
2340
+ const mergedHeaders = mergeHeaders(baseConfig.headers, overrides.headers);
2341
+ return {
2342
+ port: overrides.port ?? baseConfig.port,
2343
+ upstreamUrl: overrides.upstreamUrl ?? baseConfig.upstreamUrl,
2344
+ apiKey: overrides.apiKey ?? baseConfig.apiKey,
2345
+ headers: mergedHeaders,
2346
+ trace: overrides.trace ?? baseConfig.trace
2347
+ };
2348
+ }
2349
+ function mergeHeaders(base, override) {
2350
+ if (base === void 0 && override === void 0) {
2351
+ return void 0;
2352
+ }
2353
+ return { ...base, ...override };
2354
+ }
2355
+
2356
+ // src/provider.ts
2357
+ var LLM_ROUTER_PROVIDER_ID = "xiaoyiprovider";
2358
+ var LLM_ROUTER_PROVIDER_NAME = "LLM Router Provider";
2359
+ var LLM_ROUTER_PROVIDER_DESCRIPTION = "LLM Router local routing provider for OpenAI-compatible models";
2360
+ var LLM_ROUTER_PROVIDER_API = "openai-completions";
2361
+ function cacheReadCost(inputPrice) {
2362
+ return Number((inputPrice * 0.25).toFixed(2));
2363
+ }
2364
+ function fromMetadata(id, metadata) {
2365
+ return {
2366
+ id,
2367
+ name: metadata.name,
2368
+ api: LLM_ROUTER_PROVIDER_API,
2369
+ reasoning: metadata.reasoning,
2370
+ input: ["text"],
2371
+ cost: metadata.cost,
2372
+ contextWindow: metadata.contextWindow,
2373
+ maxTokens: metadata.maxTokens
2374
+ };
2375
+ }
2376
+ function fromPhysicalModel(id, physicalModel) {
2377
+ return {
2378
+ id,
2379
+ name: physicalModel.name,
2380
+ api: LLM_ROUTER_PROVIDER_API,
2381
+ reasoning: physicalModel.reasoning,
2382
+ input: ["text"],
2383
+ cost: {
2384
+ input: physicalModel.inputPrice,
2385
+ output: physicalModel.outputPrice,
2386
+ cacheRead: cacheReadCost(physicalModel.inputPrice),
2387
+ cacheWrite: physicalModel.inputPrice
2388
+ },
2389
+ contextWindow: physicalModel.contextWindow,
2390
+ maxTokens: physicalModel.maxOutput
2391
+ };
2392
+ }
2393
+ function generateOpenClawModels(publicModels, physicalModels) {
2394
+ const registry2 = createModelRegistry(physicalModels);
2395
+ return Object.entries(publicModels).map(([id, publicModel]) => {
2396
+ if (publicModel.kind === "router") {
2397
+ return fromMetadata(id, publicModel.metadata);
2398
+ }
2399
+ if (publicModel.metadata) {
2400
+ return fromMetadata(id, publicModel.metadata);
2401
+ }
2402
+ const physicalModel = resolvePublicModelCandidate(
2403
+ id,
2404
+ publicModels,
2405
+ registry2
2406
+ );
2407
+ return fromPhysicalModel(id, physicalModel);
2408
+ });
2409
+ }
2410
+
2411
+ // src/plugin.ts
2412
+ var defaultRuntime = {
2413
+ startProxy
2414
+ };
2415
+ var activeProxy;
2416
+ var closedProxies = /* @__PURE__ */ new WeakSet();
2417
+ var closingProxies = /* @__PURE__ */ new WeakMap();
2418
+ var RUNTIME_REGISTRATION_MODES = /* @__PURE__ */ new Set([
2419
+ "full",
2420
+ "runtime",
2421
+ "activate",
2422
+ "active"
2423
+ ]);
2424
+ function createTraceLogger(api) {
2425
+ return {
2426
+ debug: (message) => {
2427
+ if (api.logger?.debug) {
2428
+ api.logger.debug(message);
2429
+ return;
2430
+ }
2431
+ api.logger?.info?.(message);
2432
+ },
2433
+ info: (message) => {
2434
+ api.logger?.info?.(message);
2435
+ }
2436
+ };
2437
+ }
2438
+ function ensureObject(parent, key) {
2439
+ const value = parent[key];
2440
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2441
+ const created = {};
2442
+ parent[key] = created;
2443
+ return created;
2444
+ }
2445
+ return value;
2446
+ }
2447
+ function localProviderBaseUrl(port) {
2448
+ return `http://127.0.0.1:${port}/v1`;
2449
+ }
2450
+ function injectLlmRouterModelsConfig(config, providerBaseUrl, modelDefinitions) {
2451
+ const modelsConfig = ensureObject(config, "models");
2452
+ const providers = ensureObject(modelsConfig, "providers");
2453
+ const current = providers[LLM_ROUTER_PROVIDER_ID];
2454
+ const existing = current && typeof current === "object" && !Array.isArray(current) ? current : {};
2455
+ providers[LLM_ROUTER_PROVIDER_ID] = {
2456
+ ...existing,
2457
+ baseUrl: providerBaseUrl,
2458
+ api: LLM_ROUTER_PROVIDER_API,
2459
+ models: modelDefinitions.filter((model) => model.id === "auto")
2460
+ };
2461
+ }
2462
+ function parsePortValue(value) {
2463
+ if (typeof value === "number") {
2464
+ if (!Number.isInteger(value) || value <= 0 || value >= 65536)
2465
+ return void 0;
2466
+ return value;
2467
+ }
2468
+ if (typeof value !== "string" || !/^\d+$/.test(value)) return void 0;
2469
+ const port = Number.parseInt(value, 10);
2470
+ if (!Number.isInteger(port) || port <= 0 || port >= 65536) return void 0;
2471
+ return port;
2472
+ }
2473
+ function resolvePluginConfig(api) {
2474
+ const inline = api.pluginConfig?.config;
2475
+ const path = api.pluginConfig?.configPath;
2476
+ if (inline) {
2477
+ return loadConfig({ kind: "inline", config: inline });
2478
+ }
2479
+ if (path) {
2480
+ return loadConfig({ kind: "file", path });
2481
+ }
2482
+ throw new Error(
2483
+ "llm-router: missing config. Set pluginConfig.config or pluginConfig.configPath"
2484
+ );
2485
+ }
2486
+ function normalizeTraceOverride(value) {
2487
+ return value === "off" || value === "summary" || value === "debug" ? value : void 0;
2488
+ }
2489
+ function resolvePluginRuntimeConfig(api) {
2490
+ const config = resolvePluginConfig(api);
2491
+ const portOverride = parsePortValue(api.pluginConfig?.port);
2492
+ const upstreamOverride = typeof api.pluginConfig?.upstreamUrl === "string" && api.pluginConfig.upstreamUrl.trim() ? api.pluginConfig.upstreamUrl : void 0;
2493
+ return {
2494
+ ...config,
2495
+ proxy: resolveProxyConfig(config.proxy, {
2496
+ port: portOverride,
2497
+ upstreamUrl: upstreamOverride,
2498
+ trace: normalizeTraceOverride(api.pluginConfig?.trace)
2499
+ })
2500
+ };
2501
+ }
2502
+ function readProviderConfig(api) {
2503
+ const models = api.config.models;
2504
+ if (!models || typeof models !== "object" || Array.isArray(models)) return {};
2505
+ const providers = models.providers;
2506
+ if (!providers || typeof providers !== "object" || Array.isArray(providers))
2507
+ return {};
2508
+ const provider = providers[LLM_ROUTER_PROVIDER_ID];
2509
+ if (!provider || typeof provider !== "object" || Array.isArray(provider))
2510
+ return {};
2511
+ return provider;
2512
+ }
2513
+ function readStringRecord(value) {
2514
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
2515
+ const headers = {};
2516
+ for (const [key, headerValue] of Object.entries(value)) {
2517
+ if (typeof headerValue === "string") {
2518
+ headers[key] = headerValue;
2519
+ }
2520
+ }
2521
+ return headers;
2522
+ }
2523
+ function mergeHeaders2(...records) {
2524
+ const headers = {};
2525
+ for (const record of records) {
2526
+ for (const [key, value] of Object.entries(record)) {
2527
+ const existingKey = Object.keys(headers).find(
2528
+ (candidate) => candidate.toLowerCase() === key.toLowerCase()
2529
+ );
2530
+ if (existingKey) {
2531
+ delete headers[existingKey];
2532
+ }
2533
+ headers[key] = value;
2534
+ }
2535
+ }
2536
+ return headers;
2537
+ }
2538
+ function resolveProviderRuntimeOverrides(api) {
2539
+ const provider = readProviderConfig(api);
2540
+ const request = provider.request && typeof provider.request === "object" && !Array.isArray(provider.request) ? provider.request : {};
2541
+ const apiKey = typeof provider.apiKey === "string" ? provider.apiKey : typeof provider.api_key === "string" ? provider.api_key : void 0;
2542
+ const headers = mergeHeaders2(
2543
+ readStringRecord(provider.headers),
2544
+ readStringRecord(request.headers)
2545
+ );
2546
+ return {
2547
+ ...apiKey ? { apiKey } : {},
2548
+ ...Object.keys(headers).length > 0 ? { headers } : {}
2549
+ };
2550
+ }
2551
+ async function persistLlmRouterModelsConfig(api, providerBaseUrl, modelDefinitions) {
2552
+ const mutateConfigFile = api.runtime?.config?.mutateConfigFile;
2553
+ if (!mutateConfigFile) return;
2554
+ await mutateConfigFile({
2555
+ afterWrite: { mode: "auto" },
2556
+ mutate: (draft) => {
2557
+ injectLlmRouterModelsConfig(draft, providerBaseUrl, modelDefinitions);
2558
+ }
2559
+ });
2560
+ }
2561
+ function shouldStartRuntimeProxy(registrationMode) {
2562
+ if (registrationMode === void 0) return true;
2563
+ return RUNTIME_REGISTRATION_MODES.has(registrationMode);
2564
+ }
2565
+ async function closeProxyOnce(proxy) {
2566
+ if (closedProxies.has(proxy)) return;
2567
+ const closing = closingProxies.get(proxy);
2568
+ if (closing) {
2569
+ await closing;
2570
+ return;
2571
+ }
2572
+ const closePromise = (async () => {
2573
+ await proxy.close();
2574
+ closedProxies.add(proxy);
2575
+ if (activeProxy === proxy) {
2576
+ activeProxy = void 0;
2577
+ }
2578
+ })();
2579
+ closingProxies.set(proxy, closePromise);
2580
+ try {
2581
+ await closePromise;
2582
+ } finally {
2583
+ closingProxies.delete(proxy);
2584
+ }
2585
+ }
2586
+ async function closeActiveProxy() {
2587
+ const previous = activeProxy;
2588
+ if (previous) {
2589
+ await closeProxyOnce(previous);
2590
+ }
2591
+ }
2592
+ async function replaceActiveProxy(proxy) {
2593
+ const previous = activeProxy;
2594
+ if (activeProxy === proxy) {
2595
+ return;
2596
+ }
2597
+ if (previous) {
2598
+ await closeProxyOnce(previous);
2599
+ }
2600
+ activeProxy = proxy;
2601
+ }
2602
+ function createProxyService(api, runtime, runtimeConfig, providerBaseUrl, modelDefinitions) {
2603
+ let serviceProxy;
2604
+ return {
2605
+ id: "llm-router-proxy",
2606
+ async start() {
2607
+ try {
2608
+ if (serviceProxy && activeProxy === serviceProxy) {
2609
+ return;
2610
+ }
2611
+ if (serviceProxy) {
2612
+ await closeProxyOnce(serviceProxy);
2613
+ serviceProxy = void 0;
2614
+ }
2615
+ await persistLlmRouterModelsConfig(
2616
+ api,
2617
+ providerBaseUrl,
2618
+ modelDefinitions
2619
+ );
2620
+ const providerOverrides = resolveProviderRuntimeOverrides(api);
2621
+ const startConfig = {
2622
+ ...runtimeConfig,
2623
+ proxy: resolveProxyConfig(runtimeConfig.proxy, providerOverrides)
2624
+ };
2625
+ await closeActiveProxy();
2626
+ const proxy = await runtime.startProxy({
2627
+ config: startConfig,
2628
+ traceLogger: createTraceLogger(api),
2629
+ session: {}
2630
+ });
2631
+ serviceProxy = proxy;
2632
+ await replaceActiveProxy(proxy);
2633
+ api.logger?.info?.(`LLM Router listening on ${providerBaseUrl}`);
2634
+ } catch (error) {
2635
+ const message = error instanceof Error ? error.message : String(error);
2636
+ api.logger?.error?.(
2637
+ `LLM Router failed to start on port ${runtimeConfig.proxy.port}: ${message}`
2638
+ );
2639
+ throw error;
2640
+ }
2641
+ },
2642
+ async stop() {
2643
+ if (!serviceProxy) return;
2644
+ const proxy = serviceProxy;
2645
+ await closeProxyOnce(proxy);
2646
+ if (serviceProxy === proxy && activeProxy !== proxy) {
2647
+ serviceProxy = void 0;
2648
+ }
2649
+ }
2650
+ };
2651
+ }
2652
+ function registerOpenClawPlugin(api, runtime = defaultRuntime) {
2653
+ const runtimeConfig = resolvePluginRuntimeConfig(api);
2654
+ const providerBaseUrl = localProviderBaseUrl(runtimeConfig.proxy.port);
2655
+ const models = generateOpenClawModels(
2656
+ runtimeConfig.publicModels,
2657
+ runtimeConfig.models
2658
+ );
2659
+ const shouldRegisterRuntimeService = shouldStartRuntimeProxy(
2660
+ api.registrationMode
2661
+ );
2662
+ if (!shouldRegisterRuntimeService) {
2663
+ injectLlmRouterModelsConfig(api.config, providerBaseUrl, models);
2664
+ return;
2665
+ }
2666
+ const previousConfig = structuredClone(api.config);
2667
+ injectLlmRouterModelsConfig(api.config, providerBaseUrl, models);
2668
+ try {
2669
+ api.registerService(
2670
+ createProxyService(api, runtime, runtimeConfig, providerBaseUrl, models)
2671
+ );
2672
+ } catch (error) {
2673
+ for (const key of Object.keys(api.config)) {
2674
+ delete api.config[key];
2675
+ }
2676
+ Object.assign(api.config, previousConfig);
2677
+ throw error;
2678
+ }
2679
+ }
2680
+
2681
+ // src/index.ts
2682
+ var plugin = {
2683
+ id: "llm-router",
2684
+ name: "LLM Router",
2685
+ description: "LLM Router local routing proxy for OpenClaw",
2686
+ version: VERSION,
2687
+ register: registerOpenClawPlugin
2688
+ };
2689
+ var index_default = plugin;
2690
+ export {
2691
+ DEFAULT_ROUTING_CONFIG,
2692
+ DEFAULT_SESSION_CONFIG,
2693
+ LLM_ROUTER_PROVIDER_API,
2694
+ LLM_ROUTER_PROVIDER_DESCRIPTION,
2695
+ LLM_ROUTER_PROVIDER_ID,
2696
+ LLM_ROUTER_PROVIDER_NAME,
2697
+ RulesStrategy,
2698
+ SessionStore,
2699
+ VERSION,
2700
+ buildTraceSummary,
2701
+ calculateModelCost,
2702
+ createModelRegistry,
2703
+ index_default as default,
2704
+ deriveSessionId,
2705
+ emitRouteTrace,
2706
+ filterByExcludeList,
2707
+ generateOpenClawModels,
2708
+ getFallbackChain,
2709
+ getPromptPreview,
2710
+ getStrategy,
2711
+ hashRequestContent,
2712
+ injectLlmRouterModelsConfig,
2713
+ loadConfig,
2714
+ localProviderBaseUrl,
2715
+ normalizeTraceMode,
2716
+ registerOpenClawPlugin,
2717
+ registerStrategy,
2718
+ resolveConfig,
2719
+ resolvePublicModel,
2720
+ resolvePublicModelCandidate,
2721
+ resolveTraceWriter,
2722
+ route,
2723
+ selectModel,
2724
+ startProxy
2725
+ };
2726
+ //# sourceMappingURL=index.js.map