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