betahi-copilot-bridge 0.1.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/LICENSE +21 -0
- package/README.md +213 -0
- package/dist/main.js +2552 -0
- package/dist/main.js.map +1 -0
- package/package.json +55 -0
package/dist/main.js
ADDED
|
@@ -0,0 +1,2552 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { defineCommand, runMain } from "citty";
|
|
3
|
+
import consola from "consola";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { serve } from "@hono/node-server";
|
|
10
|
+
import { Hono } from "hono";
|
|
11
|
+
import { cors } from "hono/cors";
|
|
12
|
+
import { logger } from "hono/logger";
|
|
13
|
+
import { streamSSE } from "hono/streaming";
|
|
14
|
+
import { events } from "fetch-event-stream";
|
|
15
|
+
|
|
16
|
+
//#region src/lib/error.ts
|
|
17
|
+
var BridgeNotImplementedError = class extends Error {
|
|
18
|
+
constructor(message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "BridgeNotImplementedError";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var HTTPError = class extends Error {
|
|
24
|
+
response;
|
|
25
|
+
constructor(message, response) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "HTTPError";
|
|
28
|
+
this.response = response;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/lib/paths.ts
|
|
34
|
+
const APP_DIR = path.join(os.homedir(), ".local", "share", "copilot-bridge");
|
|
35
|
+
const GITHUB_TOKEN_PATH = path.join(APP_DIR, "github_token");
|
|
36
|
+
const PATHS = {
|
|
37
|
+
APP_DIR,
|
|
38
|
+
GITHUB_TOKEN_PATH
|
|
39
|
+
};
|
|
40
|
+
async function ensurePaths() {
|
|
41
|
+
await fs.mkdir(PATHS.APP_DIR, { recursive: true });
|
|
42
|
+
await ensureFile(PATHS.GITHUB_TOKEN_PATH);
|
|
43
|
+
}
|
|
44
|
+
async function ensureFile(filePath) {
|
|
45
|
+
try {
|
|
46
|
+
await fs.access(filePath, fs.constants.W_OK);
|
|
47
|
+
} catch {
|
|
48
|
+
await fs.writeFile(filePath, "");
|
|
49
|
+
await fs.chmod(filePath, 384);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/lib/state.ts
|
|
55
|
+
const runtimeState = {};
|
|
56
|
+
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/providers/copilot/client.ts
|
|
59
|
+
const COPILOT_VERSION$2 = "0.26.7";
|
|
60
|
+
const EDITOR_PLUGIN_VERSION$2 = `copilot-chat/${COPILOT_VERSION$2}`;
|
|
61
|
+
const USER_AGENT$2 = `GitHubCopilotChat/${COPILOT_VERSION$2}`;
|
|
62
|
+
const API_VERSION = "2025-04-01";
|
|
63
|
+
const getCopilotProviderContext = (config) => ({
|
|
64
|
+
baseUrl: config.copilotBaseUrl,
|
|
65
|
+
token: config.copilotToken,
|
|
66
|
+
vsCodeVersion: config.vsCodeVersion
|
|
67
|
+
});
|
|
68
|
+
const fetchCopilot = async (provider, path$1, init, options = {}) => {
|
|
69
|
+
if (!provider.token) throw new BridgeNotImplementedError("COPILOT_TOKEN is not configured for copilot-bridge.");
|
|
70
|
+
const headers = new Headers(init.headers);
|
|
71
|
+
headers.set("authorization", `Bearer ${provider.token}`);
|
|
72
|
+
headers.set("copilot-integration-id", "vscode-chat");
|
|
73
|
+
headers.set("editor-version", `vscode/${provider.vsCodeVersion}`);
|
|
74
|
+
headers.set("editor-plugin-version", EDITOR_PLUGIN_VERSION$2);
|
|
75
|
+
headers.set("user-agent", USER_AGENT$2);
|
|
76
|
+
headers.set("openai-intent", "conversation-panel");
|
|
77
|
+
headers.set("x-github-api-version", API_VERSION);
|
|
78
|
+
headers.set("x-request-id", randomUUID());
|
|
79
|
+
headers.set("x-vscode-user-agent-library-version", "electron-fetch");
|
|
80
|
+
if (options.vision) headers.set("copilot-vision-request", "true");
|
|
81
|
+
if (options.initiator) headers.set("x-initiator", options.initiator);
|
|
82
|
+
if (!headers.has("content-type") && init.body !== void 0) headers.set("content-type", "application/json");
|
|
83
|
+
if (!headers.has("accept")) headers.set("accept", "application/json");
|
|
84
|
+
return fetch(`${provider.baseUrl}${path$1}`, {
|
|
85
|
+
...init,
|
|
86
|
+
headers
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/providers/copilot/get-models.ts
|
|
92
|
+
const getModels = async (config) => {
|
|
93
|
+
const response = await fetchCopilot(getCopilotProviderContext(config), "/models", {
|
|
94
|
+
method: "GET",
|
|
95
|
+
headers: { accept: "application/json" }
|
|
96
|
+
});
|
|
97
|
+
if (!response.ok) throw new HTTPError("Failed to get models", response);
|
|
98
|
+
return await response.json();
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/lib/auth.ts
|
|
103
|
+
const COPILOT_VERSION$1 = "0.26.7";
|
|
104
|
+
const EDITOR_PLUGIN_VERSION$1 = `copilot-chat/${COPILOT_VERSION$1}`;
|
|
105
|
+
const USER_AGENT$1 = `GitHubCopilotChat/${COPILOT_VERSION$1}`;
|
|
106
|
+
const GITHUB_API_VERSION$1 = "2022-11-28";
|
|
107
|
+
const GITHUB_API_BASE_URL$1 = "https://api.github.com";
|
|
108
|
+
const GITHUB_BASE_URL = "https://github.com";
|
|
109
|
+
const GITHUB_CLIENT_ID = "Iv1.b507a08c87ecfe98";
|
|
110
|
+
const GITHUB_APP_SCOPES = ["read:user"].join(" ");
|
|
111
|
+
const standardHeaders = () => ({
|
|
112
|
+
accept: "application/json",
|
|
113
|
+
"content-type": "application/json"
|
|
114
|
+
});
|
|
115
|
+
const sleep$1 = (ms) => new Promise((resolve) => {
|
|
116
|
+
setTimeout(resolve, ms);
|
|
117
|
+
});
|
|
118
|
+
const readGitHubToken$1 = async () => {
|
|
119
|
+
return (await fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8")).trim();
|
|
120
|
+
};
|
|
121
|
+
const writeGitHubToken = (token) => fs.writeFile(PATHS.GITHUB_TOKEN_PATH, `${token.trim()}\n`, { mode: 384 });
|
|
122
|
+
const githubHeaders = (githubToken, vsCodeVersion) => ({
|
|
123
|
+
...standardHeaders(),
|
|
124
|
+
authorization: `token ${githubToken}`,
|
|
125
|
+
"editor-plugin-version": EDITOR_PLUGIN_VERSION$1,
|
|
126
|
+
"editor-version": `vscode/${vsCodeVersion}`,
|
|
127
|
+
"user-agent": USER_AGENT$1,
|
|
128
|
+
"x-github-api-version": GITHUB_API_VERSION$1,
|
|
129
|
+
"x-vscode-user-agent-library-version": "electron-fetch"
|
|
130
|
+
});
|
|
131
|
+
const getDeviceCode = async () => {
|
|
132
|
+
const response = await fetch(`${GITHUB_BASE_URL}/login/device/code`, {
|
|
133
|
+
method: "POST",
|
|
134
|
+
headers: standardHeaders(),
|
|
135
|
+
body: JSON.stringify({
|
|
136
|
+
client_id: GITHUB_CLIENT_ID,
|
|
137
|
+
scope: GITHUB_APP_SCOPES
|
|
138
|
+
})
|
|
139
|
+
});
|
|
140
|
+
if (!response.ok) throw new HTTPError("Failed to get device code", response);
|
|
141
|
+
return await response.json();
|
|
142
|
+
};
|
|
143
|
+
const pollAccessToken = async (deviceCode) => {
|
|
144
|
+
const sleepDuration = (deviceCode.interval + 1) * 1e3;
|
|
145
|
+
while (true) {
|
|
146
|
+
const response = await fetch(`${GITHUB_BASE_URL}/login/oauth/access_token`, {
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers: standardHeaders(),
|
|
149
|
+
body: JSON.stringify({
|
|
150
|
+
client_id: GITHUB_CLIENT_ID,
|
|
151
|
+
device_code: deviceCode.device_code,
|
|
152
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
153
|
+
})
|
|
154
|
+
});
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
await sleep$1(sleepDuration);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const json = await response.json();
|
|
160
|
+
if (json.access_token) return json.access_token;
|
|
161
|
+
await sleep$1(sleepDuration);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
const getGitHubUser = async (githubToken, vsCodeVersion) => {
|
|
165
|
+
const response = await fetch(`${GITHUB_API_BASE_URL$1}/user`, { headers: githubHeaders(githubToken, vsCodeVersion) });
|
|
166
|
+
if (!response.ok) throw new HTTPError("Failed to get GitHub user", response);
|
|
167
|
+
return await response.json();
|
|
168
|
+
};
|
|
169
|
+
const getCopilotToken = async (githubToken, vsCodeVersion) => {
|
|
170
|
+
const response = await fetch(`${GITHUB_API_BASE_URL$1}/copilot_internal/v2/token`, { headers: githubHeaders(githubToken, vsCodeVersion) });
|
|
171
|
+
if (!response.ok) throw new HTTPError("Failed to get Copilot token", response);
|
|
172
|
+
return await response.json();
|
|
173
|
+
};
|
|
174
|
+
const ensureGitHubToken = async (config, options = {}) => {
|
|
175
|
+
await ensurePaths();
|
|
176
|
+
const existingToken = options.force ? "" : await readGitHubToken$1();
|
|
177
|
+
if (existingToken) return existingToken;
|
|
178
|
+
const deviceCode = await getDeviceCode();
|
|
179
|
+
consola.info(`Open ${deviceCode.verification_uri} and enter code ${deviceCode.user_code}`);
|
|
180
|
+
const githubToken = await pollAccessToken(deviceCode);
|
|
181
|
+
await writeGitHubToken(githubToken);
|
|
182
|
+
if (options.showToken) consola.info("GitHub token:", githubToken);
|
|
183
|
+
const user = await getGitHubUser(githubToken, config.vsCodeVersion);
|
|
184
|
+
consola.success(`Logged in as ${user.login}`);
|
|
185
|
+
return githubToken;
|
|
186
|
+
};
|
|
187
|
+
const setupBridgeAuth = async (config, options = {}) => {
|
|
188
|
+
if (config.copilotToken) {
|
|
189
|
+
if (options.showToken) consola.info("Using COPILOT_TOKEN from environment");
|
|
190
|
+
await loadModels(config);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const githubToken = await ensureGitHubToken(config, options);
|
|
194
|
+
const applyCopilotToken = async () => {
|
|
195
|
+
const { refresh_in, token } = await getCopilotToken(githubToken, config.vsCodeVersion);
|
|
196
|
+
config.copilotToken = token;
|
|
197
|
+
if (options.showToken) consola.info("Copilot token:", token);
|
|
198
|
+
return refresh_in;
|
|
199
|
+
};
|
|
200
|
+
const refreshIn = await applyCopilotToken();
|
|
201
|
+
const refreshInterval = Math.max(refreshIn - 60, 60) * 1e3;
|
|
202
|
+
setInterval(async () => {
|
|
203
|
+
try {
|
|
204
|
+
await applyCopilotToken();
|
|
205
|
+
consola.debug("Refreshed Copilot token");
|
|
206
|
+
} catch (error) {
|
|
207
|
+
consola.error("Failed to refresh Copilot token:", error);
|
|
208
|
+
}
|
|
209
|
+
}, refreshInterval);
|
|
210
|
+
await loadModels(config);
|
|
211
|
+
};
|
|
212
|
+
const loadModels = async (config) => {
|
|
213
|
+
try {
|
|
214
|
+
runtimeState.models = await getModels(config);
|
|
215
|
+
consola.success(`Loaded ${runtimeState.models.data.length} Copilot models`);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
consola.warn("Failed to load Copilot models:", error);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
//#endregion
|
|
222
|
+
//#region src/lib/config.ts
|
|
223
|
+
const accountTypeSchema = z.enum([
|
|
224
|
+
"individual",
|
|
225
|
+
"business",
|
|
226
|
+
"enterprise"
|
|
227
|
+
]);
|
|
228
|
+
const inputSchema = z.object({
|
|
229
|
+
host: z.string().min(1),
|
|
230
|
+
port: z.number().int().min(1).max(65535)
|
|
231
|
+
});
|
|
232
|
+
const readBridgeConfig = (input) => {
|
|
233
|
+
const parsedInput = inputSchema.parse(input);
|
|
234
|
+
const accountType = accountTypeSchema.parse(process.env.COPILOT_ACCOUNT_TYPE ?? "individual");
|
|
235
|
+
const copilotBaseUrl = process.env.COPILOT_BASE_URL ?? (accountType === "individual" ? "https://api.githubcopilot.com" : `https://api.${accountType}.githubcopilot.com`);
|
|
236
|
+
return {
|
|
237
|
+
host: parsedInput.host,
|
|
238
|
+
port: parsedInput.port,
|
|
239
|
+
accountType,
|
|
240
|
+
copilotBaseUrl,
|
|
241
|
+
copilotToken: process.env.COPILOT_TOKEN,
|
|
242
|
+
vsCodeVersion: process.env.COPILOT_VSCODE_VERSION ?? "1.99.3"
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
//#endregion
|
|
247
|
+
//#region src/auth.ts
|
|
248
|
+
const auth = defineCommand({
|
|
249
|
+
meta: {
|
|
250
|
+
name: "auth",
|
|
251
|
+
description: "Run GitHub device auth and cache credentials for copilot-bridge."
|
|
252
|
+
},
|
|
253
|
+
args: {
|
|
254
|
+
host: {
|
|
255
|
+
type: "string",
|
|
256
|
+
default: "127.0.0.1",
|
|
257
|
+
description: "Host used for config initialization."
|
|
258
|
+
},
|
|
259
|
+
port: {
|
|
260
|
+
type: "string",
|
|
261
|
+
default: "4142",
|
|
262
|
+
description: "Port used for config initialization."
|
|
263
|
+
},
|
|
264
|
+
"show-token": {
|
|
265
|
+
type: "boolean",
|
|
266
|
+
default: false,
|
|
267
|
+
description: "Print GitHub and Copilot tokens during auth."
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
async run({ args }) {
|
|
271
|
+
const config = readBridgeConfig({
|
|
272
|
+
host: String(args.host),
|
|
273
|
+
port: Number(args.port)
|
|
274
|
+
});
|
|
275
|
+
config.copilotToken = void 0;
|
|
276
|
+
await setupBridgeAuth(config, {
|
|
277
|
+
force: true,
|
|
278
|
+
showToken: args["show-token"]
|
|
279
|
+
});
|
|
280
|
+
consola.success("copilot-bridge auth completed");
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
//#endregion
|
|
285
|
+
//#region src/lib/codex-config.ts
|
|
286
|
+
const BEGIN_MARK = "# >>> copilot-bridge managed block — auto-generated, do not edit between markers >>>";
|
|
287
|
+
const END_MARK = "# <<< copilot-bridge managed block — edits outside this block are preserved <<<";
|
|
288
|
+
const LEGACY_BEGIN_MARK = "# >>> copilot-bridge managed (do not edit) >>>";
|
|
289
|
+
const LEGACY_END_MARK = "# <<< copilot-bridge managed (do not edit) <<<";
|
|
290
|
+
function tomlEscape(value) {
|
|
291
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
292
|
+
}
|
|
293
|
+
function buildManagedBlock(input) {
|
|
294
|
+
const { baseUrl, settings } = input;
|
|
295
|
+
const lines = [];
|
|
296
|
+
lines.push(BEGIN_MARK);
|
|
297
|
+
if (settings.setAsDefault) lines.push(`model_provider = "${tomlEscape(settings.providerId)}"`);
|
|
298
|
+
lines.push("");
|
|
299
|
+
lines.push(`[model_providers.${settings.providerId}]`);
|
|
300
|
+
lines.push(`name = "${tomlEscape(settings.providerName)}"`);
|
|
301
|
+
lines.push(`base_url = "${tomlEscape(baseUrl)}"`);
|
|
302
|
+
lines.push(`wire_api = "responses"`);
|
|
303
|
+
lines.push(`prefer_websockets = false`);
|
|
304
|
+
lines.push(`requires_openai_auth = false`);
|
|
305
|
+
lines.push(END_MARK);
|
|
306
|
+
return lines.join("\n");
|
|
307
|
+
}
|
|
308
|
+
function stripManagedBlock(content) {
|
|
309
|
+
let next = content;
|
|
310
|
+
for (const [begin, end] of [[BEGIN_MARK, END_MARK], [LEGACY_BEGIN_MARK, LEGACY_END_MARK]]) while (true) {
|
|
311
|
+
const beginIdx = next.indexOf(begin);
|
|
312
|
+
if (beginIdx === -1) break;
|
|
313
|
+
const endIdx = next.indexOf(end, beginIdx);
|
|
314
|
+
if (endIdx === -1) break;
|
|
315
|
+
const before = next.slice(0, beginIdx).replace(/\n*$/, "");
|
|
316
|
+
const after = next.slice(endIdx + end.length).replace(/^\n+/, "");
|
|
317
|
+
if (before.length === 0) next = after;
|
|
318
|
+
else if (after.length === 0) next = `${before}\n`;
|
|
319
|
+
else next = `${before}\n\n${after}`;
|
|
320
|
+
}
|
|
321
|
+
return next;
|
|
322
|
+
}
|
|
323
|
+
function splitTopSection(content) {
|
|
324
|
+
const lines = content.split("\n");
|
|
325
|
+
let cut = lines.length;
|
|
326
|
+
for (let i = 0; i < lines.length; i++) if (/^\s*\[/.test(lines[i])) {
|
|
327
|
+
cut = i;
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
top: lines.slice(0, cut).join("\n"),
|
|
332
|
+
rest: lines.slice(cut).join("\n")
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
const scalarRegex = (key) => new RegExp(`^\\s*${key}\\s*=\\s*"((?:[^"\\\\]|\\\\.)*)"\\s*$`, "m");
|
|
336
|
+
function readCodexUserConfig(content) {
|
|
337
|
+
const { top } = splitTopSection(stripManagedBlock(content));
|
|
338
|
+
const out = {};
|
|
339
|
+
const m = top.match(scalarRegex("model"));
|
|
340
|
+
if (m) out.model = m[1];
|
|
341
|
+
const e = top.match(scalarRegex("model_reasoning_effort"));
|
|
342
|
+
if (e) out.modelReasoningEffort = e[1];
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
345
|
+
function setTopScalar(topSection, key, value) {
|
|
346
|
+
const re = scalarRegex(key);
|
|
347
|
+
if (value === void 0) return topSection;
|
|
348
|
+
const line = `${key} = "${tomlEscape(value)}"`;
|
|
349
|
+
if (re.test(topSection)) return topSection.replace(re, line);
|
|
350
|
+
if (topSection.length === 0) return `${line}\n`;
|
|
351
|
+
return `${line}\n${topSection.startsWith("\n") ? "" : ""}${topSection}`;
|
|
352
|
+
}
|
|
353
|
+
function applyUserScalars(content, input) {
|
|
354
|
+
const { top, rest } = splitTopSection(content);
|
|
355
|
+
let nextTop = top;
|
|
356
|
+
nextTop = setTopScalar(nextTop, "model", input.model);
|
|
357
|
+
nextTop = setTopScalar(nextTop, "model_reasoning_effort", input.modelReasoningEffort);
|
|
358
|
+
if (nextTop === top) return content;
|
|
359
|
+
if (rest.length === 0) return nextTop.endsWith("\n") ? nextTop : `${nextTop}\n`;
|
|
360
|
+
const sep = nextTop.endsWith("\n") ? "" : "\n";
|
|
361
|
+
return `${nextTop}${sep}${rest}`;
|
|
362
|
+
}
|
|
363
|
+
async function applyCodexConfig(input) {
|
|
364
|
+
const { configPath } = input;
|
|
365
|
+
let existing = "";
|
|
366
|
+
let created = false;
|
|
367
|
+
try {
|
|
368
|
+
existing = await fs.readFile(configPath, "utf8");
|
|
369
|
+
} catch {
|
|
370
|
+
created = true;
|
|
371
|
+
}
|
|
372
|
+
let stripped = stripManagedBlock(existing);
|
|
373
|
+
stripped = applyUserScalars(stripped, input);
|
|
374
|
+
const block = buildManagedBlock(input);
|
|
375
|
+
const trimmed = stripped.replace(/\n+$/, "");
|
|
376
|
+
const next = trimmed.length === 0 ? `${block}\n` : `${trimmed}\n\n${block}\n`;
|
|
377
|
+
if (next === existing) return {
|
|
378
|
+
configPath,
|
|
379
|
+
changed: false,
|
|
380
|
+
created: false
|
|
381
|
+
};
|
|
382
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
383
|
+
await fs.writeFile(configPath, next);
|
|
384
|
+
return {
|
|
385
|
+
configPath,
|
|
386
|
+
changed: true,
|
|
387
|
+
created
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
async function readCodexUserConfigFromDisk(configPath) {
|
|
391
|
+
try {
|
|
392
|
+
return readCodexUserConfig(await fs.readFile(configPath, "utf8"));
|
|
393
|
+
} catch {
|
|
394
|
+
return {};
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
//#endregion
|
|
399
|
+
//#region src/lib/model-capabilities.ts
|
|
400
|
+
const MODEL_CAPABILITIES = [
|
|
401
|
+
{
|
|
402
|
+
id: "gpt-5.5",
|
|
403
|
+
reasoning: {
|
|
404
|
+
supported: [
|
|
405
|
+
"none",
|
|
406
|
+
"low",
|
|
407
|
+
"medium",
|
|
408
|
+
"high",
|
|
409
|
+
"xhigh"
|
|
410
|
+
],
|
|
411
|
+
default: "medium"
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
id: "gpt-5.4",
|
|
416
|
+
reasoning: {
|
|
417
|
+
supported: [
|
|
418
|
+
"low",
|
|
419
|
+
"medium",
|
|
420
|
+
"high",
|
|
421
|
+
"xhigh"
|
|
422
|
+
],
|
|
423
|
+
default: "medium"
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
id: "gpt-5.4-mini",
|
|
428
|
+
reasoning: {
|
|
429
|
+
supported: [
|
|
430
|
+
"none",
|
|
431
|
+
"low",
|
|
432
|
+
"medium"
|
|
433
|
+
],
|
|
434
|
+
default: "medium"
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
id: "gpt-5.3-codex",
|
|
439
|
+
reasoning: {
|
|
440
|
+
supported: [
|
|
441
|
+
"low",
|
|
442
|
+
"medium",
|
|
443
|
+
"high",
|
|
444
|
+
"xhigh"
|
|
445
|
+
],
|
|
446
|
+
default: "medium"
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
id: "gpt-5.2",
|
|
451
|
+
reasoning: {
|
|
452
|
+
supported: [
|
|
453
|
+
"low",
|
|
454
|
+
"medium",
|
|
455
|
+
"high",
|
|
456
|
+
"xhigh"
|
|
457
|
+
],
|
|
458
|
+
default: "medium"
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
id: "gpt-5.2-codex",
|
|
463
|
+
reasoning: {
|
|
464
|
+
supported: [
|
|
465
|
+
"low",
|
|
466
|
+
"medium",
|
|
467
|
+
"high",
|
|
468
|
+
"xhigh"
|
|
469
|
+
],
|
|
470
|
+
default: "medium"
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
id: "gpt-5-mini",
|
|
475
|
+
reasoning: {
|
|
476
|
+
supported: [
|
|
477
|
+
"low",
|
|
478
|
+
"medium",
|
|
479
|
+
"high"
|
|
480
|
+
],
|
|
481
|
+
default: "medium"
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
id: "claude-opus-4.7",
|
|
486
|
+
fallback: "chat-completions",
|
|
487
|
+
reasoningField: "output_config.effort",
|
|
488
|
+
reasoning: {
|
|
489
|
+
supported: [
|
|
490
|
+
"low",
|
|
491
|
+
"medium",
|
|
492
|
+
"high",
|
|
493
|
+
"xhigh",
|
|
494
|
+
"max"
|
|
495
|
+
],
|
|
496
|
+
default: "medium"
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
id: "claude-opus-4.6",
|
|
501
|
+
fallback: "chat-completions",
|
|
502
|
+
reasoning: {
|
|
503
|
+
supported: [
|
|
504
|
+
"low",
|
|
505
|
+
"medium",
|
|
506
|
+
"high"
|
|
507
|
+
],
|
|
508
|
+
default: "medium"
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
id: "claude-opus-4.6-1m",
|
|
513
|
+
fallback: "chat-completions",
|
|
514
|
+
reasoning: {
|
|
515
|
+
supported: [
|
|
516
|
+
"low",
|
|
517
|
+
"medium",
|
|
518
|
+
"high"
|
|
519
|
+
],
|
|
520
|
+
default: "medium"
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
id: "claude-opus-4.5",
|
|
525
|
+
fallback: "chat-completions"
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
id: "claude-sonnet-4.6",
|
|
529
|
+
fallback: "chat-completions",
|
|
530
|
+
reasoning: {
|
|
531
|
+
supported: [
|
|
532
|
+
"low",
|
|
533
|
+
"medium",
|
|
534
|
+
"high"
|
|
535
|
+
],
|
|
536
|
+
default: "medium"
|
|
537
|
+
}
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
id: "claude-sonnet-4.5",
|
|
541
|
+
fallback: "chat-completions"
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
id: "claude-sonnet-4",
|
|
545
|
+
fallback: "chat-completions"
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
id: "claude-haiku-4.5",
|
|
549
|
+
fallback: "chat-completions"
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
id: "gemini-3.1-pro-preview",
|
|
553
|
+
fallback: "chat-completions",
|
|
554
|
+
aliases: ["gemini-3.1-pro"]
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
id: "gemini-3-flash-preview",
|
|
558
|
+
fallback: "chat-completions",
|
|
559
|
+
aliases: ["gemini-3-flash"]
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
id: "gemini-2.5-pro",
|
|
563
|
+
fallback: "chat-completions"
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
id: "gpt-4.1",
|
|
567
|
+
fallback: "chat-completions"
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
id: "gpt-4o",
|
|
571
|
+
fallback: "chat-completions"
|
|
572
|
+
}
|
|
573
|
+
];
|
|
574
|
+
const CAPABILITY_BY_ID = new Map(MODEL_CAPABILITIES.flatMap((m) => {
|
|
575
|
+
const entries = [[m.id, m]];
|
|
576
|
+
for (const alias of m.aliases ?? []) entries.push([alias, m]);
|
|
577
|
+
return entries;
|
|
578
|
+
}));
|
|
579
|
+
const resolveModelId = (modelId) => CAPABILITY_BY_ID.get(modelId)?.id ?? modelId;
|
|
580
|
+
const getModelCapability = (modelId) => CAPABILITY_BY_ID.get(modelId);
|
|
581
|
+
const isReasoningEffort = (value) => value === "none" || value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "max";
|
|
582
|
+
const clampReasoningEffort = (modelId, requested) => {
|
|
583
|
+
const capability = getModelCapability(modelId);
|
|
584
|
+
if (!capability?.reasoning) return void 0;
|
|
585
|
+
const { supported, default: defaultEffort } = capability.reasoning;
|
|
586
|
+
if (requested === void 0 || requested === null) return {
|
|
587
|
+
effort: defaultEffort,
|
|
588
|
+
changed: false
|
|
589
|
+
};
|
|
590
|
+
if (!isReasoningEffort(requested)) return {
|
|
591
|
+
effort: defaultEffort,
|
|
592
|
+
changed: true,
|
|
593
|
+
reason: "unsupported-effort"
|
|
594
|
+
};
|
|
595
|
+
if (supported.includes(requested)) return {
|
|
596
|
+
effort: requested,
|
|
597
|
+
changed: false
|
|
598
|
+
};
|
|
599
|
+
const order = [
|
|
600
|
+
"none",
|
|
601
|
+
"low",
|
|
602
|
+
"medium",
|
|
603
|
+
"high",
|
|
604
|
+
"xhigh",
|
|
605
|
+
"max"
|
|
606
|
+
];
|
|
607
|
+
const reqIdx = order.indexOf(requested);
|
|
608
|
+
const supportedSorted = [...supported].sort((a, b) => order.indexOf(a) - order.indexOf(b));
|
|
609
|
+
const lowest = supportedSorted[0];
|
|
610
|
+
const highest = supportedSorted[supportedSorted.length - 1];
|
|
611
|
+
return {
|
|
612
|
+
effort: reqIdx > order.indexOf(highest) ? highest : reqIdx < order.indexOf(lowest) ? lowest : supportedSorted.find((e) => order.indexOf(e) >= reqIdx) ?? defaultEffort,
|
|
613
|
+
changed: true,
|
|
614
|
+
reason: "unsupported-effort"
|
|
615
|
+
};
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
//#endregion
|
|
619
|
+
//#region src/lib/settings.ts
|
|
620
|
+
const SETTINGS_DIR = path.join(os.homedir(), ".config", "copilot-bridge");
|
|
621
|
+
const SETTINGS_FILE = path.join(SETTINGS_DIR, "settings.json");
|
|
622
|
+
const DEFAULT_SETTINGS = {
|
|
623
|
+
host: "127.0.0.1",
|
|
624
|
+
port: 4142,
|
|
625
|
+
codex: {
|
|
626
|
+
enabled: true,
|
|
627
|
+
providerId: "bridge",
|
|
628
|
+
providerName: "Copilot Bridge",
|
|
629
|
+
setAsDefault: true,
|
|
630
|
+
configPath: path.join(os.homedir(), ".codex", "config.toml")
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
function deepMerge(base, override) {
|
|
634
|
+
if (typeof base !== "object" || base === null || Array.isArray(base) || typeof override !== "object" || override === null || Array.isArray(override)) return override ?? base;
|
|
635
|
+
const out = { ...base };
|
|
636
|
+
for (const [key, value] of Object.entries(override)) out[key] = deepMerge(base[key], value);
|
|
637
|
+
return out;
|
|
638
|
+
}
|
|
639
|
+
async function loadSettings() {
|
|
640
|
+
try {
|
|
641
|
+
const raw = await fs.readFile(SETTINGS_FILE, "utf8");
|
|
642
|
+
return deepMerge(DEFAULT_SETTINGS, JSON.parse(raw));
|
|
643
|
+
} catch {
|
|
644
|
+
return {
|
|
645
|
+
...DEFAULT_SETTINGS,
|
|
646
|
+
codex: { ...DEFAULT_SETTINGS.codex }
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
async function ensureSettingsFile() {
|
|
651
|
+
const settings = await loadSettings();
|
|
652
|
+
try {
|
|
653
|
+
await fs.access(SETTINGS_FILE);
|
|
654
|
+
} catch {
|
|
655
|
+
await fs.mkdir(SETTINGS_DIR, { recursive: true });
|
|
656
|
+
await fs.writeFile(SETTINGS_FILE, `${JSON.stringify(settings, null, 2)}\n`);
|
|
657
|
+
}
|
|
658
|
+
return settings;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
//#endregion
|
|
662
|
+
//#region src/lib/rate-limit.ts
|
|
663
|
+
const sleep = (ms) => new Promise((resolve) => {
|
|
664
|
+
setTimeout(resolve, ms);
|
|
665
|
+
});
|
|
666
|
+
/**
|
|
667
|
+
* Enforces the configured rate limit window between mutating upstream calls.
|
|
668
|
+
*
|
|
669
|
+
* - Resolves silently if no rate limit is configured or enough time has passed.
|
|
670
|
+
* - Throws a Response (HTTP 429) when the window has not elapsed and `wait` is
|
|
671
|
+
* disabled, so route handlers can `c.json` it directly.
|
|
672
|
+
* - When `wait` is enabled, sleeps until the window elapses, then resolves.
|
|
673
|
+
*/
|
|
674
|
+
const checkRateLimit = async () => {
|
|
675
|
+
const { rateLimitSeconds, rateLimitWait } = runtimeState;
|
|
676
|
+
if (rateLimitSeconds === void 0) return;
|
|
677
|
+
const now = Date.now();
|
|
678
|
+
if (!runtimeState.lastRequestTimestamp) {
|
|
679
|
+
runtimeState.lastRequestTimestamp = now;
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const elapsed = (now - runtimeState.lastRequestTimestamp) / 1e3;
|
|
683
|
+
if (elapsed > rateLimitSeconds) {
|
|
684
|
+
runtimeState.lastRequestTimestamp = now;
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const waitSeconds = Math.ceil(rateLimitSeconds - elapsed);
|
|
688
|
+
if (!rateLimitWait) {
|
|
689
|
+
consola.warn(`Rate limit exceeded. Need to wait ${waitSeconds} more seconds.`);
|
|
690
|
+
throw new RateLimitError(waitSeconds);
|
|
691
|
+
}
|
|
692
|
+
consola.warn(`Rate limit reached. Waiting ${waitSeconds}s before proceeding...`);
|
|
693
|
+
await sleep(waitSeconds * 1e3);
|
|
694
|
+
runtimeState.lastRequestTimestamp = Date.now();
|
|
695
|
+
};
|
|
696
|
+
var RateLimitError = class extends Error {
|
|
697
|
+
waitSeconds;
|
|
698
|
+
constructor(waitSeconds) {
|
|
699
|
+
super(`Rate limit exceeded. Try again in ${waitSeconds}s.`);
|
|
700
|
+
this.name = "RateLimitError";
|
|
701
|
+
this.waitSeconds = waitSeconds;
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
//#endregion
|
|
706
|
+
//#region src/lib/claude-settings.ts
|
|
707
|
+
const getClaudeSettingsPaths = () => {
|
|
708
|
+
const cwd = process.cwd();
|
|
709
|
+
const home = process.env.HOME ?? os.homedir();
|
|
710
|
+
return [
|
|
711
|
+
path.join(home, ".claude", "settings.json"),
|
|
712
|
+
path.join(cwd, ".claude", "settings.json"),
|
|
713
|
+
path.join(cwd, ".claude", "settings.local.json")
|
|
714
|
+
];
|
|
715
|
+
};
|
|
716
|
+
const readClaudeSettingsFile = async (filePath) => {
|
|
717
|
+
try {
|
|
718
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
719
|
+
return JSON.parse(content);
|
|
720
|
+
} catch (error) {
|
|
721
|
+
if (error.code === "ENOENT") return;
|
|
722
|
+
consola.warn(`Failed to read Claude settings from ${filePath}:`, error);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
const getClaudeSettingsEnv = async () => {
|
|
727
|
+
const merged = {};
|
|
728
|
+
for (const filePath of getClaudeSettingsPaths()) {
|
|
729
|
+
const settings = await readClaudeSettingsFile(filePath);
|
|
730
|
+
if (!settings?.env) continue;
|
|
731
|
+
for (const [key, value] of Object.entries(settings.env)) if (typeof value === "string") merged[key] = value;
|
|
732
|
+
}
|
|
733
|
+
return merged;
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
//#endregion
|
|
737
|
+
//#region src/services/copilot/responses.ts
|
|
738
|
+
const RESPONSES_ONLY_MODEL_PATTERN = /^(?:gpt-5\.3-codex|gpt-5\.4-mini)(?:-|$)/i;
|
|
739
|
+
function shouldUseResponsesApiForModel(model) {
|
|
740
|
+
return RESPONSES_ONLY_MODEL_PATTERN.test(model);
|
|
741
|
+
}
|
|
742
|
+
function buildResponsesRequestPayload(payload, reasoningEffort) {
|
|
743
|
+
return {
|
|
744
|
+
model: payload.model,
|
|
745
|
+
input: translateMessagesToResponsesInput(payload.messages),
|
|
746
|
+
stream: payload.stream,
|
|
747
|
+
max_output_tokens: payload.max_tokens,
|
|
748
|
+
temperature: payload.temperature,
|
|
749
|
+
top_p: payload.top_p,
|
|
750
|
+
user: sanitizeUserIdentifier(payload.user),
|
|
751
|
+
tools: translateTools(payload.tools),
|
|
752
|
+
tool_choice: translateToolChoice(payload.tool_choice),
|
|
753
|
+
reasoning: reasoningEffort ? { effort: reasoningEffort } : void 0,
|
|
754
|
+
text: payload.response_format?.type === "json_object" ? { format: { type: "json_object" } } : void 0
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
function translateResponsesToChatCompletion(response) {
|
|
758
|
+
const assistantMessages = response.output.filter((item) => item.type === "message");
|
|
759
|
+
const functionCalls = response.output.filter((item) => item.type === "function_call");
|
|
760
|
+
const content = assistantMessages.flatMap((item) => item.content).map((part) => part.text).join("");
|
|
761
|
+
return {
|
|
762
|
+
id: response.id,
|
|
763
|
+
object: "chat.completion",
|
|
764
|
+
created: response.created_at,
|
|
765
|
+
model: response.model,
|
|
766
|
+
choices: [{
|
|
767
|
+
index: 0,
|
|
768
|
+
message: {
|
|
769
|
+
role: "assistant",
|
|
770
|
+
content: content || null,
|
|
771
|
+
...functionCalls.length > 0 && { tool_calls: functionCalls.map((toolCall) => ({
|
|
772
|
+
id: toolCall.call_id,
|
|
773
|
+
type: "function",
|
|
774
|
+
function: {
|
|
775
|
+
name: toolCall.name,
|
|
776
|
+
arguments: toolCall.arguments
|
|
777
|
+
}
|
|
778
|
+
})) }
|
|
779
|
+
},
|
|
780
|
+
logprobs: null,
|
|
781
|
+
finish_reason: getFinishReason(response, functionCalls.length > 0)
|
|
782
|
+
}],
|
|
783
|
+
usage: translateUsage(response.usage)
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
async function* translateResponsesStreamToChatCompletionStream(responseStream) {
|
|
787
|
+
const state = {
|
|
788
|
+
responseId: randomUUID(),
|
|
789
|
+
createdAt: Math.floor(Date.now() / 1e3),
|
|
790
|
+
model: "",
|
|
791
|
+
started: false
|
|
792
|
+
};
|
|
793
|
+
for await (const rawEvent of responseStream) {
|
|
794
|
+
if (!rawEvent.data || rawEvent.data === "[DONE]") continue;
|
|
795
|
+
const event = JSON.parse(rawEvent.data);
|
|
796
|
+
if (event.response) {
|
|
797
|
+
state.responseId = event.response.id;
|
|
798
|
+
state.createdAt = event.response.created_at;
|
|
799
|
+
state.model = event.response.model;
|
|
800
|
+
}
|
|
801
|
+
if (event.type === "response.output_item.added") {
|
|
802
|
+
const chunk = handleOutputItemAdded(state, event);
|
|
803
|
+
if (chunk) yield chunk;
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
if (event.type === "response.output_text.delta") {
|
|
807
|
+
yield createRoleChunk(state);
|
|
808
|
+
yield createChunk(state, { delta: { content: event.delta } });
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
if (event.type === "response.function_call_arguments.delta") {
|
|
812
|
+
if (event.output_index === void 0) continue;
|
|
813
|
+
yield createRoleChunk(state);
|
|
814
|
+
yield createChunk(state, { delta: { tool_calls: [{
|
|
815
|
+
index: event.output_index,
|
|
816
|
+
type: "function",
|
|
817
|
+
function: { arguments: event.delta ?? "" }
|
|
818
|
+
}] } });
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
if (event.type === "response.completed") {
|
|
822
|
+
if (!event.response) continue;
|
|
823
|
+
yield createChunk({
|
|
824
|
+
...state,
|
|
825
|
+
responseId: event.response.id,
|
|
826
|
+
createdAt: event.response.created_at,
|
|
827
|
+
model: event.response.model
|
|
828
|
+
}, {
|
|
829
|
+
delta: {},
|
|
830
|
+
finishReason: getFinishReason({
|
|
831
|
+
output: event.response.output ?? [],
|
|
832
|
+
incomplete_details: event.response.incomplete_details
|
|
833
|
+
}, (event.response.output ?? []).some((item) => item.type === "function_call")),
|
|
834
|
+
usage: translateUsage(event.response.usage)
|
|
835
|
+
});
|
|
836
|
+
yield { data: "[DONE]" };
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
function handleOutputItemAdded(state, event) {
|
|
842
|
+
if (event.item?.type === "message") return createRoleChunk(state);
|
|
843
|
+
if (event.item?.type === "function_call" && event.output_index !== void 0) {
|
|
844
|
+
state.started = true;
|
|
845
|
+
return createChunk(state, { delta: {
|
|
846
|
+
role: "assistant",
|
|
847
|
+
tool_calls: [{
|
|
848
|
+
index: event.output_index,
|
|
849
|
+
id: event.item.call_id,
|
|
850
|
+
type: "function",
|
|
851
|
+
function: {
|
|
852
|
+
name: event.item.name,
|
|
853
|
+
arguments: event.item.arguments ?? ""
|
|
854
|
+
}
|
|
855
|
+
}]
|
|
856
|
+
} });
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
function createRoleChunk(state) {
|
|
860
|
+
if (state.started) return { data: "" };
|
|
861
|
+
state.started = true;
|
|
862
|
+
return createChunk(state, { delta: { role: "assistant" } });
|
|
863
|
+
}
|
|
864
|
+
function createChunk(state, { delta, finishReason = null, usage }) {
|
|
865
|
+
return { data: JSON.stringify({
|
|
866
|
+
id: state.responseId,
|
|
867
|
+
object: "chat.completion.chunk",
|
|
868
|
+
created: state.createdAt,
|
|
869
|
+
model: state.model,
|
|
870
|
+
choices: [{
|
|
871
|
+
index: 0,
|
|
872
|
+
delta,
|
|
873
|
+
finish_reason: finishReason,
|
|
874
|
+
logprobs: null
|
|
875
|
+
}],
|
|
876
|
+
usage
|
|
877
|
+
}) };
|
|
878
|
+
}
|
|
879
|
+
function translateMessagesToResponsesInput(messages) {
|
|
880
|
+
const items = messages.flatMap((message) => translateMessage(message));
|
|
881
|
+
if (items.length === 1 && "role" in items[0] && items[0].role === "user" && typeof items[0].content === "string") return items[0].content;
|
|
882
|
+
return items;
|
|
883
|
+
}
|
|
884
|
+
function translateMessage(message) {
|
|
885
|
+
if (message.role === "tool") return [{
|
|
886
|
+
type: "function_call_output",
|
|
887
|
+
call_id: message.tool_call_id ?? "",
|
|
888
|
+
output: stringifyToolOutput(message.content)
|
|
889
|
+
}];
|
|
890
|
+
const translated = [];
|
|
891
|
+
if (hasContent(message.content)) translated.push({
|
|
892
|
+
role: message.role,
|
|
893
|
+
content: translateContent(message.content)
|
|
894
|
+
});
|
|
895
|
+
if (message.role === "assistant" && message.tool_calls) translated.push(...message.tool_calls.map((toolCall) => ({
|
|
896
|
+
type: "function_call",
|
|
897
|
+
call_id: toolCall.id,
|
|
898
|
+
name: toolCall.function.name,
|
|
899
|
+
arguments: toolCall.function.arguments
|
|
900
|
+
})));
|
|
901
|
+
return translated;
|
|
902
|
+
}
|
|
903
|
+
function hasContent(content) {
|
|
904
|
+
if (content === null) return false;
|
|
905
|
+
if (typeof content === "string") return content.length > 0;
|
|
906
|
+
return content.length > 0;
|
|
907
|
+
}
|
|
908
|
+
function translateContent(content) {
|
|
909
|
+
if (typeof content === "string") return content;
|
|
910
|
+
if (!content || content.length === 0) return "";
|
|
911
|
+
return content.map((part) => translateContentPart(part));
|
|
912
|
+
}
|
|
913
|
+
function translateContentPart(part) {
|
|
914
|
+
if (part.type === "text") return {
|
|
915
|
+
type: "input_text",
|
|
916
|
+
text: part.text
|
|
917
|
+
};
|
|
918
|
+
return {
|
|
919
|
+
type: "input_image",
|
|
920
|
+
image_url: part.image_url.url,
|
|
921
|
+
detail: part.image_url.detail ?? "auto"
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
function stringifyToolOutput(content) {
|
|
925
|
+
if (typeof content === "string") return content;
|
|
926
|
+
if (!content) return "";
|
|
927
|
+
return content.filter((part) => part.type === "text").map((part) => part.text).join("\n\n") || JSON.stringify(content);
|
|
928
|
+
}
|
|
929
|
+
function translateTools(tools) {
|
|
930
|
+
if (!tools) return;
|
|
931
|
+
return tools.map((tool) => ({
|
|
932
|
+
type: "function",
|
|
933
|
+
name: tool.function.name,
|
|
934
|
+
description: tool.function.description,
|
|
935
|
+
parameters: tool.function.parameters
|
|
936
|
+
}));
|
|
937
|
+
}
|
|
938
|
+
function translateToolChoice(toolChoice) {
|
|
939
|
+
if (!toolChoice) return;
|
|
940
|
+
if (typeof toolChoice === "string") return toolChoice;
|
|
941
|
+
return {
|
|
942
|
+
type: "function",
|
|
943
|
+
name: toolChoice.function.name
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
function translateUsage(usage) {
|
|
947
|
+
if (!usage) return;
|
|
948
|
+
return {
|
|
949
|
+
prompt_tokens: usage.input_tokens,
|
|
950
|
+
completion_tokens: usage.output_tokens,
|
|
951
|
+
total_tokens: usage.total_tokens,
|
|
952
|
+
...usage.input_tokens_details?.cached_tokens !== void 0 && { prompt_tokens_details: { cached_tokens: usage.input_tokens_details.cached_tokens } }
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
function getFinishReason(response, hasFunctionCalls) {
|
|
956
|
+
if (hasFunctionCalls) return "tool_calls";
|
|
957
|
+
if (response.incomplete_details?.reason?.includes("max_output_tokens")) return "length";
|
|
958
|
+
return "stop";
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
//#endregion
|
|
962
|
+
//#region src/services/copilot/create-chat-completions.ts
|
|
963
|
+
const usesMaxCompletionTokens = (modelId) => modelId.startsWith("gpt-5");
|
|
964
|
+
const isClaudeOpus47Model$1 = (modelId) => modelId === "claude-opus-4.7";
|
|
965
|
+
const MAX_USER_LENGTH = 64;
|
|
966
|
+
const defaultReasoningEffort = (modelId) => usesMaxCompletionTokens(modelId) ? "medium" : void 0;
|
|
967
|
+
const getAllowedReasoningEfforts = (modelId) => {
|
|
968
|
+
if (modelId.startsWith("gpt-5.5")) return [
|
|
969
|
+
"none",
|
|
970
|
+
"low",
|
|
971
|
+
"medium",
|
|
972
|
+
"high",
|
|
973
|
+
"xhigh"
|
|
974
|
+
];
|
|
975
|
+
if (modelId.startsWith("gpt-5.4-mini")) return [
|
|
976
|
+
"none",
|
|
977
|
+
"low",
|
|
978
|
+
"medium"
|
|
979
|
+
];
|
|
980
|
+
if (modelId.startsWith("gpt-5.4") || modelId.startsWith("gpt-5.3-codex")) return [
|
|
981
|
+
"low",
|
|
982
|
+
"medium",
|
|
983
|
+
"high",
|
|
984
|
+
"xhigh"
|
|
985
|
+
];
|
|
986
|
+
if (usesMaxCompletionTokens(modelId)) return [
|
|
987
|
+
"low",
|
|
988
|
+
"medium",
|
|
989
|
+
"high",
|
|
990
|
+
"xhigh"
|
|
991
|
+
];
|
|
992
|
+
return [];
|
|
993
|
+
};
|
|
994
|
+
const sanitizeReasoningEffortForModel = (modelId, reasoningEffort) => {
|
|
995
|
+
if (!reasoningEffort) return;
|
|
996
|
+
return getAllowedReasoningEfforts(modelId).includes(reasoningEffort) ? reasoningEffort : void 0;
|
|
997
|
+
};
|
|
998
|
+
const normalizeReasoningEffort$1 = (value) => {
|
|
999
|
+
switch (value?.toLowerCase()) {
|
|
1000
|
+
case "none": return "none";
|
|
1001
|
+
case "low": return "low";
|
|
1002
|
+
case "medium": return "medium";
|
|
1003
|
+
case "high": return "high";
|
|
1004
|
+
case "xhigh": return "xhigh";
|
|
1005
|
+
case "max": return "max";
|
|
1006
|
+
default: return;
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
const normalizeClaudeOpus47Effort = (value) => {
|
|
1010
|
+
switch (value?.toLowerCase()) {
|
|
1011
|
+
case "low": return "low";
|
|
1012
|
+
case "medium": return "medium";
|
|
1013
|
+
case "high": return "high";
|
|
1014
|
+
case "xhigh": return "xhigh";
|
|
1015
|
+
case "max": return "max";
|
|
1016
|
+
default: return;
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
const getRequestedReasoningEffort = (payload, claudeSettingsEnv) => {
|
|
1020
|
+
const requestedReasoningEffort = payload.reasoning_effort ?? normalizeReasoningEffort$1(process.env.COPILOT_REASONING_EFFORT) ?? normalizeReasoningEffort$1(claudeSettingsEnv.COPILOT_REASONING_EFFORT);
|
|
1021
|
+
return sanitizeReasoningEffortForModel(payload.model, requestedReasoningEffort) ?? defaultReasoningEffort(payload.model);
|
|
1022
|
+
};
|
|
1023
|
+
const getRequestedClaudeOpus47Effort = (payload, claudeSettingsEnv) => {
|
|
1024
|
+
if (!isClaudeOpus47Model$1(payload.model)) return;
|
|
1025
|
+
return payload.output_config?.effort ?? normalizeClaudeOpus47Effort(payload.reasoning_effort) ?? normalizeClaudeOpus47Effort(process.env.COPILOT_REASONING_EFFORT) ?? normalizeClaudeOpus47Effort(claudeSettingsEnv.COPILOT_REASONING_EFFORT);
|
|
1026
|
+
};
|
|
1027
|
+
const sanitizeUserIdentifier = (user) => {
|
|
1028
|
+
if (!user) return;
|
|
1029
|
+
return user.slice(0, MAX_USER_LENGTH);
|
|
1030
|
+
};
|
|
1031
|
+
const buildRequestPayload = (payload, claudeSettingsEnv) => {
|
|
1032
|
+
const requestedReasoningEffort = getRequestedReasoningEffort(payload, claudeSettingsEnv);
|
|
1033
|
+
const requestedClaudeOpus47Effort = getRequestedClaudeOpus47Effort(payload, claudeSettingsEnv);
|
|
1034
|
+
const reasoningEffort = usesMaxCompletionTokens(payload.model) && payload.tools !== null && payload.tools !== void 0 && payload.tools.length > 0 ? void 0 : requestedReasoningEffort;
|
|
1035
|
+
if (!usesMaxCompletionTokens(payload.model) || payload.max_tokens === null || payload.max_tokens === void 0) {
|
|
1036
|
+
const sanitizedPayload = {
|
|
1037
|
+
...payload,
|
|
1038
|
+
output_config: requestedClaudeOpus47Effort ? {
|
|
1039
|
+
...payload.output_config,
|
|
1040
|
+
effort: requestedClaudeOpus47Effort
|
|
1041
|
+
} : payload.output_config,
|
|
1042
|
+
reasoning_effort: isClaudeOpus47Model$1(payload.model) ? void 0 : payload.reasoning_effort,
|
|
1043
|
+
user: sanitizeUserIdentifier(payload.user)
|
|
1044
|
+
};
|
|
1045
|
+
return reasoningEffort === null || reasoningEffort === void 0 ? sanitizedPayload : {
|
|
1046
|
+
...sanitizedPayload,
|
|
1047
|
+
reasoning_effort: reasoningEffort
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
return {
|
|
1051
|
+
...payload,
|
|
1052
|
+
max_tokens: void 0,
|
|
1053
|
+
max_completion_tokens: payload.max_tokens,
|
|
1054
|
+
reasoning_effort: reasoningEffort,
|
|
1055
|
+
user: sanitizeUserIdentifier(payload.user)
|
|
1056
|
+
};
|
|
1057
|
+
};
|
|
1058
|
+
const isAgentInitiator = (messages) => messages.some((msg) => msg.role === "assistant" || msg.role === "tool") ? "agent" : "user";
|
|
1059
|
+
const messagesIncludeImage = (messages) => messages.some((msg) => typeof msg.content !== "string" && msg.content?.some((part) => part.type === "image_url"));
|
|
1060
|
+
const createChatCompletions = async (config, payload) => {
|
|
1061
|
+
const provider = getCopilotProviderContext(config);
|
|
1062
|
+
const enableVision = messagesIncludeImage(payload.messages);
|
|
1063
|
+
const initiator = isAgentInitiator(payload.messages);
|
|
1064
|
+
const claudeSettingsEnv = await getClaudeSettingsEnv();
|
|
1065
|
+
const requestPayload = buildRequestPayload(payload, claudeSettingsEnv);
|
|
1066
|
+
if (shouldUseResponsesApiForModel(payload.model)) return createResponses(provider, payload, claudeSettingsEnv, {
|
|
1067
|
+
vision: enableVision,
|
|
1068
|
+
initiator
|
|
1069
|
+
});
|
|
1070
|
+
const response = await fetchCopilot(provider, "/chat/completions", {
|
|
1071
|
+
method: "POST",
|
|
1072
|
+
headers: {
|
|
1073
|
+
accept: payload.stream ? "text/event-stream" : "application/json",
|
|
1074
|
+
"content-type": "application/json"
|
|
1075
|
+
},
|
|
1076
|
+
body: JSON.stringify(requestPayload)
|
|
1077
|
+
}, {
|
|
1078
|
+
vision: enableVision,
|
|
1079
|
+
initiator
|
|
1080
|
+
});
|
|
1081
|
+
if (!response.ok) {
|
|
1082
|
+
if (await shouldRetryWithResponses(response)) return createResponses(provider, payload, claudeSettingsEnv, {
|
|
1083
|
+
vision: enableVision,
|
|
1084
|
+
initiator
|
|
1085
|
+
});
|
|
1086
|
+
consola.error("Failed to create chat completions", response);
|
|
1087
|
+
throw new HTTPError("Failed to create chat completions", response);
|
|
1088
|
+
}
|
|
1089
|
+
if (payload.stream) return events(response);
|
|
1090
|
+
return await response.json();
|
|
1091
|
+
};
|
|
1092
|
+
async function createResponses(provider, payload, claudeSettingsEnv, options) {
|
|
1093
|
+
const reasoningEffort = getRequestedReasoningEffort(payload, claudeSettingsEnv);
|
|
1094
|
+
const response = await fetchCopilot(provider, "/responses", {
|
|
1095
|
+
method: "POST",
|
|
1096
|
+
headers: {
|
|
1097
|
+
accept: payload.stream ? "text/event-stream" : "application/json",
|
|
1098
|
+
"content-type": "application/json"
|
|
1099
|
+
},
|
|
1100
|
+
body: JSON.stringify(buildResponsesRequestPayload(payload, reasoningEffort))
|
|
1101
|
+
}, {
|
|
1102
|
+
vision: options.vision,
|
|
1103
|
+
initiator: options.initiator
|
|
1104
|
+
});
|
|
1105
|
+
if (!response.ok) {
|
|
1106
|
+
consola.error("Failed to create responses", response);
|
|
1107
|
+
throw new HTTPError("Failed to create responses", response);
|
|
1108
|
+
}
|
|
1109
|
+
if (payload.stream) return translateResponsesStreamToChatCompletionStream(events(response));
|
|
1110
|
+
return translateResponsesToChatCompletion(await response.json());
|
|
1111
|
+
}
|
|
1112
|
+
async function shouldRetryWithResponses(response) {
|
|
1113
|
+
try {
|
|
1114
|
+
return (await response.clone().json()).error?.code === "unsupported_api_for_model";
|
|
1115
|
+
} catch {
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
//#endregion
|
|
1121
|
+
//#region src/routes/chat-completions.ts
|
|
1122
|
+
const chatCompletionRoutes = new Hono();
|
|
1123
|
+
const isNonStreaming = (response) => typeof response === "object" && response !== null && Object.hasOwn(response, "choices");
|
|
1124
|
+
chatCompletionRoutes.post("/", async (c) => {
|
|
1125
|
+
try {
|
|
1126
|
+
await checkRateLimit();
|
|
1127
|
+
const payload = await c.req.json();
|
|
1128
|
+
const response = await createChatCompletions(c.var.config, payload);
|
|
1129
|
+
if (isNonStreaming(response)) return c.json(response);
|
|
1130
|
+
return streamSSE(c, async (stream) => {
|
|
1131
|
+
for await (const chunk of response) await stream.writeSSE(chunk);
|
|
1132
|
+
});
|
|
1133
|
+
} catch (error) {
|
|
1134
|
+
if (error instanceof RateLimitError) return c.json({ error: { message: error.message } }, 429);
|
|
1135
|
+
if (error instanceof BridgeNotImplementedError) return c.json({ error: {
|
|
1136
|
+
type: error.name,
|
|
1137
|
+
message: error.message
|
|
1138
|
+
} }, 501);
|
|
1139
|
+
if (error instanceof HTTPError) {
|
|
1140
|
+
const text = await error.response.text().catch(() => "");
|
|
1141
|
+
return new Response(text, {
|
|
1142
|
+
status: error.response.status,
|
|
1143
|
+
headers: { "content-type": error.response.headers.get("content-type") ?? "application/json" }
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
throw error;
|
|
1147
|
+
}
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
//#endregion
|
|
1151
|
+
//#region src/providers/copilot/create-embeddings.ts
|
|
1152
|
+
const createEmbeddings = async (config, payload) => {
|
|
1153
|
+
const response = await fetchCopilot(getCopilotProviderContext(config), "/embeddings", {
|
|
1154
|
+
method: "POST",
|
|
1155
|
+
headers: {
|
|
1156
|
+
accept: "application/json",
|
|
1157
|
+
"content-type": "application/json"
|
|
1158
|
+
},
|
|
1159
|
+
body: JSON.stringify(payload)
|
|
1160
|
+
});
|
|
1161
|
+
if (!response.ok) throw new HTTPError("Failed to create embeddings", response);
|
|
1162
|
+
return await response.json();
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
//#endregion
|
|
1166
|
+
//#region src/routes/embeddings.ts
|
|
1167
|
+
const embeddingRoutes = new Hono();
|
|
1168
|
+
embeddingRoutes.post("/", async (c) => {
|
|
1169
|
+
try {
|
|
1170
|
+
await checkRateLimit();
|
|
1171
|
+
const payload = await c.req.json();
|
|
1172
|
+
const response = await createEmbeddings(c.var.config, payload);
|
|
1173
|
+
return c.json(response);
|
|
1174
|
+
} catch (error) {
|
|
1175
|
+
if (error instanceof RateLimitError) return c.json({ error: { message: error.message } }, 429);
|
|
1176
|
+
if (error instanceof HTTPError) {
|
|
1177
|
+
const text = await error.response.text().catch(() => "");
|
|
1178
|
+
return new Response(text, {
|
|
1179
|
+
status: error.response.status,
|
|
1180
|
+
headers: { "content-type": error.response.headers.get("content-type") ?? "application/json" }
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
throw error;
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
//#endregion
|
|
1188
|
+
//#region src/lib/models-resolver.ts
|
|
1189
|
+
const normalizeModelId = (modelId) => modelId.trim().toLowerCase().replaceAll(/[\s._-]+/g, "");
|
|
1190
|
+
const stripSnapshotSuffix = (modelId) => {
|
|
1191
|
+
const id = modelId.trim().toLowerCase().replace(/\[1m\]$/, "");
|
|
1192
|
+
if (id.startsWith("claude-sonnet-4-")) return "claude-sonnet-4";
|
|
1193
|
+
if (/^claude-opus-4-7-\d{8}$/.test(id)) return "claude-opus-4.7";
|
|
1194
|
+
if (id.startsWith("claude-opus-4-")) return "claude-opus-4";
|
|
1195
|
+
return id;
|
|
1196
|
+
};
|
|
1197
|
+
const getAliasCandidates = (modelId) => {
|
|
1198
|
+
const canonicalModelId = stripSnapshotSuffix(modelId);
|
|
1199
|
+
const aliases = new Set([canonicalModelId]);
|
|
1200
|
+
const familyMatch = canonicalModelId.match(/^[a-z]+(?:-[a-z]+)*-\d+(?:\.\d+)?/);
|
|
1201
|
+
if (familyMatch) {
|
|
1202
|
+
aliases.add(familyMatch[0]);
|
|
1203
|
+
aliases.add(familyMatch[0].replace(/\.\d+$/, ""));
|
|
1204
|
+
}
|
|
1205
|
+
if (/^gpt-5(?:[.-]\d+)?$/i.test(canonicalModelId)) aliases.add("gpt-5");
|
|
1206
|
+
return [...aliases];
|
|
1207
|
+
};
|
|
1208
|
+
const scoreModelCandidate = (model) => {
|
|
1209
|
+
let score = 0;
|
|
1210
|
+
if (model.model_picker_enabled) score += 20;
|
|
1211
|
+
if (!model.preview) score += 10;
|
|
1212
|
+
if (/mini|nano|fast|flash|haiku/i.test(model.id)) score -= 15;
|
|
1213
|
+
return score - model.id.length / 1e3;
|
|
1214
|
+
};
|
|
1215
|
+
const pickBestModel = (models) => [...models].sort((left, right) => scoreModelCandidate(right) - scoreModelCandidate(left))[0];
|
|
1216
|
+
const getBestPrefixMatches = (models, aliasCandidates) => {
|
|
1217
|
+
let bestMatchLength = 0;
|
|
1218
|
+
const matches = [];
|
|
1219
|
+
for (const model of models) {
|
|
1220
|
+
const normalizedModelId = normalizeModelId(model.id);
|
|
1221
|
+
const matchedAliasLength = Math.max(0, ...aliasCandidates.map((candidate) => {
|
|
1222
|
+
const normalizedCandidate = normalizeModelId(candidate);
|
|
1223
|
+
return normalizedCandidate.length >= 4 && normalizedModelId.startsWith(normalizedCandidate) ? normalizedCandidate.length : 0;
|
|
1224
|
+
}));
|
|
1225
|
+
if (matchedAliasLength === 0) continue;
|
|
1226
|
+
if (matchedAliasLength > bestMatchLength) {
|
|
1227
|
+
bestMatchLength = matchedAliasLength;
|
|
1228
|
+
matches.length = 0;
|
|
1229
|
+
matches.push(model);
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
if (matchedAliasLength === bestMatchLength) matches.push(model);
|
|
1233
|
+
}
|
|
1234
|
+
return matches;
|
|
1235
|
+
};
|
|
1236
|
+
const resolveModel = (requestedModelId, models = runtimeState.models?.data) => {
|
|
1237
|
+
if (!models || models.length === 0) return;
|
|
1238
|
+
const exactMatch = models.find((model) => model.id === requestedModelId);
|
|
1239
|
+
if (exactMatch) return exactMatch;
|
|
1240
|
+
const aliasCandidates = getAliasCandidates(requestedModelId);
|
|
1241
|
+
const normalizedAliases = new Set(aliasCandidates.map((candidate) => normalizeModelId(candidate)));
|
|
1242
|
+
const normalizedExactMatches = models.filter((model) => normalizedAliases.has(normalizeModelId(model.id)));
|
|
1243
|
+
if (normalizedExactMatches.length > 0) return pickBestModel(normalizedExactMatches);
|
|
1244
|
+
return pickBestModel(getBestPrefixMatches(models, aliasCandidates));
|
|
1245
|
+
};
|
|
1246
|
+
const resolveUpstreamModelId = (requestedModelId, models = runtimeState.models?.data) => resolveModel(requestedModelId, models)?.id ?? stripSnapshotSuffix(requestedModelId);
|
|
1247
|
+
|
|
1248
|
+
//#endregion
|
|
1249
|
+
//#region src/bridges/claude/utils.ts
|
|
1250
|
+
function mapOpenAIStopReasonToAnthropic(finishReason) {
|
|
1251
|
+
if (finishReason === null) return null;
|
|
1252
|
+
return {
|
|
1253
|
+
stop: "end_turn",
|
|
1254
|
+
length: "max_tokens",
|
|
1255
|
+
tool_calls: "tool_use",
|
|
1256
|
+
content_filter: "end_turn"
|
|
1257
|
+
}[finishReason];
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
//#endregion
|
|
1261
|
+
//#region src/bridges/claude/non-stream-translation.ts
|
|
1262
|
+
const normalizeClaudeModelAlias = (model) => {
|
|
1263
|
+
const normalized = model.trim().toLowerCase().replace(/\[1m\]$/, "");
|
|
1264
|
+
if (/^claude-(opus|sonnet|haiku)-\d-\d(?:-1m)?$/.test(normalized)) return normalized.replace(/^claude-(opus|sonnet|haiku)-(\d)-(\d)(-1m)?$/, "claude-$1-$2.$3$4");
|
|
1265
|
+
if (/^claude-opus-4-7-\d{8}$/.test(normalized)) return "claude-opus-4.7";
|
|
1266
|
+
return normalized;
|
|
1267
|
+
};
|
|
1268
|
+
function translateModelName(model) {
|
|
1269
|
+
return resolveUpstreamModelId(normalizeClaudeModelAlias(model));
|
|
1270
|
+
}
|
|
1271
|
+
function isClaudeModel(modelId) {
|
|
1272
|
+
return modelId.startsWith("claude-");
|
|
1273
|
+
}
|
|
1274
|
+
function isClaudeOpus47Model(modelId) {
|
|
1275
|
+
return modelId === "claude-opus-4.7";
|
|
1276
|
+
}
|
|
1277
|
+
function normalizeClaudeEffort(value) {
|
|
1278
|
+
switch (value?.toLowerCase()) {
|
|
1279
|
+
case "low": return "low";
|
|
1280
|
+
case "medium": return "medium";
|
|
1281
|
+
case "high": return "high";
|
|
1282
|
+
case "xhigh": return "xhigh";
|
|
1283
|
+
case "max": return "max";
|
|
1284
|
+
default: return;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
function getClaudeOpus47Effort(payload) {
|
|
1288
|
+
const explicitEffort = normalizeClaudeEffort(payload.reasoning_effort);
|
|
1289
|
+
if (explicitEffort) return explicitEffort;
|
|
1290
|
+
if (payload.thinking?.type !== "enabled") return;
|
|
1291
|
+
const budgetTokens = payload.thinking.budget_tokens;
|
|
1292
|
+
if (budgetTokens === void 0) return "medium";
|
|
1293
|
+
if (budgetTokens <= 2048) return "low";
|
|
1294
|
+
if (budgetTokens <= 8192) return "medium";
|
|
1295
|
+
if (budgetTokens <= 24576) return "high";
|
|
1296
|
+
return "xhigh";
|
|
1297
|
+
}
|
|
1298
|
+
function translateThinking(payload) {
|
|
1299
|
+
if (!isClaudeOpus47Model(translateModelName(payload.model))) return;
|
|
1300
|
+
if (payload.thinking?.type === "adaptive") return { type: "adaptive" };
|
|
1301
|
+
return payload.thinking?.type === "enabled" ? { type: "adaptive" } : void 0;
|
|
1302
|
+
}
|
|
1303
|
+
function translateOutputConfig(payload) {
|
|
1304
|
+
if (!isClaudeOpus47Model(translateModelName(payload.model))) return;
|
|
1305
|
+
const effort = getClaudeOpus47Effort(payload);
|
|
1306
|
+
return effort ? { effort } : void 0;
|
|
1307
|
+
}
|
|
1308
|
+
function normalizeReasoningEffort(value) {
|
|
1309
|
+
switch (value.toLowerCase()) {
|
|
1310
|
+
case "none": return "none";
|
|
1311
|
+
case "low": return "low";
|
|
1312
|
+
case "medium": return "medium";
|
|
1313
|
+
case "high": return "high";
|
|
1314
|
+
case "xhigh": return "xhigh";
|
|
1315
|
+
case "max": return "max";
|
|
1316
|
+
default: return;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
function translateReasoningEffort(payload) {
|
|
1320
|
+
const modelId = translateModelName(payload.model);
|
|
1321
|
+
if (isClaudeModel(modelId)) return;
|
|
1322
|
+
if (payload.reasoning_effort) return sanitizeReasoningEffortForModel(modelId, normalizeReasoningEffort(payload.reasoning_effort));
|
|
1323
|
+
if (payload.thinking?.type !== "enabled") return;
|
|
1324
|
+
const budgetTokens = payload.thinking.budget_tokens;
|
|
1325
|
+
if (budgetTokens === void 0) return sanitizeReasoningEffortForModel(modelId, "medium");
|
|
1326
|
+
if (budgetTokens <= 2048) return sanitizeReasoningEffortForModel(modelId, "low");
|
|
1327
|
+
if (budgetTokens <= 8192) return sanitizeReasoningEffortForModel(modelId, "medium");
|
|
1328
|
+
if (budgetTokens <= 24576) return sanitizeReasoningEffortForModel(modelId, "high");
|
|
1329
|
+
return sanitizeReasoningEffortForModel(modelId, "xhigh");
|
|
1330
|
+
}
|
|
1331
|
+
function translateToOpenAI(payload) {
|
|
1332
|
+
return {
|
|
1333
|
+
model: translateModelName(payload.model),
|
|
1334
|
+
messages: translateAnthropicMessagesToOpenAI(payload.messages, payload.system),
|
|
1335
|
+
max_tokens: payload.max_tokens,
|
|
1336
|
+
stop: payload.stop_sequences,
|
|
1337
|
+
stream: payload.stream,
|
|
1338
|
+
temperature: payload.temperature,
|
|
1339
|
+
top_p: payload.top_p,
|
|
1340
|
+
thinking: translateThinking(payload),
|
|
1341
|
+
output_config: translateOutputConfig(payload),
|
|
1342
|
+
reasoning_effort: translateReasoningEffort(payload),
|
|
1343
|
+
user: payload.metadata?.user_id,
|
|
1344
|
+
tools: translateAnthropicToolsToOpenAI(payload.tools),
|
|
1345
|
+
tool_choice: translateAnthropicToolChoiceToOpenAI(payload.tool_choice)
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
function translateAnthropicMessagesToOpenAI(anthropicMessages, system) {
|
|
1349
|
+
const systemMessages = handleSystemPrompt(system);
|
|
1350
|
+
const otherMessages = anthropicMessages.flatMap((message) => message.role === "user" ? handleUserMessage(message) : handleAssistantMessage(message));
|
|
1351
|
+
return [...systemMessages, ...otherMessages];
|
|
1352
|
+
}
|
|
1353
|
+
function handleSystemPrompt(system) {
|
|
1354
|
+
if (!system) return [];
|
|
1355
|
+
if (typeof system === "string") return [{
|
|
1356
|
+
role: "system",
|
|
1357
|
+
content: system
|
|
1358
|
+
}];
|
|
1359
|
+
return [{
|
|
1360
|
+
role: "system",
|
|
1361
|
+
content: system.map((block) => block.text).join("\n\n")
|
|
1362
|
+
}];
|
|
1363
|
+
}
|
|
1364
|
+
function handleUserMessage(message) {
|
|
1365
|
+
const newMessages = [];
|
|
1366
|
+
if (Array.isArray(message.content)) {
|
|
1367
|
+
const toolResultBlocks = message.content.filter((block) => block.type === "tool_result");
|
|
1368
|
+
const otherBlocks = message.content.filter((block) => block.type !== "tool_result");
|
|
1369
|
+
for (const block of toolResultBlocks) newMessages.push({
|
|
1370
|
+
role: "tool",
|
|
1371
|
+
tool_call_id: block.tool_use_id,
|
|
1372
|
+
content: mapContent(block.content)
|
|
1373
|
+
});
|
|
1374
|
+
if (otherBlocks.length > 0) newMessages.push({
|
|
1375
|
+
role: "user",
|
|
1376
|
+
content: mapContent(otherBlocks)
|
|
1377
|
+
});
|
|
1378
|
+
} else newMessages.push({
|
|
1379
|
+
role: "user",
|
|
1380
|
+
content: mapContent(message.content)
|
|
1381
|
+
});
|
|
1382
|
+
return newMessages;
|
|
1383
|
+
}
|
|
1384
|
+
function handleAssistantMessage(message) {
|
|
1385
|
+
if (!Array.isArray(message.content)) return [{
|
|
1386
|
+
role: "assistant",
|
|
1387
|
+
content: mapContent(message.content)
|
|
1388
|
+
}];
|
|
1389
|
+
const toolUseBlocks = message.content.filter((block) => block.type === "tool_use");
|
|
1390
|
+
const textBlocks = message.content.filter((block) => block.type === "text");
|
|
1391
|
+
const thinkingBlocks = message.content.filter((block) => block.type === "thinking");
|
|
1392
|
+
const allTextContent = [...textBlocks.map((b) => b.text), ...thinkingBlocks.map((b) => b.thinking)].join("\n\n");
|
|
1393
|
+
return toolUseBlocks.length > 0 ? [{
|
|
1394
|
+
role: "assistant",
|
|
1395
|
+
content: allTextContent || null,
|
|
1396
|
+
tool_calls: toolUseBlocks.map((toolUse) => ({
|
|
1397
|
+
id: toolUse.id,
|
|
1398
|
+
type: "function",
|
|
1399
|
+
function: {
|
|
1400
|
+
name: toolUse.name,
|
|
1401
|
+
arguments: JSON.stringify(toolUse.input)
|
|
1402
|
+
}
|
|
1403
|
+
}))
|
|
1404
|
+
}] : [{
|
|
1405
|
+
role: "assistant",
|
|
1406
|
+
content: mapContent(message.content)
|
|
1407
|
+
}];
|
|
1408
|
+
}
|
|
1409
|
+
function mapContent(content) {
|
|
1410
|
+
if (typeof content === "string") return content;
|
|
1411
|
+
if (!Array.isArray(content)) return null;
|
|
1412
|
+
if (!content.some((block) => block.type === "image")) return content.filter((block) => block.type === "text" || block.type === "thinking").map((block) => block.type === "text" ? block.text : block.thinking).join("\n\n");
|
|
1413
|
+
const contentParts = [];
|
|
1414
|
+
for (const block of content) switch (block.type) {
|
|
1415
|
+
case "text":
|
|
1416
|
+
contentParts.push({
|
|
1417
|
+
type: "text",
|
|
1418
|
+
text: block.text
|
|
1419
|
+
});
|
|
1420
|
+
break;
|
|
1421
|
+
case "thinking":
|
|
1422
|
+
contentParts.push({
|
|
1423
|
+
type: "text",
|
|
1424
|
+
text: block.thinking
|
|
1425
|
+
});
|
|
1426
|
+
break;
|
|
1427
|
+
case "image":
|
|
1428
|
+
contentParts.push({
|
|
1429
|
+
type: "image_url",
|
|
1430
|
+
image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` }
|
|
1431
|
+
});
|
|
1432
|
+
break;
|
|
1433
|
+
}
|
|
1434
|
+
return contentParts;
|
|
1435
|
+
}
|
|
1436
|
+
function translateAnthropicToolsToOpenAI(anthropicTools) {
|
|
1437
|
+
if (!anthropicTools) return;
|
|
1438
|
+
return anthropicTools.map((tool) => ({
|
|
1439
|
+
type: "function",
|
|
1440
|
+
function: {
|
|
1441
|
+
name: tool.name,
|
|
1442
|
+
description: tool.description,
|
|
1443
|
+
parameters: tool.input_schema
|
|
1444
|
+
}
|
|
1445
|
+
}));
|
|
1446
|
+
}
|
|
1447
|
+
function translateAnthropicToolChoiceToOpenAI(anthropicToolChoice) {
|
|
1448
|
+
if (!anthropicToolChoice) return;
|
|
1449
|
+
switch (anthropicToolChoice.type) {
|
|
1450
|
+
case "auto": return "auto";
|
|
1451
|
+
case "any": return "required";
|
|
1452
|
+
case "tool":
|
|
1453
|
+
if (anthropicToolChoice.name) return {
|
|
1454
|
+
type: "function",
|
|
1455
|
+
function: { name: anthropicToolChoice.name }
|
|
1456
|
+
};
|
|
1457
|
+
return;
|
|
1458
|
+
case "none": return "none";
|
|
1459
|
+
default: return;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
function translateToAnthropic(response) {
|
|
1463
|
+
const allTextBlocks = [];
|
|
1464
|
+
const allToolUseBlocks = [];
|
|
1465
|
+
let stopReason = null;
|
|
1466
|
+
stopReason = response.choices[0]?.finish_reason ?? stopReason;
|
|
1467
|
+
for (const choice of response.choices) {
|
|
1468
|
+
allTextBlocks.push(...getAnthropicTextBlocks(choice.message.content));
|
|
1469
|
+
allToolUseBlocks.push(...getAnthropicToolUseBlocks(choice.message.tool_calls));
|
|
1470
|
+
if (choice.finish_reason === "tool_calls" || stopReason === "stop") stopReason = choice.finish_reason;
|
|
1471
|
+
}
|
|
1472
|
+
return {
|
|
1473
|
+
id: response.id,
|
|
1474
|
+
type: "message",
|
|
1475
|
+
role: "assistant",
|
|
1476
|
+
model: response.model,
|
|
1477
|
+
content: [...allTextBlocks, ...allToolUseBlocks],
|
|
1478
|
+
stop_reason: mapOpenAIStopReasonToAnthropic(stopReason),
|
|
1479
|
+
stop_sequence: null,
|
|
1480
|
+
usage: {
|
|
1481
|
+
input_tokens: (response.usage?.prompt_tokens ?? 0) - (response.usage?.prompt_tokens_details?.cached_tokens ?? 0),
|
|
1482
|
+
output_tokens: response.usage?.completion_tokens ?? 0,
|
|
1483
|
+
...response.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: response.usage.prompt_tokens_details.cached_tokens }
|
|
1484
|
+
}
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
function getAnthropicTextBlocks(messageContent) {
|
|
1488
|
+
if (typeof messageContent === "string") return [{
|
|
1489
|
+
type: "text",
|
|
1490
|
+
text: messageContent
|
|
1491
|
+
}];
|
|
1492
|
+
if (Array.isArray(messageContent)) return messageContent.filter((part) => part.type === "text").map((part) => ({
|
|
1493
|
+
type: "text",
|
|
1494
|
+
text: part.text
|
|
1495
|
+
}));
|
|
1496
|
+
return [];
|
|
1497
|
+
}
|
|
1498
|
+
function getAnthropicToolUseBlocks(toolCalls) {
|
|
1499
|
+
if (!toolCalls) return [];
|
|
1500
|
+
return toolCalls.map((toolCall) => ({
|
|
1501
|
+
type: "tool_use",
|
|
1502
|
+
id: toolCall.id,
|
|
1503
|
+
name: toolCall.function.name,
|
|
1504
|
+
input: safeJsonParse(toolCall.function.arguments)
|
|
1505
|
+
}));
|
|
1506
|
+
}
|
|
1507
|
+
function safeJsonParse(input) {
|
|
1508
|
+
try {
|
|
1509
|
+
return JSON.parse(input);
|
|
1510
|
+
} catch {
|
|
1511
|
+
return { raw: input };
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
//#endregion
|
|
1516
|
+
//#region src/bridges/claude/stream-translation.ts
|
|
1517
|
+
function isToolBlockOpen(state) {
|
|
1518
|
+
if (!state.contentBlockOpen) return false;
|
|
1519
|
+
return Object.values(state.toolCalls).some((tc) => tc.anthropicBlockIndex === state.contentBlockIndex);
|
|
1520
|
+
}
|
|
1521
|
+
function translateChunkToAnthropicEvents(chunk, state) {
|
|
1522
|
+
const events$1 = [];
|
|
1523
|
+
if (chunk.choices.length === 0) return events$1;
|
|
1524
|
+
const choice = chunk.choices[0];
|
|
1525
|
+
const { delta } = choice;
|
|
1526
|
+
if (!state.messageStartSent) {
|
|
1527
|
+
events$1.push({
|
|
1528
|
+
type: "message_start",
|
|
1529
|
+
message: {
|
|
1530
|
+
id: chunk.id,
|
|
1531
|
+
type: "message",
|
|
1532
|
+
role: "assistant",
|
|
1533
|
+
content: [],
|
|
1534
|
+
model: chunk.model,
|
|
1535
|
+
stop_reason: null,
|
|
1536
|
+
stop_sequence: null,
|
|
1537
|
+
usage: {
|
|
1538
|
+
input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0),
|
|
1539
|
+
output_tokens: 0,
|
|
1540
|
+
...chunk.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: chunk.usage.prompt_tokens_details.cached_tokens }
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
state.messageStartSent = true;
|
|
1545
|
+
}
|
|
1546
|
+
if (delta.content) {
|
|
1547
|
+
if (isToolBlockOpen(state)) {
|
|
1548
|
+
events$1.push({
|
|
1549
|
+
type: "content_block_stop",
|
|
1550
|
+
index: state.contentBlockIndex
|
|
1551
|
+
});
|
|
1552
|
+
state.contentBlockIndex++;
|
|
1553
|
+
state.contentBlockOpen = false;
|
|
1554
|
+
}
|
|
1555
|
+
if (!state.contentBlockOpen) {
|
|
1556
|
+
events$1.push({
|
|
1557
|
+
type: "content_block_start",
|
|
1558
|
+
index: state.contentBlockIndex,
|
|
1559
|
+
content_block: {
|
|
1560
|
+
type: "text",
|
|
1561
|
+
text: ""
|
|
1562
|
+
}
|
|
1563
|
+
});
|
|
1564
|
+
state.contentBlockOpen = true;
|
|
1565
|
+
}
|
|
1566
|
+
events$1.push({
|
|
1567
|
+
type: "content_block_delta",
|
|
1568
|
+
index: state.contentBlockIndex,
|
|
1569
|
+
delta: {
|
|
1570
|
+
type: "text_delta",
|
|
1571
|
+
text: delta.content
|
|
1572
|
+
}
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
if (delta.tool_calls) for (const toolCall of delta.tool_calls) {
|
|
1576
|
+
if (toolCall.id && toolCall.function?.name) {
|
|
1577
|
+
if (state.contentBlockOpen) {
|
|
1578
|
+
events$1.push({
|
|
1579
|
+
type: "content_block_stop",
|
|
1580
|
+
index: state.contentBlockIndex
|
|
1581
|
+
});
|
|
1582
|
+
state.contentBlockIndex++;
|
|
1583
|
+
state.contentBlockOpen = false;
|
|
1584
|
+
}
|
|
1585
|
+
const anthropicBlockIndex = state.contentBlockIndex;
|
|
1586
|
+
state.toolCalls[toolCall.index] = {
|
|
1587
|
+
id: toolCall.id,
|
|
1588
|
+
name: toolCall.function.name,
|
|
1589
|
+
anthropicBlockIndex
|
|
1590
|
+
};
|
|
1591
|
+
events$1.push({
|
|
1592
|
+
type: "content_block_start",
|
|
1593
|
+
index: anthropicBlockIndex,
|
|
1594
|
+
content_block: {
|
|
1595
|
+
type: "tool_use",
|
|
1596
|
+
id: toolCall.id,
|
|
1597
|
+
name: toolCall.function.name,
|
|
1598
|
+
input: {}
|
|
1599
|
+
}
|
|
1600
|
+
});
|
|
1601
|
+
state.contentBlockOpen = true;
|
|
1602
|
+
}
|
|
1603
|
+
if (toolCall.function?.arguments) {
|
|
1604
|
+
const toolCallInfo = state.toolCalls[toolCall.index];
|
|
1605
|
+
if (toolCallInfo) events$1.push({
|
|
1606
|
+
type: "content_block_delta",
|
|
1607
|
+
index: toolCallInfo.anthropicBlockIndex,
|
|
1608
|
+
delta: {
|
|
1609
|
+
type: "input_json_delta",
|
|
1610
|
+
partial_json: toolCall.function.arguments
|
|
1611
|
+
}
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
if (choice.finish_reason) {
|
|
1616
|
+
if (state.contentBlockOpen) {
|
|
1617
|
+
events$1.push({
|
|
1618
|
+
type: "content_block_stop",
|
|
1619
|
+
index: state.contentBlockIndex
|
|
1620
|
+
});
|
|
1621
|
+
state.contentBlockOpen = false;
|
|
1622
|
+
}
|
|
1623
|
+
events$1.push({
|
|
1624
|
+
type: "message_delta",
|
|
1625
|
+
delta: {
|
|
1626
|
+
stop_reason: mapOpenAIStopReasonToAnthropic(choice.finish_reason),
|
|
1627
|
+
stop_sequence: null
|
|
1628
|
+
},
|
|
1629
|
+
usage: {
|
|
1630
|
+
input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0),
|
|
1631
|
+
output_tokens: chunk.usage?.completion_tokens ?? 0,
|
|
1632
|
+
...chunk.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: chunk.usage.prompt_tokens_details.cached_tokens }
|
|
1633
|
+
}
|
|
1634
|
+
}, { type: "message_stop" });
|
|
1635
|
+
}
|
|
1636
|
+
return events$1;
|
|
1637
|
+
}
|
|
1638
|
+
function translateErrorToAnthropicErrorEvent() {
|
|
1639
|
+
return {
|
|
1640
|
+
type: "error",
|
|
1641
|
+
error: {
|
|
1642
|
+
type: "api_error",
|
|
1643
|
+
message: "An unexpected error occurred during streaming."
|
|
1644
|
+
}
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
//#endregion
|
|
1649
|
+
//#region src/lib/tokenizer.ts
|
|
1650
|
+
const ENCODING_MAP = {
|
|
1651
|
+
o200k_base: () => import("gpt-tokenizer/encoding/o200k_base"),
|
|
1652
|
+
cl100k_base: () => import("gpt-tokenizer/encoding/cl100k_base"),
|
|
1653
|
+
p50k_base: () => import("gpt-tokenizer/encoding/p50k_base"),
|
|
1654
|
+
p50k_edit: () => import("gpt-tokenizer/encoding/p50k_edit"),
|
|
1655
|
+
r50k_base: () => import("gpt-tokenizer/encoding/r50k_base")
|
|
1656
|
+
};
|
|
1657
|
+
const encodingCache = /* @__PURE__ */ new Map();
|
|
1658
|
+
const calculateToolCallsTokens = (toolCalls, encoder, constants) => {
|
|
1659
|
+
let tokens = 0;
|
|
1660
|
+
for (const toolCall of toolCalls) {
|
|
1661
|
+
tokens += constants.funcInit;
|
|
1662
|
+
tokens += encoder.encode(JSON.stringify(toolCall)).length;
|
|
1663
|
+
}
|
|
1664
|
+
tokens += constants.funcEnd;
|
|
1665
|
+
return tokens;
|
|
1666
|
+
};
|
|
1667
|
+
const calculateContentPartsTokens = (contentParts, encoder) => {
|
|
1668
|
+
let tokens = 0;
|
|
1669
|
+
for (const part of contentParts) if (part.type === "image_url") tokens += encoder.encode(part.image_url.url).length + 85;
|
|
1670
|
+
else if (part.text) tokens += encoder.encode(part.text).length;
|
|
1671
|
+
return tokens;
|
|
1672
|
+
};
|
|
1673
|
+
const calculateMessageTokens = (message, encoder, constants) => {
|
|
1674
|
+
const tokensPerMessage = 3;
|
|
1675
|
+
const tokensPerName = 1;
|
|
1676
|
+
let tokens = tokensPerMessage;
|
|
1677
|
+
for (const [key, value] of Object.entries(message)) {
|
|
1678
|
+
if (typeof value === "string") tokens += encoder.encode(value).length;
|
|
1679
|
+
if (key === "name") tokens += tokensPerName;
|
|
1680
|
+
if (key === "tool_calls") tokens += calculateToolCallsTokens(value, encoder, constants);
|
|
1681
|
+
if (key === "content" && Array.isArray(value)) tokens += calculateContentPartsTokens(value, encoder);
|
|
1682
|
+
}
|
|
1683
|
+
return tokens;
|
|
1684
|
+
};
|
|
1685
|
+
const calculateTokens = (messages, encoder, constants) => {
|
|
1686
|
+
if (messages.length === 0) return 0;
|
|
1687
|
+
let numTokens = 0;
|
|
1688
|
+
for (const message of messages) numTokens += calculateMessageTokens(message, encoder, constants);
|
|
1689
|
+
numTokens += 3;
|
|
1690
|
+
return numTokens;
|
|
1691
|
+
};
|
|
1692
|
+
const getEncodeChatFunction = async (encoding) => {
|
|
1693
|
+
if (encodingCache.has(encoding)) {
|
|
1694
|
+
const cached = encodingCache.get(encoding);
|
|
1695
|
+
if (cached) return cached;
|
|
1696
|
+
}
|
|
1697
|
+
const supported = encoding;
|
|
1698
|
+
if (!(supported in ENCODING_MAP)) {
|
|
1699
|
+
const fallback = await ENCODING_MAP.o200k_base();
|
|
1700
|
+
encodingCache.set(encoding, fallback);
|
|
1701
|
+
return fallback;
|
|
1702
|
+
}
|
|
1703
|
+
const mod = await ENCODING_MAP[supported]();
|
|
1704
|
+
encodingCache.set(encoding, mod);
|
|
1705
|
+
return mod;
|
|
1706
|
+
};
|
|
1707
|
+
const getTokenizerFromModel = (model) => model.capabilities.tokenizer || "o200k_base";
|
|
1708
|
+
const getModelConstants = (model) => model.id === "gpt-3.5-turbo" || model.id === "gpt-4" ? {
|
|
1709
|
+
funcInit: 10,
|
|
1710
|
+
propInit: 3,
|
|
1711
|
+
propKey: 3,
|
|
1712
|
+
enumInit: -3,
|
|
1713
|
+
enumItem: 3,
|
|
1714
|
+
funcEnd: 12
|
|
1715
|
+
} : {
|
|
1716
|
+
funcInit: 7,
|
|
1717
|
+
propInit: 3,
|
|
1718
|
+
propKey: 3,
|
|
1719
|
+
enumInit: -3,
|
|
1720
|
+
enumItem: 3,
|
|
1721
|
+
funcEnd: 12
|
|
1722
|
+
};
|
|
1723
|
+
const calculateParameterTokens = (key, prop, context) => {
|
|
1724
|
+
const { encoder, constants } = context;
|
|
1725
|
+
let tokens = constants.propKey;
|
|
1726
|
+
if (typeof prop !== "object" || prop === null) return tokens;
|
|
1727
|
+
const param = prop;
|
|
1728
|
+
const paramName = key;
|
|
1729
|
+
const paramType = param.type || "string";
|
|
1730
|
+
let paramDesc = param.description || "";
|
|
1731
|
+
if (param.enum && Array.isArray(param.enum)) {
|
|
1732
|
+
tokens += constants.enumInit;
|
|
1733
|
+
for (const item of param.enum) {
|
|
1734
|
+
tokens += constants.enumItem;
|
|
1735
|
+
tokens += encoder.encode(String(item)).length;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
if (paramDesc.endsWith(".")) paramDesc = paramDesc.slice(0, -1);
|
|
1739
|
+
const line = `${paramName}:${paramType}:${paramDesc}`;
|
|
1740
|
+
tokens += encoder.encode(line).length;
|
|
1741
|
+
const excludedKeys = new Set([
|
|
1742
|
+
"type",
|
|
1743
|
+
"description",
|
|
1744
|
+
"enum"
|
|
1745
|
+
]);
|
|
1746
|
+
for (const propertyName of Object.keys(param)) {
|
|
1747
|
+
if (excludedKeys.has(propertyName)) continue;
|
|
1748
|
+
const propertyValue = param[propertyName];
|
|
1749
|
+
const propertyText = typeof propertyValue === "string" ? propertyValue : JSON.stringify(propertyValue);
|
|
1750
|
+
tokens += encoder.encode(`${propertyName}:${propertyText}`).length;
|
|
1751
|
+
}
|
|
1752
|
+
return tokens;
|
|
1753
|
+
};
|
|
1754
|
+
const calculateParametersTokens = (parameters, encoder, constants) => {
|
|
1755
|
+
if (!parameters || typeof parameters !== "object") return 0;
|
|
1756
|
+
const params = parameters;
|
|
1757
|
+
let tokens = 0;
|
|
1758
|
+
for (const [key, value] of Object.entries(params)) if (key === "properties") {
|
|
1759
|
+
const properties = value;
|
|
1760
|
+
if (Object.keys(properties).length > 0) {
|
|
1761
|
+
tokens += constants.propInit;
|
|
1762
|
+
for (const propKey of Object.keys(properties)) tokens += calculateParameterTokens(propKey, properties[propKey], {
|
|
1763
|
+
encoder,
|
|
1764
|
+
constants
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
} else {
|
|
1768
|
+
const paramText = typeof value === "string" ? value : JSON.stringify(value);
|
|
1769
|
+
tokens += encoder.encode(`${key}:${paramText}`).length;
|
|
1770
|
+
}
|
|
1771
|
+
return tokens;
|
|
1772
|
+
};
|
|
1773
|
+
const calculateToolTokens = (tool, encoder, constants) => {
|
|
1774
|
+
let tokens = constants.funcInit;
|
|
1775
|
+
const func = tool.function;
|
|
1776
|
+
const fName = func.name;
|
|
1777
|
+
let fDesc = func.description || "";
|
|
1778
|
+
if (fDesc.endsWith(".")) fDesc = fDesc.slice(0, -1);
|
|
1779
|
+
tokens += encoder.encode(`${fName}:${fDesc}`).length;
|
|
1780
|
+
if (typeof func.parameters === "object" && func.parameters !== null) tokens += calculateParametersTokens(func.parameters, encoder, constants);
|
|
1781
|
+
return tokens;
|
|
1782
|
+
};
|
|
1783
|
+
const numTokensForTools = (tools, encoder, constants) => {
|
|
1784
|
+
let funcTokenCount = 0;
|
|
1785
|
+
for (const tool of tools) funcTokenCount += calculateToolTokens(tool, encoder, constants);
|
|
1786
|
+
funcTokenCount += constants.funcEnd;
|
|
1787
|
+
return funcTokenCount;
|
|
1788
|
+
};
|
|
1789
|
+
const getTokenCount = async (payload, model) => {
|
|
1790
|
+
const encoder = await getEncodeChatFunction(getTokenizerFromModel(model));
|
|
1791
|
+
const inputMessages = payload.messages.filter((msg) => msg.role !== "assistant");
|
|
1792
|
+
const outputMessages = payload.messages.filter((msg) => msg.role === "assistant");
|
|
1793
|
+
const constants = getModelConstants(model);
|
|
1794
|
+
let inputTokens = calculateTokens(inputMessages, encoder, constants);
|
|
1795
|
+
if (payload.tools && payload.tools.length > 0) inputTokens += numTokensForTools(payload.tools, encoder, constants);
|
|
1796
|
+
const outputTokens = calculateTokens(outputMessages, encoder, constants);
|
|
1797
|
+
return {
|
|
1798
|
+
input: inputTokens,
|
|
1799
|
+
output: outputTokens
|
|
1800
|
+
};
|
|
1801
|
+
};
|
|
1802
|
+
|
|
1803
|
+
//#endregion
|
|
1804
|
+
//#region src/routes/messages.ts
|
|
1805
|
+
const messageRoutes = new Hono();
|
|
1806
|
+
const isNonStreamingResponse = (response) => typeof response === "object" && response !== null && Object.hasOwn(response, "choices");
|
|
1807
|
+
messageRoutes.post("/", async (c) => {
|
|
1808
|
+
const config = c.get("config");
|
|
1809
|
+
try {
|
|
1810
|
+
await checkRateLimit();
|
|
1811
|
+
} catch (error) {
|
|
1812
|
+
if (error instanceof RateLimitError) return c.json({ error: { message: error.message } }, 429);
|
|
1813
|
+
throw error;
|
|
1814
|
+
}
|
|
1815
|
+
const openAIPayload = translateToOpenAI(await c.req.json());
|
|
1816
|
+
try {
|
|
1817
|
+
const response = await createChatCompletions(config, openAIPayload);
|
|
1818
|
+
if (isNonStreamingResponse(response)) return c.json(translateToAnthropic(response));
|
|
1819
|
+
return streamSSE(c, async (stream) => {
|
|
1820
|
+
const streamState = {
|
|
1821
|
+
messageStartSent: false,
|
|
1822
|
+
contentBlockIndex: 0,
|
|
1823
|
+
contentBlockOpen: false,
|
|
1824
|
+
toolCalls: {}
|
|
1825
|
+
};
|
|
1826
|
+
try {
|
|
1827
|
+
for await (const rawEvent of response) {
|
|
1828
|
+
if (rawEvent.data === "[DONE]") break;
|
|
1829
|
+
if (!rawEvent.data) continue;
|
|
1830
|
+
let chunk;
|
|
1831
|
+
try {
|
|
1832
|
+
chunk = JSON.parse(rawEvent.data);
|
|
1833
|
+
} catch {
|
|
1834
|
+
continue;
|
|
1835
|
+
}
|
|
1836
|
+
const events$1 = translateChunkToAnthropicEvents(chunk, streamState);
|
|
1837
|
+
for (const event of events$1) await stream.writeSSE({
|
|
1838
|
+
event: event.type,
|
|
1839
|
+
data: JSON.stringify(event)
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
} catch (error) {
|
|
1843
|
+
consola.error("Error during Anthropic stream translation:", error);
|
|
1844
|
+
const errorEvent = translateErrorToAnthropicErrorEvent();
|
|
1845
|
+
await stream.writeSSE({
|
|
1846
|
+
event: errorEvent.type,
|
|
1847
|
+
data: JSON.stringify(errorEvent)
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
});
|
|
1851
|
+
} catch (error) {
|
|
1852
|
+
if (error instanceof BridgeNotImplementedError) return c.json({ error: {
|
|
1853
|
+
type: error.name,
|
|
1854
|
+
message: error.message
|
|
1855
|
+
} }, 501);
|
|
1856
|
+
if (error instanceof HTTPError) {
|
|
1857
|
+
const text = await error.response.text().catch(() => "");
|
|
1858
|
+
return new Response(text, {
|
|
1859
|
+
status: error.response.status,
|
|
1860
|
+
headers: { "content-type": error.response.headers.get("content-type") ?? "application/json" }
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
throw error;
|
|
1864
|
+
}
|
|
1865
|
+
});
|
|
1866
|
+
messageRoutes.post("/count_tokens", async (c) => {
|
|
1867
|
+
try {
|
|
1868
|
+
const anthropicBeta = c.req.header("anthropic-beta");
|
|
1869
|
+
const anthropicPayload = await c.req.json();
|
|
1870
|
+
const openAIPayload = translateToOpenAI(anthropicPayload);
|
|
1871
|
+
const selectedModel = resolveModel(openAIPayload.model);
|
|
1872
|
+
if (!selectedModel) {
|
|
1873
|
+
consola.warn(`Model ${openAIPayload.model} not found in registry, returning fallback token count`);
|
|
1874
|
+
return c.json({ input_tokens: 1 });
|
|
1875
|
+
}
|
|
1876
|
+
const tokenCount = await getTokenCount(openAIPayload, selectedModel);
|
|
1877
|
+
const effectiveModelId = selectedModel.id;
|
|
1878
|
+
if (anthropicPayload.tools && anthropicPayload.tools.length > 0) {
|
|
1879
|
+
let mcpToolExist = false;
|
|
1880
|
+
if (anthropicBeta?.startsWith("claude-code")) mcpToolExist = anthropicPayload.tools.some((tool) => tool.name.startsWith("mcp__"));
|
|
1881
|
+
if (!mcpToolExist) {
|
|
1882
|
+
if (effectiveModelId.startsWith("claude")) tokenCount.input += 346;
|
|
1883
|
+
else if (effectiveModelId.startsWith("grok")) tokenCount.input += 480;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
let finalTokenCount = tokenCount.input + tokenCount.output;
|
|
1887
|
+
if (effectiveModelId.startsWith("claude")) finalTokenCount = Math.round(finalTokenCount * 1.15);
|
|
1888
|
+
else if (effectiveModelId.startsWith("grok")) finalTokenCount = Math.round(finalTokenCount * 1.03);
|
|
1889
|
+
return c.json({ input_tokens: Math.max(1, finalTokenCount) });
|
|
1890
|
+
} catch (error) {
|
|
1891
|
+
consola.error("Error counting tokens:", error);
|
|
1892
|
+
return c.json({ input_tokens: 1 });
|
|
1893
|
+
}
|
|
1894
|
+
});
|
|
1895
|
+
|
|
1896
|
+
//#endregion
|
|
1897
|
+
//#region src/routes/models.ts
|
|
1898
|
+
const modelRoutes = new Hono();
|
|
1899
|
+
modelRoutes.get("/", async (c) => {
|
|
1900
|
+
const provider = getCopilotProviderContext(c.get("config"));
|
|
1901
|
+
const search = new URL(c.req.url).search;
|
|
1902
|
+
try {
|
|
1903
|
+
const upstream = await fetchCopilot(provider, `/models${search}`, {
|
|
1904
|
+
method: "GET",
|
|
1905
|
+
headers: { accept: c.req.header("accept") ?? "application/json" }
|
|
1906
|
+
});
|
|
1907
|
+
return new Response(upstream.body, {
|
|
1908
|
+
status: upstream.status,
|
|
1909
|
+
headers: upstream.headers
|
|
1910
|
+
});
|
|
1911
|
+
} catch (error) {
|
|
1912
|
+
if (error instanceof BridgeNotImplementedError) return c.json({ error: {
|
|
1913
|
+
type: error.name,
|
|
1914
|
+
message: error.message
|
|
1915
|
+
} }, 500);
|
|
1916
|
+
throw error;
|
|
1917
|
+
}
|
|
1918
|
+
});
|
|
1919
|
+
|
|
1920
|
+
//#endregion
|
|
1921
|
+
//#region src/bridges/codex/chat-fallback.ts
|
|
1922
|
+
const collectText = (content) => {
|
|
1923
|
+
if (typeof content === "string") return {
|
|
1924
|
+
text: content,
|
|
1925
|
+
parts: [{
|
|
1926
|
+
type: "text",
|
|
1927
|
+
text: content
|
|
1928
|
+
}]
|
|
1929
|
+
};
|
|
1930
|
+
const parts = [];
|
|
1931
|
+
let textBuf = "";
|
|
1932
|
+
for (const p of content ?? []) if (p.type === "input_text" || p.type === "output_text" || p.type === "text") {
|
|
1933
|
+
const t = p.text ?? "";
|
|
1934
|
+
textBuf += t;
|
|
1935
|
+
parts.push({
|
|
1936
|
+
type: "text",
|
|
1937
|
+
text: t
|
|
1938
|
+
});
|
|
1939
|
+
} else if (p.type === "input_image") {
|
|
1940
|
+
const url = typeof p.image_url === "string" ? p.image_url : p.image_url?.url ?? "";
|
|
1941
|
+
if (url) parts.push({
|
|
1942
|
+
type: "image_url",
|
|
1943
|
+
image_url: {
|
|
1944
|
+
url,
|
|
1945
|
+
detail: p.detail
|
|
1946
|
+
}
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
return {
|
|
1950
|
+
text: textBuf,
|
|
1951
|
+
parts
|
|
1952
|
+
};
|
|
1953
|
+
};
|
|
1954
|
+
const placeReasoning = (payload, capability, effort) => {
|
|
1955
|
+
if (!effort || !capability.reasoning) return;
|
|
1956
|
+
if (capability.reasoningField === "output_config.effort") payload.output_config = {
|
|
1957
|
+
...payload.output_config ?? {},
|
|
1958
|
+
effort
|
|
1959
|
+
};
|
|
1960
|
+
else payload.reasoning_effort = effort;
|
|
1961
|
+
};
|
|
1962
|
+
const responsesPayloadToChatPayload = (request, capability) => {
|
|
1963
|
+
const messages = [];
|
|
1964
|
+
if (request.instructions) messages.push({
|
|
1965
|
+
role: "system",
|
|
1966
|
+
content: request.instructions
|
|
1967
|
+
});
|
|
1968
|
+
const inputItems = typeof request.input === "string" ? [{
|
|
1969
|
+
role: "user",
|
|
1970
|
+
content: request.input
|
|
1971
|
+
}] : request.input ?? [];
|
|
1972
|
+
for (const item of inputItems) if ("role" in item && (item.type === void 0 || item.type === "message")) {
|
|
1973
|
+
const { text, parts } = collectText(item.content);
|
|
1974
|
+
const role = item.role === "developer" ? "system" : item.role === "system" ? "system" : item.role === "assistant" ? "assistant" : "user";
|
|
1975
|
+
const useStringContent = parts.length <= 1 && parts.every((p) => p.type === "text");
|
|
1976
|
+
messages.push({
|
|
1977
|
+
role,
|
|
1978
|
+
content: useStringContent ? text : parts
|
|
1979
|
+
});
|
|
1980
|
+
} else if (item.type === "function_call") messages.push({
|
|
1981
|
+
role: "assistant",
|
|
1982
|
+
content: null,
|
|
1983
|
+
tool_calls: [{
|
|
1984
|
+
id: item.call_id,
|
|
1985
|
+
type: "function",
|
|
1986
|
+
function: {
|
|
1987
|
+
name: item.name,
|
|
1988
|
+
arguments: item.arguments
|
|
1989
|
+
}
|
|
1990
|
+
}]
|
|
1991
|
+
});
|
|
1992
|
+
else if (item.type === "function_call_output") messages.push({
|
|
1993
|
+
role: "tool",
|
|
1994
|
+
tool_call_id: item.call_id,
|
|
1995
|
+
content: item.output
|
|
1996
|
+
});
|
|
1997
|
+
const tools = request.tools?.filter((t) => {
|
|
1998
|
+
const name = t.function?.name ?? t.name;
|
|
1999
|
+
return t.type === "function" && typeof name === "string" && name.length > 0;
|
|
2000
|
+
}).map((t) => ({
|
|
2001
|
+
type: "function",
|
|
2002
|
+
function: {
|
|
2003
|
+
name: t.function?.name ?? t.name ?? "",
|
|
2004
|
+
description: t.function?.description ?? t.description,
|
|
2005
|
+
parameters: t.function?.parameters ?? t.parameters
|
|
2006
|
+
}
|
|
2007
|
+
}));
|
|
2008
|
+
const payload = {
|
|
2009
|
+
model: capability.id,
|
|
2010
|
+
messages,
|
|
2011
|
+
stream: false,
|
|
2012
|
+
max_tokens: request.max_output_tokens,
|
|
2013
|
+
temperature: request.temperature,
|
|
2014
|
+
top_p: request.top_p,
|
|
2015
|
+
user: request.user,
|
|
2016
|
+
tools: tools && tools.length > 0 ? tools : void 0,
|
|
2017
|
+
tool_choice: tools && tools.length > 0 ? request.tool_choice : void 0
|
|
2018
|
+
};
|
|
2019
|
+
placeReasoning(payload, capability, request.reasoning?.effort);
|
|
2020
|
+
return payload;
|
|
2021
|
+
};
|
|
2022
|
+
const buildResponsesResult = (request, chat) => {
|
|
2023
|
+
const message = chat.choices[0]?.message;
|
|
2024
|
+
const text = typeof message?.content === "string" ? message.content : Array.isArray(message?.content) ? message.content.filter((p) => p.type === "text").map((p) => p.text ?? "").join("") : "";
|
|
2025
|
+
const output = [];
|
|
2026
|
+
if (text.length > 0) output.push({
|
|
2027
|
+
id: `msg_${randomUUID()}`,
|
|
2028
|
+
type: "message",
|
|
2029
|
+
role: "assistant",
|
|
2030
|
+
status: "completed",
|
|
2031
|
+
content: [{
|
|
2032
|
+
type: "output_text",
|
|
2033
|
+
text,
|
|
2034
|
+
annotations: []
|
|
2035
|
+
}]
|
|
2036
|
+
});
|
|
2037
|
+
for (const tc of message?.tool_calls ?? []) output.push({
|
|
2038
|
+
id: `fc_${randomUUID()}`,
|
|
2039
|
+
type: "function_call",
|
|
2040
|
+
status: "completed",
|
|
2041
|
+
call_id: tc.id,
|
|
2042
|
+
name: tc.function.name,
|
|
2043
|
+
arguments: tc.function.arguments
|
|
2044
|
+
});
|
|
2045
|
+
return {
|
|
2046
|
+
id: chat.id ?? `resp_${randomUUID()}`,
|
|
2047
|
+
object: "response",
|
|
2048
|
+
status: "completed",
|
|
2049
|
+
created_at: chat.created ?? Math.floor(Date.now() / 1e3),
|
|
2050
|
+
model: chat.model ?? request.model,
|
|
2051
|
+
output,
|
|
2052
|
+
usage: chat.usage ? {
|
|
2053
|
+
input_tokens: chat.usage.prompt_tokens ?? 0,
|
|
2054
|
+
output_tokens: chat.usage.completion_tokens ?? 0,
|
|
2055
|
+
total_tokens: chat.usage.total_tokens ?? 0
|
|
2056
|
+
} : void 0
|
|
2057
|
+
};
|
|
2058
|
+
};
|
|
2059
|
+
const chatResponseToResponsesJson = buildResponsesResult;
|
|
2060
|
+
const sse = (event, data) => `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
2061
|
+
const synthesizeResponsesSseFromChat = (request, chat) => {
|
|
2062
|
+
const result = buildResponsesResult(request, chat);
|
|
2063
|
+
const encoder = new TextEncoder();
|
|
2064
|
+
const baseResponse = {
|
|
2065
|
+
id: result.id,
|
|
2066
|
+
object: "response",
|
|
2067
|
+
created_at: result.created_at,
|
|
2068
|
+
model: result.model,
|
|
2069
|
+
status: "in_progress",
|
|
2070
|
+
output: []
|
|
2071
|
+
};
|
|
2072
|
+
return new ReadableStream({ start(controller) {
|
|
2073
|
+
const push = (event, data) => controller.enqueue(encoder.encode(sse(event, data)));
|
|
2074
|
+
push("response.created", {
|
|
2075
|
+
type: "response.created",
|
|
2076
|
+
response: baseResponse
|
|
2077
|
+
});
|
|
2078
|
+
push("response.in_progress", {
|
|
2079
|
+
type: "response.in_progress",
|
|
2080
|
+
response: baseResponse
|
|
2081
|
+
});
|
|
2082
|
+
result.output.forEach((item, index) => {
|
|
2083
|
+
push("response.output_item.added", {
|
|
2084
|
+
type: "response.output_item.added",
|
|
2085
|
+
output_index: index,
|
|
2086
|
+
item
|
|
2087
|
+
});
|
|
2088
|
+
if (item.type === "message") {
|
|
2089
|
+
const text = item.content[0]?.text ?? "";
|
|
2090
|
+
const part = {
|
|
2091
|
+
type: "output_text",
|
|
2092
|
+
text: "",
|
|
2093
|
+
annotations: []
|
|
2094
|
+
};
|
|
2095
|
+
push("response.content_part.added", {
|
|
2096
|
+
type: "response.content_part.added",
|
|
2097
|
+
item_id: item.id,
|
|
2098
|
+
output_index: index,
|
|
2099
|
+
content_index: 0,
|
|
2100
|
+
part
|
|
2101
|
+
});
|
|
2102
|
+
if (text) push("response.output_text.delta", {
|
|
2103
|
+
type: "response.output_text.delta",
|
|
2104
|
+
item_id: item.id,
|
|
2105
|
+
output_index: index,
|
|
2106
|
+
content_index: 0,
|
|
2107
|
+
delta: text
|
|
2108
|
+
});
|
|
2109
|
+
push("response.output_text.done", {
|
|
2110
|
+
type: "response.output_text.done",
|
|
2111
|
+
item_id: item.id,
|
|
2112
|
+
output_index: index,
|
|
2113
|
+
content_index: 0,
|
|
2114
|
+
text
|
|
2115
|
+
});
|
|
2116
|
+
push("response.content_part.done", {
|
|
2117
|
+
type: "response.content_part.done",
|
|
2118
|
+
item_id: item.id,
|
|
2119
|
+
output_index: index,
|
|
2120
|
+
content_index: 0,
|
|
2121
|
+
part: {
|
|
2122
|
+
...part,
|
|
2123
|
+
text
|
|
2124
|
+
}
|
|
2125
|
+
});
|
|
2126
|
+
} else if (item.type === "function_call") {
|
|
2127
|
+
if (item.arguments) push("response.function_call_arguments.delta", {
|
|
2128
|
+
type: "response.function_call_arguments.delta",
|
|
2129
|
+
item_id: item.id,
|
|
2130
|
+
output_index: index,
|
|
2131
|
+
delta: item.arguments
|
|
2132
|
+
});
|
|
2133
|
+
push("response.function_call_arguments.done", {
|
|
2134
|
+
type: "response.function_call_arguments.done",
|
|
2135
|
+
item_id: item.id,
|
|
2136
|
+
output_index: index,
|
|
2137
|
+
arguments: item.arguments
|
|
2138
|
+
});
|
|
2139
|
+
}
|
|
2140
|
+
push("response.output_item.done", {
|
|
2141
|
+
type: "response.output_item.done",
|
|
2142
|
+
output_index: index,
|
|
2143
|
+
item
|
|
2144
|
+
});
|
|
2145
|
+
});
|
|
2146
|
+
push("response.completed", {
|
|
2147
|
+
type: "response.completed",
|
|
2148
|
+
response: {
|
|
2149
|
+
...result,
|
|
2150
|
+
status: "completed"
|
|
2151
|
+
}
|
|
2152
|
+
});
|
|
2153
|
+
controller.close();
|
|
2154
|
+
} });
|
|
2155
|
+
};
|
|
2156
|
+
|
|
2157
|
+
//#endregion
|
|
2158
|
+
//#region src/bridges/codex/normalize-stream.ts
|
|
2159
|
+
const normalizeEventPayload = (rawData, stableResponse) => {
|
|
2160
|
+
let event;
|
|
2161
|
+
try {
|
|
2162
|
+
event = JSON.parse(rawData);
|
|
2163
|
+
} catch {
|
|
2164
|
+
return rawData;
|
|
2165
|
+
}
|
|
2166
|
+
if (event.response && typeof event.response === "object") {
|
|
2167
|
+
const incoming = event.response;
|
|
2168
|
+
if (!stableResponse.initialized) {
|
|
2169
|
+
if (incoming.id) stableResponse.id = incoming.id;
|
|
2170
|
+
if (typeof incoming.created_at === "number") stableResponse.created_at = incoming.created_at;
|
|
2171
|
+
if (incoming.model) stableResponse.model = incoming.model;
|
|
2172
|
+
stableResponse.initialized = true;
|
|
2173
|
+
}
|
|
2174
|
+
event.response = {
|
|
2175
|
+
...incoming,
|
|
2176
|
+
...stableResponse.id ? { id: stableResponse.id } : {},
|
|
2177
|
+
...stableResponse.created_at ? { created_at: stableResponse.created_at } : {},
|
|
2178
|
+
...stableResponse.model ? { model: stableResponse.model } : {}
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
return JSON.stringify(event);
|
|
2182
|
+
};
|
|
2183
|
+
const transformSseChunk = (chunk, stableResponse) => {
|
|
2184
|
+
return chunk.split("\n").map((line) => {
|
|
2185
|
+
if (!line.startsWith("data: ")) return line;
|
|
2186
|
+
const rawData = line.slice(6);
|
|
2187
|
+
if (!rawData || rawData === "[DONE]") return line;
|
|
2188
|
+
return `data: ${normalizeEventPayload(rawData, stableResponse)}`;
|
|
2189
|
+
}).join("\n");
|
|
2190
|
+
};
|
|
2191
|
+
const normalizeResponsesSseStream = (upstreamBody) => {
|
|
2192
|
+
const stableResponse = {
|
|
2193
|
+
created_at: 0,
|
|
2194
|
+
id: "",
|
|
2195
|
+
initialized: false,
|
|
2196
|
+
model: ""
|
|
2197
|
+
};
|
|
2198
|
+
const decoder = new TextDecoder();
|
|
2199
|
+
const encoder = new TextEncoder();
|
|
2200
|
+
let buffer = "";
|
|
2201
|
+
return new ReadableStream({ async start(controller) {
|
|
2202
|
+
const reader = upstreamBody.getReader();
|
|
2203
|
+
try {
|
|
2204
|
+
while (true) {
|
|
2205
|
+
const { done, value } = await reader.read();
|
|
2206
|
+
if (done) break;
|
|
2207
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2208
|
+
let separatorIndex = buffer.indexOf("\n\n");
|
|
2209
|
+
while (separatorIndex !== -1) {
|
|
2210
|
+
const rawEvent = buffer.slice(0, separatorIndex);
|
|
2211
|
+
buffer = buffer.slice(separatorIndex + 2);
|
|
2212
|
+
const transformed = transformSseChunk(rawEvent, stableResponse);
|
|
2213
|
+
controller.enqueue(encoder.encode(`${transformed}\n\n`));
|
|
2214
|
+
separatorIndex = buffer.indexOf("\n\n");
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
if (buffer.length > 0) {
|
|
2218
|
+
const transformed = transformSseChunk(buffer, stableResponse);
|
|
2219
|
+
controller.enqueue(encoder.encode(transformed));
|
|
2220
|
+
}
|
|
2221
|
+
} finally {
|
|
2222
|
+
reader.releaseLock();
|
|
2223
|
+
}
|
|
2224
|
+
controller.close();
|
|
2225
|
+
} });
|
|
2226
|
+
};
|
|
2227
|
+
|
|
2228
|
+
//#endregion
|
|
2229
|
+
//#region src/bridges/codex/responses.ts
|
|
2230
|
+
const codexResponsesRequestSchema = z.object({
|
|
2231
|
+
model: z.string().min(1),
|
|
2232
|
+
stream: z.boolean().optional()
|
|
2233
|
+
}).passthrough();
|
|
2234
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2235
|
+
const normalizeCodexResponsesRequest = (payload) => {
|
|
2236
|
+
const parsed = codexResponsesRequestSchema.parse(payload);
|
|
2237
|
+
const canonical = resolveModelId(parsed.model);
|
|
2238
|
+
const capability = getModelCapability(canonical);
|
|
2239
|
+
if (!capability) return parsed;
|
|
2240
|
+
const next = {
|
|
2241
|
+
...parsed,
|
|
2242
|
+
model: canonical
|
|
2243
|
+
};
|
|
2244
|
+
if (!capability.reasoning) {
|
|
2245
|
+
if ("reasoning" in next) delete next.reasoning;
|
|
2246
|
+
return next;
|
|
2247
|
+
}
|
|
2248
|
+
const incoming = isPlainObject(next.reasoning) ? next.reasoning : void 0;
|
|
2249
|
+
const clamped = clampReasoningEffort(canonical, incoming?.effort);
|
|
2250
|
+
if (clamped) next.reasoning = {
|
|
2251
|
+
...incoming ?? {},
|
|
2252
|
+
effort: clamped.effort
|
|
2253
|
+
};
|
|
2254
|
+
return next;
|
|
2255
|
+
};
|
|
2256
|
+
|
|
2257
|
+
//#endregion
|
|
2258
|
+
//#region src/routes/responses.ts
|
|
2259
|
+
const responsesRoutes = new Hono();
|
|
2260
|
+
responsesRoutes.post("/", async (c) => {
|
|
2261
|
+
try {
|
|
2262
|
+
await checkRateLimit();
|
|
2263
|
+
} catch (error) {
|
|
2264
|
+
if (error instanceof RateLimitError) return c.json({ error: { message: error.message } }, 429);
|
|
2265
|
+
throw error;
|
|
2266
|
+
}
|
|
2267
|
+
const payload = normalizeCodexResponsesRequest(await c.req.json());
|
|
2268
|
+
const provider = getCopilotProviderContext(c.get("config"));
|
|
2269
|
+
const search = new URL(c.req.url).search;
|
|
2270
|
+
const capability = getModelCapability(payload.model);
|
|
2271
|
+
try {
|
|
2272
|
+
if (capability?.fallback === "chat-completions") {
|
|
2273
|
+
const chatPayload = responsesPayloadToChatPayload(payload, capability);
|
|
2274
|
+
const upstream$1 = await fetchCopilot(provider, `/chat/completions${search}`, {
|
|
2275
|
+
method: "POST",
|
|
2276
|
+
headers: {
|
|
2277
|
+
accept: "application/json",
|
|
2278
|
+
"content-type": "application/json"
|
|
2279
|
+
},
|
|
2280
|
+
body: JSON.stringify(chatPayload)
|
|
2281
|
+
});
|
|
2282
|
+
if (!upstream$1.ok) {
|
|
2283
|
+
const text = await upstream$1.text();
|
|
2284
|
+
return new Response(text, {
|
|
2285
|
+
status: upstream$1.status,
|
|
2286
|
+
headers: { "content-type": upstream$1.headers.get("content-type") ?? "application/json" }
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
const chatJson = await upstream$1.json();
|
|
2290
|
+
if (payload.stream) {
|
|
2291
|
+
const stream = synthesizeResponsesSseFromChat(payload, chatJson);
|
|
2292
|
+
return new Response(stream, {
|
|
2293
|
+
status: 200,
|
|
2294
|
+
headers: {
|
|
2295
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
2296
|
+
"cache-control": "no-cache",
|
|
2297
|
+
connection: "keep-alive"
|
|
2298
|
+
}
|
|
2299
|
+
});
|
|
2300
|
+
}
|
|
2301
|
+
const responsesJson = chatResponseToResponsesJson(payload, chatJson);
|
|
2302
|
+
return new Response(JSON.stringify(responsesJson), {
|
|
2303
|
+
status: 200,
|
|
2304
|
+
headers: { "content-type": "application/json" }
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
2307
|
+
const upstream = await fetchCopilot(provider, `/responses${search}`, {
|
|
2308
|
+
method: "POST",
|
|
2309
|
+
headers: {
|
|
2310
|
+
accept: c.req.header("accept") ?? "application/json",
|
|
2311
|
+
"content-type": c.req.header("content-type") ?? "application/json"
|
|
2312
|
+
},
|
|
2313
|
+
body: JSON.stringify(payload)
|
|
2314
|
+
});
|
|
2315
|
+
const contentType = upstream.headers.get("content-type") ?? "";
|
|
2316
|
+
if (payload.stream && upstream.body && contentType.includes("text/event-stream")) return new Response(normalizeResponsesSseStream(upstream.body), {
|
|
2317
|
+
status: upstream.status,
|
|
2318
|
+
headers: upstream.headers
|
|
2319
|
+
});
|
|
2320
|
+
return new Response(upstream.body, {
|
|
2321
|
+
status: upstream.status,
|
|
2322
|
+
headers: upstream.headers
|
|
2323
|
+
});
|
|
2324
|
+
} catch (error) {
|
|
2325
|
+
if (error instanceof BridgeNotImplementedError) return c.json({ error: {
|
|
2326
|
+
type: error.name,
|
|
2327
|
+
message: error.message
|
|
2328
|
+
} }, 501);
|
|
2329
|
+
throw error;
|
|
2330
|
+
}
|
|
2331
|
+
});
|
|
2332
|
+
|
|
2333
|
+
//#endregion
|
|
2334
|
+
//#region src/providers/github/get-copilot-usage.ts
|
|
2335
|
+
const COPILOT_VERSION = "0.26.7";
|
|
2336
|
+
const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`;
|
|
2337
|
+
const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`;
|
|
2338
|
+
const GITHUB_API_VERSION = "2022-11-28";
|
|
2339
|
+
const GITHUB_API_BASE_URL = "https://api.github.com";
|
|
2340
|
+
const readGitHubToken = async () => {
|
|
2341
|
+
return (await fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8")).trim();
|
|
2342
|
+
};
|
|
2343
|
+
const getCopilotUsage = async (config) => {
|
|
2344
|
+
const githubToken = await readGitHubToken();
|
|
2345
|
+
const response = await fetch(`${GITHUB_API_BASE_URL}/copilot_internal/user`, { headers: {
|
|
2346
|
+
accept: "application/json",
|
|
2347
|
+
authorization: `token ${githubToken}`,
|
|
2348
|
+
"editor-plugin-version": EDITOR_PLUGIN_VERSION,
|
|
2349
|
+
"editor-version": `vscode/${config.vsCodeVersion}`,
|
|
2350
|
+
"user-agent": USER_AGENT,
|
|
2351
|
+
"x-github-api-version": GITHUB_API_VERSION,
|
|
2352
|
+
"x-vscode-user-agent-library-version": "electron-fetch"
|
|
2353
|
+
} });
|
|
2354
|
+
if (!response.ok) throw new HTTPError("Failed to get Copilot usage", response);
|
|
2355
|
+
return await response.json();
|
|
2356
|
+
};
|
|
2357
|
+
|
|
2358
|
+
//#endregion
|
|
2359
|
+
//#region src/routes/usage.ts
|
|
2360
|
+
const usageRoutes = new Hono();
|
|
2361
|
+
usageRoutes.get("/", async (c) => {
|
|
2362
|
+
c.header("access-control-allow-origin", "*");
|
|
2363
|
+
c.header("access-control-allow-methods", "GET, OPTIONS");
|
|
2364
|
+
c.header("access-control-allow-headers", "*");
|
|
2365
|
+
try {
|
|
2366
|
+
const usage = await getCopilotUsage(c.var.config);
|
|
2367
|
+
return c.json(usage);
|
|
2368
|
+
} catch (error) {
|
|
2369
|
+
console.error("Error fetching Copilot usage:", error);
|
|
2370
|
+
return c.json({ error: "Failed to fetch Copilot usage" }, 500);
|
|
2371
|
+
}
|
|
2372
|
+
});
|
|
2373
|
+
usageRoutes.options("/", (c) => {
|
|
2374
|
+
c.header("access-control-allow-origin", "*");
|
|
2375
|
+
c.header("access-control-allow-methods", "GET, OPTIONS");
|
|
2376
|
+
c.header("access-control-allow-headers", "*");
|
|
2377
|
+
return c.body(null, 204);
|
|
2378
|
+
});
|
|
2379
|
+
|
|
2380
|
+
//#endregion
|
|
2381
|
+
//#region src/server.ts
|
|
2382
|
+
const createServer = (config) => {
|
|
2383
|
+
const app = new Hono();
|
|
2384
|
+
app.use(logger());
|
|
2385
|
+
app.use("*", cors());
|
|
2386
|
+
app.use("*", async (c, next) => {
|
|
2387
|
+
c.set("config", config);
|
|
2388
|
+
await next();
|
|
2389
|
+
});
|
|
2390
|
+
app.get("/", (c) => c.json({
|
|
2391
|
+
name: "copilot-bridge",
|
|
2392
|
+
status: "ok"
|
|
2393
|
+
}));
|
|
2394
|
+
app.get("/healthz", (c) => c.json({ ok: true }));
|
|
2395
|
+
app.route("/v1/models", modelRoutes);
|
|
2396
|
+
app.route("/v1/responses", responsesRoutes);
|
|
2397
|
+
app.route("/v1/messages", messageRoutes);
|
|
2398
|
+
app.route("/v1/chat/completions", chatCompletionRoutes);
|
|
2399
|
+
app.route("/v1/embeddings", embeddingRoutes);
|
|
2400
|
+
app.route("/usage", usageRoutes);
|
|
2401
|
+
return app;
|
|
2402
|
+
};
|
|
2403
|
+
const startServer = (config) => serve({
|
|
2404
|
+
fetch: createServer(config).fetch,
|
|
2405
|
+
hostname: config.host,
|
|
2406
|
+
port: config.port
|
|
2407
|
+
});
|
|
2408
|
+
|
|
2409
|
+
//#endregion
|
|
2410
|
+
//#region src/start.ts
|
|
2411
|
+
const fetchAvailableModels = async (config) => {
|
|
2412
|
+
try {
|
|
2413
|
+
const response = await fetchCopilot(getCopilotProviderContext(config), "/models", {
|
|
2414
|
+
method: "GET",
|
|
2415
|
+
headers: { accept: "application/json" }
|
|
2416
|
+
});
|
|
2417
|
+
if (!response.ok) return [];
|
|
2418
|
+
return ((await response.json()).data ?? []).map((m) => m.id).filter(Boolean);
|
|
2419
|
+
} catch {
|
|
2420
|
+
return [];
|
|
2421
|
+
}
|
|
2422
|
+
};
|
|
2423
|
+
const start = defineCommand({
|
|
2424
|
+
meta: {
|
|
2425
|
+
name: "start",
|
|
2426
|
+
description: "Start the copilot-bridge HTTP server."
|
|
2427
|
+
},
|
|
2428
|
+
args: {
|
|
2429
|
+
host: {
|
|
2430
|
+
type: "string",
|
|
2431
|
+
description: "Host to bind (overrides settings.json)."
|
|
2432
|
+
},
|
|
2433
|
+
port: {
|
|
2434
|
+
type: "string",
|
|
2435
|
+
description: "Port to listen on (overrides settings.json)."
|
|
2436
|
+
},
|
|
2437
|
+
"show-token": {
|
|
2438
|
+
type: "boolean",
|
|
2439
|
+
default: false,
|
|
2440
|
+
description: "Print GitHub and Copilot tokens during startup."
|
|
2441
|
+
},
|
|
2442
|
+
"no-codex-setup": {
|
|
2443
|
+
type: "boolean",
|
|
2444
|
+
default: false,
|
|
2445
|
+
description: "Skip writing the bridge provider into ~/.codex/config.toml."
|
|
2446
|
+
},
|
|
2447
|
+
"select-model": {
|
|
2448
|
+
type: "boolean",
|
|
2449
|
+
default: false,
|
|
2450
|
+
description: "Force the model picker even when codex.model is set."
|
|
2451
|
+
},
|
|
2452
|
+
"no-prompt": {
|
|
2453
|
+
type: "boolean",
|
|
2454
|
+
default: false,
|
|
2455
|
+
description: "Never prompt; use codex.model from settings.json as-is."
|
|
2456
|
+
},
|
|
2457
|
+
"rate-limit": {
|
|
2458
|
+
type: "string",
|
|
2459
|
+
description: "Minimum seconds between upstream requests (anti-abuse throttle)."
|
|
2460
|
+
},
|
|
2461
|
+
wait: {
|
|
2462
|
+
type: "boolean",
|
|
2463
|
+
default: false,
|
|
2464
|
+
description: "When --rate-limit is set, wait instead of returning HTTP 429."
|
|
2465
|
+
}
|
|
2466
|
+
},
|
|
2467
|
+
async run({ args }) {
|
|
2468
|
+
const settings = await ensureSettingsFile();
|
|
2469
|
+
const config = readBridgeConfig({
|
|
2470
|
+
host: args.host ? String(args.host) : settings.host,
|
|
2471
|
+
port: args.port ? Number(args.port) : settings.port
|
|
2472
|
+
});
|
|
2473
|
+
const rateLimitRaw = args["rate-limit"];
|
|
2474
|
+
if (rateLimitRaw !== void 0) {
|
|
2475
|
+
const parsed = Number.parseInt(String(rateLimitRaw), 10);
|
|
2476
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
2477
|
+
consola.error(`Invalid --rate-limit value: ${rateLimitRaw}`);
|
|
2478
|
+
process.exit(1);
|
|
2479
|
+
}
|
|
2480
|
+
runtimeState.rateLimitSeconds = parsed;
|
|
2481
|
+
runtimeState.rateLimitWait = Boolean(args.wait);
|
|
2482
|
+
consola.info(`Rate limit: ${parsed}s between requests (${runtimeState.rateLimitWait ? "wait" : "reject"} on overflow)`);
|
|
2483
|
+
}
|
|
2484
|
+
await setupBridgeAuth(config, { showToken: args["show-token"] });
|
|
2485
|
+
const server = startServer(config);
|
|
2486
|
+
consola.success(`copilot-bridge listening on http://${config.host}:${config.port}`);
|
|
2487
|
+
consola.info(`copilot base url: ${config.copilotBaseUrl}`);
|
|
2488
|
+
const models = await fetchAvailableModels(config);
|
|
2489
|
+
const supportedIds = new Set(MODEL_CAPABILITIES.map((m) => m.id));
|
|
2490
|
+
const pickable = models.length > 0 ? models.filter((id) => supportedIds.has(id)) : MODEL_CAPABILITIES.map((m) => m.id);
|
|
2491
|
+
const finalPickable = pickable.length > 0 ? pickable : MODEL_CAPABILITIES.map((m) => m.id);
|
|
2492
|
+
if (models.length > 0) consola.info(`Available models:\n${models.map((id) => `- ${id}${supportedIds.has(id) ? " (bridge-supported)" : ""}`).join("\n")}`);
|
|
2493
|
+
else consola.warn("Could not fetch model list from upstream Copilot API");
|
|
2494
|
+
const codexUserConfig = await readCodexUserConfigFromDisk(settings.codex.configPath);
|
|
2495
|
+
let chosenModel = codexUserConfig.model;
|
|
2496
|
+
let chosenEffort = codexUserConfig.modelReasoningEffort;
|
|
2497
|
+
if (!args["no-prompt"] && finalPickable.length > 0 && (args["select-model"] || !chosenModel)) {
|
|
2498
|
+
const defaultId = chosenModel && finalPickable.includes(chosenModel) ? chosenModel : finalPickable.includes("gpt-5.3-codex") ? "gpt-5.3-codex" : finalPickable[0];
|
|
2499
|
+
const selected = await consola.prompt(`Select a model for codex (writes to ${settings.codex.configPath})`, {
|
|
2500
|
+
type: "select",
|
|
2501
|
+
options: finalPickable,
|
|
2502
|
+
initial: defaultId
|
|
2503
|
+
});
|
|
2504
|
+
if (selected) {
|
|
2505
|
+
chosenModel = selected;
|
|
2506
|
+
const supported = getModelCapability(selected)?.reasoning?.supported;
|
|
2507
|
+
if (chosenEffort && supported && !supported.includes(chosenEffort)) chosenEffort = void 0;
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
const baseUrl = `http://${config.host}:${config.port}`;
|
|
2511
|
+
consola.box([
|
|
2512
|
+
`🌐 Usage viewer`,
|
|
2513
|
+
``,
|
|
2514
|
+
` https://betahi.github.io/copilot-api?endpoint=${baseUrl}/usage`
|
|
2515
|
+
].join("\n"));
|
|
2516
|
+
if (settings.codex.enabled && !args["no-codex-setup"]) {
|
|
2517
|
+
const result = await applyCodexConfig({
|
|
2518
|
+
baseUrl: `${baseUrl}/v1`,
|
|
2519
|
+
configPath: settings.codex.configPath,
|
|
2520
|
+
settings: settings.codex,
|
|
2521
|
+
model: chosenModel,
|
|
2522
|
+
modelReasoningEffort: chosenEffort
|
|
2523
|
+
});
|
|
2524
|
+
if (result.changed) {
|
|
2525
|
+
consola.success(`codex config ${result.created ? "created" : "updated"}: ${result.configPath}`);
|
|
2526
|
+
if (settings.codex.setAsDefault) consola.info(`codex now defaults to provider "${settings.codex.providerId}". Run \`codex exec "..."\` directly.`);
|
|
2527
|
+
else consola.info(`provider "${settings.codex.providerId}" registered. Use \`codex -c model_provider="${settings.codex.providerId}" ...\``);
|
|
2528
|
+
} else consola.info(`codex config already up to date: ${result.configPath}`);
|
|
2529
|
+
}
|
|
2530
|
+
await new Promise((resolve, reject) => {
|
|
2531
|
+
server.on("close", resolve);
|
|
2532
|
+
server.on("error", reject);
|
|
2533
|
+
});
|
|
2534
|
+
}
|
|
2535
|
+
});
|
|
2536
|
+
|
|
2537
|
+
//#endregion
|
|
2538
|
+
//#region src/main.ts
|
|
2539
|
+
await runMain(defineCommand({
|
|
2540
|
+
meta: {
|
|
2541
|
+
name: "copilot-bridge",
|
|
2542
|
+
description: "Model-layer bridge for routing Codex CLI, Claude Code, and similar clients to GitHub Copilot."
|
|
2543
|
+
},
|
|
2544
|
+
subCommands: {
|
|
2545
|
+
auth,
|
|
2546
|
+
start
|
|
2547
|
+
}
|
|
2548
|
+
}));
|
|
2549
|
+
|
|
2550
|
+
//#endregion
|
|
2551
|
+
export { };
|
|
2552
|
+
//# sourceMappingURL=main.js.map
|