betahi-copilot-bridge 0.20.4 → 0.20.6

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
@@ -1,7 +1,7 @@
1
1
  <h1 align="center">copilot-bridge</h1>
2
2
 
3
3
  <p align="center">
4
- <a href="https://www.npmjs.com/package/betahi-copilot-bridge"><img src="https://img.shields.io/npm/v/betahi-copilot-bridge.svg?v=0.20.4" alt="npm version"></a>
4
+ <a href="https://www.npmjs.com/package/betahi-copilot-bridge"><img src="https://img.shields.io/npm/v/betahi-copilot-bridge.svg?v=0.20.6" alt="npm version"></a>
5
5
  <a href="https://github.com/betahi/copilot-bridge/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/betahi-copilot-bridge.svg" alt="license"></a>
6
6
  </p>
7
7
 
@@ -168,8 +168,8 @@ COPILOT_WEB_SEARCH_BACKEND = "gpt-5.5"
168
168
  | Value | Search path | Requirement |
169
169
  | ----- | ----------- | ----------- |
170
170
  | Copilot model id, for example `gpt-5.5` | Copilot HTTP `/responses` + `web_search_preview` | The model must support Copilot Responses web search. |
171
- | `searxng` | Local SearXNG at `http://localhost:8080` | Start SearXNG yourself. Setup guide: https://github.com/betaHi/openclaw-searxng-search. |
172
- | `copilot-cli` or `copilot` | GitHub Copilot CLI `web_search` tool, using the current request model | Install and sign in to GitHub Copilot CLI yourself. |
171
+ | `searxng`, use `"COPILOT_WEB_SEARCH_BACKEND": "searxng"` | Local SearXNG at `http://localhost:8080` | Start SearXNG yourself. Setup guide: https://github.com/betaHi/openclaw-searxng-search. |
172
+ | `copilot-cli` or `copilot`, use `"COPILOT_WEB_SEARCH_BACKEND": "copilot-cli"` | GitHub Copilot CLI `web_search` tool, using the current request model | Install and sign in to GitHub Copilot CLI yourself. |
173
173
 
174
174
  The bridge never installs Docker, SearXNG, or Copilot CLI automatically.
175
175
 
