@bitkyc08/opencodex 2.58.0 → 2.60.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-BTuCbqQd.css +1 -0
- package/gui/dist/assets/index-DoBVdPHP.js +134 -0
- 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 +4 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +4 -1
- package/src/adapters/base.ts +34 -1
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- 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 +26 -9
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-request.ts +8 -5
- package/src/adapters/cursor/request-builder.ts +21 -4
- package/src/adapters/cursor/thread-continuity.ts +105 -31
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +45 -2
- package/src/adapters/devin/cloud-direct/chat.ts +14 -3
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
- package/src/adapters/devin.ts +125 -26
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +55 -4
- package/src/adapters/google-http.ts +57 -11
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +60 -10
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +10 -11
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +25 -4
- package/src/adapters/openai-responses/reasoning.ts +7 -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/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +28 -35
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +35 -4
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +26 -6
- 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/dispatch.ts +57 -3
- package/src/cli/doctor.ts +218 -4
- package/src/cli/help.ts +4 -1
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +95 -22
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- 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 +2 -0
- package/src/client/machine-api.ts +6 -5
- package/src/client/machine-listener.ts +16 -3
- package/src/client/runtime.ts +26 -2
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +146 -5
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +44 -10
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +16 -12
- 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 +22 -2
- package/src/codex/catalog/model-hints.ts +66 -33
- package/src/codex/catalog/parsing.ts +90 -5
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +41 -26
- package/src/codex/catalog/routed-gather.ts +61 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +24 -167
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject.ts +3 -2
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +31 -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-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +80 -2
- package/src/codex/observed-model-denials.ts +230 -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 +289 -16
- 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 +20 -16
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +114 -2
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +125 -5
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +43 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +33 -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 +31 -7
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +25 -0
- package/src/config/schema/leaf-validators.ts +76 -17
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +410 -258
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/mutation-plan.ts +815 -0
- package/src/integrations/writer.ts +85 -99
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +82 -15
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-outbound.ts +53 -5
- 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 +741 -0
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +332 -35
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +41 -27
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +3 -107
- package/src/oauth/login-flow-state.ts +127 -0
- 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 +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +327 -25
- package/src/providers/model-rename-migration.ts +56 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/quota/vendor-probes-key.ts +1 -1
- package/src/providers/registry/entries-core.ts +39 -17
- package/src/providers/registry/entries-extended.ts +131 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +87 -21
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/responses/apply-patch-envelope.ts +32 -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/freeform-wrapper-scan.ts +279 -0
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +9 -2
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +91 -115
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +13 -5
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +30 -5
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/serve-options.ts +86 -29
- package/src/server/index/spend-ledger-lifecycle.ts +66 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +24 -25
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +48 -7
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/management/route-registry.ts +14 -0
- package/src/server/management/shared.ts +10 -3
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +136 -34
- package/src/server/request-log.ts +80 -3
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +74 -30
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +63 -30
- package/src/server/responses/compact.ts +89 -18
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +90 -24
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +16 -15
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +68 -2
- package/src/server/responses/passthrough-delivery.ts +41 -15
- package/src/server/responses/passthrough-dispatch.ts +145 -53
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +96 -17
- package/src/server/responses/request-send-budget.ts +89 -8
- package/src/server/responses/request-sidecar-auth.ts +17 -9
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +15 -12
- package/src/server/responses/run-turn-execution.ts +45 -8
- package/src/server/responses/sidecar-execution.ts +19 -2
- package/src/server/responses/ws-upstream.ts +16 -28
- package/src/server/responses-custom-tool-repair.ts +29 -56
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +37 -10
- 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/service/cli.ts +16 -6
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/tray/windows-tray.ps1 +155 -3
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +111 -6
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +9 -1
- package/src/types/tools.ts +14 -0
- package/src/types/wire.ts +9 -1
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-BbrHOIY0.js +0 -128
- package/gui/dist/assets/index-C5-RdDmD.css +0 -1
package/src/adapters/google.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base";
|
|
2
|
-
import { debugDroppedFrame } from "../lib/debug";
|
|
2
|
+
import { debugDroppedFrame, debugProviderDiagnosticLazy } from "../lib/debug";
|
|
3
3
|
import { createToolCallIdAllocator } from "./tool-call-id";
|
|
4
4
|
import { createImageBudget, materializeInlineImage, MAX_ENCODED_BYTES_PER_IMAGE, artifactHttpUrl } from "../images/artifacts";
|
|
5
5
|
import type {
|
|
@@ -20,8 +20,10 @@ import { getVertexAccessToken } from "../lib/gcp-adc";
|
|
|
20
20
|
import { fetchAntigravityWithRetry, fetchVertexWithRetry } from "./google-http";
|
|
21
21
|
import { safeAntigravityHttpErrorMessage, safeVertexHttpErrorMessage } from "./google-errors";
|
|
22
22
|
import { isVertexTruncatedTurn, vertexTruncationErrorMessage } from "./google-truncation";
|
|
23
|
-
import { ANTIGRAVITY_REQUEST_UA, antigravitySessionId, isLikelyRealThoughtSignature, sanitizeAntigravityClaudeSignatures } from "./google-antigravity-wire";
|
|
23
|
+
import { ANTIGRAVITY_REQUEST_UA, antigravitySessionAnchor, antigravitySessionId, isLikelyRealThoughtSignature, sanitizeAntigravityClaudeSignatures } from "./google-antigravity-wire";
|
|
24
|
+
import { summarizeGoogleWireShape } from "./google-wire-shape";
|
|
24
25
|
import { compileGoogleWireBody } from "./google-wire-compiler";
|
|
26
|
+
import type { GoogleToolSchemaLossReport, GoogleToolSchemaProfile } from "./google-tool-schema";
|
|
25
27
|
import { identifyRoutedModel } from "./identity";
|
|
26
28
|
import {
|
|
27
29
|
antigravityUsesReplayCache,
|
|
@@ -57,7 +59,6 @@ const GOOGLE_BREVITY_INSTRUCTION = [
|
|
|
57
59
|
|
|
58
60
|
const ANTIGRAVITY_REJECTED_CLAUDE_SDK_PARAGRAPH =
|
|
59
61
|
"You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
60
|
-
|
|
61
62
|
/**
|
|
62
63
|
* CCA Flash generations that reject the Claude-Agent identity paragraph.
|
|
63
64
|
*
|
|
@@ -102,6 +103,20 @@ function stripAntigravityRejectedClaudeSdkParagraph(systemText: string): string
|
|
|
102
103
|
.join("\n\n");
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Strips Claude Code CLI's internal billing header (`x-anthropic-billing-header: ...`)
|
|
108
|
+
* at the start of the system prompt, because Cloud Code Assist / Google Antigravity inspects
|
|
109
|
+
* `systemInstruction` and rejects requests containing Anthropic billing metadata with
|
|
110
|
+
* HTTP 429 RESOURCE_EXHAUSTED.
|
|
111
|
+
*
|
|
112
|
+
* Matching is restricted to the prompt start (`^` without the `/m` multiline flag) so that
|
|
113
|
+
* user prompts discussing billing headers in intermediate lines are never modified, and
|
|
114
|
+
* prompts without a billing header preserve their leading whitespace untouched.
|
|
115
|
+
*/
|
|
116
|
+
function stripAntigravityBillingHeader(systemText: string): string {
|
|
117
|
+
return systemText.replace(/^x-anthropic-billing-header:[^\n]*\n*/, "");
|
|
118
|
+
}
|
|
119
|
+
|
|
105
120
|
/**
|
|
106
121
|
* Documented output ceiling for a Google-surface model, or `undefined` when the id is not
|
|
107
122
|
* recognized.
|
|
@@ -276,6 +291,7 @@ function messagesToGeminiFormat(
|
|
|
276
291
|
parsed: OcxParsedRequest,
|
|
277
292
|
identityModelId: string,
|
|
278
293
|
stripRejectedClaudeSdkParagraph = false,
|
|
294
|
+
isCloudCodeAssist = false,
|
|
279
295
|
): { systemInstruction?: unknown; contents: unknown[]; replayedCallIds: string[] } {
|
|
280
296
|
// Neutralize Codex's GPT-5 identity line (Gemini/Antigravity share this path) so a routed model
|
|
281
297
|
// never misreports as GPT-5/OpenAI, and never leaks the proxy identity upstream.
|
|
@@ -285,9 +301,12 @@ function messagesToGeminiFormat(
|
|
|
285
301
|
...(toolCatalogNudge ? [toolCatalogNudge] : []),
|
|
286
302
|
GOOGLE_BREVITY_INSTRUCTION,
|
|
287
303
|
].join("\n\n"), identityModelId);
|
|
288
|
-
|
|
289
|
-
?
|
|
304
|
+
let systemText = isCloudCodeAssist
|
|
305
|
+
? stripAntigravityBillingHeader(identifiedSystemText)
|
|
290
306
|
: identifiedSystemText;
|
|
307
|
+
if (stripRejectedClaudeSdkParagraph) {
|
|
308
|
+
systemText = stripAntigravityRejectedClaudeSdkParagraph(systemText);
|
|
309
|
+
}
|
|
291
310
|
const systemInstruction = { parts: [{ text: systemText }] };
|
|
292
311
|
|
|
293
312
|
const contents: unknown[] = [];
|
|
@@ -720,6 +739,14 @@ function invalidGoogleShapeEvent(
|
|
|
720
739
|
}
|
|
721
740
|
|
|
722
741
|
export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapter {
|
|
742
|
+
const toolSchemaPolicy = provider.googleToolSchemaPolicy ?? "compatible";
|
|
743
|
+
const toolSchemaProfile = {
|
|
744
|
+
endpointClass: provider.googleMode ?? "ai-studio",
|
|
745
|
+
} satisfies GoogleToolSchemaProfile;
|
|
746
|
+
const reportToolSchemaLoss = (report: GoogleToolSchemaLossReport): void => {
|
|
747
|
+
if (!report.lossy && report.uncertainComparisons === 0) return;
|
|
748
|
+
debugProviderDiagnosticLazy("google", "google-tool-schema-loss", () => ({ ...report }));
|
|
749
|
+
};
|
|
723
750
|
// Per-request closure: resolveAdapter builds a fresh adapter per request (server.ts), so buildRequest
|
|
724
751
|
// can stash the CCA model/session for parseStream's reasoning-replay observation.
|
|
725
752
|
let antigravityModel: string | undefined;
|
|
@@ -782,7 +809,11 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
782
809
|
...(provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist"
|
|
783
810
|
? {
|
|
784
811
|
fetchResponse: (request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response> =>
|
|
785
|
-
(provider.googleMode === "cloud-code-assist" ? fetchAntigravityWithRetry : fetchVertexWithRetry)(
|
|
812
|
+
(provider.googleMode === "cloud-code-assist" ? fetchAntigravityWithRetry : fetchVertexWithRetry)(
|
|
813
|
+
request,
|
|
814
|
+
ctx,
|
|
815
|
+
{ toolSchemaProfile, toolSchemaPolicy },
|
|
816
|
+
),
|
|
786
817
|
formatErrorBody: (status: number, _headers: Headers, payloadText: string): string =>
|
|
787
818
|
(provider.googleMode === "cloud-code-assist" ? safeAntigravityHttpErrorMessage : safeVertexHttpErrorMessage)(status, payloadText),
|
|
788
819
|
}
|
|
@@ -833,12 +864,14 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
833
864
|
&& /^gemini-/.test(routedModelId) && !isImageCapableModel(parsed.modelId);
|
|
834
865
|
// AI Studio's `-tiered` spelling is wire-only; CCA aliases may migrate to another generation.
|
|
835
866
|
const identityModelId = provider.googleMode === "cloud-code-assist" ? routedModelId : parsed.modelId;
|
|
836
|
-
const
|
|
867
|
+
const isCloudCodeAssist = provider.googleMode === "cloud-code-assist";
|
|
868
|
+
const stripRejectedClaudeSdkParagraph = isCloudCodeAssist
|
|
837
869
|
&& rejectsClaudeSdkParagraph(parsed.modelId, routedModelId);
|
|
838
870
|
const { systemInstruction, contents, replayedCallIds } = messagesToGeminiFormat(
|
|
839
871
|
parsed,
|
|
840
872
|
identityModelId,
|
|
841
873
|
stripRejectedClaudeSdkParagraph,
|
|
874
|
+
isCloudCodeAssist,
|
|
842
875
|
);
|
|
843
876
|
lastInjectedCallIds = [...replayedCallIds];
|
|
844
877
|
lastReasoningReplayScope = parsed._reasoningReplayScope;
|
|
@@ -951,7 +984,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
951
984
|
const fcc = (existing.functionCallingConfig ?? {}) as Record<string, unknown>;
|
|
952
985
|
draftRequest.toolConfig = { ...existing, functionCallingConfig: { ...fcc, mode: "VALIDATED" } };
|
|
953
986
|
}
|
|
954
|
-
const compiled = compileGoogleWireBody(draftRequest);
|
|
987
|
+
const compiled = compileGoogleWireBody(draftRequest, toolSchemaProfile, toolSchemaPolicy);
|
|
988
|
+
reportToolSchemaLoss(compiled.toolSchemaLossReport);
|
|
955
989
|
const request = compiled.body;
|
|
956
990
|
restoreGoogleToolName = compiled.restoreToolName;
|
|
957
991
|
// Compile names before replay: signatures are keyed by the exact provider-visible name.
|
|
@@ -970,6 +1004,20 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
970
1004
|
// Vertex and AI Studio share one decision. A second check here would append a
|
|
971
1005
|
// duplicate nudge whenever signature sanitization reshapes the tail afterwards.
|
|
972
1006
|
}
|
|
1007
|
+
// Opt-in structural description of the request that is about to leave (#5008).
|
|
1008
|
+
//
|
|
1009
|
+
// Passed as a BUILDER, not a value: the lazy form gates before invoking it, so a session
|
|
1010
|
+
// with provider debug off never pays the walk, and it evaluates the projection inside its
|
|
1011
|
+
// own try/catch, so a throw in here cannot turn a built request into a rejected one. The
|
|
1012
|
+
// projection only reads `request`, so the bytes below are the same either way.
|
|
1013
|
+
debugProviderDiagnosticLazy("google", "antigravity-wire-shape", () => summarizeGoogleWireShape(request, {
|
|
1014
|
+
sessionAnchor: antigravitySessionAnchor(parsed),
|
|
1015
|
+
// Signed at translation time, from client history or the durable store. The
|
|
1016
|
+
// Antigravity session cache signs afterwards, inside applyAntigravityReplay, and the
|
|
1017
|
+
// projection attributes that remainder to the cache rather than to this count.
|
|
1018
|
+
historySignedCalls: replayedCallIds.length,
|
|
1019
|
+
replayScopeBound: parsed._reasoningReplayScope !== undefined,
|
|
1020
|
+
}));
|
|
973
1021
|
const envelope = {
|
|
974
1022
|
model: wireModelId,
|
|
975
1023
|
// The envelope's `userAgent` field is a protocol constant ("antigravity"), distinct from
|
|
@@ -987,7 +1035,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
987
1035
|
}
|
|
988
1036
|
|
|
989
1037
|
if (provider.googleMode === "vertex") {
|
|
990
|
-
const compiled = compileGoogleWireBody(body);
|
|
1038
|
+
const compiled = compileGoogleWireBody(body, toolSchemaProfile, toolSchemaPolicy);
|
|
1039
|
+
reportToolSchemaLoss(compiled.toolSchemaLossReport);
|
|
991
1040
|
restoreGoogleToolName = compiled.restoreToolName;
|
|
992
1041
|
const vertexProject = provider.project || process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT || "api-key";
|
|
993
1042
|
const vertexLocation = provider.location || process.env.GOOGLE_CLOUD_LOCATION || "global";
|
|
@@ -1033,7 +1082,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
1033
1082
|
if (!apiKey) throw new Error("google (AI Studio) requires a non-empty API key");
|
|
1034
1083
|
headers["x-goog-api-key"] = apiKey;
|
|
1035
1084
|
|
|
1036
|
-
const compiled = compileGoogleWireBody(body);
|
|
1085
|
+
const compiled = compileGoogleWireBody(body, toolSchemaProfile, toolSchemaPolicy);
|
|
1086
|
+
reportToolSchemaLoss(compiled.toolSchemaLossReport);
|
|
1037
1087
|
restoreGoogleToolName = compiled.restoreToolName;
|
|
1038
1088
|
return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
|
|
1039
1089
|
},
|
|
@@ -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 };
|
|
@@ -13,7 +13,9 @@ import {
|
|
|
13
13
|
export const OPENAI_CHAT_IMAGE_BASE64_BUDGET = 3_670_016; // 3.5MiB
|
|
14
14
|
|
|
15
15
|
export interface NormalizeOpenAIChatImagesOptions
|
|
16
|
-
extends Pick<NormalizeOptions, "encode" | "tierBias" | "validate"> {
|
|
16
|
+
extends Pick<NormalizeOptions, "encode" | "tierBias" | "validate"> {
|
|
17
|
+
abortSignal?: AbortSignal;
|
|
18
|
+
}
|
|
17
19
|
|
|
18
20
|
/** Whether `value` is a plain object, so message and part shapes can be walked safely. */
|
|
19
21
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -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,
|
|
@@ -296,7 +297,11 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
296
297
|
};
|
|
297
298
|
};
|
|
298
299
|
if (hasShrinkableOpenAIChatImages(messages)) {
|
|
299
|
-
|
|
300
|
+
const imageOptions = { tierBias: incoming?.imageTierBias, abortSignal: incoming?.abortSignal };
|
|
301
|
+
return normalizeOpenAIChatImages(messages, imageOptions).then(finish, error => {
|
|
302
|
+
if (incoming?.abortSignal?.aborted) throw error;
|
|
303
|
+
return finish();
|
|
304
|
+
});
|
|
300
305
|
}
|
|
301
306
|
return finish();
|
|
302
307
|
},
|
|
@@ -385,7 +390,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
385
390
|
// full text-so-far, so deltas are derived by prefix-diffing per segment key.
|
|
386
391
|
// A piece that does not extend the previous snapshot is appended whole, which
|
|
387
392
|
// keeps incremental senders parseable on the same path.
|
|
388
|
-
const
|
|
393
|
+
const reasoningDetailTracker = createReasoningDetailSnapshotTracker(budget);
|
|
389
394
|
// Gate on the routed model, not list length: a mixed openai-chat provider
|
|
390
395
|
// can list MiniMax ids without putting every sibling on MiniMax semantics.
|
|
391
396
|
const reasoningDetailsOptIn = modelInList(provider.reasoningDetailsModels, lastRequestedModelId ?? "");
|
|
@@ -448,15 +453,8 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
448
453
|
const detailSegments = reasoningDetailsOptIn ? reasoningDetailSegmentsFrom(delta) : [];
|
|
449
454
|
if (detailSegments.length > 0) {
|
|
450
455
|
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
|
-
}
|
|
456
|
+
const reasoningDelta = reasoningDetailTracker.ingest(segment);
|
|
457
|
+
if (reasoningDelta !== null) yield { type: "reasoning_raw_delta", text: reasoningDelta };
|
|
460
458
|
}
|
|
461
459
|
} else {
|
|
462
460
|
const reasoningText = reasoningTextFrom(delta);
|
|
@@ -692,6 +690,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
692
690
|
throw error;
|
|
693
691
|
} finally {
|
|
694
692
|
budget.releaseRetained(bufferBytes, { kind: "live_transient" });
|
|
693
|
+
reasoningDetailTracker.release();
|
|
695
694
|
closeToolCalls();
|
|
696
695
|
reader.releaseLock();
|
|
697
696
|
}
|
|
@@ -101,14 +101,17 @@ export function preferConfiguredHostedTools(
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
let input = body.input;
|
|
104
|
-
|
|
104
|
+
// Indices arrive in increasing order, so the first stripped container is already
|
|
105
|
+
// the minimum — tracking it directly avoids spreading an attacker-sized Set into
|
|
106
|
+
// Math.min's argument list.
|
|
107
|
+
let firstStrippedAdditionalToolsIndex: number | undefined;
|
|
105
108
|
if (Array.isArray(body.input)) {
|
|
106
109
|
let nestedChanged = false;
|
|
107
110
|
const mappedInput = body.input.map((item, index) => {
|
|
108
111
|
if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) return item;
|
|
109
112
|
const nestedTools = stripGroup(item.tools);
|
|
110
113
|
if (nestedTools === item.tools) return item;
|
|
111
|
-
|
|
114
|
+
firstStrippedAdditionalToolsIndex ??= index;
|
|
112
115
|
nestedChanged = true;
|
|
113
116
|
return { ...item, tools: nestedTools };
|
|
114
117
|
});
|
|
@@ -127,17 +130,16 @@ export function preferConfiguredHostedTools(
|
|
|
127
130
|
|| (Array.isArray(input) && input.some(item => isPlainObject(item)
|
|
128
131
|
&& item.type === "additional_tools"
|
|
129
132
|
&& hasHostedImageGenTool(item.tools)));
|
|
130
|
-
if ((strippedTopLevelImageGenTool ||
|
|
133
|
+
if ((strippedTopLevelImageGenTool || firstStrippedAdditionalToolsIndex !== undefined) && !hasHostedImageGenDeclaration) {
|
|
131
134
|
if (strippedTopLevelImageGenTool && Array.isArray(tools)) {
|
|
132
135
|
tools = [...tools, { type: HOSTED_IMAGE_GENERATION_TOOL }];
|
|
133
|
-
} else if (
|
|
136
|
+
} else if (firstStrippedAdditionalToolsIndex !== undefined && Array.isArray(input)) {
|
|
134
137
|
// Restore into the FIRST stripped container only. Tool declarations are
|
|
135
138
|
// request-scoped, not container-scoped — the containers are separate carriers for
|
|
136
139
|
// one tool set, so a single hosted declaration covers the request. An earlier
|
|
137
140
|
// revision restored into every stripped container and put `image_generation` on
|
|
138
141
|
// the wire twice; review caught it.
|
|
139
|
-
|
|
140
|
-
input = input.map((item, index) => index === firstStripped
|
|
142
|
+
input = input.map((item, index) => index === firstStrippedAdditionalToolsIndex
|
|
141
143
|
&& isPlainObject(item)
|
|
142
144
|
&& Array.isArray(item.tools)
|
|
143
145
|
? { ...item, tools: [...item.tools, { type: HOSTED_IMAGE_GENERATION_TOOL }] }
|
|
@@ -18,6 +18,7 @@ import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
|
18
18
|
import { rewriteRoutedCustomToolsForUpstream } from "../../responses/custom-tool-compat";
|
|
19
19
|
import { rewriteRoutedToolSearchForUpstream } from "../../responses/tool-search-compat";
|
|
20
20
|
import { rewriteRoutedNamespaceToolsForUpstream } from "../../responses/namespace-tool-compat";
|
|
21
|
+
import { repairLegacyDottedToolCallNames } from "../../responses/legacy-dotted-tool-name-repair";
|
|
21
22
|
import { preparePlaintextV2AgentMessages } from "../../responses/plaintext-v2-agent-messages";
|
|
22
23
|
import { isMetaAiResponsesDestination, rewriteMuseToolNamesForUpstream } from "../../responses/muse-tool-name-alias";
|
|
23
24
|
import { openaiResponsesUrl } from "../openai-responses-url";
|
|
@@ -31,12 +32,13 @@ import {
|
|
|
31
32
|
import {
|
|
32
33
|
createAdapterTierMetadata,
|
|
33
34
|
} from "../../providers/fastwire";
|
|
34
|
-
import { mapRoutedResponsesReasoningEffort, normalizeConfiguredReasoningSummaryDelivery, sanitizeReasoningInputContent, stripDisabledReasoningSummaries, stripDisabledVerbosity, stripUnsupportedReasoningSummaryDelivery } from "./reasoning";
|
|
35
|
+
import { dropResponsesReasoningInputItems, mapRoutedResponsesReasoningEffort, normalizeConfiguredReasoningSummaryDelivery, sanitizeReasoningInputContent, stripDisabledReasoningSummaries, stripDisabledVerbosity, stripUnsupportedReasoningSummaryDelivery } from "./reasoning";
|
|
35
36
|
import { scrubOcxCompactionItems, stripCanonicalOnlyToolFields, stripCanonicalOnlyTopLevelFields, stripInternalChatMessageMetadataPassthrough, stripInvalidItemIds, stripItemIdsWhenUnstored } from "./request-strips";
|
|
36
37
|
import { stripCanonicalForwardPromptCacheOptions, stripDeprecatedPromptCacheRetention } from "./prompt-cache";
|
|
37
38
|
import { isPlainObject } from "./internal";
|
|
38
39
|
import { normalizeToolSchemas, promoteClientLoadedTools, stripUnsupportedHostedTools } from "./tool-schema";
|
|
39
|
-
import { annotateEmptyResponsesToolOutputs, backfillWebSearchQueries, normalizeResponsesToolResultAdjacency, repairOrphanedInputItems, repairOversizedReplayCallIds, repairUnidentifiedToolOutputItems } from "./tool-output-recovery";
|
|
40
|
+
import { annotateEmptyResponsesToolOutputs, backfillWebSearchQueries, normalizeResponsesToolResultAdjacency, repairOrphanedInputItems, repairOversizedReplayCallIds, repairUnidentifiedToolOutputItems, restoreBridgedWebSearchCalls } from "./tool-output-recovery";
|
|
41
|
+
import { bridgeSearchReplayScope } from "../../responses/bridge-search-replay-cache";
|
|
40
42
|
import { applyTierDecisionToResponsesBody, normalizeCanonicalForwardContinuationEnvelope, normalizeCanonicalForwardPromptEnvelope, stripCanonicalForwardSamplingParams, stripPreviousResponseId, stripStatefulResponsesParams, stripUnsupportedForwardParams } from "./canonical-forward";
|
|
41
43
|
import { normalizeImageGenClientTools, preferConfiguredHostedTools } from "./image-gen";
|
|
42
44
|
import { stripMuseSparkUnsupportedWebSearchFields, stripOpenAiOnlyWebSearchFields } from "./web-search";
|
|
@@ -294,6 +296,9 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
294
296
|
if (forward || stateless || pairedToolResults) {
|
|
295
297
|
outBody = repairOrphanedInputItems(outBody, unexpandedMiss, synthesizeMissingCallOutputs);
|
|
296
298
|
}
|
|
299
|
+
if (provider.dropResponsesReasoningItems === true) {
|
|
300
|
+
outBody = dropResponsesReasoningInputItems(outBody);
|
|
301
|
+
}
|
|
297
302
|
if (adjacentToolResults) {
|
|
298
303
|
outBody = normalizeResponsesToolResultAdjacency(outBody);
|
|
299
304
|
}
|
|
@@ -321,11 +326,25 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
321
326
|
outBody = repairOversizedReplayCallIds(outBody);
|
|
322
327
|
}
|
|
323
328
|
outBody = stripUnsupportedReasoningSummaryDelivery(outBody, parsed.modelId);
|
|
329
|
+
// #4587: on a bridged provider, hand the destination back the search call and result the
|
|
330
|
+
// proxy executed on its behalf, in place of the hosted cell the caller replays. Scoped to
|
|
331
|
+
// this destination and recorded by the bridge itself, so a provider without the opt-in
|
|
332
|
+
// computes no identity and keeps the body reference it already had. This runs before the
|
|
333
|
+
// query backfill below because a restored cell is no longer a web_search_call to repair.
|
|
334
|
+
if (provider.webSearchBridge?.enabled === true) {
|
|
335
|
+
outBody = restoreBridgedWebSearchCalls(outBody, bridgeSearchReplayScope(provider.baseUrl));
|
|
336
|
+
}
|
|
324
337
|
// Repair stored history from before the bridge emitted both keys, in either
|
|
325
338
|
// direction: a conversation that already recorded a web_search_call replays it
|
|
326
339
|
// every turn, and a strict parser rejects the whole request over the missing key —
|
|
327
340
|
// `queries` for DeepSeek (#930), `query` for Console Go (#3071).
|
|
328
341
|
outBody = backfillWebSearchQueries(outBody);
|
|
342
|
+
// #5095: a conversation that already contains a `default.`-prefixed call name is refused by
|
|
343
|
+
// the upstream `^[a-zA-Z0-9_-]+$` name pattern on every later turn that replays it, so the
|
|
344
|
+
// task cannot be compacted or continued at all. Repair the replayed item here, before the
|
|
345
|
+
// canonical-destination split below, because the reported failure was a side chat on a plain
|
|
346
|
+
// OpenAI model inheriting history a routed provider had damaged.
|
|
347
|
+
outBody = repairLegacyDottedToolCallNames(outBody);
|
|
329
348
|
if (!isCanonicalOpenAiForwardProvider(provider)) {
|
|
330
349
|
outBody = stripInternalChatMessageMetadataPassthrough(outBody);
|
|
331
350
|
// The same class of private field, one level up, but keyed on the DESTINATION rather than
|
|
@@ -388,14 +407,16 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
388
407
|
// Last, so promoted namespace children are also cleared of Codex-private fields.
|
|
389
408
|
outBody = stripCanonicalOnlyToolFields(outBody, provider.supportsOpenAiWebSearchToolFields === false);
|
|
390
409
|
}
|
|
391
|
-
if (!forward)
|
|
410
|
+
if (!forward) {
|
|
411
|
+
outBody = normalizeOpenCodeGoAdditionalTools(outBody, url, parsed._replayPrefixLen);
|
|
412
|
+
}
|
|
392
413
|
// Same predicate as the routedCompaction gate in handleResponses(): an authMode check would
|
|
393
414
|
// let a noncanonical custom forward provider skip this rewrite while the server still routes
|
|
394
415
|
// it as a summarizer turn (#422). The compaction body build removes the tool surface and must
|
|
395
416
|
// therefore be the last routed transform that may depend on those declarations. Structural
|
|
396
417
|
// sanitizers below can still run after it.
|
|
397
418
|
outBody = normalizeResponsesCodeMode(outBody, parsed, provider);
|
|
398
|
-
if (parsed._compactionRequest === true && !isCanonicalOpenAiForwardProvider(provider)) {
|
|
419
|
+
if (parsed._compactionRequest === true && (!isCanonicalOpenAiForwardProvider(provider) || parsed._portableCompaction === true)) {
|
|
399
420
|
outBody = buildRoutedCompactionBody(outBody);
|
|
400
421
|
}
|
|
401
422
|
// Run after routed compaction so nested input_image parts are replaced before a malformed
|
|
@@ -4,6 +4,13 @@ import { OCX_REASONING_PREFIX } from "../../responses/reasoning-envelope";
|
|
|
4
4
|
import { configuredReasoningEfforts, mapReasoningEffort, modelRecordValue } from "../../reasoning-effort";
|
|
5
5
|
import { isPlainObject } from "./internal";
|
|
6
6
|
|
|
7
|
+
/** Drop only replayed Responses reasoning items; all other continuation input stays untouched. */
|
|
8
|
+
export function dropResponsesReasoningInputItems(body: unknown): unknown {
|
|
9
|
+
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
|
|
10
|
+
const input = body.input.filter(item => !isPlainObject(item) || item.type !== "reasoning");
|
|
11
|
+
return input.length === body.input.length ? body : { ...body, input };
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
/**
|
|
8
15
|
* Sanitize reasoning input by field policy, not by preserving each item's shape. Retaining a
|
|
9
16
|
* native `encrypted_content` guarantees only that blob value: `status` is always removed;
|
|
@@ -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;
|
|
@@ -3,7 +3,11 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
3
3
|
}
|
|
4
4
|
|
|
5
5
|
/** Console Go accepts public tools but rejects the private additional_tools input wrapper. */
|
|
6
|
-
export function normalizeOpenCodeGoAdditionalTools(
|
|
6
|
+
export function normalizeOpenCodeGoAdditionalTools(
|
|
7
|
+
body: unknown,
|
|
8
|
+
responseUrl: string,
|
|
9
|
+
replayPrefixLength = 0,
|
|
10
|
+
): unknown {
|
|
7
11
|
let destination: URL;
|
|
8
12
|
try {
|
|
9
13
|
destination = new URL(responseUrl);
|
|
@@ -20,10 +24,16 @@ export function normalizeOpenCodeGoAdditionalTools(body: unknown, responseUrl: s
|
|
|
20
24
|
|
|
21
25
|
const input: unknown[] = [];
|
|
22
26
|
const promoted: unknown[] = [];
|
|
27
|
+
const currentTurnStart = Number.isFinite(replayPrefixLength)
|
|
28
|
+
? Math.min(body.input.length, Math.max(0, Math.trunc(replayPrefixLength)))
|
|
29
|
+
: 0;
|
|
23
30
|
let changed = false;
|
|
24
|
-
for (const item of body.input) {
|
|
31
|
+
for (const [index, item] of body.input.entries()) {
|
|
25
32
|
if (isRecord(item) && item.type === "additional_tools" && Array.isArray(item.tools)) {
|
|
26
33
|
changed = true;
|
|
34
|
+
// Replayed wrappers are conversation history, not authority for the current request.
|
|
35
|
+
// Go cannot accept the wrapper itself, so remove it without promoting its catalog.
|
|
36
|
+
if (index < currentTurnStart) continue;
|
|
27
37
|
// Custom/search/namespace lowering already owns identity and deduplication. This pass
|
|
28
38
|
// only moves declarations, including hosted tools that intentionally have no name.
|
|
29
39
|
for (const tool of item.tools) promoted.push(tool);
|