omniroute 2.8.0 → 2.8.1

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.
Files changed (119) hide show
  1. package/app/.next/BUILD_ID +1 -1
  2. package/app/.next/build-manifest.json +2 -2
  3. package/app/.next/prerender-manifest.json +3 -3
  4. package/app/.next/server/app/(dashboard)/dashboard/a2a/page_client-reference-manifest.js +1 -1
  5. package/app/.next/server/app/(dashboard)/dashboard/agents/page_client-reference-manifest.js +1 -1
  6. package/app/.next/server/app/(dashboard)/dashboard/analytics/page_client-reference-manifest.js +1 -1
  7. package/app/.next/server/app/(dashboard)/dashboard/api-manager/page_client-reference-manifest.js +1 -1
  8. package/app/.next/server/app/(dashboard)/dashboard/audit-log/page_client-reference-manifest.js +1 -1
  9. package/app/.next/server/app/(dashboard)/dashboard/auto-combo/page_client-reference-manifest.js +1 -1
  10. package/app/.next/server/app/(dashboard)/dashboard/cli-tools/page_client-reference-manifest.js +1 -1
  11. package/app/.next/server/app/(dashboard)/dashboard/combos/page_client-reference-manifest.js +1 -1
  12. package/app/.next/server/app/(dashboard)/dashboard/costs/page_client-reference-manifest.js +1 -1
  13. package/app/.next/server/app/(dashboard)/dashboard/endpoint/page_client-reference-manifest.js +1 -1
  14. package/app/.next/server/app/(dashboard)/dashboard/health/page_client-reference-manifest.js +1 -1
  15. package/app/.next/server/app/(dashboard)/dashboard/limits/page_client-reference-manifest.js +1 -1
  16. package/app/.next/server/app/(dashboard)/dashboard/logs/page_client-reference-manifest.js +1 -1
  17. package/app/.next/server/app/(dashboard)/dashboard/mcp/page_client-reference-manifest.js +1 -1
  18. package/app/.next/server/app/(dashboard)/dashboard/media/page_client-reference-manifest.js +1 -1
  19. package/app/.next/server/app/(dashboard)/dashboard/onboarding/page_client-reference-manifest.js +1 -1
  20. package/app/.next/server/app/(dashboard)/dashboard/page_client-reference-manifest.js +1 -1
  21. package/app/.next/server/app/(dashboard)/dashboard/playground/page_client-reference-manifest.js +1 -1
  22. package/app/.next/server/app/(dashboard)/dashboard/profile/page_client-reference-manifest.js +1 -1
  23. package/app/.next/server/app/(dashboard)/dashboard/providers/[id]/page_client-reference-manifest.js +1 -1
  24. package/app/.next/server/app/(dashboard)/dashboard/providers/new/page_client-reference-manifest.js +1 -1
  25. package/app/.next/server/app/(dashboard)/dashboard/providers/page_client-reference-manifest.js +1 -1
  26. package/app/.next/server/app/(dashboard)/dashboard/search-tools/page_client-reference-manifest.js +1 -1
  27. package/app/.next/server/app/(dashboard)/dashboard/settings/page_client-reference-manifest.js +1 -1
  28. package/app/.next/server/app/(dashboard)/dashboard/settings/pricing/page_client-reference-manifest.js +1 -1
  29. package/app/.next/server/app/(dashboard)/dashboard/translator/page_client-reference-manifest.js +1 -1
  30. package/app/.next/server/app/(dashboard)/dashboard/usage/page_client-reference-manifest.js +1 -1
  31. package/app/.next/server/app/400/page_client-reference-manifest.js +1 -1
  32. package/app/.next/server/app/401/page_client-reference-manifest.js +1 -1
  33. package/app/.next/server/app/403/page_client-reference-manifest.js +1 -1
  34. package/app/.next/server/app/408/page_client-reference-manifest.js +1 -1
  35. package/app/.next/server/app/429/page_client-reference-manifest.js +1 -1
  36. package/app/.next/server/app/500/page_client-reference-manifest.js +1 -1
  37. package/app/.next/server/app/502/page_client-reference-manifest.js +1 -1
  38. package/app/.next/server/app/503/page_client-reference-manifest.js +1 -1
  39. package/app/.next/server/app/_global-error.html +2 -2
  40. package/app/.next/server/app/_global-error.rsc +1 -1
  41. package/app/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  42. package/app/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  43. package/app/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  44. package/app/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  45. package/app/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  46. package/app/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  47. package/app/.next/server/app/callback/page_client-reference-manifest.js +1 -1
  48. package/app/.next/server/app/docs/page_client-reference-manifest.js +1 -1
  49. package/app/.next/server/app/forbidden/page_client-reference-manifest.js +1 -1
  50. package/app/.next/server/app/forgot-password/page_client-reference-manifest.js +1 -1
  51. package/app/.next/server/app/landing/page_client-reference-manifest.js +1 -1
  52. package/app/.next/server/app/login/page_client-reference-manifest.js +1 -1
  53. package/app/.next/server/app/maintenance/page_client-reference-manifest.js +1 -1
  54. package/app/.next/server/app/offline/page_client-reference-manifest.js +1 -1
  55. package/app/.next/server/app/page_client-reference-manifest.js +1 -1
  56. package/app/.next/server/app/privacy/page_client-reference-manifest.js +1 -1
  57. package/app/.next/server/app/status/page_client-reference-manifest.js +1 -1
  58. package/app/.next/server/app/terms/page_client-reference-manifest.js +1 -1
  59. package/app/.next/server/chunks/[root-of-the-server]__09c944b3._.js +1 -1
  60. package/app/.next/server/chunks/[root-of-the-server]__167585da._.js +1 -1
  61. package/app/.next/server/chunks/[root-of-the-server]__3972de72._.js +5 -5
  62. package/app/.next/server/chunks/[root-of-the-server]__64bd5d97._.js +1 -1
  63. package/app/.next/server/chunks/[root-of-the-server]__784fb7c5._.js +1 -1
  64. package/app/.next/server/chunks/[root-of-the-server]__7d9b23e7._.js +1 -1
  65. package/app/.next/server/chunks/[root-of-the-server]__80e3bfc3._.js +1 -1
  66. package/app/.next/server/chunks/[root-of-the-server]__84e445b2._.js +2 -2
  67. package/app/.next/server/chunks/[root-of-the-server]__92cb0def._.js +2 -2
  68. package/app/.next/server/chunks/[root-of-the-server]__a630d6ef._.js +5 -5
  69. package/app/.next/server/chunks/[root-of-the-server]__cb8a67d1._.js +1 -1
  70. package/app/.next/server/chunks/[root-of-the-server]__db2f9fe0._.js +1 -1
  71. package/app/.next/server/chunks/[root-of-the-server]__dc47ee64._.js +1 -1
  72. package/app/.next/server/chunks/[root-of-the-server]__f0f9eb3f._.js +2 -2
  73. package/app/.next/server/chunks/_05c48915._.js +1 -1
  74. package/app/.next/server/chunks/_1244636c._.js +5 -5
  75. package/app/.next/server/chunks/_2115d8de._.js +1 -1
  76. package/app/.next/server/chunks/_3ac953eb._.js +1 -1
  77. package/app/.next/server/chunks/_4b8fd853._.js +1 -1
  78. package/app/.next/server/chunks/_68683848._.js +1 -1
  79. package/app/.next/server/chunks/_ee9b677b._.js +1 -1
  80. package/app/.next/server/chunks/open-sse_translator_index_ts_f5fd0821._.js +9 -9
  81. package/app/.next/server/chunks/src_lib_a886ee1f._.js +1 -1
  82. package/app/.next/server/chunks/src_shared_validation_schemas_ts_4e63863a._.js +1 -1
  83. package/app/.next/server/chunks/ssr/[root-of-the-server]__9affb65e._.js +1 -1
  84. package/app/.next/server/chunks/ssr/[root-of-the-server]__a6942102._.js +1 -1
  85. package/app/.next/server/chunks/ssr/_5a9cd299._.js +1 -1
  86. package/app/.next/server/chunks/ssr/src_d3225e36._.js +1 -1
  87. package/app/.next/server/chunks/ssr/src_i18n_messages_zh-CN_json_f4112d90._.js +1 -1
  88. package/app/.next/server/pages/500.html +2 -2
  89. package/app/.next/server/server-reference-manifest.js +1 -1
  90. package/app/.next/server/server-reference-manifest.json +1 -1
  91. package/app/.next/static/chunks/02afac1b585d8219.css +1 -0
  92. package/app/.next/static/chunks/{d3cb88d181c61235.js → 4bed5b394f9dece9.js} +1 -1
  93. package/app/.next/static/chunks/{4f099d0f5b0f00d6.js → b5cc103e16794392.js} +1 -1
  94. package/app/.next/static/chunks/d19ab4efcaddd1db.js +1 -0
  95. package/app/CHANGELOG.md +20 -0
  96. package/app/docs/openapi.yaml +1 -1
  97. package/app/open-sse/executors/kiro.ts +5 -2
  98. package/app/open-sse/handlers/chatCore.ts +11 -3
  99. package/app/open-sse/translator/helpers/toolCallHelper.ts +58 -15
  100. package/app/open-sse/translator/index.ts +10 -3
  101. package/app/open-sse/utils/stream.ts +129 -19
  102. package/app/open-sse/utils/usageTracking.ts +3 -1
  103. package/app/package-lock.json +2 -2
  104. package/app/package.json +1 -1
  105. package/app/src/app/(dashboard)/dashboard/providers/[id]/page.tsx +22 -0
  106. package/app/src/app/api/keys/[id]/route.ts +26 -4
  107. package/app/src/app/api/provider-models/route.ts +3 -1
  108. package/app/src/i18n/messages/zh-CN.json +862 -787
  109. package/app/src/lib/db/models.ts +25 -0
  110. package/app/src/lib/usage/callLogs.ts +6 -2
  111. package/app/src/shared/components/RequestLoggerDetail.tsx +10 -10
  112. package/app/src/shared/validation/schemas.ts +1 -0
  113. package/app/src/sse/handlers/chat.ts +8 -3
  114. package/package.json +1 -1
  115. package/app/.next/static/chunks/194dbd564ba7150b.css +0 -1
  116. package/app/.next/static/chunks/3f3d822837a5ff5f.js +0 -1
  117. /package/app/.next/static/{Y1ARupD8_nbYoMt6WYrBO → JoLoKHY_9DVANe7Qs4D0K}/_buildManifest.js +0 -0
  118. /package/app/.next/static/{Y1ARupD8_nbYoMt6WYrBO → JoLoKHY_9DVANe7Qs4D0K}/_clientMiddlewareManifest.json +0 -0
  119. /package/app/.next/static/{Y1ARupD8_nbYoMt6WYrBO → JoLoKHY_9DVANe7Qs4D0K}/_ssgManifest.js +0 -0
