betahi-copilot-bridge 0.20.4 → 0.20.5

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.5" 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);
@@ -3207,11 +3233,11 @@ const buildSearchInput = (request) => {
3207
3233
  conversation ? `Conversation:\n${conversation}` : ""
3208
3234
  ].filter(Boolean).join("\n\n");
3209
3235
  };
3210
- const createExecutionRequest = (request) => ({
3236
+ const createExecutionRequest = (request, requestedQuery) => ({
3211
3237
  clientName: "Codex",
3212
3238
  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
3239
  maxOutputTokens: request.max_output_tokens,
3214
- requestedQuery: getRequestedQuery(request),
3240
+ requestedQuery: requestedQuery?.trim() || getRequestedQuery(request),
3215
3241
  searchInput: buildSearchInput(request),
3216
3242
  temperature: request.temperature,
3217
3243
  topP: request.top_p
@@ -3220,7 +3246,20 @@ const isCodexNativeWebSearchTool = (tool) => {
3220
3246
  if (!isRecord(tool)) return false;
3221
3247
  return tool.type === "web_search" || tool.type === "web_search_preview";
3222
3248
  };
3249
+ const isCodexNativeWebSearchToolChoice = (toolChoice) => {
3250
+ if (!isRecord(toolChoice)) return false;
3251
+ return toolChoice.type === "web_search" || toolChoice.type === "web_search_preview";
3252
+ };
3253
+ const hasOnlyCodexNativeWebSearchTools = (tools) => {
3254
+ if (!Array.isArray(tools) || tools.length === 0) return false;
3255
+ return tools.every(isCodexNativeWebSearchTool);
3256
+ };
3223
3257
  const hasCodexNativeWebSearch = (request) => request.tools?.some(isCodexNativeWebSearchTool) ?? false;
3258
+ const isCodexNativeWebSearchRequested = (request) => {
3259
+ if (!hasCodexNativeWebSearch(request)) return false;
3260
+ if (isCodexNativeWebSearchToolChoice(request.tool_choice)) return true;
3261
+ return request.tool_choice === "required" && hasOnlyCodexNativeWebSearchTools(request.tools);
3262
+ };
3224
3263
  const fallbackSearchText = (search) => {
3225
3264
  if (search.text) return search.text;
3226
3265
  if (search.results.length === 0) return "Web search did not return search results.";
@@ -3344,7 +3383,7 @@ const codexWebSearchResponseToSse = (response) => {
3344
3383
  } });
3345
3384
  };
3346
3385
  async function createCodexWebSearchResponse(config, request, options) {
3347
- return buildCodexWebSearchResponse(request, await createWebSearchExecution(config, createExecutionRequest(request), {
3386
+ return buildCodexWebSearchResponse(request, await createWebSearchExecution(config, createExecutionRequest(request, options.requestedQuery), {
3348
3387
  backend: options.backend,
3349
3388
  copilotCliModel: request.model
3350
3389
  }));
@@ -3520,6 +3559,17 @@ const logResponsesUpstreamError = async (message, response, context) => {
3520
3559
  const readCodexUserConfig = async () => {
3521
3560
  return readCodexUserConfigFromDisk(process.env.CODEX_CONFIG_PATH ?? CODEX_DEFAULTS.configPath);
3522
3561
  };
3562
+ const getCodexWebSearchQueryFromChatResponse = (chatJson) => {
3563
+ const toolCall = chatJson.choices.flatMap((choice) => choice.message.tool_calls ?? []).find((call) => call.function.name === "web_search");
3564
+ if (!toolCall) return;
3565
+ try {
3566
+ const parsed = JSON.parse(toolCall.function.arguments);
3567
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && typeof parsed.query === "string") return parsed.query;
3568
+ } catch {
3569
+ return toolCall.function.arguments;
3570
+ }
3571
+ return toolCall.function.arguments;
3572
+ };
3523
3573
  responsesRoutes.post("/", async (c) => {
3524
3574
  try {
3525
3575
  await checkRateLimit();
@@ -3540,7 +3590,7 @@ responsesRoutes.post("/", async (c) => {
3540
3590
  const capability = getModelCapability(payload.model);
3541
3591
  try {
3542
3592
  if (capability?.fallback === "chat-completions") {
3543
- if (hasCodexNativeWebSearch(payload)) {
3593
+ if (isCodexNativeWebSearchRequested(payload)) {
3544
3594
  const response = await createCodexWebSearchResponse(config, payload, { backend: codexUserConfig.webSearchBackend });
3545
3595
  if (payload.stream) return new Response(codexWebSearchResponseToSse(response), {
3546
3596
  status: 200,
@@ -3577,6 +3627,25 @@ responsesRoutes.post("/", async (c) => {
3577
3627
  });
3578
3628
  }
3579
3629
  const chatJson = await upstream$1.json();
3630
+ const requestedWebSearchQuery = getCodexWebSearchQueryFromChatResponse(chatJson);
3631
+ if (requestedWebSearchQuery) {
3632
+ const response = await createCodexWebSearchResponse(config, payload, {
3633
+ backend: codexUserConfig.webSearchBackend,
3634
+ requestedQuery: requestedWebSearchQuery
3635
+ });
3636
+ if (payload.stream) return new Response(codexWebSearchResponseToSse(response), {
3637
+ status: 200,
3638
+ headers: {
3639
+ "content-type": "text/event-stream; charset=utf-8",
3640
+ "cache-control": "no-cache",
3641
+ connection: "keep-alive"
3642
+ }
3643
+ });
3644
+ return new Response(JSON.stringify(response), {
3645
+ status: 200,
3646
+ headers: { "content-type": "application/json" }
3647
+ });
3648
+ }
3580
3649
  if (payload.stream) {
3581
3650
  const stream = synthesizeResponsesSseFromChat(payload, chatJson);
3582
3651
  return new Response(stream, {