@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/LICENSE +21 -0
- package/README.md +160 -0
- package/dist/cli.d.ts +23 -0
- package/dist/cli.js +2490 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +370 -0
- package/dist/index.js +2726 -0
- package/dist/index.js.map +1 -0
- package/dist/proxy-CrRX9deF.d.ts +222 -0
- package/openclaw.plugin.json +37 -0
- package/package.json +63 -0
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
|