@amir83nasr/10router 0.1.14 → 0.1.15

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 (187) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +6 -6
  3. package/.next/build-manifest.json +2 -2
  4. package/.next/prerender-manifest.json +3 -3
  5. package/.next/server/app/(dashboard)/dashboard/cli-tools/[toolId]/page.js +2 -2
  6. package/.next/server/app/(dashboard)/dashboard/cli-tools/[toolId]/page_client-reference-manifest.js +1 -1
  7. package/.next/server/app/(dashboard)/dashboard/cli-tools/page_client-reference-manifest.js +1 -1
  8. package/.next/server/app/(dashboard)/dashboard/combos/page_client-reference-manifest.js +1 -1
  9. package/.next/server/app/(dashboard)/dashboard/console-log/page_client-reference-manifest.js +1 -1
  10. package/.next/server/app/(dashboard)/dashboard/endpoint/page_client-reference-manifest.js +1 -1
  11. package/.next/server/app/(dashboard)/dashboard/media-providers/systemone/[id]/page_client-reference-manifest.js +1 -1
  12. package/.next/server/app/(dashboard)/dashboard/media-providers/systemone/page_client-reference-manifest.js +1 -1
  13. package/.next/server/app/(dashboard)/dashboard/page_client-reference-manifest.js +1 -1
  14. package/.next/server/app/(dashboard)/dashboard/profile/page_client-reference-manifest.js +1 -1
  15. package/.next/server/app/(dashboard)/dashboard/providers/[id]/page_client-reference-manifest.js +1 -1
  16. package/.next/server/app/(dashboard)/dashboard/providers/new/page_client-reference-manifest.js +1 -1
  17. package/.next/server/app/(dashboard)/dashboard/providers/page_client-reference-manifest.js +1 -1
  18. package/.next/server/app/(dashboard)/dashboard/proxy-pools/page_client-reference-manifest.js +1 -1
  19. package/.next/server/app/(dashboard)/dashboard/pxpipe/page_client-reference-manifest.js +1 -1
  20. package/.next/server/app/(dashboard)/dashboard/quota/page_client-reference-manifest.js +1 -1
  21. package/.next/server/app/(dashboard)/dashboard/token-saver/page_client-reference-manifest.js +1 -1
  22. package/.next/server/app/(dashboard)/dashboard/usage/page_client-reference-manifest.js +1 -1
  23. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  24. package/.next/server/app/_global-error.html +1 -1
  25. package/.next/server/app/_global-error.rsc +1 -1
  26. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  27. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  28. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  29. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  30. package/.next/server/app/_not-found.html +1 -1
  31. package/.next/server/app/_not-found.rsc +5 -5
  32. package/.next/server/app/_not-found.segments/_full.segment.rsc +5 -5
  33. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +5 -5
  34. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  35. package/.next/server/app/api/cli-tools/all-statuses/route.js +1 -1
  36. package/.next/server/app/api/cli-tools/claude-settings/route.js +2 -2
  37. package/.next/server/app/api/cli-tools/codex-settings/route.js +1 -1
  38. package/.next/server/app/api/cli-tools/cowork-mcp-registry/route.js +1 -1
  39. package/.next/server/app/api/cli-tools/cowork-mcp-tools/route.js +1 -1
  40. package/.next/server/app/api/cli-tools/cowork-settings/route.js +1 -1
  41. package/.next/server/app/api/cli-tools/dsh-settings/route.js +1 -1
  42. package/.next/server/app/api/cli-tools/opencode-settings/route.js +1 -1
  43. package/.next/server/app/api/oauth/[provider]/[action]/route.js +1 -1
  44. package/.next/server/app/api/oauth/codex/bulk-import/route.js +1 -1
  45. package/.next/server/app/api/oauth/codex/import-token/route.js +1 -1
  46. package/.next/server/app/api/providers/client/route.js +1 -1
  47. package/.next/server/app/api/translator/send/route.js +1 -1
  48. package/.next/server/app/api/translator/translate/route.js +1 -1
  49. package/.next/server/app/api/tunnel/tailscale-install/route.js +2 -2
  50. package/.next/server/app/api/usage/[connectionId]/codex-reset-credits/route.js +1 -1
  51. package/.next/server/app/api/usage/[connectionId]/route.js +1 -1
  52. package/.next/server/app/api/version/route.js +1 -1
  53. package/.next/server/app/callback/page_client-reference-manifest.js +1 -1
  54. package/.next/server/app/callback.html +1 -1
  55. package/.next/server/app/callback.rsc +6 -6
  56. package/.next/server/app/callback.segments/_full.segment.rsc +6 -6
  57. package/.next/server/app/callback.segments/_tree.segment.rsc +1 -1
  58. package/.next/server/app/callback.segments/callback/__PAGE__.segment.rsc +6 -6
  59. package/.next/server/app/dashboard/cli-tools.html +1 -1
  60. package/.next/server/app/dashboard/cli-tools.rsc +8 -8
  61. package/.next/server/app/dashboard/cli-tools.segments/!KGRhc2hib2FyZCk/dashboard/cli-tools/__PAGE__.segment.rsc +7 -7
  62. package/.next/server/app/dashboard/cli-tools.segments/_full.segment.rsc +8 -8
  63. package/.next/server/app/dashboard/cli-tools.segments/_tree.segment.rsc +1 -1
  64. package/.next/server/app/dashboard/combos.html +1 -1
  65. package/.next/server/app/dashboard/combos.rsc +7 -7
  66. package/.next/server/app/dashboard/combos.segments/!KGRhc2hib2FyZCk/dashboard/combos/__PAGE__.segment.rsc +7 -7
  67. package/.next/server/app/dashboard/combos.segments/_full.segment.rsc +7 -7
  68. package/.next/server/app/dashboard/combos.segments/_tree.segment.rsc +1 -1
  69. package/.next/server/app/dashboard/endpoint.html +1 -1
  70. package/.next/server/app/dashboard/endpoint.rsc +8 -8
  71. package/.next/server/app/dashboard/endpoint.segments/!KGRhc2hib2FyZCk/dashboard/endpoint/__PAGE__.segment.rsc +7 -7
  72. package/.next/server/app/dashboard/endpoint.segments/_full.segment.rsc +8 -8
  73. package/.next/server/app/dashboard/endpoint.segments/_tree.segment.rsc +1 -1
  74. package/.next/server/app/dashboard/media-providers/systemone.html +1 -1
  75. package/.next/server/app/dashboard/media-providers/systemone.rsc +7 -7
  76. package/.next/server/app/dashboard/media-providers/systemone.segments/!KGRhc2hib2FyZCk/dashboard/media-providers/systemone/__PAGE__.segment.rsc +7 -7
  77. package/.next/server/app/dashboard/media-providers/systemone.segments/_full.segment.rsc +7 -7
  78. package/.next/server/app/dashboard/media-providers/systemone.segments/_tree.segment.rsc +1 -1
  79. package/.next/server/app/dashboard/profile.html +1 -1
  80. package/.next/server/app/dashboard/profile.rsc +7 -7
  81. package/.next/server/app/dashboard/profile.segments/!KGRhc2hib2FyZCk/dashboard/profile/__PAGE__.segment.rsc +7 -7
  82. package/.next/server/app/dashboard/profile.segments/_full.segment.rsc +7 -7
  83. package/.next/server/app/dashboard/profile.segments/_tree.segment.rsc +1 -1
  84. package/.next/server/app/dashboard/providers/new.html +1 -1
  85. package/.next/server/app/dashboard/providers/new.rsc +7 -7
  86. package/.next/server/app/dashboard/providers/new.segments/!KGRhc2hib2FyZCk/dashboard/providers/new/__PAGE__.segment.rsc +7 -7
  87. package/.next/server/app/dashboard/providers/new.segments/_full.segment.rsc +7 -7
  88. package/.next/server/app/dashboard/providers/new.segments/_tree.segment.rsc +1 -1
  89. package/.next/server/app/dashboard/providers.html +1 -1
  90. package/.next/server/app/dashboard/providers.rsc +7 -7
  91. package/.next/server/app/dashboard/providers.segments/!KGRhc2hib2FyZCk/dashboard/providers/__PAGE__.segment.rsc +7 -7
  92. package/.next/server/app/dashboard/providers.segments/_full.segment.rsc +7 -7
  93. package/.next/server/app/dashboard/providers.segments/_tree.segment.rsc +1 -1
  94. package/.next/server/app/dashboard/proxy-pools.html +1 -1
  95. package/.next/server/app/dashboard/proxy-pools.rsc +7 -7
  96. package/.next/server/app/dashboard/proxy-pools.segments/!KGRhc2hib2FyZCk/dashboard/proxy-pools/__PAGE__.segment.rsc +7 -7
  97. package/.next/server/app/dashboard/proxy-pools.segments/_full.segment.rsc +7 -7
  98. package/.next/server/app/dashboard/proxy-pools.segments/_tree.segment.rsc +1 -1
  99. package/.next/server/app/dashboard/pxpipe.html +1 -1
  100. package/.next/server/app/dashboard/pxpipe.rsc +6 -6
  101. package/.next/server/app/dashboard/pxpipe.segments/!KGRhc2hib2FyZCk/dashboard/pxpipe/__PAGE__.segment.rsc +6 -6
  102. package/.next/server/app/dashboard/pxpipe.segments/_full.segment.rsc +6 -6
  103. package/.next/server/app/dashboard/pxpipe.segments/_tree.segment.rsc +1 -1
  104. package/.next/server/app/dashboard/quota.html +2 -2
  105. package/.next/server/app/dashboard/quota.rsc +8 -8
  106. package/.next/server/app/dashboard/quota.segments/!KGRhc2hib2FyZCk/dashboard/quota/__PAGE__.segment.rsc +8 -8
  107. package/.next/server/app/dashboard/quota.segments/_full.segment.rsc +8 -8
  108. package/.next/server/app/dashboard/quota.segments/_tree.segment.rsc +1 -1
  109. package/.next/server/app/dashboard/settings/pricing/page_client-reference-manifest.js +1 -1
  110. package/.next/server/app/dashboard/settings/pricing.html +1 -1
  111. package/.next/server/app/dashboard/settings/pricing.rsc +5 -5
  112. package/.next/server/app/dashboard/settings/pricing.segments/_full.segment.rsc +5 -5
  113. package/.next/server/app/dashboard/settings/pricing.segments/_tree.segment.rsc +1 -1
  114. package/.next/server/app/dashboard/settings/pricing.segments/dashboard/settings/pricing/__PAGE__.segment.rsc +5 -5
  115. package/.next/server/app/dashboard/token-saver.html +1 -1
  116. package/.next/server/app/dashboard/token-saver.rsc +6 -6
  117. package/.next/server/app/dashboard/token-saver.segments/!KGRhc2hib2FyZCk/dashboard/token-saver/__PAGE__.segment.rsc +6 -6
  118. package/.next/server/app/dashboard/token-saver.segments/_full.segment.rsc +6 -6
  119. package/.next/server/app/dashboard/token-saver.segments/_tree.segment.rsc +1 -1
  120. package/.next/server/app/dashboard/usage.html +2 -2
  121. package/.next/server/app/dashboard/usage.rsc +7 -7
  122. package/.next/server/app/dashboard/usage.segments/!KGRhc2hib2FyZCk/dashboard/usage/__PAGE__.segment.rsc +7 -7
  123. package/.next/server/app/dashboard/usage.segments/_full.segment.rsc +7 -7
  124. package/.next/server/app/dashboard/usage.segments/_tree.segment.rsc +1 -1
  125. package/.next/server/app/dashboard.html +1 -1
  126. package/.next/server/app/dashboard.rsc +8 -8
  127. package/.next/server/app/dashboard.segments/!KGRhc2hib2FyZCk/dashboard/__PAGE__.segment.rsc +7 -7
  128. package/.next/server/app/dashboard.segments/_full.segment.rsc +8 -8
  129. package/.next/server/app/dashboard.segments/_tree.segment.rsc +1 -1
  130. package/.next/server/app/index.html +1 -1
  131. package/.next/server/app/index.rsc +5 -5
  132. package/.next/server/app/index.segments/__PAGE__.segment.rsc +5 -5
  133. package/.next/server/app/index.segments/_full.segment.rsc +5 -5
  134. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  135. package/.next/server/app/landing/page_client-reference-manifest.js +1 -1
  136. package/.next/server/app/landing.html +1 -1
  137. package/.next/server/app/landing.rsc +6 -6
  138. package/.next/server/app/landing.segments/_full.segment.rsc +6 -6
  139. package/.next/server/app/landing.segments/_tree.segment.rsc +1 -1
  140. package/.next/server/app/landing.segments/landing/__PAGE__.segment.rsc +6 -6
  141. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  142. package/.next/server/app/login.html +1 -1
  143. package/.next/server/app/login.rsc +5 -5
  144. package/.next/server/app/login.segments/_full.segment.rsc +5 -5
  145. package/.next/server/app/login.segments/_tree.segment.rsc +1 -1
  146. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +5 -5
  147. package/.next/server/app/page_client-reference-manifest.js +1 -1
  148. package/.next/server/app-paths-manifest.json +6 -6
  149. package/.next/server/chunks/1011.js +1 -1
  150. package/.next/server/chunks/1194.js +1 -1
  151. package/.next/server/chunks/1289.js +1 -1
  152. package/.next/server/chunks/1633.js +1 -1
  153. package/.next/server/chunks/2282.js +7 -7
  154. package/.next/server/chunks/2339.js +1 -1
  155. package/.next/server/chunks/2801.js +1 -1
  156. package/.next/server/chunks/3425.js +1 -1
  157. package/.next/server/chunks/6056.js +1 -1
  158. package/.next/server/chunks/6343.js +6 -6
  159. package/.next/server/chunks/9429.js +1 -1
  160. package/.next/server/middleware-build-manifest.js +1 -1
  161. package/.next/server/pages/404.html +1 -1
  162. package/.next/server/pages/500.html +1 -1
  163. package/.next/server/server-reference-manifest.js +1 -1
  164. package/.next/server/server-reference-manifest.json +1 -1
  165. package/.next/static/chunks/{1278-1bef1ffb32ea7f41.js → 1278-5800a984ad8ad005.js} +1 -1
  166. package/.next/static/chunks/{1660-25564899e7912146.js → 1660-7f2e79bde5eff607.js} +1 -1
  167. package/.next/static/chunks/{1699-49be310981e2759e.js → 1699-13ee6864a8a31a96.js} +2 -2
  168. package/.next/static/chunks/{3577-1f02993330d7fba7.js → 3577-cd98d40e3099e1b0.js} +1 -1
  169. package/.next/static/chunks/{5862-6f3a3a6d4c00100d.js → 5862-f98c2f3d480b80d2.js} +1 -1
  170. package/.next/static/chunks/{9150-32b4247b1af5e206.js → 9150-8316559e1687b4a8.js} +1 -1
  171. package/.next/static/chunks/{9ce8c186-6cd204e9c27ecee1.js → 9ce8c186-3c7d7dbc5d428052.js} +1 -1
  172. package/.next/static/chunks/app/(dashboard)/dashboard/cli-tools/[toolId]/{page-c913b130003c1672.js → page-dd70f2cee9c910a9.js} +3 -3
  173. package/CHANGELOG.md +11 -0
  174. package/open-sse/handlers/chatCore/claudeMessageBody.js +357 -0
  175. package/open-sse/handlers/chatCore/nonStreamingHandler.js +36 -110
  176. package/open-sse/handlers/chatCore/sseToJsonHandler.js +147 -4
  177. package/open-sse/handlers/chatCore.js +48 -12
  178. package/open-sse/translator/concerns/classifier.js +70 -0
  179. package/open-sse/translator/concerns/thinkingUnified.js +18 -6
  180. package/open-sse/translator/request/claude-to-openai.js +20 -0
  181. package/open-sse/translator/request/openai-to-claude.js +11 -0
  182. package/package.json +1 -1
  183. package/src/app/(dashboard)/dashboard/cli-tools/components/cards/ClaudeToolCard.tsx +12 -0
  184. package/src/app/api/cli-tools/claude-settings/route.js +10 -0
  185. package/src/shared/constants/cliTools.js +8 -0
  186. /package/.next/static/{wBARQPScPhD6SLn7WQvI9 → 8QVpvFFKxoB53L449Kgan}/_buildManifest.js +0 -0
  187. /package/.next/static/{wBARQPScPhD6SLn7WQvI9 → 8QVpvFFKxoB53L449Kgan}/_ssgManifest.js +0 -0
