copillm 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/README.md +52 -0
- package/dist/agentconfig/apply.js +53 -0
- package/dist/agentconfig/load.js +163 -0
- package/dist/agentconfig/markerBlock.js +76 -0
- package/dist/agentconfig/render.js +317 -0
- package/dist/agentconfig/schema.js +65 -0
- package/dist/auth/copilotToken.js +122 -0
- package/dist/auth/credentials.js +221 -0
- package/dist/auth/deviceFlow.js +89 -0
- package/dist/auth/ensureAuthenticated.js +55 -0
- package/dist/auth/githubIdentity.js +42 -0
- package/dist/auth/interactivePrompt.js +135 -0
- package/dist/claude/cache.js +20 -0
- package/dist/claude/settingsConflict.js +85 -0
- package/dist/cli/agentEnv.js +56 -0
- package/dist/cli/configCommands.js +149 -0
- package/dist/cli/envBlock.js +43 -0
- package/dist/cli/launchAgent.js +59 -0
- package/dist/cli/resolveAgent.js +361 -0
- package/dist/cli.js +1178 -0
- package/dist/codex/init.js +93 -0
- package/dist/config/config.js +51 -0
- package/dist/config/fsSecurity.js +39 -0
- package/dist/config/home.js +62 -0
- package/dist/config/logging.js +33 -0
- package/dist/config/upstream.js +38 -0
- package/dist/models/anthropicDefaults.js +138 -0
- package/dist/models/discovery.js +208 -0
- package/dist/pi/init.js +174 -0
- package/dist/server/anthropicModelsResponse.js +151 -0
- package/dist/server/codexSchema.js +100 -0
- package/dist/server/debugInfo.js +48 -0
- package/dist/server/lock.js +150 -0
- package/dist/server/proxy.js +715 -0
- package/dist/translation/openaiAnthropic.js +391 -0
- package/dist/translation/streamingOpenAIToAnthropic.js +290 -0
- package/dist/types/index.js +1 -0
- package/package.json +50 -0
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
4
|
+
import { Readable } from "node:stream";
|
|
5
|
+
import { pipeline } from "node:stream/promises";
|
|
6
|
+
import { accountBaseUrl, listModels, listModelsUnion, resolveModelId } from "../models/discovery.js";
|
|
7
|
+
import { CopilotTokenExchangeError, CopilotTokenManagerError } from "../auth/copilotToken.js";
|
|
8
|
+
import { anthropicToOpenAI, openAIToAnthropic, ProtocolTranslationError, stripOneMillionAlias } from "../translation/openaiAnthropic.js";
|
|
9
|
+
import { translateOpenAIStreamToAnthropic, writeAnthropicPrelude } from "../translation/streamingOpenAIToAnthropic.js";
|
|
10
|
+
import { buildCodexCatalog } from "./codexSchema.js";
|
|
11
|
+
import { getGithubUserSummary, GithubUserFetchError } from "./debugInfo.js";
|
|
12
|
+
import { buildAnthropicModelsResponse } from "./anthropicModelsResponse.js";
|
|
13
|
+
const COPILOT_HEADERS = {
|
|
14
|
+
"Content-Type": "application/json",
|
|
15
|
+
"Copilot-Integration-Id": "vscode-chat",
|
|
16
|
+
"Editor-Version": "vscode/1.95.0",
|
|
17
|
+
"Editor-Plugin-Version": "copilot-chat/0.26.7",
|
|
18
|
+
"User-Agent": "GitHubCopilotChat/0.26.7",
|
|
19
|
+
"Openai-Intent": "conversation-panel",
|
|
20
|
+
"X-GitHub-Api-Version": "2025-04-01",
|
|
21
|
+
"X-VScode-User-Agent-Library-Version": "electron-fetch",
|
|
22
|
+
// Disable gzip/br on the upstream response. Compressed SSE streams get
|
|
23
|
+
// buffered at gzip flush boundaries by undici's decoder, which causes
|
|
24
|
+
// visible "freeze then dump a paragraph" behaviour in Claude Code and
|
|
25
|
+
// other Anthropic-shape clients. Identity encoding lets each SSE event
|
|
26
|
+
// flow through immediately.
|
|
27
|
+
"Accept-Encoding": "identity"
|
|
28
|
+
};
|
|
29
|
+
const HEALTH_REFRESH_THRESHOLD_SECONDS = 60;
|
|
30
|
+
const RETRYABLE_UPSTREAM_STATUSES = new Set([408, 409, 425, 429, 500, 502, 503, 504]);
|
|
31
|
+
const MAX_UPSTREAM_ATTEMPTS = 3;
|
|
32
|
+
const BASE_BACKOFF_MS = 200;
|
|
33
|
+
const DAEMON_STARTED_AT_ISO = new Date().toISOString();
|
|
34
|
+
class JsonRequestParseError extends Error {
|
|
35
|
+
constructor(message) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = "JsonRequestParseError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
class InvalidRequestShapeError extends Error {
|
|
41
|
+
constructor(message) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = "InvalidRequestShapeError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function startProxyServer(input) {
|
|
47
|
+
const debugEnabled = input.debug === true;
|
|
48
|
+
const server = createServer(async (req, res) => {
|
|
49
|
+
const requestId = randomUUID();
|
|
50
|
+
const startedAt = Date.now();
|
|
51
|
+
const pathname = safePathname(req.url);
|
|
52
|
+
res.on("finish", () => {
|
|
53
|
+
input.logger.info({
|
|
54
|
+
event: "http_request",
|
|
55
|
+
request_id: requestId,
|
|
56
|
+
method: req.method ?? "UNKNOWN",
|
|
57
|
+
path: pathname,
|
|
58
|
+
status_code: res.statusCode,
|
|
59
|
+
duration_ms: Date.now() - startedAt
|
|
60
|
+
}, "request completed");
|
|
61
|
+
});
|
|
62
|
+
try {
|
|
63
|
+
if (!isLocalRequest(req)) {
|
|
64
|
+
sendJson(res, 403, { error: "non_loopback_request_rejected" });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const route = resolveRoute(req.method, req.url);
|
|
68
|
+
if (input.config.requireCallerSecret && route.kind !== "livez" && route.kind !== "healthz") {
|
|
69
|
+
const auth = req.headers.authorization;
|
|
70
|
+
if (!input.callerSecret || auth !== `Bearer ${input.callerSecret}`) {
|
|
71
|
+
sendJson(res, 401, { error: "invalid_caller_secret" });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (route.kind === "livez") {
|
|
76
|
+
sendJson(res, 200, { status: "ok", uptime_seconds: Math.floor(process.uptime()) });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (route.kind === "healthz") {
|
|
80
|
+
const ttl = input.tokenManager.expiresInSeconds();
|
|
81
|
+
if (ttl !== null && ttl > HEALTH_REFRESH_THRESHOLD_SECONDS) {
|
|
82
|
+
sendJson(res, 200, {
|
|
83
|
+
status: "ok",
|
|
84
|
+
token_state: "fresh",
|
|
85
|
+
refresh_threshold_seconds: HEALTH_REFRESH_THRESHOLD_SECONDS,
|
|
86
|
+
bearer_ttl_seconds: ttl
|
|
87
|
+
});
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
await input.tokenManager.ensureToken({ refreshThresholdSeconds: HEALTH_REFRESH_THRESHOLD_SECONDS });
|
|
92
|
+
const refreshedTtl = input.tokenManager.expiresInSeconds() ?? 0;
|
|
93
|
+
sendJson(res, 200, {
|
|
94
|
+
status: "ok",
|
|
95
|
+
token_state: "refreshed",
|
|
96
|
+
refresh_threshold_seconds: HEALTH_REFRESH_THRESHOLD_SECONDS,
|
|
97
|
+
bearer_ttl_seconds: refreshedTtl
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
const failed = healthFailure(error);
|
|
102
|
+
sendJson(res, failed.httpStatus, failed.payload);
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (route.kind === "models" || route.kind === "codex_models" || route.kind === "anthropic_models") {
|
|
107
|
+
try {
|
|
108
|
+
await input.tokenManager.ensureToken(false);
|
|
109
|
+
const githubToken = input.githubToken;
|
|
110
|
+
if (!githubToken) {
|
|
111
|
+
sendJson(res, 503, { error: "github_token_unavailable" });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const result = route.kind === "codex_models" || route.kind === "anthropic_models"
|
|
115
|
+
? await listModelsUnion(input.config.accountType, githubToken, 3)
|
|
116
|
+
: await listModels(input.config.accountType, githubToken);
|
|
117
|
+
if (route.kind === "codex_models") {
|
|
118
|
+
sendJson(res, 200, buildCodexCatalog(result.models));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (route.kind === "anthropic_models") {
|
|
122
|
+
sendJson(res, 200, buildAnthropicModelsResponse(result.models));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
sendJson(res, 200, {
|
|
126
|
+
models: result.models,
|
|
127
|
+
discovery: {
|
|
128
|
+
source: result.source,
|
|
129
|
+
stale: result.stale,
|
|
130
|
+
cache_age_seconds: result.cacheAgeSeconds,
|
|
131
|
+
warning: result.warning
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (error instanceof CopilotTokenManagerError) {
|
|
137
|
+
sendJson(res, 503, { error: "token_refresh_failed" });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (route.kind === "debug") {
|
|
145
|
+
if (!debugEnabled) {
|
|
146
|
+
sendJson(res, 404, { error: "not_found" });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
await handleDebug(res, {
|
|
150
|
+
config: input.config,
|
|
151
|
+
tokenManager: input.tokenManager,
|
|
152
|
+
githubToken: input.githubToken,
|
|
153
|
+
port: input.port
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (route.kind === "not_found") {
|
|
158
|
+
sendJson(res, 404, { error: "not_found" });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const requestBody = await readJson(req);
|
|
162
|
+
const translatedBody = translateRequestBody(route.kind, requestBody);
|
|
163
|
+
const requestedModel = readRequestedModel(translatedBody);
|
|
164
|
+
if (input.config.selectedModels.length > 0 && !requestedModel) {
|
|
165
|
+
sendJson(res, 400, {
|
|
166
|
+
error: "model_not_selected",
|
|
167
|
+
detail: "Requested model is not enabled in local selection."
|
|
168
|
+
});
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
let resolvedModel = null;
|
|
172
|
+
try {
|
|
173
|
+
resolvedModel = requestedModel ? resolveModelId(requestedModel, input.config.selectedModels) : null;
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
const detail = error instanceof Error ? error.message : "Model resolution failed.";
|
|
177
|
+
sendJson(res, 400, { error: "ambiguous_model_selection", detail });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (input.config.selectedModels.length > 0 && !resolvedModel) {
|
|
181
|
+
sendJson(res, 400, {
|
|
182
|
+
error: "model_not_selected",
|
|
183
|
+
detail: "Requested model is not enabled in local selection."
|
|
184
|
+
});
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const upstreamBody = resolvedModel ? rewriteRequestedModel(translatedBody, resolvedModel.id) : translatedBody;
|
|
188
|
+
const upstreamPath = route.kind === "codex_responses" ? "/responses" : "/chat/completions";
|
|
189
|
+
const isAnthropicStreaming = route.anthroShape && isStreamingRequestBody(translatedBody);
|
|
190
|
+
let prelude = null;
|
|
191
|
+
if (isAnthropicStreaming) {
|
|
192
|
+
beginAnthropicSseResponse(res, req);
|
|
193
|
+
prelude = writeAnthropicPrelude(res, requestedModel ?? "");
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
const upstream = await postToCopilot({
|
|
197
|
+
tokenManager: input.tokenManager,
|
|
198
|
+
accountType: input.config.accountType,
|
|
199
|
+
body: upstreamBody,
|
|
200
|
+
requestId,
|
|
201
|
+
logger: input.logger,
|
|
202
|
+
upstreamPath
|
|
203
|
+
});
|
|
204
|
+
await forwardResponse(upstream, route.anthroShape, res, requestedModel ?? undefined, prelude);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
if (error instanceof CopilotTokenManagerError) {
|
|
208
|
+
if (prelude) {
|
|
209
|
+
writeAnthropicSseError(res, prelude, "token_refresh_failed");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
sendJson(res, 503, { error: "token_refresh_failed" });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (prelude) {
|
|
216
|
+
writeAnthropicSseError(res, prelude, "internal_error");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
if (error instanceof JsonRequestParseError) {
|
|
224
|
+
sendJson(res, 400, { error: "invalid_request_json", detail: error.message });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (error instanceof InvalidRequestShapeError) {
|
|
228
|
+
sendJson(res, 400, { error: "invalid_request_shape", detail: error.message });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (error instanceof ProtocolTranslationError) {
|
|
232
|
+
sendJson(res, 400, { error: error.code, detail: error.message });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
input.logger.error({ err: error }, "request failed");
|
|
236
|
+
sendJson(res, 500, { error: "internal_error" });
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
await new Promise((resolve, reject) => {
|
|
240
|
+
server.listen(input.port, "127.0.0.1", () => resolve());
|
|
241
|
+
server.on("error", reject);
|
|
242
|
+
});
|
|
243
|
+
return {
|
|
244
|
+
close: async () => new Promise((resolve, reject) => {
|
|
245
|
+
server.close((error) => {
|
|
246
|
+
if (error) {
|
|
247
|
+
reject(error);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
resolve();
|
|
251
|
+
});
|
|
252
|
+
})
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
async function postToCopilot(input) {
|
|
256
|
+
let forceRefresh = false;
|
|
257
|
+
let authRefreshRetried = false;
|
|
258
|
+
for (let attempt = 1; attempt <= MAX_UPSTREAM_ATTEMPTS; attempt += 1) {
|
|
259
|
+
try {
|
|
260
|
+
const response = await postWithCurrentBearer(input.tokenManager, input.accountType, input.body, forceRefresh, input.requestId, input.upstreamPath);
|
|
261
|
+
forceRefresh = false;
|
|
262
|
+
if (response.status === 401 && !authRefreshRetried && attempt < MAX_UPSTREAM_ATTEMPTS) {
|
|
263
|
+
authRefreshRetried = true;
|
|
264
|
+
forceRefresh = true;
|
|
265
|
+
input.logger.warn({ event: "upstream_retry", request_id: input.requestId, attempt, reason: "upstream_auth_401" }, "retrying upstream request after forced token refresh");
|
|
266
|
+
await discardUpstreamBody(response);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (isRetryableStatus(response.status) && attempt < MAX_UPSTREAM_ATTEMPTS) {
|
|
270
|
+
input.logger.warn({ event: "upstream_retry", request_id: input.requestId, attempt, status_code: response.status }, "retrying upstream request");
|
|
271
|
+
await discardUpstreamBody(response);
|
|
272
|
+
await sleep(retryDelayMs(attempt));
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
return response;
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
if (!isRetryableTransportError(error) || attempt >= MAX_UPSTREAM_ATTEMPTS) {
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
input.logger.warn({ event: "upstream_retry", request_id: input.requestId, attempt, reason: "transport_error" }, "retrying upstream request after transport error");
|
|
282
|
+
await sleep(retryDelayMs(attempt));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
throw new Error("Upstream retry budget exhausted unexpectedly.");
|
|
286
|
+
}
|
|
287
|
+
async function postWithCurrentBearer(tokenManager, accountType, body, forceRefresh, requestId, upstreamPath) {
|
|
288
|
+
const bearer = await tokenManager.ensureToken({ forceRefresh });
|
|
289
|
+
return fetch(`${accountBaseUrl(accountType)}${upstreamPath}`, {
|
|
290
|
+
method: "POST",
|
|
291
|
+
headers: {
|
|
292
|
+
...COPILOT_HEADERS,
|
|
293
|
+
Authorization: `Bearer ${bearer}`,
|
|
294
|
+
"X-Request-Id": requestId
|
|
295
|
+
},
|
|
296
|
+
body: JSON.stringify(body)
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
async function forwardResponse(upstream, anthroShape, res, requestedModel, prelude) {
|
|
300
|
+
if (!upstream.ok) {
|
|
301
|
+
await discardUpstreamBody(upstream);
|
|
302
|
+
if (prelude) {
|
|
303
|
+
writeAnthropicSseError(res, prelude, upstreamStatusCategory(upstream.status));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
sendJson(res, upstream.status, { error: upstreamStatusCategory(upstream.status) });
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (isEventStream(upstream)) {
|
|
310
|
+
if (anthroShape) {
|
|
311
|
+
if (!upstream.body) {
|
|
312
|
+
if (prelude) {
|
|
313
|
+
writeAnthropicSseError(res, prelude, "invalid_upstream_response");
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
sendJson(res, 502, { error: "invalid_upstream_response", detail: "Upstream stream body is missing." });
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (!prelude) {
|
|
320
|
+
beginAnthropicSseResponse(res);
|
|
321
|
+
}
|
|
322
|
+
const upstreamReadable = Readable.fromWeb(upstream.body);
|
|
323
|
+
await translateOpenAIStreamToAnthropic({
|
|
324
|
+
upstream: upstreamReadable,
|
|
325
|
+
downstream: res,
|
|
326
|
+
fallbackModel: requestedModel,
|
|
327
|
+
preEmittedMessageId: prelude?.messageId
|
|
328
|
+
});
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
await pipeEventStream(upstream, res);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (prelude) {
|
|
335
|
+
writeAnthropicSseError(res, prelude, "invalid_upstream_response");
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
let json;
|
|
339
|
+
try {
|
|
340
|
+
json = (await upstream.json());
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
sendJson(res, 502, { error: "invalid_upstream_response" });
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
let payload = json;
|
|
347
|
+
if (anthroShape) {
|
|
348
|
+
try {
|
|
349
|
+
payload = openAIToAnthropic(json);
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
if (error instanceof ProtocolTranslationError) {
|
|
353
|
+
sendJson(res, 502, { error: error.code, detail: error.message });
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
sendJson(res, 200, payload);
|
|
360
|
+
}
|
|
361
|
+
async function pipeEventStream(upstream, res) {
|
|
362
|
+
if (!upstream.body) {
|
|
363
|
+
sendJson(res, 502, { error: "invalid_upstream_response", detail: "Upstream stream body is missing." });
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
res.statusCode = upstream.status;
|
|
367
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
368
|
+
const cacheControl = upstream.headers.get("cache-control");
|
|
369
|
+
if (cacheControl) {
|
|
370
|
+
res.setHeader("Cache-Control", cacheControl);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
374
|
+
}
|
|
375
|
+
const connection = upstream.headers.get("connection");
|
|
376
|
+
if (connection) {
|
|
377
|
+
res.setHeader("Connection", connection);
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
res.setHeader("Connection", "keep-alive");
|
|
381
|
+
}
|
|
382
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
383
|
+
if (typeof res.flushHeaders === "function") {
|
|
384
|
+
res.flushHeaders();
|
|
385
|
+
}
|
|
386
|
+
if (res.socket && typeof res.socket.setNoDelay === "function") {
|
|
387
|
+
res.socket.setNoDelay(true);
|
|
388
|
+
}
|
|
389
|
+
await pipeline(Readable.fromWeb(upstream.body), res);
|
|
390
|
+
}
|
|
391
|
+
function isEventStream(upstream) {
|
|
392
|
+
const contentType = upstream.headers.get("content-type");
|
|
393
|
+
return typeof contentType === "string" && contentType.toLowerCase().includes("text/event-stream");
|
|
394
|
+
}
|
|
395
|
+
function isStreamingRequestBody(body) {
|
|
396
|
+
return typeof body === "object" && body !== null && body.stream === true;
|
|
397
|
+
}
|
|
398
|
+
function beginAnthropicSseResponse(res, req) {
|
|
399
|
+
if (res.headersSent) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
res.statusCode = 200;
|
|
403
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
404
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
405
|
+
res.setHeader("Connection", "keep-alive");
|
|
406
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
407
|
+
if (typeof res.flushHeaders === "function") {
|
|
408
|
+
res.flushHeaders();
|
|
409
|
+
}
|
|
410
|
+
const socket = res.socket ?? req?.socket;
|
|
411
|
+
if (socket && typeof socket.setNoDelay === "function") {
|
|
412
|
+
socket.setNoDelay(true);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
export function writeAnthropicSseError(res, prelude, code) {
|
|
416
|
+
void prelude;
|
|
417
|
+
try {
|
|
418
|
+
res.write(`event: message_delta\ndata: ${JSON.stringify({
|
|
419
|
+
type: "message_delta",
|
|
420
|
+
delta: { stop_reason: "end_turn", stop_sequence: null },
|
|
421
|
+
usage: { input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0 }
|
|
422
|
+
})}\n\n`);
|
|
423
|
+
res.write(`event: error\ndata: ${JSON.stringify({ type: "error", error: { type: "api_error", message: code } })}\n\n`);
|
|
424
|
+
res.write(`event: message_stop\ndata: ${JSON.stringify({ type: "message_stop" })}\n\n`);
|
|
425
|
+
}
|
|
426
|
+
finally {
|
|
427
|
+
res.end();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
function isLocalRequest(req) {
|
|
431
|
+
const remote = req.socket.remoteAddress ?? "";
|
|
432
|
+
const local = req.socket.localAddress ?? "";
|
|
433
|
+
return isLoopbackAddress(remote) && (local.length === 0 || isLoopbackAddress(local));
|
|
434
|
+
}
|
|
435
|
+
function isLoopbackAddress(value) {
|
|
436
|
+
return value === "127.0.0.1" || value === "::1" || value === "::ffff:127.0.0.1";
|
|
437
|
+
}
|
|
438
|
+
async function readJson(req) {
|
|
439
|
+
const chunks = [];
|
|
440
|
+
for await (const chunk of req) {
|
|
441
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
442
|
+
}
|
|
443
|
+
if (chunks.length === 0) {
|
|
444
|
+
return {};
|
|
445
|
+
}
|
|
446
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
447
|
+
try {
|
|
448
|
+
return JSON.parse(text);
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
if (error instanceof SyntaxError) {
|
|
452
|
+
throw new JsonRequestParseError("Failed to parse JSON body.");
|
|
453
|
+
}
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
function sendJson(res, status, payload) {
|
|
458
|
+
res.statusCode = status;
|
|
459
|
+
res.setHeader("Content-Type", "application/json");
|
|
460
|
+
res.end(JSON.stringify(payload));
|
|
461
|
+
}
|
|
462
|
+
function readRequestedModel(payload) {
|
|
463
|
+
if (!payload || typeof payload !== "object") {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
const maybeModel = payload.model;
|
|
467
|
+
return typeof maybeModel === "string" ? maybeModel : null;
|
|
468
|
+
}
|
|
469
|
+
function rewriteRequestedModel(payload, model) {
|
|
470
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
471
|
+
return payload;
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
...payload,
|
|
475
|
+
model
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function translateRequestBody(routeKind, body) {
|
|
479
|
+
if (routeKind !== "anthropic") {
|
|
480
|
+
return normaliseAliasedModelInPlace(body);
|
|
481
|
+
}
|
|
482
|
+
try {
|
|
483
|
+
return anthropicToOpenAI(body);
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
if (error instanceof Error) {
|
|
487
|
+
throw new InvalidRequestShapeError(error.message);
|
|
488
|
+
}
|
|
489
|
+
throw new InvalidRequestShapeError("Invalid Anthropic request body.");
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Defensive strip of the `[1m]` alias for pass-through routes (OpenAI chat
|
|
494
|
+
* completions, Codex responses). The Anthropic route already strips inside
|
|
495
|
+
* `anthropicToOpenAI`; this catches anything that might land on the
|
|
496
|
+
* pass-through paths with a hand-pasted aliased id so upstream Copilot
|
|
497
|
+
* always receives the canonical model id.
|
|
498
|
+
*/
|
|
499
|
+
function normaliseAliasedModelInPlace(body) {
|
|
500
|
+
if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
501
|
+
const record = body;
|
|
502
|
+
if (typeof record.model === "string") {
|
|
503
|
+
const stripped = stripOneMillionAlias(record.model);
|
|
504
|
+
if (stripped !== record.model) {
|
|
505
|
+
record.model = stripped;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return body;
|
|
510
|
+
}
|
|
511
|
+
async function handleDebug(res, input) {
|
|
512
|
+
const bearerTtlSeconds = input.tokenManager.expiresInSeconds();
|
|
513
|
+
const uptimeSeconds = Math.max(0, Math.floor((Date.now() - Date.parse(DAEMON_STARTED_AT_ISO)) / 1_000));
|
|
514
|
+
let user = null;
|
|
515
|
+
let userError = null;
|
|
516
|
+
if (input.githubToken) {
|
|
517
|
+
try {
|
|
518
|
+
const summary = await getGithubUserSummary(input.githubToken);
|
|
519
|
+
user = {
|
|
520
|
+
login: summary.login,
|
|
521
|
+
id: summary.id,
|
|
522
|
+
name: summary.name,
|
|
523
|
+
email: summary.email,
|
|
524
|
+
type: summary.type,
|
|
525
|
+
avatar_url: summary.avatar_url,
|
|
526
|
+
html_url: summary.html_url,
|
|
527
|
+
plan_name: summary.plan_name
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
if (error instanceof GithubUserFetchError) {
|
|
532
|
+
userError = `github_user_lookup_failed_${error.status}`;
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
userError = error instanceof Error ? error.message : "unknown_error";
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
userError = "github_token_unavailable_in_proxy";
|
|
541
|
+
}
|
|
542
|
+
sendJson(res, 200, {
|
|
543
|
+
server: {
|
|
544
|
+
port: input.port,
|
|
545
|
+
pid: process.pid,
|
|
546
|
+
node_version: process.version,
|
|
547
|
+
started_at_iso: DAEMON_STARTED_AT_ISO,
|
|
548
|
+
uptime_seconds: uptimeSeconds,
|
|
549
|
+
account_type: input.config.accountType,
|
|
550
|
+
selected_models: input.config.selectedModels,
|
|
551
|
+
require_caller_secret: input.config.requireCallerSecret
|
|
552
|
+
},
|
|
553
|
+
auth: {
|
|
554
|
+
bearer_ttl_seconds: bearerTtlSeconds,
|
|
555
|
+
bearer_present: input.tokenManager.current !== null,
|
|
556
|
+
bearer_expires_at_unix: input.tokenManager.current?.expiresAtUnix ?? null
|
|
557
|
+
},
|
|
558
|
+
user,
|
|
559
|
+
user_error: userError,
|
|
560
|
+
routes: [
|
|
561
|
+
"GET /livez",
|
|
562
|
+
"GET /healthz",
|
|
563
|
+
"GET /models",
|
|
564
|
+
"GET /v1/models",
|
|
565
|
+
"GET /codex/v1/models",
|
|
566
|
+
"GET /anthropic/v1/models",
|
|
567
|
+
"POST /codex/v1/responses",
|
|
568
|
+
"POST /v1/chat/completions",
|
|
569
|
+
"POST /v1/messages",
|
|
570
|
+
"POST /anthropic/v1/messages",
|
|
571
|
+
"GET /_debug"
|
|
572
|
+
],
|
|
573
|
+
debug_enabled: true
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
function resolveRoute(method, rawUrl) {
|
|
577
|
+
if (!method || !rawUrl) {
|
|
578
|
+
return { kind: "not_found", anthroShape: false };
|
|
579
|
+
}
|
|
580
|
+
let pathname;
|
|
581
|
+
try {
|
|
582
|
+
pathname = new URL(rawUrl, "http://127.0.0.1").pathname;
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
return { kind: "not_found", anthroShape: false };
|
|
586
|
+
}
|
|
587
|
+
if (method === "GET" && pathname === "/livez") {
|
|
588
|
+
return { kind: "livez", anthroShape: false };
|
|
589
|
+
}
|
|
590
|
+
if (method === "GET" && pathname === "/healthz") {
|
|
591
|
+
return { kind: "healthz", anthroShape: false };
|
|
592
|
+
}
|
|
593
|
+
if (method === "GET" && (pathname === "/models" || pathname === "/v1/models")) {
|
|
594
|
+
return { kind: "models", anthroShape: false };
|
|
595
|
+
}
|
|
596
|
+
if (method === "GET" && pathname === "/codex/v1/models") {
|
|
597
|
+
return { kind: "codex_models", anthroShape: false };
|
|
598
|
+
}
|
|
599
|
+
if (method === "GET" && pathname === "/anthropic/v1/models") {
|
|
600
|
+
return { kind: "anthropic_models", anthroShape: false };
|
|
601
|
+
}
|
|
602
|
+
if (method === "POST" && pathname === "/codex/v1/responses") {
|
|
603
|
+
return { kind: "codex_responses", anthroShape: false };
|
|
604
|
+
}
|
|
605
|
+
if (method === "GET" && pathname === "/_debug") {
|
|
606
|
+
return { kind: "debug", anthroShape: false };
|
|
607
|
+
}
|
|
608
|
+
if (method === "POST" && pathname === "/v1/chat/completions") {
|
|
609
|
+
return { kind: "openai", anthroShape: false };
|
|
610
|
+
}
|
|
611
|
+
if (method === "POST" && (pathname === "/anthropic/v1/messages" || pathname === "/v1/messages")) {
|
|
612
|
+
return { kind: "anthropic", anthroShape: true };
|
|
613
|
+
}
|
|
614
|
+
return { kind: "not_found", anthroShape: false };
|
|
615
|
+
}
|
|
616
|
+
function isRetryableStatus(status) {
|
|
617
|
+
return RETRYABLE_UPSTREAM_STATUSES.has(status);
|
|
618
|
+
}
|
|
619
|
+
function retryDelayMs(attempt) {
|
|
620
|
+
return BASE_BACKOFF_MS * Math.pow(2, Math.max(0, attempt - 1));
|
|
621
|
+
}
|
|
622
|
+
function isRetryableTransportError(error) {
|
|
623
|
+
if (!error || typeof error !== "object") {
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
const typedError = error;
|
|
627
|
+
const directCode = typedError.code?.toUpperCase();
|
|
628
|
+
const causeCode = typedError.cause?.code?.toUpperCase();
|
|
629
|
+
if (directCode === "ECONNRESET" || directCode === "ECONNREFUSED" || directCode === "ETIMEDOUT") {
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
if (causeCode === "ECONNRESET" || causeCode === "ECONNREFUSED" || causeCode === "ETIMEDOUT") {
|
|
633
|
+
return true;
|
|
634
|
+
}
|
|
635
|
+
if (!(typedError instanceof Error)) {
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
const message = typedError.message.toLowerCase();
|
|
639
|
+
if (message.includes("timed out") || message.includes("timeout")) {
|
|
640
|
+
return true;
|
|
641
|
+
}
|
|
642
|
+
return message.includes("econnreset") || message.includes("econnrefused") || message.includes("enotfound");
|
|
643
|
+
}
|
|
644
|
+
function upstreamStatusCategory(status) {
|
|
645
|
+
if (status === 401 || status === 403) {
|
|
646
|
+
return "upstream_auth_error";
|
|
647
|
+
}
|
|
648
|
+
if (status === 429) {
|
|
649
|
+
return "upstream_rate_limited";
|
|
650
|
+
}
|
|
651
|
+
if (status >= 500) {
|
|
652
|
+
return "upstream_server_error";
|
|
653
|
+
}
|
|
654
|
+
if (status >= 400) {
|
|
655
|
+
return "upstream_request_error";
|
|
656
|
+
}
|
|
657
|
+
return "upstream_error";
|
|
658
|
+
}
|
|
659
|
+
function healthFailure(error) {
|
|
660
|
+
if (error instanceof CopilotTokenExchangeError) {
|
|
661
|
+
if (error.statusCode === 401 || error.statusCode === 403) {
|
|
662
|
+
return {
|
|
663
|
+
httpStatus: 401,
|
|
664
|
+
payload: {
|
|
665
|
+
status: "unauthenticated",
|
|
666
|
+
error: "github_auth_invalid",
|
|
667
|
+
upstream_status_code: error.statusCode
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
return {
|
|
672
|
+
httpStatus: 503,
|
|
673
|
+
payload: {
|
|
674
|
+
status: "upstream_unreachable",
|
|
675
|
+
error: "token_exchange_failed",
|
|
676
|
+
upstream_status_code: error.statusCode
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
if (error instanceof CopilotTokenManagerError) {
|
|
681
|
+
return {
|
|
682
|
+
httpStatus: 401,
|
|
683
|
+
payload: {
|
|
684
|
+
status: "unauthenticated",
|
|
685
|
+
error: "token_refresh_failed"
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
return {
|
|
690
|
+
httpStatus: 503,
|
|
691
|
+
payload: {
|
|
692
|
+
status: "upstream_unreachable",
|
|
693
|
+
error: "token_refresh_unavailable"
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
function safePathname(rawUrl) {
|
|
698
|
+
if (!rawUrl) {
|
|
699
|
+
return "/";
|
|
700
|
+
}
|
|
701
|
+
try {
|
|
702
|
+
return new URL(rawUrl, "http://127.0.0.1").pathname;
|
|
703
|
+
}
|
|
704
|
+
catch {
|
|
705
|
+
return "/";
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
async function discardUpstreamBody(response) {
|
|
709
|
+
try {
|
|
710
|
+
await response.arrayBuffer();
|
|
711
|
+
}
|
|
712
|
+
catch {
|
|
713
|
+
// ignore body drain failures
|
|
714
|
+
}
|
|
715
|
+
}
|