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