@@ -30,6 +30,8 @@ type StreamLogger = {
30
30
  type StreamCompletePayload = {
31
31
  status: number;
32
32
  usage: unknown;
33
+ /** Minimal response body for call log (streaming: usage + note; non-streaming not used) */
34
+ responseBody?: unknown;
33
35
  };
34
36
 
35
37
  type StreamOptions = {
@@ -51,6 +53,8 @@ type TranslateState = ReturnType<typeof initState> & {
51
53
  toolNameMap?: unknown;
52
54
  usage?: unknown;
53
55
  finishReason?: unknown;
56
+ /** Accumulated message content for call log response body */
57
+ accumulatedContent?: string;
54
58
  };
55
59
 
56
60
  function getOpenAIIntermediateChunks(value: unknown): unknown[] {
@@ -106,14 +110,21 @@ export function createSSEStream(options: StreamOptions = {}) {
106
110
  let buffer = "";
107
111
  let usage = null;
108
112
 
109
- // State for translate mode
113
+ // State for translate mode (accumulatedContent for call log response body)
110
114
  const state: TranslateState | null =
111
115
  mode === STREAM_MODE.TRANSLATE
112
- ? { ...(initState(sourceFormat) as TranslateState), provider, toolNameMap }
116
+ ? {
117
+ ...(initState(sourceFormat) as TranslateState),
118
+ provider,
119
+ toolNameMap,
120
+ accumulatedContent: "",
121
+ }
113
122
  : null;
114
123
 
115
124
  // Track content length for usage estimation (both modes)
116
125
  let totalContentLength = 0;
126
+ // Passthrough: accumulate content for call log response body
127
+ let passthroughAccumulatedContent = "";
117
128
 
118
129
  // Guard against duplicate [DONE] events — ensures exactly one per stream
119
130
  let doneSent = false;
@@ -201,9 +212,10 @@ export function createSSEStream(options: StreamOptions = {}) {
201
212
  if (extracted) {
202
213
  usage = extracted;
203
214
  }
204
- // Track content length from Responses format
215
+ // Track content length and accumulate for call log
205
216
  if (parsed.delta && typeof parsed.delta === "string") {
206
217
  totalContentLength += parsed.delta.length;
218
+ passthroughAccumulatedContent += parsed.delta;
207
219
  }
208
220
  } else if (isClaudeSSE) {
209
221
  // Claude SSE: extract usage, track content, forward as-is
@@ -213,14 +225,23 @@ export function createSSEStream(options: StreamOptions = {}) {
213
225
  // message_start carries input_tokens, message_delta carries output_tokens
214
226
  if (!usage) usage = {};
215
227
  if (extracted.prompt_tokens > 0) usage.prompt_tokens = extracted.prompt_tokens;
216
- if (extracted.completion_tokens > 0) usage.completion_tokens = extracted.completion_tokens;
228
+ if (extracted.completion_tokens > 0)
229
+ usage.completion_tokens = extracted.completion_tokens;
217
230
  if (extracted.total_tokens > 0) usage.total_tokens = extracted.total_tokens;
218
- if (extracted.cache_read_input_tokens) usage.cache_read_input_tokens = extracted.cache_read_input_tokens;
219
- if (extracted.cache_creation_input_tokens) usage.cache_creation_input_tokens = extracted.cache_creation_input_tokens;
231
+ if (extracted.cache_read_input_tokens)
232
+ usage.cache_read_input_tokens = extracted.cache_read_input_tokens;
233
+ if (extracted.cache_creation_input_tokens)
234
+ usage.cache_creation_input_tokens = extracted.cache_creation_input_tokens;
235
+ }
236
+ // Track content length and accumulate from Claude format
237
+ if (parsed.delta?.text) {
238
+ totalContentLength += parsed.delta.text.length;
239
+ passthroughAccumulatedContent += parsed.delta.text;
240
+ }
241
+ if (parsed.delta?.thinking) {
242
+ totalContentLength += parsed.delta.thinking.length;
243
+ passthroughAccumulatedContent += parsed.delta.thinking;
220
244
  }
221
- // Track content length from Claude format
222
- if (parsed.delta?.text) totalContentLength += parsed.delta.text.length;
223
- if (parsed.delta?.thinking) totalContentLength += parsed.delta.thinking.length;
224
245
  } else {
225
246
  // Chat Completions: full sanitization pipeline
226
247
  parsed = sanitizeStreamingChunk(parsed);
@@ -246,6 +267,10 @@ export function createSSEStream(options: StreamOptions = {}) {
246
267
  if (content && typeof content === "string") {
247
268
  totalContentLength += content.length;
248
269
  }
270
+ if (typeof delta?.content === "string")
271
+ passthroughAccumulatedContent += delta.content;
272
+ if (typeof delta?.reasoning_content === "string")
273
+ passthroughAccumulatedContent += delta.reasoning_content;
249
274
 
250
275
  const extracted = extractUsage(parsed);
251
276
  if (extracted) {
@@ -301,23 +326,45 @@ export function createSSEStream(options: StreamOptions = {}) {
301
326
  continue;
302
327
  }
303
328
 
304
- // Track content length for estimation (from various formats)
305
- // Include both regular content and reasoning/thinking content
329
+ // Track content length and accumulate for call log (from raw provider chunk, so content is never missed)
330
+ // Do this before translation so we capture content regardless of translator output shape
306
331
 
307
332
  // Claude format
308
333
  if (parsed.delta?.text) {
309
- totalContentLength += parsed.delta.text.length;
334
+ const t = parsed.delta.text;
335
+ totalContentLength += t.length;
336
+ if (state?.accumulatedContent !== undefined && typeof t === "string")
337
+ state.accumulatedContent += t;
310
338
  }
311
339
  if (parsed.delta?.thinking) {
312
- totalContentLength += parsed.delta.thinking.length;
340
+ const t = parsed.delta.thinking;
341
+ totalContentLength += t.length;
342
+ if (state?.accumulatedContent !== undefined && typeof t === "string")
343
+ state.accumulatedContent += t;
313
344
  }
314
345
 
315
346
  // OpenAI format
316
347
  if (parsed.choices?.[0]?.delta?.content) {
317
- totalContentLength += parsed.choices[0].delta.content.length;
348
+ const c = parsed.choices[0].delta.content;
349
+ if (typeof c === "string") {
350
+ totalContentLength += c.length;
351
+ if (state?.accumulatedContent !== undefined) state.accumulatedContent += c;
352
+ } else if (Array.isArray(c)) {
353
+ for (const part of c) {
354
+ if (part?.text && typeof part.text === "string") {
355
+ totalContentLength += part.text.length;
356
+ if (state?.accumulatedContent !== undefined)
357
+ state.accumulatedContent += part.text;
358
+ }
359
+ }
360
+ }
318
361
  }
319
362
  if (parsed.choices?.[0]?.delta?.reasoning_content) {
320
- totalContentLength += parsed.choices[0].delta.reasoning_content.length;
363
+ const r = parsed.choices[0].delta.reasoning_content;
364
+ if (typeof r === "string") {
365
+ totalContentLength += r.length;
366
+ if (state?.accumulatedContent !== undefined) state.accumulatedContent += r;
367
+ }
321
368
  }
322
369
 
323
370
  // Gemini format - may have multiple parts
@@ -325,10 +372,30 @@ export function createSSEStream(options: StreamOptions = {}) {
325
372
  for (const part of parsed.candidates[0].content.parts) {
326
373
  if (part.text && typeof part.text === "string") {
327
374
  totalContentLength += part.text.length;
375
+ if (state?.accumulatedContent !== undefined) state.accumulatedContent += part.text;
328
376
  }
329
377
  }
330
378
  }
331
379
 
380
+ // Generic fallback: delta string, top-level content/text (e.g. some SSE payloads)
381
+ if (state?.accumulatedContent !== undefined) {
382
+ if (typeof (parsed as JsonRecord).delta === "string") {
383
+ const d = (parsed as JsonRecord).delta as string;
384
+ state.accumulatedContent += d;
385
+ totalContentLength += d.length;
386
+ }
387
+ if (typeof (parsed as JsonRecord).content === "string") {
388
+ const c = (parsed as JsonRecord).content as string;
389
+ state.accumulatedContent += c;
390
+ totalContentLength += c.length;
391
+ }
392
+ if (typeof (parsed as JsonRecord).text === "string") {
393
+ const t = (parsed as JsonRecord).text as string;
394
+ state.accumulatedContent += t;
395
+ totalContentLength += t.length;
396
+ }
397
+ }
398
+
332
399
  // Extract usage
333
400
  const extracted = extractUsage(parsed);
334
401
  if (extracted) state.usage = extracted; // Keep original usage for logging
@@ -344,6 +411,9 @@ export function createSSEStream(options: StreamOptions = {}) {
344
411
 
345
412
  if (translated?.length > 0) {
346
413
  for (const item of translated) {
414
+ // Content for call log is accumulated only from parsed (above) to avoid double-counting;
415
+ // do not add again from item here.
416
+
347
417
  // Filter empty chunks
348
418
  if (!hasValuableContent(item, sourceFormat)) {
349
419
  continue; // Skip this empty chunk
@@ -415,10 +485,30 @@ export function createSSEStream(options: StreamOptions = {}) {
415
485
  status: "200 OK",
416
486
  }).catch(() => {});
417
487
  }
418
- // Notify caller for call log persistence
488
+ // Notify caller for call log persistence (include full response body with accumulated content)
419
489
  if (onComplete) {
420
490
  try {
421
- onComplete({ status: 200, usage });
491
+ const u = usage as Record<string, unknown> | null;
492
+ const prompt = Number(u?.prompt_tokens ?? u?.input_tokens ?? 0);
493
+ const completion = Number(u?.completion_tokens ?? u?.output_tokens ?? 0);
494
+ const content = passthroughAccumulatedContent.trim() || "";
495
+ const responseBody = {
496
+ choices: [
497
+ {
498
+ message: {
499
+ role: "assistant",
500
+ content,
501
+ },
502
+ },
503
+ ],
504
+ usage: {
505
+ prompt_tokens: prompt,
506
+ completion_tokens: completion,
507
+ total_tokens: prompt + completion,
508
+ },
509
+ _streamed: true,
510
+ };
511
+ onComplete({ status: 200, usage, responseBody });
422
512
  } catch {}
423
513
  }
424
514
  return;
@@ -497,10 +587,30 @@ export function createSSEStream(options: StreamOptions = {}) {
497
587
  status: "200 OK",
498
588
  }).catch(() => {});
499
589
  }
500
- // Notify caller for call log persistence
590
+ // Notify caller for call log persistence (include full response body with accumulated content)
501
591
  if (onComplete) {
502
592
  try {
503
- onComplete({ status: 200, usage: state?.usage });
593
+ const u = state?.usage as Record<string, unknown> | null | undefined;
594
+ const prompt = Number(u?.prompt_tokens ?? u?.input_tokens ?? 0);
595
+ const completion = Number(u?.completion_tokens ?? u?.output_tokens ?? 0);
596
+ const content = (state?.accumulatedContent ?? "").trim() || "";
597
+ const responseBody = {
598
+ choices: [
599
+ {
600
+ message: {
601
+ role: "assistant",
602
+ content,
603
+ },
604
+ },
605
+ ],
606
+ usage: {
607
+ prompt_tokens: prompt,
608
+ completion_tokens: completion,
609
+ total_tokens: prompt + completion,
610
+ },
611
+ _streamed: true,
612
+ };
613
+ onComplete({ status: 200, usage: state?.usage, responseBody });
504
614
  } catch {}
505
615
  }
506
616
  } catch (error) {
@@ -400,8 +400,10 @@ export function logUsage(provider, usage, model = null, connectionId = null, api
400
400
  console.log(msg);
401
401
 
402
402
  // Save to usage DB
403
+ // input = total input tokens (non-cached + cache_read + cache_creation)
404
+ // This ensures analytics show correct totals for heavily-cached requests
403
405
  const tokens = {
404
- input: inTokens,
406
+ input: inTokens + (cacheRead || 0) + (cacheCreation || 0),
405
407
  output: outTokens,
406
408
  cacheRead: cacheRead || 0,
407
409
  cacheCreation: cacheCreation || 0,
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "omniroute",
3
- "version": "2.8.0",
3
+ "version": "2.8.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "omniroute",
9
- "version": "2.8.0",
9
+ "version": "2.8.1",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "workspaces": [
package/app/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omniroute",
3
- "version": "2.8.0",
3
+ "version": "2.8.1",
4
4
  "description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1477,6 +1477,7 @@ function CustomModelsSection({ providerId, providerAlias, copied, onCopy }) {
1477
1477
  const [editingModelId, setEditingModelId] = useState<string | null>(null);
1478
1478
  const [editingApiFormat, setEditingApiFormat] = useState("chat-completions");
1479
1479
  const [editingEndpoints, setEditingEndpoints] = useState<string[]>(["chat"]);
1480
+ const [editingNormalizeToolCallId, setEditingNormalizeToolCallId] = useState(false);
1480
1481
  const [savingModelId, setSavingModelId] = useState<string | null>(null);
1481
1482
 
1482
1483
  const fetchCustomModels = useCallback(async () => {
@@ -1548,12 +1549,14 @@ function CustomModelsSection({ providerId, providerAlias, copied, onCopy }) {
1548
1549
  ? model.supportedEndpoints
1549
1550
  : ["chat"]
1550
1551
  );
1552
+ setEditingNormalizeToolCallId(Boolean(model.normalizeToolCallId));
1551
1553
  };
1552
1554
 
1553
1555
  const cancelEdit = () => {
1554
1556
  setEditingModelId(null);
1555
1557
  setEditingApiFormat("chat-completions");
1556
1558
  setEditingEndpoints(["chat"]);
1559
+ setEditingNormalizeToolCallId(false);
1557
1560
  setSavingModelId(null);
1558
1561
  };
1559
1562
 
@@ -1577,6 +1580,7 @@ function CustomModelsSection({ providerId, providerAlias, copied, onCopy }) {
1577
1580
  source: model?.source || "manual",
1578
1581
  apiFormat: editingApiFormat,
1579
1582
  supportedEndpoints: editingEndpoints,
1583
+ normalizeToolCallId: editingNormalizeToolCallId,
1580
1584
  }),
1581
1585
  });
1582
1586
 
@@ -1738,6 +1742,14 @@ function CustomModelsSection({ providerId, providerAlias, copied, onCopy }) {
1738
1742
  🔊 Audio
1739
1743
  </span>
1740
1744
  )}
1745
+ {model.normalizeToolCallId && (
1746
+ <span
1747
+ className="text-[10px] px-1.5 py-0.5 rounded-full bg-slate-500/15 text-slate-400 font-medium"
1748
+ title="9-char tool call ID (Mistral)"
1749
+ >
1750
+ ID×9
1751
+ </span>
1752
+ )}
1741
1753
  </div>
1742
1754
 
1743
1755
  {editingModelId === model.id && (
@@ -1790,6 +1802,16 @@ function CustomModelsSection({ providerId, providerAlias, copied, onCopy }) {
1790
1802
  ))}
1791
1803
  </div>
1792
1804
  </div>
1805
+
1806
+ <label className="flex items-center gap-2 text-xs text-text-main cursor-pointer">
1807
+ <input
1808
+ type="checkbox"
1809
+ checked={editingNormalizeToolCallId}
1810
+ onChange={(e) => setEditingNormalizeToolCallId(e.target.checked)}
1811
+ className="rounded border-border"
1812
+ />
1813
+ Normalize Tool Call ID (9 chars, Mistral)
1814
+ </label>
1793
1815
  </div>
1794
1816
  <div className="mt-3 flex items-center gap-2">
1795
1817
  <Button
@@ -55,9 +55,26 @@ export async function PATCH(request, { params }) {
55
55
  if (isValidationFailure(validation)) {
56
56
  return NextResponse.json({ error: validation.error }, { status: 400 });
57
57
  }
58
- const { allowedModels, noLog } = validation.data;
58
+ const {
59
+ name,
60
+ allowedModels,
61
+ allowedConnections,
62
+ noLog,
63
+ autoResolve,
64
+ isActive,
65
+ accessSchedule,
66
+ } = validation.data;
67
+
68
+ const payload: Parameters<typeof updateApiKeyPermissions>[1] = {};
69
+ if (name !== undefined) payload.name = name;
70
+ if (allowedModels !== undefined) payload.allowedModels = allowedModels;
71
+ if (allowedConnections !== undefined) payload.allowedConnections = allowedConnections;
72
+ if (noLog !== undefined) payload.noLog = noLog;
73
+ if (autoResolve !== undefined) payload.autoResolve = autoResolve;
74
+ if (isActive !== undefined) payload.isActive = isActive;
75
+ if (accessSchedule !== undefined) payload.accessSchedule = accessSchedule;
59
76
 
60
- const updated = await updateApiKeyPermissions(id, { allowedModels, noLog });
77
+ const updated = await updateApiKeyPermissions(id, payload);
61
78
  if (!updated) {
62
79
  return NextResponse.json({ error: "Key not found" }, { status: 404 });
63
80
  }
@@ -67,8 +84,13 @@ export async function PATCH(request, { params }) {
67
84
 
68
85
  return NextResponse.json({
69
86
  message: "API key settings updated successfully",
70
- allowedModels,
71
- noLog,
87
+ ...(name !== undefined && { name }),
88
+ ...(allowedModels !== undefined && { allowedModels }),
89
+ ...(allowedConnections !== undefined && { allowedConnections }),
90
+ ...(noLog !== undefined && { noLog }),
91
+ ...(autoResolve !== undefined && { autoResolve }),
92
+ ...(isActive !== undefined && { isActive }),
93
+ ...(accessSchedule !== undefined && { accessSchedule }),
72
94
  });
73
95
  } catch (error) {
74
96
  console.log("Error updating key permissions:", error);
@@ -113,12 +113,14 @@ export async function PUT(request) {
113
113
  return Response.json({ error: validation.error }, { status: 400 });
114
114
  }
115
115
 
116
- const { provider, modelId, modelName, apiFormat, supportedEndpoints } = validation.data;
116
+ const { provider, modelId, modelName, apiFormat, supportedEndpoints, normalizeToolCallId } =
117
+ validation.data;
117
118
 
118
119
  const model = await updateCustomModel(provider, modelId, {
119
120
  modelName,
120
121
  apiFormat,
121
122
  supportedEndpoints,
123
+ normalizeToolCallId,
122
124
  });
123
125
 
124
126
  if (!model) {