copilot-relay 0.2.6 → 0.3.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 +15 -3
- package/dist/main.js +253 -33
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,13 +24,25 @@ Public API:
|
|
|
24
24
|
- `POST /v1/messages/count_tokens`
|
|
25
25
|
- `GET /v1/models`
|
|
26
26
|
- `GET /healthz`
|
|
27
|
+
- `GET|HEAD /api/hello`
|
|
28
|
+
|
|
29
|
+
`/api/hello` is a static reachability probe Claude Code sends on startup and
|
|
30
|
+
around real traffic. Like `/healthz` it never contacts Copilot, so it proves the
|
|
31
|
+
relay is listening and nothing more.
|
|
27
32
|
|
|
28
33
|
Claude WebSearch is bridge-managed: when the model selects the WebSearch tool,
|
|
29
34
|
the relay executes Copilot `/responses` with `web_search_preview`, then sends the
|
|
30
35
|
retrieved context through a final model pass and returns Claude
|
|
31
|
-
`server_tool_use` / `web_search_tool_result` blocks.
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
`server_tool_use` / `web_search_tool_result` blocks. The final pass keeps your
|
|
37
|
+
other tools available, so the model can act on what it found in the same turn.
|
|
38
|
+
|
|
39
|
+
Advertising WebSearch no longer costs streaming. The relay reads the model's
|
|
40
|
+
response only as far as it takes to tell whether a search is coming, so a turn
|
|
41
|
+
that never searches streams normally — which is most of them, since Claude Code
|
|
42
|
+
offers the tool on every request.
|
|
43
|
+
|
|
44
|
+
Unknown API routes return `500` and log method, path, selected headers, and
|
|
45
|
+
request payload to help implement compatible endpoints later.
|
|
34
46
|
|
|
35
47
|
Routing:
|
|
36
48
|
|
package/dist/main.js
CHANGED
|
@@ -1508,6 +1508,7 @@ function getFinishReason(response, hasFunctionCalls) {
|
|
|
1508
1508
|
//#region src/copilot/chat.ts
|
|
1509
1509
|
const usesMaxCompletionTokens = (modelId) => modelId.startsWith("gpt-5");
|
|
1510
1510
|
const continuePrefillPrompt$1 = "Continue the assistant response from the previous assistant message.";
|
|
1511
|
+
const continueAfterNonUserPrompt = "Continue based on the context above.";
|
|
1511
1512
|
const maxUserLength = 64;
|
|
1512
1513
|
const sanitizeReasoningEffortForModel = (_modelId, reasoningEffort) => {
|
|
1513
1514
|
return reasoningEffort ?? void 0;
|
|
@@ -1536,7 +1537,14 @@ const buildRequestPayload = (payload) => {
|
|
|
1536
1537
|
};
|
|
1537
1538
|
const normalizeFinalAssistantPrefill$1 = (payload) => {
|
|
1538
1539
|
const lastMessage = payload.messages.at(-1);
|
|
1539
|
-
if (lastMessage
|
|
1540
|
+
if (!lastMessage || lastMessage.role === "user") return payload;
|
|
1541
|
+
if (lastMessage.role !== "assistant") return {
|
|
1542
|
+
...payload,
|
|
1543
|
+
messages: [...payload.messages, {
|
|
1544
|
+
role: "user",
|
|
1545
|
+
content: continueAfterNonUserPrompt
|
|
1546
|
+
}]
|
|
1547
|
+
};
|
|
1540
1548
|
const messages = [...payload.messages];
|
|
1541
1549
|
if (typeof lastMessage.content !== "string") {
|
|
1542
1550
|
messages.pop();
|
|
@@ -2435,10 +2443,10 @@ const getWebSearchResultText = (search) => {
|
|
|
2435
2443
|
].join("\n");
|
|
2436
2444
|
};
|
|
2437
2445
|
const createWebSearchResultContextMessage = (search) => ({
|
|
2438
|
-
role: "
|
|
2446
|
+
role: "user",
|
|
2439
2447
|
content: [
|
|
2440
2448
|
"Trusted bridge retrieval context: the assistant selected web_search, and copilot-relay executed it.",
|
|
2441
|
-
"Use this context
|
|
2449
|
+
"Use this context to complete the request. Do not describe it as user-provided or injected.",
|
|
2442
2450
|
"If the user requested a specific output format, answer using only matching information from this context.",
|
|
2443
2451
|
"If the user asked for a URL only, output only that URL with no surrounding text.",
|
|
2444
2452
|
"",
|
|
@@ -2447,24 +2455,26 @@ const createWebSearchResultContextMessage = (search) => ({
|
|
|
2447
2455
|
getWebSearchResultText(search)
|
|
2448
2456
|
].join("\n")
|
|
2449
2457
|
});
|
|
2450
|
-
const
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2458
|
+
const isWebSearchTool = (tool, toolNameMapper) => isClaudeWebSearchToolName(toolNameMapper.toClaude(tool.function.name));
|
|
2459
|
+
const removeWebSearchTool = (tools, toolNameMapper) => {
|
|
2460
|
+
if (!tools || tools.length === 0) return;
|
|
2461
|
+
const remaining = tools.filter((tool) => !isWebSearchTool(tool, toolNameMapper));
|
|
2462
|
+
return remaining.length > 0 ? remaining : void 0;
|
|
2463
|
+
};
|
|
2464
|
+
const normalizeFinalToolChoice = (toolChoice, tools, toolNameMapper) => {
|
|
2465
|
+
if (!tools || tools.length === 0) return;
|
|
2466
|
+
if (toolChoice === "required") return "auto";
|
|
2467
|
+
if (toolChoice && typeof toolChoice === "object" && isClaudeWebSearchToolName(toolNameMapper.toClaude(toolChoice.function.name))) return "auto";
|
|
2468
|
+
return toolChoice;
|
|
2469
|
+
};
|
|
2470
|
+
const createFinalWebSearchPayload = (payload, search, toolNameMapper) => {
|
|
2471
|
+
const tools = removeWebSearchTool(payload.tools, toolNameMapper);
|
|
2462
2472
|
return {
|
|
2463
2473
|
...payload,
|
|
2464
2474
|
stream: false,
|
|
2465
|
-
tools
|
|
2466
|
-
tool_choice:
|
|
2467
|
-
messages: [...messages, createWebSearchResultContextMessage(search)]
|
|
2475
|
+
tools,
|
|
2476
|
+
tool_choice: normalizeFinalToolChoice(payload.tool_choice, tools, toolNameMapper),
|
|
2477
|
+
messages: [...payload.messages, createWebSearchResultContextMessage(search)]
|
|
2468
2478
|
};
|
|
2469
2479
|
};
|
|
2470
2480
|
const mergeWebSearchAndFinalResponse = (searchResponse, finalResponse) => ({
|
|
@@ -2478,6 +2488,137 @@ const mergeWebSearchAndFinalResponse = (searchResponse, finalResponse) => ({
|
|
|
2478
2488
|
}
|
|
2479
2489
|
});
|
|
2480
2490
|
|
|
2491
|
+
//#endregion
|
|
2492
|
+
//#region src/claude/web-search-stream.ts
|
|
2493
|
+
const isWebSearchName = (name, toolNameMapper, isWebSearchToolName) => !!name && isWebSearchToolName(toolNameMapper.toClaude(name));
|
|
2494
|
+
const chunkSettlesDecision = (chunk) => {
|
|
2495
|
+
const choice = chunk.choices[0];
|
|
2496
|
+
if (!choice) return false;
|
|
2497
|
+
return choice.delta?.tool_calls?.some((call) => call.function?.name) === true || !!choice.finish_reason;
|
|
2498
|
+
};
|
|
2499
|
+
const chunkHasWebSearchCall = (chunk, toolNameMapper, isWebSearchToolName) => chunk.choices[0]?.delta?.tool_calls?.some((call) => isWebSearchName(call.function?.name, toolNameMapper, isWebSearchToolName)) ?? false;
|
|
2500
|
+
const accumulateChunks = (chunks) => {
|
|
2501
|
+
const first = chunks[0];
|
|
2502
|
+
const toolCalls = [];
|
|
2503
|
+
let content = "";
|
|
2504
|
+
let reasoning = "";
|
|
2505
|
+
let finishReason = null;
|
|
2506
|
+
let usage;
|
|
2507
|
+
for (const chunk of chunks) {
|
|
2508
|
+
if (chunk.usage) usage = chunk.usage;
|
|
2509
|
+
const choice = chunk.choices[0];
|
|
2510
|
+
if (!choice) continue;
|
|
2511
|
+
if (typeof choice.delta?.content === "string") content += choice.delta.content;
|
|
2512
|
+
const chunkReasoning = choice.delta?.reasoning_text ?? choice.delta?.reasoning_content;
|
|
2513
|
+
if (typeof chunkReasoning === "string") reasoning += chunkReasoning;
|
|
2514
|
+
if (choice.finish_reason) finishReason = choice.finish_reason;
|
|
2515
|
+
for (const call of choice.delta?.tool_calls ?? []) {
|
|
2516
|
+
const existing = toolCalls[call.index];
|
|
2517
|
+
if (existing) {
|
|
2518
|
+
existing.function.arguments += call.function?.arguments ?? "";
|
|
2519
|
+
continue;
|
|
2520
|
+
}
|
|
2521
|
+
toolCalls[call.index] = {
|
|
2522
|
+
id: call.id ?? `call_${call.index}`,
|
|
2523
|
+
type: "function",
|
|
2524
|
+
function: {
|
|
2525
|
+
name: call.function?.name ?? "",
|
|
2526
|
+
arguments: call.function?.arguments ?? ""
|
|
2527
|
+
}
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
const collectedToolCalls = toolCalls.filter(Boolean);
|
|
2532
|
+
return {
|
|
2533
|
+
id: first?.id ?? "chat_stream_accumulated",
|
|
2534
|
+
object: "chat.completion",
|
|
2535
|
+
created: first?.created ?? 0,
|
|
2536
|
+
model: first?.model ?? "",
|
|
2537
|
+
choices: [{
|
|
2538
|
+
index: 0,
|
|
2539
|
+
message: {
|
|
2540
|
+
role: "assistant",
|
|
2541
|
+
content: content || null,
|
|
2542
|
+
...reasoning ? { reasoning_text: reasoning } : {},
|
|
2543
|
+
...collectedToolCalls.length > 0 ? { tool_calls: collectedToolCalls } : {}
|
|
2544
|
+
},
|
|
2545
|
+
finish_reason: finishReason ?? "stop"
|
|
2546
|
+
}],
|
|
2547
|
+
...usage ? { usage } : {}
|
|
2548
|
+
};
|
|
2549
|
+
};
|
|
2550
|
+
const resolveWebSearchStreamDecision = async (stream, toolNameMapper, isWebSearchToolName, onChunk) => {
|
|
2551
|
+
const buffered = [];
|
|
2552
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
2553
|
+
let sawWebSearch = false;
|
|
2554
|
+
let settled = false;
|
|
2555
|
+
let done = false;
|
|
2556
|
+
let alreadyStreamed = false;
|
|
2557
|
+
while (!settled) {
|
|
2558
|
+
const next = await iterator.next();
|
|
2559
|
+
if (next.done) {
|
|
2560
|
+
done = true;
|
|
2561
|
+
break;
|
|
2562
|
+
}
|
|
2563
|
+
const raw = next.value;
|
|
2564
|
+
if (raw.data === "[DONE]") {
|
|
2565
|
+
done = true;
|
|
2566
|
+
break;
|
|
2567
|
+
}
|
|
2568
|
+
if (!raw.data) continue;
|
|
2569
|
+
let chunk;
|
|
2570
|
+
try {
|
|
2571
|
+
chunk = JSON.parse(raw.data);
|
|
2572
|
+
} catch {
|
|
2573
|
+
continue;
|
|
2574
|
+
}
|
|
2575
|
+
buffered.push(chunk);
|
|
2576
|
+
if (chunkHasWebSearchCall(chunk, toolNameMapper, isWebSearchToolName)) {
|
|
2577
|
+
sawWebSearch = true;
|
|
2578
|
+
settled = true;
|
|
2579
|
+
break;
|
|
2580
|
+
}
|
|
2581
|
+
if (onChunk) {
|
|
2582
|
+
await onChunk(chunk);
|
|
2583
|
+
alreadyStreamed = true;
|
|
2584
|
+
}
|
|
2585
|
+
if (chunkSettlesDecision(chunk)) settled = true;
|
|
2586
|
+
}
|
|
2587
|
+
const rest = { async *[Symbol.asyncIterator]() {
|
|
2588
|
+
if (done) return;
|
|
2589
|
+
while (true) {
|
|
2590
|
+
const next = await iterator.next();
|
|
2591
|
+
if (next.done) return;
|
|
2592
|
+
yield next.value;
|
|
2593
|
+
}
|
|
2594
|
+
} };
|
|
2595
|
+
if (!sawWebSearch) return {
|
|
2596
|
+
decision: {
|
|
2597
|
+
kind: "streamed",
|
|
2598
|
+
buffered: onChunk ? [] : buffered
|
|
2599
|
+
},
|
|
2600
|
+
rest,
|
|
2601
|
+
alreadyStreamed
|
|
2602
|
+
};
|
|
2603
|
+
for await (const raw of rest) {
|
|
2604
|
+
if (raw.data === "[DONE]") break;
|
|
2605
|
+
if (!raw.data) continue;
|
|
2606
|
+
try {
|
|
2607
|
+
buffered.push(JSON.parse(raw.data));
|
|
2608
|
+
} catch {
|
|
2609
|
+
continue;
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
return {
|
|
2613
|
+
decision: {
|
|
2614
|
+
kind: "webSearch",
|
|
2615
|
+
response: accumulateChunks(buffered)
|
|
2616
|
+
},
|
|
2617
|
+
rest: { async *[Symbol.asyncIterator]() {} },
|
|
2618
|
+
alreadyStreamed
|
|
2619
|
+
};
|
|
2620
|
+
};
|
|
2621
|
+
|
|
2481
2622
|
//#endregion
|
|
2482
2623
|
//#region src/lib/tokenizer.ts
|
|
2483
2624
|
const encodingMap = {
|
|
@@ -2706,6 +2847,65 @@ const eventsFromClaudeResponse = (response) => {
|
|
|
2706
2847
|
events$1.push({ type: "message_stop" });
|
|
2707
2848
|
return events$1;
|
|
2708
2849
|
};
|
|
2850
|
+
const continuationEventsFromClaudeResponse = (response, state) => {
|
|
2851
|
+
const events$1 = [];
|
|
2852
|
+
if (state.contentBlockOpen) {
|
|
2853
|
+
events$1.push({
|
|
2854
|
+
type: "content_block_stop",
|
|
2855
|
+
index: state.contentBlockIndex
|
|
2856
|
+
});
|
|
2857
|
+
state.contentBlockIndex += 1;
|
|
2858
|
+
state.contentBlockOpen = false;
|
|
2859
|
+
state.thinkingBlockOpen = false;
|
|
2860
|
+
}
|
|
2861
|
+
for (const block of response.content) {
|
|
2862
|
+
const index = state.contentBlockIndex;
|
|
2863
|
+
events$1.push({
|
|
2864
|
+
type: "content_block_start",
|
|
2865
|
+
index,
|
|
2866
|
+
content_block: block
|
|
2867
|
+
});
|
|
2868
|
+
if (block.type === "text") events$1.push({
|
|
2869
|
+
type: "content_block_delta",
|
|
2870
|
+
index,
|
|
2871
|
+
delta: {
|
|
2872
|
+
type: "text_delta",
|
|
2873
|
+
text: block.text
|
|
2874
|
+
}
|
|
2875
|
+
});
|
|
2876
|
+
else if (block.type === "thinking") events$1.push({
|
|
2877
|
+
type: "content_block_delta",
|
|
2878
|
+
index,
|
|
2879
|
+
delta: {
|
|
2880
|
+
type: "thinking_delta",
|
|
2881
|
+
thinking: block.thinking
|
|
2882
|
+
}
|
|
2883
|
+
});
|
|
2884
|
+
else if (block.type === "tool_use" || block.type === "server_tool_use") events$1.push({
|
|
2885
|
+
type: "content_block_delta",
|
|
2886
|
+
index,
|
|
2887
|
+
delta: {
|
|
2888
|
+
type: "input_json_delta",
|
|
2889
|
+
partial_json: JSON.stringify(block.input)
|
|
2890
|
+
}
|
|
2891
|
+
});
|
|
2892
|
+
events$1.push({
|
|
2893
|
+
type: "content_block_stop",
|
|
2894
|
+
index
|
|
2895
|
+
});
|
|
2896
|
+
state.contentBlockIndex += 1;
|
|
2897
|
+
}
|
|
2898
|
+
events$1.push({
|
|
2899
|
+
type: "message_delta",
|
|
2900
|
+
delta: {
|
|
2901
|
+
stop_reason: response.stop_reason,
|
|
2902
|
+
stop_sequence: response.stop_sequence
|
|
2903
|
+
},
|
|
2904
|
+
usage: { output_tokens: response.usage.output_tokens }
|
|
2905
|
+
});
|
|
2906
|
+
events$1.push({ type: "message_stop" });
|
|
2907
|
+
return events$1;
|
|
2908
|
+
};
|
|
2709
2909
|
const createQueuedClaudeStreamWriter = (write) => {
|
|
2710
2910
|
let pending = Promise.resolve();
|
|
2711
2911
|
return (event) => {
|
|
@@ -2722,7 +2922,8 @@ const handleClaudeMessageRequest = async (config, claudePayload, requestSignal,
|
|
|
2722
2922
|
const decisionPayload = shouldLetModelDecideWebSearch ? prepareClaudeWebSearchDecisionPayload(claudePayload) : claudePayload;
|
|
2723
2923
|
const toolNameMapper = createClaudeToolNameMapper(decisionPayload.tools, { ...getToolNameMapperOptionsForModel(upstreamModel) });
|
|
2724
2924
|
const openAIPayload = translateToOpenAI(decisionPayload, void 0, toolNameMapper);
|
|
2725
|
-
|
|
2925
|
+
const canStreamWebSearchDecision = shouldLetModelDecideWebSearch && !!writeEvent;
|
|
2926
|
+
if (shouldLetModelDecideWebSearch && !canStreamWebSearchDecision) openAIPayload.stream = false;
|
|
2726
2927
|
const response = await createChatCompletions(config, openAIPayload, {
|
|
2727
2928
|
client: "claude",
|
|
2728
2929
|
requestedModel: claudePayload.model,
|
|
@@ -2732,8 +2933,31 @@ const handleClaudeMessageRequest = async (config, claudePayload, requestSignal,
|
|
|
2732
2933
|
signal: requestSignal,
|
|
2733
2934
|
timeoutMs: config.upstreamTimeoutMs
|
|
2734
2935
|
});
|
|
2735
|
-
|
|
2736
|
-
|
|
2936
|
+
let decidedResponse;
|
|
2937
|
+
let bufferedChunks = [];
|
|
2938
|
+
let remainingStream;
|
|
2939
|
+
const streamState = {
|
|
2940
|
+
messageStartSent: false,
|
|
2941
|
+
contentBlockIndex: 0,
|
|
2942
|
+
contentBlockOpen: false,
|
|
2943
|
+
thinkingBlockOpen: false,
|
|
2944
|
+
toolCalls: {}
|
|
2945
|
+
};
|
|
2946
|
+
let streamedBeforeDecision = false;
|
|
2947
|
+
if (canStreamWebSearchDecision && !isNonStreamingResponse(response)) {
|
|
2948
|
+
const { decision, rest, alreadyStreamed } = await resolveWebSearchStreamDecision(response, toolNameMapper, isClaudeWebSearchToolName, async (chunk) => {
|
|
2949
|
+
await writeClaudeStreamEvents(translateChunkToClaudeEvents(chunk, streamState, toolNameMapper), writeEvent);
|
|
2950
|
+
});
|
|
2951
|
+
streamedBeforeDecision = alreadyStreamed;
|
|
2952
|
+
if (decision.kind === "webSearch") decidedResponse = decision.response;
|
|
2953
|
+
else {
|
|
2954
|
+
bufferedChunks = decision.buffered;
|
|
2955
|
+
remainingStream = rest;
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
const effectiveResponse = decidedResponse ?? response;
|
|
2959
|
+
if (isNonStreamingResponse(effectiveResponse)) {
|
|
2960
|
+
const webSearchToolCall = shouldLetModelDecideWebSearch ? getClaudeWebSearchToolCallFromChatResponse(effectiveResponse, toolNameMapper) : void 0;
|
|
2737
2961
|
let claudeResponse;
|
|
2738
2962
|
if (webSearchToolCall) {
|
|
2739
2963
|
const search = await createClaudeWebSearchExecution(config, claudePayload, webSearchToolCall.query, {
|
|
@@ -2744,7 +2968,7 @@ const handleClaudeMessageRequest = async (config, claudePayload, requestSignal,
|
|
|
2744
2968
|
const searchResponse = createClaudeWebSearchResponse(search);
|
|
2745
2969
|
if (search.results.length === 0) claudeResponse = searchResponse;
|
|
2746
2970
|
else {
|
|
2747
|
-
const finalResponse = await createChatCompletions(config, createFinalWebSearchPayload(openAIPayload, search), {
|
|
2971
|
+
const finalResponse = await createChatCompletions(config, createFinalWebSearchPayload(openAIPayload, search, toolNameMapper), {
|
|
2748
2972
|
client: "claude",
|
|
2749
2973
|
requestedModel: claudePayload.model,
|
|
2750
2974
|
requestedThinkEffort: getClaudeRequestedThinkEffort(claudePayload),
|
|
@@ -2759,14 +2983,14 @@ const handleClaudeMessageRequest = async (config, claudePayload, requestSignal,
|
|
|
2759
2983
|
}));
|
|
2760
2984
|
claudeResponse = mergeWebSearchAndFinalResponse(searchResponse, translateToClaude(finalResponse, toolNameMapper));
|
|
2761
2985
|
}
|
|
2762
|
-
} else claudeResponse = translateToClaude(
|
|
2986
|
+
} else claudeResponse = translateToClaude(effectiveResponse, toolNameMapper);
|
|
2763
2987
|
if (writeEvent) {
|
|
2764
|
-
await writeClaudeStreamEvents(eventsFromClaudeResponse(claudeResponse), writeEvent);
|
|
2988
|
+
await writeClaudeStreamEvents(streamedBeforeDecision ? continuationEventsFromClaudeResponse(claudeResponse, streamState) : eventsFromClaudeResponse(claudeResponse), writeEvent);
|
|
2765
2989
|
return;
|
|
2766
2990
|
}
|
|
2767
2991
|
return claudeResponse;
|
|
2768
2992
|
}
|
|
2769
|
-
if (shouldLetModelDecideWebSearch) throw new HTTPError("Claude web search model-decision request unexpectedly streamed", new Response("Claude web search model-decision request unexpectedly streamed", {
|
|
2993
|
+
if (shouldLetModelDecideWebSearch && !canStreamWebSearchDecision) throw new HTTPError("Claude web search model-decision request unexpectedly streamed", new Response("Claude web search model-decision request unexpectedly streamed", {
|
|
2770
2994
|
status: 502,
|
|
2771
2995
|
headers: { "content-type": "text/plain" }
|
|
2772
2996
|
}));
|
|
@@ -2774,14 +2998,9 @@ const handleClaudeMessageRequest = async (config, claudePayload, requestSignal,
|
|
|
2774
2998
|
status: 502,
|
|
2775
2999
|
headers: { "content-type": "text/plain" }
|
|
2776
3000
|
}));
|
|
2777
|
-
const streamState
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
contentBlockOpen: false,
|
|
2781
|
-
thinkingBlockOpen: false,
|
|
2782
|
-
toolCalls: {}
|
|
2783
|
-
};
|
|
2784
|
-
for await (const rawEvent of response) {
|
|
3001
|
+
for (const chunk of bufferedChunks) await writeClaudeStreamEvents(translateChunkToClaudeEvents(chunk, streamState, toolNameMapper), writeEvent);
|
|
3002
|
+
const upstreamStream = remainingStream ?? effectiveResponse;
|
|
3003
|
+
for await (const rawEvent of upstreamStream) {
|
|
2785
3004
|
if (rawEvent.data === "[DONE]") break;
|
|
2786
3005
|
if (!rawEvent.data) continue;
|
|
2787
3006
|
let chunk;
|
|
@@ -2924,6 +3143,7 @@ const createServer = (config) => {
|
|
|
2924
3143
|
status: "ok"
|
|
2925
3144
|
}));
|
|
2926
3145
|
app.get("/healthz", (c) => c.json({ ok: true }));
|
|
3146
|
+
app.on(["GET", "HEAD"], "/api/hello", (c) => c.body(null, 200));
|
|
2927
3147
|
app.route("/v1", claudeRoutes);
|
|
2928
3148
|
app.notFound(async (c) => {
|
|
2929
3149
|
const message = "Unsupported Claude API route";
|