@bitkyc08/opencodex 2.57.0 → 2.59.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/README.md +28 -10
- package/gui/dist/assets/index-C5IebErG.js +136 -0
- package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +2 -2
- package/src/adapters/base.ts +11 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +5 -4
- package/src/adapters/command-code.ts +13 -4
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +65 -1
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-events.ts +110 -11
- package/src/adapters/cursor/protobuf-request.ts +27 -6
- package/src/adapters/cursor/request-builder.ts +14 -3
- package/src/adapters/cursor/text-toolcall.ts +230 -0
- package/src/adapters/cursor/thread-continuity.ts +141 -0
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/types.ts +5 -0
- package/src/adapters/cursor.ts +97 -6
- package/src/adapters/devin/cloud-direct/chat.ts +11 -2
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
- package/src/adapters/devin.ts +75 -13
- package/src/adapters/google-antigravity-wire.ts +29 -2
- package/src/adapters/google-http.ts +45 -13
- package/src/adapters/google.ts +23 -4
- package/src/adapters/mimo-free.ts +32 -17
- package/src/adapters/ollama-native.ts +42 -8
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat.ts +5 -10
- package/src/adapters/openai-responses/passthrough.ts +40 -5
- package/src/adapters/openai-responses/request-strips.ts +43 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/physical-send.ts +50 -0
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +2 -2
- package/src/bridge/sse.ts +166 -25
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +46 -5
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +22 -2
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/config-command.ts +35 -18
- package/src/cli/dispatch.ts +71 -4
- package/src/cli/doctor.ts +197 -2
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +132 -22
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +72 -1
- package/src/cli/uninstall-client-state.ts +12 -0
- package/src/client/machine-api.ts +4 -3
- package/src/client/machine-listener.ts +14 -1
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +81 -5
- package/src/codex/auth-api/pool-quota-probe.ts +14 -3
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +58 -20
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +1 -0
- package/src/codex/catalog/model-hints.ts +37 -5
- package/src/codex/catalog/parsing.ts +83 -5
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +19 -0
- package/src/codex/catalog/routed-gather.ts +42 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desktop-switches.ts +145 -0
- package/src/codex/history-job.ts +5 -1
- package/src/codex/history-provider.ts +37 -5
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/history-worker.ts +14 -1
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject/remove.ts +145 -7
- package/src/codex/inject/restore.ts +204 -32
- package/src/codex/inject.ts +6 -9
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +40 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-entitlements.ts +60 -1
- package/src/codex/native-profile-startup.ts +64 -20
- package/src/codex/observed-model-denials.ts +137 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +282 -12
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +13 -15
- package/src/codex/subagent-model-fallback.ts +94 -0
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +122 -5
- package/src/config/atomic-write.ts +83 -8
- package/src/config/diagnostics.ts +21 -0
- package/src/config/load-degrade.ts +15 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +23 -5
- package/src/config/schema/config-schema.ts +23 -0
- package/src/config/schema/leaf-validators.ts +65 -17
- package/src/generated/compatibility-version.json +337 -201
- package/src/generated/model-metadata.ts +1 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/bounded-subprocess.ts +62 -10
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +3 -15
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/provider-outbound.ts +3 -3
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +631 -0
- package/src/lib/spend-reservation-ledger.ts +115 -9
- package/src/lib/windows-secret-acl.ts +151 -15
- package/src/lib/windows-user-principal.ts +5 -1
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/generic-account-failover.ts +40 -27
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/derive.ts +6 -0
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +261 -7
- package/src/providers/model-discovery.ts +19 -7
- package/src/providers/model-rename-migration.ts +1 -0
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/registry/entries-core.ts +11 -0
- package/src/providers/registry/entries-extended.ts +146 -28
- package/src/providers/registry/model-seeds.ts +136 -29
- package/src/providers/registry/types.ts +9 -0
- package/src/responses/apply-patch-envelope.ts +44 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/schema.ts +9 -2
- package/src/responses/spill-store.ts +17 -0
- package/src/responses/state/body-policy.ts +25 -0
- package/src/responses/state/spill-queue.ts +8 -6
- package/src/responses/state.ts +3 -22
- package/src/router.ts +4 -0
- package/src/server/auth-cors.ts +27 -0
- package/src/server/chat-completions.ts +9 -4
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +24 -2
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/websocket-handler.ts +48 -1
- package/src/server/index.ts +15 -19
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/config-routes.ts +57 -10
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/models-capabilities.ts +24 -3
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/relay.ts +17 -24
- package/src/server/request-log.ts +25 -1
- package/src/server/responses/adapter-continuation.ts +71 -27
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +52 -24
- package/src/server/responses/codex-ws-exchange.ts +65 -4
- package/src/server/responses/combo-stream-preflight.ts +68 -5
- package/src/server/responses/compact.ts +60 -11
- package/src/server/responses/core-codex-account.ts +83 -22
- package/src/server/responses/core-combo.ts +26 -0
- package/src/server/responses/core-normalize.ts +12 -5
- package/src/server/responses/core-options.ts +3 -0
- package/src/server/responses/fetch-helpers.ts +72 -3
- package/src/server/responses/native-injection-protocol.ts +42 -0
- package/src/server/responses/native-injection-replay.ts +105 -0
- package/src/server/responses/native-injection.ts +242 -0
- package/src/server/responses/native-response-control.ts +56 -0
- package/src/server/responses/native-response-json.ts +14 -0
- package/src/server/responses/native-response-output.ts +37 -0
- package/src/server/responses/native-steering-log.ts +44 -0
- package/src/server/responses/native-steering-policy.ts +49 -0
- package/src/server/responses/native-steering-replay.ts +126 -0
- package/src/server/responses/native-steering-settings.ts +76 -0
- package/src/server/responses/native-steering.ts +400 -0
- package/src/server/responses/native-tool-results.ts +130 -0
- package/src/server/responses/passthrough-delivery.ts +21 -1
- package/src/server/responses/passthrough-dispatch.ts +146 -49
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/request-prepare.ts +70 -0
- package/src/server/responses/request-send-budget.ts +84 -7
- package/src/server/responses/request-sidecar-auth.ts +16 -8
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +13 -10
- package/src/server/responses/run-turn-execution.ts +20 -5
- package/src/server/responses/sidecar-execution.ts +2 -0
- package/src/server/responses/ws-upstream.ts +23 -2
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +36 -9
- package/src/server/stop-teardown.ts +8 -1
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/server/ws-bridge.ts +16 -1
- package/src/service/cli.ts +29 -7
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +104 -3
- package/src/types/provider.ts +32 -0
- package/src/types/request.ts +7 -1
- package/src/types/wire.ts +9 -1
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-Cz7CLdif.js +0 -128
package/src/adapters/google.ts
CHANGED
|
@@ -57,7 +57,6 @@ const GOOGLE_BREVITY_INSTRUCTION = [
|
|
|
57
57
|
|
|
58
58
|
const ANTIGRAVITY_REJECTED_CLAUDE_SDK_PARAGRAPH =
|
|
59
59
|
"You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
60
|
-
|
|
61
60
|
/**
|
|
62
61
|
* CCA Flash generations that reject the Claude-Agent identity paragraph.
|
|
63
62
|
*
|
|
@@ -102,6 +101,20 @@ function stripAntigravityRejectedClaudeSdkParagraph(systemText: string): string
|
|
|
102
101
|
.join("\n\n");
|
|
103
102
|
}
|
|
104
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Strips Claude Code CLI's internal billing header (`x-anthropic-billing-header: ...`)
|
|
106
|
+
* at the start of the system prompt, because Cloud Code Assist / Google Antigravity inspects
|
|
107
|
+
* `systemInstruction` and rejects requests containing Anthropic billing metadata with
|
|
108
|
+
* HTTP 429 RESOURCE_EXHAUSTED.
|
|
109
|
+
*
|
|
110
|
+
* Matching is restricted to the prompt start (`^` without the `/m` multiline flag) so that
|
|
111
|
+
* user prompts discussing billing headers in intermediate lines are never modified, and
|
|
112
|
+
* prompts without a billing header preserve their leading whitespace untouched.
|
|
113
|
+
*/
|
|
114
|
+
function stripAntigravityBillingHeader(systemText: string): string {
|
|
115
|
+
return systemText.replace(/^x-anthropic-billing-header:[^\n]*\n*/, "");
|
|
116
|
+
}
|
|
117
|
+
|
|
105
118
|
/**
|
|
106
119
|
* Documented output ceiling for a Google-surface model, or `undefined` when the id is not
|
|
107
120
|
* recognized.
|
|
@@ -276,6 +289,7 @@ function messagesToGeminiFormat(
|
|
|
276
289
|
parsed: OcxParsedRequest,
|
|
277
290
|
identityModelId: string,
|
|
278
291
|
stripRejectedClaudeSdkParagraph = false,
|
|
292
|
+
isCloudCodeAssist = false,
|
|
279
293
|
): { systemInstruction?: unknown; contents: unknown[]; replayedCallIds: string[] } {
|
|
280
294
|
// Neutralize Codex's GPT-5 identity line (Gemini/Antigravity share this path) so a routed model
|
|
281
295
|
// never misreports as GPT-5/OpenAI, and never leaks the proxy identity upstream.
|
|
@@ -285,9 +299,12 @@ function messagesToGeminiFormat(
|
|
|
285
299
|
...(toolCatalogNudge ? [toolCatalogNudge] : []),
|
|
286
300
|
GOOGLE_BREVITY_INSTRUCTION,
|
|
287
301
|
].join("\n\n"), identityModelId);
|
|
288
|
-
|
|
289
|
-
?
|
|
302
|
+
let systemText = isCloudCodeAssist
|
|
303
|
+
? stripAntigravityBillingHeader(identifiedSystemText)
|
|
290
304
|
: identifiedSystemText;
|
|
305
|
+
if (stripRejectedClaudeSdkParagraph) {
|
|
306
|
+
systemText = stripAntigravityRejectedClaudeSdkParagraph(systemText);
|
|
307
|
+
}
|
|
291
308
|
const systemInstruction = { parts: [{ text: systemText }] };
|
|
292
309
|
|
|
293
310
|
const contents: unknown[] = [];
|
|
@@ -833,12 +850,14 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
833
850
|
&& /^gemini-/.test(routedModelId) && !isImageCapableModel(parsed.modelId);
|
|
834
851
|
// AI Studio's `-tiered` spelling is wire-only; CCA aliases may migrate to another generation.
|
|
835
852
|
const identityModelId = provider.googleMode === "cloud-code-assist" ? routedModelId : parsed.modelId;
|
|
836
|
-
const
|
|
853
|
+
const isCloudCodeAssist = provider.googleMode === "cloud-code-assist";
|
|
854
|
+
const stripRejectedClaudeSdkParagraph = isCloudCodeAssist
|
|
837
855
|
&& rejectsClaudeSdkParagraph(parsed.modelId, routedModelId);
|
|
838
856
|
const { systemInstruction, contents, replayedCallIds } = messagesToGeminiFormat(
|
|
839
857
|
parsed,
|
|
840
858
|
identityModelId,
|
|
841
859
|
stripRejectedClaudeSdkParagraph,
|
|
860
|
+
isCloudCodeAssist,
|
|
842
861
|
);
|
|
843
862
|
lastInjectedCallIds = [...replayedCallIds];
|
|
844
863
|
lastReasoningReplayScope = parsed._reasoningReplayScope;
|
|
@@ -6,6 +6,8 @@ import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
|
6
6
|
import type { OcxProviderConfig, OcxParsedRequest } from "../types";
|
|
7
7
|
import { createOpenAIChatAdapter } from "./openai-chat";
|
|
8
8
|
import type { ProviderAdapter, AdapterRequest, IncomingMeta } from "./base";
|
|
9
|
+
import { createAdapterPhysicalSend } from "./physical-send";
|
|
10
|
+
import { SendBudgetExhaustedError } from "../lib/upstream-retry";
|
|
9
11
|
|
|
10
12
|
const BOOTSTRAP_URL = "https://api.xiaomimimo.com/api/free-ai/bootstrap";
|
|
11
13
|
export const MIMO_CHAT_URL = "https://api.xiaomimimo.com/api/free-ai/openai/chat";
|
|
@@ -248,33 +250,46 @@ export function createMimoFreeAdapter(provider: OcxProviderConfig): ProviderAdap
|
|
|
248
250
|
},
|
|
249
251
|
|
|
250
252
|
async fetchResponse(request: AdapterRequest, ctx): Promise<Response> {
|
|
251
|
-
const
|
|
253
|
+
const send = createAdapterPhysicalSend(ctx);
|
|
254
|
+
const response = await send({ url: request.url, dispatch: executor => executor(request.url, {
|
|
252
255
|
method: request.method,
|
|
253
256
|
redirect: "manual",
|
|
254
257
|
headers: request.headers as Record<string, string>,
|
|
255
258
|
body: request.body,
|
|
256
259
|
signal: ctx?.abortSignal,
|
|
257
|
-
});
|
|
260
|
+
}) });
|
|
258
261
|
|
|
259
262
|
// Retry predicate: 401 (expired/invalid JWT) retries ONCE with a fresh token.
|
|
260
263
|
// 403 is NOT retried — Xiaomi uses it for anti-abuse "Illegal access" and there is
|
|
261
264
|
// no documented token-expiry signature that would mark a 403 as retryable.
|
|
262
265
|
if (response.status === 401) {
|
|
263
|
-
|
|
264
|
-
try {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
266
|
+
let retryHeaders = request.headers;
|
|
267
|
+
try {
|
|
268
|
+
return await send({ url: request.url, sendClass: "auth-recovery", recovery: "oauth-401",
|
|
269
|
+
beforeDispatch: async () => {
|
|
270
|
+
// Drain the first response body and refresh the JWT only after admission: a
|
|
271
|
+
// refused replay still returns THIS response to the caller, body intact.
|
|
272
|
+
// Draining comes first within the block because getMimoJwt issues its own
|
|
273
|
+
// network call and may throw, and the 401 body would then never be released.
|
|
274
|
+
try { void response.body?.cancel().catch(() => {}); } catch { /* already consumed */ }
|
|
275
|
+
resetMimoJwtCache();
|
|
276
|
+
const freshJwt = await getMimoJwt(ctx?.abortSignal);
|
|
277
|
+
retryHeaders = {
|
|
278
|
+
...(request.headers as Record<string, string>),
|
|
279
|
+
"Authorization": `Bearer ${freshJwt}`,
|
|
280
|
+
};
|
|
281
|
+
},
|
|
282
|
+
dispatch: executor => executor(request.url, {
|
|
283
|
+
method: request.method,
|
|
284
|
+
redirect: "manual",
|
|
285
|
+
headers: retryHeaders,
|
|
286
|
+
body: request.body,
|
|
287
|
+
signal: ctx?.abortSignal,
|
|
288
|
+
}) });
|
|
289
|
+
} catch (error) {
|
|
290
|
+
if (error instanceof SendBudgetExhaustedError) return response;
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
278
293
|
}
|
|
279
294
|
|
|
280
295
|
return response;
|
|
@@ -306,17 +306,37 @@ function buildNativeMessages(
|
|
|
306
306
|
// owned by this adapter/request lifecycle rather than process-global state.
|
|
307
307
|
reservedToolCallIds.clear();
|
|
308
308
|
let pending: PendingToolBatch | undefined;
|
|
309
|
+
// Codex records mid-turn injections (a PostToolUse hook verdict, a context notice) between an
|
|
310
|
+
// assistant tool call and that call's own tool result. Native Ollama needs the call and its
|
|
311
|
+
// results adjacent, so those conversational messages wait here instead of closing the batch
|
|
312
|
+
// early. The openai-chat adapter defers them the same way; refusing the replay killed the turn.
|
|
313
|
+
let deferred: OllamaNativeMessage[] = [];
|
|
314
|
+
|
|
315
|
+
const releaseDeferred = (): void => {
|
|
316
|
+
if (deferred.length === 0) return;
|
|
317
|
+
messages.push(...deferred);
|
|
318
|
+
deferred = [];
|
|
319
|
+
};
|
|
309
320
|
|
|
310
321
|
const flushPending = (): void => {
|
|
311
322
|
if (!pending) return;
|
|
312
323
|
for (const call of pending.calls) {
|
|
313
324
|
if (!call.result) {
|
|
314
|
-
|
|
325
|
+
// No result exists anywhere in the replayed history: the turn was interrupted, or the
|
|
326
|
+
// result never reached it. State exactly that instead of inventing an outcome, and keep
|
|
327
|
+
// the conversation replayable.
|
|
328
|
+
messages.push({
|
|
329
|
+
role: "tool",
|
|
330
|
+
tool_call_id: call.id,
|
|
331
|
+
tool_name: call.wireName,
|
|
332
|
+
// Same marker text as the chat adapter (openai-chat/messages.ts), so both adapters read
|
|
333
|
+
// the same in an operator's log. The name is this wire's flattened tool name, which is
|
|
334
|
+
// what the assistant turn above it carries.
|
|
335
|
+
content: `[ocx] no tool result was recorded for "${call.wireName}"; execution status unknown — do not treat this as success, failure, or user-provided input.`,
|
|
336
|
+
});
|
|
337
|
+
continue;
|
|
315
338
|
}
|
|
316
|
-
|
|
317
|
-
for (const call of pending.calls) {
|
|
318
|
-
const result = call.result!;
|
|
319
|
-
const translated = contentToNative(result.content, "tool result");
|
|
339
|
+
const translated = contentToNative(call.result.content, "tool result");
|
|
320
340
|
messages.push({
|
|
321
341
|
role: "tool",
|
|
322
342
|
tool_call_id: call.id,
|
|
@@ -326,6 +346,7 @@ function buildNativeMessages(
|
|
|
326
346
|
});
|
|
327
347
|
}
|
|
328
348
|
pending = undefined;
|
|
349
|
+
releaseDeferred();
|
|
329
350
|
};
|
|
330
351
|
|
|
331
352
|
for (const message of parsed.context.messages) {
|
|
@@ -347,9 +368,22 @@ function buildNativeMessages(
|
|
|
347
368
|
continue;
|
|
348
369
|
}
|
|
349
370
|
|
|
350
|
-
// Native Ollama requires the whole assistant tool-call turn followed by its tool results.
|
|
351
|
-
//
|
|
352
|
-
|
|
371
|
+
// Native Ollama requires the whole assistant tool-call turn followed by its tool results. A
|
|
372
|
+
// conversational message that arrives while the batch is still open is held aside instead of
|
|
373
|
+
// closing it, so the call keeps its results adjacent; it is released right after the batch
|
|
374
|
+
// flushes. Anything else (a new assistant turn) settles the batch first.
|
|
375
|
+
if (pending) {
|
|
376
|
+
if (message.role === "user" || message.role === "developer") {
|
|
377
|
+
const translated = message.role === "user"
|
|
378
|
+
? contentToNative(message.content, "user")
|
|
379
|
+
: contentToNative(message.content, "developer", false);
|
|
380
|
+
deferred.push(message.role === "user"
|
|
381
|
+
? { role: "user", content: translated.content, ...(translated.images ? { images: translated.images } : {}) }
|
|
382
|
+
: { role: "system", content: translated.content });
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
flushPending();
|
|
386
|
+
}
|
|
353
387
|
|
|
354
388
|
switch (message.role) {
|
|
355
389
|
case "user": {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { diagnoseInvalidToolCalls, isRecord, type InvalidToolCallDiagnostic } from "./tool-call-validation";
|
|
2
|
+
import { TranslatorBudgetExceededError, type TranslatorBudget } from "../../lib/translator-budget";
|
|
2
3
|
import type { AdapterEvent, OcxUsage } from "../../types";
|
|
3
4
|
|
|
4
5
|
export function stopReasonFor(finishReason: unknown): "max_tokens" | "content_filter" | undefined {
|
|
@@ -22,6 +23,9 @@ export interface ReasoningDetailSegment {
|
|
|
22
23
|
text: string;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
const MAX_REASONING_DETAIL_KEY_BYTES = 1024;
|
|
27
|
+
const MAX_REASONING_DETAIL_SEGMENTS = 1024;
|
|
28
|
+
|
|
25
29
|
/**
|
|
26
30
|
* Structured `reasoning_details` array (MiniMax M-series with `reasoning_split`).
|
|
27
31
|
* Each segment's key scopes cumulative-snapshot tracking: upstream repeats the
|
|
@@ -35,6 +39,11 @@ export function reasoningDetailSegmentsFrom(record: Record<string, unknown>): Re
|
|
|
35
39
|
const item: unknown = raw[i];
|
|
36
40
|
if (!isRecord(item)) continue;
|
|
37
41
|
if (typeof item.text !== "string" || item.text.length === 0) continue;
|
|
42
|
+
// Parsing retains nothing, so it rejects nothing. The non-streaming
|
|
43
|
+
// parseResponse path shares this function and reads only `text`; failing a
|
|
44
|
+
// whole valid response there because an opaque upstream id is long would be
|
|
45
|
+
// a new rejection unrelated to the retention bound. The key cap lives with
|
|
46
|
+
// the map that holds the key; see the tracker below.
|
|
38
47
|
const key = typeof item.id === "string" && item.id.length > 0
|
|
39
48
|
? `id:${item.id}`
|
|
40
49
|
: typeof item.index === "number"
|
|
@@ -45,6 +54,58 @@ export function reasoningDetailSegmentsFrom(record: Record<string, unknown>): Re
|
|
|
45
54
|
return segments;
|
|
46
55
|
}
|
|
47
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Per-stream cumulative-snapshot store for structured `reasoning_details`. Each stream chunk
|
|
59
|
+
* repeats a detail's full text-so-far, so deltas are derived by prefix-diffing per segment key;
|
|
60
|
+
* a piece that does not extend the previous snapshot is appended whole, which keeps incremental
|
|
61
|
+
* senders parseable on the same path. Retained key+text bytes are charged to the translator
|
|
62
|
+
* budget under the `reasoning` kind, and both the key length and the segment count are capped
|
|
63
|
+
* here, where the map actually retains them, so a hostile upstream cannot grow it without bound.
|
|
64
|
+
*/
|
|
65
|
+
export function createReasoningDetailSnapshotTracker(budget: TranslatorBudget): {
|
|
66
|
+
ingest(segment: ReasoningDetailSegment): string | null;
|
|
67
|
+
release(): void;
|
|
68
|
+
} {
|
|
69
|
+
const snapshots = new Map<string, string>();
|
|
70
|
+
const encoder = new TextEncoder();
|
|
71
|
+
let retainedBytes = 0;
|
|
72
|
+
return {
|
|
73
|
+
ingest(segment) {
|
|
74
|
+
const existing = snapshots.get(segment.key);
|
|
75
|
+
if (existing === undefined && encoder.encode(segment.key).byteLength > MAX_REASONING_DETAIL_KEY_BYTES) {
|
|
76
|
+
throw new TranslatorBudgetExceededError("reasoning", MAX_REASONING_DETAIL_KEY_BYTES);
|
|
77
|
+
}
|
|
78
|
+
if (existing === undefined && snapshots.size >= MAX_REASONING_DETAIL_SEGMENTS) {
|
|
79
|
+
throw new TranslatorBudgetExceededError("reasoning", MAX_REASONING_DETAIL_SEGMENTS);
|
|
80
|
+
}
|
|
81
|
+
const prev = existing ?? "";
|
|
82
|
+
if (segment.text === prev) return null;
|
|
83
|
+
const extendsPrev = segment.text.startsWith(prev);
|
|
84
|
+
const next = extendsPrev ? segment.text : prev + segment.text;
|
|
85
|
+
const previousBytes = existing === undefined
|
|
86
|
+
? 0
|
|
87
|
+
: encoder.encode(segment.key).byteLength + encoder.encode(prev).byteLength;
|
|
88
|
+
const nextBytes = encoder.encode(segment.key).byteLength + encoder.encode(next).byteLength;
|
|
89
|
+
const reservation = budget.reserveTransient(nextBytes, { kind: "reasoning" });
|
|
90
|
+
try {
|
|
91
|
+
snapshots.set(segment.key, next);
|
|
92
|
+
reservation.commitRetained();
|
|
93
|
+
budget.releaseRetained(previousBytes, { kind: "reasoning" });
|
|
94
|
+
retainedBytes += nextBytes - previousBytes;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
reservation.release();
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
return extendsPrev ? segment.text.slice(prev.length) : segment.text;
|
|
100
|
+
},
|
|
101
|
+
release() {
|
|
102
|
+
budget.releaseRetained(retainedBytes, { kind: "reasoning" });
|
|
103
|
+
retainedBytes = 0;
|
|
104
|
+
snapshots.clear();
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
48
109
|
/** Single-segment `reasoning_details` entry for replaying preserved reasoning (MiniMax wire shape). */
|
|
49
110
|
export function reasoningDetailSegmentForWire(text: string): Record<string, unknown> {
|
|
50
111
|
return { type: "reasoning.text", id: "reasoning-text-1", format: "MiniMax-response-v1", index: 0, text };
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
type InvalidToolCallDiagnostic,
|
|
24
24
|
} from "./openai-chat/tool-call-validation";
|
|
25
25
|
import {
|
|
26
|
+
createReasoningDetailSnapshotTracker,
|
|
26
27
|
invalidChoicesEvent,
|
|
27
28
|
invalidToolCallsEvent,
|
|
28
29
|
reasoningDetailSegmentsFrom,
|
|
@@ -385,7 +386,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
385
386
|
// full text-so-far, so deltas are derived by prefix-diffing per segment key.
|
|
386
387
|
// A piece that does not extend the previous snapshot is appended whole, which
|
|
387
388
|
// keeps incremental senders parseable on the same path.
|
|
388
|
-
const
|
|
389
|
+
const reasoningDetailTracker = createReasoningDetailSnapshotTracker(budget);
|
|
389
390
|
// Gate on the routed model, not list length: a mixed openai-chat provider
|
|
390
391
|
// can list MiniMax ids without putting every sibling on MiniMax semantics.
|
|
391
392
|
const reasoningDetailsOptIn = modelInList(provider.reasoningDetailsModels, lastRequestedModelId ?? "");
|
|
@@ -448,15 +449,8 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
448
449
|
const detailSegments = reasoningDetailsOptIn ? reasoningDetailSegmentsFrom(delta) : [];
|
|
449
450
|
if (detailSegments.length > 0) {
|
|
450
451
|
for (const segment of detailSegments) {
|
|
451
|
-
const
|
|
452
|
-
if (
|
|
453
|
-
if (segment.text.startsWith(prev)) {
|
|
454
|
-
reasoningDetailSnapshots.set(segment.key, segment.text);
|
|
455
|
-
yield { type: "reasoning_raw_delta", text: segment.text.slice(prev.length) };
|
|
456
|
-
} else {
|
|
457
|
-
reasoningDetailSnapshots.set(segment.key, prev + segment.text);
|
|
458
|
-
yield { type: "reasoning_raw_delta", text: segment.text };
|
|
459
|
-
}
|
|
452
|
+
const reasoningDelta = reasoningDetailTracker.ingest(segment);
|
|
453
|
+
if (reasoningDelta !== null) yield { type: "reasoning_raw_delta", text: reasoningDelta };
|
|
460
454
|
}
|
|
461
455
|
} else {
|
|
462
456
|
const reasoningText = reasoningTextFrom(delta);
|
|
@@ -692,6 +686,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
692
686
|
throw error;
|
|
693
687
|
} finally {
|
|
694
688
|
budget.releaseRetained(bufferBytes, { kind: "live_transient" });
|
|
689
|
+
reasoningDetailTracker.release();
|
|
695
690
|
closeToolCalls();
|
|
696
691
|
reader.releaseLock();
|
|
697
692
|
}
|
|
@@ -32,11 +32,12 @@ import {
|
|
|
32
32
|
createAdapterTierMetadata,
|
|
33
33
|
} from "../../providers/fastwire";
|
|
34
34
|
import { mapRoutedResponsesReasoningEffort, normalizeConfiguredReasoningSummaryDelivery, sanitizeReasoningInputContent, stripDisabledReasoningSummaries, stripDisabledVerbosity, stripUnsupportedReasoningSummaryDelivery } from "./reasoning";
|
|
35
|
-
import { scrubOcxCompactionItems, stripCanonicalOnlyToolFields, stripInternalChatMessageMetadataPassthrough, stripInvalidItemIds, stripItemIdsWhenUnstored } from "./request-strips";
|
|
35
|
+
import { scrubOcxCompactionItems, stripCanonicalOnlyToolFields, stripCanonicalOnlyTopLevelFields, stripInternalChatMessageMetadataPassthrough, stripInvalidItemIds, stripItemIdsWhenUnstored } from "./request-strips";
|
|
36
36
|
import { stripCanonicalForwardPromptCacheOptions, stripDeprecatedPromptCacheRetention } from "./prompt-cache";
|
|
37
37
|
import { isPlainObject } from "./internal";
|
|
38
38
|
import { normalizeToolSchemas, promoteClientLoadedTools, stripUnsupportedHostedTools } from "./tool-schema";
|
|
39
|
-
import { annotateEmptyResponsesToolOutputs, backfillWebSearchQueries, normalizeResponsesToolResultAdjacency, repairOrphanedInputItems, repairOversizedReplayCallIds, repairUnidentifiedToolOutputItems } from "./tool-output-recovery";
|
|
39
|
+
import { annotateEmptyResponsesToolOutputs, backfillWebSearchQueries, normalizeResponsesToolResultAdjacency, repairOrphanedInputItems, repairOversizedReplayCallIds, repairUnidentifiedToolOutputItems, restoreBridgedWebSearchCalls } from "./tool-output-recovery";
|
|
40
|
+
import { bridgeSearchReplayScope } from "../../responses/bridge-search-replay-cache";
|
|
40
41
|
import { applyTierDecisionToResponsesBody, normalizeCanonicalForwardContinuationEnvelope, normalizeCanonicalForwardPromptEnvelope, stripCanonicalForwardSamplingParams, stripPreviousResponseId, stripStatefulResponsesParams, stripUnsupportedForwardParams } from "./canonical-forward";
|
|
41
42
|
import { normalizeImageGenClientTools, preferConfiguredHostedTools } from "./image-gen";
|
|
42
43
|
import { stripMuseSparkUnsupportedWebSearchFields, stripOpenAiOnlyWebSearchFields } from "./web-search";
|
|
@@ -270,19 +271,31 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
270
271
|
// tier write so a force-fast/default decision can never mutate parsed._rawBody.
|
|
271
272
|
outBody = applyTierDecisionToResponsesBody(outBody, parsed.options?.tierDecision);
|
|
272
273
|
const stateless = provider.statelessResponses === true;
|
|
274
|
+
const adjacentToolResults = provider.requiresAdjacentResponsesToolResults === true;
|
|
275
|
+
// Adjacency reorders items the upstream would accept in some order. Pairing synthesizes an
|
|
276
|
+
// item the client never sent, which is a larger claim about the conversation, so it is its
|
|
277
|
+
// own capability: Kimi carries the adjacency flag but accepts a dangling call (#4726) and
|
|
278
|
+
// must not start receiving placeholders it never needed.
|
|
279
|
+
const pairedToolResults = provider.requiresPairedResponsesToolResults === true;
|
|
273
280
|
if (stateless) outBody = stripStatefulResponsesParams(outBody);
|
|
274
281
|
// A replay miss can leave a function_call_output whose paired function_call sat
|
|
275
282
|
// in the prefix that was never expanded. A stateless upstream cannot resolve the
|
|
276
283
|
// pair from its own storage either, so it needs the same repair the forward
|
|
277
284
|
// backend gets — dropping previous_response_id is not much use if the body that
|
|
278
285
|
// reaches the wire is unparseable.
|
|
286
|
+
// A parser can also 400 on a function_call with no matching output at all. DeepSeek gets
|
|
287
|
+
// that repair through statelessResponses. xAI cannot be marked stateless: its Responses API
|
|
288
|
+
// stores conversations for 30 days and documents previous_response_id. So it carries the
|
|
289
|
+
// pairing capability instead, which reuses the orphan-call placeholder without touching
|
|
290
|
+
// store or previous_response_id.
|
|
279
291
|
if (provider.annotateEmptyToolOutputs === true) {
|
|
280
292
|
outBody = annotateEmptyResponsesToolOutputs(outBody, true);
|
|
281
293
|
}
|
|
282
|
-
|
|
283
|
-
|
|
294
|
+
const synthesizeMissingCallOutputs = !forward && (stateless || pairedToolResults);
|
|
295
|
+
if (forward || stateless || pairedToolResults) {
|
|
296
|
+
outBody = repairOrphanedInputItems(outBody, unexpandedMiss, synthesizeMissingCallOutputs);
|
|
284
297
|
}
|
|
285
|
-
if (
|
|
298
|
+
if (adjacentToolResults) {
|
|
286
299
|
outBody = normalizeResponsesToolResultAdjacency(outBody);
|
|
287
300
|
}
|
|
288
301
|
if (forward) {
|
|
@@ -309,6 +322,14 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
309
322
|
outBody = repairOversizedReplayCallIds(outBody);
|
|
310
323
|
}
|
|
311
324
|
outBody = stripUnsupportedReasoningSummaryDelivery(outBody, parsed.modelId);
|
|
325
|
+
// #4587: on a bridged provider, hand the destination back the search call and result the
|
|
326
|
+
// proxy executed on its behalf, in place of the hosted cell the caller replays. Scoped to
|
|
327
|
+
// this destination and recorded by the bridge itself, so a provider without the opt-in
|
|
328
|
+
// computes no identity and keeps the body reference it already had. This runs before the
|
|
329
|
+
// query backfill below because a restored cell is no longer a web_search_call to repair.
|
|
330
|
+
if (provider.webSearchBridge?.enabled === true) {
|
|
331
|
+
outBody = restoreBridgedWebSearchCalls(outBody, bridgeSearchReplayScope(provider.baseUrl));
|
|
332
|
+
}
|
|
312
333
|
// Repair stored history from before the bridge emitted both keys, in either
|
|
313
334
|
// direction: a conversation that already recorded a web_search_call replays it
|
|
314
335
|
// every turn, and a strict parser rejects the whole request over the missing key —
|
|
@@ -316,6 +337,20 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
316
337
|
outBody = backfillWebSearchQueries(outBody);
|
|
317
338
|
if (!isCanonicalOpenAiForwardProvider(provider)) {
|
|
318
339
|
outBody = stripInternalChatMessageMetadataPassthrough(outBody);
|
|
340
|
+
// The same class of private field, one level up, but keyed on the DESTINATION rather than
|
|
341
|
+
// on the canonical surface alone. `src/server/responses/compact.ts` spreads the caller's
|
|
342
|
+
// raw body into the native `/responses/compact` request without passing through this
|
|
343
|
+
// adapter, and that endpoint is offered only to OpenAI-operated destinations
|
|
344
|
+
// (supportsNativeResponsesCompactEndpoint). Stripping on the canonical predicate here
|
|
345
|
+
// would make the two paths disagree for `openai-apikey`; stripping on the destination
|
|
346
|
+
// keeps every OpenAI-operated route byte-identical and removes the field exactly where it
|
|
347
|
+
// is known to break, which is a gateway this proxy does not operate.
|
|
348
|
+
//
|
|
349
|
+
// Placed before the routed compaction body is built and before serialization, so the HTTP,
|
|
350
|
+
// routed-compaction and WebSocket outbounds are all covered by this one call.
|
|
351
|
+
if (!isOpenAiOperatedResponsesDestination(provider)) {
|
|
352
|
+
outBody = stripCanonicalOnlyTopLevelFields(outBody);
|
|
353
|
+
}
|
|
319
354
|
outBody = promoteClientLoadedTools(outBody);
|
|
320
355
|
}
|
|
321
356
|
if (!isCanonicalOpenAiForwardProvider(provider)) {
|
|
@@ -120,6 +120,49 @@ export function stripInternalChatMessageMetadataPassthrough(body: unknown): unkn
|
|
|
120
120
|
return changed ? { ...body, input } : body;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* OpenAI-private TOP-LEVEL request keys, the sibling of `CANONICAL_ONLY_TOOL_FIELDS` one level up.
|
|
125
|
+
*
|
|
126
|
+
* Codex attaches these on the request body itself rather than on a tool or an input item, and gates
|
|
127
|
+
* them on its own auth rather than on the destination URL. Loopback injection keeps Codex pointed at
|
|
128
|
+
* its built-in `openai` provider, so the client still believes it is addressing the canonical
|
|
129
|
+
* ChatGPT backend and keeps the key no matter where this proxy routes the turn. A Responses gateway
|
|
130
|
+
* that validates its top-level schema then rejects the whole request before inference.
|
|
131
|
+
*
|
|
132
|
+
* Keep this a table, and keep it to keys a client is OBSERVED to send. It is not an unknown-field
|
|
133
|
+
* sanitizer: a top-level key nobody has traced to a client is forwarded untouched, because deleting
|
|
134
|
+
* it would silently drop a parameter some other caller means.
|
|
135
|
+
*/
|
|
136
|
+
const CANONICAL_ONLY_TOP_LEVEL_FIELDS: readonly string[] = [
|
|
137
|
+
// Cyber access program selector, new in Codex 0.155. codex-rs mints it from
|
|
138
|
+
// `cyber_access_program::for_auth`, which filters on ChatGPT auth alone and never on the
|
|
139
|
+
// destination base URL, and serializes it on the Responses request, the compaction input and the
|
|
140
|
+
// WebSocket `response.create` envelope. No public specification defines it, so a strict
|
|
141
|
+
// third-party gateway answers with an unknown-parameter error naming it, and every turn of that
|
|
142
|
+
// thread fails (#4853).
|
|
143
|
+
//
|
|
144
|
+
// `codex_output_schema` is deliberately NOT here. In codex-rs it is the `name` of the JSON-schema
|
|
145
|
+
// `text.format` object, not a top-level key, so listing it would delete a field this client never
|
|
146
|
+
// sends and discard it for any client that does send it meaningfully.
|
|
147
|
+
"access_programs",
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Remove the OpenAI-private top-level keys.
|
|
152
|
+
*
|
|
153
|
+
* The caller decides the boundary; see the call site in `passthrough.ts`, which applies this only
|
|
154
|
+
* to a destination OpenCodex does not operate. Returns the input unchanged when no listed key is
|
|
155
|
+
* present, so the common path allocates nothing and the caller-owned raw body is never mutated.
|
|
156
|
+
*/
|
|
157
|
+
export function stripCanonicalOnlyTopLevelFields(body: unknown): unknown {
|
|
158
|
+
if (!isPlainObject(body)) return body;
|
|
159
|
+
if (!CANONICAL_ONLY_TOP_LEVEL_FIELDS.some(field => Object.hasOwn(body, field))) return body;
|
|
160
|
+
|
|
161
|
+
const next = { ...body };
|
|
162
|
+
for (const field of CANONICAL_ONLY_TOP_LEVEL_FIELDS) delete next[field];
|
|
163
|
+
return next;
|
|
164
|
+
}
|
|
165
|
+
|
|
123
166
|
/**
|
|
124
167
|
* When `store` is false, the upstream API does not persist response items. Any item ID
|
|
125
168
|
* forwarded in `input` is then interpreted as a reference to a stored item that does not
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { EMPTY_TOOL_OUTPUT_ANNOTATION, isWhitespaceOnlyTextPartArray } from "../empty-tool-output-annotation";
|
|
3
3
|
import { isPlainObject } from "./internal";
|
|
4
|
+
import { peekBridgeSearchReplay } from "../../responses/bridge-search-replay-cache";
|
|
4
5
|
|
|
5
6
|
const MAX_RESPONSES_CALL_ID_LENGTH = 64;
|
|
6
7
|
|
|
@@ -265,6 +266,80 @@ export function backfillWebSearchQueries(body: unknown): unknown {
|
|
|
265
266
|
return changed ? { ...body, input } : body;
|
|
266
267
|
}
|
|
267
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Give a bridged destination back its own search call and result (issue #4587).
|
|
271
|
+
*
|
|
272
|
+
* When `providers.<name>.webSearchBridge` is armed, the proxy intercepts the destination's
|
|
273
|
+
* `function_call` named `web_search`, runs the search, and shows the CALLER a hosted
|
|
274
|
+
* `web_search_call` cell. The caller stores that cell and replays it on every later turn, so the
|
|
275
|
+
* destination receives an item type it never produced, carrying a query and sources but no result.
|
|
276
|
+
* It typically responds by searching again.
|
|
277
|
+
*
|
|
278
|
+
* This restores the exchange the destination actually had: the cell becomes the destination's own
|
|
279
|
+
* `function_call`, immediately followed by the `function_call_output` the bridge produced for
|
|
280
|
+
* it, in the cell's original position. It runs before the first leg of the next turn is
|
|
281
|
+
* dispatched, which is the only place it can run — by the time the bridge wraps a turn, that
|
|
282
|
+
* turn's first leg is already on the wire.
|
|
283
|
+
*
|
|
284
|
+
* Three things it deliberately does not do:
|
|
285
|
+
* - It never re-runs a search. A missing memo entry means the result is gone, and paying for a
|
|
286
|
+
* second search would answer the model with a different search than its history claims.
|
|
287
|
+
* - It never invents result text. A miss leaves the item exactly as the caller sent it, which is
|
|
288
|
+
* the behaviour every unbridged conversation already has.
|
|
289
|
+
* - It never restores a call id the body already carries. If the history somehow holds that
|
|
290
|
+
* `function_call` too, emitting a second one would be a duplicate the upstream must reject.
|
|
291
|
+
*
|
|
292
|
+
* Entries are scoped to the upstream destination, so a history replayed against a different
|
|
293
|
+
* provider cannot resurrect a call that provider never made. Callers pass `undefined` for any
|
|
294
|
+
* provider without the bridge armed, and the common path then returns the original reference.
|
|
295
|
+
*/
|
|
296
|
+
export function restoreBridgedWebSearchCalls(body: unknown, destinationScope: string | undefined): unknown {
|
|
297
|
+
if (destinationScope === undefined) return body;
|
|
298
|
+
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
|
|
299
|
+
const input = body.input;
|
|
300
|
+
|
|
301
|
+
// Cheap pre-check: nothing to do for a conversation that carries no hosted search cell at all,
|
|
302
|
+
// which is every turn before the model's first bridged search.
|
|
303
|
+
let hasCell = false;
|
|
304
|
+
for (const item of input) {
|
|
305
|
+
if (isPlainObject(item) && item.type === "web_search_call" && typeof item.id === "string") {
|
|
306
|
+
hasCell = true;
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (!hasCell) return body;
|
|
311
|
+
|
|
312
|
+
const occupiedCallIds = new Set<string>();
|
|
313
|
+
for (const item of input) {
|
|
314
|
+
if (isPlainObject(item) && typeof item.call_id === "string") occupiedCallIds.add(item.call_id);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let changed = false;
|
|
318
|
+
const restored: unknown[] = [];
|
|
319
|
+
for (const item of input) {
|
|
320
|
+
if (isPlainObject(item) && item.type === "web_search_call" && typeof item.id === "string") {
|
|
321
|
+
const memo = peekBridgeSearchReplay(destinationScope, item.id);
|
|
322
|
+
if (memo && !occupiedCallIds.has(memo.callId)) {
|
|
323
|
+
changed = true;
|
|
324
|
+
occupiedCallIds.add(memo.callId);
|
|
325
|
+
restored.push({
|
|
326
|
+
type: "function_call",
|
|
327
|
+
...(memo.sourceItemId ? { id: memo.sourceItemId } : {}),
|
|
328
|
+
call_id: memo.callId,
|
|
329
|
+
name: memo.name,
|
|
330
|
+
// The bridge records the complete arguments text from the call's own done frame; the
|
|
331
|
+
// empty-object fallback matches what a continuation leg would have sent.
|
|
332
|
+
arguments: memo.argumentsText.length > 0 ? memo.argumentsText : "{}",
|
|
333
|
+
});
|
|
334
|
+
restored.push({ type: "function_call_output", call_id: memo.callId, output: memo.output });
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
restored.push(item);
|
|
339
|
+
}
|
|
340
|
+
return changed ? { ...body, input: restored } : body;
|
|
341
|
+
}
|
|
342
|
+
|
|
268
343
|
export function repairOrphanedInputItems(body: unknown, dropReasoning: boolean, synthesizeMissingCallOutputs = false): unknown {
|
|
269
344
|
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
|
|
270
345
|
const input = body.input;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage, type TierDecision } from "../../types";
|
|
2
|
-
import { isHostedToolUnsupportedForModel } from "../../responses/hosted-tool-policy";
|
|
2
|
+
import { declaredUnsupportedHostedTools, isHostedToolUnsupportedForModel } from "../../responses/hosted-tool-policy";
|
|
3
3
|
import { debugProviderDiagnostic } from "../../lib/debug";
|
|
4
4
|
import { stripUnicodePropertyPatterns } from "../responses-tool-schema";
|
|
5
5
|
import {
|
|
@@ -225,17 +225,29 @@ export function promoteClientLoadedTools(body: unknown): unknown {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
/**
|
|
228
|
-
* Remove hosted tool entries the
|
|
229
|
-
* carries a tool the upstream
|
|
230
|
-
*
|
|
228
|
+
* Remove hosted tool entries the destination rejects, so the OAuth-passthrough body never
|
|
229
|
+
* carries a tool the upstream 400s on. Two sources of truth are consulted: the built-in
|
|
230
|
+
* table of known-broken native slugs and destinations, and the routed provider's own
|
|
231
|
+
* `unsupportedHostedTools` declaration. The declaration is what lets an OpenAI-compatible
|
|
232
|
+
* Responses gateway with a narrower capability set be described in config instead of
|
|
233
|
+
* requiring a hard-coded destination rule per vendor (#5002).
|
|
234
|
+
*
|
|
235
|
+
* No-op (returns the original reference) when nothing matches, keeping the common path
|
|
236
|
+
* allocation-free.
|
|
231
237
|
*/
|
|
232
|
-
export function stripUnsupportedHostedTools(
|
|
238
|
+
export function stripUnsupportedHostedTools(
|
|
239
|
+
body: unknown,
|
|
240
|
+
provider: Pick<OcxProviderConfig, "baseUrl" | "unsupportedHostedTools">,
|
|
241
|
+
): unknown {
|
|
233
242
|
if (!isPlainObject(body)) return body;
|
|
234
243
|
const model = typeof body.model === "string" ? body.model : "";
|
|
244
|
+
// Expanded once per request rather than per tool: the alias walk is the only
|
|
245
|
+
// non-lookup work in this filter.
|
|
246
|
+
const declaredUnsupported = declaredUnsupportedHostedTools(provider);
|
|
235
247
|
const filterTools = (tools: unknown[]): unknown[] => {
|
|
236
248
|
const filtered = tools.filter(t => {
|
|
237
249
|
const type = isPlainObject(t) && typeof t.type === "string" ? t.type : undefined;
|
|
238
|
-
return !type || !isHostedToolUnsupportedForModel(model, type, provider.baseUrl);
|
|
250
|
+
return !type || !isHostedToolUnsupportedForModel(model, type, provider.baseUrl, declaredUnsupported);
|
|
239
251
|
});
|
|
240
252
|
return filtered.length === tools.length ? tools : filtered;
|
|
241
253
|
};
|
|
@@ -274,7 +286,7 @@ export function stripUnsupportedHostedTools(body: unknown, provider: Pick<OcxPro
|
|
|
274
286
|
} else if (
|
|
275
287
|
isPlainObject(toolChoice)
|
|
276
288
|
&& typeof toolChoice.type === "string"
|
|
277
|
-
&& isHostedToolUnsupportedForModel(model, toolChoice.type, provider.baseUrl)
|
|
289
|
+
&& isHostedToolUnsupportedForModel(model, toolChoice.type, provider.baseUrl, declaredUnsupported)
|
|
278
290
|
) {
|
|
279
291
|
next = { ...next, tool_choice: "none" };
|
|
280
292
|
changed = true;
|