@khanglvm/llm-router 2.3.2 → 2.3.4
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/CHANGELOG.md +10 -0
- package/package.json +1 -1
- package/src/runtime/handler/provider-call.js +91 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.3.4] - 2026-04-17
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Updated the live provider suite to exercise RamCloud with `minimax-m2.7` only and switched the Claude Code live alias from `normal` to `default`, matching the generated router config so real-provider publish checks pass again.
|
|
14
|
+
|
|
15
|
+
## [2.3.3] - 2026-04-17
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Prevented repeated failed OpenAI `/v1/chat/completions` tool-routing attempts for Claude Code requests on dual-format Claude routes by respecting model format preferences and suppressing noisy re-tries after a successful Claude fallback.
|
|
19
|
+
|
|
10
20
|
## [2.3.2] - 2026-04-17
|
|
11
21
|
|
|
12
22
|
### Fixed
|
package/package.json
CHANGED
|
@@ -43,10 +43,87 @@ import {
|
|
|
43
43
|
resolveLargeRequestLogThresholdBytes
|
|
44
44
|
} from "./large-request-log.js";
|
|
45
45
|
|
|
46
|
+
const OPENAI_TOOL_ROUTING_SUPPRESSION_TTL_MS = 30 * 60 * 1000;
|
|
47
|
+
const openAIToolRoutingSuppressionUntil = new Map();
|
|
48
|
+
|
|
46
49
|
function isSubscriptionProvider(provider) {
|
|
47
50
|
return provider?.type === "subscription";
|
|
48
51
|
}
|
|
49
52
|
|
|
53
|
+
function normalizeFormatList(values) {
|
|
54
|
+
return [...new Set(
|
|
55
|
+
(Array.isArray(values) ? values : [values])
|
|
56
|
+
.map((value) => String(value || "").trim())
|
|
57
|
+
.filter((value) => value === FORMATS.OPENAI || value === FORMATS.CLAUDE)
|
|
58
|
+
)];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resolveCandidateModel(provider, model, modelId) {
|
|
62
|
+
if (model && typeof model === "object" && !Array.isArray(model)) {
|
|
63
|
+
return model;
|
|
64
|
+
}
|
|
65
|
+
const normalizedModelId = String(modelId || "").trim();
|
|
66
|
+
if (!normalizedModelId || !Array.isArray(provider?.models)) return null;
|
|
67
|
+
return provider.models.find((entry) => String(entry?.id || "").trim() === normalizedModelId) || null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getProviderModelSupportedFormats(provider, model, modelId) {
|
|
71
|
+
const resolvedModel = resolveCandidateModel(provider, model, modelId);
|
|
72
|
+
const configuredFormats = normalizeFormatList(resolvedModel?.formats || resolvedModel?.format);
|
|
73
|
+
const resolvedModelId = String(resolvedModel?.id || modelId || "").trim();
|
|
74
|
+
if (!resolvedModelId) return configuredFormats;
|
|
75
|
+
|
|
76
|
+
const preferredFormat = provider?.lastProbe?.modelPreferredFormat?.[resolvedModelId];
|
|
77
|
+
if (preferredFormat === FORMATS.OPENAI || preferredFormat === FORMATS.CLAUDE) {
|
|
78
|
+
return [preferredFormat];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const probedFormats = normalizeFormatList(provider?.lastProbe?.modelSupport?.[resolvedModelId]);
|
|
82
|
+
return probedFormats.length > 0 ? probedFormats : configuredFormats;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getProviderModelPreferredFormat(provider, model, modelId) {
|
|
86
|
+
const resolvedModel = resolveCandidateModel(provider, model, modelId);
|
|
87
|
+
const resolvedModelId = String(resolvedModel?.id || modelId || "").trim();
|
|
88
|
+
if (!resolvedModelId) return "";
|
|
89
|
+
const preferredFormat = String(provider?.lastProbe?.modelPreferredFormat?.[resolvedModelId] || "").trim();
|
|
90
|
+
return preferredFormat === FORMATS.OPENAI || preferredFormat === FORMATS.CLAUDE
|
|
91
|
+
? preferredFormat
|
|
92
|
+
: "";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function buildOpenAIToolRoutingSuppressionKey(candidate) {
|
|
96
|
+
const providerId = String(candidate?.providerId || candidate?.provider?.id || "").trim();
|
|
97
|
+
const modelId = String(candidate?.modelId || candidate?.model?.id || candidate?.backend || "").trim();
|
|
98
|
+
if (!providerId || !modelId) return "";
|
|
99
|
+
return `${providerId}/${modelId}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function pruneOpenAIToolRoutingSuppressions(now = Date.now()) {
|
|
103
|
+
for (const [key, expiresAt] of openAIToolRoutingSuppressionUntil.entries()) {
|
|
104
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= now) {
|
|
105
|
+
openAIToolRoutingSuppressionUntil.delete(key);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isOpenAIToolRoutingSuppressed(candidate, now = Date.now()) {
|
|
111
|
+
const key = buildOpenAIToolRoutingSuppressionKey(candidate);
|
|
112
|
+
if (!key) return false;
|
|
113
|
+
pruneOpenAIToolRoutingSuppressions(now);
|
|
114
|
+
return Number(openAIToolRoutingSuppressionUntil.get(key)) > now;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function suppressOpenAIToolRouting(candidate, now = Date.now()) {
|
|
118
|
+
const key = buildOpenAIToolRoutingSuppressionKey(candidate);
|
|
119
|
+
if (!key) return;
|
|
120
|
+
openAIToolRoutingSuppressionUntil.set(key, now + OPENAI_TOOL_ROUTING_SUPPRESSION_TTL_MS);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function resetOpenAIToolRoutingLearningState() {
|
|
124
|
+
openAIToolRoutingSuppressionUntil.clear();
|
|
125
|
+
}
|
|
126
|
+
|
|
50
127
|
function queueLargeRequestEvent(onLargeRequestLog, payload) {
|
|
51
128
|
if (typeof onLargeRequestLog !== "function") return;
|
|
52
129
|
try {
|
|
@@ -313,6 +390,9 @@ function normalizeProviderRequestKind(targetFormat, requestKind) {
|
|
|
313
390
|
|
|
314
391
|
function shouldPreferOpenAIForClaudeToolCalls({
|
|
315
392
|
provider,
|
|
393
|
+
model,
|
|
394
|
+
modelId,
|
|
395
|
+
candidate,
|
|
316
396
|
sourceFormat,
|
|
317
397
|
targetFormat,
|
|
318
398
|
requestKind,
|
|
@@ -320,6 +400,11 @@ function shouldPreferOpenAIForClaudeToolCalls({
|
|
|
320
400
|
} = {}) {
|
|
321
401
|
if (sourceFormat !== FORMATS.CLAUDE || targetFormat !== FORMATS.CLAUDE) return false;
|
|
322
402
|
if (!hasToolDefinitions(body)) return false;
|
|
403
|
+
if (candidate && isOpenAIToolRoutingSuppressed(candidate)) return false;
|
|
404
|
+
const preferredFormat = getProviderModelPreferredFormat(provider, model, modelId);
|
|
405
|
+
if (preferredFormat === FORMATS.CLAUDE) return false;
|
|
406
|
+
const modelFormats = getProviderModelSupportedFormats(provider, model, modelId);
|
|
407
|
+
if (modelFormats.length > 0 && !modelFormats.includes(FORMATS.OPENAI)) return false;
|
|
323
408
|
if (!getProviderFormats(provider).includes(FORMATS.OPENAI)) return false;
|
|
324
409
|
return Boolean(resolveProviderUrl(provider, FORMATS.OPENAI, normalizeProviderRequestKind(FORMATS.OPENAI, requestKind)));
|
|
325
410
|
}
|
|
@@ -664,6 +749,9 @@ export async function makeProviderCall({
|
|
|
664
749
|
|
|
665
750
|
const preferOpenAIToolRouting = !isSubscriptionProvider(provider) && shouldPreferOpenAIForClaudeToolCalls({
|
|
666
751
|
provider,
|
|
752
|
+
model: candidate?.model,
|
|
753
|
+
modelId: candidate?.modelId,
|
|
754
|
+
candidate,
|
|
667
755
|
sourceFormat,
|
|
668
756
|
targetFormat,
|
|
669
757
|
requestKind,
|
|
@@ -1064,6 +1152,9 @@ export async function makeProviderCall({
|
|
|
1064
1152
|
try {
|
|
1065
1153
|
const fallbackResponse = await executeHttpProviderRequest(fallbackPlan);
|
|
1066
1154
|
if (fallbackResponse instanceof Response && fallbackResponse.ok) {
|
|
1155
|
+
if (preferOpenAIToolRouting) {
|
|
1156
|
+
suppressOpenAIToolRouting(candidate);
|
|
1157
|
+
}
|
|
1067
1158
|
response = fallbackResponse;
|
|
1068
1159
|
activePlan = fallbackPlan;
|
|
1069
1160
|
}
|