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 +3 -3
- package/dist/main.js +89 -16
- 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.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
|
|
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);
|
|
@@ -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
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
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 (
|
|
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, {
|