package/dist/main.js CHANGED
@@ -2197,6 +2197,7 @@ function translateErrorToAnthropicErrorEvent() {
2197
2197
  //#endregion
2198
2198
  //#region src/bridges/claude/web-search.ts
2199
2199
  const ANTHROPIC_WEB_SEARCH_TOOL_PATTERN = /^web_search_\d{8}$/;
2200
+ const CLAUDE_CODE_WEB_SEARCH_TOOL_NAME = "WebSearch";
2200
2201
  const DEFAULT_SEARXNG_BASE_URL = "http://localhost:8080";
2201
2202
  const SEARCH_RESULT_LIMIT = 8;
2202
2203
  const SEARXNG_READINESS_TIMEOUT_MS = 800;
@@ -2207,7 +2208,7 @@ const COPILOT_CHAT_WRAPPER_PATH_PATTERN = /github\.copilot-chat[/\\]copilotCli/;
2207
2208
  const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2208
2209
  const isAnthropicNativeWebSearchTool = (tool) => {
2209
2210
  if (!isRecord$1(tool)) return false;
2210
- return tool.name === "web_search" && typeof tool.type === "string" && ANTHROPIC_WEB_SEARCH_TOOL_PATTERN.test(tool.type);
2211
+ return tool.name === "web_search" && typeof tool.type === "string" && ANTHROPIC_WEB_SEARCH_TOOL_PATTERN.test(tool.type) || tool.name === CLAUDE_CODE_WEB_SEARCH_TOOL_NAME;
2211
2212
  };
2212
2213
  const hasAnthropicNativeWebSearch = (payload) => payload.tools?.some(isAnthropicNativeWebSearchTool) ?? false;
2213
2214
  const textFromContent$1 = (content) => {
@@ -2914,6 +2915,24 @@ modelRoutes.get("/", async (c) => {
2914
2915
 
2915
2916
  //#endregion
2916
2917
  //#region src/bridges/codex/chat-fallback.ts
2918
+ const isResponsesFunctionTool = (tool) => tool.type === "function";
2919
+ const isResponsesWebSearchTool = (tool) => tool.type === "web_search" || tool.type === "web_search_preview";
2920
+ const WEB_SEARCH_FUNCTION_TOOL = {
2921
+ type: "function",
2922
+ function: {
2923
+ name: "web_search",
2924
+ description: "Search the web for current information and source URLs.",
2925
+ parameters: {
2926
+ type: "object",
2927
+ properties: { query: {
2928
+ type: "string",
2929
+ description: "The web search query."
2930
+ } },
2931
+ required: ["query"],
2932
+ additionalProperties: false
2933
+ }
2934
+ }
2935
+ };
2917
2936
  const collectText = (content) => {
2918
2937
  if (typeof content === "string") return {
2919
2938
  text: content,
@@ -2989,9 +3008,9 @@ const responsesPayloadToChatPayload = (request, capability) => {
2989
3008
  tool_call_id: item.call_id,
2990
3009
  content: item.output
2991
3010
  });
2992
- const tools = request.tools?.filter((t) => {
3011
+ const functionTools = request.tools?.filter(isResponsesFunctionTool).filter((t) => {
2993
3012
  const name = t.function?.name ?? t.name;
2994
- return t.type === "function" && typeof name === "string" && name.length > 0;
3013
+ return typeof name === "string" && name.length > 0;
2995
3014
  }).map((t) => ({
2996
3015
  type: "function",
2997
3016
  function: {
@@ -3000,6 +3019,13 @@ const responsesPayloadToChatPayload = (request, capability) => {
3000
3019
  parameters: t.function?.parameters ?? t.parameters
3001
3020
  }
3002
3021
  }));
3022
+ const hasWebSearchTool = request.tools?.some(isResponsesWebSearchTool) ?? false;
3023
+ const hasNamedWebSearchFunction = functionTools?.some((tool) => tool.function.name === "web_search") ?? false;
3024
+ const tools = [...functionTools ?? [], ...hasWebSearchTool && !hasNamedWebSearchFunction ? [WEB_SEARCH_FUNCTION_TOOL] : []];
3025
+ if (hasWebSearchTool) messages.unshift({
3026
+ role: "system",
3027
+ content: "A web_search tool is available for current web information and source URLs. Use it when the user asks to search the web; do not use it for unrelated tasks."
3028
+ });
3003
3029
  const payload = {
3004
3030
  model: capability.id,
3005
3031
  messages,
@@ -3008,7 +3034,7 @@ const responsesPayloadToChatPayload = (request, capability) => {
3008
3034
  temperature: request.temperature,
3009
3035
  top_p: request.top_p,
3010
3036
  user: request.user,
3011
- tools: tools && tools.length > 0 ? tools : void 0,
3037
+ tools: tools.length > 0 ? tools : void 0,
3012
3038
  tool_choice: tools && tools.length > 0 ? request.tool_choice : void 0
3013
3039
  };
3014
3040
  placeReasoning(payload, capability, request.reasoning?.effort);
@@ -3191,7 +3217,7 @@ const getRequestedQuery = (request) => {
3191
3217
  const cleaned = textFromContent([...inputItemsFromRequest(request)].reverse().find((item) => item.role === "user")?.content).replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, " ").replace(/\s+/g, " ").trim();
3192
3218
  return cleaned.match(/\bsearch(?:\s+the\s+web)?(?:\s+for)?\s+(.+)$/i)?.[1]?.trim() || cleaned || "web search";
3193
3219
  };
3194
- const buildSearchInput = (request) => {
3220
+ const buildSearchInput = (request, requestedQuery) => {
3195
3221
  const conversation = inputItemsFromRequest(request).flatMap((item) => {
3196
3222
  const role = typeof item.role === "string" ? item.role : item.type;
3197
3223
  const text = textFromContent(item.content ?? item.output);
@@ -3203,24 +3229,41 @@ const buildSearchInput = (request) => {
3203
3229
  "Return useful search results as plain text lines in this exact shape:",
3204
3230
  "1. Title - https://example.com/page",
3205
3231
  "Include only real source URLs from the search results.",
3232
+ `Search query:\n${requestedQuery}`,
3206
3233
  request.instructions ? `Instructions:\n${request.instructions}` : "",
3207
3234
  conversation ? `Conversation:\n${conversation}` : ""
3208
3235
  ].filter(Boolean).join("\n\n");
3209
3236
  };
3210
- const createExecutionRequest = (request) => ({
3211
- clientName: "Codex",
3212
- configurationHint: "Set COPILOT_WEB_SEARCH_BACKEND in ~/.codex/config.toml to a Copilot Responses search model id such as gpt-5.5, searxng, or copilot-cli.",
3213
- maxOutputTokens: request.max_output_tokens,
3214
- requestedQuery: getRequestedQuery(request),
3215
- searchInput: buildSearchInput(request),
3216
- temperature: request.temperature,
3217
- topP: request.top_p
3218
- });
3237
+ const createExecutionRequest = (request, requestedQuery) => {
3238
+ const query = requestedQuery?.trim() || getRequestedQuery(request);
3239
+ return {
3240
+ clientName: "Codex",
3241
+ configurationHint: "Set COPILOT_WEB_SEARCH_BACKEND in ~/.codex/config.toml to a Copilot Responses search model id such as gpt-5.5, searxng, or copilot-cli.",
3242
+ maxOutputTokens: request.max_output_tokens,
3243
+ requestedQuery: query,
3244
+ searchInput: buildSearchInput(request, query),
3245
+ temperature: request.temperature,
3246
+ topP: request.top_p
3247
+ };
3248
+ };
3219
3249
  const isCodexNativeWebSearchTool = (tool) => {
3220
3250
  if (!isRecord(tool)) return false;
3221
3251
  return tool.type === "web_search" || tool.type === "web_search_preview";
3222
3252
  };
3253
+ const isCodexNativeWebSearchToolChoice = (toolChoice) => {
3254
+ if (!isRecord(toolChoice)) return false;
3255
+ return toolChoice.type === "web_search" || toolChoice.type === "web_search_preview";
3256
+ };
3257
+ const hasOnlyCodexNativeWebSearchTools = (tools) => {
3258
+ if (!Array.isArray(tools) || tools.length === 0) return false;
3259
+ return tools.every(isCodexNativeWebSearchTool);
3260
+ };
3223
3261
  const hasCodexNativeWebSearch = (request) => request.tools?.some(isCodexNativeWebSearchTool) ?? false;
3262
+ const isCodexNativeWebSearchRequested = (request) => {
3263
+ if (!hasCodexNativeWebSearch(request)) return false;
3264
+ if (isCodexNativeWebSearchToolChoice(request.tool_choice)) return true;
3265
+ return request.tool_choice === "required" && hasOnlyCodexNativeWebSearchTools(request.tools);
3266
+ };
3224
3267
  const fallbackSearchText = (search) => {
3225
3268
  if (search.text) return search.text;
3226
3269
  if (search.results.length === 0) return "Web search did not return search results.";
@@ -3344,7 +3387,7 @@ const codexWebSearchResponseToSse = (response) => {
3344
3387
  } });
3345
3388
  };
3346
3389
  async function createCodexWebSearchResponse(config, request, options) {
3347
- return buildCodexWebSearchResponse(request, await createWebSearchExecution(config, createExecutionRequest(request), {
3390
+ return buildCodexWebSearchResponse(request, await createWebSearchExecution(config, createExecutionRequest(request, options.requestedQuery), {
3348
3391
  backend: options.backend,
3349
3392
  copilotCliModel: request.model
3350
3393
  }));
@@ -3520,6 +3563,17 @@ const logResponsesUpstreamError = async (message, response, context) => {
3520
3563
  const readCodexUserConfig = async () => {
3521
3564
  return readCodexUserConfigFromDisk(process.env.CODEX_CONFIG_PATH ?? CODEX_DEFAULTS.configPath);
3522
3565
  };
3566
+ const getCodexWebSearchQueryFromChatResponse = (chatJson) => {
3567
+ const toolCall = chatJson.choices.flatMap((choice) => choice.message.tool_calls ?? []).find((call) => call.function.name === "web_search");
3568
+ if (!toolCall) return;
3569
+ try {
3570
+ const parsed = JSON.parse(toolCall.function.arguments);
3571
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && typeof parsed.query === "string") return parsed.query;
3572
+ } catch {
3573
+ return toolCall.function.arguments;
3574
+ }
3575
+ return toolCall.function.arguments;
3576
+ };
3523
3577
  responsesRoutes.post("/", async (c) => {
3524
3578
  try {
3525
3579
  await checkRateLimit();
@@ -3540,7 +3594,7 @@ responsesRoutes.post("/", async (c) => {
3540
3594
  const capability = getModelCapability(payload.model);
3541
3595
  try {
3542
3596
  if (capability?.fallback === "chat-completions") {
3543
- if (hasCodexNativeWebSearch(payload)) {
3597
+ if (isCodexNativeWebSearchRequested(payload)) {
3544
3598
  const response = await createCodexWebSearchResponse(config, payload, { backend: codexUserConfig.webSearchBackend });
3545
3599
  if (payload.stream) return new Response(codexWebSearchResponseToSse(response), {
3546
3600
  status: 200,
@@ -3577,6 +3631,25 @@ responsesRoutes.post("/", async (c) => {
3577
3631
  });
3578
3632
  }
3579
3633
  const chatJson = await upstream$1.json();
3634
+ const requestedWebSearchQuery = getCodexWebSearchQueryFromChatResponse(chatJson);
3635
+ if (requestedWebSearchQuery) {
3636
+ const response = await createCodexWebSearchResponse(config, payload, {
3637
+ backend: codexUserConfig.webSearchBackend,
3638
+ requestedQuery: requestedWebSearchQuery
3639
+ });
3640
+ if (payload.stream) return new Response(codexWebSearchResponseToSse(response), {
3641
+ status: 200,
3642
+ headers: {
3643
+ "content-type": "text/event-stream; charset=utf-8",
3644
+ "cache-control": "no-cache",
3645
+ connection: "keep-alive"
3646
+ }
3647
+ });
3648
+ return new Response(JSON.stringify(response), {
3649
+ status: 200,
3650
+ headers: { "content-type": "application/json" }
3651
+ });
3652
+ }
3580
3653
  if (payload.stream) {
3581
3654
  const stream = synthesizeResponsesSseFromChat(payload, chatJson);
3582
3655
  return new Response(stream, {