copilot-relay 0.0.9 → 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 +2 -0
- package/dist/main.js +143 -90
- package/dist/main.js.map +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -45,6 +45,7 @@ Routing:
|
|
|
45
45
|
npx copilot-relay@latest auth
|
|
46
46
|
npx copilot-relay@latest start
|
|
47
47
|
npx copilot-relay@latest restart
|
|
48
|
+
npx copilot-relay@latest stop
|
|
48
49
|
```
|
|
49
50
|
|
|
50
51
|
`start` writes `ANTHROPIC_BASE_URL` and a dummy `ANTHROPIC_AUTH_TOKEN` into `~/.claude/settings.json`.
|
|
@@ -92,6 +93,7 @@ The same folder stores `copilot_token.json` for the cached Copilot bearer token,
|
|
|
92
93
|
copilot-relay auth
|
|
93
94
|
copilot-relay start
|
|
94
95
|
copilot-relay restart
|
|
96
|
+
copilot-relay stop
|
|
95
97
|
```
|
|
96
98
|
|
|
97
99
|
## Logging
|
package/dist/main.js
CHANGED
|
@@ -8,6 +8,7 @@ import path, { dirname, resolve } from "node:path";
|
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { execFile } from "node:child_process";
|
|
10
10
|
import { createHash, randomUUID } from "node:crypto";
|
|
11
|
+
import { Agent, fetch as fetch$1 } from "undici";
|
|
11
12
|
import { events } from "fetch-event-stream";
|
|
12
13
|
import { readFileSync } from "node:fs";
|
|
13
14
|
import { createAdaptorServer } from "@hono/node-server";
|
|
@@ -868,6 +869,10 @@ const userAgent = `GitHubCopilotChat/${copilotVersion}`;
|
|
|
868
869
|
const apiVersion = "2025-04-01";
|
|
869
870
|
const maxFetchAttempts = 2;
|
|
870
871
|
const copilotRequestTimeoutMs = 18e4;
|
|
872
|
+
const copilotDispatcher = new Agent({
|
|
873
|
+
allowH2: false,
|
|
874
|
+
connect: { allowH2: false }
|
|
875
|
+
});
|
|
871
876
|
const getCopilotProviderContext = (config) => ({
|
|
872
877
|
baseUrl: config.copilotBaseUrl,
|
|
873
878
|
token: config.copilotToken,
|
|
@@ -926,7 +931,7 @@ const buildHeaders = (provider, init, options, upstreamRequestId) => {
|
|
|
926
931
|
if (options.initiator) headers.set("x-initiator", options.initiator);
|
|
927
932
|
if (!headers.has("content-type") && init.body !== void 0) headers.set("content-type", "application/json");
|
|
928
933
|
if (!headers.has("accept")) headers.set("accept", "application/json");
|
|
929
|
-
return headers;
|
|
934
|
+
return Object.fromEntries(headers.entries());
|
|
930
935
|
};
|
|
931
936
|
const formatRequestId = (requestId) => requestId ? `request_id=${requestId} ` : "";
|
|
932
937
|
const logUpstreamLifecycle = (requestId, message) => {
|
|
@@ -946,9 +951,10 @@ const fetchCopilot = async (provider, path$1, init, options = {}) => {
|
|
|
946
951
|
try {
|
|
947
952
|
const started = performance.now();
|
|
948
953
|
logUpstreamLifecycle(options.requestId, `send upstream method=${init.method ?? "GET"} path=${path$1} attempt=${attempt} upstream_request_id=${upstreamRequestId}`);
|
|
949
|
-
const response = await fetch(`${provider.baseUrl}${path$1}`, {
|
|
954
|
+
const response = await fetch$1(`${provider.baseUrl}${path$1}`, {
|
|
950
955
|
...init,
|
|
951
956
|
headers: buildHeaders(provider, init, options, upstreamRequestId),
|
|
957
|
+
dispatcher: copilotDispatcher,
|
|
952
958
|
signal
|
|
953
959
|
});
|
|
954
960
|
const ms = Math.round(performance.now() - started);
|
|
@@ -2471,6 +2477,7 @@ const getTokenCount = async (payload, model) => {
|
|
|
2471
2477
|
//#endregion
|
|
2472
2478
|
//#region src/routes/claude.ts
|
|
2473
2479
|
const claudeRoutes = new Hono();
|
|
2480
|
+
const streamKeepAliveIntervalMs = 15e3;
|
|
2474
2481
|
const createTokenCountModel = (modelId) => ({
|
|
2475
2482
|
capabilities: { tokenizer: "o200k_base" },
|
|
2476
2483
|
id: modelId
|
|
@@ -2541,6 +2548,104 @@ const eventsFromClaudeResponse = (response) => {
|
|
|
2541
2548
|
events$1.push({ type: "message_stop" });
|
|
2542
2549
|
return events$1;
|
|
2543
2550
|
};
|
|
2551
|
+
const createQueuedClaudeStreamWriter = (write) => {
|
|
2552
|
+
let pending = Promise.resolve();
|
|
2553
|
+
return (event) => {
|
|
2554
|
+
pending = pending.then(() => write(event));
|
|
2555
|
+
return pending;
|
|
2556
|
+
};
|
|
2557
|
+
};
|
|
2558
|
+
const startClaudeStreamKeepAlive = (writeEvent) => {
|
|
2559
|
+
const writePing = () => {
|
|
2560
|
+
writeEvent({ type: "ping" }).catch(() => void 0);
|
|
2561
|
+
};
|
|
2562
|
+
writePing();
|
|
2563
|
+
const timer = setInterval(writePing, streamKeepAliveIntervalMs);
|
|
2564
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
2565
|
+
return () => {
|
|
2566
|
+
clearInterval(timer);
|
|
2567
|
+
};
|
|
2568
|
+
};
|
|
2569
|
+
const writeClaudeStreamEvents = async (events$1, writeEvent) => {
|
|
2570
|
+
for (const event of events$1) await writeEvent(event);
|
|
2571
|
+
};
|
|
2572
|
+
const handleClaudeMessageRequest = async (config, claudePayload, requestSignal, requestId, writeEvent) => {
|
|
2573
|
+
const upstreamModel = translateModelName(claudePayload.model);
|
|
2574
|
+
const shouldLetModelDecideWebSearch = hasClaudeWebSearch(claudePayload);
|
|
2575
|
+
const decisionPayload = shouldLetModelDecideWebSearch ? prepareClaudeWebSearchDecisionPayload(claudePayload) : claudePayload;
|
|
2576
|
+
const toolNameMapper = createClaudeToolNameMapper(decisionPayload.tools, { ...getToolNameMapperOptionsForModel(upstreamModel) });
|
|
2577
|
+
const openAIPayload = translateToOpenAI(decisionPayload, void 0, toolNameMapper);
|
|
2578
|
+
if (shouldLetModelDecideWebSearch) openAIPayload.stream = false;
|
|
2579
|
+
const response = await createChatCompletions(config, openAIPayload, {
|
|
2580
|
+
client: "claude",
|
|
2581
|
+
requestedModel: claudePayload.model,
|
|
2582
|
+
requestedThinkEffort: getClaudeRequestedThinkEffort(claudePayload),
|
|
2583
|
+
requestedThinking: getClaudeRequestedThinking(claudePayload),
|
|
2584
|
+
requestId,
|
|
2585
|
+
signal: requestSignal,
|
|
2586
|
+
timeoutMs: config.upstreamTimeoutMs
|
|
2587
|
+
});
|
|
2588
|
+
if (isNonStreamingResponse(response)) {
|
|
2589
|
+
const webSearchToolCall = shouldLetModelDecideWebSearch ? getClaudeWebSearchToolCallFromChatResponse(response, toolNameMapper) : void 0;
|
|
2590
|
+
let claudeResponse;
|
|
2591
|
+
if (webSearchToolCall) {
|
|
2592
|
+
const search = await createClaudeWebSearchExecution(config, claudePayload, webSearchToolCall.query, {
|
|
2593
|
+
requestId,
|
|
2594
|
+
signal: requestSignal,
|
|
2595
|
+
timeoutMs: config.upstreamTimeoutMs
|
|
2596
|
+
});
|
|
2597
|
+
const searchResponse = createClaudeWebSearchResponse(search);
|
|
2598
|
+
if (search.results.length === 0) claudeResponse = searchResponse;
|
|
2599
|
+
else {
|
|
2600
|
+
const finalResponse = await createChatCompletions(config, createFinalWebSearchPayload(openAIPayload, search), {
|
|
2601
|
+
client: "claude",
|
|
2602
|
+
requestedModel: claudePayload.model,
|
|
2603
|
+
requestedThinkEffort: getClaudeRequestedThinkEffort(claudePayload),
|
|
2604
|
+
requestedThinking: getClaudeRequestedThinking(claudePayload),
|
|
2605
|
+
requestId,
|
|
2606
|
+
signal: requestSignal,
|
|
2607
|
+
timeoutMs: config.upstreamTimeoutMs
|
|
2608
|
+
});
|
|
2609
|
+
if (!isNonStreamingResponse(finalResponse)) throw new HTTPError("Claude web search final answer request unexpectedly streamed", new Response("Claude web search final answer request unexpectedly streamed", {
|
|
2610
|
+
status: 502,
|
|
2611
|
+
headers: { "content-type": "text/plain" }
|
|
2612
|
+
}));
|
|
2613
|
+
claudeResponse = mergeWebSearchAndFinalResponse(searchResponse, translateToClaude(finalResponse, toolNameMapper));
|
|
2614
|
+
}
|
|
2615
|
+
} else claudeResponse = translateToClaude(response, toolNameMapper);
|
|
2616
|
+
if (writeEvent) {
|
|
2617
|
+
await writeClaudeStreamEvents(eventsFromClaudeResponse(claudeResponse), writeEvent);
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
return claudeResponse;
|
|
2621
|
+
}
|
|
2622
|
+
if (shouldLetModelDecideWebSearch) throw new HTTPError("Claude web search model-decision request unexpectedly streamed", new Response("Claude web search model-decision request unexpectedly streamed", {
|
|
2623
|
+
status: 502,
|
|
2624
|
+
headers: { "content-type": "text/plain" }
|
|
2625
|
+
}));
|
|
2626
|
+
if (!writeEvent) throw new HTTPError("Claude non-streaming request unexpectedly streamed", new Response("Claude non-streaming request unexpectedly streamed", {
|
|
2627
|
+
status: 502,
|
|
2628
|
+
headers: { "content-type": "text/plain" }
|
|
2629
|
+
}));
|
|
2630
|
+
const streamState = {
|
|
2631
|
+
messageStartSent: false,
|
|
2632
|
+
contentBlockIndex: 0,
|
|
2633
|
+
contentBlockOpen: false,
|
|
2634
|
+
thinkingBlockOpen: false,
|
|
2635
|
+
toolCalls: {}
|
|
2636
|
+
};
|
|
2637
|
+
for await (const rawEvent of response) {
|
|
2638
|
+
if (rawEvent.data === "[DONE]") break;
|
|
2639
|
+
if (!rawEvent.data) continue;
|
|
2640
|
+
let chunk;
|
|
2641
|
+
try {
|
|
2642
|
+
chunk = JSON.parse(rawEvent.data);
|
|
2643
|
+
} catch {
|
|
2644
|
+
continue;
|
|
2645
|
+
}
|
|
2646
|
+
await writeClaudeStreamEvents(translateChunkToClaudeEvents(chunk, streamState, toolNameMapper), writeEvent);
|
|
2647
|
+
}
|
|
2648
|
+
};
|
|
2544
2649
|
claudeRoutes.get("/models", (c) => c.json({
|
|
2545
2650
|
object: "list",
|
|
2546
2651
|
data: getExposedModelIds().map((id) => ({
|
|
@@ -2556,94 +2661,25 @@ claudeRoutes.post("/messages", async (c) => {
|
|
|
2556
2661
|
const claudePayload = await c.req.json();
|
|
2557
2662
|
const requestSignal = createCopilotRequestSignal(c.req.raw.signal, config.upstreamTimeoutMs);
|
|
2558
2663
|
log.debug("Full Claude request payload", { payload: claudePayload });
|
|
2559
|
-
|
|
2560
|
-
const
|
|
2561
|
-
const
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
const openAIPayload = translateToOpenAI(decisionPayload, void 0, toolNameMapper);
|
|
2565
|
-
if (shouldLetModelDecideWebSearch) openAIPayload.stream = false;
|
|
2566
|
-
const response = await createChatCompletions(config, openAIPayload, {
|
|
2567
|
-
client: "claude",
|
|
2568
|
-
requestedModel: claudePayload.model,
|
|
2569
|
-
requestedThinkEffort: getClaudeRequestedThinkEffort(claudePayload),
|
|
2570
|
-
requestedThinking: getClaudeRequestedThinking(claudePayload),
|
|
2571
|
-
requestId,
|
|
2572
|
-
signal: requestSignal,
|
|
2573
|
-
timeoutMs: config.upstreamTimeoutMs
|
|
2574
|
-
});
|
|
2575
|
-
if (isNonStreamingResponse(response)) {
|
|
2576
|
-
const webSearchToolCall = shouldLetModelDecideWebSearch ? getClaudeWebSearchToolCallFromChatResponse(response, toolNameMapper) : void 0;
|
|
2577
|
-
let claudeResponse;
|
|
2578
|
-
if (webSearchToolCall) {
|
|
2579
|
-
const search = await createClaudeWebSearchExecution(config, claudePayload, webSearchToolCall.query, {
|
|
2580
|
-
requestId,
|
|
2581
|
-
signal: requestSignal,
|
|
2582
|
-
timeoutMs: config.upstreamTimeoutMs
|
|
2583
|
-
});
|
|
2584
|
-
const searchResponse = createClaudeWebSearchResponse(search);
|
|
2585
|
-
if (search.results.length === 0) claudeResponse = searchResponse;
|
|
2586
|
-
else {
|
|
2587
|
-
const finalResponse = await createChatCompletions(config, createFinalWebSearchPayload(openAIPayload, search), {
|
|
2588
|
-
client: "claude",
|
|
2589
|
-
requestedModel: claudePayload.model,
|
|
2590
|
-
requestedThinkEffort: getClaudeRequestedThinkEffort(claudePayload),
|
|
2591
|
-
requestedThinking: getClaudeRequestedThinking(claudePayload),
|
|
2592
|
-
requestId,
|
|
2593
|
-
signal: requestSignal,
|
|
2594
|
-
timeoutMs: config.upstreamTimeoutMs
|
|
2595
|
-
});
|
|
2596
|
-
if (!isNonStreamingResponse(finalResponse)) throw new HTTPError("Claude web search final answer request unexpectedly streamed", new Response("Claude web search final answer request unexpectedly streamed", {
|
|
2597
|
-
status: 502,
|
|
2598
|
-
headers: { "content-type": "text/plain" }
|
|
2599
|
-
}));
|
|
2600
|
-
claudeResponse = mergeWebSearchAndFinalResponse(searchResponse, translateToClaude(finalResponse, toolNameMapper));
|
|
2601
|
-
}
|
|
2602
|
-
} else claudeResponse = translateToClaude(response, toolNameMapper);
|
|
2603
|
-
if (!claudePayload.stream) return c.json(claudeResponse);
|
|
2604
|
-
return streamSSE(c, async (stream) => {
|
|
2605
|
-
for (const event of eventsFromClaudeResponse(claudeResponse)) await stream.writeSSE({
|
|
2606
|
-
event: event.type,
|
|
2607
|
-
data: JSON.stringify(event)
|
|
2608
|
-
});
|
|
2609
|
-
});
|
|
2610
|
-
}
|
|
2611
|
-
if (shouldLetModelDecideWebSearch) throw new HTTPError("Claude web search model-decision request unexpectedly streamed", new Response("Claude web search model-decision request unexpectedly streamed", {
|
|
2612
|
-
status: 502,
|
|
2613
|
-
headers: { "content-type": "text/plain" }
|
|
2664
|
+
if (claudePayload.stream) return streamSSE(c, async (stream) => {
|
|
2665
|
+
const streamStarted = performance.now();
|
|
2666
|
+
const writeEvent = createQueuedClaudeStreamWriter((event) => stream.writeSSE({
|
|
2667
|
+
event: event.type,
|
|
2668
|
+
data: JSON.stringify(event)
|
|
2614
2669
|
}));
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
try {
|
|
2629
|
-
chunk = JSON.parse(rawEvent.data);
|
|
2630
|
-
} catch {
|
|
2631
|
-
continue;
|
|
2632
|
-
}
|
|
2633
|
-
for (const event of translateChunkToClaudeEvents(chunk, streamState, toolNameMapper)) await stream.writeSSE({
|
|
2634
|
-
event: event.type,
|
|
2635
|
-
data: JSON.stringify(event)
|
|
2636
|
-
});
|
|
2637
|
-
}
|
|
2638
|
-
} catch (error) {
|
|
2639
|
-
log.error("Error during Claude stream translation:", error);
|
|
2640
|
-
const errorEvent = translateErrorToClaudeErrorEvent();
|
|
2641
|
-
await stream.writeSSE({
|
|
2642
|
-
event: errorEvent.type,
|
|
2643
|
-
data: JSON.stringify(errorEvent)
|
|
2644
|
-
});
|
|
2645
|
-
}
|
|
2646
|
-
});
|
|
2670
|
+
const stopKeepAlive = startClaudeStreamKeepAlive(writeEvent);
|
|
2671
|
+
try {
|
|
2672
|
+
await handleClaudeMessageRequest(config, claudePayload, requestSignal, requestId, writeEvent);
|
|
2673
|
+
} catch (error) {
|
|
2674
|
+
log.error("Error during Claude stream request:", error);
|
|
2675
|
+
await writeEvent(translateErrorToClaudeErrorEvent());
|
|
2676
|
+
} finally {
|
|
2677
|
+
stopKeepAlive();
|
|
2678
|
+
log.info(`request_id=${requestId} stream completed ${Math.round(performance.now() - streamStarted)}ms`);
|
|
2679
|
+
}
|
|
2680
|
+
});
|
|
2681
|
+
try {
|
|
2682
|
+
return c.json(await handleClaudeMessageRequest(config, claudePayload, requestSignal, requestId));
|
|
2647
2683
|
} catch (error) {
|
|
2648
2684
|
if (error instanceof ProxyNotImplementedError) {
|
|
2649
2685
|
c.set("requestErrorMessage", error.message);
|
|
@@ -2886,6 +2922,22 @@ const restart = defineCommand({
|
|
|
2886
2922
|
}
|
|
2887
2923
|
});
|
|
2888
2924
|
|
|
2925
|
+
//#endregion
|
|
2926
|
+
//#region src/stop.ts
|
|
2927
|
+
const stop = defineCommand({
|
|
2928
|
+
meta: {
|
|
2929
|
+
name: "stop",
|
|
2930
|
+
description: "Stop all detected copilot-relay server instances."
|
|
2931
|
+
},
|
|
2932
|
+
async run() {
|
|
2933
|
+
const appConfig = await readAppConfig();
|
|
2934
|
+
setLogLevel(appConfig.logLevel);
|
|
2935
|
+
await cleanupLogs(appConfig.logRetentionDays);
|
|
2936
|
+
const stopped = await stopExistingRelay(readProxyConfig(appConfig));
|
|
2937
|
+
if (stopped.length > 0) log.info(`Stopped copilot-relay pid(s): ${stopped.join(", ")}`);
|
|
2938
|
+
}
|
|
2939
|
+
});
|
|
2940
|
+
|
|
2889
2941
|
//#endregion
|
|
2890
2942
|
//#region src/main.ts
|
|
2891
2943
|
await runMain(defineCommand({
|
|
@@ -2896,7 +2948,8 @@ await runMain(defineCommand({
|
|
|
2896
2948
|
subCommands: {
|
|
2897
2949
|
auth,
|
|
2898
2950
|
restart,
|
|
2899
|
-
start
|
|
2951
|
+
start,
|
|
2952
|
+
stop
|
|
2900
2953
|
}
|
|
2901
2954
|
}));
|
|
2902
2955
|
|