@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.
- package/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +6 -6
- package/.next/build-manifest.json +2 -2
- package/.next/prerender-manifest.json +3 -3
- package/.next/server/app/(dashboard)/dashboard/cli-tools/[toolId]/page.js +2 -2
- package/.next/server/app/(dashboard)/dashboard/cli-tools/[toolId]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/cli-tools/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/combos/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/console-log/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/endpoint/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/media-providers/systemone/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/media-providers/systemone/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/profile/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/providers/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/providers/new/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/providers/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/proxy-pools/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/pxpipe/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/quota/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/token-saver/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/usage/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +5 -5
- package/.next/server/app/_not-found.segments/_full.segment.rsc +5 -5
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +5 -5
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/api/cli-tools/all-statuses/route.js +1 -1
- package/.next/server/app/api/cli-tools/claude-settings/route.js +2 -2
- package/.next/server/app/api/cli-tools/codex-settings/route.js +1 -1
- package/.next/server/app/api/cli-tools/cowork-mcp-registry/route.js +1 -1
- package/.next/server/app/api/cli-tools/cowork-mcp-tools/route.js +1 -1
- package/.next/server/app/api/cli-tools/cowork-settings/route.js +1 -1
- package/.next/server/app/api/cli-tools/dsh-settings/route.js +1 -1
- package/.next/server/app/api/cli-tools/opencode-settings/route.js +1 -1
- package/.next/server/app/api/oauth/[provider]/[action]/route.js +1 -1
- package/.next/server/app/api/oauth/codex/bulk-import/route.js +1 -1
- package/.next/server/app/api/oauth/codex/import-token/route.js +1 -1
- package/.next/server/app/api/providers/client/route.js +1 -1
- package/.next/server/app/api/translator/send/route.js +1 -1
- package/.next/server/app/api/translator/translate/route.js +1 -1
- package/.next/server/app/api/tunnel/tailscale-install/route.js +2 -2
- package/.next/server/app/api/usage/[connectionId]/codex-reset-credits/route.js +1 -1
- package/.next/server/app/api/usage/[connectionId]/route.js +1 -1
- package/.next/server/app/api/version/route.js +1 -1
- package/.next/server/app/callback/page_client-reference-manifest.js +1 -1
- package/.next/server/app/callback.html +1 -1
- package/.next/server/app/callback.rsc +6 -6
- package/.next/server/app/callback.segments/_full.segment.rsc +6 -6
- package/.next/server/app/callback.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/callback.segments/callback/__PAGE__.segment.rsc +6 -6
- package/.next/server/app/dashboard/cli-tools.html +1 -1
- package/.next/server/app/dashboard/cli-tools.rsc +8 -8
- package/.next/server/app/dashboard/cli-tools.segments/!KGRhc2hib2FyZCk/dashboard/cli-tools/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard/cli-tools.segments/_full.segment.rsc +8 -8
- package/.next/server/app/dashboard/cli-tools.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/combos.html +1 -1
- package/.next/server/app/dashboard/combos.rsc +7 -7
- package/.next/server/app/dashboard/combos.segments/!KGRhc2hib2FyZCk/dashboard/combos/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard/combos.segments/_full.segment.rsc +7 -7
- package/.next/server/app/dashboard/combos.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/endpoint.html +1 -1
- package/.next/server/app/dashboard/endpoint.rsc +8 -8
- package/.next/server/app/dashboard/endpoint.segments/!KGRhc2hib2FyZCk/dashboard/endpoint/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard/endpoint.segments/_full.segment.rsc +8 -8
- package/.next/server/app/dashboard/endpoint.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/media-providers/systemone.html +1 -1
- package/.next/server/app/dashboard/media-providers/systemone.rsc +7 -7
- package/.next/server/app/dashboard/media-providers/systemone.segments/!KGRhc2hib2FyZCk/dashboard/media-providers/systemone/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard/media-providers/systemone.segments/_full.segment.rsc +7 -7
- package/.next/server/app/dashboard/media-providers/systemone.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/profile.html +1 -1
- package/.next/server/app/dashboard/profile.rsc +7 -7
- package/.next/server/app/dashboard/profile.segments/!KGRhc2hib2FyZCk/dashboard/profile/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard/profile.segments/_full.segment.rsc +7 -7
- package/.next/server/app/dashboard/profile.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/providers/new.html +1 -1
- package/.next/server/app/dashboard/providers/new.rsc +7 -7
- package/.next/server/app/dashboard/providers/new.segments/!KGRhc2hib2FyZCk/dashboard/providers/new/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard/providers/new.segments/_full.segment.rsc +7 -7
- package/.next/server/app/dashboard/providers/new.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/providers.html +1 -1
- package/.next/server/app/dashboard/providers.rsc +7 -7
- package/.next/server/app/dashboard/providers.segments/!KGRhc2hib2FyZCk/dashboard/providers/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard/providers.segments/_full.segment.rsc +7 -7
- package/.next/server/app/dashboard/providers.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/proxy-pools.html +1 -1
- package/.next/server/app/dashboard/proxy-pools.rsc +7 -7
- package/.next/server/app/dashboard/proxy-pools.segments/!KGRhc2hib2FyZCk/dashboard/proxy-pools/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard/proxy-pools.segments/_full.segment.rsc +7 -7
- package/.next/server/app/dashboard/proxy-pools.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/pxpipe.html +1 -1
- package/.next/server/app/dashboard/pxpipe.rsc +6 -6
- package/.next/server/app/dashboard/pxpipe.segments/!KGRhc2hib2FyZCk/dashboard/pxpipe/__PAGE__.segment.rsc +6 -6
- package/.next/server/app/dashboard/pxpipe.segments/_full.segment.rsc +6 -6
- package/.next/server/app/dashboard/pxpipe.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/quota.html +2 -2
- package/.next/server/app/dashboard/quota.rsc +8 -8
- package/.next/server/app/dashboard/quota.segments/!KGRhc2hib2FyZCk/dashboard/quota/__PAGE__.segment.rsc +8 -8
- package/.next/server/app/dashboard/quota.segments/_full.segment.rsc +8 -8
- package/.next/server/app/dashboard/quota.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/settings/pricing/page_client-reference-manifest.js +1 -1
- package/.next/server/app/dashboard/settings/pricing.html +1 -1
- package/.next/server/app/dashboard/settings/pricing.rsc +5 -5
- package/.next/server/app/dashboard/settings/pricing.segments/_full.segment.rsc +5 -5
- package/.next/server/app/dashboard/settings/pricing.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/settings/pricing.segments/dashboard/settings/pricing/__PAGE__.segment.rsc +5 -5
- package/.next/server/app/dashboard/token-saver.html +1 -1
- package/.next/server/app/dashboard/token-saver.rsc +6 -6
- package/.next/server/app/dashboard/token-saver.segments/!KGRhc2hib2FyZCk/dashboard/token-saver/__PAGE__.segment.rsc +6 -6
- package/.next/server/app/dashboard/token-saver.segments/_full.segment.rsc +6 -6
- package/.next/server/app/dashboard/token-saver.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/usage.html +2 -2
- package/.next/server/app/dashboard/usage.rsc +7 -7
- package/.next/server/app/dashboard/usage.segments/!KGRhc2hib2FyZCk/dashboard/usage/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard/usage.segments/_full.segment.rsc +7 -7
- package/.next/server/app/dashboard/usage.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard.html +1 -1
- package/.next/server/app/dashboard.rsc +8 -8
- package/.next/server/app/dashboard.segments/!KGRhc2hib2FyZCk/dashboard/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard.segments/_full.segment.rsc +8 -8
- package/.next/server/app/dashboard.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +5 -5
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +5 -5
- package/.next/server/app/index.segments/_full.segment.rsc +5 -5
- package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/landing/page_client-reference-manifest.js +1 -1
- package/.next/server/app/landing.html +1 -1
- package/.next/server/app/landing.rsc +6 -6
- package/.next/server/app/landing.segments/_full.segment.rsc +6 -6
- package/.next/server/app/landing.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/landing.segments/landing/__PAGE__.segment.rsc +6 -6
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login.html +1 -1
- package/.next/server/app/login.rsc +5 -5
- package/.next/server/app/login.segments/_full.segment.rsc +5 -5
- package/.next/server/app/login.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +5 -5
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +6 -6
- package/.next/server/chunks/1011.js +1 -1
- package/.next/server/chunks/1194.js +1 -1
- package/.next/server/chunks/1289.js +1 -1
- package/.next/server/chunks/1633.js +1 -1
- package/.next/server/chunks/2282.js +7 -7
- package/.next/server/chunks/2339.js +1 -1
- package/.next/server/chunks/2801.js +1 -1
- package/.next/server/chunks/3425.js +1 -1
- package/.next/server/chunks/6056.js +1 -1
- package/.next/server/chunks/6343.js +6 -6
- package/.next/server/chunks/9429.js +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/{1278-1bef1ffb32ea7f41.js → 1278-5800a984ad8ad005.js} +1 -1
- package/.next/static/chunks/{1660-25564899e7912146.js → 1660-7f2e79bde5eff607.js} +1 -1
- package/.next/static/chunks/{1699-49be310981e2759e.js → 1699-13ee6864a8a31a96.js} +2 -2
- package/.next/static/chunks/{3577-1f02993330d7fba7.js → 3577-cd98d40e3099e1b0.js} +1 -1
- package/.next/static/chunks/{5862-6f3a3a6d4c00100d.js → 5862-f98c2f3d480b80d2.js} +1 -1
- package/.next/static/chunks/{9150-32b4247b1af5e206.js → 9150-8316559e1687b4a8.js} +1 -1
- package/.next/static/chunks/{9ce8c186-6cd204e9c27ecee1.js → 9ce8c186-3c7d7dbc5d428052.js} +1 -1
- package/.next/static/chunks/app/(dashboard)/dashboard/cli-tools/[toolId]/{page-c913b130003c1672.js → page-dd70f2cee9c910a9.js} +3 -3
- package/CHANGELOG.md +11 -0
- package/open-sse/handlers/chatCore/claudeMessageBody.js +357 -0
- package/open-sse/handlers/chatCore/nonStreamingHandler.js +36 -110
- package/open-sse/handlers/chatCore/sseToJsonHandler.js +147 -4
- package/open-sse/handlers/chatCore.js +48 -12
- package/open-sse/translator/concerns/classifier.js +70 -0
- package/open-sse/translator/concerns/thinkingUnified.js +18 -6
- package/open-sse/translator/request/claude-to-openai.js +20 -0
- package/open-sse/translator/request/openai-to-claude.js +11 -0
- package/package.json +1 -1
- package/src/app/(dashboard)/dashboard/cli-tools/components/cards/ClaudeToolCard.tsx +12 -0
- package/src/app/api/cli-tools/claude-settings/route.js +10 -0
- package/src/shared/constants/cliTools.js +8 -0
- /package/.next/static/{wBARQPScPhD6SLn7WQvI9 → 8QVpvFFKxoB53L449Kgan}/_buildManifest.js +0 -0
- /package/.next/static/{wBARQPScPhD6SLn7WQvI9 → 8QVpvFFKxoB53L449Kgan}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
// ── CLAUDE MESSAGE BODY ──────────────────────────────────────────────
|
|
2
|
+
// A Claude-format client (/v1/messages) must always receive an Anthropic
|
|
3
|
+
// `message` body, whichever wire format the upstream spoke. Claude Code reads
|
|
4
|
+
// `message.content[]`; a raw `chat.completion` makes it throw a TypeError,
|
|
5
|
+
// which auto mode surfaces as "Auto mode check unavailable" — every classifier
|
|
6
|
+
// call is a non-streaming /v1/messages request, so the whole mode fails closed.
|
|
7
|
+
//
|
|
8
|
+
// This module owns both directions and the Anthropic SSE aggregation used when
|
|
9
|
+
// a forced-streaming upstream has to answer a JSON client. It imports no
|
|
10
|
+
// handler, so both non-streaming paths can use it without a circular import.
|
|
11
|
+
|
|
12
|
+
import { FORMATS } from "../../translator/formats.js";
|
|
13
|
+
import { fromOpenAIFinish } from "../../translator/concerns/finishReason.js";
|
|
14
|
+
import { extractReasoningText } from "../../translator/concerns/reasoning.js";
|
|
15
|
+
import { ROLE, CLAUDE_BLOCK } from "../../translator/schema/index.js";
|
|
16
|
+
|
|
17
|
+
// ── HELPERS ─────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function parseToolArguments(value) {
|
|
20
|
+
if (!value) return {};
|
|
21
|
+
if (typeof value === "object") return value;
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(value);
|
|
24
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
25
|
+
} catch {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Text carried by an OpenAI message whose `content` is a parts array. */
|
|
31
|
+
function textFromContentParts(content) {
|
|
32
|
+
if (!Array.isArray(content)) return "";
|
|
33
|
+
return content
|
|
34
|
+
.filter((part) => typeof part?.text === "string")
|
|
35
|
+
.map((part) => part.text)
|
|
36
|
+
.join("");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function claudeUsageFromOpenAI(usage) {
|
|
40
|
+
const cacheRead =
|
|
41
|
+
usage.prompt_tokens_details?.cached_tokens || usage.cache_read_input_tokens || 0;
|
|
42
|
+
const cacheCreate = usage.prompt_tokens_details?.cache_creation_tokens || 0;
|
|
43
|
+
// OpenAI counts cached tokens inside prompt_tokens; Anthropic reports them
|
|
44
|
+
// outside input_tokens. Subtract so a client summing the three matches — but
|
|
45
|
+
// only from prompt_tokens. An `input_tokens`-shaped body is already
|
|
46
|
+
// cache-exclusive, and subtracting there would count the cache twice.
|
|
47
|
+
const inputTokens =
|
|
48
|
+
usage.prompt_tokens !== undefined
|
|
49
|
+
? Math.max(0, usage.prompt_tokens - cacheRead - cacheCreate)
|
|
50
|
+
: usage.input_tokens || 0;
|
|
51
|
+
|
|
52
|
+
const claudeUsage = {
|
|
53
|
+
input_tokens: inputTokens,
|
|
54
|
+
output_tokens: usage.completion_tokens || usage.output_tokens || 0,
|
|
55
|
+
};
|
|
56
|
+
if (cacheRead > 0) claudeUsage.cache_read_input_tokens = cacheRead;
|
|
57
|
+
if (cacheCreate > 0) claudeUsage.cache_creation_input_tokens = cacheCreate;
|
|
58
|
+
return claudeUsage;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── OPENAI CHAT → CLAUDE MESSAGE ────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Convert an OpenAI Chat Completions body into an Anthropic `message`.
|
|
65
|
+
* A body that is already Claude-shaped (has a `content` array) is passed through.
|
|
66
|
+
*
|
|
67
|
+
* Usage follows the Anthropic contract: `input_tokens` EXCLUDES cached tokens,
|
|
68
|
+
* so the cache counters are surfaced separately (`cache_read_input_tokens`,
|
|
69
|
+
* `cache_creation_input_tokens`) exactly as the streaming translator does.
|
|
70
|
+
*/
|
|
71
|
+
export function chatCompletionToClaudeMessage(responseBody) {
|
|
72
|
+
if (!responseBody || typeof responseBody !== "object") return responseBody;
|
|
73
|
+
// Upstream error payload — keep it so the failure is not masked as an empty turn.
|
|
74
|
+
if (responseBody.error) return responseBody;
|
|
75
|
+
// Already a Claude message (e.g. an upstream that speaks Claude on the wire).
|
|
76
|
+
if (Array.isArray(responseBody.content) && !responseBody.choices) return responseBody;
|
|
77
|
+
|
|
78
|
+
const choice = responseBody?.choices?.[0];
|
|
79
|
+
const message = choice?.message || {};
|
|
80
|
+
const content = [];
|
|
81
|
+
|
|
82
|
+
const reasoning = extractReasoningText(message);
|
|
83
|
+
if (reasoning) {
|
|
84
|
+
content.push({ type: CLAUDE_BLOCK.THINKING, thinking: reasoning });
|
|
85
|
+
}
|
|
86
|
+
const text =
|
|
87
|
+
typeof message.content === "string" ? message.content : textFromContentParts(message.content);
|
|
88
|
+
if (text.length > 0) {
|
|
89
|
+
content.push({ type: CLAUDE_BLOCK.TEXT, text });
|
|
90
|
+
}
|
|
91
|
+
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
92
|
+
for (const toolCall of toolCalls) {
|
|
93
|
+
const fn = toolCall.function || {};
|
|
94
|
+
content.push({
|
|
95
|
+
type: CLAUDE_BLOCK.TOOL_USE,
|
|
96
|
+
id: toolCall.id || `toolu_${Date.now()}_${content.length}`,
|
|
97
|
+
name: fn.name || toolCall.name || "",
|
|
98
|
+
input: parseToolArguments(fn.arguments ?? toolCall.arguments),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// A missing/empty choice still has to leave the client a readable message.
|
|
102
|
+
if (content.length === 0) content.push({ type: CLAUDE_BLOCK.TEXT, text: "" });
|
|
103
|
+
|
|
104
|
+
// Some upstreams omit finish_reason; tool calls imply a tool_use stop.
|
|
105
|
+
const finishReason = choice?.finish_reason || (toolCalls.length ? "tool_calls" : "stop");
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
id: String(responseBody.id || `msg_${Date.now()}`).replace(/^chatcmpl-/, ""),
|
|
109
|
+
type: "message",
|
|
110
|
+
role: ROLE.ASSISTANT,
|
|
111
|
+
model: responseBody.model || "unknown",
|
|
112
|
+
content,
|
|
113
|
+
stop_reason: fromOpenAIFinish(finishReason, FORMATS.CLAUDE),
|
|
114
|
+
stop_sequence: null,
|
|
115
|
+
usage: claudeUsageFromOpenAI(responseBody.usage || {}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── CLAUDE MESSAGE → OPENAI CHAT COMPLETION ─────────────────────────
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Convert an Anthropic `message` into an OpenAI Chat Completions body.
|
|
123
|
+
*
|
|
124
|
+
* A body that already carries `choices` (an upstream that answers in chat shape
|
|
125
|
+
* regardless of the requested format) is passed through, as is a body whose
|
|
126
|
+
* `content` is not an array (some gateways return a bare string).
|
|
127
|
+
*/
|
|
128
|
+
export function claudeMessageToChatCompletion(responseBody) {
|
|
129
|
+
if (!responseBody || typeof responseBody !== "object") return responseBody;
|
|
130
|
+
if (responseBody.choices) return responseBody;
|
|
131
|
+
if (responseBody.content && !Array.isArray(responseBody.content)) return responseBody;
|
|
132
|
+
|
|
133
|
+
let textContent = "",
|
|
134
|
+
thinkingContent = "";
|
|
135
|
+
const toolCalls = [];
|
|
136
|
+
|
|
137
|
+
for (const block of responseBody.content || []) {
|
|
138
|
+
if (block.type === CLAUDE_BLOCK.TEXT) {
|
|
139
|
+
// Strip markdown code block markers (e.g. kimi wraps JSON in ```json...```)
|
|
140
|
+
const raw = block.text ?? "";
|
|
141
|
+
const text = raw.replace(/^\s*```\s*json\s*\n?/i, "").replace(/\n?\s*```\s*$/i, "");
|
|
142
|
+
textContent += text;
|
|
143
|
+
} else if (block.type === CLAUDE_BLOCK.THINKING) thinkingContent += block.thinking || "";
|
|
144
|
+
else if (block.type === CLAUDE_BLOCK.TOOL_USE) {
|
|
145
|
+
toolCalls.push({
|
|
146
|
+
id: block.id,
|
|
147
|
+
type: "function",
|
|
148
|
+
function: { name: block.name, arguments: JSON.stringify(block.input || {}) },
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const message = { role: ROLE.ASSISTANT };
|
|
154
|
+
if (textContent) message.content = textContent;
|
|
155
|
+
if (thinkingContent) message.reasoning_content = thinkingContent;
|
|
156
|
+
if (toolCalls.length > 0) message.tool_calls = toolCalls;
|
|
157
|
+
if (!message.content && !message.tool_calls) message.content = "";
|
|
158
|
+
|
|
159
|
+
let finishReason = responseBody.stop_reason || "stop";
|
|
160
|
+
if (finishReason === "end_turn") finishReason = "stop";
|
|
161
|
+
if (finishReason === "tool_use") finishReason = "tool_calls";
|
|
162
|
+
|
|
163
|
+
const result = {
|
|
164
|
+
id: `chatcmpl-${responseBody.id || Date.now()}`,
|
|
165
|
+
object: "chat.completion",
|
|
166
|
+
created: Math.floor(Date.now() / 1000),
|
|
167
|
+
model: responseBody.model || "claude",
|
|
168
|
+
choices: [{ index: 0, message, finish_reason: finishReason }],
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const usage = responseBody.usage;
|
|
172
|
+
if (usage) {
|
|
173
|
+
result.usage = {
|
|
174
|
+
prompt_tokens: usage.input_tokens || 0,
|
|
175
|
+
completion_tokens: usage.output_tokens || 0,
|
|
176
|
+
total_tokens: (usage.input_tokens || 0) + (usage.output_tokens || 0),
|
|
177
|
+
};
|
|
178
|
+
if (usage.cache_read_input_tokens)
|
|
179
|
+
result.usage.prompt_tokens_details = {
|
|
180
|
+
...(result.usage.prompt_tokens_details || {}),
|
|
181
|
+
cached_tokens: usage.cache_read_input_tokens,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── CLAUDE SSE → CLAUDE MESSAGE ─────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
function parseSSEPayloads(rawSSE) {
|
|
190
|
+
const events = [];
|
|
191
|
+
for (const line of String(rawSSE || "").split("\n")) {
|
|
192
|
+
const trimmed = line.trim();
|
|
193
|
+
if (!trimmed.startsWith("data:")) continue;
|
|
194
|
+
const payload = trimmed.slice(5).trim();
|
|
195
|
+
if (!payload || payload === "[DONE]") continue;
|
|
196
|
+
try {
|
|
197
|
+
const parsed = JSON.parse(payload);
|
|
198
|
+
if (parsed && typeof parsed === "object") events.push(parsed);
|
|
199
|
+
} catch {
|
|
200
|
+
/* ignore malformed lines */
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return events;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function appendDelta(block, parts, delta) {
|
|
207
|
+
switch (delta.type) {
|
|
208
|
+
case "text_delta":
|
|
209
|
+
parts.push(String(delta.text ?? ""));
|
|
210
|
+
break;
|
|
211
|
+
case "thinking_delta":
|
|
212
|
+
parts.push(String(delta.thinking ?? ""));
|
|
213
|
+
break;
|
|
214
|
+
case "input_json_delta":
|
|
215
|
+
parts.push(String(delta.partial_json ?? ""));
|
|
216
|
+
break;
|
|
217
|
+
case "signature_delta":
|
|
218
|
+
block.signature = `${block.signature || ""}${delta.signature || ""}`;
|
|
219
|
+
break;
|
|
220
|
+
case "citations_delta":
|
|
221
|
+
if (delta.citation) block.citations = [...(block.citations || []), delta.citation];
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Assemble an Anthropic `message` from an Anthropic Messages SSE stream.
|
|
228
|
+
*
|
|
229
|
+
* Used when a forced-streaming provider speaks Claude on the wire (for example
|
|
230
|
+
* opencode's `union-alpha`) but the client asked for JSON. Mirrors the
|
|
231
|
+
* streaming translator's block model: text/thinking deltas concatenate, tool
|
|
232
|
+
* `input_json_delta` fragments parse into `tool_use.input`, and usage is merged
|
|
233
|
+
* so the cache counters from `message_start` survive the `message_delta` total.
|
|
234
|
+
*
|
|
235
|
+
* @returns {object|null} the message, `{ error }` on an `event: error`, or null
|
|
236
|
+
* when the stream carried no Anthropic event (the caller then aggregates it in
|
|
237
|
+
* the dialect the upstream actually used).
|
|
238
|
+
*/
|
|
239
|
+
export function parseClaudeSSEToMessage(rawSSE, fallbackModel) {
|
|
240
|
+
const events = parseSSEPayloads(rawSSE);
|
|
241
|
+
let streamError = null;
|
|
242
|
+
let message = {};
|
|
243
|
+
// Only an Anthropic lifecycle event proves the upstream spoke Claude.
|
|
244
|
+
// (`completed` is not tracked separately: a truncated stream is still assembled
|
|
245
|
+
// so a partial answer is not thrown away.)
|
|
246
|
+
// `targetFormat` is what we asked for, not what came back: a provider can
|
|
247
|
+
// answer its Claude transport in OpenAI dialect, and parsing that as Anthropic
|
|
248
|
+
// would silently drop the answer.
|
|
249
|
+
let sawAnthropicEvent = false;
|
|
250
|
+
const blocks = new Map(); // index -> content block
|
|
251
|
+
const parts = new Map(); // index -> accumulated delta strings
|
|
252
|
+
|
|
253
|
+
for (const event of events) {
|
|
254
|
+
const type = event.type;
|
|
255
|
+
if (type === "error" || event.error) {
|
|
256
|
+
streamError = event.error || {
|
|
257
|
+
type: "api_error",
|
|
258
|
+
message: event.message || "upstream error",
|
|
259
|
+
};
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (type === "message_start") {
|
|
263
|
+
sawAnthropicEvent = true;
|
|
264
|
+
if (event.message && typeof event.message === "object") message = { ...event.message };
|
|
265
|
+
} else if (type === "content_block_start") {
|
|
266
|
+
const idx = event.index;
|
|
267
|
+
if (
|
|
268
|
+
typeof idx === "number" &&
|
|
269
|
+
event.content_block &&
|
|
270
|
+
typeof event.content_block === "object"
|
|
271
|
+
) {
|
|
272
|
+
sawAnthropicEvent = true;
|
|
273
|
+
blocks.set(idx, { ...event.content_block });
|
|
274
|
+
if (!parts.has(idx)) parts.set(idx, []);
|
|
275
|
+
}
|
|
276
|
+
} else if (type === "content_block_delta") {
|
|
277
|
+
const idx = event.index;
|
|
278
|
+
const delta = event.delta;
|
|
279
|
+
if (typeof idx !== "number" || !delta || typeof delta !== "object") continue;
|
|
280
|
+
// A delta alone is not proof of dialect — OpenAI chunks also carry one.
|
|
281
|
+
// Only the Anthropic-typed `content_block_start` / `message_start` above
|
|
282
|
+
// are.
|
|
283
|
+
if (!blocks.has(idx)) {
|
|
284
|
+
blocks.set(idx, { type: CLAUDE_BLOCK.TEXT, text: "" });
|
|
285
|
+
parts.set(idx, []);
|
|
286
|
+
}
|
|
287
|
+
appendDelta(blocks.get(idx), parts.get(idx), delta);
|
|
288
|
+
} else if (type === "message_delta") {
|
|
289
|
+
const delta = event.delta;
|
|
290
|
+
if (delta && typeof delta === "object") {
|
|
291
|
+
if (delta.stop_reason) message.stop_reason = delta.stop_reason;
|
|
292
|
+
if ("stop_sequence" in delta) message.stop_sequence = delta.stop_sequence;
|
|
293
|
+
}
|
|
294
|
+
if (event.usage && typeof event.usage === "object") {
|
|
295
|
+
const merged = { ...(message.usage || {}) };
|
|
296
|
+
for (const [key, value] of Object.entries(event.usage)) {
|
|
297
|
+
if (value !== undefined) merged[key] = value;
|
|
298
|
+
}
|
|
299
|
+
message.usage = merged;
|
|
300
|
+
}
|
|
301
|
+
} else if (type === "message_stop") {
|
|
302
|
+
sawAnthropicEvent = true;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (streamError) return { error: streamError };
|
|
307
|
+
// No Anthropic lifecycle event at all: this is not our dialect (or there was
|
|
308
|
+
// nothing to read). Returning null lets the caller aggregate it as a chat
|
|
309
|
+
// stream instead of answering with the empty message this parser would build.
|
|
310
|
+
if (!sawAnthropicEvent) return null;
|
|
311
|
+
|
|
312
|
+
const content = [];
|
|
313
|
+
for (const idx of [...blocks.keys()].sort((a, b) => a - b)) {
|
|
314
|
+
const block = blocks.get(idx);
|
|
315
|
+
const accumulated = (parts.get(idx) || []).join("");
|
|
316
|
+
if (block.type === CLAUDE_BLOCK.TEXT) {
|
|
317
|
+
block.text = `${block.text || ""}${accumulated}`;
|
|
318
|
+
} else if (block.type === CLAUDE_BLOCK.THINKING) {
|
|
319
|
+
block.thinking = `${block.thinking || ""}${accumulated}`;
|
|
320
|
+
} else if (block.type === CLAUDE_BLOCK.TOOL_USE && accumulated) {
|
|
321
|
+
let input;
|
|
322
|
+
try {
|
|
323
|
+
input = JSON.parse(accumulated);
|
|
324
|
+
} catch {
|
|
325
|
+
input = undefined;
|
|
326
|
+
}
|
|
327
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
328
|
+
// The client cannot call a tool whose input is not an object; failing
|
|
329
|
+
// here is better than emitting a half-parsed tool_use block.
|
|
330
|
+
throw new Error("Claude tool_use input is not a JSON object");
|
|
331
|
+
}
|
|
332
|
+
block.input = input;
|
|
333
|
+
}
|
|
334
|
+
content.push(block);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// A stream that only carried message_start/stop keeps whatever it declared.
|
|
338
|
+
if (content.length === 0 && Array.isArray(message.content) && message.content.length > 0) {
|
|
339
|
+
content.push(...message.content);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const usage = message.usage && typeof message.usage === "object" ? { ...message.usage } : {};
|
|
343
|
+
if (usage.input_tokens === undefined) usage.input_tokens = 0;
|
|
344
|
+
if (usage.output_tokens === undefined) usage.output_tokens = 0;
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
...message,
|
|
348
|
+
id: message.id || `msg_${Date.now()}`,
|
|
349
|
+
type: "message",
|
|
350
|
+
role: ROLE.ASSISTANT,
|
|
351
|
+
model: message.model || fallbackModel || "unknown",
|
|
352
|
+
content,
|
|
353
|
+
stop_reason: message.stop_reason || "end_turn",
|
|
354
|
+
stop_sequence: message.stop_sequence ?? null,
|
|
355
|
+
usage,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { FORMATS } from "../../translator/formats.js";
|
|
2
2
|
import { needsTranslation } from "../../translator/index.js";
|
|
3
|
-
import { fromOpenAIFinish } from "../../translator/concerns/finishReason.js";
|
|
4
3
|
import { addBufferToUsage, filterUsageForFormat } from "../../utils/usageTracking.js";
|
|
5
4
|
import { createErrorResult } from "../../utils/error.js";
|
|
6
5
|
import { HTTP_STATUS } from "../../config/runtimeConfig.js";
|
|
@@ -16,57 +15,11 @@ import { appendRequestLog, saveRequestDetail } from "@/lib/usageDb.js";
|
|
|
16
15
|
import { decloakToolNames } from "../../utils/claudeCloaking.js";
|
|
17
16
|
import { restoreToolNames } from "../../utils/opencodeFingerprint.js";
|
|
18
17
|
import { ROLE, RESPONSES_ITEM } from "../../translator/schema/index.js";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return JSON.parse(value);
|
|
25
|
-
} catch {
|
|
26
|
-
return {};
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function openAICompletionToClaudeMessage(responseBody) {
|
|
31
|
-
if (!responseBody?.choices?.[0]) return responseBody;
|
|
32
|
-
const choice = responseBody.choices[0];
|
|
33
|
-
const message = choice.message || {};
|
|
34
|
-
const content = [];
|
|
35
|
-
|
|
36
|
-
const reasoning =
|
|
37
|
-
message.reasoning_content || message.provider_specific_fields?.reasoning_content || "";
|
|
38
|
-
if (reasoning) {
|
|
39
|
-
content.push({ type: "thinking", thinking: reasoning });
|
|
40
|
-
}
|
|
41
|
-
if (typeof message.content === "string" && message.content.length > 0) {
|
|
42
|
-
content.push({ type: "text", text: message.content });
|
|
43
|
-
}
|
|
44
|
-
for (const toolCall of message.tool_calls || []) {
|
|
45
|
-
const fn = toolCall.function || {};
|
|
46
|
-
content.push({
|
|
47
|
-
type: "tool_use",
|
|
48
|
-
id: toolCall.id || `toolu_${Date.now()}_${content.length}`,
|
|
49
|
-
name: fn.name || toolCall.name || "",
|
|
50
|
-
input: parseToolArguments(fn.arguments || toolCall.arguments),
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
if (content.length === 0) content.push({ type: "text", text: "" });
|
|
54
|
-
|
|
55
|
-
const usage = responseBody.usage || {};
|
|
56
|
-
return {
|
|
57
|
-
id: String(responseBody.id || `msg_${Date.now()}`).replace(/^chatcmpl-/, ""),
|
|
58
|
-
type: "message",
|
|
59
|
-
role: "assistant",
|
|
60
|
-
model: responseBody.model || "unknown",
|
|
61
|
-
content,
|
|
62
|
-
stop_reason: fromOpenAIFinish(choice.finish_reason, FORMATS.CLAUDE),
|
|
63
|
-
stop_sequence: null,
|
|
64
|
-
usage: {
|
|
65
|
-
input_tokens: usage.prompt_tokens || usage.input_tokens || 0,
|
|
66
|
-
output_tokens: usage.completion_tokens || usage.output_tokens || 0,
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
}
|
|
18
|
+
import {
|
|
19
|
+
chatCompletionToClaudeMessage,
|
|
20
|
+
claudeMessageToChatCompletion,
|
|
21
|
+
parseClaudeSSEToMessage,
|
|
22
|
+
} from "./claudeMessageBody.js";
|
|
70
23
|
|
|
71
24
|
/**
|
|
72
25
|
* Convert an OpenAI Chat Completions non-streaming response body into the
|
|
@@ -174,7 +127,7 @@ export function translateNonStreamingResponse(
|
|
|
174
127
|
return openAICompletionToResponses(responseBody, customToolNames);
|
|
175
128
|
}
|
|
176
129
|
if (targetFormat === FORMATS.OPENAI && sourceFormat === FORMATS.CLAUDE) {
|
|
177
|
-
return
|
|
130
|
+
return chatCompletionToClaudeMessage(responseBody);
|
|
178
131
|
}
|
|
179
132
|
if (targetFormat === FORMATS.OPENAI) return responseBody;
|
|
180
133
|
|
|
@@ -240,7 +193,9 @@ export function translateNonStreamingResponse(
|
|
|
240
193
|
result.usage.completion_tokens_details = { reasoning_tokens: usage.thoughtsTokenCount };
|
|
241
194
|
}
|
|
242
195
|
}
|
|
243
|
-
|
|
196
|
+
// Gemini is the last branch that could hand a Claude client a
|
|
197
|
+
// `chat.completion`; the client reads `message.content[]` and throws.
|
|
198
|
+
return sourceFormat === FORMATS.CLAUDE ? chatCompletionToClaudeMessage(result) : result;
|
|
244
199
|
}
|
|
245
200
|
|
|
246
201
|
// Claude
|
|
@@ -249,61 +204,9 @@ export function translateNonStreamingResponse(
|
|
|
249
204
|
// missing/null (e.g. M3 with max_tokens:1 spends the budget on thinking
|
|
250
205
|
// and returns `content: null`). Returning the raw body would leave the
|
|
251
206
|
// OpenAI client without a `choices` array and surface as a UI test error.
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
// the request was translated to Claude format — the targetFormat is Claude but the
|
|
256
|
-
// actual response is OpenAI-native and needs no further translation.
|
|
257
|
-
if (responseBody.choices || (responseBody.content && !Array.isArray(responseBody.content)))
|
|
258
|
-
return responseBody;
|
|
259
|
-
|
|
260
|
-
let textContent = "",
|
|
261
|
-
thinkingContent = "";
|
|
262
|
-
const toolCalls = [];
|
|
263
|
-
|
|
264
|
-
for (const block of responseBody.content || []) {
|
|
265
|
-
if (block.type === "text") {
|
|
266
|
-
// Strip markdown code block markers (e.g. kimi wraps JSON in ```json...```)
|
|
267
|
-
const raw = block.text ?? "";
|
|
268
|
-
const text = raw.replace(/^\s*```\s*json\s*\n?/i, "").replace(/\n?\s*```\s*$/i, "");
|
|
269
|
-
textContent += text;
|
|
270
|
-
} else if (block.type === "thinking") thinkingContent += block.thinking || "";
|
|
271
|
-
else if (block.type === "tool_use") {
|
|
272
|
-
toolCalls.push({
|
|
273
|
-
id: block.id,
|
|
274
|
-
type: "function",
|
|
275
|
-
function: { name: block.name, arguments: JSON.stringify(block.input || {}) },
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const message = { role: "assistant" };
|
|
281
|
-
if (textContent) message.content = textContent;
|
|
282
|
-
if (thinkingContent) message.reasoning_content = thinkingContent;
|
|
283
|
-
if (toolCalls.length > 0) message.tool_calls = toolCalls;
|
|
284
|
-
if (!message.content && !message.tool_calls) message.content = "";
|
|
285
|
-
|
|
286
|
-
let finishReason = responseBody.stop_reason || "stop";
|
|
287
|
-
if (finishReason === "end_turn") finishReason = "stop";
|
|
288
|
-
if (finishReason === "tool_use") finishReason = "tool_calls";
|
|
289
|
-
|
|
290
|
-
const result = {
|
|
291
|
-
id: `chatcmpl-${responseBody.id || Date.now()}`,
|
|
292
|
-
object: "chat.completion",
|
|
293
|
-
created: Math.floor(Date.now() / 1000),
|
|
294
|
-
model: responseBody.model || "claude",
|
|
295
|
-
choices: [{ index: 0, message, finish_reason: finishReason }],
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
if (responseBody.usage) {
|
|
299
|
-
result.usage = {
|
|
300
|
-
prompt_tokens: responseBody.usage.input_tokens || 0,
|
|
301
|
-
completion_tokens: responseBody.usage.output_tokens || 0,
|
|
302
|
-
total_tokens:
|
|
303
|
-
(responseBody.usage.input_tokens || 0) + (responseBody.usage.output_tokens || 0),
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
return result;
|
|
207
|
+
// A body already in OpenAI shape (or with non-array content) passes through
|
|
208
|
+
// untouched — some providers answer chat-native regardless of the request.
|
|
209
|
+
return claudeMessageToChatCompletion(responseBody);
|
|
307
210
|
}
|
|
308
211
|
|
|
309
212
|
return responseBody;
|
|
@@ -342,7 +245,30 @@ export async function handleNonStreamingResponse({
|
|
|
342
245
|
|
|
343
246
|
if (contentType.includes("text/event-stream")) {
|
|
344
247
|
const sseText = await providerResponse.text();
|
|
345
|
-
|
|
248
|
+
// The upstream chose to stream a body we wanted as JSON. Aggregate in the
|
|
249
|
+
// format it actually used: a Claude-dialect provider emits Anthropic events
|
|
250
|
+
// that the chat aggregator would fold into an empty message. The parser
|
|
251
|
+
// returns null for a non-Anthropic body, and throws on a tool block whose
|
|
252
|
+
// arguments are not an object — both must reach the caller as an error
|
|
253
|
+
// result, never as an uncaught throw.
|
|
254
|
+
let claudeMessage = null;
|
|
255
|
+
try {
|
|
256
|
+
const isClaudeUpstream =
|
|
257
|
+
targetFormat === FORMATS.CLAUDE || /"type"\s*:\s*"message_start"/.test(sseText);
|
|
258
|
+
claudeMessage = isClaudeUpstream ? parseClaudeSSEToMessage(sseText, model) : null;
|
|
259
|
+
} catch (err) {
|
|
260
|
+
appendLog({ status: `FAILED ${HTTP_STATUS.BAD_GATEWAY}` });
|
|
261
|
+
log?.error?.("CHAT", `Claude SSE aggregation failed for ${provider}: ${err.message}`);
|
|
262
|
+
return createErrorResult(HTTP_STATUS.BAD_GATEWAY, err.message);
|
|
263
|
+
}
|
|
264
|
+
if (claudeMessage?.error) {
|
|
265
|
+
appendLog({ status: `FAILED ${HTTP_STATUS.BAD_GATEWAY}` });
|
|
266
|
+
return createErrorResult(
|
|
267
|
+
HTTP_STATUS.BAD_GATEWAY,
|
|
268
|
+
claudeMessage.error.message || "Upstream SSE stream failed",
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
const parsed = claudeMessage || parseSSEToOpenAIResponse(sseText, model);
|
|
346
272
|
if (!parsed) {
|
|
347
273
|
appendLog({ status: `FAILED ${HTTP_STATUS.BAD_GATEWAY}` });
|
|
348
274
|
return createErrorResult(
|