@sentropic/h2a-runtime 0.91.0 → 0.92.0
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/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +16 -3
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +79 -58
- package/dist/index.js.map +1 -1
- package/dist/llm-gateway-runtime/index.d.ts +6 -0
- package/dist/llm-gateway-runtime/index.d.ts.map +1 -1
- package/dist/llm-gateway-runtime/index.js +134 -60
- package/dist/llm-gateway-runtime/index.js.map +1 -1
- package/dist/llm-gateway-runtime/session-ledger.d.ts +38 -55
- package/dist/llm-gateway-runtime/session-ledger.d.ts.map +1 -1
- package/dist/llm-gateway-runtime/session-ledger.js +102 -250
- package/dist/llm-gateway-runtime/session-ledger.js.map +1 -1
- package/dist/llm-gateway-runtime/sticky.d.ts +25 -52
- package/dist/llm-gateway-runtime/sticky.d.ts.map +1 -1
- package/dist/llm-gateway-runtime/sticky.js +36 -605
- package/dist/llm-gateway-runtime/sticky.js.map +1 -1
- package/dist/llm-mesh.d.ts +19 -72
- package/dist/llm-mesh.d.ts.map +1 -1
- package/dist/llm-mesh.js +42 -399
- package/dist/llm-mesh.js.map +1 -1
- package/dist/llm-routing-config.d.ts +28 -0
- package/dist/llm-routing-config.d.ts.map +1 -0
- package/dist/llm-routing-config.js +100 -0
- package/dist/llm-routing-config.js.map +1 -0
- package/dist/native-terminal/client.d.ts +23 -0
- package/dist/native-terminal/client.d.ts.map +1 -0
- package/dist/native-terminal/client.js +203 -0
- package/dist/native-terminal/client.js.map +1 -0
- package/dist/native-terminal/host.d.ts +78 -0
- package/dist/native-terminal/host.d.ts.map +1 -0
- package/dist/native-terminal/host.js +275 -0
- package/dist/native-terminal/host.js.map +1 -0
- package/dist/native-terminal/process.d.ts +11 -0
- package/dist/native-terminal/process.d.ts.map +1 -0
- package/dist/native-terminal/process.js +137 -0
- package/dist/native-terminal/process.js.map +1 -0
- package/dist/native-terminal/protocol.d.ts +57 -0
- package/dist/native-terminal/protocol.d.ts.map +1 -0
- package/dist/native-terminal/protocol.js +102 -0
- package/dist/native-terminal/protocol.js.map +1 -0
- package/dist/native-terminal/replay-buffer.d.ts +26 -0
- package/dist/native-terminal/replay-buffer.d.ts.map +1 -0
- package/dist/native-terminal/replay-buffer.js +84 -0
- package/dist/native-terminal/replay-buffer.js.map +1 -0
- package/dist/native-terminal/server.d.ts +13 -0
- package/dist/native-terminal/server.d.ts.map +1 -0
- package/dist/native-terminal/server.js +555 -0
- package/dist/native-terminal/server.js.map +1 -0
- package/dist/native-terminal/socket-path.d.ts +8 -0
- package/dist/native-terminal/socket-path.d.ts.map +1 -0
- package/dist/native-terminal/socket-path.js +41 -0
- package/dist/native-terminal/socket-path.js.map +1 -0
- package/dist/native-terminal/supervisor.d.ts +26 -0
- package/dist/native-terminal/supervisor.d.ts.map +1 -0
- package/dist/native-terminal/supervisor.js +273 -0
- package/dist/native-terminal/supervisor.js.map +1 -0
- package/dist/pty.d.ts +1 -0
- package/dist/pty.d.ts.map +1 -1
- package/dist/pty.js +78 -2
- package/dist/pty.js.map +1 -1
- package/dist/registry.d.ts +7 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +37 -1
- package/dist/registry.js.map +1 -1
- package/dist/status-projection.d.ts.map +1 -1
- package/dist/tmux.d.ts +7 -5
- package/dist/tmux.d.ts.map +1 -1
- package/dist/tmux.js +33 -7
- package/dist/tmux.js.map +1 -1
- package/package.json +3 -3
- package/dist/llm-gateway-runtime/accounts.d.ts +0 -62
- package/dist/llm-gateway-runtime/accounts.d.ts.map +0 -1
- package/dist/llm-gateway-runtime/accounts.js +0 -323
- package/dist/llm-gateway-runtime/accounts.js.map +0 -1
- package/dist/llm-gateway-runtime/model-catalog.d.ts +0 -44
- package/dist/llm-gateway-runtime/model-catalog.d.ts.map +0 -1
- package/dist/llm-gateway-runtime/model-catalog.js +0 -191
- package/dist/llm-gateway-runtime/model-catalog.js.map +0 -1
- package/dist/llm-gateway-runtime/proxy-anthropic.d.ts +0 -3
- package/dist/llm-gateway-runtime/proxy-anthropic.d.ts.map +0 -1
- package/dist/llm-gateway-runtime/proxy-anthropic.js +0 -318
- package/dist/llm-gateway-runtime/proxy-anthropic.js.map +0 -1
- package/dist/llm-gateway-runtime/proxy-cloud-code.d.ts +0 -11
- package/dist/llm-gateway-runtime/proxy-cloud-code.d.ts.map +0 -1
- package/dist/llm-gateway-runtime/proxy-cloud-code.js +0 -215
- package/dist/llm-gateway-runtime/proxy-cloud-code.js.map +0 -1
- package/dist/llm-gateway-runtime/proxy-openai.d.ts +0 -118
- package/dist/llm-gateway-runtime/proxy-openai.d.ts.map +0 -1
- package/dist/llm-gateway-runtime/proxy-openai.js +0 -1305
- package/dist/llm-gateway-runtime/proxy-openai.js.map +0 -1
|
@@ -1,1305 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Anthropic Messages API → OpenAI Chat Completions proxy.
|
|
3
|
-
*
|
|
4
|
-
* Activated when the account's provider is "openai" or "codex". Translates
|
|
5
|
-
* requests and responses between the two formats so Claude Code (which speaks
|
|
6
|
-
* Anthropic) can transparently use OpenAI/Codex models.
|
|
7
|
-
*
|
|
8
|
-
* Model routing comes from @sentropic/llm-gateway's canonical descriptions,
|
|
9
|
-
* with OPENAI_MODEL_MAP retained only as an explicit operator override.
|
|
10
|
-
*
|
|
11
|
-
* Thinking budget_tokens → reasoning effort:
|
|
12
|
-
* ≥ 50 000 (xhigh) → "xhigh"
|
|
13
|
-
* ≥ 25 000 (high) → "high"
|
|
14
|
-
* ≥ 8 000 (med) → "medium"
|
|
15
|
-
* < 8 000 / none → "low"
|
|
16
|
-
*/
|
|
17
|
-
import { CODEX_RESPONSES_URL } from "@sentropic/llm-gateway";
|
|
18
|
-
import { isCodexOAuthToken, refreshOAuthToken, } from "./accounts.js";
|
|
19
|
-
import { routeModelOrThrow } from "./model-catalog.js";
|
|
20
|
-
const OPENAI_BASE = process.env.OPENAI_UPSTREAM_URL ?? "https://api.openai.com";
|
|
21
|
-
const DEFAULT_CODEX_MAX_INPUT_CHARS = 200_000;
|
|
22
|
-
const CODEX_CONTEXT_TRUNCATION_NOTICE = "[llm-gateway: older Claude Code transcript omitted to fit the Codex upstream context window.]";
|
|
23
|
-
const TRANSIENT_UPSTREAM_ERROR_CODES = new Set([
|
|
24
|
-
"EAI_AGAIN",
|
|
25
|
-
"ECONNRESET",
|
|
26
|
-
"ETIMEDOUT",
|
|
27
|
-
"UND_ERR_CONNECT_TIMEOUT",
|
|
28
|
-
"UND_ERR_HEADERS_TIMEOUT",
|
|
29
|
-
"UND_ERR_SOCKET",
|
|
30
|
-
]);
|
|
31
|
-
const UPSTREAM_RETRY_DELAYS_MS = process.env.NODE_ENV === "test" ? [0, 0] : [250, 1000];
|
|
32
|
-
function abortError(err) {
|
|
33
|
-
return err.name === "AbortError";
|
|
34
|
-
}
|
|
35
|
-
function upstreamErrorCause(err) {
|
|
36
|
-
const candidate = err;
|
|
37
|
-
if (candidate.code || candidate.hostname)
|
|
38
|
-
return candidate;
|
|
39
|
-
return candidate.cause;
|
|
40
|
-
}
|
|
41
|
-
function upstreamFetchErrorMessage(err) {
|
|
42
|
-
const cause = upstreamErrorCause(err);
|
|
43
|
-
if (cause?.code && cause.hostname)
|
|
44
|
-
return `${cause.code} ${cause.hostname}`;
|
|
45
|
-
if (cause?.code)
|
|
46
|
-
return cause.code;
|
|
47
|
-
if (err instanceof Error && err.message)
|
|
48
|
-
return err.message;
|
|
49
|
-
return "unknown upstream fetch error";
|
|
50
|
-
}
|
|
51
|
-
function isTransientUpstreamError(err) {
|
|
52
|
-
const code = upstreamErrorCause(err)?.code;
|
|
53
|
-
return !!code && TRANSIENT_UPSTREAM_ERROR_CODES.has(code);
|
|
54
|
-
}
|
|
55
|
-
function anthropicGatewayError(message) {
|
|
56
|
-
return { type: "error", error: { type: "api_error", message } };
|
|
57
|
-
}
|
|
58
|
-
function asRecord(value) {
|
|
59
|
-
return value && typeof value === "object" && !Array.isArray(value)
|
|
60
|
-
? value
|
|
61
|
-
: undefined;
|
|
62
|
-
}
|
|
63
|
-
function prepareCodexResponsesRequest(request) {
|
|
64
|
-
const { max_output_tokens: _maxOutputTokens, ...rest } = request;
|
|
65
|
-
return {
|
|
66
|
-
url: CODEX_RESPONSES_URL,
|
|
67
|
-
body: {
|
|
68
|
-
...rest,
|
|
69
|
-
store: false,
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
function codexRequestEffort(body) {
|
|
74
|
-
const reasoning = asRecord(body.reasoning);
|
|
75
|
-
const effort = reasoning?.effort;
|
|
76
|
-
return typeof effort === "string" ? effort : undefined;
|
|
77
|
-
}
|
|
78
|
-
async function refreshOAuthTokenForRetry(accountId) {
|
|
79
|
-
try {
|
|
80
|
-
return await refreshOAuthToken(accountId);
|
|
81
|
-
}
|
|
82
|
-
catch (err) {
|
|
83
|
-
console.error(`[llm-gateway] OAuth refresh failed for ${accountId}:`, err);
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
async function sleep(ms) {
|
|
88
|
-
if (ms <= 0)
|
|
89
|
-
return;
|
|
90
|
-
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
91
|
-
}
|
|
92
|
-
async function fetchUpstreamWithRetry(fetcher) {
|
|
93
|
-
for (let attempt = 0;; attempt += 1) {
|
|
94
|
-
try {
|
|
95
|
-
return await fetcher();
|
|
96
|
-
}
|
|
97
|
-
catch (err) {
|
|
98
|
-
if (abortError(err) ||
|
|
99
|
-
!isTransientUpstreamError(err) ||
|
|
100
|
-
attempt >= UPSTREAM_RETRY_DELAYS_MS.length) {
|
|
101
|
-
throw err;
|
|
102
|
-
}
|
|
103
|
-
await sleep(UPSTREAM_RETRY_DELAYS_MS[attempt] ?? 0);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
// ---------------------------------------------------------------------------
|
|
108
|
-
// Model mapping
|
|
109
|
-
// ---------------------------------------------------------------------------
|
|
110
|
-
export function mapModel(anthropicModel) {
|
|
111
|
-
return routeModelOrThrow(anthropicModel).upstreamModel ?? anthropicModel;
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Anthropic thinking budget_tokens → OpenAI reasoning_effort.
|
|
115
|
-
* Covers all 4 Claude effort tiers:
|
|
116
|
-
* xhigh (≥ 50 k) → "xhigh" (pass-through; upstream rejection stays explicit)
|
|
117
|
-
* high (≥ 25 k) → "high"
|
|
118
|
-
* med (≥ 8 k) → "medium"
|
|
119
|
-
* low (< 8 k) → "low"
|
|
120
|
-
*/
|
|
121
|
-
export function budgetToEffort(budgetTokens) {
|
|
122
|
-
if (budgetTokens >= 50_000)
|
|
123
|
-
return "xhigh";
|
|
124
|
-
if (budgetTokens >= 25_000)
|
|
125
|
-
return "high";
|
|
126
|
-
if (budgetTokens >= 8_000)
|
|
127
|
-
return "medium";
|
|
128
|
-
return "low";
|
|
129
|
-
}
|
|
130
|
-
const EMPTY_TOOL_RESULT_PLACEHOLDER = "[llm-gateway: empty tool result omitted.]";
|
|
131
|
-
function unsupportedContentPlaceholder(block) {
|
|
132
|
-
const record = block;
|
|
133
|
-
const source = asRecord(record.source);
|
|
134
|
-
const mediaType = typeof source?.media_type === "string"
|
|
135
|
-
? source.media_type
|
|
136
|
-
: typeof record.media_type === "string"
|
|
137
|
-
? record.media_type
|
|
138
|
-
: undefined;
|
|
139
|
-
const label = mediaType ? `${block.type} ${mediaType}` : block.type;
|
|
140
|
-
return `[llm-gateway: unsupported ${label} content omitted.]`;
|
|
141
|
-
}
|
|
142
|
-
function normalizeUnsupportedContentBlocks(content) {
|
|
143
|
-
if (typeof content === "string")
|
|
144
|
-
return content;
|
|
145
|
-
return content.map((block) => {
|
|
146
|
-
if (block.type === "text" ||
|
|
147
|
-
block.type === "tool_use" ||
|
|
148
|
-
block.type === "thinking") {
|
|
149
|
-
return block;
|
|
150
|
-
}
|
|
151
|
-
if (block.type === "tool_result") {
|
|
152
|
-
const tr = block;
|
|
153
|
-
return {
|
|
154
|
-
...tr,
|
|
155
|
-
content: typeof tr.content === "string"
|
|
156
|
-
? tr.content
|
|
157
|
-
: normalizeUnsupportedContentBlocks(tr.content),
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
return {
|
|
161
|
-
type: "text",
|
|
162
|
-
text: unsupportedContentPlaceholder(block),
|
|
163
|
-
};
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
function normalizeUnsupportedRequestContent(body) {
|
|
167
|
-
const normalized = {
|
|
168
|
-
...body,
|
|
169
|
-
messages: body.messages.map((message) => ({
|
|
170
|
-
...message,
|
|
171
|
-
content: normalizeUnsupportedContentBlocks(message.content),
|
|
172
|
-
})),
|
|
173
|
-
};
|
|
174
|
-
if (Array.isArray(body.system)) {
|
|
175
|
-
normalized.system = normalizeUnsupportedContentBlocks(body.system);
|
|
176
|
-
}
|
|
177
|
-
else if (body.system !== undefined) {
|
|
178
|
-
normalized.system = body.system;
|
|
179
|
-
}
|
|
180
|
-
return normalized;
|
|
181
|
-
}
|
|
182
|
-
function toolResultContentToText(content) {
|
|
183
|
-
const text = typeof content === "string"
|
|
184
|
-
? content
|
|
185
|
-
: normalizeUnsupportedContentBlocks(content)
|
|
186
|
-
.map((block) => block.type === "text"
|
|
187
|
-
? block.text
|
|
188
|
-
: unsupportedContentPlaceholder(block))
|
|
189
|
-
.join("");
|
|
190
|
-
return text.length > 0 ? text : EMPTY_TOOL_RESULT_PLACEHOLDER;
|
|
191
|
-
}
|
|
192
|
-
function extractAssistantContent(content) {
|
|
193
|
-
if (typeof content === "string")
|
|
194
|
-
return { text: content || null, toolCalls: [] };
|
|
195
|
-
const texts = [];
|
|
196
|
-
const toolCalls = [];
|
|
197
|
-
for (const item of content) {
|
|
198
|
-
if (item.type === "text") {
|
|
199
|
-
texts.push(item.text);
|
|
200
|
-
}
|
|
201
|
-
else if (item.type === "tool_use") {
|
|
202
|
-
const tc = item;
|
|
203
|
-
toolCalls.push({
|
|
204
|
-
id: tc.id,
|
|
205
|
-
type: "function",
|
|
206
|
-
function: { name: tc.name, arguments: JSON.stringify(tc.input) },
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
// thinking blocks: dropped (OpenAI reasons internally)
|
|
210
|
-
}
|
|
211
|
-
return { text: texts.join("") || null, toolCalls };
|
|
212
|
-
}
|
|
213
|
-
function toOAIMessages(messages) {
|
|
214
|
-
const result = [];
|
|
215
|
-
for (const msg of messages) {
|
|
216
|
-
if (msg.role === "assistant") {
|
|
217
|
-
const { text, toolCalls } = extractAssistantContent(msg.content);
|
|
218
|
-
const entry = { role: "assistant", content: text };
|
|
219
|
-
if (toolCalls.length > 0)
|
|
220
|
-
entry.tool_calls = toolCalls;
|
|
221
|
-
result.push(entry);
|
|
222
|
-
}
|
|
223
|
-
else {
|
|
224
|
-
// user: may contain tool_result blocks
|
|
225
|
-
if (Array.isArray(msg.content)) {
|
|
226
|
-
for (const item of msg.content) {
|
|
227
|
-
if (item.type === "tool_result") {
|
|
228
|
-
const tr = item;
|
|
229
|
-
result.push({
|
|
230
|
-
role: "tool",
|
|
231
|
-
tool_call_id: tr.tool_use_id,
|
|
232
|
-
content: toolResultContentToText(tr.content),
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
const textItems = msg.content.filter((c) => c.type === "text");
|
|
237
|
-
if (textItems.length > 0) {
|
|
238
|
-
result.push({
|
|
239
|
-
role: "user",
|
|
240
|
-
content: textItems.map((t) => ({
|
|
241
|
-
type: "text",
|
|
242
|
-
text: t.text,
|
|
243
|
-
})),
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
else {
|
|
248
|
-
result.push({ role: "user", content: msg.content });
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
return result;
|
|
253
|
-
}
|
|
254
|
-
function systemToText(system) {
|
|
255
|
-
if (typeof system === "string")
|
|
256
|
-
return system || undefined;
|
|
257
|
-
if (!Array.isArray(system))
|
|
258
|
-
return undefined;
|
|
259
|
-
const parts = [];
|
|
260
|
-
for (const block of system) {
|
|
261
|
-
if (block.type === "text") {
|
|
262
|
-
parts.push(block.text);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
return parts.join("\n\n") || undefined;
|
|
266
|
-
}
|
|
267
|
-
// ---------------------------------------------------------------------------
|
|
268
|
-
// Request translation: Anthropic → OpenAI Responses API (Codex OAuth path)
|
|
269
|
-
// ---------------------------------------------------------------------------
|
|
270
|
-
function toCodexInput(messages) {
|
|
271
|
-
const items = [];
|
|
272
|
-
for (const msg of messages) {
|
|
273
|
-
if (msg.role === "assistant") {
|
|
274
|
-
const texts = [];
|
|
275
|
-
const toolCalls = [];
|
|
276
|
-
if (typeof msg.content === "string") {
|
|
277
|
-
if (msg.content)
|
|
278
|
-
texts.push(msg.content);
|
|
279
|
-
}
|
|
280
|
-
else {
|
|
281
|
-
for (const block of msg.content) {
|
|
282
|
-
if (block.type === "text")
|
|
283
|
-
texts.push(block.text);
|
|
284
|
-
else if (block.type === "tool_use")
|
|
285
|
-
toolCalls.push(block);
|
|
286
|
-
// thinking blocks: skip
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
const text = texts.join("");
|
|
290
|
-
if (text || toolCalls.length === 0) {
|
|
291
|
-
items.push({ type: "message", role: "assistant", content: text });
|
|
292
|
-
}
|
|
293
|
-
for (const tc of toolCalls) {
|
|
294
|
-
items.push({
|
|
295
|
-
type: "function_call",
|
|
296
|
-
call_id: tc.id,
|
|
297
|
-
name: tc.name,
|
|
298
|
-
arguments: JSON.stringify(tc.input),
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
else {
|
|
303
|
-
const texts = [];
|
|
304
|
-
const toolResults = [];
|
|
305
|
-
if (typeof msg.content === "string") {
|
|
306
|
-
if (msg.content)
|
|
307
|
-
texts.push(msg.content);
|
|
308
|
-
}
|
|
309
|
-
else {
|
|
310
|
-
for (const block of msg.content) {
|
|
311
|
-
if (block.type === "text")
|
|
312
|
-
texts.push(block.text);
|
|
313
|
-
else if (block.type === "tool_result")
|
|
314
|
-
toolResults.push(block);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
for (const tr of toolResults) {
|
|
318
|
-
items.push({
|
|
319
|
-
type: "function_call_output",
|
|
320
|
-
call_id: tr.tool_use_id,
|
|
321
|
-
output: toolResultContentToText(tr.content),
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
const text = texts.join("");
|
|
325
|
-
if (text)
|
|
326
|
-
items.push({ type: "message", role: "user", content: text });
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
return items;
|
|
330
|
-
}
|
|
331
|
-
function codexEffort(budgetTokens) {
|
|
332
|
-
return budgetToEffort(budgetTokens);
|
|
333
|
-
}
|
|
334
|
-
export function toCodexRequest(body, canonicalEffort) {
|
|
335
|
-
body = normalizeUnsupportedRequestContent(body);
|
|
336
|
-
const req = {
|
|
337
|
-
model: mapModel(body.model),
|
|
338
|
-
input: toCodexInput(body.messages),
|
|
339
|
-
store: false,
|
|
340
|
-
stream: true,
|
|
341
|
-
};
|
|
342
|
-
const instructions = systemToText(body.system);
|
|
343
|
-
if (instructions)
|
|
344
|
-
req.instructions = instructions;
|
|
345
|
-
const effort = canonicalEffort ??
|
|
346
|
-
(body.thinking?.type === "enabled"
|
|
347
|
-
? codexEffort(body.thinking.budget_tokens)
|
|
348
|
-
: undefined);
|
|
349
|
-
if (effort) {
|
|
350
|
-
req.reasoning = { effort };
|
|
351
|
-
}
|
|
352
|
-
if (body.tools && body.tools.length > 0) {
|
|
353
|
-
req.tools = body.tools.map((t) => ({
|
|
354
|
-
type: "function",
|
|
355
|
-
name: t.name,
|
|
356
|
-
...(t.description !== undefined ? { description: t.description } : {}),
|
|
357
|
-
parameters: t.input_schema,
|
|
358
|
-
strict: false,
|
|
359
|
-
}));
|
|
360
|
-
}
|
|
361
|
-
return req;
|
|
362
|
-
}
|
|
363
|
-
function codexMaxInputChars() {
|
|
364
|
-
const configured = Number.parseInt(process.env.CODEX_MAX_INPUT_CHARS ?? "", 10);
|
|
365
|
-
return Number.isFinite(configured) && configured > 0
|
|
366
|
-
? configured
|
|
367
|
-
: DEFAULT_CODEX_MAX_INPUT_CHARS;
|
|
368
|
-
}
|
|
369
|
-
function anthropicInputChars(body) {
|
|
370
|
-
return JSON.stringify({ system: body.system, messages: body.messages })
|
|
371
|
-
.length;
|
|
372
|
-
}
|
|
373
|
-
function trimTextTail(text, budget) {
|
|
374
|
-
if (text.length <= budget)
|
|
375
|
-
return text;
|
|
376
|
-
if (budget <= CODEX_CONTEXT_TRUNCATION_NOTICE.length + 8) {
|
|
377
|
-
return CODEX_CONTEXT_TRUNCATION_NOTICE.slice(0, Math.max(0, budget));
|
|
378
|
-
}
|
|
379
|
-
const prefix = `${CODEX_CONTEXT_TRUNCATION_NOTICE}\n\n`;
|
|
380
|
-
return prefix + text.slice(-(budget - prefix.length));
|
|
381
|
-
}
|
|
382
|
-
function trimContentTail(content, budget) {
|
|
383
|
-
if (typeof content === "string")
|
|
384
|
-
return trimTextTail(content, budget);
|
|
385
|
-
const kept = [];
|
|
386
|
-
let remaining = budget;
|
|
387
|
-
for (let i = content.length - 1; i >= 0; i -= 1) {
|
|
388
|
-
const block = content[i];
|
|
389
|
-
const size = JSON.stringify(block).length;
|
|
390
|
-
if (size <= remaining) {
|
|
391
|
-
kept.unshift(block);
|
|
392
|
-
remaining -= size;
|
|
393
|
-
continue;
|
|
394
|
-
}
|
|
395
|
-
if (block.type === "text" &&
|
|
396
|
-
remaining > CODEX_CONTEXT_TRUNCATION_NOTICE.length + 32) {
|
|
397
|
-
kept.unshift({
|
|
398
|
-
...block,
|
|
399
|
-
text: trimTextTail(block.text, remaining),
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
break;
|
|
403
|
-
}
|
|
404
|
-
return kept.length > 0
|
|
405
|
-
? kept
|
|
406
|
-
: [{ type: "text", text: CODEX_CONTEXT_TRUNCATION_NOTICE }];
|
|
407
|
-
}
|
|
408
|
-
function stripLeadingOrphanToolResults(messages) {
|
|
409
|
-
if (messages.length === 0)
|
|
410
|
-
return messages;
|
|
411
|
-
const first = messages[0];
|
|
412
|
-
if (first.role !== "user" || !Array.isArray(first.content))
|
|
413
|
-
return messages;
|
|
414
|
-
const filtered = first.content.filter((block) => block.type !== "tool_result");
|
|
415
|
-
if (filtered.length === first.content.length)
|
|
416
|
-
return messages;
|
|
417
|
-
if (filtered.length === 0)
|
|
418
|
-
return messages.slice(1);
|
|
419
|
-
return [{ ...first, content: filtered }, ...messages.slice(1)];
|
|
420
|
-
}
|
|
421
|
-
export function trimCodexBodyForContext(body, maxChars = codexMaxInputChars()) {
|
|
422
|
-
body = normalizeUnsupportedRequestContent(body);
|
|
423
|
-
const beforeChars = anthropicInputChars(body);
|
|
424
|
-
if (beforeChars <= maxChars) {
|
|
425
|
-
return { body, trimmed: false, beforeChars, afterChars: beforeChars };
|
|
426
|
-
}
|
|
427
|
-
const notice = {
|
|
428
|
-
role: "user",
|
|
429
|
-
content: CODEX_CONTEXT_TRUNCATION_NOTICE,
|
|
430
|
-
};
|
|
431
|
-
const systemChars = JSON.stringify(body.system ?? "").length;
|
|
432
|
-
const noticeChars = JSON.stringify(notice).length;
|
|
433
|
-
let remaining = Math.max(1024, maxChars - systemChars - noticeChars);
|
|
434
|
-
const kept = [];
|
|
435
|
-
for (let i = body.messages.length - 1; i >= 0; i -= 1) {
|
|
436
|
-
const message = body.messages[i];
|
|
437
|
-
const messageChars = JSON.stringify(message).length;
|
|
438
|
-
if (messageChars <= remaining) {
|
|
439
|
-
kept.unshift(message);
|
|
440
|
-
remaining -= messageChars;
|
|
441
|
-
continue;
|
|
442
|
-
}
|
|
443
|
-
if (kept.length === 0 && remaining > 1024) {
|
|
444
|
-
kept.unshift({
|
|
445
|
-
...message,
|
|
446
|
-
content: trimContentTail(message.content, remaining),
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
break;
|
|
450
|
-
}
|
|
451
|
-
const trimmedMessages = stripLeadingOrphanToolResults(kept);
|
|
452
|
-
const trimmedBody = {
|
|
453
|
-
...body,
|
|
454
|
-
messages: [notice, ...trimmedMessages],
|
|
455
|
-
};
|
|
456
|
-
const afterChars = anthropicInputChars(trimmedBody);
|
|
457
|
-
return { body: trimmedBody, trimmed: true, beforeChars, afterChars };
|
|
458
|
-
}
|
|
459
|
-
// ---------------------------------------------------------------------------
|
|
460
|
-
// Request translation: Anthropic → OpenAI Chat Completions (standard sk- key)
|
|
461
|
-
// ---------------------------------------------------------------------------
|
|
462
|
-
export function toOpenAIRequest(body, canonicalEffort) {
|
|
463
|
-
body = normalizeUnsupportedRequestContent(body);
|
|
464
|
-
const messages = [];
|
|
465
|
-
const system = systemToText(body.system);
|
|
466
|
-
if (system)
|
|
467
|
-
messages.push({ role: "system", content: system });
|
|
468
|
-
messages.push(...toOAIMessages(body.messages));
|
|
469
|
-
const req = {
|
|
470
|
-
model: mapModel(body.model),
|
|
471
|
-
messages,
|
|
472
|
-
max_completion_tokens: body.max_tokens,
|
|
473
|
-
};
|
|
474
|
-
if (body.stream) {
|
|
475
|
-
req.stream = true;
|
|
476
|
-
req.stream_options = { include_usage: true };
|
|
477
|
-
}
|
|
478
|
-
const effort = canonicalEffort ??
|
|
479
|
-
(body.thinking?.type === "enabled"
|
|
480
|
-
? budgetToEffort(body.thinking.budget_tokens)
|
|
481
|
-
: undefined);
|
|
482
|
-
if (effort) {
|
|
483
|
-
req.reasoning_effort = effort;
|
|
484
|
-
}
|
|
485
|
-
if (body.tools && body.tools.length > 0) {
|
|
486
|
-
req.tools = body.tools.map((t) => ({
|
|
487
|
-
type: "function",
|
|
488
|
-
function: {
|
|
489
|
-
name: t.name,
|
|
490
|
-
...(t.description !== undefined ? { description: t.description } : {}),
|
|
491
|
-
parameters: t.input_schema,
|
|
492
|
-
},
|
|
493
|
-
}));
|
|
494
|
-
}
|
|
495
|
-
return req;
|
|
496
|
-
}
|
|
497
|
-
export function toAnthropicResponse(openai, originalModel) {
|
|
498
|
-
const choice = openai.choices[0];
|
|
499
|
-
const message = choice?.message;
|
|
500
|
-
const content = [];
|
|
501
|
-
if (message?.content) {
|
|
502
|
-
content.push({ type: "text", text: message.content });
|
|
503
|
-
}
|
|
504
|
-
for (const tc of message?.tool_calls ?? []) {
|
|
505
|
-
let input = {};
|
|
506
|
-
try {
|
|
507
|
-
input = JSON.parse(tc.function.arguments);
|
|
508
|
-
}
|
|
509
|
-
catch {
|
|
510
|
-
/* leave empty */
|
|
511
|
-
}
|
|
512
|
-
content.push({
|
|
513
|
-
type: "tool_use",
|
|
514
|
-
id: tc.id,
|
|
515
|
-
name: tc.function.name,
|
|
516
|
-
input,
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
const stopReason = choice?.finish_reason === "tool_calls"
|
|
520
|
-
? "tool_use"
|
|
521
|
-
: choice?.finish_reason === "length"
|
|
522
|
-
? "max_tokens"
|
|
523
|
-
: "end_turn";
|
|
524
|
-
return {
|
|
525
|
-
id: openai.id ?? `msg_${Date.now().toString(36)}`,
|
|
526
|
-
type: "message",
|
|
527
|
-
role: "assistant",
|
|
528
|
-
content,
|
|
529
|
-
model: originalModel,
|
|
530
|
-
stop_reason: stopReason,
|
|
531
|
-
stop_sequence: null,
|
|
532
|
-
usage: {
|
|
533
|
-
input_tokens: openai.usage?.prompt_tokens ?? 0,
|
|
534
|
-
output_tokens: openai.usage?.completion_tokens ?? 0,
|
|
535
|
-
},
|
|
536
|
-
};
|
|
537
|
-
}
|
|
538
|
-
// ---------------------------------------------------------------------------
|
|
539
|
-
// Streaming translation: OpenAI SSE → Anthropic SSE
|
|
540
|
-
// ---------------------------------------------------------------------------
|
|
541
|
-
function sseEvent(event, data) {
|
|
542
|
-
return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
543
|
-
}
|
|
544
|
-
function closeStreamWithGatewayError(controller, enc, message) {
|
|
545
|
-
try {
|
|
546
|
-
controller.enqueue(enc.encode(sseEvent("error", anthropicGatewayError(message))));
|
|
547
|
-
}
|
|
548
|
-
catch {
|
|
549
|
-
// The downstream client may already be gone.
|
|
550
|
-
}
|
|
551
|
-
try {
|
|
552
|
-
controller.close();
|
|
553
|
-
}
|
|
554
|
-
catch {
|
|
555
|
-
// The stream may already be closed or errored.
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
export function translateOpenAIStreamToAnthropic(openaiStream, originalModel, messageId, estimatedInputTokens) {
|
|
559
|
-
const enc = new TextEncoder();
|
|
560
|
-
return new ReadableStream({
|
|
561
|
-
async start(controller) {
|
|
562
|
-
const emit = (s) => controller.enqueue(enc.encode(s));
|
|
563
|
-
// Anthropic content block state
|
|
564
|
-
let nextBlockIdx = 0;
|
|
565
|
-
let textBlockIdx = -1;
|
|
566
|
-
let textBlockOpen = false;
|
|
567
|
-
// OpenAI tool_call index → Anthropic block index
|
|
568
|
-
const toolBlockMap = new Map();
|
|
569
|
-
let outputTokens = 0;
|
|
570
|
-
let stopReason = "end_turn";
|
|
571
|
-
emit(sseEvent("message_start", {
|
|
572
|
-
type: "message_start",
|
|
573
|
-
message: {
|
|
574
|
-
id: messageId,
|
|
575
|
-
type: "message",
|
|
576
|
-
role: "assistant",
|
|
577
|
-
content: [],
|
|
578
|
-
model: originalModel,
|
|
579
|
-
stop_reason: null,
|
|
580
|
-
stop_sequence: null,
|
|
581
|
-
usage: { input_tokens: estimatedInputTokens, output_tokens: 0 },
|
|
582
|
-
},
|
|
583
|
-
}));
|
|
584
|
-
emit(sseEvent("ping", { type: "ping" }));
|
|
585
|
-
const reader = openaiStream.getReader();
|
|
586
|
-
let buf = "";
|
|
587
|
-
try {
|
|
588
|
-
while (true) {
|
|
589
|
-
const { done, value } = await reader.read();
|
|
590
|
-
if (done)
|
|
591
|
-
break;
|
|
592
|
-
buf += new TextDecoder().decode(value);
|
|
593
|
-
const lines = buf.split("\n");
|
|
594
|
-
buf = lines.pop() ?? "";
|
|
595
|
-
for (const line of lines) {
|
|
596
|
-
if (!line.startsWith("data: "))
|
|
597
|
-
continue;
|
|
598
|
-
const raw = line.slice(6).trim();
|
|
599
|
-
if (raw === "[DONE]") {
|
|
600
|
-
// Close any open blocks
|
|
601
|
-
if (textBlockOpen) {
|
|
602
|
-
emit(sseEvent("content_block_stop", {
|
|
603
|
-
type: "content_block_stop",
|
|
604
|
-
index: textBlockIdx,
|
|
605
|
-
}));
|
|
606
|
-
}
|
|
607
|
-
for (const [, blockIdx] of toolBlockMap) {
|
|
608
|
-
emit(sseEvent("content_block_stop", {
|
|
609
|
-
type: "content_block_stop",
|
|
610
|
-
index: blockIdx,
|
|
611
|
-
}));
|
|
612
|
-
}
|
|
613
|
-
emit(sseEvent("message_delta", {
|
|
614
|
-
type: "message_delta",
|
|
615
|
-
delta: { stop_reason: stopReason, stop_sequence: null },
|
|
616
|
-
usage: { output_tokens: outputTokens },
|
|
617
|
-
}));
|
|
618
|
-
emit(sseEvent("message_stop", { type: "message_stop" }));
|
|
619
|
-
controller.close();
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
let chunk;
|
|
623
|
-
try {
|
|
624
|
-
chunk = JSON.parse(raw);
|
|
625
|
-
}
|
|
626
|
-
catch {
|
|
627
|
-
continue;
|
|
628
|
-
}
|
|
629
|
-
// Trailing usage (stream_options: {include_usage: true})
|
|
630
|
-
if (chunk.usage?.completion_tokens)
|
|
631
|
-
outputTokens = chunk.usage.completion_tokens;
|
|
632
|
-
const choice = chunk.choices?.[0];
|
|
633
|
-
if (!choice)
|
|
634
|
-
continue;
|
|
635
|
-
if (choice.finish_reason) {
|
|
636
|
-
stopReason =
|
|
637
|
-
choice.finish_reason === "tool_calls"
|
|
638
|
-
? "tool_use"
|
|
639
|
-
: choice.finish_reason === "length"
|
|
640
|
-
? "max_tokens"
|
|
641
|
-
: "end_turn";
|
|
642
|
-
}
|
|
643
|
-
const delta = choice.delta;
|
|
644
|
-
if (!delta)
|
|
645
|
-
continue;
|
|
646
|
-
// Text content
|
|
647
|
-
if (typeof delta.content === "string" && delta.content.length > 0) {
|
|
648
|
-
if (!textBlockOpen) {
|
|
649
|
-
textBlockIdx = nextBlockIdx++;
|
|
650
|
-
textBlockOpen = true;
|
|
651
|
-
emit(sseEvent("content_block_start", {
|
|
652
|
-
type: "content_block_start",
|
|
653
|
-
index: textBlockIdx,
|
|
654
|
-
content_block: { type: "text", text: "" },
|
|
655
|
-
}));
|
|
656
|
-
}
|
|
657
|
-
emit(sseEvent("content_block_delta", {
|
|
658
|
-
type: "content_block_delta",
|
|
659
|
-
index: textBlockIdx,
|
|
660
|
-
delta: { type: "text_delta", text: delta.content },
|
|
661
|
-
}));
|
|
662
|
-
}
|
|
663
|
-
// Tool calls
|
|
664
|
-
for (const tc of delta.tool_calls ?? []) {
|
|
665
|
-
// Close text block when tool calls start
|
|
666
|
-
if (textBlockOpen) {
|
|
667
|
-
emit(sseEvent("content_block_stop", {
|
|
668
|
-
type: "content_block_stop",
|
|
669
|
-
index: textBlockIdx,
|
|
670
|
-
}));
|
|
671
|
-
textBlockOpen = false;
|
|
672
|
-
}
|
|
673
|
-
if (!toolBlockMap.has(tc.index)) {
|
|
674
|
-
const blockIdx = nextBlockIdx++;
|
|
675
|
-
toolBlockMap.set(tc.index, blockIdx);
|
|
676
|
-
emit(sseEvent("content_block_start", {
|
|
677
|
-
type: "content_block_start",
|
|
678
|
-
index: blockIdx,
|
|
679
|
-
content_block: {
|
|
680
|
-
type: "tool_use",
|
|
681
|
-
id: tc.id ?? `toolu_${tc.index}`,
|
|
682
|
-
name: tc.function?.name ?? "",
|
|
683
|
-
input: {},
|
|
684
|
-
},
|
|
685
|
-
}));
|
|
686
|
-
}
|
|
687
|
-
const blockIdx = toolBlockMap.get(tc.index);
|
|
688
|
-
if (tc.function?.arguments) {
|
|
689
|
-
emit(sseEvent("content_block_delta", {
|
|
690
|
-
type: "content_block_delta",
|
|
691
|
-
index: blockIdx,
|
|
692
|
-
delta: {
|
|
693
|
-
type: "input_json_delta",
|
|
694
|
-
partial_json: tc.function.arguments,
|
|
695
|
-
},
|
|
696
|
-
}));
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
// Stream ended without [DONE] — close gracefully
|
|
702
|
-
if (textBlockOpen) {
|
|
703
|
-
emit(sseEvent("content_block_stop", {
|
|
704
|
-
type: "content_block_stop",
|
|
705
|
-
index: textBlockIdx,
|
|
706
|
-
}));
|
|
707
|
-
}
|
|
708
|
-
for (const [, blockIdx] of toolBlockMap) {
|
|
709
|
-
emit(sseEvent("content_block_stop", {
|
|
710
|
-
type: "content_block_stop",
|
|
711
|
-
index: blockIdx,
|
|
712
|
-
}));
|
|
713
|
-
}
|
|
714
|
-
emit(sseEvent("message_delta", {
|
|
715
|
-
type: "message_delta",
|
|
716
|
-
delta: { stop_reason: stopReason, stop_sequence: null },
|
|
717
|
-
usage: { output_tokens: outputTokens },
|
|
718
|
-
}));
|
|
719
|
-
emit(sseEvent("message_stop", { type: "message_stop" }));
|
|
720
|
-
controller.close();
|
|
721
|
-
}
|
|
722
|
-
catch (err) {
|
|
723
|
-
closeStreamWithGatewayError(controller, enc, `Upstream stream failed: ${upstreamFetchErrorMessage(err)}`);
|
|
724
|
-
}
|
|
725
|
-
finally {
|
|
726
|
-
reader.releaseLock();
|
|
727
|
-
}
|
|
728
|
-
},
|
|
729
|
-
});
|
|
730
|
-
}
|
|
731
|
-
// ---------------------------------------------------------------------------
|
|
732
|
-
// Streaming translation: Codex Responses API SSE → Anthropic SSE
|
|
733
|
-
// ---------------------------------------------------------------------------
|
|
734
|
-
export function translateCodexStreamToAnthropic(codexStream, originalModel, messageId, estimatedInputTokens) {
|
|
735
|
-
const enc = new TextEncoder();
|
|
736
|
-
return new ReadableStream({
|
|
737
|
-
async start(controller) {
|
|
738
|
-
const emit = (s) => controller.enqueue(enc.encode(s));
|
|
739
|
-
let nextBlockIdx = 0;
|
|
740
|
-
// output_index → {type, idx}. `argsSent` accumulates the tool-argument
|
|
741
|
-
// JSON already relayed to the client, so the terminal arguments can be
|
|
742
|
-
// reconciled against it rather than blindly appended or blindly skipped.
|
|
743
|
-
const blockMap = new Map();
|
|
744
|
-
let textBlockOpen = false;
|
|
745
|
-
let outputTokens = 0;
|
|
746
|
-
let stopReason = "end_turn";
|
|
747
|
-
emit(sseEvent("message_start", {
|
|
748
|
-
type: "message_start",
|
|
749
|
-
message: {
|
|
750
|
-
id: messageId,
|
|
751
|
-
type: "message",
|
|
752
|
-
role: "assistant",
|
|
753
|
-
content: [],
|
|
754
|
-
model: originalModel,
|
|
755
|
-
stop_reason: null,
|
|
756
|
-
stop_sequence: null,
|
|
757
|
-
usage: { input_tokens: estimatedInputTokens, output_tokens: 0 },
|
|
758
|
-
},
|
|
759
|
-
}));
|
|
760
|
-
emit(sseEvent("ping", { type: "ping" }));
|
|
761
|
-
const reader = codexStream.getReader();
|
|
762
|
-
let buf = "";
|
|
763
|
-
let currentEvent = "";
|
|
764
|
-
try {
|
|
765
|
-
while (true) {
|
|
766
|
-
const { done, value } = await reader.read();
|
|
767
|
-
if (done)
|
|
768
|
-
break;
|
|
769
|
-
buf += new TextDecoder().decode(value);
|
|
770
|
-
const lines = buf.split("\n");
|
|
771
|
-
buf = lines.pop() ?? "";
|
|
772
|
-
for (const line of lines) {
|
|
773
|
-
if (line.startsWith("event: ")) {
|
|
774
|
-
currentEvent = line.slice(7).trim();
|
|
775
|
-
continue;
|
|
776
|
-
}
|
|
777
|
-
if (!line.startsWith("data: ")) {
|
|
778
|
-
if (line === "")
|
|
779
|
-
currentEvent = "";
|
|
780
|
-
continue;
|
|
781
|
-
}
|
|
782
|
-
const raw = line.slice(6).trim();
|
|
783
|
-
if (raw === "[DONE]")
|
|
784
|
-
continue;
|
|
785
|
-
let data;
|
|
786
|
-
try {
|
|
787
|
-
data = JSON.parse(raw);
|
|
788
|
-
}
|
|
789
|
-
catch {
|
|
790
|
-
continue;
|
|
791
|
-
}
|
|
792
|
-
const evType = data.type ?? currentEvent;
|
|
793
|
-
const outputIndex = data.output_index ?? 0;
|
|
794
|
-
switch (evType) {
|
|
795
|
-
case "response.output_item.added": {
|
|
796
|
-
const item = data.item;
|
|
797
|
-
if (!item)
|
|
798
|
-
break;
|
|
799
|
-
if (item.type === "function_call") {
|
|
800
|
-
const blockIdx = nextBlockIdx++;
|
|
801
|
-
blockMap.set(outputIndex, { type: "tool", idx: blockIdx });
|
|
802
|
-
emit(sseEvent("content_block_start", {
|
|
803
|
-
type: "content_block_start",
|
|
804
|
-
index: blockIdx,
|
|
805
|
-
content_block: {
|
|
806
|
-
type: "tool_use",
|
|
807
|
-
id: item.call_id ??
|
|
808
|
-
`toolu_${outputIndex}`,
|
|
809
|
-
name: item.name ?? "",
|
|
810
|
-
input: {},
|
|
811
|
-
},
|
|
812
|
-
}));
|
|
813
|
-
}
|
|
814
|
-
break;
|
|
815
|
-
}
|
|
816
|
-
case "response.output_text.delta": {
|
|
817
|
-
const delta = data.delta;
|
|
818
|
-
if (!delta)
|
|
819
|
-
break;
|
|
820
|
-
if (!blockMap.has(outputIndex)) {
|
|
821
|
-
const blockIdx = nextBlockIdx++;
|
|
822
|
-
blockMap.set(outputIndex, { type: "text", idx: blockIdx });
|
|
823
|
-
textBlockOpen = true;
|
|
824
|
-
emit(sseEvent("content_block_start", {
|
|
825
|
-
type: "content_block_start",
|
|
826
|
-
index: blockIdx,
|
|
827
|
-
content_block: { type: "text", text: "" },
|
|
828
|
-
}));
|
|
829
|
-
}
|
|
830
|
-
const tBlock = blockMap.get(outputIndex);
|
|
831
|
-
emit(sseEvent("content_block_delta", {
|
|
832
|
-
type: "content_block_delta",
|
|
833
|
-
index: tBlock.idx,
|
|
834
|
-
delta: { type: "text_delta", text: delta },
|
|
835
|
-
}));
|
|
836
|
-
break;
|
|
837
|
-
}
|
|
838
|
-
case "response.function_call_arguments.delta": {
|
|
839
|
-
const delta = data.delta;
|
|
840
|
-
if (!delta)
|
|
841
|
-
break;
|
|
842
|
-
const fBlock = blockMap.get(outputIndex);
|
|
843
|
-
if (fBlock?.type === "tool") {
|
|
844
|
-
fBlock.argsSent = (fBlock.argsSent ?? "") + delta;
|
|
845
|
-
emit(sseEvent("content_block_delta", {
|
|
846
|
-
type: "content_block_delta",
|
|
847
|
-
index: fBlock.idx,
|
|
848
|
-
delta: { type: "input_json_delta", partial_json: delta },
|
|
849
|
-
}));
|
|
850
|
-
}
|
|
851
|
-
break;
|
|
852
|
-
}
|
|
853
|
-
case "response.output_item.done": {
|
|
854
|
-
const block = blockMap.get(outputIndex);
|
|
855
|
-
const item = data.item;
|
|
856
|
-
if (block) {
|
|
857
|
-
// The Codex Responses upstream delivers function-call
|
|
858
|
-
// arguments here, as one complete string; today it never emits
|
|
859
|
-
// response.function_call_arguments.delta. Relaying them as an
|
|
860
|
-
// input_json_delta before closing the block is what lets a
|
|
861
|
-
// client reconstruct the tool input; without it every tool
|
|
862
|
-
// call reaches the client with an empty input.
|
|
863
|
-
//
|
|
864
|
-
// The terminal value is authoritative, so it is reconciled
|
|
865
|
-
// against what was already relayed instead of being skipped
|
|
866
|
-
// whenever any delta arrived: an upstream that streamed only
|
|
867
|
-
// PART of the arguments would otherwise leave the client with
|
|
868
|
-
// a silently truncated input. Only the missing suffix is sent.
|
|
869
|
-
// If what we relayed is not a prefix of the terminal value the
|
|
870
|
-
// two cannot be reconciled inside an append-only stream, so
|
|
871
|
-
// nothing is emitted rather than corrupting the JSON.
|
|
872
|
-
if (block.type === "tool" &&
|
|
873
|
-
typeof item?.arguments === "string" &&
|
|
874
|
-
item.arguments !== "") {
|
|
875
|
-
const sent = block.argsSent ?? "";
|
|
876
|
-
if (item.arguments.startsWith(sent)) {
|
|
877
|
-
const remainder = item.arguments.slice(sent.length);
|
|
878
|
-
if (remainder !== "") {
|
|
879
|
-
emit(sseEvent("content_block_delta", {
|
|
880
|
-
type: "content_block_delta",
|
|
881
|
-
index: block.idx,
|
|
882
|
-
delta: {
|
|
883
|
-
type: "input_json_delta",
|
|
884
|
-
partial_json: remainder,
|
|
885
|
-
},
|
|
886
|
-
}));
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
else {
|
|
890
|
-
// Irreconcilable: what was relayed is not a prefix of the
|
|
891
|
-
// authoritative terminal value, and an append-only stream
|
|
892
|
-
// cannot retract it. Emitting anything would corrupt the
|
|
893
|
-
// JSON, so nothing is emitted — but the client is then
|
|
894
|
-
// holding a tool input that CONTRADICTS the upstream, and
|
|
895
|
-
// that must never be a silent outcome. Announce it, with
|
|
896
|
-
// the call identity, so it is attributable in a log.
|
|
897
|
-
console.warn(`[llm-gateway] tool-call arguments could not be reconciled for ` +
|
|
898
|
-
`${item.name ?? "?"} ` +
|
|
899
|
-
`(${item.call_id ?? "?"}): ` +
|
|
900
|
-
`${sent.length} relayed chars are not a prefix of the ` +
|
|
901
|
-
`${item.arguments.length}-char terminal value; the client ` +
|
|
902
|
-
`input differs from upstream`);
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
emit(sseEvent("content_block_stop", {
|
|
906
|
-
type: "content_block_stop",
|
|
907
|
-
index: block.idx,
|
|
908
|
-
}));
|
|
909
|
-
if (block.type === "text")
|
|
910
|
-
textBlockOpen = false;
|
|
911
|
-
}
|
|
912
|
-
if (item?.type === "function_call")
|
|
913
|
-
stopReason = "tool_use";
|
|
914
|
-
break;
|
|
915
|
-
}
|
|
916
|
-
case "response.completed": {
|
|
917
|
-
const response = data.response;
|
|
918
|
-
const usage = response?.usage;
|
|
919
|
-
if (typeof usage?.output_tokens === "number")
|
|
920
|
-
outputTokens = usage.output_tokens;
|
|
921
|
-
// Close any unclosed blocks (shouldn't happen but be safe)
|
|
922
|
-
for (const [, block] of blockMap) {
|
|
923
|
-
if (block.type === "text" && textBlockOpen) {
|
|
924
|
-
emit(sseEvent("content_block_stop", {
|
|
925
|
-
type: "content_block_stop",
|
|
926
|
-
index: block.idx,
|
|
927
|
-
}));
|
|
928
|
-
textBlockOpen = false;
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
emit(sseEvent("message_delta", {
|
|
932
|
-
type: "message_delta",
|
|
933
|
-
delta: { stop_reason: stopReason, stop_sequence: null },
|
|
934
|
-
usage: { output_tokens: outputTokens },
|
|
935
|
-
}));
|
|
936
|
-
emit(sseEvent("message_stop", { type: "message_stop" }));
|
|
937
|
-
controller.close();
|
|
938
|
-
return;
|
|
939
|
-
}
|
|
940
|
-
case "response.failed": {
|
|
941
|
-
emit(sseEvent("error", {
|
|
942
|
-
type: "error",
|
|
943
|
-
error: {
|
|
944
|
-
type: "api_error",
|
|
945
|
-
message: codexFailureMessage(data),
|
|
946
|
-
},
|
|
947
|
-
}));
|
|
948
|
-
controller.close();
|
|
949
|
-
return;
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
// Stream ended without response.completed — close gracefully
|
|
955
|
-
if (textBlockOpen) {
|
|
956
|
-
for (const [, block] of blockMap) {
|
|
957
|
-
if (block.type === "text") {
|
|
958
|
-
emit(sseEvent("content_block_stop", {
|
|
959
|
-
type: "content_block_stop",
|
|
960
|
-
index: block.idx,
|
|
961
|
-
}));
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
for (const [, block] of blockMap) {
|
|
966
|
-
if (block.type === "tool") {
|
|
967
|
-
emit(sseEvent("content_block_stop", {
|
|
968
|
-
type: "content_block_stop",
|
|
969
|
-
index: block.idx,
|
|
970
|
-
}));
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
emit(sseEvent("message_delta", {
|
|
974
|
-
type: "message_delta",
|
|
975
|
-
delta: { stop_reason: stopReason, stop_sequence: null },
|
|
976
|
-
usage: { output_tokens: outputTokens },
|
|
977
|
-
}));
|
|
978
|
-
emit(sseEvent("message_stop", { type: "message_stop" }));
|
|
979
|
-
controller.close();
|
|
980
|
-
}
|
|
981
|
-
catch (err) {
|
|
982
|
-
closeStreamWithGatewayError(controller, enc, `Upstream stream failed: ${upstreamFetchErrorMessage(err)}`);
|
|
983
|
-
}
|
|
984
|
-
finally {
|
|
985
|
-
reader.releaseLock();
|
|
986
|
-
}
|
|
987
|
-
},
|
|
988
|
-
});
|
|
989
|
-
}
|
|
990
|
-
function codexFailureMessage(data) {
|
|
991
|
-
const response = data.response;
|
|
992
|
-
const error = data.error ??
|
|
993
|
-
response?.error;
|
|
994
|
-
const message = error?.message ?? error?.code ?? response?.status;
|
|
995
|
-
return typeof message === "string" && message
|
|
996
|
-
? message
|
|
997
|
-
: "Codex upstream response failed";
|
|
998
|
-
}
|
|
999
|
-
export async function codexStreamToAnthropicResponse(codexStream, originalModel, messageId, estimatedInputTokens) {
|
|
1000
|
-
const reader = codexStream.getReader();
|
|
1001
|
-
const blockMap = new Map();
|
|
1002
|
-
let nextBlockIdx = 0;
|
|
1003
|
-
let outputTokens = 0;
|
|
1004
|
-
let buf = "";
|
|
1005
|
-
let currentEvent = "";
|
|
1006
|
-
const ensureTextBlock = (outputIndex) => {
|
|
1007
|
-
const existing = blockMap.get(outputIndex);
|
|
1008
|
-
if (existing?.type === "text")
|
|
1009
|
-
return existing;
|
|
1010
|
-
const block = {
|
|
1011
|
-
type: "text",
|
|
1012
|
-
idx: nextBlockIdx++,
|
|
1013
|
-
text: "",
|
|
1014
|
-
};
|
|
1015
|
-
blockMap.set(outputIndex, block);
|
|
1016
|
-
return block;
|
|
1017
|
-
};
|
|
1018
|
-
try {
|
|
1019
|
-
while (true) {
|
|
1020
|
-
const { done, value } = await reader.read();
|
|
1021
|
-
if (done)
|
|
1022
|
-
break;
|
|
1023
|
-
buf += new TextDecoder().decode(value);
|
|
1024
|
-
const lines = buf.split("\n");
|
|
1025
|
-
buf = lines.pop() ?? "";
|
|
1026
|
-
for (const line of lines) {
|
|
1027
|
-
if (line.startsWith("event: ")) {
|
|
1028
|
-
currentEvent = line.slice(7).trim();
|
|
1029
|
-
continue;
|
|
1030
|
-
}
|
|
1031
|
-
if (!line.startsWith("data: ")) {
|
|
1032
|
-
if (line === "")
|
|
1033
|
-
currentEvent = "";
|
|
1034
|
-
continue;
|
|
1035
|
-
}
|
|
1036
|
-
const raw = line.slice(6).trim();
|
|
1037
|
-
if (raw === "[DONE]")
|
|
1038
|
-
continue;
|
|
1039
|
-
let data;
|
|
1040
|
-
try {
|
|
1041
|
-
data = JSON.parse(raw);
|
|
1042
|
-
}
|
|
1043
|
-
catch {
|
|
1044
|
-
continue;
|
|
1045
|
-
}
|
|
1046
|
-
const evType = data.type ?? currentEvent;
|
|
1047
|
-
const outputIndex = data.output_index ?? 0;
|
|
1048
|
-
switch (evType) {
|
|
1049
|
-
case "response.output_item.added": {
|
|
1050
|
-
const item = data.item;
|
|
1051
|
-
if (item?.type === "function_call") {
|
|
1052
|
-
blockMap.set(outputIndex, {
|
|
1053
|
-
type: "tool",
|
|
1054
|
-
idx: nextBlockIdx++,
|
|
1055
|
-
id: item.call_id ??
|
|
1056
|
-
`toolu_${outputIndex}`,
|
|
1057
|
-
name: item.name ?? "",
|
|
1058
|
-
args: "",
|
|
1059
|
-
});
|
|
1060
|
-
}
|
|
1061
|
-
break;
|
|
1062
|
-
}
|
|
1063
|
-
case "response.output_text.delta": {
|
|
1064
|
-
const delta = data.delta;
|
|
1065
|
-
if (delta)
|
|
1066
|
-
ensureTextBlock(outputIndex).text += delta;
|
|
1067
|
-
break;
|
|
1068
|
-
}
|
|
1069
|
-
case "response.output_text.done": {
|
|
1070
|
-
const text = data.text;
|
|
1071
|
-
if (text !== undefined)
|
|
1072
|
-
ensureTextBlock(outputIndex).text = text;
|
|
1073
|
-
break;
|
|
1074
|
-
}
|
|
1075
|
-
case "response.function_call_arguments.delta": {
|
|
1076
|
-
const delta = data.delta;
|
|
1077
|
-
const block = blockMap.get(outputIndex);
|
|
1078
|
-
if (delta && block?.type === "tool")
|
|
1079
|
-
block.args += delta;
|
|
1080
|
-
break;
|
|
1081
|
-
}
|
|
1082
|
-
case "response.output_item.done": {
|
|
1083
|
-
const item = data.item;
|
|
1084
|
-
const block = blockMap.get(outputIndex);
|
|
1085
|
-
if (item?.type === "function_call" && block?.type === "tool") {
|
|
1086
|
-
if (typeof item.call_id === "string")
|
|
1087
|
-
block.id = item.call_id;
|
|
1088
|
-
if (typeof item.name === "string")
|
|
1089
|
-
block.name = item.name;
|
|
1090
|
-
if (typeof item.arguments === "string")
|
|
1091
|
-
block.args = item.arguments;
|
|
1092
|
-
}
|
|
1093
|
-
break;
|
|
1094
|
-
}
|
|
1095
|
-
case "response.completed": {
|
|
1096
|
-
const response = data.response;
|
|
1097
|
-
const usage = response?.usage;
|
|
1098
|
-
if (typeof usage?.output_tokens === "number")
|
|
1099
|
-
outputTokens = usage.output_tokens;
|
|
1100
|
-
break;
|
|
1101
|
-
}
|
|
1102
|
-
case "response.failed":
|
|
1103
|
-
throw new Error(codexFailureMessage(data));
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
finally {
|
|
1109
|
-
reader.releaseLock();
|
|
1110
|
-
}
|
|
1111
|
-
const content = [...blockMap.values()]
|
|
1112
|
-
.sort((a, b) => a.idx - b.idx)
|
|
1113
|
-
.map((block) => {
|
|
1114
|
-
if (block.type === "text")
|
|
1115
|
-
return { type: "text", text: block.text };
|
|
1116
|
-
let input = {};
|
|
1117
|
-
try {
|
|
1118
|
-
input = block.args ? JSON.parse(block.args) : {};
|
|
1119
|
-
}
|
|
1120
|
-
catch {
|
|
1121
|
-
input = {};
|
|
1122
|
-
}
|
|
1123
|
-
return { type: "tool_use", id: block.id, name: block.name, input };
|
|
1124
|
-
});
|
|
1125
|
-
return {
|
|
1126
|
-
id: messageId,
|
|
1127
|
-
type: "message",
|
|
1128
|
-
role: "assistant",
|
|
1129
|
-
content,
|
|
1130
|
-
model: originalModel,
|
|
1131
|
-
stop_reason: content.some((block) => block.type === "tool_use")
|
|
1132
|
-
? "tool_use"
|
|
1133
|
-
: "end_turn",
|
|
1134
|
-
stop_sequence: null,
|
|
1135
|
-
usage: { input_tokens: estimatedInputTokens, output_tokens: outputTokens },
|
|
1136
|
-
};
|
|
1137
|
-
}
|
|
1138
|
-
// ---------------------------------------------------------------------------
|
|
1139
|
-
// Route handler
|
|
1140
|
-
// ---------------------------------------------------------------------------
|
|
1141
|
-
export async function handleMessagesViaOpenAI(c, session, requestBody) {
|
|
1142
|
-
const rawBody = requestBody ?? (await c.req.arrayBuffer());
|
|
1143
|
-
let body;
|
|
1144
|
-
try {
|
|
1145
|
-
body = JSON.parse(new TextDecoder().decode(rawBody));
|
|
1146
|
-
}
|
|
1147
|
-
catch {
|
|
1148
|
-
return c.json({ error: "invalid JSON" }, 400);
|
|
1149
|
-
}
|
|
1150
|
-
const originalModel = body.model;
|
|
1151
|
-
let route;
|
|
1152
|
-
try {
|
|
1153
|
-
route = routeModelOrThrow(originalModel);
|
|
1154
|
-
}
|
|
1155
|
-
catch (err) {
|
|
1156
|
-
return c.json(anthropicGatewayError(err instanceof Error ? err.message : String(err)), 400);
|
|
1157
|
-
}
|
|
1158
|
-
const tokenTransport = isCodexOAuthToken(session.token)
|
|
1159
|
-
? "codex-responses"
|
|
1160
|
-
: "openai-chat-completions";
|
|
1161
|
-
if (session.requiredTransport &&
|
|
1162
|
-
tokenTransport !== session.requiredTransport) {
|
|
1163
|
-
return c.json({ error: "gateway credential no longer satisfies session transport" }, 503);
|
|
1164
|
-
}
|
|
1165
|
-
const isCodex = tokenTransport === "codex-responses";
|
|
1166
|
-
const codexContext = isCodex ? trimCodexBodyForContext(body) : null;
|
|
1167
|
-
const upstreamBody = codexContext?.body ?? body;
|
|
1168
|
-
if (codexContext?.trimmed) {
|
|
1169
|
-
console.warn(`[llm-gateway] Codex context trimmed for ${originalModel}: ` +
|
|
1170
|
-
`${codexContext.beforeChars} -> ${codexContext.afterChars} chars`);
|
|
1171
|
-
}
|
|
1172
|
-
const upstreamReq = isCodex
|
|
1173
|
-
? toCodexRequest(upstreamBody, route.effort)
|
|
1174
|
-
: toOpenAIRequest(upstreamBody, route.effort);
|
|
1175
|
-
// Build the Codex request ONCE. Response attestation is derived from this
|
|
1176
|
-
// exact post-mapping payload, never copied from the Anthropic input.
|
|
1177
|
-
const codexRequest = isCodex
|
|
1178
|
-
? prepareCodexResponsesRequest(upstreamReq)
|
|
1179
|
-
: undefined;
|
|
1180
|
-
const attestedModel = codexRequest?.body.model;
|
|
1181
|
-
const attestedEffort = codexRequest
|
|
1182
|
-
? codexRequestEffort(codexRequest.body)
|
|
1183
|
-
: undefined;
|
|
1184
|
-
const withCodexAttestation = (response) => {
|
|
1185
|
-
if (!codexRequest)
|
|
1186
|
-
return response;
|
|
1187
|
-
const headers = new Headers(response.headers);
|
|
1188
|
-
if (typeof attestedModel === "string")
|
|
1189
|
-
headers.set("x-h2a-resolved-model", attestedModel);
|
|
1190
|
-
if (attestedEffort)
|
|
1191
|
-
headers.set("x-h2a-reasoning-effort", attestedEffort);
|
|
1192
|
-
return new Response(response.body, {
|
|
1193
|
-
status: response.status,
|
|
1194
|
-
statusText: response.statusText,
|
|
1195
|
-
headers,
|
|
1196
|
-
});
|
|
1197
|
-
};
|
|
1198
|
-
const messageId = `msg_${Date.now().toString(36)}`;
|
|
1199
|
-
const estimatedInputTokens = Math.ceil(JSON.stringify(upstreamBody.messages).length / 4);
|
|
1200
|
-
const doFetch = (token) => {
|
|
1201
|
-
if (codexRequest) {
|
|
1202
|
-
if (attestedEffort) {
|
|
1203
|
-
console.info(`[llm-gateway] Codex request: model=${String(codexRequest.body.model ?? "")} reasoning.effort=${attestedEffort}`);
|
|
1204
|
-
}
|
|
1205
|
-
return fetch(codexRequest.url, {
|
|
1206
|
-
method: "POST",
|
|
1207
|
-
headers: {
|
|
1208
|
-
Authorization: `Bearer ${token}`,
|
|
1209
|
-
"content-type": "application/json",
|
|
1210
|
-
originator: "opencode",
|
|
1211
|
-
"User-Agent": "opencode/0.1.0",
|
|
1212
|
-
session_id: `codex_${Date.now().toString(36)}`,
|
|
1213
|
-
},
|
|
1214
|
-
body: JSON.stringify(codexRequest.body),
|
|
1215
|
-
// @ts-expect-error Node 18+ supports duplex for streaming
|
|
1216
|
-
duplex: "half",
|
|
1217
|
-
signal: c.req.raw.signal,
|
|
1218
|
-
});
|
|
1219
|
-
}
|
|
1220
|
-
return fetch(`${OPENAI_BASE}/v1/chat/completions`, {
|
|
1221
|
-
method: "POST",
|
|
1222
|
-
headers: {
|
|
1223
|
-
Authorization: `Bearer ${token}`,
|
|
1224
|
-
"content-type": "application/json",
|
|
1225
|
-
},
|
|
1226
|
-
body: JSON.stringify(upstreamReq),
|
|
1227
|
-
// @ts-expect-error Node 18+ supports duplex for streaming
|
|
1228
|
-
duplex: "half",
|
|
1229
|
-
signal: c.req.raw.signal,
|
|
1230
|
-
});
|
|
1231
|
-
};
|
|
1232
|
-
let upstream;
|
|
1233
|
-
try {
|
|
1234
|
-
upstream = await fetchUpstreamWithRetry(() => doFetch(session.token));
|
|
1235
|
-
}
|
|
1236
|
-
catch (err) {
|
|
1237
|
-
if (abortError(err))
|
|
1238
|
-
return new Response(null, { status: 499 });
|
|
1239
|
-
return c.json(anthropicGatewayError(`Upstream fetch failed: ${upstreamFetchErrorMessage(err)}`), 502);
|
|
1240
|
-
}
|
|
1241
|
-
// 401 → attempt OAuth token refresh + retry once
|
|
1242
|
-
if (upstream.status === 401 && session.accountId) {
|
|
1243
|
-
const newToken = await refreshOAuthTokenForRetry(session.accountId);
|
|
1244
|
-
if (newToken) {
|
|
1245
|
-
await upstream.body?.cancel().catch(() => { });
|
|
1246
|
-
try {
|
|
1247
|
-
upstream = await fetchUpstreamWithRetry(() => doFetch(newToken));
|
|
1248
|
-
}
|
|
1249
|
-
catch (err) {
|
|
1250
|
-
if (abortError(err))
|
|
1251
|
-
return new Response(null, { status: 499 });
|
|
1252
|
-
return c.json(anthropicGatewayError(`Upstream fetch failed: ${upstreamFetchErrorMessage(err)}`), 502);
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
else {
|
|
1256
|
-
await upstream.body?.cancel().catch(() => { });
|
|
1257
|
-
return c.json(anthropicGatewayError("Upstream Codex OAuth returned 401 and token refresh failed; re-enroll the Codex account with `h2a llm-mesh enroll codex`."), 502);
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
if (!upstream.ok) {
|
|
1261
|
-
const errBody = await upstream.text();
|
|
1262
|
-
return new Response(errBody, {
|
|
1263
|
-
status: upstream.status,
|
|
1264
|
-
headers: {
|
|
1265
|
-
"content-type": upstream.headers.get("content-type") ?? "application/json",
|
|
1266
|
-
},
|
|
1267
|
-
});
|
|
1268
|
-
}
|
|
1269
|
-
if (body.stream && isCodex && upstream.body) {
|
|
1270
|
-
const stream = translateCodexStreamToAnthropic(upstream.body, originalModel, messageId, estimatedInputTokens);
|
|
1271
|
-
return withCodexAttestation(new Response(stream, {
|
|
1272
|
-
status: 200,
|
|
1273
|
-
headers: {
|
|
1274
|
-
"content-type": "text/event-stream; charset=utf-8",
|
|
1275
|
-
"cache-control": "no-cache",
|
|
1276
|
-
"x-accel-buffering": "no",
|
|
1277
|
-
},
|
|
1278
|
-
}));
|
|
1279
|
-
}
|
|
1280
|
-
// Codex OAuth Responses API streams even for requests where the Anthropic
|
|
1281
|
-
// client expects a non-streaming JSON message. In that case collect the SSE
|
|
1282
|
-
// upstream and return a normal Anthropic Messages JSON response.
|
|
1283
|
-
if (isCodex && upstream.body) {
|
|
1284
|
-
try {
|
|
1285
|
-
return withCodexAttestation(c.json(await codexStreamToAnthropicResponse(upstream.body, originalModel, messageId, estimatedInputTokens)));
|
|
1286
|
-
}
|
|
1287
|
-
catch (err) {
|
|
1288
|
-
return c.json(anthropicGatewayError(err instanceof Error ? err.message : String(err)), 502);
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
if (body.stream && upstream.body) {
|
|
1292
|
-
const stream = translateOpenAIStreamToAnthropic(upstream.body, originalModel, messageId, estimatedInputTokens);
|
|
1293
|
-
return new Response(stream, {
|
|
1294
|
-
status: 200,
|
|
1295
|
-
headers: {
|
|
1296
|
-
"content-type": "text/event-stream; charset=utf-8",
|
|
1297
|
-
"cache-control": "no-cache",
|
|
1298
|
-
"x-accel-buffering": "no",
|
|
1299
|
-
},
|
|
1300
|
-
});
|
|
1301
|
-
}
|
|
1302
|
-
const openaiResp = (await upstream.json());
|
|
1303
|
-
return c.json(toAnthropicResponse(openaiResp, originalModel));
|
|
1304
|
-
}
|
|
1305
|
-
//# sourceMappingURL=proxy-openai.js.map
|