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
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
//#region qoder-bridge/errors.ts
|
|
5
|
+
var BridgeError = class extends Error {
|
|
6
|
+
status;
|
|
7
|
+
code;
|
|
8
|
+
constructor(message, status = 400, code = "invalid_request_error") {
|
|
9
|
+
super(message);
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.code = code;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
function record(value) {
|
|
15
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region qoder-bridge/client-tools.ts
|
|
19
|
+
function parseClientTools(value) {
|
|
20
|
+
if (value === void 0) return [];
|
|
21
|
+
if (!Array.isArray(value) || value.length > 128) throw new BridgeError("tools must be an array of at most 128 functions");
|
|
22
|
+
const names = /* @__PURE__ */ new Set();
|
|
23
|
+
return value.map((raw) => {
|
|
24
|
+
const tool = record(raw), fn = record(tool.function), schema = record(fn.parameters);
|
|
25
|
+
if (tool.type !== "function" || typeof fn.name !== "string" || !/^[A-Za-z0-9_.-]{1,128}$/.test(fn.name) || names.has(fn.name)) throw new BridgeError("Invalid or duplicate function tool");
|
|
26
|
+
if (schema.type !== "object") throw new BridgeError("Function parameters must be an object JSON schema");
|
|
27
|
+
if (fn.description !== void 0 && typeof fn.description !== "string") throw new BridgeError("Invalid tool description");
|
|
28
|
+
names.add(fn.name);
|
|
29
|
+
return {
|
|
30
|
+
name: fn.name,
|
|
31
|
+
description: fn.description,
|
|
32
|
+
parameters: schema
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function clientToolServer(tools) {
|
|
37
|
+
const server = new McpServer({
|
|
38
|
+
name: "copilot-client",
|
|
39
|
+
version: "1.0.0"
|
|
40
|
+
}, { capabilities: { tools: {} } });
|
|
41
|
+
const names = /* @__PURE__ */ new Map();
|
|
42
|
+
const list = tools.map((tool, index) => {
|
|
43
|
+
const name = `client_${index}`;
|
|
44
|
+
names.set(name, tool.name);
|
|
45
|
+
names.set(`mcp__client__${name}`, tool.name);
|
|
46
|
+
return {
|
|
47
|
+
name,
|
|
48
|
+
description: tool.description ?? tool.name,
|
|
49
|
+
inputSchema: tool.parameters
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
const pending = /* @__PURE__ */ new Set();
|
|
53
|
+
server.server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: list }));
|
|
54
|
+
server.server.setRequestHandler(CallToolRequestSchema, () => new Promise((resolve) => pending.add(resolve)));
|
|
55
|
+
return {
|
|
56
|
+
names,
|
|
57
|
+
config: {
|
|
58
|
+
type: "sdk",
|
|
59
|
+
name: "client",
|
|
60
|
+
instance: server,
|
|
61
|
+
tools: list.map((t) => ({
|
|
62
|
+
name: t.name,
|
|
63
|
+
exposedName: t.name,
|
|
64
|
+
alwaysLoad: true,
|
|
65
|
+
permission_policy: "always_allow"
|
|
66
|
+
}))
|
|
67
|
+
},
|
|
68
|
+
async close() {
|
|
69
|
+
for (const resolve of pending) resolve({
|
|
70
|
+
isError: true,
|
|
71
|
+
content: [{
|
|
72
|
+
type: "text",
|
|
73
|
+
text: "Tool delegated to the API client"
|
|
74
|
+
}]
|
|
75
|
+
});
|
|
76
|
+
pending.clear();
|
|
77
|
+
await server.close();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region qoder-bridge/images.ts
|
|
83
|
+
const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
84
|
+
const MAX_TOTAL_IMAGE_BYTES = 15 * 1024 * 1024;
|
|
85
|
+
const MAX_IMAGE_REQUEST_BYTES = 22 * 1024 * 1024;
|
|
86
|
+
function parseImagePart(part) {
|
|
87
|
+
if (Object.keys(part).some((key) => !["type", "image_url"].includes(key))) throw new BridgeError("Unsupported image block field");
|
|
88
|
+
const image = record(part.image_url);
|
|
89
|
+
if (typeof image.url !== "string" || !image.url) throw new BridgeError("image_url.url is required");
|
|
90
|
+
if (Object.keys(image).some((key) => !["url", "detail"].includes(key))) throw new BridgeError("Unsupported image_url field");
|
|
91
|
+
if (image.detail !== void 0 && image.detail !== "auto") throw new BridgeError("Only image_url.detail=auto is supported", 400, "unsupported_parameter");
|
|
92
|
+
if (image.url.startsWith("data:")) decodeDataImage(image.url);
|
|
93
|
+
else validateImageUrl(image.url);
|
|
94
|
+
return { url: image.url };
|
|
95
|
+
}
|
|
96
|
+
function validateImageUrl(raw) {
|
|
97
|
+
let url;
|
|
98
|
+
try {
|
|
99
|
+
url = new URL(raw);
|
|
100
|
+
} catch {
|
|
101
|
+
throw new BridgeError("Invalid image URL");
|
|
102
|
+
}
|
|
103
|
+
if (url.protocol !== "https:" || url.username || url.password || url.port && url.port !== "443") throw new BridgeError("Remote images require HTTPS without credentials on port 443");
|
|
104
|
+
return url;
|
|
105
|
+
}
|
|
106
|
+
function imageMediaType(data) {
|
|
107
|
+
const b = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
108
|
+
if (b.length >= 8 && b.subarray(0, 8).equals(Buffer.from([
|
|
109
|
+
137,
|
|
110
|
+
80,
|
|
111
|
+
78,
|
|
112
|
+
71,
|
|
113
|
+
13,
|
|
114
|
+
10,
|
|
115
|
+
26,
|
|
116
|
+
10
|
|
117
|
+
]))) return "image/png";
|
|
118
|
+
if (b.length >= 3 && b[0] === 255 && b[1] === 216 && b[2] === 255) return "image/jpeg";
|
|
119
|
+
if (b.length >= 6 && ["GIF87a", "GIF89a"].includes(b.toString("ascii", 0, 6))) return "image/gif";
|
|
120
|
+
if (b.length >= 12 && b.toString("ascii", 0, 4) === "RIFF" && b.toString("ascii", 8, 12) === "WEBP") return "image/webp";
|
|
121
|
+
throw new BridgeError("Unsupported or invalid image bytes; expected PNG, JPEG, WebP or GIF");
|
|
122
|
+
}
|
|
123
|
+
function makeImage(data, mime) {
|
|
124
|
+
if (data.byteLength > 5242880) throw new BridgeError("Image exceeds the 5 MiB limit", 413);
|
|
125
|
+
const detected = imageMediaType(data);
|
|
126
|
+
if (mime.toLowerCase() !== detected) throw new BridgeError("Image content does not match its declared media type");
|
|
127
|
+
return {
|
|
128
|
+
type: "image",
|
|
129
|
+
source: {
|
|
130
|
+
type: "base64",
|
|
131
|
+
media_type: detected,
|
|
132
|
+
data: Buffer.from(data).toString("base64")
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function decodeDataImage(raw) {
|
|
137
|
+
const match = /^data:(image\/(?:png|jpeg|webp|gif));base64,([A-Za-z0-9+/]*={0,2})$/i.exec(raw);
|
|
138
|
+
if (!match || !match[2] || match[2].length % 4 !== 0) throw new BridgeError("Expected a valid base64 PNG, JPEG, WebP or GIF data URL");
|
|
139
|
+
if (match[2].length > 4 * Math.ceil(5242880 / 3)) throw new BridgeError("Image exceeds the 5 MiB limit", 413);
|
|
140
|
+
const data = Buffer.from(match[2], "base64");
|
|
141
|
+
if (data.toString("base64") !== match[2]) throw new BridgeError("Invalid base64 image encoding");
|
|
142
|
+
return makeImage(data, match[1]);
|
|
143
|
+
}
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region qoder-bridge/protocol.ts
|
|
146
|
+
const ALLOWED_FIELDS = new Set([
|
|
147
|
+
"model",
|
|
148
|
+
"messages",
|
|
149
|
+
"stream",
|
|
150
|
+
"stream_options",
|
|
151
|
+
"tools",
|
|
152
|
+
"tool_choice",
|
|
153
|
+
"n",
|
|
154
|
+
"max_completion_tokens",
|
|
155
|
+
"parallel_tool_calls",
|
|
156
|
+
"reasoning_effort",
|
|
157
|
+
"context_window",
|
|
158
|
+
"prompt_cache_key",
|
|
159
|
+
"user"
|
|
160
|
+
]);
|
|
161
|
+
function parseChat(value) {
|
|
162
|
+
const input = record(value);
|
|
163
|
+
for (const key of Object.keys(input)) if (!ALLOWED_FIELDS.has(key) && input[key] !== void 0 && input[key] !== null) throw new BridgeError(`Qoder bridge does not yet support '${key}'`, 400, "unsupported_parameter");
|
|
164
|
+
if (typeof input.model !== "string" || !input.model.trim()) throw new BridgeError("model is required");
|
|
165
|
+
if (input.stream !== void 0 && typeof input.stream !== "boolean") throw new BridgeError("stream must be boolean");
|
|
166
|
+
if (input.n !== void 0 && input.n !== 1) throw new BridgeError("Only n=1 is supported");
|
|
167
|
+
if (input.prompt_cache_key !== void 0 && typeof input.prompt_cache_key !== "string") throw new BridgeError("prompt_cache_key must be a string");
|
|
168
|
+
let tools = parseClientTools(input.tools);
|
|
169
|
+
const choice = input.tool_choice;
|
|
170
|
+
if (choice === "none") tools = [];
|
|
171
|
+
else if (choice !== void 0 && choice !== "auto") throw new BridgeError("Only tool_choice=auto or none is supported");
|
|
172
|
+
if (input.parallel_tool_calls !== void 0 && typeof input.parallel_tool_calls !== "boolean") throw new BridgeError("parallel_tool_calls must be boolean");
|
|
173
|
+
if (input.parallel_tool_calls === false && tools.length) throw new BridgeError("Serial-only tool selection is not supported");
|
|
174
|
+
if (input.max_completion_tokens !== void 0 && (!Number.isInteger(input.max_completion_tokens) || Number(input.max_completion_tokens) < 32e3)) throw new BridgeError("Qoder uses its native 32000-token output cap; smaller max_completion_tokens is not supported");
|
|
175
|
+
if (input.context_window !== void 0 && (!Number.isInteger(input.context_window) || Number(input.context_window) <= 0)) throw new BridgeError("context_window must be a positive integer");
|
|
176
|
+
if (input.reasoning_effort !== void 0 && typeof input.reasoning_effort !== "string") throw new BridgeError("Invalid reasoning_effort");
|
|
177
|
+
const streamOptions = record(input.stream_options);
|
|
178
|
+
if (input.stream_options !== void 0 && (input.stream_options === null || typeof input.stream_options !== "object" || Array.isArray(input.stream_options))) throw new BridgeError("Invalid stream_options");
|
|
179
|
+
if (Object.keys(streamOptions).some((key) => key !== "include_usage") || streamOptions.include_usage !== void 0 && typeof streamOptions.include_usage !== "boolean") throw new BridgeError("Only stream_options.include_usage is supported");
|
|
180
|
+
if (!Array.isArray(input.messages) || input.messages.length === 0) throw new BridgeError("messages must be a nonempty array");
|
|
181
|
+
const system = [];
|
|
182
|
+
const images = [];
|
|
183
|
+
const conversation = [];
|
|
184
|
+
for (const raw of input.messages) {
|
|
185
|
+
const message = record(raw);
|
|
186
|
+
if (Object.keys(message).some((key) => ![
|
|
187
|
+
"role",
|
|
188
|
+
"content",
|
|
189
|
+
"tool_calls",
|
|
190
|
+
"tool_call_id",
|
|
191
|
+
"reasoning_content"
|
|
192
|
+
].includes(key))) throw new BridgeError("Only message role and content are supported", 400, "unsupported_parameter");
|
|
193
|
+
if (![
|
|
194
|
+
"system",
|
|
195
|
+
"developer",
|
|
196
|
+
"user",
|
|
197
|
+
"assistant",
|
|
198
|
+
"tool"
|
|
199
|
+
].includes(String(message.role))) throw new BridgeError("Only system, developer, user and assistant roles are supported");
|
|
200
|
+
if (message.role === "tool" && typeof message.tool_call_id !== "string") throw new BridgeError("Tool results require tool_call_id");
|
|
201
|
+
if (message.tool_calls !== void 0 && (message.role !== "assistant" || !Array.isArray(message.tool_calls))) throw new BridgeError("Invalid assistant tool_calls");
|
|
202
|
+
let content = message.content ?? (message.role === "assistant" && message.tool_calls ? "" : void 0);
|
|
203
|
+
if (Array.isArray(content)) content = content.map((part) => {
|
|
204
|
+
const block = record(part);
|
|
205
|
+
if (block.type === "text" && typeof block.text === "string") return block.text;
|
|
206
|
+
if (block.type === "image_url") {
|
|
207
|
+
if (message.role !== "user") throw new BridgeError("Images are only supported in user messages");
|
|
208
|
+
if (images.length >= 4) throw new BridgeError(`At most 4 images are supported`, 413);
|
|
209
|
+
const image = parseImagePart(block);
|
|
210
|
+
const label = `Image ${images.length + 1}`;
|
|
211
|
+
images.push({
|
|
212
|
+
...image,
|
|
213
|
+
label
|
|
214
|
+
});
|
|
215
|
+
return `[${label}]`;
|
|
216
|
+
}
|
|
217
|
+
throw new BridgeError("Only text and image_url content blocks are supported", 400, "unsupported_parameter");
|
|
218
|
+
}).join("\n");
|
|
219
|
+
if (typeof content !== "string") throw new BridgeError("Message content must be text");
|
|
220
|
+
if (message.role === "system" || message.role === "developer") system.push(content);
|
|
221
|
+
else conversation.push({
|
|
222
|
+
role: String(message.role),
|
|
223
|
+
content,
|
|
224
|
+
...Array.isArray(message.tool_calls) ? { tool_calls: message.tool_calls } : {},
|
|
225
|
+
...typeof message.tool_call_id === "string" ? { tool_call_id: message.tool_call_id } : {}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
if (!["user", "tool"].includes(conversation.at(-1)?.role ?? "")) throw new BridgeError("The last conversation message must be from the user");
|
|
229
|
+
if (Buffer.byteLength(system.join("\n") + JSON.stringify(conversation) + JSON.stringify(tools)) > 1024 * 1024) throw new BridgeError("Text exceeds the 1 MiB limit", 413);
|
|
230
|
+
const history = conversation.slice(0, -1);
|
|
231
|
+
const prompt = history.length ? `Previous conversation (JSON data):\n${JSON.stringify(history)}\n\nCurrent message (JSON data):\n${conversation.at(-1)?.role === "tool" ? JSON.stringify(conversation.at(-1)) : conversation.at(-1).content}` : conversation[0].content;
|
|
232
|
+
return {
|
|
233
|
+
model: input.model.trim(),
|
|
234
|
+
stream: input.stream === true,
|
|
235
|
+
includeUsage: streamOptions.include_usage === true,
|
|
236
|
+
systemPrompt: system.join("\n\n") || "Answer the user's question. Use only the provided client tools when needed.",
|
|
237
|
+
prompt,
|
|
238
|
+
tools,
|
|
239
|
+
...input.context_window !== void 0 ? { contextWindow: Number(input.context_window) } : {},
|
|
240
|
+
...typeof input.reasoning_effort === "string" ? { reasoningEffort: input.reasoning_effort } : {},
|
|
241
|
+
...images.length ? { images } : {}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function completionIdentity(model) {
|
|
245
|
+
return {
|
|
246
|
+
id: `chatcmpl-${randomUUID()}`,
|
|
247
|
+
created: Math.floor(Date.now() / 1e3),
|
|
248
|
+
model
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
/** Do not expose SDK error text: it may contain authentication responses or local paths. */
|
|
252
|
+
function publicError(error) {
|
|
253
|
+
if (error instanceof BridgeError) return error;
|
|
254
|
+
return new BridgeError("Qoder request failed. Check the local CLI login or Qoder PAT and network settings.", 502, "qoder_upstream_error");
|
|
255
|
+
}
|
|
256
|
+
//#endregion
|
|
257
|
+
export { MAX_IMAGE_REQUEST_BYTES as a, makeImage as c, BridgeError as d, record as f, MAX_IMAGE_BYTES as i, validateImageUrl as l, parseChat as n, MAX_TOTAL_IMAGE_BYTES as o, publicError as r, decodeDataImage as s, completionIdentity as t, clientToolServer as u };
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { O as getResponsesTransportConfig, u as isQoderSdkProvider } from "./config-DnMKoKrg.js";
|
|
2
|
+
import { B as fetchResponsesWithLifecycle, K as state, L as requestContext, f as parseUserIdMetadata } from "./token-CQftvMU4.js";
|
|
3
|
+
import consola from "consola";
|
|
4
|
+
import { fetch as fetch$1, getGlobalDispatcher } from "undici";
|
|
5
|
+
//#region src/lib/models.ts
|
|
6
|
+
/**
|
|
7
|
+
* Converts a Copilot upstream model ID to a client-friendly ID that Claude Code
|
|
8
|
+
* and Claude Desktop recognize (dots in version replaced with hyphens).
|
|
9
|
+
* e.g. "claude-sonnet-4.6" -> "claude-sonnet-4-6"
|
|
10
|
+
* Non-Claude models are returned unchanged.
|
|
11
|
+
*/
|
|
12
|
+
const toClientModelId = (modelId) => {
|
|
13
|
+
const normalized = normalizeSdkModelId(modelId);
|
|
14
|
+
if (!normalized) return modelId;
|
|
15
|
+
const versionHyphenated = normalized.version.replaceAll(".", "-");
|
|
16
|
+
return `claude-${normalized.family}-${versionHyphenated}`;
|
|
17
|
+
};
|
|
18
|
+
const findEndpointModel = (sdkModelId) => {
|
|
19
|
+
const models = state.models?.data ?? [];
|
|
20
|
+
const exactMatch = models.find((m) => m.id === sdkModelId);
|
|
21
|
+
if (exactMatch) return exactMatch;
|
|
22
|
+
const normalized = normalizeSdkModelId(sdkModelId);
|
|
23
|
+
if (!normalized) return;
|
|
24
|
+
const modelName = `claude-${normalized.family}-${normalized.version}`;
|
|
25
|
+
const model = models.find((m) => m.id === modelName);
|
|
26
|
+
if (model) return model;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Finds the latest available model for a given Claude family (e.g. "opus",
|
|
30
|
+
* "sonnet", "haiku") among the models currently cached in `state.models`.
|
|
31
|
+
* "Latest" is determined by the highest semantic version parsed from the model
|
|
32
|
+
* ID. Returns `undefined` when no model of that family is available.
|
|
33
|
+
*/
|
|
34
|
+
const getLatestModelForFamily = (family) => {
|
|
35
|
+
const models = state.models?.data ?? [];
|
|
36
|
+
let best;
|
|
37
|
+
for (const model of models) {
|
|
38
|
+
const normalized = normalizeSdkModelId(model.id);
|
|
39
|
+
if (!normalized || normalized.family !== family) continue;
|
|
40
|
+
const [majorPart, minorPart = "0"] = normalized.version.split(".");
|
|
41
|
+
const major = Number.parseInt(majorPart, 10);
|
|
42
|
+
const minor = Number.parseInt(minorPart, 10);
|
|
43
|
+
if (Number.isNaN(major) || Number.isNaN(minor)) continue;
|
|
44
|
+
if (!best || major > best.major || major === best.major && minor > best.minor) best = {
|
|
45
|
+
model,
|
|
46
|
+
major,
|
|
47
|
+
minor
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return best?.model;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Normalizes an SDK model ID to extract the model family and version.
|
|
54
|
+
* this method from github copilot extension
|
|
55
|
+
* Examples:
|
|
56
|
+
* - "claude-opus-4-5-20251101" -> { family: "opus", version: "4.5" }
|
|
57
|
+
* - "claude-3-5-sonnet-20241022" -> { family: "sonnet", version: "3.5" }
|
|
58
|
+
* - "claude-sonnet-4-20250514" -> { family: "sonnet", version: "4" }
|
|
59
|
+
* - "claude-haiku-3-5-20250514" -> { family: "haiku", version: "3.5" }
|
|
60
|
+
* - "claude-haiku-4.5" -> { family: "haiku", version: "4.5" }
|
|
61
|
+
*/
|
|
62
|
+
const normalizeSdkModelId = (sdkModelId) => {
|
|
63
|
+
const withoutDate = sdkModelId.toLowerCase().replace(/-\d{8}$/, "");
|
|
64
|
+
const pattern1 = withoutDate.match(/^claude-(\w+)-(\d+)\.(\d+)$/);
|
|
65
|
+
if (pattern1) return {
|
|
66
|
+
family: pattern1[1],
|
|
67
|
+
version: `${pattern1[2]}.${pattern1[3]}`
|
|
68
|
+
};
|
|
69
|
+
const pattern2 = withoutDate.match(/^claude-(\w+)-(\d+)-(\d+)$/);
|
|
70
|
+
if (pattern2) return {
|
|
71
|
+
family: pattern2[1],
|
|
72
|
+
version: `${pattern2[2]}.${pattern2[3]}`
|
|
73
|
+
};
|
|
74
|
+
const pattern3 = withoutDate.match(/^claude-(\d+)-(\d+)-(\w+)$/);
|
|
75
|
+
if (pattern3) return {
|
|
76
|
+
family: pattern3[3],
|
|
77
|
+
version: `${pattern3[1]}.${pattern3[2]}`
|
|
78
|
+
};
|
|
79
|
+
const pattern4 = withoutDate.match(/^claude-(\w+)-(\d+)$/);
|
|
80
|
+
if (pattern4) return {
|
|
81
|
+
family: pattern4[1],
|
|
82
|
+
version: pattern4[2]
|
|
83
|
+
};
|
|
84
|
+
const pattern5 = withoutDate.match(/^claude-(\d+)-(\w+)$/);
|
|
85
|
+
if (pattern5) return {
|
|
86
|
+
family: pattern5[2],
|
|
87
|
+
version: pattern5[1]
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/lib/timeout-dispatcher.ts
|
|
92
|
+
/**
|
|
93
|
+
* Node's global fetch keeps Undici's shorter default headers/body timeouts.
|
|
94
|
+
* Wrap the global dispatcher so long-running requests (e.g. image
|
|
95
|
+
* generation) get an explicit cap.
|
|
96
|
+
*/
|
|
97
|
+
const createTimeoutDispatcher = (timeoutMs) => ({ dispatch(options, handler) {
|
|
98
|
+
return getGlobalDispatcher().dispatch({
|
|
99
|
+
...options,
|
|
100
|
+
bodyTimeout: timeoutMs,
|
|
101
|
+
headersTimeout: timeoutMs
|
|
102
|
+
}, handler);
|
|
103
|
+
} });
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/services/providers/provider-proxy.ts
|
|
106
|
+
async function providerFetch(provider, path, init) {
|
|
107
|
+
if (isQoderSdkProvider(provider)) {
|
|
108
|
+
const { forwardQoder } = await import("./transport-HOWgkXsv.js");
|
|
109
|
+
return forwardQoder(provider, path, init);
|
|
110
|
+
}
|
|
111
|
+
return fetch(`${provider.baseUrl}${path}`, init);
|
|
112
|
+
}
|
|
113
|
+
const SHARED_FORWARDABLE_HEADERS = ["accept", "user-agent"];
|
|
114
|
+
const ANTHROPIC_FORWARDABLE_HEADERS = ["anthropic-version", "anthropic-beta"];
|
|
115
|
+
const STRIPPED_RESPONSE_HEADERS = [
|
|
116
|
+
"connection",
|
|
117
|
+
"content-encoding",
|
|
118
|
+
"content-length",
|
|
119
|
+
"keep-alive",
|
|
120
|
+
"proxy-authenticate",
|
|
121
|
+
"proxy-authorization",
|
|
122
|
+
"te",
|
|
123
|
+
"trailer",
|
|
124
|
+
"transfer-encoding",
|
|
125
|
+
"upgrade"
|
|
126
|
+
];
|
|
127
|
+
function buildProviderUpstreamHeaders(providerConfig, requestHeaders) {
|
|
128
|
+
const authHeaders = {};
|
|
129
|
+
if (providerConfig.authType === "x-api-key") authHeaders["x-api-key"] = providerConfig.apiKey;
|
|
130
|
+
else authHeaders.authorization = `Bearer ${providerConfig.apiKey}`;
|
|
131
|
+
const headers = {
|
|
132
|
+
"content-type": "application/json",
|
|
133
|
+
accept: "application/json",
|
|
134
|
+
...authHeaders
|
|
135
|
+
};
|
|
136
|
+
for (const headerName of SHARED_FORWARDABLE_HEADERS) {
|
|
137
|
+
const headerValue = requestHeaders.get(headerName);
|
|
138
|
+
if (headerValue) headers[headerName] = headerValue;
|
|
139
|
+
}
|
|
140
|
+
if (providerConfig.type !== "anthropic") return headers;
|
|
141
|
+
for (const headerName of ANTHROPIC_FORWARDABLE_HEADERS) {
|
|
142
|
+
const headerValue = requestHeaders.get(headerName);
|
|
143
|
+
if (headerValue) headers[headerName] = headerValue;
|
|
144
|
+
}
|
|
145
|
+
return headers;
|
|
146
|
+
}
|
|
147
|
+
const OPENCODE_GO_PROVIDER_NAME = "opencode-go";
|
|
148
|
+
const OPENCODE_SESSION_HEADER = "x-opencode-session";
|
|
149
|
+
const resolveOpencodeMessagesSession = (payload) => {
|
|
150
|
+
const sessionAffinity = requestContext.getStore()?.sessionAffinity?.trim();
|
|
151
|
+
if (sessionAffinity) return sessionAffinity;
|
|
152
|
+
const userId = payload.metadata?.user_id;
|
|
153
|
+
if (!userId?.trim()) return;
|
|
154
|
+
const { sessionId } = parseUserIdMetadata(userId);
|
|
155
|
+
return sessionId ?? userId;
|
|
156
|
+
};
|
|
157
|
+
const applyOpencodeSessionHeader = (providerConfig, headers, session) => {
|
|
158
|
+
if (providerConfig.name !== OPENCODE_GO_PROVIDER_NAME || !session) return;
|
|
159
|
+
headers[OPENCODE_SESSION_HEADER] = session;
|
|
160
|
+
};
|
|
161
|
+
function createProviderProxyResponse(upstreamResponse, body) {
|
|
162
|
+
const headers = new Headers(upstreamResponse.headers);
|
|
163
|
+
for (const headerName of STRIPPED_RESPONSE_HEADERS) headers.delete(headerName);
|
|
164
|
+
return new Response(body ?? upstreamResponse.body, {
|
|
165
|
+
headers,
|
|
166
|
+
status: upstreamResponse.status,
|
|
167
|
+
statusText: upstreamResponse.statusText
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
async function forwardProviderMessages(providerConfig, payload, requestHeaders) {
|
|
171
|
+
consola.log(`<-- model: ${payload.model}`);
|
|
172
|
+
const headers = buildProviderUpstreamHeaders(providerConfig, requestHeaders);
|
|
173
|
+
applyOpencodeSessionHeader(providerConfig, headers, resolveOpencodeMessagesSession(payload));
|
|
174
|
+
return await providerFetch(providerConfig, "/v1/messages", {
|
|
175
|
+
method: "POST",
|
|
176
|
+
headers,
|
|
177
|
+
body: JSON.stringify(payload)
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
async function forwardProviderChatCompletions(providerConfig, payload, requestHeaders, options = {}) {
|
|
181
|
+
consola.log(`<-- model: ${payload.model}`);
|
|
182
|
+
const headers = buildProviderUpstreamHeaders(providerConfig, requestHeaders);
|
|
183
|
+
applyOpencodeSessionHeader(providerConfig, headers, payload.prompt_cache_key?.trim() || void 0);
|
|
184
|
+
return await providerFetch(providerConfig, "/v1/chat/completions", {
|
|
185
|
+
method: "POST",
|
|
186
|
+
signal: options.signal,
|
|
187
|
+
headers,
|
|
188
|
+
body: JSON.stringify(payload)
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
async function forwardProviderResponses(providerConfig, payload, requestHeaders, options = {}) {
|
|
192
|
+
consola.log(`<-- model: ${payload.model}`);
|
|
193
|
+
const transportConfig = getResponsesTransportConfig();
|
|
194
|
+
const headers = buildProviderUpstreamHeaders(providerConfig, requestHeaders);
|
|
195
|
+
applyOpencodeSessionHeader(providerConfig, headers, payload.prompt_cache_key?.trim() || void 0);
|
|
196
|
+
if (isQoderSdkProvider(providerConfig)) return providerFetch(providerConfig, "/v1/responses", {
|
|
197
|
+
method: "POST",
|
|
198
|
+
signal: options.signal
|
|
199
|
+
});
|
|
200
|
+
return await fetchResponsesWithLifecycle(`${providerConfig.baseUrl}/v1/responses`, {
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers,
|
|
203
|
+
body: JSON.stringify(payload)
|
|
204
|
+
}, {
|
|
205
|
+
headersTimeoutMs: transportConfig.headersTimeoutMs,
|
|
206
|
+
signal: options.signal,
|
|
207
|
+
streamInactivityTimeoutMs: transportConfig.streamInactivityTimeoutMs
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
const PROVIDER_MODELS_TIMEOUT_MS = 15e3;
|
|
211
|
+
async function forwardProviderModels(providerConfig, requestHeaders) {
|
|
212
|
+
return await providerFetch(providerConfig, "/v1/models", {
|
|
213
|
+
method: "GET",
|
|
214
|
+
headers: buildProviderUpstreamHeaders(providerConfig, requestHeaders),
|
|
215
|
+
signal: AbortSignal.timeout(PROVIDER_MODELS_TIMEOUT_MS)
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
/** Align with Codex images: long-running generation/edits need a generous cap. */
|
|
219
|
+
const PROVIDER_IMAGES_TIMEOUT_MS = 900 * 1e3;
|
|
220
|
+
const providerImagesDispatcher = createTimeoutDispatcher(PROVIDER_IMAGES_TIMEOUT_MS);
|
|
221
|
+
function resolveProviderRequestUrl(providerConfig, requestUrl, path) {
|
|
222
|
+
const upstreamUrl = new URL(`${providerConfig.baseUrl}${path}`);
|
|
223
|
+
upstreamUrl.search = new URL(requestUrl, "http://localhost").search;
|
|
224
|
+
return upstreamUrl.toString();
|
|
225
|
+
}
|
|
226
|
+
async function forwardProviderAlphaSearch(providerConfig, request) {
|
|
227
|
+
if (isQoderSdkProvider(providerConfig)) return providerFetch(providerConfig, "/unsupported", {
|
|
228
|
+
method: "POST",
|
|
229
|
+
signal: request.signal
|
|
230
|
+
});
|
|
231
|
+
const headers = buildProviderUpstreamHeaders(providerConfig, request.headers);
|
|
232
|
+
const body = await request.arrayBuffer();
|
|
233
|
+
return await fetch(resolveProviderRequestUrl(providerConfig, request.url, "/v1/alpha/search"), {
|
|
234
|
+
method: "POST",
|
|
235
|
+
headers,
|
|
236
|
+
body
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
async function forwardProviderImages(providerConfig, request, operation) {
|
|
240
|
+
if (isQoderSdkProvider(providerConfig)) return providerFetch(providerConfig, "/unsupported", {
|
|
241
|
+
method: "POST",
|
|
242
|
+
signal: request.signal
|
|
243
|
+
});
|
|
244
|
+
const headers = buildProviderUpstreamHeaders(providerConfig, request.headers);
|
|
245
|
+
const contentType = request.headers.get("content-type");
|
|
246
|
+
if (contentType) headers["content-type"] = contentType;
|
|
247
|
+
else if (operation === "edits") delete headers["content-type"];
|
|
248
|
+
const init = {
|
|
249
|
+
method: "POST",
|
|
250
|
+
headers,
|
|
251
|
+
body: request.body,
|
|
252
|
+
duplex: "half",
|
|
253
|
+
signal: AbortSignal.timeout(PROVIDER_IMAGES_TIMEOUT_MS)
|
|
254
|
+
};
|
|
255
|
+
const upstreamUrl = resolveProviderRequestUrl(providerConfig, request.url, `/v1/images/${operation}`);
|
|
256
|
+
if (typeof Bun !== "undefined") return await fetch(upstreamUrl, init);
|
|
257
|
+
return await fetch$1(upstreamUrl, {
|
|
258
|
+
...init,
|
|
259
|
+
dispatcher: providerImagesDispatcher
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
//#endregion
|
|
263
|
+
export { forwardProviderMessages as a, createTimeoutDispatcher as c, normalizeSdkModelId as d, toClientModelId as f, forwardProviderImages as i, findEndpointModel as l, forwardProviderAlphaSearch as n, forwardProviderModels as o, forwardProviderChatCompletions as r, forwardProviderResponses as s, createProviderProxyResponse as t, getLatestModelForFamily as u };
|