xiaodcs-copilot-api 2.3.9-public.3 → 2.5.1-public.2
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/README.md +46 -17
- package/README.zh-CN.md +46 -17
- package/dist/{auth-BiVetBYt.js → auth-D-M3qCeV.js} +79 -16
- package/dist/auth-DIGdDJx3.js +2 -0
- package/dist/{config-SytZjLq8.js → config-DnMKoKrg.js} +43 -29
- package/dist/{debug-BFadhEB4.js → debug-CuLfQpTq.js} +3 -3
- package/dist/main.js +6 -6
- package/dist/{mcp-fpSlKZxK.js → mcp-DqslOG3W.js} +1 -1
- package/dist/{mcp-server-BeNu_Edl.js → mcp-server-B-_dWuXe.js} +1 -1
- package/dist/mcp-server-Byq5DomJ.js +2 -0
- package/dist/protocol-BTFLbI8m.js +257 -0
- package/dist/provider-proxy-BbLwvtD_.js +263 -0
- package/dist/runtime-CWU5BNBf.js +442 -0
- package/dist/server-DmKNMBLd.js +13628 -0
- package/dist/{start-FFVCi8su.js → start-WdcRgBSL.js} +110 -11
- package/dist/{token-D9svRIYW.js → token-CQftvMU4.js} +48 -31
- package/dist/transport-DE8Q0wU-.js +252 -0
- package/dist/transport-HOWgkXsv.js +2 -0
- package/package.json +20 -8
- package/pages/index.html +106 -32
- package/qoder-bridge/README.md +112 -0
- package/dist/auth-Bu4MXadr.js +0 -2
- package/dist/mcp-server-DQ4r-fAy.js +0 -2
- package/dist/models-YMUf33c-.js +0 -88
- package/dist/server-CFQmvoAJ.js +0 -11710
- /package/dist/{electron-fetch-BRX-ug5E.js → electron-fetch-D5uOld7F.js} +0 -0
- /package/dist/{tls-Aq1Dd8E2.js → tls-B5VJAJJM.js} +0 -0
- /package/dist/{tool-search-Ds1vbmGG.js → tool-search-DCb3HbWj.js} +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { K as state, S as initOpencodeVersion, _ as copilotModelsHeaders, h as copilotBaseUrl, i as setupCopilotToken, j as getMissingApiKeysMessage, m as readGitHubToken, q as HTTPError, t as logUser } from "./token-
|
|
3
|
-
import { i as runProviderSetup } from "./auth-
|
|
4
|
-
import {
|
|
1
|
+
import { I as mergeConfigWithDefaults, i as listEnabledProviders, t as getProviderConfig, u as isQoderSdkProvider, z as ensurePaths } from "./config-DnMKoKrg.js";
|
|
2
|
+
import { A as getConfiguredApiKeys, K as state, S as initOpencodeVersion, _ as copilotModelsHeaders, h as copilotBaseUrl, i as setupCopilotToken, j as getMissingApiKeysMessage, m as readGitHubToken, q as HTTPError, t as logUser } from "./token-CQftvMU4.js";
|
|
3
|
+
import { i as runProviderSetup } from "./auth-D-M3qCeV.js";
|
|
4
|
+
import { o as forwardProviderModels, u as getLatestModelForFamily } from "./provider-proxy-BbLwvtD_.js";
|
|
5
|
+
import { networkInterfaces } from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
8
|
+
import { isIP } from "node:net";
|
|
5
9
|
import { defineCommand } from "citty";
|
|
6
10
|
import consola from "consola";
|
|
7
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
8
|
-
import path from "node:path";
|
|
9
|
-
import { networkInterfaces } from "node:os";
|
|
10
11
|
import { getProxyForUrl } from "proxy-from-env";
|
|
11
12
|
import { Agent, ProxyAgent, setGlobalDispatcher } from "undici";
|
|
12
13
|
import { execSync } from "node:child_process";
|
|
@@ -64,6 +65,65 @@ function initProxyFromEnv() {
|
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
//#endregion
|
|
68
|
+
//#region src/lib/server-host-shared.ts
|
|
69
|
+
const INVALID_HOST_CHARACTERS = /[\s/?#]/;
|
|
70
|
+
const NORMALIZED_WILDCARD_HOSTS = new Set(["0.0.0.0", "::"]);
|
|
71
|
+
function stripIpv6Brackets(hostname) {
|
|
72
|
+
if (hostname.startsWith("[") && hostname.endsWith("]")) return hostname.slice(1, -1);
|
|
73
|
+
return hostname;
|
|
74
|
+
}
|
|
75
|
+
function normalizeHostnameBase(hostname) {
|
|
76
|
+
const normalized = stripIpv6Brackets(hostname.trim());
|
|
77
|
+
if (!normalized || INVALID_HOST_CHARACTERS.test(normalized)) throw new Error(`Invalid server host: ${JSON.stringify(hostname)}`);
|
|
78
|
+
return normalized;
|
|
79
|
+
}
|
|
80
|
+
function isWildcardHostname(hostname) {
|
|
81
|
+
return NORMALIZED_WILDCARD_HOSTS.has(stripIpv6Brackets(hostname.trim()));
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/lib/server-host.ts
|
|
85
|
+
const DEFAULT_SERVER_HOST = "127.0.0.1";
|
|
86
|
+
function isLoopbackIpv4(hostname) {
|
|
87
|
+
return isIP(hostname) === 4 && hostname.split(".", 1)[0] === "127";
|
|
88
|
+
}
|
|
89
|
+
function isLoopbackMappedIpv4(hostname) {
|
|
90
|
+
try {
|
|
91
|
+
const mappedIpv4 = new URL(`http://[${hostname}]/`).hostname.slice(1, -1).match(/^::ffff:([\da-f]{1,4}):[\da-f]{1,4}$/);
|
|
92
|
+
if (!mappedIpv4) return false;
|
|
93
|
+
return (Number.parseInt(mappedIpv4[1], 16) & 65280) === 32512;
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function normalizeServerHostname(hostname) {
|
|
99
|
+
return normalizeHostnameBase(hostname);
|
|
100
|
+
}
|
|
101
|
+
function isLoopbackHostname(hostname) {
|
|
102
|
+
const normalized = normalizeServerHostname(hostname).toLowerCase();
|
|
103
|
+
if (normalized === "localhost" || normalized === "localhost.") return true;
|
|
104
|
+
if (isLoopbackIpv4(normalized)) return true;
|
|
105
|
+
if (isIP(normalized) !== 6) return false;
|
|
106
|
+
if (normalized === "::1" || normalized === "0:0:0:0:0:0:0:1") return true;
|
|
107
|
+
return isLoopbackMappedIpv4(normalized);
|
|
108
|
+
}
|
|
109
|
+
function resolveServerBinding(hostname, hasApiKeys) {
|
|
110
|
+
const normalizedHostname = normalizeServerHostname(hostname);
|
|
111
|
+
const networkExposed = !isLoopbackHostname(normalizedHostname);
|
|
112
|
+
if (networkExposed && !hasApiKeys) throw new Error(`Refusing to listen on non-loopback host ${JSON.stringify(normalizedHostname)} without gateway API keys. Run \`npx copilot-api auth keys --add <key>\` first, or use \`--host ${DEFAULT_SERVER_HOST}\`.`);
|
|
113
|
+
return {
|
|
114
|
+
hostname: normalizedHostname,
|
|
115
|
+
clientHostname: resolveClientHostname(normalizedHostname),
|
|
116
|
+
networkExposed
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function resolveClientHostname(hostname) {
|
|
120
|
+
const normalizedHostname = normalizeServerHostname(hostname);
|
|
121
|
+
return isWildcardHostname(normalizedHostname) ? DEFAULT_SERVER_HOST : normalizedHostname.toLowerCase();
|
|
122
|
+
}
|
|
123
|
+
function formatServerUrl(hostname, port) {
|
|
124
|
+
return `http://${isIP(hostname) === 6 ? `[${hostname.replace("%", "%25")}]` : hostname}:${port}`;
|
|
125
|
+
}
|
|
126
|
+
//#endregion
|
|
67
127
|
//#region src/lib/shell.ts
|
|
68
128
|
function getShell() {
|
|
69
129
|
const { platform, ppid, env } = process$1;
|
|
@@ -115,6 +175,33 @@ function generateEnvScript(envVars, commandToRun = "") {
|
|
|
115
175
|
return commandBlock || commandToRun;
|
|
116
176
|
}
|
|
117
177
|
//#endregion
|
|
178
|
+
//#region src/services/qoder/startup-models.ts
|
|
179
|
+
async function loadQoderStartupModels(provider, fetchModels = forwardProviderModels, bunVersion = process.versions.bun) {
|
|
180
|
+
if (!provider || !isQoderSdkProvider(provider)) return { models: [] };
|
|
181
|
+
if (bunVersion) return {
|
|
182
|
+
models: [],
|
|
183
|
+
warning: "Qoder SDK Worker is not supported by this Bun startup path. Use `bun run start` (builds and runs with Node), or `node dist/main.js start`."
|
|
184
|
+
};
|
|
185
|
+
try {
|
|
186
|
+
const response = await fetchModels(provider, new Headers());
|
|
187
|
+
if (!response.ok) return {
|
|
188
|
+
models: [],
|
|
189
|
+
warning: `Qoder model discovery failed (HTTP ${response.status}). Check PAT and SDK runtime.`
|
|
190
|
+
};
|
|
191
|
+
const body = await response.json();
|
|
192
|
+
const models = Array.isArray(body.data) ? body.data.flatMap((m) => typeof m?.id === "string" && m.id.trim() ? [`qoder/${m.id}`] : []) : [];
|
|
193
|
+
return models.length ? { models } : {
|
|
194
|
+
models: [],
|
|
195
|
+
warning: "Qoder returned no available models."
|
|
196
|
+
};
|
|
197
|
+
} catch {
|
|
198
|
+
return {
|
|
199
|
+
models: [],
|
|
200
|
+
warning: "Qoder model discovery failed. Check PAT, SDK runtime and network settings."
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
//#endregion
|
|
118
205
|
//#region src/services/copilot/get-models.ts
|
|
119
206
|
const getModels = async () => {
|
|
120
207
|
consola.info(`Fetching models from ${copilotBaseUrl(state)}/models`);
|
|
@@ -447,9 +534,11 @@ async function setupProviderMode(serverUrl, claudeCode) {
|
|
|
447
534
|
consola.info(`Configured providers: ${providersAfterSetup.join(", ")}`);
|
|
448
535
|
}
|
|
449
536
|
async function runServer(options) {
|
|
450
|
-
(await import("./tls-
|
|
537
|
+
(await import("./tls-B5VJAJJM.js")).enableSystemCACompat();
|
|
451
538
|
consola.options.throttle = 0;
|
|
452
539
|
mergeConfigWithDefaults();
|
|
540
|
+
const configuredApiKeys = getConfiguredApiKeys();
|
|
541
|
+
const binding = resolveServerBinding(options.host, configuredApiKeys.length > 0);
|
|
453
542
|
const missingApiKeysMessage = getMissingApiKeysMessage();
|
|
454
543
|
if (missingApiKeysMessage) consola.info(missingApiKeysMessage);
|
|
455
544
|
await initOpencodeVersion();
|
|
@@ -461,14 +550,18 @@ async function runServer(options) {
|
|
|
461
550
|
}
|
|
462
551
|
state.showToken = options.showToken;
|
|
463
552
|
await ensurePaths();
|
|
464
|
-
const serverUrl =
|
|
553
|
+
const serverUrl = formatServerUrl(binding.clientHostname, options.port);
|
|
465
554
|
const githubToken = options.githubToken || await readGitHubToken();
|
|
466
555
|
if (githubToken) await setupCopilotMode(githubToken, Boolean(options.githubToken), serverUrl, options.claudeCode);
|
|
467
556
|
else await setupProviderMode(serverUrl, options.claudeCode);
|
|
557
|
+
const qoder = await loadQoderStartupModels(getProviderConfig("qoder"));
|
|
558
|
+
if (qoder.warning) consola.warn(qoder.warning);
|
|
559
|
+
if (qoder.models.length) consola.info(`Qoder models (${qoder.models.length}):\n${qoder.models.map((model) => `- ${model}`).join("\n")}`);
|
|
468
560
|
consola.box(`🌐 Usage Viewer: ${serverUrl}/usage-viewer?endpoint=${serverUrl}/usage`);
|
|
469
|
-
const {
|
|
561
|
+
const { createServer } = await import("./server-DmKNMBLd.js");
|
|
470
562
|
serve({
|
|
471
|
-
fetch:
|
|
563
|
+
fetch: createServer({ networkExposed: binding.networkExposed }).fetch,
|
|
564
|
+
hostname: binding.hostname,
|
|
472
565
|
port: options.port,
|
|
473
566
|
bun: { idleTimeout: 0 }
|
|
474
567
|
});
|
|
@@ -479,6 +572,11 @@ const start = defineCommand({
|
|
|
479
572
|
description: "Start the Copilot API server"
|
|
480
573
|
},
|
|
481
574
|
args: {
|
|
575
|
+
host: {
|
|
576
|
+
type: "string",
|
|
577
|
+
default: process.env.HOST?.trim() || "127.0.0.1",
|
|
578
|
+
description: "Host to listen on"
|
|
579
|
+
},
|
|
482
580
|
port: {
|
|
483
581
|
alias: "p",
|
|
484
582
|
type: "string",
|
|
@@ -515,6 +613,7 @@ const start = defineCommand({
|
|
|
515
613
|
},
|
|
516
614
|
run({ args }) {
|
|
517
615
|
return runServer({
|
|
616
|
+
host: args.host,
|
|
518
617
|
port: Number.parseInt(args.port, 10),
|
|
519
618
|
verbose: args.verbose,
|
|
520
619
|
githubToken: args["github-token"],
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import consola from "consola";
|
|
3
|
-
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
4
|
-
import path from "node:path";
|
|
1
|
+
import { A as isCodexResponsesApiWebSocketEnabled, B as writeFileAtomically, E as getConfig, O as getResponsesTransportConfig, R as PATHS, c as setProviderConfig, n as getRawProviderConfig } from "./config-DnMKoKrg.js";
|
|
5
2
|
import fs, { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
5
|
+
import consola from "consola";
|
|
6
6
|
import { createServer } from "node:http";
|
|
7
7
|
import { getProxyForUrl } from "proxy-from-env";
|
|
8
8
|
import { WebSocket } from "undici";
|
|
@@ -142,6 +142,7 @@ const runPooledWebSocketRequest = async function* (request, options) {
|
|
|
142
142
|
throwIfAborted(request.signal);
|
|
143
143
|
messageStream = createWebSocketMessageStream(websocket, request.signal, options);
|
|
144
144
|
messageStream.start();
|
|
145
|
+
options.onBeforeSend?.();
|
|
145
146
|
websocket.send(JSON.stringify(request.payload));
|
|
146
147
|
for await (const data of messageStream.iterable) {
|
|
147
148
|
const chunk = options.createChunk(data);
|
|
@@ -785,7 +786,7 @@ function buildCodexRequestHeaders(requestHeaders) {
|
|
|
785
786
|
return headers;
|
|
786
787
|
}
|
|
787
788
|
function resolveCodexResponsesTransport(transport) {
|
|
788
|
-
return transport ?? (
|
|
789
|
+
return transport ?? (isCodexResponsesApiWebSocketEnabled() ? "websocket" : "http");
|
|
789
790
|
}
|
|
790
791
|
function buildCodexResponsesWebSocketHeaders(requestHeaders) {
|
|
791
792
|
const headers = buildCodexResponsesHeaders(requestHeaders);
|
|
@@ -818,6 +819,11 @@ async function forwardCodexResponses(payload, requestHeaders, baseUrl = CODEX_AP
|
|
|
818
819
|
consola.log(`<-- model: ${payload.model}`);
|
|
819
820
|
const transport = resolveCodexResponsesTransport(options.transport);
|
|
820
821
|
if (payload.stream && transport === "websocket") return forwardCodexResponsesOverWebSocket(payload, requestHeaders, baseUrl, options.signal);
|
|
822
|
+
const response = await fetchCodexResponsesOverHttp(payload, requestHeaders, baseUrl, options.signal);
|
|
823
|
+
if (payload.stream) return createResponsesSafeStream(createResponsesHttpEventStream(response, options.signal), { signal: options.signal });
|
|
824
|
+
return await response.json();
|
|
825
|
+
}
|
|
826
|
+
const fetchCodexResponsesOverHttp = async (payload, requestHeaders, baseUrl, signal) => {
|
|
821
827
|
const normalizedPayload = normalizeCodexResponsesPayload(payload);
|
|
822
828
|
const transportConfig = getResponsesTransportConfig();
|
|
823
829
|
const response = await fetchResponsesWithLifecycle(resolveCodexResponsesUrl(baseUrl), {
|
|
@@ -826,13 +832,12 @@ async function forwardCodexResponses(payload, requestHeaders, baseUrl = CODEX_AP
|
|
|
826
832
|
body: JSON.stringify(normalizedPayload)
|
|
827
833
|
}, {
|
|
828
834
|
headersTimeoutMs: transportConfig.headersTimeoutMs,
|
|
829
|
-
signal
|
|
835
|
+
signal,
|
|
830
836
|
streamInactivityTimeoutMs: transportConfig.streamInactivityTimeoutMs
|
|
831
837
|
});
|
|
832
838
|
if (!response.ok) throw new HTTPError("Failed to create codex responses", response);
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
}
|
|
839
|
+
return response;
|
|
840
|
+
};
|
|
836
841
|
const normalizeCodexResponsesPayload = (payload) => {
|
|
837
842
|
const normalizedPayload = {
|
|
838
843
|
...payload,
|
|
@@ -897,22 +902,36 @@ const buildCodexResponsesWebSocketPoolKey = (payload, headers, baseUrl) => {
|
|
|
897
902
|
].map(encodePoolKeyPart).join("|");
|
|
898
903
|
};
|
|
899
904
|
const forwardCodexResponsesOverWebSocket = (payload, requestHeaders, baseUrl, signal) => {
|
|
900
|
-
return
|
|
905
|
+
return createResponsesSafeStream(createCodexResponsesStreamWithFallback(prepareCodexResponsesWebSocketRequest(payload, requestHeaders, baseUrl, signal), payload, requestHeaders, baseUrl), { signal });
|
|
901
906
|
};
|
|
902
|
-
const
|
|
907
|
+
const createCodexResponsesStreamWithFallback = async function* (request, payload, requestHeaders, baseUrl) {
|
|
908
|
+
let sendAttempted = false;
|
|
909
|
+
try {
|
|
910
|
+
yield* createCodexResponsesWebSocketStream(request, () => {
|
|
911
|
+
sendAttempted = true;
|
|
912
|
+
});
|
|
913
|
+
return;
|
|
914
|
+
} catch (error) {
|
|
915
|
+
if (sendAttempted || request.signal?.aborted || error instanceof Error && error.name === "AbortError" || payload.previous_response_id != null || payload.conversation != null) throw error;
|
|
916
|
+
consola.warn("Codex websocket failed before sending the request; falling back to HTTP once");
|
|
917
|
+
}
|
|
918
|
+
yield* createResponsesHttpEventStream(await fetchCodexResponsesOverHttp(payload, requestHeaders, baseUrl, request.signal), request.signal);
|
|
919
|
+
};
|
|
920
|
+
const createCodexResponsesWebSocketStream = (request, onBeforeSend) => {
|
|
903
921
|
const transportConfig = getResponsesTransportConfig();
|
|
904
|
-
return
|
|
922
|
+
return createPooledWebSocketStream(request, {
|
|
905
923
|
createChunk: createCodexResponsesWebSocketStreamChunk,
|
|
906
924
|
maxBufferedBytes: transportConfig.websocketMaxBufferedBytes,
|
|
907
925
|
maxBufferedMessages: transportConfig.websocketMaxBufferedMessages,
|
|
908
926
|
isTerminalChunk: isTerminalResponsesStreamChunk,
|
|
909
927
|
openErrorMessage: "Failed to create codex responses websocket",
|
|
910
928
|
openTimeoutMs: transportConfig.websocketOpenTimeoutMs,
|
|
929
|
+
onBeforeSend,
|
|
911
930
|
poolIdleTimeoutMs: transportConfig.websocketPoolIdleTimeoutMs,
|
|
912
931
|
streamInactivityTimeoutMs: transportConfig.streamInactivityTimeoutMs,
|
|
913
932
|
streamErrorMessage: "Upstream connection lost, Codex responses websocket stream error",
|
|
914
933
|
terminalChunkMissingMessage: "Codex responses websocket ended without a terminal response, retry your request."
|
|
915
|
-
})
|
|
934
|
+
});
|
|
916
935
|
};
|
|
917
936
|
const createCodexResponsesWebSocketStreamChunk = (data) => {
|
|
918
937
|
if (data === "[DONE]") return { data };
|
|
@@ -1080,7 +1099,7 @@ async function exchangeAuthorizationCode(code, verifier) {
|
|
|
1080
1099
|
throw new Error(`Codex token exchange failed (${response.status}): ${details || response.statusText}`);
|
|
1081
1100
|
}
|
|
1082
1101
|
const payload = await response.json();
|
|
1083
|
-
if (typeof payload.access_token !== "string" || typeof payload.refresh_token !== "string" || typeof payload.expires_in !== "number") throw new TypeError(
|
|
1102
|
+
if (typeof payload.access_token !== "string" || typeof payload.refresh_token !== "string" || typeof payload.expires_in !== "number") throw new TypeError("Codex token exchange response missing required fields");
|
|
1084
1103
|
return {
|
|
1085
1104
|
accessToken: payload.access_token,
|
|
1086
1105
|
refreshToken: payload.refresh_token,
|
|
@@ -1102,7 +1121,7 @@ async function refreshAccessToken(refreshToken) {
|
|
|
1102
1121
|
throw new Error(`Codex token refresh failed (${response.status}): ${details || response.statusText}`);
|
|
1103
1122
|
}
|
|
1104
1123
|
const payload = await response.json();
|
|
1105
|
-
if (typeof payload.access_token !== "string" || typeof payload.refresh_token !== "string" || typeof payload.expires_in !== "number") throw new TypeError(
|
|
1124
|
+
if (typeof payload.access_token !== "string" || typeof payload.refresh_token !== "string" || typeof payload.expires_in !== "number") throw new TypeError("Codex token refresh response missing required fields");
|
|
1106
1125
|
return {
|
|
1107
1126
|
accessToken: payload.access_token,
|
|
1108
1127
|
refreshToken: payload.refresh_token,
|
|
@@ -1565,14 +1584,8 @@ async function readOptionalFile(filePath) {
|
|
|
1565
1584
|
throw error;
|
|
1566
1585
|
}
|
|
1567
1586
|
}
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
await fs.writeFile(filePath, content, "utf8");
|
|
1571
|
-
try {
|
|
1572
|
-
await fs.chmod(filePath, 384);
|
|
1573
|
-
} catch {
|
|
1574
|
-
return;
|
|
1575
|
-
}
|
|
1587
|
+
function writeProtectedFile(filePath, content) {
|
|
1588
|
+
return Promise.resolve().then(() => writeFileAtomically(filePath, content));
|
|
1576
1589
|
}
|
|
1577
1590
|
function normalizeCodexCredentials(credentials) {
|
|
1578
1591
|
if (!credentials || typeof credentials !== "object") return null;
|
|
@@ -1744,7 +1757,7 @@ async function pollAccessToken(deviceCode) {
|
|
|
1744
1757
|
continue;
|
|
1745
1758
|
}
|
|
1746
1759
|
const json = await response.json();
|
|
1747
|
-
consola.debug("Polling access token response
|
|
1760
|
+
consola.debug("Polling access token response received");
|
|
1748
1761
|
const { access_token } = json;
|
|
1749
1762
|
if (access_token) return access_token;
|
|
1750
1763
|
else await sleep(sleepDuration);
|
|
@@ -1754,6 +1767,10 @@ async function pollAccessToken(deviceCode) {
|
|
|
1754
1767
|
//#region src/lib/token.ts
|
|
1755
1768
|
let copilotRefreshLoopController = null;
|
|
1756
1769
|
let codexRefreshLoopController = null;
|
|
1770
|
+
const defaultCopilotTokenDependencies = {
|
|
1771
|
+
getCopilotToken,
|
|
1772
|
+
getCopilotUsage
|
|
1773
|
+
};
|
|
1757
1774
|
const stopCopilotRefreshLoop = () => {
|
|
1758
1775
|
if (!copilotRefreshLoopController) return;
|
|
1759
1776
|
copilotRefreshLoopController.abort();
|
|
@@ -1801,7 +1818,7 @@ const applyCopilotTokenResponse = (response) => {
|
|
|
1801
1818
|
state.copilotToken = response.token;
|
|
1802
1819
|
if (response.endpoints?.api) state.copilotApiUrl = response.endpoints.api;
|
|
1803
1820
|
};
|
|
1804
|
-
const setupCopilotToken = async () => {
|
|
1821
|
+
const setupCopilotToken = async (dependencies = defaultCopilotTokenDependencies) => {
|
|
1805
1822
|
if (isOpencodeOauthApp()) {
|
|
1806
1823
|
if (!state.githubToken) throw new Error(`opencode token not found`);
|
|
1807
1824
|
state.copilotToken = state.githubToken;
|
|
@@ -1810,14 +1827,14 @@ const setupCopilotToken = async () => {
|
|
|
1810
1827
|
stopCopilotRefreshLoop();
|
|
1811
1828
|
return;
|
|
1812
1829
|
}
|
|
1813
|
-
const response = await getCopilotToken();
|
|
1830
|
+
const response = await dependencies.getCopilotToken();
|
|
1814
1831
|
applyCopilotTokenResponse(response);
|
|
1815
1832
|
consola.debug("GitHub Copilot Token fetched successfully!");
|
|
1816
1833
|
if (state.showToken) consola.info("Copilot token:", state.copilotToken);
|
|
1817
1834
|
stopCopilotRefreshLoop();
|
|
1818
1835
|
const controller = new AbortController();
|
|
1819
1836
|
copilotRefreshLoopController = controller;
|
|
1820
|
-
runCopilotRefreshLoop(response.refresh_in, controller.signal).catch(() => {
|
|
1837
|
+
runCopilotRefreshLoop(response.refresh_in, controller.signal, dependencies).catch(() => {
|
|
1821
1838
|
consola.warn("Copilot token refresh loop stopped");
|
|
1822
1839
|
}).finally(() => {
|
|
1823
1840
|
if (copilotRefreshLoopController === controller) copilotRefreshLoopController = null;
|
|
@@ -1856,7 +1873,7 @@ const RETRY_REFRESH_JITTER_MS = 15e3;
|
|
|
1856
1873
|
const MIN_REFRESH_DELAY_MS = 1e3;
|
|
1857
1874
|
const getRefreshDeadlineMs = (refreshIn, nowMs = Date.now()) => nowMs + Math.max(refreshIn * 1e3 - EARLY_REFRESH_BUFFER_MS, MIN_REFRESH_DELAY_MS);
|
|
1858
1875
|
const getRefreshPollDelayMs = (refreshAtMs, nowMs = Date.now()) => Math.min(Math.max(refreshAtMs - nowMs, 0), REFRESH_POLL_INTERVAL_MS);
|
|
1859
|
-
const runCopilotRefreshLoop = async (refreshIn, signal) => {
|
|
1876
|
+
const runCopilotRefreshLoop = async (refreshIn, signal, dependencies) => {
|
|
1860
1877
|
let refreshAtMs = getRefreshDeadlineMs(refreshIn);
|
|
1861
1878
|
let retryDelayMs = RETRY_REFRESH_DELAY_MS;
|
|
1862
1879
|
while (!signal.aborted) {
|
|
@@ -1867,7 +1884,7 @@ const runCopilotRefreshLoop = async (refreshIn, signal) => {
|
|
|
1867
1884
|
}
|
|
1868
1885
|
consola.debug("Refreshing Copilot token");
|
|
1869
1886
|
try {
|
|
1870
|
-
const response = await getCopilotToken();
|
|
1887
|
+
const response = await dependencies.getCopilotToken();
|
|
1871
1888
|
applyCopilotTokenResponse(response);
|
|
1872
1889
|
refreshAtMs = getRefreshDeadlineMs(response.refresh_in);
|
|
1873
1890
|
retryDelayMs = RETRY_REFRESH_DELAY_MS;
|
|
@@ -1938,8 +1955,8 @@ async function setupGitHubToken(options) {
|
|
|
1938
1955
|
throw error;
|
|
1939
1956
|
}
|
|
1940
1957
|
}
|
|
1941
|
-
async function logUser() {
|
|
1942
|
-
const copilotUser = await getCopilotUsage();
|
|
1958
|
+
async function logUser(dependencies = defaultCopilotTokenDependencies) {
|
|
1959
|
+
const copilotUser = await dependencies.getCopilotUsage();
|
|
1943
1960
|
if (!copilotUser) throw new Error("GitHub token not found");
|
|
1944
1961
|
state.userName = copilotUser.login;
|
|
1945
1962
|
consola.info(`Logged in as ${copilotUser.login}`);
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { d as BridgeError, n as parseChat, r as publicError, t as completionIdentity } from "./protocol-BTFLbI8m.js";
|
|
2
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
import { streamSSE } from "hono/streaming";
|
|
5
|
+
import { bodyLimit } from "hono/body-limit";
|
|
6
|
+
//#region qoder-bridge/app.ts
|
|
7
|
+
function authorized(header, key) {
|
|
8
|
+
const actual = Buffer.from(header ?? "");
|
|
9
|
+
const expected = Buffer.from(`Bearer ${key}`);
|
|
10
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
11
|
+
}
|
|
12
|
+
function abortable(promise, signal) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const abort = () => reject(signal.reason instanceof Error ? signal.reason : new BridgeError("Request cancelled", 502, "cancelled"));
|
|
15
|
+
if (signal.aborted) {
|
|
16
|
+
abort();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
20
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort)).catch(() => {});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function createBridge(backend, options) {
|
|
24
|
+
if (!options.apiKey.trim()) throw new Error("QODER_BRIDGE_API_KEY is required");
|
|
25
|
+
const app = new Hono();
|
|
26
|
+
let active = 0;
|
|
27
|
+
app.get("/health", (c) => c.json({
|
|
28
|
+
status: "ok",
|
|
29
|
+
service: "qoder-bridge",
|
|
30
|
+
capabilities: [
|
|
31
|
+
"text",
|
|
32
|
+
"image-input",
|
|
33
|
+
"chat-completions",
|
|
34
|
+
"client-tool-calls",
|
|
35
|
+
"streaming",
|
|
36
|
+
"models",
|
|
37
|
+
"credits"
|
|
38
|
+
]
|
|
39
|
+
}));
|
|
40
|
+
app.use("*", async (c, next) => {
|
|
41
|
+
if (!authorized(c.req.header("authorization"), options.apiKey)) return c.json({ error: {
|
|
42
|
+
message: "Invalid bridge API key",
|
|
43
|
+
type: "authentication_error"
|
|
44
|
+
} }, 401);
|
|
45
|
+
await next();
|
|
46
|
+
});
|
|
47
|
+
app.use("*", bodyLimit({
|
|
48
|
+
maxSize: options.maxBodyBytes ?? 23068672,
|
|
49
|
+
onError: (c) => c.json({ error: {
|
|
50
|
+
message: "Request body too large",
|
|
51
|
+
type: "invalid_request_error"
|
|
52
|
+
} }, 413)
|
|
53
|
+
}));
|
|
54
|
+
app.onError((error, c) => {
|
|
55
|
+
const failure = publicError(error);
|
|
56
|
+
return c.json({ error: {
|
|
57
|
+
message: failure.message,
|
|
58
|
+
type: failure.code
|
|
59
|
+
} }, failure.status);
|
|
60
|
+
});
|
|
61
|
+
function lease(signal) {
|
|
62
|
+
if (active >= (options.maxConcurrent ?? 2)) throw new BridgeError("Qoder bridge is busy", 429, "rate_limit_error");
|
|
63
|
+
active++;
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
const abort = () => controller.abort(signal.reason);
|
|
66
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
67
|
+
if (signal.aborted) abort();
|
|
68
|
+
const timer = setTimeout(() => controller.abort(new BridgeError("Qoder request timed out", 504, "timeout")), options.timeoutMs ?? 12e4);
|
|
69
|
+
let released = false;
|
|
70
|
+
return {
|
|
71
|
+
signal: controller.signal,
|
|
72
|
+
abort: () => controller.abort(),
|
|
73
|
+
release: () => {
|
|
74
|
+
if (released) return;
|
|
75
|
+
released = true;
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
signal.removeEventListener("abort", abort);
|
|
78
|
+
active--;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
for (const path of ["/v1/models", "/usage"]) app.get(path, async (c) => {
|
|
83
|
+
const session = lease(c.req.raw.signal);
|
|
84
|
+
try {
|
|
85
|
+
const result = await abortable(backend.catalog(session.signal), session.signal);
|
|
86
|
+
if (path === "/usage") return c.json({
|
|
87
|
+
provider: "qoder",
|
|
88
|
+
usage: result.usage
|
|
89
|
+
});
|
|
90
|
+
return c.json({
|
|
91
|
+
object: "list",
|
|
92
|
+
data: result.models.map((m) => ({
|
|
93
|
+
...m,
|
|
94
|
+
id: m.id,
|
|
95
|
+
object: "model",
|
|
96
|
+
created: 0,
|
|
97
|
+
owned_by: "qoder",
|
|
98
|
+
name: m.name ?? m.id
|
|
99
|
+
}))
|
|
100
|
+
});
|
|
101
|
+
} finally {
|
|
102
|
+
session.release();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
app.post("/v1/chat/completions", async (c) => {
|
|
106
|
+
let json;
|
|
107
|
+
try {
|
|
108
|
+
json = await c.req.json();
|
|
109
|
+
} catch {
|
|
110
|
+
throw new BridgeError("Invalid JSON request");
|
|
111
|
+
}
|
|
112
|
+
const input = parseChat(json);
|
|
113
|
+
const session = lease(c.req.raw.signal);
|
|
114
|
+
const identity = completionIdentity(input.model);
|
|
115
|
+
const iterator = backend.turn(input, session.signal)[Symbol.asyncIterator]();
|
|
116
|
+
async function* consume() {
|
|
117
|
+
try {
|
|
118
|
+
let completed = false;
|
|
119
|
+
while (true) {
|
|
120
|
+
const item = await abortable(iterator.next(), session.signal);
|
|
121
|
+
if (item.done) break;
|
|
122
|
+
if (completed) throw new BridgeError("Qoder sent data after completion", 502, "invalid_stream");
|
|
123
|
+
completed = item.value.type === "complete";
|
|
124
|
+
yield item.value;
|
|
125
|
+
}
|
|
126
|
+
if (!completed) throw new BridgeError("Qoder stream ended before completion", 502, "incomplete_stream");
|
|
127
|
+
} finally {
|
|
128
|
+
session.abort();
|
|
129
|
+
iterator.return?.().catch(() => {});
|
|
130
|
+
session.release();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (input.stream) return streamSSE(c, async (stream) => {
|
|
134
|
+
stream.onAbort(session.abort);
|
|
135
|
+
const chunk = (choices, extra = {}) => JSON.stringify({
|
|
136
|
+
...identity,
|
|
137
|
+
object: "chat.completion.chunk",
|
|
138
|
+
choices,
|
|
139
|
+
...extra
|
|
140
|
+
});
|
|
141
|
+
try {
|
|
142
|
+
await stream.writeSSE({ data: chunk([{
|
|
143
|
+
index: 0,
|
|
144
|
+
delta: { role: "assistant" },
|
|
145
|
+
finish_reason: null
|
|
146
|
+
}]) });
|
|
147
|
+
let info;
|
|
148
|
+
let hasTools = false;
|
|
149
|
+
for await (const event of consume()) if (event.type === "text") await stream.writeSSE({ data: chunk([{
|
|
150
|
+
index: 0,
|
|
151
|
+
delta: { content: event.text },
|
|
152
|
+
finish_reason: null
|
|
153
|
+
}]) });
|
|
154
|
+
else if (event.type === "tool_calls") {
|
|
155
|
+
hasTools = true;
|
|
156
|
+
await stream.writeSSE({ data: chunk([{
|
|
157
|
+
index: 0,
|
|
158
|
+
delta: { tool_calls: event.calls.map((call, index) => ({
|
|
159
|
+
...call,
|
|
160
|
+
index
|
|
161
|
+
})) },
|
|
162
|
+
finish_reason: null
|
|
163
|
+
}]) });
|
|
164
|
+
} else info = event.info;
|
|
165
|
+
await stream.writeSSE({ data: chunk([{
|
|
166
|
+
index: 0,
|
|
167
|
+
delta: {},
|
|
168
|
+
finish_reason: hasTools ? "tool_calls" : "stop"
|
|
169
|
+
}], { qoder_usage: info?.qoder_usage }) });
|
|
170
|
+
if (input.includeUsage) await stream.writeSSE({ data: chunk([], {
|
|
171
|
+
usage: info?.usage ?? null,
|
|
172
|
+
qoder_usage: info?.qoder_usage
|
|
173
|
+
}) });
|
|
174
|
+
await stream.writeSSE({ data: "[DONE]" });
|
|
175
|
+
} catch (error) {
|
|
176
|
+
const failure = publicError(error);
|
|
177
|
+
if (!stream.aborted) await stream.writeSSE({ data: JSON.stringify({ error: {
|
|
178
|
+
message: failure.message,
|
|
179
|
+
type: failure.code
|
|
180
|
+
} }) });
|
|
181
|
+
} finally {
|
|
182
|
+
session.abort();
|
|
183
|
+
session.release();
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
let text = "";
|
|
187
|
+
let calls = [];
|
|
188
|
+
let info;
|
|
189
|
+
for await (const event of consume()) if (event.type === "text") text += event.text;
|
|
190
|
+
else if (event.type === "tool_calls") calls = event.calls;
|
|
191
|
+
else info = event.info;
|
|
192
|
+
return c.json({
|
|
193
|
+
...identity,
|
|
194
|
+
object: "chat.completion",
|
|
195
|
+
choices: [{
|
|
196
|
+
index: 0,
|
|
197
|
+
message: {
|
|
198
|
+
role: "assistant",
|
|
199
|
+
content: text || null,
|
|
200
|
+
...calls.length ? { tool_calls: calls } : {}
|
|
201
|
+
},
|
|
202
|
+
finish_reason: calls.length ? "tool_calls" : "stop"
|
|
203
|
+
}],
|
|
204
|
+
...info
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
return app;
|
|
208
|
+
}
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/services/qoder/transport.ts
|
|
211
|
+
async function sdkBackend(token) {
|
|
212
|
+
const { createBackend } = await import("./runtime-CWU5BNBf.js");
|
|
213
|
+
return createBackend(void 0, token);
|
|
214
|
+
}
|
|
215
|
+
function createQoderTransport(loadBackend = sdkBackend) {
|
|
216
|
+
let current;
|
|
217
|
+
const localKey = randomBytes(32).toString("hex");
|
|
218
|
+
return async (provider, path, init = {}) => {
|
|
219
|
+
const token = provider.apiKey.trim();
|
|
220
|
+
if (!token) return Response.json({ error: {
|
|
221
|
+
message: "Configure Qoder with auth login --provider qoder",
|
|
222
|
+
type: "authentication_error"
|
|
223
|
+
} }, { status: 401 });
|
|
224
|
+
if (!current || current.token !== token) {
|
|
225
|
+
const entry = {
|
|
226
|
+
token,
|
|
227
|
+
app: loadBackend(token).then((backend) => createBridge(backend, { apiKey: localKey }))
|
|
228
|
+
};
|
|
229
|
+
current = entry;
|
|
230
|
+
entry.app.catch(() => {
|
|
231
|
+
if (current === entry) current = void 0;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
const app = await current.app;
|
|
236
|
+
const headers = new Headers(init.headers);
|
|
237
|
+
headers.set("authorization", `Bearer ${localKey}`);
|
|
238
|
+
return await app.fetch(new Request(`http://qoder.internal${path}`, {
|
|
239
|
+
...init,
|
|
240
|
+
headers
|
|
241
|
+
}));
|
|
242
|
+
} catch {
|
|
243
|
+
return Response.json({ error: {
|
|
244
|
+
message: "Qoder SDK could not start. Reinstall the package with npm install scripts enabled, then retry.",
|
|
245
|
+
type: "qoder_runtime_error"
|
|
246
|
+
} }, { status: 502 });
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const forwardQoder = createQoderTransport();
|
|
251
|
+
//#endregion
|
|
252
|
+
export { forwardQoder as n, createQoderTransport as t };
|