@sentropic/remote-cli 0.0.3
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/attach-YI2AMS3B.js +22 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-E3GXAKJ3.js +258 -0
- package/dist/chunk-PG4CWJNC.js +197 -0
- package/dist/chunk-RLFQALHC.js +45 -0
- package/dist/chunk-VRTZ23J3.js +541 -0
- package/dist/chunk-Z277FK2S.js +294 -0
- package/dist/h2a-bridge-FPB5D3TB.js +20 -0
- package/dist/index.d.ts +730 -0
- package/dist/index.js +13966 -0
- package/dist/llm-gateway-runtime/index.d.ts +2 -0
- package/dist/llm-gateway-runtime/index.js +1295 -0
- package/dist/sync-status-WRQK4YRK.js +15 -0
- package/dist/workspace-O32VX2JG.js +38 -0
- package/dist/workspace-sync-incremental-W3SHAYHS.js +159 -0
- package/package.json +46 -0
|
@@ -0,0 +1,1295 @@
|
|
|
1
|
+
import "../chunk-3RG5ZIWI.js";
|
|
2
|
+
|
|
3
|
+
// src/llm-gateway-runtime/index.ts
|
|
4
|
+
import { serve } from "@hono/node-server";
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
|
|
7
|
+
// src/llm-gateway-runtime/sticky.ts
|
|
8
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
9
|
+
import { dirname } from "path";
|
|
10
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
11
|
+
|
|
12
|
+
// src/llm-gateway-runtime/accounts.ts
|
|
13
|
+
function isUnsupportedClaudeOAuthAccount(account) {
|
|
14
|
+
return account.provider === "anthropic" && (account.authType === "bearer" || account.id === "claude-oauth" || account.token.startsWith("sk-ant-oat"));
|
|
15
|
+
}
|
|
16
|
+
function parse() {
|
|
17
|
+
const raw = process.env.GATEWAY_ACCOUNTS;
|
|
18
|
+
if (!raw) throw new Error("GATEWAY_ACCOUNTS env var is required");
|
|
19
|
+
const list = JSON.parse(raw);
|
|
20
|
+
if (!Array.isArray(list) || list.length === 0)
|
|
21
|
+
throw new Error("GATEWAY_ACCOUNTS must be a non-empty JSON array");
|
|
22
|
+
const accounts = list.filter(
|
|
23
|
+
(account) => !isUnsupportedClaudeOAuthAccount(account)
|
|
24
|
+
);
|
|
25
|
+
if (accounts.length === 0) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
"GATEWAY_ACCOUNTS has no supported accounts: Claude Code OAuth is not a supported upstream transport"
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return accounts;
|
|
31
|
+
}
|
|
32
|
+
var _accounts = null;
|
|
33
|
+
function getAccounts() {
|
|
34
|
+
if (!_accounts) _accounts = parse();
|
|
35
|
+
return _accounts;
|
|
36
|
+
}
|
|
37
|
+
var _rrIdx = 0;
|
|
38
|
+
function selectAccount() {
|
|
39
|
+
const accounts = getAccounts();
|
|
40
|
+
const acct = accounts[_rrIdx % accounts.length];
|
|
41
|
+
_rrIdx++;
|
|
42
|
+
return acct;
|
|
43
|
+
}
|
|
44
|
+
function findAccount(accountId) {
|
|
45
|
+
return getAccounts().find((a) => a.id === accountId);
|
|
46
|
+
}
|
|
47
|
+
function updateAccountToken(accountId, newToken, expiresAt) {
|
|
48
|
+
const accounts = getAccounts();
|
|
49
|
+
const acc = accounts.find((a) => a.id === accountId);
|
|
50
|
+
if (!acc) return;
|
|
51
|
+
acc.token = newToken;
|
|
52
|
+
if (expiresAt !== void 0) acc.expiresAt = expiresAt;
|
|
53
|
+
}
|
|
54
|
+
function jwtExpiry(token) {
|
|
55
|
+
try {
|
|
56
|
+
const parts = token.split(".");
|
|
57
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
58
|
+
if (typeof payload.exp === "number") return new Date(payload.exp * 1e3).toISOString();
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
return void 0;
|
|
62
|
+
}
|
|
63
|
+
async function refreshOAuthToken(accountId) {
|
|
64
|
+
const acc = findAccount(accountId);
|
|
65
|
+
if (!acc?.refreshToken) return null;
|
|
66
|
+
let resp;
|
|
67
|
+
try {
|
|
68
|
+
resp = await fetch("https://auth.openai.com/oauth/token", {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
71
|
+
body: new URLSearchParams({
|
|
72
|
+
grant_type: "refresh_token",
|
|
73
|
+
refresh_token: acc.refreshToken,
|
|
74
|
+
client_id: "app_EMoamEEZ73f0CkXaXp7hrann"
|
|
75
|
+
})
|
|
76
|
+
});
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error(`[llm-gateway] OAuth refresh network error for ${accountId}:`, err);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
if (!resp.ok) {
|
|
82
|
+
const body = await resp.text().catch(() => "");
|
|
83
|
+
console.error(`[llm-gateway] OAuth refresh failed (${resp.status}) for ${accountId}: ${body}`);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const data = await resp.json();
|
|
87
|
+
if (!data.access_token) {
|
|
88
|
+
console.error(`[llm-gateway] OAuth refresh: no access_token in response for ${accountId}`);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const expiresAt = jwtExpiry(data.access_token);
|
|
92
|
+
updateAccountToken(accountId, data.access_token, expiresAt);
|
|
93
|
+
console.log(`[llm-gateway] OAuth token refreshed for ${accountId}${expiresAt ? `, expires ${expiresAt}` : ""}`);
|
|
94
|
+
return data.access_token;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/llm-gateway-runtime/sticky.ts
|
|
98
|
+
var SA_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token";
|
|
99
|
+
var K8S_HOST = process.env.KUBERNETES_SERVICE_HOST ?? "kubernetes.default.svc.cluster.local";
|
|
100
|
+
var K8S_PORT = process.env.KUBERNETES_SERVICE_PORT ?? "443";
|
|
101
|
+
var NAMESPACE = process.env.POD_NAMESPACE ?? "sentropic-remote";
|
|
102
|
+
var CONFIGMAP_NAME = process.env.STICKY_CONFIGMAP ?? "llm-gateway-sticky";
|
|
103
|
+
var LOCAL_STICKY_FILE = process.env.LLM_GATEWAY_STICKY_FILE;
|
|
104
|
+
function saToken() {
|
|
105
|
+
try {
|
|
106
|
+
return readFileSync(SA_TOKEN_PATH, "utf-8").trim();
|
|
107
|
+
} catch {
|
|
108
|
+
return process.env.K8S_TOKEN ?? "";
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function cmUrl() {
|
|
112
|
+
return `https://${K8S_HOST}:${K8S_PORT}/api/v1/namespaces/${NAMESPACE}/configmaps/${CONFIGMAP_NAME}`;
|
|
113
|
+
}
|
|
114
|
+
function readLocalSticky() {
|
|
115
|
+
if (!LOCAL_STICKY_FILE) return {};
|
|
116
|
+
try {
|
|
117
|
+
return JSON.parse(readFileSync(LOCAL_STICKY_FILE, "utf8"));
|
|
118
|
+
} catch {
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function writeLocalSticky(data) {
|
|
123
|
+
if (!LOCAL_STICKY_FILE) return;
|
|
124
|
+
mkdirSync(dirname(LOCAL_STICKY_FILE), { recursive: true });
|
|
125
|
+
const tmp = `${LOCAL_STICKY_FILE}.tmp.${process.pid}`;
|
|
126
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n", { mode: 384 });
|
|
127
|
+
chmodSync(tmp, 384);
|
|
128
|
+
renameSync(tmp, LOCAL_STICKY_FILE);
|
|
129
|
+
}
|
|
130
|
+
async function readSticky() {
|
|
131
|
+
const token = saToken();
|
|
132
|
+
if (!token) return readLocalSticky();
|
|
133
|
+
const resp = await fetch(cmUrl(), {
|
|
134
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
135
|
+
});
|
|
136
|
+
if (resp.status === 404) return {};
|
|
137
|
+
if (!resp.ok) throw new Error(`sticky ConfigMap read failed: ${resp.status}`);
|
|
138
|
+
const cm = await resp.json();
|
|
139
|
+
return cm.data ?? {};
|
|
140
|
+
}
|
|
141
|
+
async function writeSticky(sessionId, accountId) {
|
|
142
|
+
const token = saToken();
|
|
143
|
+
if (!token) {
|
|
144
|
+
writeLocalSticky({ ...readLocalSticky(), [sessionId]: accountId });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const patch = {
|
|
148
|
+
apiVersion: "v1",
|
|
149
|
+
kind: "ConfigMap",
|
|
150
|
+
metadata: { name: CONFIGMAP_NAME, namespace: NAMESPACE },
|
|
151
|
+
data: { [sessionId]: accountId }
|
|
152
|
+
};
|
|
153
|
+
const resp = await fetch(cmUrl(), {
|
|
154
|
+
method: "PATCH",
|
|
155
|
+
headers: {
|
|
156
|
+
Authorization: `Bearer ${token}`,
|
|
157
|
+
"Content-Type": "application/strategic-merge-patch+json"
|
|
158
|
+
},
|
|
159
|
+
body: JSON.stringify(patch)
|
|
160
|
+
});
|
|
161
|
+
if (!resp.ok) throw new Error(`sticky ConfigMap patch failed: ${resp.status}`);
|
|
162
|
+
}
|
|
163
|
+
var _sessions = /* @__PURE__ */ new Map();
|
|
164
|
+
var TOKEN_PREFIX = "gw-v1-";
|
|
165
|
+
function tokenSeed() {
|
|
166
|
+
const seed = process.env.LLM_GATEWAY_TOKEN_SEED;
|
|
167
|
+
if (!seed) throw new Error("LLM_GATEWAY_TOKEN_SEED env var is required");
|
|
168
|
+
return seed;
|
|
169
|
+
}
|
|
170
|
+
function b64url(value) {
|
|
171
|
+
return Buffer.from(value, "utf8").toString("base64url");
|
|
172
|
+
}
|
|
173
|
+
function unb64url(value) {
|
|
174
|
+
try {
|
|
175
|
+
return Buffer.from(value, "base64url").toString("utf8");
|
|
176
|
+
} catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function macFor(sessionId) {
|
|
181
|
+
return createHmac("sha256", tokenSeed()).update("gw-token-v1\0").update(sessionId).digest("base64url");
|
|
182
|
+
}
|
|
183
|
+
function gatewayTokenForSession(sessionId) {
|
|
184
|
+
return `${TOKEN_PREFIX}${b64url(sessionId)}.${macFor(sessionId)}`;
|
|
185
|
+
}
|
|
186
|
+
function sessionIdFromGatewayToken(gatewayToken) {
|
|
187
|
+
if (!gatewayToken.startsWith(TOKEN_PREFIX)) return null;
|
|
188
|
+
const rest = gatewayToken.slice(TOKEN_PREFIX.length);
|
|
189
|
+
const dot = rest.indexOf(".");
|
|
190
|
+
if (dot <= 0) return null;
|
|
191
|
+
const encodedSessionId = rest.slice(0, dot);
|
|
192
|
+
const suppliedMac = rest.slice(dot + 1);
|
|
193
|
+
const sessionId = unb64url(encodedSessionId);
|
|
194
|
+
if (!sessionId) return null;
|
|
195
|
+
const expectedMac = macFor(sessionId);
|
|
196
|
+
const a = Buffer.from(suppliedMac);
|
|
197
|
+
const b = Buffer.from(expectedMac);
|
|
198
|
+
if (a.length !== b.length || !timingSafeEqual(a, b)) return null;
|
|
199
|
+
return sessionId;
|
|
200
|
+
}
|
|
201
|
+
function sessionCount() {
|
|
202
|
+
return _sessions.size;
|
|
203
|
+
}
|
|
204
|
+
async function lookupToken(gatewayToken) {
|
|
205
|
+
const cached = _sessions.get(gatewayToken);
|
|
206
|
+
if (cached) return cached;
|
|
207
|
+
const sessionId = sessionIdFromGatewayToken(gatewayToken);
|
|
208
|
+
if (!sessionId) return void 0;
|
|
209
|
+
const existing = await readSticky();
|
|
210
|
+
const accountId = existing[sessionId];
|
|
211
|
+
let account = accountId === void 0 ? void 0 : findAccount(accountId);
|
|
212
|
+
if (!account) {
|
|
213
|
+
account = selectAccount();
|
|
214
|
+
await writeSticky(sessionId, account.id);
|
|
215
|
+
}
|
|
216
|
+
const entry = {
|
|
217
|
+
gatewayToken,
|
|
218
|
+
accountId: account.id,
|
|
219
|
+
token: account.token,
|
|
220
|
+
provider: account.provider,
|
|
221
|
+
...account.authType ? { authType: account.authType } : {}
|
|
222
|
+
};
|
|
223
|
+
_sessions.set(gatewayToken, entry);
|
|
224
|
+
return entry;
|
|
225
|
+
}
|
|
226
|
+
function updateSessionToken(gatewayToken, newToken) {
|
|
227
|
+
const entry = _sessions.get(gatewayToken);
|
|
228
|
+
if (entry) entry.token = newToken;
|
|
229
|
+
}
|
|
230
|
+
async function acquireSession(sessionId) {
|
|
231
|
+
const existing = await readSticky();
|
|
232
|
+
let account;
|
|
233
|
+
const boundId = existing[sessionId];
|
|
234
|
+
if (boundId !== void 0) {
|
|
235
|
+
const found = findAccount(boundId);
|
|
236
|
+
if (found) {
|
|
237
|
+
account = found;
|
|
238
|
+
} else {
|
|
239
|
+
account = selectAccount();
|
|
240
|
+
await writeSticky(sessionId, account.id);
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
account = selectAccount();
|
|
244
|
+
await writeSticky(sessionId, account.id);
|
|
245
|
+
}
|
|
246
|
+
const gatewayToken = gatewayTokenForSession(sessionId);
|
|
247
|
+
_sessions.set(gatewayToken, {
|
|
248
|
+
gatewayToken,
|
|
249
|
+
accountId: account.id,
|
|
250
|
+
token: account.token,
|
|
251
|
+
provider: account.provider,
|
|
252
|
+
...account.authType ? { authType: account.authType } : {}
|
|
253
|
+
});
|
|
254
|
+
return { gatewayToken, accountId: account.id };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// src/llm-gateway-runtime/proxy-openai.ts
|
|
258
|
+
import {
|
|
259
|
+
mapCodexReasoningEffort,
|
|
260
|
+
prepareCodexResponsesRequest
|
|
261
|
+
} from "@sentropic/llm-gateway";
|
|
262
|
+
var OPENAI_BASE = process.env.OPENAI_UPSTREAM_URL ?? "https://api.openai.com";
|
|
263
|
+
var DEFAULT_CODEX_MAX_INPUT_CHARS = 2e5;
|
|
264
|
+
var CODEX_CONTEXT_TRUNCATION_NOTICE = "[llm-gateway: older Claude Code transcript omitted to fit the Codex upstream context window.]";
|
|
265
|
+
function isCodexOAuthToken(token) {
|
|
266
|
+
return !token.startsWith("sk-") && token.split(".").length === 3;
|
|
267
|
+
}
|
|
268
|
+
var TRANSIENT_UPSTREAM_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
269
|
+
"EAI_AGAIN",
|
|
270
|
+
"ECONNRESET",
|
|
271
|
+
"ETIMEDOUT",
|
|
272
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
273
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
274
|
+
"UND_ERR_SOCKET"
|
|
275
|
+
]);
|
|
276
|
+
var UPSTREAM_RETRY_DELAYS_MS = process.env.NODE_ENV === "test" ? [0, 0] : [250, 1e3];
|
|
277
|
+
function abortError(err) {
|
|
278
|
+
return err.name === "AbortError";
|
|
279
|
+
}
|
|
280
|
+
function upstreamErrorCause(err) {
|
|
281
|
+
const candidate = err;
|
|
282
|
+
if (candidate.code || candidate.hostname) return candidate;
|
|
283
|
+
return candidate.cause;
|
|
284
|
+
}
|
|
285
|
+
function upstreamFetchErrorMessage(err) {
|
|
286
|
+
const cause = upstreamErrorCause(err);
|
|
287
|
+
if (cause?.code && cause.hostname) return `${cause.code} ${cause.hostname}`;
|
|
288
|
+
if (cause?.code) return cause.code;
|
|
289
|
+
if (err instanceof Error && err.message) return err.message;
|
|
290
|
+
return "unknown upstream fetch error";
|
|
291
|
+
}
|
|
292
|
+
function isTransientUpstreamError(err) {
|
|
293
|
+
const code = upstreamErrorCause(err)?.code;
|
|
294
|
+
return !!code && TRANSIENT_UPSTREAM_ERROR_CODES.has(code);
|
|
295
|
+
}
|
|
296
|
+
function anthropicGatewayError(message) {
|
|
297
|
+
return { type: "error", error: { type: "api_error", message } };
|
|
298
|
+
}
|
|
299
|
+
async function sleep(ms) {
|
|
300
|
+
if (ms <= 0) return;
|
|
301
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
302
|
+
}
|
|
303
|
+
async function fetchUpstreamWithRetry(fetcher) {
|
|
304
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
305
|
+
try {
|
|
306
|
+
return await fetcher();
|
|
307
|
+
} catch (err) {
|
|
308
|
+
if (abortError(err) || !isTransientUpstreamError(err) || attempt >= UPSTREAM_RETRY_DELAYS_MS.length) {
|
|
309
|
+
throw err;
|
|
310
|
+
}
|
|
311
|
+
await sleep(UPSTREAM_RETRY_DELAYS_MS[attempt] ?? 0);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
var DEFAULT_MODEL_MAP = {
|
|
316
|
+
"gpt-5.5": "gpt-5.5",
|
|
317
|
+
"gpt-5.3-codex-spark": "gpt-5.3-codex-spark",
|
|
318
|
+
"claude-opus-4-8": "gpt-5.5",
|
|
319
|
+
"claude-opus-4-7": "gpt-5.5",
|
|
320
|
+
"claude-opus-4-6": "gpt-5.5",
|
|
321
|
+
"claude-sonnet-4-6": "gpt-5.5",
|
|
322
|
+
"claude-sonnet-4-5": "gpt-5.5",
|
|
323
|
+
"claude-haiku-4-5-20251001": "gpt-5.5"
|
|
324
|
+
};
|
|
325
|
+
var _modelMap = null;
|
|
326
|
+
function modelMap() {
|
|
327
|
+
if (!_modelMap) {
|
|
328
|
+
_modelMap = process.env.OPENAI_MODEL_MAP ? { ...DEFAULT_MODEL_MAP, ...JSON.parse(process.env.OPENAI_MODEL_MAP) } : DEFAULT_MODEL_MAP;
|
|
329
|
+
}
|
|
330
|
+
return _modelMap;
|
|
331
|
+
}
|
|
332
|
+
function mapModel(anthropicModel) {
|
|
333
|
+
if (anthropicModel.startsWith("gpt-")) return anthropicModel;
|
|
334
|
+
return modelMap()[anthropicModel] ?? "gpt-5.5";
|
|
335
|
+
}
|
|
336
|
+
function budgetToEffort(budgetTokens) {
|
|
337
|
+
if (budgetTokens >= 5e4) return "xhigh";
|
|
338
|
+
if (budgetTokens >= 25e3) return "high";
|
|
339
|
+
if (budgetTokens >= 8e3) return "medium";
|
|
340
|
+
return "low";
|
|
341
|
+
}
|
|
342
|
+
function extractAssistantContent(content) {
|
|
343
|
+
if (typeof content === "string") return { text: content || null, toolCalls: [] };
|
|
344
|
+
const texts = [];
|
|
345
|
+
const toolCalls = [];
|
|
346
|
+
for (const item of content) {
|
|
347
|
+
if (item.type === "text") {
|
|
348
|
+
texts.push(item.text);
|
|
349
|
+
} else if (item.type === "tool_use") {
|
|
350
|
+
const tc = item;
|
|
351
|
+
toolCalls.push({
|
|
352
|
+
id: tc.id,
|
|
353
|
+
type: "function",
|
|
354
|
+
function: { name: tc.name, arguments: JSON.stringify(tc.input) }
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return { text: texts.join("") || null, toolCalls };
|
|
359
|
+
}
|
|
360
|
+
function toOAIMessages(messages) {
|
|
361
|
+
const result = [];
|
|
362
|
+
for (const msg of messages) {
|
|
363
|
+
if (msg.role === "assistant") {
|
|
364
|
+
const { text, toolCalls } = extractAssistantContent(msg.content);
|
|
365
|
+
const entry = { role: "assistant", content: text };
|
|
366
|
+
if (toolCalls.length > 0) entry.tool_calls = toolCalls;
|
|
367
|
+
result.push(entry);
|
|
368
|
+
} else {
|
|
369
|
+
if (Array.isArray(msg.content)) {
|
|
370
|
+
for (const item of msg.content) {
|
|
371
|
+
if (item.type === "tool_result") {
|
|
372
|
+
const tr = item;
|
|
373
|
+
const c = typeof tr.content === "string" ? tr.content : tr.content.map((b) => b.text).join("");
|
|
374
|
+
result.push({ role: "tool", tool_call_id: tr.tool_use_id, content: c });
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const textItems = msg.content.filter(
|
|
378
|
+
(c) => c.type === "text"
|
|
379
|
+
);
|
|
380
|
+
if (textItems.length > 0) {
|
|
381
|
+
result.push({
|
|
382
|
+
role: "user",
|
|
383
|
+
content: textItems.map((t) => ({ type: "text", text: t.text }))
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
result.push({ role: "user", content: msg.content });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return result;
|
|
392
|
+
}
|
|
393
|
+
function systemToText(system) {
|
|
394
|
+
if (typeof system === "string") return system || void 0;
|
|
395
|
+
if (!Array.isArray(system)) return void 0;
|
|
396
|
+
const parts = [];
|
|
397
|
+
for (const block of system) {
|
|
398
|
+
if (block.type === "text") {
|
|
399
|
+
parts.push(block.text);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return parts.join("\n\n") || void 0;
|
|
403
|
+
}
|
|
404
|
+
function toCodexInput(messages) {
|
|
405
|
+
const items = [];
|
|
406
|
+
for (const msg of messages) {
|
|
407
|
+
if (msg.role === "assistant") {
|
|
408
|
+
const texts = [];
|
|
409
|
+
const toolCalls = [];
|
|
410
|
+
if (typeof msg.content === "string") {
|
|
411
|
+
if (msg.content) texts.push(msg.content);
|
|
412
|
+
} else {
|
|
413
|
+
for (const block of msg.content) {
|
|
414
|
+
if (block.type === "text") texts.push(block.text);
|
|
415
|
+
else if (block.type === "tool_use") toolCalls.push(block);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const text = texts.join("");
|
|
419
|
+
if (text || toolCalls.length === 0) {
|
|
420
|
+
items.push({ type: "message", role: "assistant", content: text });
|
|
421
|
+
}
|
|
422
|
+
for (const tc of toolCalls) {
|
|
423
|
+
items.push({ type: "function_call", call_id: tc.id, name: tc.name, arguments: JSON.stringify(tc.input) });
|
|
424
|
+
}
|
|
425
|
+
} else {
|
|
426
|
+
const texts = [];
|
|
427
|
+
const toolResults = [];
|
|
428
|
+
if (typeof msg.content === "string") {
|
|
429
|
+
if (msg.content) texts.push(msg.content);
|
|
430
|
+
} else {
|
|
431
|
+
for (const block of msg.content) {
|
|
432
|
+
if (block.type === "text") texts.push(block.text);
|
|
433
|
+
else if (block.type === "tool_result") toolResults.push(block);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
for (const tr of toolResults) {
|
|
437
|
+
const output = typeof tr.content === "string" ? tr.content : tr.content.map((b) => b.text).join("");
|
|
438
|
+
items.push({ type: "function_call_output", call_id: tr.tool_use_id, output });
|
|
439
|
+
}
|
|
440
|
+
const text = texts.join("");
|
|
441
|
+
if (text) items.push({ type: "message", role: "user", content: text });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return items;
|
|
445
|
+
}
|
|
446
|
+
function codexEffort(budgetTokens) {
|
|
447
|
+
return mapCodexReasoningEffort(budgetToEffort(budgetTokens)) ?? "low";
|
|
448
|
+
}
|
|
449
|
+
function toCodexRequest(body) {
|
|
450
|
+
const req = {
|
|
451
|
+
model: mapModel(body.model),
|
|
452
|
+
input: toCodexInput(body.messages),
|
|
453
|
+
store: false,
|
|
454
|
+
stream: true
|
|
455
|
+
};
|
|
456
|
+
const instructions = systemToText(body.system);
|
|
457
|
+
if (instructions) req.instructions = instructions;
|
|
458
|
+
if (body.thinking?.type === "enabled") {
|
|
459
|
+
req.reasoning = { effort: codexEffort(body.thinking.budget_tokens) };
|
|
460
|
+
}
|
|
461
|
+
if (body.tools && body.tools.length > 0) {
|
|
462
|
+
req.tools = body.tools.map((t) => ({
|
|
463
|
+
type: "function",
|
|
464
|
+
name: t.name,
|
|
465
|
+
...t.description !== void 0 ? { description: t.description } : {},
|
|
466
|
+
parameters: t.input_schema,
|
|
467
|
+
strict: false
|
|
468
|
+
}));
|
|
469
|
+
}
|
|
470
|
+
return req;
|
|
471
|
+
}
|
|
472
|
+
function codexMaxInputChars() {
|
|
473
|
+
const configured = Number.parseInt(process.env.CODEX_MAX_INPUT_CHARS ?? "", 10);
|
|
474
|
+
return Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_CODEX_MAX_INPUT_CHARS;
|
|
475
|
+
}
|
|
476
|
+
function anthropicInputChars(body) {
|
|
477
|
+
return JSON.stringify({ system: body.system, messages: body.messages }).length;
|
|
478
|
+
}
|
|
479
|
+
function trimTextTail(text, budget) {
|
|
480
|
+
if (text.length <= budget) return text;
|
|
481
|
+
if (budget <= CODEX_CONTEXT_TRUNCATION_NOTICE.length + 8) {
|
|
482
|
+
return CODEX_CONTEXT_TRUNCATION_NOTICE.slice(0, Math.max(0, budget));
|
|
483
|
+
}
|
|
484
|
+
const prefix = `${CODEX_CONTEXT_TRUNCATION_NOTICE}
|
|
485
|
+
|
|
486
|
+
`;
|
|
487
|
+
return prefix + text.slice(-(budget - prefix.length));
|
|
488
|
+
}
|
|
489
|
+
function trimContentTail(content, budget) {
|
|
490
|
+
if (typeof content === "string") return trimTextTail(content, budget);
|
|
491
|
+
const kept = [];
|
|
492
|
+
let remaining = budget;
|
|
493
|
+
for (let i = content.length - 1; i >= 0; i -= 1) {
|
|
494
|
+
const block = content[i];
|
|
495
|
+
const size = JSON.stringify(block).length;
|
|
496
|
+
if (size <= remaining) {
|
|
497
|
+
kept.unshift(block);
|
|
498
|
+
remaining -= size;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
if (block.type === "text" && remaining > CODEX_CONTEXT_TRUNCATION_NOTICE.length + 32) {
|
|
502
|
+
kept.unshift({
|
|
503
|
+
...block,
|
|
504
|
+
text: trimTextTail(block.text, remaining)
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
return kept.length > 0 ? kept : [{ type: "text", text: CODEX_CONTEXT_TRUNCATION_NOTICE }];
|
|
510
|
+
}
|
|
511
|
+
function stripLeadingOrphanToolResults(messages) {
|
|
512
|
+
if (messages.length === 0) return messages;
|
|
513
|
+
const first = messages[0];
|
|
514
|
+
if (first.role !== "user" || !Array.isArray(first.content)) return messages;
|
|
515
|
+
const filtered = first.content.filter((block) => block.type !== "tool_result");
|
|
516
|
+
if (filtered.length === first.content.length) return messages;
|
|
517
|
+
if (filtered.length === 0) return messages.slice(1);
|
|
518
|
+
return [{ ...first, content: filtered }, ...messages.slice(1)];
|
|
519
|
+
}
|
|
520
|
+
function trimCodexBodyForContext(body, maxChars = codexMaxInputChars()) {
|
|
521
|
+
const beforeChars = anthropicInputChars(body);
|
|
522
|
+
if (beforeChars <= maxChars) {
|
|
523
|
+
return { body, trimmed: false, beforeChars, afterChars: beforeChars };
|
|
524
|
+
}
|
|
525
|
+
const notice = { role: "user", content: CODEX_CONTEXT_TRUNCATION_NOTICE };
|
|
526
|
+
const systemChars = JSON.stringify(body.system ?? "").length;
|
|
527
|
+
const noticeChars = JSON.stringify(notice).length;
|
|
528
|
+
let remaining = Math.max(1024, maxChars - systemChars - noticeChars);
|
|
529
|
+
const kept = [];
|
|
530
|
+
for (let i = body.messages.length - 1; i >= 0; i -= 1) {
|
|
531
|
+
const message = body.messages[i];
|
|
532
|
+
const messageChars = JSON.stringify(message).length;
|
|
533
|
+
if (messageChars <= remaining) {
|
|
534
|
+
kept.unshift(message);
|
|
535
|
+
remaining -= messageChars;
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
if (kept.length === 0 && remaining > 1024) {
|
|
539
|
+
kept.unshift({ ...message, content: trimContentTail(message.content, remaining) });
|
|
540
|
+
}
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
const trimmedMessages = stripLeadingOrphanToolResults(kept);
|
|
544
|
+
const trimmedBody = {
|
|
545
|
+
...body,
|
|
546
|
+
messages: [notice, ...trimmedMessages]
|
|
547
|
+
};
|
|
548
|
+
const afterChars = anthropicInputChars(trimmedBody);
|
|
549
|
+
return { body: trimmedBody, trimmed: true, beforeChars, afterChars };
|
|
550
|
+
}
|
|
551
|
+
function toOpenAIRequest(body) {
|
|
552
|
+
const messages = [];
|
|
553
|
+
const system = systemToText(body.system);
|
|
554
|
+
if (system) messages.push({ role: "system", content: system });
|
|
555
|
+
messages.push(...toOAIMessages(body.messages));
|
|
556
|
+
const req = {
|
|
557
|
+
model: mapModel(body.model),
|
|
558
|
+
messages,
|
|
559
|
+
max_completion_tokens: body.max_tokens
|
|
560
|
+
};
|
|
561
|
+
if (body.stream) {
|
|
562
|
+
req.stream = true;
|
|
563
|
+
req.stream_options = { include_usage: true };
|
|
564
|
+
}
|
|
565
|
+
if (body.thinking?.type === "enabled") {
|
|
566
|
+
req.reasoning_effort = budgetToEffort(body.thinking.budget_tokens);
|
|
567
|
+
}
|
|
568
|
+
if (body.tools && body.tools.length > 0) {
|
|
569
|
+
req.tools = body.tools.map((t) => ({
|
|
570
|
+
type: "function",
|
|
571
|
+
function: {
|
|
572
|
+
name: t.name,
|
|
573
|
+
...t.description !== void 0 ? { description: t.description } : {},
|
|
574
|
+
parameters: t.input_schema
|
|
575
|
+
}
|
|
576
|
+
}));
|
|
577
|
+
}
|
|
578
|
+
return req;
|
|
579
|
+
}
|
|
580
|
+
function toAnthropicResponse(openai, originalModel) {
|
|
581
|
+
const choice = openai.choices[0];
|
|
582
|
+
const message = choice?.message;
|
|
583
|
+
const content = [];
|
|
584
|
+
if (message?.content) {
|
|
585
|
+
content.push({ type: "text", text: message.content });
|
|
586
|
+
}
|
|
587
|
+
for (const tc of message?.tool_calls ?? []) {
|
|
588
|
+
let input = {};
|
|
589
|
+
try {
|
|
590
|
+
input = JSON.parse(tc.function.arguments);
|
|
591
|
+
} catch {
|
|
592
|
+
}
|
|
593
|
+
content.push({ type: "tool_use", id: tc.id, name: tc.function.name, input });
|
|
594
|
+
}
|
|
595
|
+
const stopReason = choice?.finish_reason === "tool_calls" ? "tool_use" : choice?.finish_reason === "length" ? "max_tokens" : "end_turn";
|
|
596
|
+
return {
|
|
597
|
+
id: openai.id ?? `msg_${Date.now().toString(36)}`,
|
|
598
|
+
type: "message",
|
|
599
|
+
role: "assistant",
|
|
600
|
+
content,
|
|
601
|
+
model: originalModel,
|
|
602
|
+
stop_reason: stopReason,
|
|
603
|
+
stop_sequence: null,
|
|
604
|
+
usage: {
|
|
605
|
+
input_tokens: openai.usage?.prompt_tokens ?? 0,
|
|
606
|
+
output_tokens: openai.usage?.completion_tokens ?? 0
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
function sseEvent(event, data) {
|
|
611
|
+
return `event: ${event}
|
|
612
|
+
data: ${JSON.stringify(data)}
|
|
613
|
+
|
|
614
|
+
`;
|
|
615
|
+
}
|
|
616
|
+
function closeStreamWithGatewayError(controller, enc, message) {
|
|
617
|
+
try {
|
|
618
|
+
controller.enqueue(enc.encode(sseEvent("error", anthropicGatewayError(message))));
|
|
619
|
+
} catch {
|
|
620
|
+
}
|
|
621
|
+
try {
|
|
622
|
+
controller.close();
|
|
623
|
+
} catch {
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
function translateOpenAIStreamToAnthropic(openaiStream, originalModel, messageId, estimatedInputTokens) {
|
|
627
|
+
const enc = new TextEncoder();
|
|
628
|
+
return new ReadableStream({
|
|
629
|
+
async start(controller) {
|
|
630
|
+
const emit = (s) => controller.enqueue(enc.encode(s));
|
|
631
|
+
let nextBlockIdx = 0;
|
|
632
|
+
let textBlockIdx = -1;
|
|
633
|
+
let textBlockOpen = false;
|
|
634
|
+
const toolBlockMap = /* @__PURE__ */ new Map();
|
|
635
|
+
let outputTokens = 0;
|
|
636
|
+
let stopReason = "end_turn";
|
|
637
|
+
emit(sseEvent("message_start", {
|
|
638
|
+
type: "message_start",
|
|
639
|
+
message: {
|
|
640
|
+
id: messageId,
|
|
641
|
+
type: "message",
|
|
642
|
+
role: "assistant",
|
|
643
|
+
content: [],
|
|
644
|
+
model: originalModel,
|
|
645
|
+
stop_reason: null,
|
|
646
|
+
stop_sequence: null,
|
|
647
|
+
usage: { input_tokens: estimatedInputTokens, output_tokens: 0 }
|
|
648
|
+
}
|
|
649
|
+
}));
|
|
650
|
+
emit(sseEvent("ping", { type: "ping" }));
|
|
651
|
+
const reader = openaiStream.getReader();
|
|
652
|
+
let buf = "";
|
|
653
|
+
try {
|
|
654
|
+
while (true) {
|
|
655
|
+
const { done, value } = await reader.read();
|
|
656
|
+
if (done) break;
|
|
657
|
+
buf += new TextDecoder().decode(value);
|
|
658
|
+
const lines = buf.split("\n");
|
|
659
|
+
buf = lines.pop() ?? "";
|
|
660
|
+
for (const line of lines) {
|
|
661
|
+
if (!line.startsWith("data: ")) continue;
|
|
662
|
+
const raw = line.slice(6).trim();
|
|
663
|
+
if (raw === "[DONE]") {
|
|
664
|
+
if (textBlockOpen) {
|
|
665
|
+
emit(sseEvent("content_block_stop", { type: "content_block_stop", index: textBlockIdx }));
|
|
666
|
+
}
|
|
667
|
+
for (const [, blockIdx] of toolBlockMap) {
|
|
668
|
+
emit(sseEvent("content_block_stop", { type: "content_block_stop", index: blockIdx }));
|
|
669
|
+
}
|
|
670
|
+
emit(sseEvent("message_delta", {
|
|
671
|
+
type: "message_delta",
|
|
672
|
+
delta: { stop_reason: stopReason, stop_sequence: null },
|
|
673
|
+
usage: { output_tokens: outputTokens }
|
|
674
|
+
}));
|
|
675
|
+
emit(sseEvent("message_stop", { type: "message_stop" }));
|
|
676
|
+
controller.close();
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
let chunk;
|
|
680
|
+
try {
|
|
681
|
+
chunk = JSON.parse(raw);
|
|
682
|
+
} catch {
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
if (chunk.usage?.completion_tokens) outputTokens = chunk.usage.completion_tokens;
|
|
686
|
+
const choice = chunk.choices?.[0];
|
|
687
|
+
if (!choice) continue;
|
|
688
|
+
if (choice.finish_reason) {
|
|
689
|
+
stopReason = choice.finish_reason === "tool_calls" ? "tool_use" : choice.finish_reason === "length" ? "max_tokens" : "end_turn";
|
|
690
|
+
}
|
|
691
|
+
const delta = choice.delta;
|
|
692
|
+
if (!delta) continue;
|
|
693
|
+
if (typeof delta.content === "string" && delta.content.length > 0) {
|
|
694
|
+
if (!textBlockOpen) {
|
|
695
|
+
textBlockIdx = nextBlockIdx++;
|
|
696
|
+
textBlockOpen = true;
|
|
697
|
+
emit(sseEvent("content_block_start", {
|
|
698
|
+
type: "content_block_start",
|
|
699
|
+
index: textBlockIdx,
|
|
700
|
+
content_block: { type: "text", text: "" }
|
|
701
|
+
}));
|
|
702
|
+
}
|
|
703
|
+
emit(sseEvent("content_block_delta", {
|
|
704
|
+
type: "content_block_delta",
|
|
705
|
+
index: textBlockIdx,
|
|
706
|
+
delta: { type: "text_delta", text: delta.content }
|
|
707
|
+
}));
|
|
708
|
+
}
|
|
709
|
+
for (const tc of delta.tool_calls ?? []) {
|
|
710
|
+
if (textBlockOpen) {
|
|
711
|
+
emit(sseEvent("content_block_stop", { type: "content_block_stop", index: textBlockIdx }));
|
|
712
|
+
textBlockOpen = false;
|
|
713
|
+
}
|
|
714
|
+
if (!toolBlockMap.has(tc.index)) {
|
|
715
|
+
const blockIdx2 = nextBlockIdx++;
|
|
716
|
+
toolBlockMap.set(tc.index, blockIdx2);
|
|
717
|
+
emit(sseEvent("content_block_start", {
|
|
718
|
+
type: "content_block_start",
|
|
719
|
+
index: blockIdx2,
|
|
720
|
+
content_block: {
|
|
721
|
+
type: "tool_use",
|
|
722
|
+
id: tc.id ?? `toolu_${tc.index}`,
|
|
723
|
+
name: tc.function?.name ?? "",
|
|
724
|
+
input: {}
|
|
725
|
+
}
|
|
726
|
+
}));
|
|
727
|
+
}
|
|
728
|
+
const blockIdx = toolBlockMap.get(tc.index);
|
|
729
|
+
if (tc.function?.arguments) {
|
|
730
|
+
emit(sseEvent("content_block_delta", {
|
|
731
|
+
type: "content_block_delta",
|
|
732
|
+
index: blockIdx,
|
|
733
|
+
delta: { type: "input_json_delta", partial_json: tc.function.arguments }
|
|
734
|
+
}));
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (textBlockOpen) {
|
|
740
|
+
emit(sseEvent("content_block_stop", { type: "content_block_stop", index: textBlockIdx }));
|
|
741
|
+
}
|
|
742
|
+
for (const [, blockIdx] of toolBlockMap) {
|
|
743
|
+
emit(sseEvent("content_block_stop", { type: "content_block_stop", index: blockIdx }));
|
|
744
|
+
}
|
|
745
|
+
emit(sseEvent("message_delta", {
|
|
746
|
+
type: "message_delta",
|
|
747
|
+
delta: { stop_reason: stopReason, stop_sequence: null },
|
|
748
|
+
usage: { output_tokens: outputTokens }
|
|
749
|
+
}));
|
|
750
|
+
emit(sseEvent("message_stop", { type: "message_stop" }));
|
|
751
|
+
controller.close();
|
|
752
|
+
} catch (err) {
|
|
753
|
+
closeStreamWithGatewayError(
|
|
754
|
+
controller,
|
|
755
|
+
enc,
|
|
756
|
+
`Upstream stream failed: ${upstreamFetchErrorMessage(err)}`
|
|
757
|
+
);
|
|
758
|
+
} finally {
|
|
759
|
+
reader.releaseLock();
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
function translateCodexStreamToAnthropic(codexStream, originalModel, messageId, estimatedInputTokens) {
|
|
765
|
+
const enc = new TextEncoder();
|
|
766
|
+
return new ReadableStream({
|
|
767
|
+
async start(controller) {
|
|
768
|
+
const emit = (s) => controller.enqueue(enc.encode(s));
|
|
769
|
+
let nextBlockIdx = 0;
|
|
770
|
+
const blockMap = /* @__PURE__ */ new Map();
|
|
771
|
+
let textBlockOpen = false;
|
|
772
|
+
let outputTokens = 0;
|
|
773
|
+
let stopReason = "end_turn";
|
|
774
|
+
emit(sseEvent("message_start", {
|
|
775
|
+
type: "message_start",
|
|
776
|
+
message: {
|
|
777
|
+
id: messageId,
|
|
778
|
+
type: "message",
|
|
779
|
+
role: "assistant",
|
|
780
|
+
content: [],
|
|
781
|
+
model: originalModel,
|
|
782
|
+
stop_reason: null,
|
|
783
|
+
stop_sequence: null,
|
|
784
|
+
usage: { input_tokens: estimatedInputTokens, output_tokens: 0 }
|
|
785
|
+
}
|
|
786
|
+
}));
|
|
787
|
+
emit(sseEvent("ping", { type: "ping" }));
|
|
788
|
+
const reader = codexStream.getReader();
|
|
789
|
+
let buf = "";
|
|
790
|
+
let currentEvent = "";
|
|
791
|
+
try {
|
|
792
|
+
while (true) {
|
|
793
|
+
const { done, value } = await reader.read();
|
|
794
|
+
if (done) break;
|
|
795
|
+
buf += new TextDecoder().decode(value);
|
|
796
|
+
const lines = buf.split("\n");
|
|
797
|
+
buf = lines.pop() ?? "";
|
|
798
|
+
for (const line of lines) {
|
|
799
|
+
if (line.startsWith("event: ")) {
|
|
800
|
+
currentEvent = line.slice(7).trim();
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
if (!line.startsWith("data: ")) {
|
|
804
|
+
if (line === "") currentEvent = "";
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
const raw = line.slice(6).trim();
|
|
808
|
+
if (raw === "[DONE]") continue;
|
|
809
|
+
let data;
|
|
810
|
+
try {
|
|
811
|
+
data = JSON.parse(raw);
|
|
812
|
+
} catch {
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
const evType = data.type ?? currentEvent;
|
|
816
|
+
const outputIndex = data.output_index ?? 0;
|
|
817
|
+
switch (evType) {
|
|
818
|
+
case "response.output_item.added": {
|
|
819
|
+
const item = data.item;
|
|
820
|
+
if (!item) break;
|
|
821
|
+
if (item.type === "function_call") {
|
|
822
|
+
const blockIdx = nextBlockIdx++;
|
|
823
|
+
blockMap.set(outputIndex, { type: "tool", idx: blockIdx });
|
|
824
|
+
emit(sseEvent("content_block_start", {
|
|
825
|
+
type: "content_block_start",
|
|
826
|
+
index: blockIdx,
|
|
827
|
+
content_block: {
|
|
828
|
+
type: "tool_use",
|
|
829
|
+
id: item.call_id ?? `toolu_${outputIndex}`,
|
|
830
|
+
name: item.name ?? "",
|
|
831
|
+
input: {}
|
|
832
|
+
}
|
|
833
|
+
}));
|
|
834
|
+
}
|
|
835
|
+
break;
|
|
836
|
+
}
|
|
837
|
+
case "response.output_text.delta": {
|
|
838
|
+
const delta = data.delta;
|
|
839
|
+
if (!delta) break;
|
|
840
|
+
if (!blockMap.has(outputIndex)) {
|
|
841
|
+
const blockIdx = nextBlockIdx++;
|
|
842
|
+
blockMap.set(outputIndex, { type: "text", idx: blockIdx });
|
|
843
|
+
textBlockOpen = true;
|
|
844
|
+
emit(sseEvent("content_block_start", {
|
|
845
|
+
type: "content_block_start",
|
|
846
|
+
index: blockIdx,
|
|
847
|
+
content_block: { type: "text", text: "" }
|
|
848
|
+
}));
|
|
849
|
+
}
|
|
850
|
+
const tBlock = blockMap.get(outputIndex);
|
|
851
|
+
emit(sseEvent("content_block_delta", {
|
|
852
|
+
type: "content_block_delta",
|
|
853
|
+
index: tBlock.idx,
|
|
854
|
+
delta: { type: "text_delta", text: delta }
|
|
855
|
+
}));
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
case "response.function_call_arguments.delta": {
|
|
859
|
+
const delta = data.delta;
|
|
860
|
+
if (!delta) break;
|
|
861
|
+
const fBlock = blockMap.get(outputIndex);
|
|
862
|
+
if (fBlock?.type === "tool") {
|
|
863
|
+
emit(sseEvent("content_block_delta", {
|
|
864
|
+
type: "content_block_delta",
|
|
865
|
+
index: fBlock.idx,
|
|
866
|
+
delta: { type: "input_json_delta", partial_json: delta }
|
|
867
|
+
}));
|
|
868
|
+
}
|
|
869
|
+
break;
|
|
870
|
+
}
|
|
871
|
+
case "response.output_item.done": {
|
|
872
|
+
const block = blockMap.get(outputIndex);
|
|
873
|
+
if (block) {
|
|
874
|
+
emit(sseEvent("content_block_stop", { type: "content_block_stop", index: block.idx }));
|
|
875
|
+
if (block.type === "text") textBlockOpen = false;
|
|
876
|
+
}
|
|
877
|
+
const item = data.item;
|
|
878
|
+
if (item?.type === "function_call") stopReason = "tool_use";
|
|
879
|
+
break;
|
|
880
|
+
}
|
|
881
|
+
case "response.completed": {
|
|
882
|
+
const response = data.response;
|
|
883
|
+
const usage = response?.usage;
|
|
884
|
+
if (typeof usage?.output_tokens === "number") outputTokens = usage.output_tokens;
|
|
885
|
+
for (const [, block] of blockMap) {
|
|
886
|
+
if (block.type === "text" && textBlockOpen) {
|
|
887
|
+
emit(sseEvent("content_block_stop", { type: "content_block_stop", index: block.idx }));
|
|
888
|
+
textBlockOpen = false;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
emit(sseEvent("message_delta", {
|
|
892
|
+
type: "message_delta",
|
|
893
|
+
delta: { stop_reason: stopReason, stop_sequence: null },
|
|
894
|
+
usage: { output_tokens: outputTokens }
|
|
895
|
+
}));
|
|
896
|
+
emit(sseEvent("message_stop", { type: "message_stop" }));
|
|
897
|
+
controller.close();
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
case "response.failed": {
|
|
901
|
+
emit(sseEvent("error", {
|
|
902
|
+
type: "error",
|
|
903
|
+
error: { type: "api_error", message: codexFailureMessage(data) }
|
|
904
|
+
}));
|
|
905
|
+
controller.close();
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
if (textBlockOpen) {
|
|
912
|
+
for (const [, block] of blockMap) {
|
|
913
|
+
if (block.type === "text") {
|
|
914
|
+
emit(sseEvent("content_block_stop", { type: "content_block_stop", index: block.idx }));
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
for (const [, block] of blockMap) {
|
|
919
|
+
if (block.type === "tool") {
|
|
920
|
+
emit(sseEvent("content_block_stop", { type: "content_block_stop", index: block.idx }));
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
emit(sseEvent("message_delta", {
|
|
924
|
+
type: "message_delta",
|
|
925
|
+
delta: { stop_reason: stopReason, stop_sequence: null },
|
|
926
|
+
usage: { output_tokens: outputTokens }
|
|
927
|
+
}));
|
|
928
|
+
emit(sseEvent("message_stop", { type: "message_stop" }));
|
|
929
|
+
controller.close();
|
|
930
|
+
} catch (err) {
|
|
931
|
+
closeStreamWithGatewayError(
|
|
932
|
+
controller,
|
|
933
|
+
enc,
|
|
934
|
+
`Upstream stream failed: ${upstreamFetchErrorMessage(err)}`
|
|
935
|
+
);
|
|
936
|
+
} finally {
|
|
937
|
+
reader.releaseLock();
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
function codexFailureMessage(data) {
|
|
943
|
+
const response = data.response;
|
|
944
|
+
const error = data.error ?? response?.error;
|
|
945
|
+
const message = error?.message ?? error?.code ?? response?.status;
|
|
946
|
+
return typeof message === "string" && message ? message : "Codex upstream response failed";
|
|
947
|
+
}
|
|
948
|
+
async function codexStreamToAnthropicResponse(codexStream, originalModel, messageId, estimatedInputTokens) {
|
|
949
|
+
const reader = codexStream.getReader();
|
|
950
|
+
const blockMap = /* @__PURE__ */ new Map();
|
|
951
|
+
let nextBlockIdx = 0;
|
|
952
|
+
let outputTokens = 0;
|
|
953
|
+
let buf = "";
|
|
954
|
+
let currentEvent = "";
|
|
955
|
+
const ensureTextBlock = (outputIndex) => {
|
|
956
|
+
const existing = blockMap.get(outputIndex);
|
|
957
|
+
if (existing?.type === "text") return existing;
|
|
958
|
+
const block = {
|
|
959
|
+
type: "text",
|
|
960
|
+
idx: nextBlockIdx++,
|
|
961
|
+
text: ""
|
|
962
|
+
};
|
|
963
|
+
blockMap.set(outputIndex, block);
|
|
964
|
+
return block;
|
|
965
|
+
};
|
|
966
|
+
try {
|
|
967
|
+
while (true) {
|
|
968
|
+
const { done, value } = await reader.read();
|
|
969
|
+
if (done) break;
|
|
970
|
+
buf += new TextDecoder().decode(value);
|
|
971
|
+
const lines = buf.split("\n");
|
|
972
|
+
buf = lines.pop() ?? "";
|
|
973
|
+
for (const line of lines) {
|
|
974
|
+
if (line.startsWith("event: ")) {
|
|
975
|
+
currentEvent = line.slice(7).trim();
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
if (!line.startsWith("data: ")) {
|
|
979
|
+
if (line === "") currentEvent = "";
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
const raw = line.slice(6).trim();
|
|
983
|
+
if (raw === "[DONE]") continue;
|
|
984
|
+
let data;
|
|
985
|
+
try {
|
|
986
|
+
data = JSON.parse(raw);
|
|
987
|
+
} catch {
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
const evType = data.type ?? currentEvent;
|
|
991
|
+
const outputIndex = data.output_index ?? 0;
|
|
992
|
+
switch (evType) {
|
|
993
|
+
case "response.output_item.added": {
|
|
994
|
+
const item = data.item;
|
|
995
|
+
if (item?.type === "function_call") {
|
|
996
|
+
blockMap.set(outputIndex, {
|
|
997
|
+
type: "tool",
|
|
998
|
+
idx: nextBlockIdx++,
|
|
999
|
+
id: item.call_id ?? `toolu_${outputIndex}`,
|
|
1000
|
+
name: item.name ?? "",
|
|
1001
|
+
args: ""
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
break;
|
|
1005
|
+
}
|
|
1006
|
+
case "response.output_text.delta": {
|
|
1007
|
+
const delta = data.delta;
|
|
1008
|
+
if (delta) ensureTextBlock(outputIndex).text += delta;
|
|
1009
|
+
break;
|
|
1010
|
+
}
|
|
1011
|
+
case "response.output_text.done": {
|
|
1012
|
+
const text = data.text;
|
|
1013
|
+
if (text !== void 0) ensureTextBlock(outputIndex).text = text;
|
|
1014
|
+
break;
|
|
1015
|
+
}
|
|
1016
|
+
case "response.function_call_arguments.delta": {
|
|
1017
|
+
const delta = data.delta;
|
|
1018
|
+
const block = blockMap.get(outputIndex);
|
|
1019
|
+
if (delta && block?.type === "tool") block.args += delta;
|
|
1020
|
+
break;
|
|
1021
|
+
}
|
|
1022
|
+
case "response.output_item.done": {
|
|
1023
|
+
const item = data.item;
|
|
1024
|
+
const block = blockMap.get(outputIndex);
|
|
1025
|
+
if (item?.type === "function_call" && block?.type === "tool") {
|
|
1026
|
+
if (typeof item.call_id === "string") block.id = item.call_id;
|
|
1027
|
+
if (typeof item.name === "string") block.name = item.name;
|
|
1028
|
+
if (typeof item.arguments === "string") block.args = item.arguments;
|
|
1029
|
+
}
|
|
1030
|
+
break;
|
|
1031
|
+
}
|
|
1032
|
+
case "response.completed": {
|
|
1033
|
+
const response = data.response;
|
|
1034
|
+
const usage = response?.usage;
|
|
1035
|
+
if (typeof usage?.output_tokens === "number") outputTokens = usage.output_tokens;
|
|
1036
|
+
break;
|
|
1037
|
+
}
|
|
1038
|
+
case "response.failed":
|
|
1039
|
+
throw new Error(codexFailureMessage(data));
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
} finally {
|
|
1044
|
+
reader.releaseLock();
|
|
1045
|
+
}
|
|
1046
|
+
const content = [...blockMap.values()].sort((a, b) => a.idx - b.idx).map((block) => {
|
|
1047
|
+
if (block.type === "text") return { type: "text", text: block.text };
|
|
1048
|
+
let input = {};
|
|
1049
|
+
try {
|
|
1050
|
+
input = block.args ? JSON.parse(block.args) : {};
|
|
1051
|
+
} catch {
|
|
1052
|
+
input = {};
|
|
1053
|
+
}
|
|
1054
|
+
return { type: "tool_use", id: block.id, name: block.name, input };
|
|
1055
|
+
});
|
|
1056
|
+
return {
|
|
1057
|
+
id: messageId,
|
|
1058
|
+
type: "message",
|
|
1059
|
+
role: "assistant",
|
|
1060
|
+
content,
|
|
1061
|
+
model: originalModel,
|
|
1062
|
+
stop_reason: content.some((block) => block.type === "tool_use") ? "tool_use" : "end_turn",
|
|
1063
|
+
stop_sequence: null,
|
|
1064
|
+
usage: { input_tokens: estimatedInputTokens, output_tokens: outputTokens }
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
async function handleMessagesViaOpenAI(c, session) {
|
|
1068
|
+
const rawBody = await c.req.arrayBuffer();
|
|
1069
|
+
let body;
|
|
1070
|
+
try {
|
|
1071
|
+
body = JSON.parse(new TextDecoder().decode(rawBody));
|
|
1072
|
+
} catch {
|
|
1073
|
+
return c.json({ error: "invalid JSON" }, 400);
|
|
1074
|
+
}
|
|
1075
|
+
const originalModel = body.model;
|
|
1076
|
+
const isCodex = isCodexOAuthToken(session.token);
|
|
1077
|
+
const codexContext = isCodex ? trimCodexBodyForContext(body) : null;
|
|
1078
|
+
const upstreamBody = codexContext?.body ?? body;
|
|
1079
|
+
if (codexContext?.trimmed) {
|
|
1080
|
+
console.warn(
|
|
1081
|
+
`[llm-gateway] Codex context trimmed for ${originalModel}: ${codexContext.beforeChars} -> ${codexContext.afterChars} chars`
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
const upstreamReq = isCodex ? toCodexRequest(upstreamBody) : toOpenAIRequest(upstreamBody);
|
|
1085
|
+
const messageId = `msg_${Date.now().toString(36)}`;
|
|
1086
|
+
const estimatedInputTokens = Math.ceil(JSON.stringify(upstreamBody.messages).length / 4);
|
|
1087
|
+
const doFetch = (token) => {
|
|
1088
|
+
if (isCodex) {
|
|
1089
|
+
const codexRequest = prepareCodexResponsesRequest(
|
|
1090
|
+
upstreamReq
|
|
1091
|
+
);
|
|
1092
|
+
return fetch(codexRequest.url, {
|
|
1093
|
+
method: "POST",
|
|
1094
|
+
headers: {
|
|
1095
|
+
Authorization: `Bearer ${token}`,
|
|
1096
|
+
"content-type": "application/json",
|
|
1097
|
+
originator: "opencode",
|
|
1098
|
+
"User-Agent": "opencode/0.1.0",
|
|
1099
|
+
session_id: `codex_${Date.now().toString(36)}`
|
|
1100
|
+
},
|
|
1101
|
+
body: JSON.stringify(codexRequest.body),
|
|
1102
|
+
// @ts-expect-error Node 18+ supports duplex for streaming
|
|
1103
|
+
duplex: "half",
|
|
1104
|
+
signal: c.req.raw.signal
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
return fetch(`${OPENAI_BASE}/v1/chat/completions`, {
|
|
1108
|
+
method: "POST",
|
|
1109
|
+
headers: {
|
|
1110
|
+
Authorization: `Bearer ${token}`,
|
|
1111
|
+
"content-type": "application/json"
|
|
1112
|
+
},
|
|
1113
|
+
body: JSON.stringify(upstreamReq),
|
|
1114
|
+
// @ts-expect-error Node 18+ supports duplex for streaming
|
|
1115
|
+
duplex: "half",
|
|
1116
|
+
signal: c.req.raw.signal
|
|
1117
|
+
});
|
|
1118
|
+
};
|
|
1119
|
+
let upstream;
|
|
1120
|
+
try {
|
|
1121
|
+
upstream = await fetchUpstreamWithRetry(() => doFetch(session.token));
|
|
1122
|
+
} catch (err) {
|
|
1123
|
+
if (abortError(err)) return new Response(null, { status: 499 });
|
|
1124
|
+
return c.json(
|
|
1125
|
+
anthropicGatewayError(`Upstream fetch failed: ${upstreamFetchErrorMessage(err)}`),
|
|
1126
|
+
502
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
if (upstream.status === 401 && session.accountId) {
|
|
1130
|
+
await upstream.body?.cancel().catch(() => {
|
|
1131
|
+
});
|
|
1132
|
+
const newToken = await refreshOAuthToken(session.accountId);
|
|
1133
|
+
if (newToken) {
|
|
1134
|
+
if (session.gatewayToken) updateSessionToken(session.gatewayToken, newToken);
|
|
1135
|
+
try {
|
|
1136
|
+
upstream = await fetchUpstreamWithRetry(() => doFetch(newToken));
|
|
1137
|
+
} catch (err) {
|
|
1138
|
+
if (abortError(err)) return new Response(null, { status: 499 });
|
|
1139
|
+
return c.json(
|
|
1140
|
+
anthropicGatewayError(`Upstream fetch failed: ${upstreamFetchErrorMessage(err)}`),
|
|
1141
|
+
502
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
if (!upstream.ok) {
|
|
1147
|
+
const errBody = await upstream.text();
|
|
1148
|
+
return new Response(errBody, {
|
|
1149
|
+
status: upstream.status,
|
|
1150
|
+
headers: { "content-type": upstream.headers.get("content-type") ?? "application/json" }
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
if (body.stream && isCodex && upstream.body) {
|
|
1154
|
+
const stream = translateCodexStreamToAnthropic(
|
|
1155
|
+
upstream.body,
|
|
1156
|
+
originalModel,
|
|
1157
|
+
messageId,
|
|
1158
|
+
estimatedInputTokens
|
|
1159
|
+
);
|
|
1160
|
+
return new Response(stream, {
|
|
1161
|
+
status: 200,
|
|
1162
|
+
headers: {
|
|
1163
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
1164
|
+
"cache-control": "no-cache",
|
|
1165
|
+
"x-accel-buffering": "no"
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
if (isCodex && upstream.body) {
|
|
1170
|
+
try {
|
|
1171
|
+
return c.json(
|
|
1172
|
+
await codexStreamToAnthropicResponse(
|
|
1173
|
+
upstream.body,
|
|
1174
|
+
originalModel,
|
|
1175
|
+
messageId,
|
|
1176
|
+
estimatedInputTokens
|
|
1177
|
+
)
|
|
1178
|
+
);
|
|
1179
|
+
} catch (err) {
|
|
1180
|
+
return c.json(anthropicGatewayError(err instanceof Error ? err.message : String(err)), 502);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
if (body.stream && upstream.body) {
|
|
1184
|
+
const stream = translateOpenAIStreamToAnthropic(
|
|
1185
|
+
upstream.body,
|
|
1186
|
+
originalModel,
|
|
1187
|
+
messageId,
|
|
1188
|
+
estimatedInputTokens
|
|
1189
|
+
);
|
|
1190
|
+
return new Response(stream, {
|
|
1191
|
+
status: 200,
|
|
1192
|
+
headers: {
|
|
1193
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
1194
|
+
"cache-control": "no-cache",
|
|
1195
|
+
"x-accel-buffering": "no"
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
const openaiResp = await upstream.json();
|
|
1200
|
+
return c.json(toAnthropicResponse(openaiResp, originalModel));
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// src/llm-gateway-runtime/proxy-anthropic.ts
|
|
1204
|
+
var ANTHROPIC_BASE = process.env.ANTHROPIC_UPSTREAM_URL ?? "https://api.anthropic.com";
|
|
1205
|
+
var PASSTHROUGH_REQUEST_HEADERS = [
|
|
1206
|
+
"anthropic-version",
|
|
1207
|
+
"anthropic-beta",
|
|
1208
|
+
"content-type"
|
|
1209
|
+
];
|
|
1210
|
+
var PASSTHROUGH_RESPONSE_HEADERS = [
|
|
1211
|
+
"content-type",
|
|
1212
|
+
"transfer-encoding",
|
|
1213
|
+
"retry-after"
|
|
1214
|
+
];
|
|
1215
|
+
function gatewayTokenFromRequest(c) {
|
|
1216
|
+
const auth = c.req.header("authorization") ?? "";
|
|
1217
|
+
if (auth.startsWith("Bearer gw-")) return auth.slice("Bearer ".length);
|
|
1218
|
+
const apiKey = c.req.header("x-api-key") ?? "";
|
|
1219
|
+
if (apiKey.startsWith("gw-")) return apiKey;
|
|
1220
|
+
return null;
|
|
1221
|
+
}
|
|
1222
|
+
async function handleMessages(c) {
|
|
1223
|
+
const gatewayToken = gatewayTokenFromRequest(c);
|
|
1224
|
+
if (!gatewayToken) return c.json({ error: "unauthorized" }, 403);
|
|
1225
|
+
const session = await lookupToken(gatewayToken);
|
|
1226
|
+
if (!session) return c.json({ error: "unauthorized" }, 403);
|
|
1227
|
+
if (session.provider === "openai" || session.provider === "codex") {
|
|
1228
|
+
return handleMessagesViaOpenAI(c, {
|
|
1229
|
+
token: session.token,
|
|
1230
|
+
gatewayToken: session.gatewayToken,
|
|
1231
|
+
accountId: session.accountId
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
const upstreamUrl = `${ANTHROPIC_BASE}/v1/messages`;
|
|
1235
|
+
const body = await c.req.raw.arrayBuffer();
|
|
1236
|
+
const requestHeaders = {
|
|
1237
|
+
"anthropic-version": c.req.header("anthropic-version") ?? "2023-06-01"
|
|
1238
|
+
};
|
|
1239
|
+
for (const h of PASSTHROUGH_REQUEST_HEADERS) {
|
|
1240
|
+
const v = c.req.header(h);
|
|
1241
|
+
if (v !== void 0) requestHeaders[h] = v;
|
|
1242
|
+
}
|
|
1243
|
+
requestHeaders["x-api-key"] = session.token;
|
|
1244
|
+
let upstream;
|
|
1245
|
+
try {
|
|
1246
|
+
upstream = await fetch(upstreamUrl, {
|
|
1247
|
+
method: "POST",
|
|
1248
|
+
headers: requestHeaders,
|
|
1249
|
+
body,
|
|
1250
|
+
// @ts-expect-error Node 18+ fetch supports duplex for streaming
|
|
1251
|
+
duplex: "half",
|
|
1252
|
+
signal: c.req.raw.signal
|
|
1253
|
+
});
|
|
1254
|
+
} catch (err) {
|
|
1255
|
+
if (err.name === "AbortError") {
|
|
1256
|
+
return new Response(null, { status: 499 });
|
|
1257
|
+
}
|
|
1258
|
+
throw err;
|
|
1259
|
+
}
|
|
1260
|
+
const responseHeaders = {};
|
|
1261
|
+
for (const h of PASSTHROUGH_RESPONSE_HEADERS) {
|
|
1262
|
+
const v = upstream.headers.get(h);
|
|
1263
|
+
if (v !== null) responseHeaders[h] = v;
|
|
1264
|
+
}
|
|
1265
|
+
return new Response(upstream.body, {
|
|
1266
|
+
status: upstream.status,
|
|
1267
|
+
headers: responseHeaders
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// src/llm-gateway-runtime/index.ts
|
|
1272
|
+
var app = new Hono();
|
|
1273
|
+
app.get("/health", (c) => c.json({ ok: true }));
|
|
1274
|
+
app.get("/healthz", (c) => c.json({ ok: true }));
|
|
1275
|
+
app.post("/v1/session", async (c) => {
|
|
1276
|
+
let body;
|
|
1277
|
+
try {
|
|
1278
|
+
body = await c.req.json();
|
|
1279
|
+
} catch {
|
|
1280
|
+
return c.json({ error: "invalid JSON" }, 400);
|
|
1281
|
+
}
|
|
1282
|
+
if (typeof body.sessionId !== "string" || !body.sessionId) {
|
|
1283
|
+
return c.json({ error: "sessionId (string) required" }, 400);
|
|
1284
|
+
}
|
|
1285
|
+
const result = await acquireSession(body.sessionId);
|
|
1286
|
+
return c.json(result, 201);
|
|
1287
|
+
});
|
|
1288
|
+
app.post("/v1/messages", handleMessages);
|
|
1289
|
+
var port = parseInt(process.env.PORT ?? "3001", 10);
|
|
1290
|
+
serve({ fetch: app.fetch, port }, () => {
|
|
1291
|
+
process.stdout.write(
|
|
1292
|
+
`[llm-gateway] listening on :${port} \u2014 ${sessionCount()} sessions in memory
|
|
1293
|
+
`
|
|
1294
|
+
);
|
|
1295
|
+
});
|