@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanglvm/llm-router",
3
- "version": "2.3.2",
3
+ "version": "2.3.4",
4
4
  "description": "LLM Router: single gateway endpoint for multi-provider LLMs with unified OpenAI+Anthropic format and seamless fallback",
5
5
  "keywords": [
6
6
  "llm-router",
@@ -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
  }