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/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