@@ -10,7 +10,12 @@ import {
10
10
  saveUsageStats,
11
11
  formatDoneLine,
12
12
  } from "./requestDetail.js";
13
- import { ROLE, RESPONSES_ITEM } from "../../translator/schema/index.js";
13
+ import { ROLE, RESPONSES_ITEM, CLAUDE_BLOCK } from "../../translator/schema/index.js";
14
+ import {
15
+ chatCompletionToClaudeMessage,
16
+ claudeMessageToChatCompletion,
17
+ parseClaudeSSEToMessage,
18
+ } from "./claudeMessageBody.js";
14
19
 
15
20
  // Responses-API providers (e.g. codex) may emit SSE without content-type + use Responses output shape
16
21
  const isResponsesProvider = (p) => PROVIDERS[p]?.format === FORMATS.OPENAI_RESPONSES;
@@ -227,10 +232,15 @@ export async function handleForcedSSEToJson({
227
232
  log,
228
233
  }) {
229
234
  const contentType = providerResponse.headers.get("content-type") || "";
235
+ // Responses-API providers (codex) are known to stream without a content type;
236
+ // for every other provider an absent header means the body is not a stream we
237
+ // may consume here — returning null leaves it intact for the JSON handler.
230
238
  const isSSE =
231
239
  contentType.includes("text/event-stream") ||
232
240
  (contentType === "" && isResponsesProvider(provider));
233
- if (!isSSE) return null; // not handled here
241
+ // Not an aggregated stream — the caller falls back to the JSON handler. The
242
+ // pending counter must stay untouched here: this handler did not serve it.
243
+ if (!isSSE) return null;
234
244
 
235
245
  trackDone();
236
246
 
@@ -251,6 +261,15 @@ export async function handleForcedSSEToJson({
251
261
  if (isCodexResponsesApi) {
252
262
  try {
253
263
  const jsonResponse = await convertResponsesStreamToJson(providerResponse.body);
264
+ // A `response.failed` event is the upstream's own failure signal; an empty
265
+ // "successful" body would hide it from the client.
266
+ if (jsonResponse.status === "failed") {
267
+ appendLog({ status: `FAILED ${HTTP_STATUS.BAD_GATEWAY}` });
268
+ return createErrorResult(
269
+ HTTP_STATUS.BAD_GATEWAY,
270
+ "Upstream Responses stream reported failure",
271
+ );
272
+ }
254
273
  if (onRequestSuccess) await onRequestSuccess();
255
274
 
256
275
  const usage = jsonResponse.usage || {};
@@ -345,7 +364,36 @@ export async function handleForcedSSEToJson({
345
364
  }));
346
365
  const hasToolCalls = toolCalls.length > 0;
347
366
 
348
- if (sourceFormat === FORMATS.ANTIGRAVITY || sourceFormat === FORMATS.GEMINI) {
367
+ if (sourceFormat === FORMATS.CLAUDE) {
368
+ // The client speaks Anthropic Messages: reuse the same conversion a
369
+ // chat-native upstream gets, so both forced-stream upstream dialects
370
+ // produce one identical Claude body.
371
+ finalResp = chatCompletionToClaudeMessage({
372
+ id: jsonResponse.id,
373
+ created: jsonResponse.created_at,
374
+ model: jsonResponse.model || model,
375
+ choices: [
376
+ {
377
+ index: 0,
378
+ message: {
379
+ role: ROLE.ASSISTANT,
380
+ content: textContent,
381
+ ...(hasToolCalls ? { tool_calls: toolCalls } : {}),
382
+ },
383
+ finish_reason: hasToolCalls
384
+ ? "tool_calls"
385
+ : jsonResponse.status === "completed" || jsonResponse.status === "done"
386
+ ? "stop"
387
+ : jsonResponse.status || "stop",
388
+ },
389
+ ],
390
+ usage: {
391
+ prompt_tokens: inTokens,
392
+ completion_tokens: outTokens,
393
+ ...cacheDetails,
394
+ },
395
+ });
396
+ } else if (sourceFormat === FORMATS.ANTIGRAVITY || sourceFormat === FORMATS.GEMINI) {
349
397
  finalResp = {
350
398
  response: {
351
399
  candidates: [
@@ -406,6 +454,94 @@ export async function handleForcedSSEToJson({
406
454
  // Standard Chat Completions SSE path
407
455
  try {
408
456
  const sseText = await providerResponse.text();
457
+
458
+ // A provider that speaks Claude on the wire (opencode's `union-alpha`, any
459
+ // anthropic-compatible endpoint) streams Anthropic events, which the chat
460
+ // aggregator cannot fold — it would produce an empty message. Aggregate in
461
+ // the format the upstream actually used.
462
+ //
463
+ // `targetFormat` is what we asked for, not proof of what came back: some
464
+ // providers answer their Claude transport in OpenAI dialect. The aggregator
465
+ // returns null when no Anthropic event was seen, and the standard chat path
466
+ // below then handles the body it really is.
467
+ const isClaudeUpstream =
468
+ targetFormat === FORMATS.CLAUDE || /"type"\s*:\s*"message_start"/.test(sseText);
469
+ const claudeMessage = isClaudeUpstream ? parseClaudeSSEToMessage(sseText, model) : null;
470
+ if (claudeMessage) {
471
+ const message = claudeMessage;
472
+ if (message.error) {
473
+ const upstreamCode = String(message.error.code || "");
474
+ const status =
475
+ message.error.type === "invalid_request_error" ||
476
+ upstreamCode === "context_length_exceeded" ||
477
+ upstreamCode === "bad_request"
478
+ ? HTTP_STATUS.BAD_REQUEST
479
+ : HTTP_STATUS.BAD_GATEWAY;
480
+ return createErrorResult(status, message.error.message || "Upstream SSE stream failed");
481
+ }
482
+
483
+ if (onRequestSuccess) await onRequestSuccess();
484
+
485
+ const usage = message.usage || {};
486
+ appendLog({ tokens: usage, status: "200 OK" });
487
+ saveUsageStats({
488
+ provider,
489
+ model,
490
+ tokens: usage,
491
+ connectionId,
492
+ apiKey,
493
+ endpoint: clientRawRequest?.endpoint,
494
+ silent: true,
495
+ });
496
+ if (log?.line)
497
+ log.line(
498
+ reqTag,
499
+ "DONE",
500
+ formatDoneLine({ usage, latency: { total: Date.now() - requestStartTime } }),
501
+ );
502
+
503
+ const totalLatency = Date.now() - requestStartTime;
504
+ const textBlocks = (message.content || []).filter((b) => b.type === CLAUDE_BLOCK.TEXT);
505
+ const thinkingBlocks = (message.content || []).filter(
506
+ (b) => b.type === CLAUDE_BLOCK.THINKING,
507
+ );
508
+ saveRequestDetail(
509
+ buildRequestDetail(
510
+ {
511
+ ...ctx,
512
+ latency: { ttft: totalLatency, total: totalLatency },
513
+ tokens: {
514
+ prompt_tokens: usage.input_tokens || 0,
515
+ completion_tokens: usage.output_tokens || 0,
516
+ },
517
+ response: {
518
+ content: textBlocks.map((b) => b.text || "").join("") || null,
519
+ thinking: thinkingBlocks.map((b) => b.thinking || "").join("") || null,
520
+ finish_reason: message.stop_reason || "unknown",
521
+ },
522
+ status: "success",
523
+ },
524
+ { endpoint: clientRawRequest?.endpoint || null },
525
+ ),
526
+ ).catch(() => {});
527
+
528
+ // Claude → chat conversion only when the client asked for chat; a Claude
529
+ // client (or any other) receives the aggregated message unchanged.
530
+ const finalBody =
531
+ sourceFormat === FORMATS.CLAUDE
532
+ ? message
533
+ : sourceFormat === FORMATS.OPENAI
534
+ ? claudeMessageToChatCompletion(message)
535
+ : message;
536
+
537
+ return {
538
+ success: true,
539
+ response: new Response(JSON.stringify(restoreToolNames(finalBody, toolNameMap)), {
540
+ headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
541
+ }),
542
+ };
543
+ }
544
+
409
545
  const parsed = parseSSEToOpenAIResponse(sseText, model);
410
546
  if (!parsed)
411
547
  return createErrorResult(
@@ -491,10 +627,17 @@ export async function handleForcedSSEToJson({
491
627
  // lost on the non-streaming return path. Inlined (not imported from
492
628
  // nonStreamingHandler.js) to avoid a circular import: nonStreamingHandler
493
629
  // already imports parseSSEToOpenAIResponse from this module.
630
+ // A Claude-format client gets an Anthropic `message`, never a raw
631
+ // `chat.completion`. Every non-streaming /v1/messages request lands here
632
+ // when the provider forces streaming (opencode, codex, zed, openai), and
633
+ // Claude Code reads `message.content[]` — an OpenAI body throws a TypeError
634
+ // inside the client, which auto mode reports as "classifier unavailable".
494
635
  const finalBody =
495
636
  sourceFormat === FORMATS.OPENAI_RESPONSES
496
637
  ? chatCompletionToResponses(parsed, customToolNames)
497
- : parsed;
638
+ : sourceFormat === FORMATS.CLAUDE
639
+ ? chatCompletionToClaudeMessage(parsed)
640
+ : parsed;
498
641
 
499
642
  return {
500
643
  success: true,
@@ -177,10 +177,24 @@ export async function handleChatCore({
177
177
  }
178
178
  }
179
179
 
180
+ // Client preference (what the response must look like) is independent of the
181
+ // transport a provider needs. `stream` below is the upstream flag; the
182
+ // client-facing decision is `clientWantsJson` further down.
183
+ const acceptHeader = clientRawRequest?.headers?.accept || "";
184
+ const clientPrefersJson = acceptHeader.includes("application/json");
185
+ const clientPrefersSSE = acceptHeader.includes("text/event-stream");
180
186
  const clientRequestedStreaming =
181
187
  body.stream === true || sourceFormat === FORMATS.ANTIGRAVITY || sourceFormat === FORMATS.GEMINI;
182
188
  const providerRequiresStreaming = PROVIDERS[provider]?.forceStream === true;
183
- let stream = providerRequiresStreaming ? true : body.stream !== false;
189
+ // Anthropic Messages defaults to a JSON body when `stream` is omitted; OpenAI
190
+ // chat callers have historically been served SSE in that case here. An
191
+ // explicit Accept: text/event-stream still opts a Claude client into SSE.
192
+ const sourceDefaultsToJson = sourceFormat === FORMATS.CLAUDE && !clientPrefersSSE;
193
+ let stream = providerRequiresStreaming
194
+ ? true
195
+ : body.stream === undefined
196
+ ? !sourceDefaultsToJson
197
+ : body.stream !== false;
184
198
 
185
199
  // Image generation models require non-streaming (Google v1internal:generateContent)
186
200
  const modelType = getModelType(alias, model);
@@ -197,9 +211,8 @@ export async function handleChatCore({
197
211
 
198
212
  // Check client Accept header preference for non-streaming requests
199
213
  // This fixes AI SDK compatibility where clients send Accept: application/json
200
- const acceptHeader = clientRawRequest?.headers?.accept || "";
201
- const clientPrefersJson = acceptHeader.includes("application/json");
202
- const clientPrefersSSE = acceptHeader.includes("text/event-stream");
214
+ // (`acceptHeader`, `clientPrefersJson` and `clientPrefersSSE` are resolved
215
+ // with the stream negotiation above so both decisions share one reading.)
203
216
  if (
204
217
  clientPrefersJson &&
205
218
  !clientPrefersSSE &&
@@ -293,6 +306,21 @@ export async function handleChatCore({
293
306
  stripContinuityFields(translatedBody);
294
307
  }
295
308
 
309
+ // Reasoning depth is a model capability, so a model that cannot reason silently
310
+ // loses the client's effort selection inside applyThinking. Report that drop
311
+ // once per request: without it an ignored effort looks like a provider quirk.
312
+ const requestedThinking = extractThinking(body);
313
+ if (requestedThinking && requestedThinking.mode !== "none") {
314
+ const thinkingCaps = getCapabilitiesForModel(provider, stripThinkingSuffix(upstreamModel));
315
+ if (!thinkingCaps.reasoning) {
316
+ const requested = log?.fmtThink?.(requestedThinking) || requestedThinking.mode;
317
+ log?.warn?.(
318
+ "THINKING",
319
+ `${provider}/${model} cannot reason — requested ${requested} omitted`,
320
+ );
321
+ }
322
+ }
323
+
296
324
  // Dedupe duplicate built-in tools when equivalent MCP tools are present (Claude clients only).
297
325
  if (clientTool === "claude" && Array.isArray(translatedBody.tools)) {
298
326
  const { tools: deduped, stripped } = dedupeTools(translatedBody.tools);
@@ -687,9 +715,20 @@ export async function handleChatCore({
687
715
  appendRequestLog({ model, provider, connectionId, ...extra }).catch(() => {});
688
716
  const trackDone = () => trackPendingRequest(model, provider, connectionId, false);
689
717
 
690
- // Provider forced streaming but client wants JSON
691
- if (!clientRequestedStreaming && providerRequiresStreaming) {
692
- const result = await handleForcedSSEToJson({
718
+ // The client did not ask for SSE: either the provider only streams, or the
719
+ // request was negotiated as JSON. Aggregating the upstream stream is tried
720
+ // first; it declines a non-SSE body so a provider that ignored `stream` — or
721
+ // one that answered JSON on a forced-stream endpoint — still reaches the
722
+ // ordinary non-streaming handler instead of being written as an SSE response.
723
+ const clientWantsJson =
724
+ !stream ||
725
+ (providerRequiresStreaming && !clientRequestedStreaming && !clientPrefersSSE) ||
726
+ // Source-format contract: an Anthropic Messages client that did not ask to
727
+ // stream keeps a JSON body even if its provider forces streaming upstream.
728
+ (sourceFormat === FORMATS.CLAUDE && body.stream !== true && !clientPrefersSSE);
729
+
730
+ if (clientWantsJson) {
731
+ const forcedResult = await handleForcedSSEToJson({
693
732
  ...sharedCtx,
694
733
  providerResponse,
695
734
  sourceFormat,
@@ -699,14 +738,11 @@ export async function handleChatCore({
699
738
  trackDone,
700
739
  appendLog,
701
740
  });
702
- if (result) {
741
+ if (forcedResult) {
703
742
  streamController.handleComplete();
704
- return result;
743
+ return forcedResult;
705
744
  }
706
- }
707
745
 
708
- // True non-streaming response
709
- if (!stream) {
710
746
  const result = await handleNonStreamingResponse({
711
747
  ...sharedCtx,
712
748
  providerResponse,
@@ -0,0 +1,70 @@
1
+ // Claude Code auto-mode safety classifier detection.
2
+ //
3
+ // The classifier is an ordinary non-streaming /v1/messages call, but it sends
4
+ // `stop_sequences: ["</severity>"]` (current schema) or `["</block>"]` (legacy)
5
+ // to end its verdict early. Providers that implement `stop` by trimming the
6
+ // matched text from the completion return `<severity>0` instead of
7
+ // `<severity>0</severity>`, and the client cannot parse that verdict — auto
8
+ // mode then fails closed for every action.
9
+ //
10
+ // Detection is structural, never model-based: the classifier is the only
11
+ // utility call that carries a `<transcript>` block, asks for the verdict tag in
12
+ // its system prompt, and declares no tools. User content alone can never opt a
13
+ // request into this policy.
14
+
15
+ import { ROLE } from "../schema/index.js";
16
+
17
+ const TRANSCRIPT_MARKER = "<transcript>";
18
+ const SEVERITY_INSTRUCTION = "Output <severity>N</severity>";
19
+ const LEGACY_YES = "yes</block>";
20
+ const LEGACY_NO = "no</block>";
21
+
22
+ const SEVERITY_STOP = "</severity>";
23
+ const LEGACY_STOP = "</block>";
24
+
25
+ /** Concatenate the text of a Claude content value (string or block array). */
26
+ function textOf(content) {
27
+ if (typeof content === "string") return content;
28
+ if (!Array.isArray(content)) return "";
29
+ return content
30
+ .filter((block) => typeof block?.text === "string")
31
+ .map((block) => block.text)
32
+ .join("");
33
+ }
34
+
35
+ /**
36
+ * Return the stop sequence a known auto-mode classifier owns, or null.
37
+ *
38
+ * @param {object} body - Claude Messages request body.
39
+ * @returns {string|null} `"</severity>"`, `"</block>"`, or null.
40
+ */
41
+ export function detectClassifierStopSequence(body) {
42
+ if (!body || typeof body !== "object") return null;
43
+ if (Array.isArray(body.tools) && body.tools.length > 0) return null;
44
+
45
+ const messages = Array.isArray(body.messages) ? body.messages : [];
46
+ const transcript = messages
47
+ .filter((message) => message?.role !== ROLE.SYSTEM)
48
+ .map((message) => textOf(message?.content))
49
+ .join("\n");
50
+ if (!transcript.includes(TRANSCRIPT_MARKER)) return null;
51
+
52
+ const systemText = [
53
+ textOf(body.system),
54
+ ...messages.filter((message) => message?.role === ROLE.SYSTEM).map((m) => textOf(m.content)),
55
+ ].join("\n");
56
+
57
+ // Current schema wins when both known instructions appear.
58
+ if (systemText.includes(SEVERITY_INSTRUCTION)) return SEVERITY_STOP;
59
+ if (systemText.includes(LEGACY_YES) || systemText.includes(LEGACY_NO)) return LEGACY_STOP;
60
+ return null;
61
+ }
62
+
63
+ /**
64
+ * Drop the classifier-owned stop hint from a `stop_sequences` array.
65
+ * Every other sequence passes through untouched.
66
+ */
67
+ export function withoutClassifierStop(stopSequences, classifierStop) {
68
+ if (!Array.isArray(stopSequences) || !classifierStop) return stopSequences;
69
+ return stopSequences.filter((sequence) => sequence !== classifierStop);
70
+ }
@@ -276,7 +276,12 @@ function applyFormat(fmt, body, cfg, caps, supportedLevels, display) {
276
276
  break;
277
277
  }
278
278
  const level = toLevel(eff);
279
- if (level) body.reasoning_effort = normalizeOpenAILevel(level, supportedLevels);
279
+ // "auto" (adaptive thinking, no explicit effort) is not a member of the
280
+ // OpenAI reasoning_effort enum. Omitting the field is the only way to say
281
+ // "provider default" — every declared enum value names a specific depth.
282
+ if (level && level !== "auto") {
283
+ body.reasoning_effort = normalizeOpenAILevel(level, supportedLevels);
284
+ }
280
285
  break;
281
286
  }
282
287
  case "claude-adaptive": {
@@ -335,13 +340,17 @@ function applyFormat(fmt, body, cfg, caps, supportedLevels, display) {
335
340
  const zaiLvl = toLevel(eff);
336
341
  // GLM-5.3 only accepts exactly low|high|max (anything else errors); GLM-5.2 accepts
337
342
  // a wider set but z.ai maps low/medium->high and xhigh->max server-side anyway, so
338
- // this 3-value mapping matches both.
339
- body.reasoning_effort =
343
+ // this 3-value mapping matches both. No explicit effort ("auto") omits the field:
344
+ // the fallback below would otherwise turn "let the model decide" into max.
345
+ const mapped =
340
346
  zaiLvl === "low" || zaiLvl === "minimal"
341
347
  ? "low"
342
348
  : zaiLvl === "high" || zaiLvl === "medium"
343
349
  ? "high"
344
- : "max";
350
+ : zaiLvl === "xhigh" || zaiLvl === "max"
351
+ ? "max"
352
+ : null;
353
+ if (mapped) body.reasoning_effort = mapped;
345
354
  }
346
355
  break;
347
356
  }
@@ -394,7 +403,9 @@ function applyFormat(fmt, body, cfg, caps, supportedLevels, display) {
394
403
  case "step": {
395
404
  if (none && canDisable) break;
396
405
  const level = toLevel(eff);
397
- if (level) body.reasoning_effort = level === "xhigh" || level === "max" ? "high" : level;
406
+ // "auto" is not in Step's low|medium|high enum — omit and take the default.
407
+ if (level && level !== "auto")
408
+ body.reasoning_effort = level === "xhigh" || level === "max" ? "high" : level;
398
409
  break;
399
410
  }
400
411
  case "tokenrouter": {
@@ -416,7 +427,8 @@ function applyFormat(fmt, body, cfg, caps, supportedLevels, display) {
416
427
  break;
417
428
  }
418
429
  const level = toLevel(eff);
419
- if (level) body.params.reasoning_effort = level;
430
+ // Native CLI accepts named levels only; "auto" means "no preference" → omit.
431
+ if (level && level !== "auto") body.params.reasoning_effort = level;
420
432
  break;
421
433
  }
422
434
  default:
@@ -4,6 +4,7 @@ import { adjustMaxTokens } from "../formats/maxTokens.js";
4
4
  import { encodeDataUri } from "../concerns/image.js";
5
5
  import { ROLE, OPENAI_BLOCK, CLAUDE_BLOCK, EMPTY_TOOL_RESULT } from "../schema/index.js";
6
6
  import { collapseTextParts } from "../concerns/message.js";
7
+ import { detectClassifierStopSequence, withoutClassifierStop } from "../concerns/classifier.js";
7
8
 
8
9
  function stripAnthropicBillingHeader(text) {
9
10
  if (typeof text !== "string") return "";
@@ -28,6 +29,25 @@ export function claudeToOpenAIRequest(model, body, stream) {
28
29
  result.temperature = body.temperature;
29
30
  }
30
31
 
32
+ // Sampling. Only top_p: `top_k` has no OpenAI equivalent, and strict
33
+ // OpenAI-dialect upstreams reject unknown body fields with a 400.
34
+ if (body.top_p !== undefined) result.top_p = body.top_p;
35
+
36
+ // stop_sequences → stop. The wires name the same thing, so an ordinary
37
+ // caller's terminators carry over unchanged.
38
+ //
39
+ // One exception: Claude Code's auto-mode classifier asks for a structural
40
+ // verdict (<severity>N</severity> / <block>yes</block>) and also sends the
41
+ // closing tag as its own stop hint. OpenAI-dialect `stop` implementations
42
+ // trim the matched text, leaving "<severity>0" — a verdict the client cannot
43
+ // parse, so auto mode fails closed. Drop only that owned hint; the model then
44
+ // finishes the tag it was asked for.
45
+ const classifierStop = detectClassifierStopSequence(body);
46
+ const forwardedStop = withoutClassifierStop(body.stop_sequences, classifierStop);
47
+ if (Array.isArray(forwardedStop) && forwardedStop.length > 0) {
48
+ result.stop = forwardedStop;
49
+ }
50
+
31
51
  // System message
32
52
  if (body.system) {
33
53
  const systemContent = Array.isArray(body.system)
@@ -31,6 +31,17 @@ export function openaiToClaudeRequest(model, body, stream) {
31
31
  result.temperature = body.temperature;
32
32
  }
33
33
 
34
+ if (body.top_p !== undefined) result.top_p = body.top_p;
35
+
36
+ // stop → stop_sequences. OpenAI accepts a single string or an array; Claude
37
+ // only takes an array. Round-tripping this keeps a stop-terminated answer
38
+ // identical in both directions.
39
+ if (typeof body.stop === "string" && body.stop) {
40
+ result.stop_sequences = [body.stop];
41
+ } else if (Array.isArray(body.stop) && body.stop.length > 0) {
42
+ result.stop_sequences = body.stop;
43
+ }
44
+
34
45
  // Messages
35
46
  result.messages = [];
36
47
  const systemParts = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amir83nasr/10router",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Amir83Nasr/10Router.git"
@@ -250,6 +250,12 @@ export default function ClaudeToolCard({
250
250
  // model ids never match, so the escape-hatch flag must ride along.
251
251
  env[tool.envVars.todoTools] = "1";
252
252
  }
253
+ if (tool.envVars?.autoModeServer) {
254
+ // Auto mode asks the Anthropic API to review actions in-band. A gateway
255
+ // cannot provide that review, so declaring it skips the dead round-trip
256
+ // and the classifier billing notice; the local classifier still runs.
257
+ env[tool.envVars.autoModeServer] = "0";
258
+ }
253
259
  const res = await fetch("/api/cli-tools/claude-settings", {
254
260
  method: "POST",
255
261
  headers: { "Content-Type": "application/json" },
@@ -331,6 +337,12 @@ export default function ClaudeToolCard({
331
337
  // model ids never match, so the escape-hatch flag must ride along.
332
338
  env[tool.envVars.todoTools] = "1";
333
339
  }
340
+ if (tool.envVars?.autoModeServer) {
341
+ // Auto mode asks the Anthropic API to review actions in-band. A gateway
342
+ // cannot provide that review, so declaring it skips the dead round-trip
343
+ // and the classifier billing notice; the local classifier still runs.
344
+ env[tool.envVars.autoModeServer] = "0";
345
+ }
334
346
 
335
347
  return [
336
348
  {
@@ -191,6 +191,15 @@ export async function POST(request) {
191
191
  const todoToolsKey = CLI_TOOLS.claude.envVars.todoTools;
192
192
  if (todoToolsKey) newSettings.env[todoToolsKey] = "1";
193
193
 
194
+ // CLAUDE_CODE_AUTO_MODE_SERVER=0 — auto mode asks the Anthropic API to
195
+ // review actions in-band (`safeguards` + the dangerous-tool-use beta). Behind
196
+ // this gateway that request never gets reviewed, so Claude Code spends one
197
+ // dead round-trip per session, shows the classifier billing notice, and then
198
+ // uses its own classifier regardless. Saying so up front keeps auto mode the
199
+ // same on every launch and keeps the notice out of the session.
200
+ const autoModeServerKey = CLI_TOOLS.claude.envVars.autoModeServer;
201
+ if (autoModeServerKey) newSettings.env[autoModeServerKey] = "0";
202
+
194
203
  // Write new settings
195
204
  await fs.writeFile(settingsPath, JSON.stringify(newSettings, null, 2));
196
205
 
@@ -219,6 +228,7 @@ const RESET_ENV_KEYS = [
219
228
  "API_TIMEOUT_MS",
220
229
  "CLAUDE_CODE_AUTO_COMPACT_WINDOW",
221
230
  "CLAUDE_CODE_ENABLE_TODO_TOOLS",
231
+ "CLAUDE_CODE_AUTO_MODE_SERVER",
222
232
  ];
223
233
 
224
234
  // DELETE - Reset settings (remove env fields)
@@ -19,6 +19,14 @@ export const CLI_TOOLS = {
19
19
  // as `oc/...` or a combo name never match, so the task list disappears.
20
20
  // This flag is the CLI's documented escape hatch for that gate.
21
21
  todoTools: "CLAUDE_CODE_ENABLE_TODO_TOOLS",
22
+ // Auto mode's server-side classifier review asks the Anthropic API to
23
+ // review actions as part of the session's own requests (`safeguards` body
24
+ // field + `dangerous-tool-use` beta). A gateway cannot provide that review,
25
+ // so Claude Code prints the "this session isn't eligible" billing notice on
26
+ // the first checked action and falls back to its own classifier anyway.
27
+ // Declaring the gateway up front skips the notice: auto mode uses the local
28
+ // classifier for every session, with no dead round-trip first.
29
+ autoModeServer: "CLAUDE_CODE_AUTO_MODE_SERVER",
22
30
  },
23
31
  modelAliases: ["default", "sonnet", "opus", "fable", "haiku", "opusplan"],
24
32
  settingsFile: "~/.claude/settings.json",