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 +3 -3
- package/dist/main.js +77 -8
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
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
|
+
<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
|
|
3011
|
+
const functionTools = request.tools?.filter(isResponsesFunctionTool).filter((t) => {
|
|
2993
3012
|
const name = t.function?.name ?? t.name;
|
|
2994
|
-
return
|
|
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
|
|
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 (
|
|
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, {
|