copilot-relay 0.0.9 → 0.1.1
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 +129 -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);
|
|
@@ -2541,6 +2547,93 @@ const eventsFromClaudeResponse = (response) => {
|
|
|
2541
2547
|
events$1.push({ type: "message_stop" });
|
|
2542
2548
|
return events$1;
|
|
2543
2549
|
};
|
|
2550
|
+
const createQueuedClaudeStreamWriter = (write) => {
|
|
2551
|
+
let pending = Promise.resolve();
|
|
2552
|
+
return (event) => {
|
|
2553
|
+
pending = pending.then(() => write(event));
|
|
2554
|
+
return pending;
|
|
2555
|
+
};
|
|
2556
|
+
};
|
|
2557
|
+
const writeClaudeStreamEvents = async (events$1, writeEvent) => {
|
|
2558
|
+
for (const event of events$1) await writeEvent(event);
|
|
2559
|
+
};
|
|
2560
|
+
const handleClaudeMessageRequest = async (config, claudePayload, requestSignal, requestId, writeEvent) => {
|
|
2561
|
+
const upstreamModel = translateModelName(claudePayload.model);
|
|
2562
|
+
const shouldLetModelDecideWebSearch = hasClaudeWebSearch(claudePayload);
|
|
2563
|
+
const decisionPayload = shouldLetModelDecideWebSearch ? prepareClaudeWebSearchDecisionPayload(claudePayload) : claudePayload;
|
|
2564
|
+
const toolNameMapper = createClaudeToolNameMapper(decisionPayload.tools, { ...getToolNameMapperOptionsForModel(upstreamModel) });
|
|
2565
|
+
const openAIPayload = translateToOpenAI(decisionPayload, void 0, toolNameMapper);
|
|
2566
|
+
if (shouldLetModelDecideWebSearch) openAIPayload.stream = false;
|
|
2567
|
+
const response = await createChatCompletions(config, openAIPayload, {
|
|
2568
|
+
client: "claude",
|
|
2569
|
+
requestedModel: claudePayload.model,
|
|
2570
|
+
requestedThinkEffort: getClaudeRequestedThinkEffort(claudePayload),
|
|
2571
|
+
requestedThinking: getClaudeRequestedThinking(claudePayload),
|
|
2572
|
+
requestId,
|
|
2573
|
+
signal: requestSignal,
|
|
2574
|
+
timeoutMs: config.upstreamTimeoutMs
|
|
2575
|
+
});
|
|
2576
|
+
if (isNonStreamingResponse(response)) {
|
|
2577
|
+
const webSearchToolCall = shouldLetModelDecideWebSearch ? getClaudeWebSearchToolCallFromChatResponse(response, toolNameMapper) : void 0;
|
|
2578
|
+
let claudeResponse;
|
|
2579
|
+
if (webSearchToolCall) {
|
|
2580
|
+
const search = await createClaudeWebSearchExecution(config, claudePayload, webSearchToolCall.query, {
|
|
2581
|
+
requestId,
|
|
2582
|
+
signal: requestSignal,
|
|
2583
|
+
timeoutMs: config.upstreamTimeoutMs
|
|
2584
|
+
});
|
|
2585
|
+
const searchResponse = createClaudeWebSearchResponse(search);
|
|
2586
|
+
if (search.results.length === 0) claudeResponse = searchResponse;
|
|
2587
|
+
else {
|
|
2588
|
+
const finalResponse = await createChatCompletions(config, createFinalWebSearchPayload(openAIPayload, search), {
|
|
2589
|
+
client: "claude",
|
|
2590
|
+
requestedModel: claudePayload.model,
|
|
2591
|
+
requestedThinkEffort: getClaudeRequestedThinkEffort(claudePayload),
|
|
2592
|
+
requestedThinking: getClaudeRequestedThinking(claudePayload),
|
|
2593
|
+
requestId,
|
|
2594
|
+
signal: requestSignal,
|
|
2595
|
+
timeoutMs: config.upstreamTimeoutMs
|
|
2596
|
+
});
|
|
2597
|
+
if (!isNonStreamingResponse(finalResponse)) throw new HTTPError("Claude web search final answer request unexpectedly streamed", new Response("Claude web search final answer request unexpectedly streamed", {
|
|
2598
|
+
status: 502,
|
|
2599
|
+
headers: { "content-type": "text/plain" }
|
|
2600
|
+
}));
|
|
2601
|
+
claudeResponse = mergeWebSearchAndFinalResponse(searchResponse, translateToClaude(finalResponse, toolNameMapper));
|
|
2602
|
+
}
|
|
2603
|
+
} else claudeResponse = translateToClaude(response, toolNameMapper);
|
|
2604
|
+
if (writeEvent) {
|
|
2605
|
+
await writeClaudeStreamEvents(eventsFromClaudeResponse(claudeResponse), writeEvent);
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
return claudeResponse;
|
|
2609
|
+
}
|
|
2610
|
+
if (shouldLetModelDecideWebSearch) throw new HTTPError("Claude web search model-decision request unexpectedly streamed", new Response("Claude web search model-decision request unexpectedly streamed", {
|
|
2611
|
+
status: 502,
|
|
2612
|
+
headers: { "content-type": "text/plain" }
|
|
2613
|
+
}));
|
|
2614
|
+
if (!writeEvent) throw new HTTPError("Claude non-streaming request unexpectedly streamed", new Response("Claude non-streaming request unexpectedly streamed", {
|
|
2615
|
+
status: 502,
|
|
2616
|
+
headers: { "content-type": "text/plain" }
|
|
2617
|
+
}));
|
|
2618
|
+
const streamState = {
|
|
2619
|
+
messageStartSent: false,
|
|
2620
|
+
contentBlockIndex: 0,
|
|
2621
|
+
contentBlockOpen: false,
|
|
2622
|
+
thinkingBlockOpen: false,
|
|
2623
|
+
toolCalls: {}
|
|
2624
|
+
};
|
|
2625
|
+
for await (const rawEvent of response) {
|
|
2626
|
+
if (rawEvent.data === "[DONE]") break;
|
|
2627
|
+
if (!rawEvent.data) continue;
|
|
2628
|
+
let chunk;
|
|
2629
|
+
try {
|
|
2630
|
+
chunk = JSON.parse(rawEvent.data);
|
|
2631
|
+
} catch {
|
|
2632
|
+
continue;
|
|
2633
|
+
}
|
|
2634
|
+
await writeClaudeStreamEvents(translateChunkToClaudeEvents(chunk, streamState, toolNameMapper), writeEvent);
|
|
2635
|
+
}
|
|
2636
|
+
};
|
|
2544
2637
|
claudeRoutes.get("/models", (c) => c.json({
|
|
2545
2638
|
object: "list",
|
|
2546
2639
|
data: getExposedModelIds().map((id) => ({
|
|
@@ -2556,94 +2649,23 @@ claudeRoutes.post("/messages", async (c) => {
|
|
|
2556
2649
|
const claudePayload = await c.req.json();
|
|
2557
2650
|
const requestSignal = createCopilotRequestSignal(c.req.raw.signal, config.upstreamTimeoutMs);
|
|
2558
2651
|
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" }
|
|
2652
|
+
if (claudePayload.stream) return streamSSE(c, async (stream) => {
|
|
2653
|
+
const streamStarted = performance.now();
|
|
2654
|
+
const writeEvent = createQueuedClaudeStreamWriter((event) => stream.writeSSE({
|
|
2655
|
+
event: event.type,
|
|
2656
|
+
data: JSON.stringify(event)
|
|
2614
2657
|
}));
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
if (!rawEvent.data) continue;
|
|
2627
|
-
let chunk;
|
|
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
|
-
});
|
|
2658
|
+
try {
|
|
2659
|
+
await handleClaudeMessageRequest(config, claudePayload, requestSignal, requestId, writeEvent);
|
|
2660
|
+
} catch (error) {
|
|
2661
|
+
log.error("Error during Claude stream request:", error);
|
|
2662
|
+
await writeEvent(translateErrorToClaudeErrorEvent());
|
|
2663
|
+
} finally {
|
|
2664
|
+
log.info(`request_id=${requestId} stream completed ${Math.round(performance.now() - streamStarted)}ms`);
|
|
2665
|
+
}
|
|
2666
|
+
});
|
|
2667
|
+
try {
|
|
2668
|
+
return c.json(await handleClaudeMessageRequest(config, claudePayload, requestSignal, requestId));
|
|
2647
2669
|
} catch (error) {
|
|
2648
2670
|
if (error instanceof ProxyNotImplementedError) {
|
|
2649
2671
|
c.set("requestErrorMessage", error.message);
|
|
@@ -2886,6 +2908,22 @@ const restart = defineCommand({
|
|
|
2886
2908
|
}
|
|
2887
2909
|
});
|
|
2888
2910
|
|
|
2911
|
+
//#endregion
|
|
2912
|
+
//#region src/stop.ts
|
|
2913
|
+
const stop = defineCommand({
|
|
2914
|
+
meta: {
|
|
2915
|
+
name: "stop",
|
|
2916
|
+
description: "Stop all detected copilot-relay server instances."
|
|
2917
|
+
},
|
|
2918
|
+
async run() {
|
|
2919
|
+
const appConfig = await readAppConfig();
|
|
2920
|
+
setLogLevel(appConfig.logLevel);
|
|
2921
|
+
await cleanupLogs(appConfig.logRetentionDays);
|
|
2922
|
+
const stopped = await stopExistingRelay(readProxyConfig(appConfig));
|
|
2923
|
+
if (stopped.length > 0) log.info(`Stopped copilot-relay pid(s): ${stopped.join(", ")}`);
|
|
2924
|
+
}
|
|
2925
|
+
});
|
|
2926
|
+
|
|
2889
2927
|
//#endregion
|
|
2890
2928
|
//#region src/main.ts
|
|
2891
2929
|
await runMain(defineCommand({
|
|
@@ -2896,7 +2934,8 @@ await runMain(defineCommand({
|
|
|
2896
2934
|
subCommands: {
|
|
2897
2935
|
auth,
|
|
2898
2936
|
restart,
|
|
2899
|
-
start
|
|
2937
|
+
start,
|
|
2938
|
+
stop
|
|
2900
2939
|
}
|
|
2901
2940
|
}));
|
|
2902
2941
|
|