@bitkyc08/opencodex 2.54.0 → 2.55.0
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/gui/dist/assets/{index-CkvITofZ.js → index-VuoiWj9J.js} +10 -10
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +57 -0
- package/src/adapters/anthropic-image-normalize.ts +28 -1
- package/src/adapters/anthropic.ts +68 -6
- package/src/adapters/base.ts +8 -0
- package/src/adapters/coding-agent/protocol.ts +41 -16
- package/src/adapters/cursor/cursor-errors.ts +1 -1
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/native-exec-fs.ts +10 -10
- package/src/adapters/cursor/native-exec-network.ts +2 -2
- package/src/adapters/cursor/native-exec-shell.ts +13 -12
- package/src/adapters/cursor/native-exec.ts +51 -10
- package/src/adapters/cursor/policy-error.ts +75 -0
- package/src/adapters/cursor/protobuf-request.ts +105 -1
- package/src/adapters/devin/cloud-direct/catalog.ts +34 -2
- package/src/adapters/devin/live-models.ts +33 -2
- package/src/adapters/google-wire-compiler.ts +8 -0
- package/src/adapters/google.ts +46 -0
- package/src/adapters/input-media-guard.ts +45 -0
- package/src/adapters/kiro/adapter.ts +8 -0
- package/src/adapters/kiro/payload.ts +28 -6
- package/src/adapters/kiro-events.ts +25 -6
- package/src/adapters/kiro-images.ts +30 -0
- package/src/adapters/kiro-retry.ts +8 -0
- package/src/adapters/openai-chat.ts +33 -4
- package/src/adapters/openai-responses.ts +26 -0
- package/src/adapters/registry.ts +4 -0
- package/src/bridge.ts +163 -116
- package/src/chat/image-parts.ts +151 -0
- package/src/chat/inbound.ts +70 -33
- package/src/cli/connect.ts +30 -9
- package/src/cli/dispatch.ts +7 -3
- package/src/cli/index.ts +3 -0
- package/src/cli/runtime-api.ts +25 -0
- package/src/cli/status.ts +21 -19
- package/src/cli/system-restart-client.ts +25 -0
- package/src/clients/config-export.ts +14 -4
- package/src/codex/app-server-processes.ts +25 -0
- package/src/codex/auth-context.ts +8 -0
- package/src/codex/autostart-health.ts +36 -2
- package/src/codex/catalog/provider-fetch.ts +41 -0
- package/src/codex/catalog-auto-refresh.ts +182 -0
- package/src/codex/catalog-refresh-status.ts +93 -0
- package/src/codex/history-provider.ts +55 -0
- package/src/codex/model-entitlements.ts +78 -0
- package/src/codex/native-profile-processes.ts +114 -15
- package/src/codex/prompt-text-probe.ts +274 -41
- package/src/codex/routing-adoption.ts +189 -0
- package/src/codex/routing.ts +520 -48
- package/src/codex/runtime.ts +249 -7
- package/src/combos/failover.ts +45 -0
- package/src/config.ts +124 -4
- package/src/generated/compatibility-version.json +110 -74
- package/src/generated/model-metadata.ts +1 -0
- package/src/lib/request-execution-budget.ts +202 -0
- package/src/lib/upstream-retry.ts +95 -8
- package/src/lib/workflow-budget.ts +172 -0
- package/src/oauth/devin.ts +57 -12
- package/src/providers/quota.ts +37 -6
- package/src/providers/registry.ts +53 -6
- package/src/responses/input-media.ts +65 -0
- package/src/responses/parser-content.ts +42 -0
- package/src/responses/schema.ts +12 -2
- package/src/server/audio-live.ts +1 -2
- package/src/server/audio-transcriptions.ts +1 -2
- package/src/server/auth-cors.ts +1 -1
- package/src/server/background-lifecycle.ts +18 -0
- package/src/server/chat-completions.ts +23 -8
- package/src/server/chat-native.ts +17 -17
- package/src/server/index.ts +24 -0
- package/src/server/management/request-history-routes.ts +5 -0
- package/src/server/request-log.ts +8 -2
- package/src/server/responses/compact.ts +51 -3
- package/src/server/responses/core.ts +238 -28
- package/src/server/search.ts +7 -9
- package/src/types/config.ts +51 -6
- package/src/usage/log.ts +37 -0
- package/src/vision/eligibility.ts +37 -4
- package/src/vision/index.ts +1 -0
- package/src/vision/plan.ts +45 -10
- package/src/web-search/alpha-search.ts +324 -0
- package/src/web-search/index.ts +13 -22
- package/src/web-search/passthrough-bridge.ts +195 -22
- package/src/web-search/sidecar-providers.ts +22 -0
|
@@ -18,10 +18,19 @@
|
|
|
18
18
|
*
|
|
19
19
|
* Deliberate boundaries of this first slice:
|
|
20
20
|
* - Streaming SSE turns only. A non-streaming turn stays on the existing path.
|
|
21
|
-
* - A leg that mixes the search call with
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* - A leg that mixes the search call with a client-executed tool call ends the turn ON that
|
|
22
|
+
* leg: the intercepted searches still run proxy-side so the hosted cell completes, the
|
|
23
|
+
* held client calls are released for Codex to run, and the leg's own terminal closes the
|
|
24
|
+
* turn. No continuation is sent upstream, because the client's call is unanswered and the
|
|
25
|
+
* conversation owes the client a turn, not the gateway. When the leg's terminal already
|
|
26
|
+
* ended the turn (response.failed / response.incomplete) the searches are not run at all:
|
|
27
|
+
* the opened cells close unanswered and that terminal is relayed, because billing a search
|
|
28
|
+
* for a dead turn buys nothing. What is still not fixed: the
|
|
29
|
+
* gateway never receives the executed search result -- Codex replays the hosted
|
|
30
|
+
* web_search_call cell (query and sources, no result text) on the next turn and the
|
|
31
|
+
* gateway's own function_call/function_call_output pair is not reconstructed. Making it
|
|
32
|
+
* whole needs the outbound body rewritten before the first leg is dispatched, which lives
|
|
33
|
+
* in src/server/responses/core.ts and is out of this module's scope.
|
|
25
34
|
* - Assistant text is never treated as a search instruction. The bridge intercepts structured
|
|
26
35
|
* function_call / custom_tool_call items named web_search, not XML-like prose.
|
|
27
36
|
* - Non-Ollama backends reuse the sidecar executors and those executors' own credentials.
|
|
@@ -61,13 +70,56 @@ import {
|
|
|
61
70
|
findAnthropicSidecarProvider,
|
|
62
71
|
findGeminiSidecarProvider,
|
|
63
72
|
findXaiSidecarProvider,
|
|
73
|
+
resolveSidecarBackend,
|
|
64
74
|
xaiSearchOptionsFromConfig,
|
|
65
75
|
} from "./sidecar-providers";
|
|
76
|
+
import { providerDestinationConfigError } from "../lib/destination-policy";
|
|
77
|
+
import { redactSecretString } from "../lib/redact";
|
|
66
78
|
|
|
67
79
|
/** Canonical Ollama Cloud origin. The only origin the "ollama" backend derives on its own. */
|
|
68
80
|
export const OLLAMA_CLOUD_ORIGIN = "https://ollama.com";
|
|
69
81
|
const OLLAMA_WEB_SEARCH_PATH = "/api/web_search";
|
|
70
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Providers already warned about a destination-refused bridge endpoint. The planner runs per
|
|
85
|
+
* request, so without this a refused endpoint would warn on every turn. Keyed on provider plus
|
|
86
|
+
* endpoint so that editing the config warns again; the key itself is never logged.
|
|
87
|
+
*/
|
|
88
|
+
const warnedRefusedBridgeEndpoints = new Set<string>();
|
|
89
|
+
/** Bound the dedupe set so a pathological config cannot grow it without limit. */
|
|
90
|
+
const MAX_WARNED_REFUSED_ENDPOINTS = 64;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A refused endpoint disarms the bridge, and the refusal itself has to stay silent at the point of
|
|
94
|
+
* use -- returning undefined is what keeps the key unspent. But silence alone made a real
|
|
95
|
+
* configuration fail invisibly: a provider keyed under a CUSTOM name (say "my-ollama") pointing at
|
|
96
|
+
* a loopback endpoint used to arm, and the destination policy now refuses it because only the
|
|
97
|
+
* registry ids are local by default. The config file never reaches
|
|
98
|
+
* "providerWebSearchBridgeConfigError", so nothing else would tell the operator. One warning per
|
|
99
|
+
* provider and endpoint gives them the remedy without leaking the destination: the URL is
|
|
100
|
+
* deliberately omitted and the provider name is redacted, because a provider key is
|
|
101
|
+
* caller-controlled and can be token-shaped.
|
|
102
|
+
*/
|
|
103
|
+
function warnRefusedBridgeEndpointOnce(providerName: string, endpoint: string): void {
|
|
104
|
+
const key = providerName + "\u0000" + endpoint;
|
|
105
|
+
if (warnedRefusedBridgeEndpoints.has(key)) return;
|
|
106
|
+
if (warnedRefusedBridgeEndpoints.size >= MAX_WARNED_REFUSED_ENDPOINTS) {
|
|
107
|
+
warnedRefusedBridgeEndpoints.clear();
|
|
108
|
+
}
|
|
109
|
+
warnedRefusedBridgeEndpoints.add(key);
|
|
110
|
+
console.warn(
|
|
111
|
+
"[web-search] provider " + JSON.stringify(redactSecretString(providerName))
|
|
112
|
+
+ " webSearchBridge.endpoint was refused by destination policy, so the bridge stays disarmed."
|
|
113
|
+
+ " Set allowPrivateNetwork:true for an intentionally local endpoint, or key the provider under"
|
|
114
|
+
+ " its registry id (ollama, vllm, lm-studio, litellm).",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Test seam: the dedupe is process-wide, so a test that asserts the warning must reset it. */
|
|
119
|
+
export function resetRefusedBridgeEndpointWarningsForTests(): void {
|
|
120
|
+
warnedRefusedBridgeEndpoints.clear();
|
|
121
|
+
}
|
|
122
|
+
|
|
71
123
|
const DEFAULT_BRIDGE_MAX_SEARCHES = 3;
|
|
72
124
|
const DEFAULT_BRIDGE_TIMEOUT_MS = 60_000;
|
|
73
125
|
/** Queries honored from one call's "queries" array; the rest are ignored rather than billed. */
|
|
@@ -77,6 +129,11 @@ const MAX_RETAINED_OUTPUT_ITEMS = 500;
|
|
|
77
129
|
/** Refuse to buffer an unbounded partial SSE event from a misbehaving upstream. */
|
|
78
130
|
const MAX_SSE_BUFFER_CHARS = 8 * 1024 * 1024;
|
|
79
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Retained for importers that pinned the first slice's contract: a leg mixing the search with
|
|
134
|
+
* a client-executed call used to fail with this code. Such legs now end the turn on the leg
|
|
135
|
+
* instead of failing, so nothing emits it any more.
|
|
136
|
+
*/
|
|
80
137
|
export const WEB_SEARCH_BRIDGE_MIXED_TOOLS_ERROR_CODE = "web_search_bridge_mixed_tools";
|
|
81
138
|
export const WEB_SEARCH_BRIDGE_ERROR_CODE = "web_search_bridge_failed";
|
|
82
139
|
|
|
@@ -122,13 +179,33 @@ function originOf(value: string | undefined): string | undefined {
|
|
|
122
179
|
* that receives this provider's API key. Without one, the origin must be canonical Ollama Cloud
|
|
123
180
|
* -- a renamed row pointing at an arbitrary host must not silently receive the key just because
|
|
124
181
|
* its adapter happens to be openai-responses.
|
|
182
|
+
*
|
|
183
|
+
* Naming a destination is not the same as it being an allowed one. The endpoint therefore gets the
|
|
184
|
+
* same literal destination assessment "baseUrl" already gets (#4519): metadata addresses are
|
|
185
|
+
* refused outright, and loopback/private need the provider's "allowPrivateNetwork" opt-in or a
|
|
186
|
+
* registry entry that is local by definition, so a local Ollama on 127.0.0.1 keeps working. This
|
|
187
|
+
* is the ONLY reader of "webSearchBridge.endpoint" in the tree, which is what lets it act as the
|
|
188
|
+
* authorization boundary for a config file the operator edited by hand -- that path never reaches
|
|
189
|
+
* "providerWebSearchBridgeConfigError", so a value that survives file load simply cannot be spent.
|
|
190
|
+
* The refusal returns undefined rather than an error, because disarming is what keeps the key
|
|
191
|
+
* unspent -- but it is not silent: see warnRefusedBridgeEndpointOnce for why a custom-named local
|
|
192
|
+
* provider has to be told, once, that its endpoint was refused and how to re-authorize it.
|
|
125
193
|
*/
|
|
126
194
|
export function resolveOllamaWebSearchEndpoint(
|
|
195
|
+
providerName: string,
|
|
127
196
|
provider: OcxProviderConfig,
|
|
128
197
|
): string | undefined {
|
|
129
198
|
const configured = provider.webSearchBridge?.endpoint;
|
|
130
199
|
if (configured !== undefined) {
|
|
131
|
-
|
|
200
|
+
if (originOf(configured) === undefined) return undefined;
|
|
201
|
+
if (providerDestinationConfigError(providerName, {
|
|
202
|
+
baseUrl: configured,
|
|
203
|
+
allowPrivateNetwork: provider.allowPrivateNetwork,
|
|
204
|
+
})) {
|
|
205
|
+
warnRefusedBridgeEndpointOnce(providerName, configured);
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
return configured;
|
|
132
209
|
}
|
|
133
210
|
return originOf(provider.baseUrl) === OLLAMA_CLOUD_ORIGIN
|
|
134
211
|
? OLLAMA_CLOUD_ORIGIN + OLLAMA_WEB_SEARCH_PATH
|
|
@@ -210,6 +287,12 @@ export function planPassthroughWebSearchBridge(
|
|
|
210
287
|
parsed: OcxParsedRequest,
|
|
211
288
|
provider: OcxProviderConfig,
|
|
212
289
|
options: {
|
|
290
|
+
/**
|
|
291
|
+
* Registry key for this provider. Required rather than optional: the destination assessment
|
|
292
|
+
* consults the registry's local-by-default entries, and an absent name would silently pick a
|
|
293
|
+
* different answer than the operator configured.
|
|
294
|
+
*/
|
|
295
|
+
providerName: string;
|
|
213
296
|
isPassthrough: boolean;
|
|
214
297
|
stream: boolean;
|
|
215
298
|
auth?: PassthroughWebSearchBridgeAuth;
|
|
@@ -238,7 +321,7 @@ export function planPassthroughWebSearchBridge(
|
|
|
238
321
|
? bridge.timeoutMs!
|
|
239
322
|
: DEFAULT_BRIDGE_TIMEOUT_MS;
|
|
240
323
|
if (backend === "ollama") {
|
|
241
|
-
const endpoint = resolveOllamaWebSearchEndpoint(provider);
|
|
324
|
+
const endpoint = resolveOllamaWebSearchEndpoint(options.providerName, provider);
|
|
242
325
|
if (!endpoint) return undefined;
|
|
243
326
|
return { backend, endpoint, maxSearches, timeoutMs };
|
|
244
327
|
}
|
|
@@ -357,10 +440,16 @@ async function* readSseBlocks(
|
|
|
357
440
|
}
|
|
358
441
|
}
|
|
359
442
|
interface LegDecision {
|
|
360
|
-
kind: "end" | "continue" | "fail";
|
|
443
|
+
kind: "end" | "endAfterSearch" | "endWithoutSearch" | "continue" | "fail";
|
|
361
444
|
searches: InterceptedSearchCall[];
|
|
362
445
|
message?: string;
|
|
363
446
|
code?: string;
|
|
447
|
+
/**
|
|
448
|
+
* Whether an endWithoutSearch leg may hand its withheld client-executed calls back.
|
|
449
|
+
* Only `response.incomplete` may: the client can still act on that turn. A
|
|
450
|
+
* `response.failed` terminal must not, for the same reason the fail path drops them.
|
|
451
|
+
*/
|
|
452
|
+
releaseHeldCalls?: boolean;
|
|
364
453
|
}
|
|
365
454
|
|
|
366
455
|
/** One client-executed call event held until the leg's fate is known. */
|
|
@@ -584,21 +673,40 @@ class BridgeStreamState {
|
|
|
584
673
|
return blocks;
|
|
585
674
|
}
|
|
586
675
|
|
|
676
|
+
/**
|
|
677
|
+
* Discard the withheld client-executed calls without emitting them. Used when the turn is
|
|
678
|
+
* ending in a state the client cannot act on, where releasing the call would start work
|
|
679
|
+
* under a turn that is already over.
|
|
680
|
+
*/
|
|
681
|
+
dropHeldCalls(): void {
|
|
682
|
+
this.heldCalls = [];
|
|
683
|
+
}
|
|
684
|
+
|
|
587
685
|
/** Decide what the leg's terminal means once the whole leg has been read. */
|
|
588
686
|
decide(remainingLegs: number): LegDecision {
|
|
589
687
|
if (this.searches.length === 0) return { kind: "end", searches: [] };
|
|
590
|
-
|
|
688
|
+
const terminalType = this.terminalPayload?.type;
|
|
689
|
+
if (terminalType === "response.failed" || terminalType === "response.incomplete") {
|
|
690
|
+
// The upstream terminal already ended this leg, so running the intercepted searches now
|
|
691
|
+
// would bill a search for a dead turn. The opened cells are closed unanswered instead.
|
|
692
|
+
//
|
|
693
|
+
// The two terminals differ in what happens to a withheld client-executed call, and
|
|
694
|
+
// lumping them together released one under a failed turn. `response.incomplete` leaves a
|
|
695
|
+
// turn the client can still act on, so its held call goes back. `response.failed` does
|
|
696
|
+
// not, and handing Codex a tool call to start executing inside a dead turn is the exact
|
|
697
|
+
// thing the fail path below refuses to do.
|
|
591
698
|
return {
|
|
592
|
-
kind: "
|
|
699
|
+
kind: "endWithoutSearch",
|
|
593
700
|
searches: this.searches,
|
|
594
|
-
|
|
595
|
-
message: "routed provider requested web_search alongside another client tool in one turn; "
|
|
596
|
-
+ "the web-search bridge cannot answer both without dropping the client's call",
|
|
701
|
+
releaseHeldCalls: terminalType === "response.incomplete",
|
|
597
702
|
};
|
|
598
703
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
704
|
+
if (this.sawClientExecutedCall) {
|
|
705
|
+
// The client's own call is unanswered, so this leg cannot continue upstream: the
|
|
706
|
+
// conversation owes the client a turn, not the gateway. The intercepted searches still
|
|
707
|
+
// run so the hosted cell completes rather than dangling, then the held calls go back to
|
|
708
|
+
// the client and the leg's own terminal ends the turn.
|
|
709
|
+
return { kind: "endAfterSearch", searches: this.searches };
|
|
602
710
|
}
|
|
603
711
|
if (remainingLegs <= 0) {
|
|
604
712
|
return {
|
|
@@ -677,7 +785,7 @@ export interface PassthroughWebSearchBridgeExecutorContext {
|
|
|
677
785
|
auth?: PassthroughWebSearchBridgeAuth;
|
|
678
786
|
hostedTool?: Record<string, unknown>;
|
|
679
787
|
describeImages?: boolean;
|
|
680
|
-
sidecar?: Pick<OcxWebSearchSidecarConfig, "model" | "reasoning" | "xSearch">;
|
|
788
|
+
sidecar?: Pick<OcxWebSearchSidecarConfig, "backend" | "model" | "reasoning" | "xSearch">;
|
|
681
789
|
}
|
|
682
790
|
|
|
683
791
|
const DEFAULT_OPENAI_BRIDGE_MODEL = "gpt-5.6-luna";
|
|
@@ -686,18 +794,52 @@ const DEFAULT_XAI_BRIDGE_MODEL = "grok-4.6";
|
|
|
686
794
|
const DEFAULT_GEMINI_BRIDGE_MODEL = "gemini-3.8-flash";
|
|
687
795
|
const DEFAULT_BRIDGE_REASONING = "low";
|
|
688
796
|
|
|
689
|
-
|
|
797
|
+
/**
|
|
798
|
+
* Search model each bridge backend runs when the global sidecar block was configured for a
|
|
799
|
+
* DIFFERENT backend (see modelForBridgeBackend). Exhaustive over the backend union on purpose:
|
|
800
|
+
* a seventh backend must decide its own default here rather than fall through to a ChatGPT model.
|
|
801
|
+
* The `ollama` and `exa` rows are inert — runOllamaWebSearch takes no model argument and
|
|
802
|
+
* runExaWebSearch reads only settings.timeoutMs — and must stay that way.
|
|
803
|
+
*/
|
|
804
|
+
const DEFAULT_BRIDGE_MODELS: Record<ProviderWebSearchBridgeBackend, string> = {
|
|
805
|
+
ollama: DEFAULT_OPENAI_BRIDGE_MODEL,
|
|
806
|
+
openai: DEFAULT_OPENAI_BRIDGE_MODEL,
|
|
807
|
+
anthropic: DEFAULT_ANTHROPIC_BRIDGE_MODEL,
|
|
808
|
+
xai: DEFAULT_XAI_BRIDGE_MODEL,
|
|
809
|
+
gemini: DEFAULT_GEMINI_BRIDGE_MODEL,
|
|
810
|
+
exa: DEFAULT_OPENAI_BRIDGE_MODEL,
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* `sidecar` is the GLOBAL `config.webSearchSidecar` block, which carries the model chosen for
|
|
815
|
+
* ITS backend. The bridge backend is the per-provider `webSearchBridge.backend` and the two are
|
|
816
|
+
* configured independently, so the operator's model only means anything here when they agree:
|
|
817
|
+
* a global {backend:"openai", model:"gpt-5.6-luna"} otherwise reaches runAnthropicWebSearch and
|
|
818
|
+
* Anthropic rejects the model. On a mismatch the bridge falls back to the backend's own default.
|
|
819
|
+
* The same reasoning already pins the backend first in planWebSearch.
|
|
820
|
+
*
|
|
821
|
+
* Only the model is gated. `reasoning` is a generic effort level, and `xSearch` is xai-only with
|
|
822
|
+
* no per-backend default and no `webSearchBridge.xSearch` equivalent, so gating it would make an
|
|
823
|
+
* openai sidecar plus an xai bridge plus x_search impossible to express at all.
|
|
824
|
+
*/
|
|
825
|
+
function modelForBridgeBackend(
|
|
826
|
+
backend: ProviderWebSearchBridgeBackend,
|
|
827
|
+
sidecar: Pick<OcxWebSearchSidecarConfig, "backend" | "model">,
|
|
828
|
+
): string {
|
|
829
|
+
const backendDefault = DEFAULT_BRIDGE_MODELS[backend];
|
|
830
|
+
if (resolveSidecarBackend(sidecar.backend) !== backend) return backendDefault;
|
|
831
|
+
return sidecar.model ?? backendDefault;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/** The settings a bridge executor will run with. Exported for tests; the executor closes over it. */
|
|
835
|
+
export function sidecarSettingsForBridge(
|
|
690
836
|
backend: ProviderWebSearchBridgeBackend,
|
|
691
837
|
plan: PassthroughWebSearchBridgePlan,
|
|
692
838
|
context: PassthroughWebSearchBridgeExecutorContext,
|
|
693
839
|
): SidecarSettings {
|
|
694
840
|
const sidecar = context.sidecar ?? {};
|
|
695
|
-
const model = backend === "anthropic" ? sidecar.model ?? DEFAULT_ANTHROPIC_BRIDGE_MODEL
|
|
696
|
-
: backend === "xai" ? sidecar.model ?? DEFAULT_XAI_BRIDGE_MODEL
|
|
697
|
-
: backend === "gemini" ? sidecar.model ?? DEFAULT_GEMINI_BRIDGE_MODEL
|
|
698
|
-
: sidecar.model ?? DEFAULT_OPENAI_BRIDGE_MODEL;
|
|
699
841
|
return {
|
|
700
|
-
model,
|
|
842
|
+
model: modelForBridgeBackend(backend, sidecar),
|
|
701
843
|
reasoning: sidecar.reasoning ?? DEFAULT_BRIDGE_REASONING,
|
|
702
844
|
timeoutMs: plan.timeoutMs,
|
|
703
845
|
describeImages: context.describeImages === true,
|
|
@@ -881,6 +1023,26 @@ async function* bridgeStreamBlocks(
|
|
|
881
1023
|
return;
|
|
882
1024
|
}
|
|
883
1025
|
|
|
1026
|
+
if (decision.kind === "endWithoutSearch") {
|
|
1027
|
+
// The upstream terminal already ended this leg, so billing a search now would pay for a
|
|
1028
|
+
// dead turn. The opened cells still have to close -- an in_progress web_search_call left
|
|
1029
|
+
// under a finished turn is the same dangling "Searching the web" spinner the failure path
|
|
1030
|
+
// above closes for. This also tightens the pre-existing non-mixed failed-leg path, which
|
|
1031
|
+
// used to drop the searches and leave the cell open.
|
|
1032
|
+
for (const call of decision.searches) {
|
|
1033
|
+
yield* emit(state.searchEndFrames(call, [], {
|
|
1034
|
+
text: "",
|
|
1035
|
+
sources: [],
|
|
1036
|
+
error: "the upstream turn ended before the web search could run",
|
|
1037
|
+
}));
|
|
1038
|
+
}
|
|
1039
|
+
// Only an incomplete terminal hands the withheld call back; a failed one drops it.
|
|
1040
|
+
if (decision.releaseHeldCalls) yield* emit(state.flushHeldCalls());
|
|
1041
|
+
else state.dropHeldCalls();
|
|
1042
|
+
yield* emit(state.terminalFrames());
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
884
1046
|
const turns: { call: InterceptedSearchCall; output: string }[] = [];
|
|
885
1047
|
for (const call of decision.searches) {
|
|
886
1048
|
const queries = parseQueries(call.argumentsText);
|
|
@@ -907,6 +1069,17 @@ async function* bridgeStreamBlocks(
|
|
|
907
1069
|
});
|
|
908
1070
|
}
|
|
909
1071
|
|
|
1072
|
+
if (decision.kind === "endAfterSearch") {
|
|
1073
|
+
// A mixed leg ends here rather than continuing upstream: the client's own call is
|
|
1074
|
+
// unanswered, so the conversation owes the CLIENT a turn, not the gateway. The searches
|
|
1075
|
+
// completed their hosted cells above; now the held calls go back for Codex to run and
|
|
1076
|
+
// the leg's terminal closes the turn. No continuation is sent and no function_call_output
|
|
1077
|
+
// is fabricated for a call the bridge cannot execute.
|
|
1078
|
+
yield* emit(state.flushHeldCalls());
|
|
1079
|
+
yield* emit(state.terminalFrames());
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
910
1083
|
const nextBody = appendBridgeSearchTurn(requestBody, turns);
|
|
911
1084
|
if (nextBody === undefined) {
|
|
912
1085
|
yield* emit(state.failureFrames(
|
|
@@ -9,6 +9,28 @@ import { resolveSidecarAuth } from "../sidecar/auth";
|
|
|
9
9
|
import { getAccountSet } from "../oauth/store";
|
|
10
10
|
import type { XaiSearchOptions } from "./xai-executor";
|
|
11
11
|
|
|
12
|
+
/** Every backend id the config union admits. New ids are explicit-only and inert until their executor ships. */
|
|
13
|
+
export type WebSearchBackendId = "openai" | "anthropic" | "xai" | "gemini" | "exa";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Precedence: explicit config wins; unset defaults to "openai" (ChatGPT forward path). The
|
|
17
|
+
* anthropic backend (web_search_20250305) is only used when explicitly configured — auto-selecting
|
|
18
|
+
* it from credential availability caused the sidecar to send incompatible models (e.g. gpt-5.6-luna)
|
|
19
|
+
* to the Anthropic API.
|
|
20
|
+
* The 2188 follow-up ids (xai/gemini/exa) resolve to themselves the same explicit-only way; their
|
|
21
|
+
* planWebSearch arms stay fail-closed until each executor layer lands.
|
|
22
|
+
*
|
|
23
|
+
* Lives here rather than in `index.ts` for the reason at the top of this file: the passthrough
|
|
24
|
+
* bridge has to answer "which backend was this global sidecar block configured for?" without
|
|
25
|
+
* value-importing the barrel.
|
|
26
|
+
*/
|
|
27
|
+
export function resolveSidecarBackend(
|
|
28
|
+
explicit: WebSearchBackendId | undefined,
|
|
29
|
+
): WebSearchBackendId {
|
|
30
|
+
if (explicit === "anthropic" || explicit === "xai" || explicit === "gemini" || explicit === "exa") return explicit;
|
|
31
|
+
return "openai";
|
|
32
|
+
}
|
|
33
|
+
|
|
12
34
|
/** A configured anthropic-adapter OAuth provider whose ACTIVE stored account is usable (not needs-reauth). */
|
|
13
35
|
export interface AnthropicSidecarProvider {
|
|
14
36
|
providerName: string;
|