copilot-relay 0.2.6 → 0.2.7

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 CHANGED
@@ -24,6 +24,11 @@ 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
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?.role !== "assistant") return payload;
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: "system",
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 for the final answer. Do not describe it as user-provided or injected. Do not call web_search again.",
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 createFinalWebSearchPayload = (payload, search) => {
2451
- const messages = payload.messages.flatMap((message) => {
2452
- if (message.role === "tool") return [{
2453
- role: "developer",
2454
- content: ["Prior local tool result from the conversation:", String(message.content ?? "")].join("\n")
2455
- }];
2456
- if (message.tool_calls && message.tool_calls.length > 0) return message.content ? [{
2457
- ...message,
2458
- tool_calls: void 0
2459
- }] : [];
2460
- return [message];
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: void 0,
2466
- tool_choice: void 0,
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) => ({
@@ -2744,7 +2754,7 @@ const handleClaudeMessageRequest = async (config, claudePayload, requestSignal,
2744
2754
  const searchResponse = createClaudeWebSearchResponse(search);
2745
2755
  if (search.results.length === 0) claudeResponse = searchResponse;
2746
2756
  else {
2747
- const finalResponse = await createChatCompletions(config, createFinalWebSearchPayload(openAIPayload, search), {
2757
+ const finalResponse = await createChatCompletions(config, createFinalWebSearchPayload(openAIPayload, search, toolNameMapper), {
2748
2758
  client: "claude",
2749
2759
  requestedModel: claudePayload.model,
2750
2760
  requestedThinkEffort: getClaudeRequestedThinkEffort(claudePayload),
@@ -2924,6 +2934,7 @@ const createServer = (config) => {
2924
2934
  status: "ok"
2925
2935
  }));
2926
2936
  app.get("/healthz", (c) => c.json({ ok: true }));
2937
+ app.on(["GET", "HEAD"], "/api/hello", (c) => c.body(null, 200));
2927
2938
  app.route("/v1", claudeRoutes);
2928
2939
  app.notFound(async (c) => {
2929
2940
  const message = "Unsupported Claude API route";