@yansigit/opencodex 2.31.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/AGENTS_INSTALL.md +109 -0
- package/LICENSE +21 -0
- package/README.md +303 -0
- package/assets/architecture.png +0 -0
- package/assets/banner.png +0 -0
- package/assets/claude-code-models.gif +0 -0
- package/assets/codex-app-picker.png +0 -0
- package/bin/ocx.mjs +587 -0
- package/bin/package-main.mjs +9 -0
- package/gui/dist/assets/index-BNJ7r4Gd.js +102 -0
- package/gui/dist/assets/index-CGoDO3uO.css +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/icons.svg +24 -0
- package/gui/dist/index.html +25 -0
- package/gui/dist/logo.png +0 -0
- package/gui/dist/provider-icons/alibaba-color.svg +1 -0
- package/gui/dist/provider-icons/antigravity-color.svg +1 -0
- package/gui/dist/provider-icons/claude-color.svg +1 -0
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -0
- package/gui/dist/provider-icons/cursor-color.svg +2 -0
- package/gui/dist/provider-icons/deepseek-color.svg +1 -0
- package/gui/dist/provider-icons/discord.svg +1 -0
- package/gui/dist/provider-icons/firepass-color.svg +1 -0
- package/gui/dist/provider-icons/fireworks-color.svg +1 -0
- package/gui/dist/provider-icons/gemini-color.svg +1 -0
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
- package/gui/dist/provider-icons/grok.svg +1 -0
- package/gui/dist/provider-icons/groq-color.svg +1 -0
- package/gui/dist/provider-icons/huggingface-color.svg +1 -0
- package/gui/dist/provider-icons/kimi-color.svg +1 -0
- package/gui/dist/provider-icons/kiro-color.svg +15 -0
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
- package/gui/dist/provider-icons/mistral-color.svg +1 -0
- package/gui/dist/provider-icons/moonshot-color.svg +1 -0
- package/gui/dist/provider-icons/nvidia-color.svg +1 -0
- package/gui/dist/provider-icons/ollama-color.svg +1 -0
- package/gui/dist/provider-icons/openai.svg +1 -0
- package/gui/dist/provider-icons/opencode.svg +2 -0
- package/gui/dist/provider-icons/openrouter-color.svg +1 -0
- package/gui/dist/provider-icons/pi.svg +21 -0
- package/gui/dist/provider-icons/qianfan-color.svg +1 -0
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
- package/gui/dist/provider-icons/telegram.svg +1 -0
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/vllm-color.svg +1 -0
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
- package/package.json +108 -0
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic-image-guard.ts +251 -0
- package/src/adapters/anthropic-image-normalize.ts +518 -0
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +1327 -0
- package/src/adapters/azure.ts +36 -0
- package/src/adapters/base.ts +121 -0
- package/src/adapters/client-fingerprint.ts +65 -0
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
- package/src/adapters/command-code.ts +601 -0
- package/src/adapters/cursor/arg-codec.ts +38 -0
- package/src/adapters/cursor/arg-normalize.ts +104 -0
- package/src/adapters/cursor/checkpoint-store.ts +303 -0
- package/src/adapters/cursor/cursor-errors.ts +288 -0
- package/src/adapters/cursor/discovery.ts +333 -0
- package/src/adapters/cursor/effort-map.ts +151 -0
- package/src/adapters/cursor/exec-policy.ts +88 -0
- package/src/adapters/cursor/framing.ts +250 -0
- package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
- package/src/adapters/cursor/h2-pool.ts +123 -0
- package/src/adapters/cursor/http1-bidi.ts +361 -0
- package/src/adapters/cursor/images.ts +704 -0
- package/src/adapters/cursor/kv-store.ts +52 -0
- package/src/adapters/cursor/live-models.ts +269 -0
- package/src/adapters/cursor/live-smoke-gate.ts +41 -0
- package/src/adapters/cursor/live-transport.ts +1653 -0
- package/src/adapters/cursor/mcp-config.ts +42 -0
- package/src/adapters/cursor/mcp-manager.ts +333 -0
- package/src/adapters/cursor/message-mapper.ts +49 -0
- package/src/adapters/cursor/native-exec-common.ts +76 -0
- package/src/adapters/cursor/native-exec-desktop.ts +184 -0
- package/src/adapters/cursor/native-exec-fs.ts +332 -0
- package/src/adapters/cursor/native-exec-mcp.ts +153 -0
- package/src/adapters/cursor/native-exec-network.ts +43 -0
- package/src/adapters/cursor/native-exec-shell.ts +547 -0
- package/src/adapters/cursor/native-exec-tools.ts +118 -0
- package/src/adapters/cursor/native-exec.ts +663 -0
- package/src/adapters/cursor/protobuf-events.ts +1381 -0
- package/src/adapters/cursor/protobuf-request.ts +1032 -0
- package/src/adapters/cursor/request-builder.ts +461 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/tool-definitions.ts +735 -0
- package/src/adapters/cursor/tool-result-normalize.ts +92 -0
- package/src/adapters/cursor/transport-retry.ts +132 -0
- package/src/adapters/cursor/transport.ts +79 -0
- package/src/adapters/cursor/types.ts +79 -0
- package/src/adapters/cursor.ts +322 -0
- package/src/adapters/google-antigravity-hosts.ts +48 -0
- package/src/adapters/google-antigravity-replay.ts +827 -0
- package/src/adapters/google-antigravity-tools.ts +105 -0
- package/src/adapters/google-antigravity-wire.ts +141 -0
- package/src/adapters/google-errors.ts +92 -0
- package/src/adapters/google-http.ts +460 -0
- package/src/adapters/google-tool-schema.ts +238 -0
- package/src/adapters/google-truncation.ts +24 -0
- package/src/adapters/google-wire-compiler.ts +232 -0
- package/src/adapters/google.ts +1377 -0
- package/src/adapters/identity.ts +77 -0
- package/src/adapters/image.ts +23 -0
- package/src/adapters/kiro-constants.ts +16 -0
- package/src/adapters/kiro-errors.ts +208 -0
- package/src/adapters/kiro-events.ts +197 -0
- package/src/adapters/kiro-images.ts +129 -0
- package/src/adapters/kiro-retry.ts +312 -0
- package/src/adapters/kiro-thinking.ts +112 -0
- package/src/adapters/kiro-tool-fallback.ts +36 -0
- package/src/adapters/kiro-tools.ts +224 -0
- package/src/adapters/kiro-truncation.ts +33 -0
- package/src/adapters/kiro-wire.ts +129 -0
- package/src/adapters/kiro.ts +1936 -0
- package/src/adapters/mimo-free.ts +280 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1980 -0
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +1911 -0
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/run-turn-queue.ts +114 -0
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +154 -0
- package/src/adapters/upstream-http-error.ts +48 -0
- package/src/adapters/xai-web-search.ts +185 -0
- package/src/bridge.ts +1986 -0
- package/src/chat/inbound.ts +319 -0
- package/src/chat/outbound.ts +821 -0
- package/src/claude/agents-inject.ts +266 -0
- package/src/claude/alias.ts +149 -0
- package/src/claude/auth-detect.ts +229 -0
- package/src/claude/auth-mode-migration.ts +32 -0
- package/src/claude/auth-mode.ts +62 -0
- package/src/claude/context-windows.ts +205 -0
- package/src/claude/desktop-3p-guard.ts +35 -0
- package/src/claude/desktop-3p-paths.ts +84 -0
- package/src/claude/desktop-3p.ts +615 -0
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/gateway-cache.ts +107 -0
- package/src/claude/inbound-debug.ts +163 -0
- package/src/claude/inbound.ts +578 -0
- package/src/claude/model-info.ts +174 -0
- package/src/claude/outbound.ts +926 -0
- package/src/cli/access.ts +108 -0
- package/src/cli/account-api.ts +302 -0
- package/src/cli/account-auth.ts +250 -0
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +737 -0
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +299 -0
- package/src/cli/agent-driven.ts +70 -0
- package/src/cli/agent.ts +290 -0
- package/src/cli/catalog-prewarm.ts +27 -0
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +213 -0
- package/src/cli/claude.ts +355 -0
- package/src/cli/codex-log-guard-doctor.ts +103 -0
- package/src/cli/codex-shim-autorestore.ts +47 -0
- package/src/cli/codex-shim-readiness.ts +76 -0
- package/src/cli/combo.ts +127 -0
- package/src/cli/config-command.ts +209 -0
- package/src/cli/debug.ts +228 -0
- package/src/cli/dispatch.ts +585 -0
- package/src/cli/doctor.ts +1202 -0
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +213 -0
- package/src/cli/help.ts +101 -0
- package/src/cli/index.ts +973 -0
- package/src/cli/init.ts +211 -0
- package/src/cli/integrations.ts +260 -0
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/lab.ts +607 -0
- package/src/cli/launcher-context.ts +77 -0
- package/src/cli/minimax.ts +497 -0
- package/src/cli/models-runtime.ts +245 -0
- package/src/cli/models.ts +422 -0
- package/src/cli/observe.ts +206 -0
- package/src/cli/opencode.ts +588 -0
- package/src/cli/provider-replit.ts +232 -0
- package/src/cli/provider-runtime.ts +179 -0
- package/src/cli/provider.ts +492 -0
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +422 -0
- package/src/cli/replit-gateway-key-input.ts +138 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +328 -0
- package/src/cli/star-prompt.ts +211 -0
- package/src/cli/status-oauth.ts +78 -0
- package/src/cli/status.ts +328 -0
- package/src/cli/system-command.ts +112 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +199 -0
- package/src/cli/v2.ts +268 -0
- package/src/cli.ts +10 -0
- package/src/clients/config-export.ts +1704 -0
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-label.ts +47 -0
- package/src/codex/account-lifecycle.ts +172 -0
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +195 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/account-runtime-state.ts +31 -0
- package/src/codex/account-store.ts +544 -0
- package/src/codex/account-usability.ts +43 -0
- package/src/codex/admission.ts +256 -0
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/agent-roles-sync.ts +225 -0
- package/src/codex/agent-roles.ts +238 -0
- package/src/codex/app-server-processes.ts +1143 -0
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +2147 -0
- package/src/codex/auth-collision.ts +109 -0
- package/src/codex/auth-context.ts +665 -0
- package/src/codex/autostart-health.ts +156 -0
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +436 -0
- package/src/codex/catalog/bundled.ts +549 -0
- package/src/codex/catalog/effort.ts +446 -0
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +664 -0
- package/src/codex/catalog/native-models.ts +72 -0
- package/src/codex/catalog/parsing.ts +650 -0
- package/src/codex/catalog/provider-fetch.ts +2064 -0
- package/src/codex/catalog/sync.ts +1883 -0
- package/src/codex/catalog-admission.ts +199 -0
- package/src/codex/catalog-refresh-status.ts +105 -0
- package/src/codex/catalog-write-serialization.ts +242 -0
- package/src/codex/catalog.ts +14 -0
- package/src/codex/codex-write-lock.ts +384 -0
- package/src/codex/convergence-types.ts +614 -0
- package/src/codex/convergence.ts +651 -0
- package/src/codex/coordinator-doctor.ts +332 -0
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/desired-state.ts +230 -0
- package/src/codex/exec-invocation.ts +22 -0
- package/src/codex/features.ts +1566 -0
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +407 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-migration-guardian.ts +108 -0
- package/src/codex/history-provider.ts +979 -0
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +220 -0
- package/src/codex/home.ts +206 -0
- package/src/codex/inject-coordination.ts +290 -0
- package/src/codex/inject.ts +1733 -0
- package/src/codex/injected-marker.ts +106 -0
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +225 -0
- package/src/codex/log-guard/inspect.ts +506 -0
- package/src/codex/log-guard/lock.ts +150 -0
- package/src/codex/log-guard/maintenance.ts +403 -0
- package/src/codex/log-guard/path-safety.ts +88 -0
- package/src/codex/log-guard/policy.ts +44 -0
- package/src/codex/log-guard/processes.ts +205 -0
- package/src/codex/log-guard/protection.ts +489 -0
- package/src/codex/log-guard/sqlite-errors.ts +9 -0
- package/src/codex/main-account-cache.ts +56 -0
- package/src/codex/main-account.ts +68 -0
- package/src/codex/management-convergence.ts +167 -0
- package/src/codex/model-cache.ts +273 -0
- package/src/codex/model-entitlements.ts +353 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +178 -0
- package/src/codex/native-main-lock-file.ts +162 -0
- package/src/codex/native-main-owner.ts +329 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1531 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +492 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +682 -0
- package/src/codex/paths.ts +144 -0
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +40 -0
- package/src/codex/plugins-doctor.ts +242 -0
- package/src/codex/pool-rotation.ts +295 -0
- package/src/codex/project-config-warnings.ts +425 -0
- package/src/codex/prompt-journal.ts +352 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +298 -0
- package/src/codex/quota.ts +573 -0
- package/src/codex/refresh.ts +62 -0
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +1888 -0
- package/src/codex/runtime.ts +659 -0
- package/src/codex/shim.ts +2170 -0
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +784 -0
- package/src/codex/sync.ts +319 -0
- package/src/codex/transition-state.ts +612 -0
- package/src/codex/upstream-host-health.ts +368 -0
- package/src/codex/user-identity.ts +557 -0
- package/src/codex/warmup.ts +298 -0
- package/src/codex/websocket-registry.ts +100 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +160 -0
- package/src/combos/index.ts +45 -0
- package/src/combos/request.ts +94 -0
- package/src/combos/resolve.ts +232 -0
- package/src/combos/types.ts +398 -0
- package/src/config/provider-name.ts +24 -0
- package/src/config.ts +4041 -0
- package/src/fork/register.ts +3 -0
- package/src/generated/compatibility-version.json +3116 -0
- package/src/generated/model-metadata.ts +106 -0
- package/src/github/star-state.ts +203 -0
- package/src/grok/inject.ts +530 -0
- package/src/grok/inspect.ts +45 -0
- package/src/grok/status.ts +121 -0
- package/src/grok/sync.ts +66 -0
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +149 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +955 -0
- package/src/images/plan.ts +143 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/index.ts +22 -0
- package/src/integrations/config-io.ts +269 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/mutation-flight.ts +71 -0
- package/src/integrations/native/ownership-preflight.ts +202 -0
- package/src/integrations/omp-yaml-source.ts +358 -0
- package/src/integrations/owned-refresh.ts +74 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +159 -0
- package/src/integrations/serialize.ts +314 -0
- package/src/integrations/state.ts +361 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +691 -0
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +499 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +64 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +164 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +68 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +214 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +229 -0
- package/src/lab/events/validate.ts +781 -0
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +489 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +54 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +310 -0
- package/src/lab/ledger/store.ts +532 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +336 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +118 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +301 -0
- package/src/lab/observe/from-live.ts +117 -0
- package/src/lab/paths.ts +153 -0
- package/src/lab/projection/rebuild.ts +495 -0
- package/src/lab/projection/schema.ts +135 -0
- package/src/lab/projection/verdicts.ts +474 -0
- package/src/lab/projection/verification.ts +412 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +243 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lab/query/catalog.ts +101 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +45 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +112 -0
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +74 -0
- package/src/lib/abort.ts +146 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +195 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bounded-body.ts +346 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +184 -0
- package/src/lib/bun-stream-caps.ts +130 -0
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/config-ownership.ts +364 -0
- package/src/lib/crash-guard.ts +344 -0
- package/src/lib/debug-log-buffer.ts +83 -0
- package/src/lib/debug-settings.ts +108 -0
- package/src/lib/debug.ts +31 -0
- package/src/lib/destination-policy.ts +380 -0
- package/src/lib/errors.ts +406 -0
- package/src/lib/eventstream-decoder.ts +253 -0
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/gcp-adc.ts +341 -0
- package/src/lib/injection-debug-log.ts +58 -0
- package/src/lib/lab-activation.ts +223 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +56 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/lab-passive-linker-registration.ts +26 -0
- package/src/lib/local-management-attestation.ts +51 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/local-provider-reload-contract.ts +100 -0
- package/src/lib/open-url.ts +25 -0
- package/src/lib/optional-shutdown-hooks.ts +57 -0
- package/src/lib/pinned-http.ts +270 -0
- package/src/lib/privacy.ts +20 -0
- package/src/lib/process-control.ts +168 -0
- package/src/lib/provider-outbound.ts +210 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/redact.ts +521 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/service-secrets.ts +25 -0
- package/src/lib/shadow-call.ts +61 -0
- package/src/lib/sidecar-tracker.ts +52 -0
- package/src/lib/sse-decoder.ts +364 -0
- package/src/lib/state-store-registrations.ts +119 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/token-estimate.ts +86 -0
- package/src/lib/tool-argument-integers.ts +202 -0
- package/src/lib/translator-budget.ts +400 -0
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-reachability.ts +95 -0
- package/src/lib/upstream-retry.ts +392 -0
- package/src/lib/win-exec.ts +115 -0
- package/src/lib/win-paths.ts +68 -0
- package/src/lib/windows-atomic-replace.ts +156 -0
- package/src/lib/windows-elevation.ts +773 -0
- package/src/lib/windows-secret-acl.ts +854 -0
- package/src/lib/windows-service-wrappers.ts +72 -0
- package/src/lib/windows-text.ts +106 -0
- package/src/lib/windows-user-principal.ts +341 -0
- package/src/lib/winsw.ts +403 -0
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic-routing.ts +594 -0
- package/src/oauth/anthropic.ts +188 -0
- package/src/oauth/antigravity-routing.ts +151 -0
- package/src/oauth/callback-server.ts +300 -0
- package/src/oauth/chatgpt.ts +161 -0
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +252 -0
- package/src/oauth/github-copilot.ts +428 -0
- package/src/oauth/google-antigravity.ts +262 -0
- package/src/oauth/health.ts +407 -0
- package/src/oauth/index.ts +1504 -0
- package/src/oauth/key-providers.ts +124 -0
- package/src/oauth/kimi.ts +227 -0
- package/src/oauth/kiro-credentials.ts +726 -0
- package/src/oauth/kiro.ts +621 -0
- package/src/oauth/local-token-detect.ts +130 -0
- package/src/oauth/log.ts +50 -0
- package/src/oauth/login-cli.ts +223 -0
- package/src/oauth/nous.ts +798 -0
- package/src/oauth/pkce.ts +15 -0
- package/src/oauth/store.ts +728 -0
- package/src/oauth/token-guardian.ts +309 -0
- package/src/oauth/types.ts +62 -0
- package/src/oauth/xai.ts +241 -0
- package/src/providers/alibaba-region-backup.ts +75 -0
- package/src/providers/alibaba-region-migration.ts +156 -0
- package/src/providers/alibaba-region-startup.ts +36 -0
- package/src/providers/antigravity-models.ts +695 -0
- package/src/providers/antigravity-quota.ts +216 -0
- package/src/providers/api-keys.ts +140 -0
- package/src/providers/base-url-choices.ts +74 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +144 -0
- package/src/providers/context-cap.ts +82 -0
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/derive.ts +586 -0
- package/src/providers/fastwire.ts +501 -0
- package/src/providers/free-directory.ts +187 -0
- package/src/providers/github-copilot-transport.ts +56 -0
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +271 -0
- package/src/providers/kiro-models.ts +67 -0
- package/src/providers/label.ts +19 -0
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +449 -0
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -0
- package/src/providers/openai-sidecar.ts +243 -0
- package/src/providers/openai-tier-startup.ts +56 -0
- package/src/providers/openai-tiers.ts +423 -0
- package/src/providers/openai-virtual-models.ts +83 -0
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/openrouter-routing.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +185 -0
- package/src/providers/quota.ts +2345 -0
- package/src/providers/registry.ts +2918 -0
- package/src/providers/replit/constants.ts +27 -0
- package/src/providers/replit/derive.ts +85 -0
- package/src/providers/replit/headers.ts +28 -0
- package/src/providers/replit/origin.ts +55 -0
- package/src/providers/replit/pair-install-response.ts +72 -0
- package/src/providers/replit/probe.ts +199 -0
- package/src/providers/replit/setup.ts +350 -0
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +103 -0
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/providers/xai-responses-opt-in.ts +15 -0
- package/src/providers/xai-transport.ts +148 -0
- package/src/reasoning-effort.ts +183 -0
- package/src/responses/compaction.ts +142 -0
- package/src/responses/custom-tool-compat.ts +266 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/namespace-tool-compat.ts +355 -0
- package/src/responses/parser.ts +838 -0
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/reasoning-envelope.ts +60 -0
- package/src/responses/reasoning-replay-cache.ts +426 -0
- package/src/responses/schema.ts +165 -0
- package/src/responses/spill-store.ts +459 -0
- package/src/responses/state.ts +1433 -0
- package/src/responses/thought-signature-replay.ts +347 -0
- package/src/responses/tool-groups.ts +19 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/router.ts +761 -0
- package/src/routing/analytics.ts +378 -0
- package/src/routing/capability.ts +244 -0
- package/src/routing/compatibility/assemble.ts +73 -0
- package/src/routing/compatibility/behavior.ts +278 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/provider-slot.ts +56 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +495 -0
- package/src/routing/health.ts +412 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +605 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +547 -0
- package/src/routing/quota.ts +145 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +776 -0
- package/src/server/adapter-resolve.ts +53 -0
- package/src/server/auth-cors.ts +751 -0
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +442 -0
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +426 -0
- package/src/server/claude-messages.ts +1030 -0
- package/src/server/direct-local-http.ts +347 -0
- package/src/server/effort-policy.ts +190 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +152 -0
- package/src/server/image-retry.ts +42 -0
- package/src/server/images.ts +568 -0
- package/src/server/index.ts +1813 -0
- package/src/server/lifecycle.ts +498 -0
- package/src/server/live.ts +717 -0
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/local-provider-reload-client.ts +137 -0
- package/src/server/management/agent-settings-routes.ts +1433 -0
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/api-key-usage.ts +193 -0
- package/src/server/management/body.ts +41 -0
- package/src/server/management/combo-routes.ts +263 -0
- package/src/server/management/config-routes.ts +835 -0
- package/src/server/management/context.ts +113 -0
- package/src/server/management/integration-routes.ts +498 -0
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +563 -0
- package/src/server/management/logs-usage-routes.ts +586 -0
- package/src/server/management/model-routes.ts +560 -0
- package/src/server/management/model-rows.ts +163 -0
- package/src/server/management/native-integration-routes.ts +769 -0
- package/src/server/management/oauth-account-routes.ts +637 -0
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +1033 -0
- package/src/server/management/replit-provider-routes.ts +86 -0
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +385 -0
- package/src/server/management/shared.ts +286 -0
- package/src/server/management/sidebar-routes.ts +106 -0
- package/src/server/management/storage-log-guard-routes.ts +186 -0
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +435 -0
- package/src/server/management/system-routes.ts +194 -0
- package/src/server/management/usage-summary-cache.ts +94 -0
- package/src/server/management/vision-sidecar-options.ts +167 -0
- package/src/server/management/web-search-sidecar-options.ts +120 -0
- package/src/server/management-api.ts +314 -0
- package/src/server/management-auth.ts +482 -0
- package/src/server/memory-watchdog.ts +156 -0
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/port-reclaim.ts +307 -0
- package/src/server/ports.ts +156 -0
- package/src/server/proxy-liveness.ts +328 -0
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +353 -0
- package/src/server/relay.ts +1209 -0
- package/src/server/request-decompress.ts +239 -0
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +1259 -0
- package/src/server/responses/agent-task-recovery-cache.ts +143 -0
- package/src/server/responses/agent-task-recovery.ts +465 -0
- package/src/server/responses/collaboration.ts +551 -0
- package/src/server/responses/compact.ts +771 -0
- package/src/server/responses/core.ts +5389 -0
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/encrypted-payload.ts +331 -0
- package/src/server/responses/fetch-helpers.ts +232 -0
- package/src/server/responses/input-admission.ts +185 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/passthrough-error.ts +78 -0
- package/src/server/responses/policy-fallback.ts +178 -0
- package/src/server/responses/responses-field-backfill.ts +251 -0
- package/src/server/responses/terminal-guard.ts +251 -0
- package/src/server/responses/upstream-error.ts +53 -0
- package/src/server/responses/ws-upstream.ts +308 -0
- package/src/server/responses-custom-tool-repair.ts +282 -0
- package/src/server/responses-image-gen-repair.ts +132 -0
- package/src/server/responses-item-id-repair.ts +272 -0
- package/src/server/responses-json-events.ts +90 -0
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/responses-terminal-repair.ts +342 -0
- package/src/server/responses-tool-search-repair.ts +267 -0
- package/src/server/responses-undeclared-tool-guard.ts +153 -0
- package/src/server/responses.ts +25 -0
- package/src/server/search.ts +201 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +263 -0
- package/src/server/startup-action-control.ts +315 -0
- package/src/server/startup-health-cache.ts +131 -0
- package/src/server/system-env.ts +484 -0
- package/src/server/windows-tcp-drop.ts +184 -0
- package/src/server/windows-tray-control.ts +41 -0
- package/src/server/ws-bridge.ts +472 -0
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +3575 -0
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/stall-timeout.ts +20 -0
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +3085 -0
- package/src/storage/policy-job.ts +457 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +59 -0
- package/src/storage/policy.ts +527 -0
- package/src/storage/restore-job.ts +299 -0
- package/src/storage/restore-worker.ts +58 -0
- package/src/storage/scanner.ts +238 -0
- package/src/storage/storage-mutation-coordinator.ts +139 -0
- package/src/storage/worker-lifecycle.ts +215 -0
- package/src/tray/assets/opencodex-tray-offline.ico +0 -0
- package/src/tray/assets/opencodex-tray-online.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning.ico +0 -0
- package/src/tray/assets/opencodex-tray.png +0 -0
- package/src/tray/windows-tray.ps1 +364 -0
- package/src/tray/windows.ts +757 -0
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +876 -0
- package/src/types/provider.ts +545 -0
- package/src/types/request.ts +384 -0
- package/src/types/tools.ts +131 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +106 -0
- package/src/update/badge.ts +72 -0
- package/src/update/index.ts +415 -0
- package/src/update/job.ts +1887 -0
- package/src/update/notify.ts +263 -0
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +259 -0
- package/src/update/tray-update-plan.d.mts +18 -0
- package/src/update/tray-update-plan.mjs +38 -0
- package/src/usage/cost.ts +625 -0
- package/src/usage/debug.ts +97 -0
- package/src/usage/expected-prices.ts +416 -0
- package/src/usage/log.ts +1223 -0
- package/src/usage/summary.ts +753 -0
- package/src/usage/totals.ts +14 -0
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/anthropic-describe.ts +189 -0
- package/src/vision/backends.ts +97 -0
- package/src/vision/describe.ts +131 -0
- package/src/vision/eligibility.ts +250 -0
- package/src/vision/index.ts +681 -0
- package/src/vision/reasoning.ts +55 -0
- package/src/vision/routed-describe.ts +175 -0
- package/src/vision/timeout-bounds.ts +9 -0
- package/src/web-search/anthropic-executor.ts +195 -0
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +88 -0
- package/src/web-search/executor.ts +113 -0
- package/src/web-search/format-result.ts +89 -0
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +331 -0
- package/src/web-search/loop.ts +896 -0
- package/src/web-search/parse.ts +315 -0
- package/src/web-search/progress-stream.ts +342 -0
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/synthetic-tool.ts +47 -0
- package/src/web-search/xai-executor.ts +219 -0
|
@@ -0,0 +1,1377 @@
|
|
|
1
|
+
import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base";
|
|
2
|
+
import { debugDroppedFrame } from "../lib/debug";
|
|
3
|
+
import { createToolCallIdAllocator } from "./tool-call-id";
|
|
4
|
+
import { createImageBudget, materializeInlineImage, MAX_ENCODED_BYTES_PER_IMAGE, artifactHttpUrl } from "../images/artifacts";
|
|
5
|
+
import type {
|
|
6
|
+
AdapterEvent,
|
|
7
|
+
OcxAssistantMessage,
|
|
8
|
+
OcxContentPart,
|
|
9
|
+
OcxParsedRequest,
|
|
10
|
+
OcxProviderConfig,
|
|
11
|
+
OcxProviderOpaqueToolCallMetadata,
|
|
12
|
+
OcxTextContent,
|
|
13
|
+
OcxToolCall,
|
|
14
|
+
OcxToolResultMessage,
|
|
15
|
+
OcxUsage,
|
|
16
|
+
} from "../types";
|
|
17
|
+
import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolAllowedByChoice } from "../types";
|
|
18
|
+
import { contentPartsToText, parseDataUrl } from "./image";
|
|
19
|
+
import { getVertexAccessToken } from "../lib/gcp-adc";
|
|
20
|
+
import { fetchAntigravityWithRetry, fetchVertexWithRetry } from "./google-http";
|
|
21
|
+
import { safeAntigravityHttpErrorMessage, safeVertexHttpErrorMessage } from "./google-errors";
|
|
22
|
+
import { isVertexTruncatedTurn, vertexTruncationErrorMessage } from "./google-truncation";
|
|
23
|
+
import { ANTIGRAVITY_REQUEST_UA, antigravitySessionId, isLikelyRealThoughtSignature, sanitizeAntigravityClaudeSignatures } from "./google-antigravity-wire";
|
|
24
|
+
import { repairGoogleToolPairs, stripTrailingClaudePrefill } from "./google-antigravity-tools";
|
|
25
|
+
import { canonicalAntigravityHttpsHost, isAntigravityHttpsHost } from "./google-antigravity-hosts";
|
|
26
|
+
import { compileGoogleWireBody } from "./google-wire-compiler";
|
|
27
|
+
import { identifyRoutedModel } from "./identity";
|
|
28
|
+
import { antigravityUsesReplayCache, applyAntigravityReplay, clearAntigravityReplay, observeAntigravityReplay } from "./google-antigravity-replay";
|
|
29
|
+
import { resolveAntigravityEffortWireModel } from "../providers/antigravity-models";
|
|
30
|
+
import { googleVertexLocationConfigError } from "../providers/google-vertex-location";
|
|
31
|
+
import { lookupReplayThoughtSignature } from "../responses/thought-signature-replay";
|
|
32
|
+
import {
|
|
33
|
+
isTranslatorBudgetExceededError,
|
|
34
|
+
releaseTranslatedEvent,
|
|
35
|
+
retainTranslatedEvent,
|
|
36
|
+
retainTranslatedEventBatch,
|
|
37
|
+
type TranslatorBudget,
|
|
38
|
+
} from "../lib/translator-budget";
|
|
39
|
+
import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
|
|
40
|
+
import { configuredReasoningEfforts, mapReasoningEffort } from "../reasoning-effort";
|
|
41
|
+
|
|
42
|
+
// Google-family models (Gemini/Vertex/Antigravity) tend to emit long running commentary between
|
|
43
|
+
// tool calls. This steers them to keep the BETWEEN-STEP text to one line and reason internally
|
|
44
|
+
// while still driving tools to completion. The FINAL answer is explicitly exempt so task output is
|
|
45
|
+
// not truncated. Appended to systemInstruction for the `google` adapter only, so non-Google
|
|
46
|
+
// providers are unaffected.
|
|
47
|
+
const GOOGLE_BREVITY_INSTRUCTION = [
|
|
48
|
+
"Output style for this session:",
|
|
49
|
+
"- While you are still working (between tool calls), keep any text you emit to a single short line; do not narrate at length.",
|
|
50
|
+
"- Do detailed reasoning internally, not as visible intermediate output.",
|
|
51
|
+
"- Prefer taking the next tool action over explaining; keep calling tools until the task is complete.",
|
|
52
|
+
"- This applies only to intermediate progress text. Your final answer after the work is done is exempt: write it in full and at whatever length the task requires.",
|
|
53
|
+
].join("\n");
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Some Google direct deployments expose current Gemini Flash generations with a `-tiered`
|
|
57
|
+
* wire suffix (`gemini-3.7-flash` -> `gemini-3.7-flash-tiered`). Keep the picker-visible id
|
|
58
|
+
* stable and make the mapping configurable for deployments that still serve the bare id.
|
|
59
|
+
*/
|
|
60
|
+
const GEMINI_DIRECT_WIRE_RENAMES: Record<string, string> = {
|
|
61
|
+
"gemini-3.7-flash": "gemini-3.7-flash-tiered",
|
|
62
|
+
"gemini-3.6-flash": "gemini-3.6-flash-tiered",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
function resolveDirectGeminiWireModelId(modelId: string, applyRenames: boolean): string {
|
|
66
|
+
if (!applyRenames) return modelId;
|
|
67
|
+
return Object.hasOwn(GEMINI_DIRECT_WIRE_RENAMES, modelId)
|
|
68
|
+
? GEMINI_DIRECT_WIRE_RENAMES[modelId]!
|
|
69
|
+
: modelId;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Vertex API key: provider.apiKey if it looks real (not a sentinel), else GOOGLE_CLOUD_API_KEY env. */
|
|
73
|
+
function resolveVertexApiKey(optKey?: string): string | undefined {
|
|
74
|
+
const realKey = optKey && !optKey.startsWith("<") && optKey !== "N/A" ? optKey : undefined;
|
|
75
|
+
return realKey || process.env.GOOGLE_CLOUD_API_KEY;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Prefer Codex's stable opaque thread key; retain the existing deterministic fallback for clients
|
|
79
|
+
* that omit it. The replay store hashes this value and never retains the raw session identifier. */
|
|
80
|
+
function vertexReplaySessionId(parsed: OcxParsedRequest): string {
|
|
81
|
+
const threadId = parsed._clientThreadId?.trim();
|
|
82
|
+
return threadId || antigravitySessionId(parsed);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Stable tool-call id for the Gemini wire `functionCall.id` / `functionResponse.id` fields.
|
|
87
|
+
*
|
|
88
|
+
* Gemini treats these ids as optional and pairs a call with its response by id when present, so
|
|
89
|
+
* emitting them is harmless for Gemini models. They are REQUIRED, however, for Claude-on-Antigravity:
|
|
90
|
+
* the backend converts the Gemini-shaped request into Anthropic `messages`, mapping
|
|
91
|
+
* `functionCall.id -> tool_use.id` and `functionResponse.id -> tool_result.tool_use_id`. With no id
|
|
92
|
+
* the conversion fails upstream with `messages.N.content.M.tool_use.id: Field required` (HTTP 400).
|
|
93
|
+
*
|
|
94
|
+
* Anthropic's `tool_use.id` only accepts `[a-zA-Z0-9_-]`, so non-conforming characters are mapped to
|
|
95
|
+
* `_`. To keep the mapping injective (so two distinct raw ids like `call:a` and `call/a` cannot
|
|
96
|
+
* collide into one `tool_use.id` within a request), a short hash of the original raw id is appended
|
|
97
|
+
* whenever any character had to be rewritten. The transform is deterministic, so a call id and its
|
|
98
|
+
* matching result id — equal at the source, since Codex pairs them — still normalize identically and
|
|
99
|
+
* the call/response pairing is preserved. Returns `undefined` for an empty id so the caller omits the
|
|
100
|
+
* field entirely rather than inventing a non-matching one.
|
|
101
|
+
*/
|
|
102
|
+
// Aliasing the stateless transform here would reintroduce the collision it cannot prevent:
|
|
103
|
+
// a rewritten id can equal a distinct raw id that already conforms. Use a request-scoped
|
|
104
|
+
// allocator, exactly as the Anthropic adapter does, so call/response pairing stays injective.
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Inline image parts (Gemini `inline_data`) extracted from tool-result content. Only base64 data URLs
|
|
108
|
+
* can be inlined; a remote URL has no mime type we can supply, so it is skipped here (the textual
|
|
109
|
+
* result already carries an "[image]" marker via contentPartsToText).
|
|
110
|
+
*/
|
|
111
|
+
function toolResultImageParts(content: string | OcxContentPart[]): unknown[] {
|
|
112
|
+
if (typeof content === "string") return [];
|
|
113
|
+
const parts: unknown[] = [];
|
|
114
|
+
for (const p of content) {
|
|
115
|
+
if (p.type !== "image") continue;
|
|
116
|
+
const data = parseDataUrl(p.imageUrl);
|
|
117
|
+
if (data) parts.push({ inline_data: { mime_type: data.mediaType, data: data.base64 } });
|
|
118
|
+
}
|
|
119
|
+
return parts;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Antigravity translates these Gemini `contents` into Anthropic `messages` for Claude models, and
|
|
124
|
+
* Anthropic rejects a text block whose `text` is empty or absent. An empty Gemini text part reaches
|
|
125
|
+
* that upstream as `{"type":"text"}` — a proto3 empty string is omitted from the translated JSON —
|
|
126
|
+
* and 400s with `messages.N.content.M.text.text: Field required` (issue #420). An empty `parts: []`
|
|
127
|
+
* model turn fails the same way. Gemini itself accepts both shapes, which is why this only ever
|
|
128
|
+
* surfaced on Claude-on-Antigravity; the guard lives here because this is where the parts are
|
|
129
|
+
* built. Mirrors the Anthropic adapter's own empty-block guard (src/adapters/anthropic.ts).
|
|
130
|
+
*/
|
|
131
|
+
const GEMINI_EMPTY_PLACEHOLDER = "(empty)";
|
|
132
|
+
const GEMINI_EMPTY_TOOL_OUTPUT_PLACEHOLDER = "(empty tool output)";
|
|
133
|
+
const GEMINI_MISSING_TOOL_RESULT = "[missing tool_result for this tool_use in history]";
|
|
134
|
+
|
|
135
|
+
/** A Gemini text part, or undefined when the value cannot form a valid non-empty text block. */
|
|
136
|
+
function geminiTextPart(text: unknown): { text: string } | undefined {
|
|
137
|
+
return typeof text === "string" && text.length > 0 ? { text } : undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Text for `functionResponse.response.result`. `contentPartsToText` collapses an empty array — or one
|
|
142
|
+
* holding only empty text — to its "[image]" marker, which would claim an image the turn does not
|
|
143
|
+
* actually carry (`toolResultImageParts` adds none). Fall back to the placeholder unless the content
|
|
144
|
+
* has something representable.
|
|
145
|
+
*/
|
|
146
|
+
function geminiToolResultText(content: string | OcxContentPart[]): string {
|
|
147
|
+
if (typeof content === "string") return content || GEMINI_EMPTY_TOOL_OUTPUT_PLACEHOLDER;
|
|
148
|
+
const hasContent = content.some(p => p.type === "image" || (typeof p.text === "string" && p.text.length > 0));
|
|
149
|
+
return hasContent ? contentPartsToText(content) : GEMINI_EMPTY_TOOL_OUTPUT_PLACEHOLDER;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function geminiToolResultParts(
|
|
153
|
+
msg: OcxToolResultMessage,
|
|
154
|
+
wireName: string,
|
|
155
|
+
wireCallId: string,
|
|
156
|
+
): unknown[] {
|
|
157
|
+
const functionResponse: Record<string, unknown> = {
|
|
158
|
+
name: wireName,
|
|
159
|
+
response: { result: geminiToolResultText(msg.content) },
|
|
160
|
+
id: wireCallId,
|
|
161
|
+
};
|
|
162
|
+
return [{ functionResponse }, ...toolResultImageParts(msg.content)];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function geminiMissingToolResultPart(wireName: string, wireCallId: string): unknown {
|
|
166
|
+
return {
|
|
167
|
+
functionResponse: {
|
|
168
|
+
name: wireName,
|
|
169
|
+
response: { result: GEMINI_MISSING_TOOL_RESULT },
|
|
170
|
+
id: wireCallId,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function geminiUnrepresentableToolCallPart(tc: OcxToolCall, wireName: string): unknown {
|
|
176
|
+
const args = typeof tc.arguments === "string" ? tc.arguments : JSON.stringify(tc.arguments);
|
|
177
|
+
return { text: `[tool_use without a usable id: ${wireName}]\n${args}` };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function geminiOrphanToolResultParts(msg: OcxToolResultMessage): unknown[] {
|
|
181
|
+
const label = msg.toolName ? `${msg.toolName} (${msg.toolCallId})` : msg.toolCallId;
|
|
182
|
+
return [
|
|
183
|
+
{ text: `[tool_result without adjacent tool_use: ${label}]\n${geminiToolResultText(msg.content)}` },
|
|
184
|
+
...toolResultImageParts(msg.content),
|
|
185
|
+
];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function messagesToGeminiFormat(
|
|
189
|
+
parsed: OcxParsedRequest,
|
|
190
|
+
identityModelId: string,
|
|
191
|
+
repairToolPairs: boolean,
|
|
192
|
+
): { systemInstruction?: unknown; contents: unknown[] } {
|
|
193
|
+
// Neutralize Codex's GPT-5 identity line (Gemini/Antigravity share this path) so a routed model
|
|
194
|
+
// never misreports as GPT-5/OpenAI, and never leaks the proxy identity upstream.
|
|
195
|
+
const toolCatalogNudge = buildNonOpenAIToolCatalogNudgeForTools(parsed.context.tools, parsed.options.toolChoice);
|
|
196
|
+
const systemText = identifyRoutedModel([
|
|
197
|
+
...(parsed.context.systemPrompt ?? []),
|
|
198
|
+
...(toolCatalogNudge ? [toolCatalogNudge] : []),
|
|
199
|
+
GOOGLE_BREVITY_INSTRUCTION,
|
|
200
|
+
].join("\n\n"), identityModelId);
|
|
201
|
+
const systemInstruction = { parts: [{ text: systemText }] };
|
|
202
|
+
|
|
203
|
+
const contents: unknown[] = [];
|
|
204
|
+
const messages = repairGoogleToolPairs(parsed.context.messages, { dropUnmatchedCalls: repairToolPairs });
|
|
205
|
+
|
|
206
|
+
const callIds = createToolCallIdAllocator();
|
|
207
|
+
for (const msg of messages) {
|
|
208
|
+
if (msg.role === "assistant") {
|
|
209
|
+
for (const part of (msg as OcxAssistantMessage).content) {
|
|
210
|
+
if (part.type === "toolCall") callIds.reserve((part as OcxToolCall).id);
|
|
211
|
+
}
|
|
212
|
+
} else if (msg.role === "toolResult") {
|
|
213
|
+
callIds.reserve((msg as OcxToolResultMessage).toolCallId);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
for (let i = 0; i < parsed.context.messages.length; i++) {
|
|
217
|
+
const msg = parsed.context.messages[i];
|
|
218
|
+
switch (msg.role) {
|
|
219
|
+
case "user":
|
|
220
|
+
case "developer": {
|
|
221
|
+
if (typeof msg.content === "string") {
|
|
222
|
+
contents.push({ role: "user", parts: [{ text: msg.content || GEMINI_EMPTY_PLACEHOLDER }] });
|
|
223
|
+
} else {
|
|
224
|
+
const parts: unknown[] = [];
|
|
225
|
+
for (const p of msg.content as OcxContentPart[]) {
|
|
226
|
+
if (p.type === "image") {
|
|
227
|
+
const data = parseDataUrl(p.imageUrl);
|
|
228
|
+
// Gemini takes base64 via inline_data; a remote URL needs a mime type we don't have, so
|
|
229
|
+
// fall back to a short marker rather than inlining the URL as a huge text blob.
|
|
230
|
+
parts.push(data ? { inline_data: { mime_type: data.mediaType, data: data.base64 } } : { text: `[image: ${p.imageUrl}]` });
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
// Drop empty/malformed text instead of emitting `{ text: "" }` or a bare `{}` part.
|
|
234
|
+
const textPart = geminiTextPart(p.text);
|
|
235
|
+
if (textPart) parts.push(textPart);
|
|
236
|
+
}
|
|
237
|
+
contents.push({ role: "user", parts: parts.length > 0 ? parts : [{ text: GEMINI_EMPTY_PLACEHOLDER }] });
|
|
238
|
+
}
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
case "assistant": {
|
|
242
|
+
const aMsg = msg as OcxAssistantMessage;
|
|
243
|
+
const parts: unknown[] = [];
|
|
244
|
+
const toolCalls: Array<{ wireCallId: string; wireName: string }> = [];
|
|
245
|
+
for (const p of aMsg.content) {
|
|
246
|
+
if (p.type === "text") {
|
|
247
|
+
const textPart = geminiTextPart((p as OcxTextContent).text);
|
|
248
|
+
if (textPart) parts.push(textPart);
|
|
249
|
+
} else if (p.type === "toolCall") {
|
|
250
|
+
const tc = p as OcxToolCall;
|
|
251
|
+
// Preserve the thought signature on the function-call part so Antigravity/Gemini-3
|
|
252
|
+
// reasoning continuity survives history-driven (stateless) turns, not just same-process
|
|
253
|
+
// streaming covered by the replay cache. Only forward a REAL upstream signature — the
|
|
254
|
+
// Responses parser also stashes synthetic item ids (`fc_...`) on this field, and sending
|
|
255
|
+
// those as a thoughtSignature breaks continuity (the replay cache supplies the real one).
|
|
256
|
+
const callId = callIds.allocate(tc.id);
|
|
257
|
+
const wireName = namespacedToolName(tc.namespace, tc.name);
|
|
258
|
+
if (callId === undefined) {
|
|
259
|
+
// Claude-on-Antigravity requires a usable id for every translated tool_use. An empty
|
|
260
|
+
// source id cannot be paired safely, so preserve the call as text and let its result
|
|
261
|
+
// follow the same orphan-text path instead of emitting an invalid functionCall.
|
|
262
|
+
parts.push(geminiUnrepresentableToolCallPart(tc, wireName));
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const functionCall: Record<string, unknown> = { name: wireName, args: tc.arguments };
|
|
266
|
+
// Claude-on-Antigravity maps this id to Anthropic `tool_use.id`; without it the upstream
|
|
267
|
+
// conversion 400s. Gemini accepts the optional id and pairs call/response by it.
|
|
268
|
+
functionCall.id = callId;
|
|
269
|
+
toolCalls.push({ wireCallId: callId, wireName });
|
|
270
|
+
const part: Record<string, unknown> = { functionCall };
|
|
271
|
+
// Prefer the metadata that travelled with this exact call; fall back to the legacy
|
|
272
|
+
// field for callers that have not been migrated. Never merge or synthesize.
|
|
273
|
+
// Final fallback (#1926): the durable store, read AT SERIALIZATION TIME. The
|
|
274
|
+
// Responses parser runs before the route/credential scope is bound, so its
|
|
275
|
+
// parse-time lookup can never hit; by the time this adapter serializes, the
|
|
276
|
+
// credential-scoped identity is bound and the durable lookup is meaningful.
|
|
277
|
+
const signature = tc.providerMetadata?.google?.thoughtSignature
|
|
278
|
+
?? tc.thoughtSignature
|
|
279
|
+
?? lookupReplayThoughtSignature(tc.id, parsed._reasoningReplayScope);
|
|
280
|
+
if (isLikelyRealThoughtSignature(signature)) part.thoughtSignature = signature;
|
|
281
|
+
parts.push(part);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// A turn with nothing Gemini can represent (e.g. thinking-only) would serialize as
|
|
285
|
+
// `parts: []`, which the Anthropic translation rejects. Skip it, as the Anthropic
|
|
286
|
+
// adapter does for its own empty assistant content.
|
|
287
|
+
if (parts.length === 0) break;
|
|
288
|
+
contents.push({ role: "model", parts });
|
|
289
|
+
if (toolCalls.length > 0) {
|
|
290
|
+
// Gemini/Claude-on-Antigravity requires one adjacent response batch for the whole
|
|
291
|
+
// function-call turn. Replayed histories can be interrupted, reversed, duplicated, or
|
|
292
|
+
// contain an orphan result; repair only this wire boundary without inventing success.
|
|
293
|
+
const requiredIds = new Set(toolCalls.map(call => call.wireCallId));
|
|
294
|
+
const resultsById = new Map<string, OcxToolResultMessage>();
|
|
295
|
+
const orphanResults: OcxToolResultMessage[] = [];
|
|
296
|
+
let j = i + 1;
|
|
297
|
+
while (j < parsed.context.messages.length && parsed.context.messages[j].role === "toolResult") {
|
|
298
|
+
const result = parsed.context.messages[j] as OcxToolResultMessage;
|
|
299
|
+
const wireResultId = callIds.lookup(result.toolCallId);
|
|
300
|
+
if (wireResultId !== undefined && requiredIds.has(wireResultId) && !resultsById.has(wireResultId)) {
|
|
301
|
+
resultsById.set(wireResultId, result);
|
|
302
|
+
} else {
|
|
303
|
+
orphanResults.push(result);
|
|
304
|
+
}
|
|
305
|
+
j++;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const responseParts: unknown[] = [];
|
|
309
|
+
for (const call of toolCalls) {
|
|
310
|
+
const result = resultsById.get(call.wireCallId);
|
|
311
|
+
if (result) responseParts.push(...geminiToolResultParts(result, call.wireName, call.wireCallId));
|
|
312
|
+
else responseParts.push(geminiMissingToolResultPart(call.wireName, call.wireCallId));
|
|
313
|
+
}
|
|
314
|
+
for (const orphan of orphanResults) {
|
|
315
|
+
responseParts.push(...geminiOrphanToolResultParts(orphan));
|
|
316
|
+
}
|
|
317
|
+
contents.push({ role: "user", parts: responseParts });
|
|
318
|
+
i = j - 1;
|
|
319
|
+
}
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
case "toolResult": {
|
|
323
|
+
// repairGoogleToolPairs already dropped this result from pairing/id allocation, so a
|
|
324
|
+
// lone functionResponse cannot be emitted. Consecutive results after a functionCall
|
|
325
|
+
// batch never reach here (the assistant branch consumes them). Standalone or
|
|
326
|
+
// barrier-delayed results still have to stay visible — especially image-bearing
|
|
327
|
+
// screenshots — as explicit user text rather than vanishing or 400ing CCA.
|
|
328
|
+
contents.push({ role: "user", parts: geminiOrphanToolResultParts(msg as OcxToolResultMessage) });
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return { systemInstruction, contents };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function toolsToGeminiFormat(parsed: OcxParsedRequest): unknown[] | undefined {
|
|
338
|
+
if (!parsed.context.tools?.length) return undefined;
|
|
339
|
+
const allowed = isAllowedToolChoice(parsed.options.toolChoice)
|
|
340
|
+
? new Set(parsed.options.toolChoice.allowedTools)
|
|
341
|
+
: undefined;
|
|
342
|
+
const tools = allowed
|
|
343
|
+
? parsed.context.tools.filter(t => toolAllowedByChoice(t, allowed, parsed.context.tools))
|
|
344
|
+
: parsed.context.tools;
|
|
345
|
+
if (tools.length === 0) return undefined;
|
|
346
|
+
return [{
|
|
347
|
+
functionDeclarations: tools.map(t => ({
|
|
348
|
+
name: namespacedToolName(t.namespace, t.name),
|
|
349
|
+
description: t.description,
|
|
350
|
+
parameters: t.parameters,
|
|
351
|
+
})),
|
|
352
|
+
}];
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Client tool_choice enforcement on the wire. The catalog nudge states the same contract in
|
|
357
|
+
* prose, but without functionCallingConfig the model is free to ignore it. "auto" stays absent
|
|
358
|
+
* so the common case is byte-identical. The allowedTools variant already filters the
|
|
359
|
+
* declarations in toolsToGeminiFormat; only its "required" half needs a wire mode.
|
|
360
|
+
*/
|
|
361
|
+
function toolChoiceToGeminiToolConfig(parsed: OcxParsedRequest): Record<string, unknown> | undefined {
|
|
362
|
+
const choice = parsed.options.toolChoice;
|
|
363
|
+
if (!choice || choice === "auto") return undefined;
|
|
364
|
+
if (choice === "none") return { functionCallingConfig: { mode: "NONE" } };
|
|
365
|
+
if (choice === "required") return { functionCallingConfig: { mode: "ANY" } };
|
|
366
|
+
if (isAllowedToolChoice(choice)) {
|
|
367
|
+
return choice.mode === "required" ? { functionCallingConfig: { mode: "ANY" } } : undefined;
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
functionCallingConfig: {
|
|
371
|
+
mode: "ANY",
|
|
372
|
+
allowedFunctionNames: [resolveToolChoiceWireName(parsed.context.tools, choice.name)],
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function usageFromGemini(usage: Record<string, number> | undefined): OcxUsage | undefined {
|
|
378
|
+
if (!usage) return undefined;
|
|
379
|
+
return {
|
|
380
|
+
inputTokens: usage.promptTokenCount ?? 0,
|
|
381
|
+
outputTokens: usage.candidatesTokenCount ?? 0,
|
|
382
|
+
...(usage.cachedContentTokenCount !== undefined ? { cachedInputTokens: usage.cachedContentTokenCount } : {}),
|
|
383
|
+
...(usage.thoughtsTokenCount !== undefined ? { reasoningOutputTokens: usage.thoughtsTokenCount } : {}),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Cap on the buffered non-streaming response body (100 MiB), matching
|
|
389
|
+
* IMAGES_RESPONSE_MAX_BYTES in src/server/images.ts. Enforced by streaming the
|
|
390
|
+
* body with a hard byte cap before JSON.parse — Content-Length alone is not
|
|
391
|
+
* trusted (missing/lying headers must still reject oversized payloads).
|
|
392
|
+
* Streaming SSE responses also cap each data frame before JSON.parse.
|
|
393
|
+
*/
|
|
394
|
+
const MAX_RESPONSE_BYTES = 100 * 1024 * 1024;
|
|
395
|
+
const MAX_SSE_FRAME_BYTES = MAX_RESPONSE_BYTES;
|
|
396
|
+
let sseFrameMaxBytes = MAX_SSE_FRAME_BYTES;
|
|
397
|
+
|
|
398
|
+
/** Test-only: lower the SSE frame byte cap without allocating a 100 MiB fixture. */
|
|
399
|
+
export function setGoogleSseFrameMaxBytesForTests(bytes?: number): void {
|
|
400
|
+
sseFrameMaxBytes = bytes ?? MAX_SSE_FRAME_BYTES;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function scanSseLineBytes(incompleteLineBytes: number, incoming: Uint8Array): {
|
|
404
|
+
maximum: number;
|
|
405
|
+
residual: number;
|
|
406
|
+
} {
|
|
407
|
+
let lineBytes = incompleteLineBytes;
|
|
408
|
+
let maximum = lineBytes;
|
|
409
|
+
for (let index = 0; index < incoming.length; index++) {
|
|
410
|
+
if (incoming[index] === 0x0a) {
|
|
411
|
+
lineBytes = 0;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
lineBytes += 1;
|
|
415
|
+
maximum = Math.max(maximum, lineBytes);
|
|
416
|
+
}
|
|
417
|
+
return { maximum, residual: lineBytes };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Note: imagen-* models use a different API surface (prediction/image-generation
|
|
421
|
+
// schema) and must NOT be treated as responseModalities-capable Gemini models.
|
|
422
|
+
// Explicit allowlist only — never `/gemini/ && /image/` (resurrects media-gen IDs).
|
|
423
|
+
const IMAGE_CAPABLE_MODELS = new Set([
|
|
424
|
+
"gemini-3.1-flash-image",
|
|
425
|
+
"gemini-2.0-flash-preview-image-generation",
|
|
426
|
+
"gemini-3-pro-image-preview",
|
|
427
|
+
]);
|
|
428
|
+
|
|
429
|
+
function isImageCapableModel(modelId: string): boolean {
|
|
430
|
+
return IMAGE_CAPABLE_MODELS.has(modelId);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Model-visible markdown link for a materialized artifact. Uses the authenticated
|
|
435
|
+
* opaque HTTP route so remote/container clients can fetch the image without host
|
|
436
|
+
* filesystem paths leaking into the transcript.
|
|
437
|
+
*/
|
|
438
|
+
function artifactMarkdownUrl(filePath: string): string {
|
|
439
|
+
return artifactHttpUrl(filePath).replace(/([()])/g, "\\$1");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
interface GoogleResponsePart {
|
|
443
|
+
text?: unknown;
|
|
444
|
+
thought?: boolean;
|
|
445
|
+
thoughtSignature?: string;
|
|
446
|
+
thought_signature?: string;
|
|
447
|
+
extra_content?: { google?: { thought_signature?: unknown } };
|
|
448
|
+
functionCall?: unknown;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
interface GoogleFunctionCall {
|
|
452
|
+
name: string;
|
|
453
|
+
args?: unknown;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Read a Gemini/Antigravity thought signature from a response part. Antigravity can place it
|
|
458
|
+
* either directly on the part (`thoughtSignature` / `thought_signature`) or inside the same
|
|
459
|
+
* nested `extra_content.google.thought_signature` shape used on the Responses wire.
|
|
460
|
+
*/
|
|
461
|
+
function googlePartThoughtSignature(part: GoogleResponsePart): string | undefined {
|
|
462
|
+
const direct = part.thoughtSignature ?? part.thought_signature;
|
|
463
|
+
if (typeof direct === "string" && direct.length > 0) return direct;
|
|
464
|
+
const nested = part.extra_content?.google?.thought_signature;
|
|
465
|
+
return typeof nested === "string" && nested.length > 0 ? nested : undefined;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Carry a Gemini thought signature with the exact function-call part that produced it. Google
|
|
470
|
+
* validates the signature against that specific part, so it must ride the individual tool call
|
|
471
|
+
* rather than be re-matched by name/arguments later (issue #1735).
|
|
472
|
+
*/
|
|
473
|
+
function googleToolCallMetadataFromPart(
|
|
474
|
+
part: GoogleResponsePart,
|
|
475
|
+
fallbackSignature?: string,
|
|
476
|
+
): { providerMetadata: OcxProviderOpaqueToolCallMetadata } | undefined {
|
|
477
|
+
const signature = googlePartThoughtSignature(part) ?? fallbackSignature;
|
|
478
|
+
if (!isLikelyRealThoughtSignature(signature)) return undefined;
|
|
479
|
+
return { providerMetadata: { google: { thoughtSignature: signature } } };
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Google marks model-internal reasoning as a normal text-bearing part plus `thought: true`.
|
|
484
|
+
* Keep that provider visibility bit authoritative here so the streaming and buffered parsers
|
|
485
|
+
* cannot accidentally expose the same hidden reasoning through different event types.
|
|
486
|
+
*/
|
|
487
|
+
function googlePartTextEvent(part: GoogleResponsePart): AdapterEvent | undefined {
|
|
488
|
+
// A malformed scalar/object is not text and must not cross the AdapterEvent boundary. Dropping
|
|
489
|
+
// only this optional field preserves the rest of the part without inventing assistant output by
|
|
490
|
+
// coercion; an empty string keeps its existing no-event behavior.
|
|
491
|
+
if (typeof part.text !== "string" || part.text.length === 0) return undefined;
|
|
492
|
+
return part.thought === true
|
|
493
|
+
? { type: "reasoning_raw_delta", text: part.text }
|
|
494
|
+
: { type: "text_delta", text: part.text };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
interface InvalidGoogleFunctionCallDiagnostic {
|
|
498
|
+
reason:
|
|
499
|
+
| "function_call_not_object"
|
|
500
|
+
| "function_call_name_invalid"
|
|
501
|
+
| "function_call_name_blank";
|
|
502
|
+
partIndex: number;
|
|
503
|
+
valueType: string;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
interface InvalidGoogleShapeDiagnostic {
|
|
507
|
+
reason:
|
|
508
|
+
| "candidates_not_array"
|
|
509
|
+
| "candidate_not_object"
|
|
510
|
+
| "content_not_object"
|
|
511
|
+
| "parts_not_array"
|
|
512
|
+
| "part_not_object";
|
|
513
|
+
partIndex?: number;
|
|
514
|
+
valueType: string;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function isGoogleRecord(value: unknown): value is Record<string, unknown> {
|
|
518
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function googleStructuralValueType(value: unknown): string {
|
|
522
|
+
if (value === null) return "null";
|
|
523
|
+
return Array.isArray(value) ? "array" : typeof value;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Gemini delivers one complete functionCall per part, so a missing name cannot be repaired by a
|
|
528
|
+
* later delta. Validate the whole parts array before observing signatures or emitting content: a
|
|
529
|
+
* malformed call must terminate the claimed response rather than enter replay state or reach the
|
|
530
|
+
* bridge as a nameless dispatch. Null remains an absence encoding, matching other optional fields.
|
|
531
|
+
*/
|
|
532
|
+
function diagnoseGoogleFunctionCalls(
|
|
533
|
+
parts: readonly GoogleResponsePart[],
|
|
534
|
+
): InvalidGoogleFunctionCallDiagnostic | undefined {
|
|
535
|
+
for (let partIndex = 0; partIndex < parts.length; partIndex++) {
|
|
536
|
+
const functionCall = parts[partIndex]?.functionCall;
|
|
537
|
+
if (functionCall === undefined || functionCall === null) continue;
|
|
538
|
+
if (!isGoogleRecord(functionCall)) {
|
|
539
|
+
return {
|
|
540
|
+
reason: "function_call_not_object",
|
|
541
|
+
partIndex,
|
|
542
|
+
valueType: googleStructuralValueType(functionCall),
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
if (typeof functionCall.name !== "string") {
|
|
546
|
+
return {
|
|
547
|
+
reason: "function_call_name_invalid",
|
|
548
|
+
partIndex,
|
|
549
|
+
valueType: googleStructuralValueType(functionCall.name),
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
if (functionCall.name.trim().length === 0) {
|
|
553
|
+
return {
|
|
554
|
+
reason: "function_call_name_blank",
|
|
555
|
+
partIndex,
|
|
556
|
+
valueType: "string",
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return undefined;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function googleFunctionCall(part: GoogleResponsePart): GoogleFunctionCall | undefined {
|
|
564
|
+
const functionCall = part.functionCall;
|
|
565
|
+
if (!isGoogleRecord(functionCall) || typeof functionCall.name !== "string") return undefined;
|
|
566
|
+
return { name: functionCall.name, args: functionCall.args };
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function invalidGoogleFunctionCallEvent(
|
|
570
|
+
diagnostic: InvalidGoogleFunctionCallDiagnostic,
|
|
571
|
+
): Extract<AdapterEvent, { type: "error" }> {
|
|
572
|
+
const subject = diagnostic.reason === "function_call_not_object"
|
|
573
|
+
? "invalid function call"
|
|
574
|
+
: diagnostic.reason === "function_call_name_blank"
|
|
575
|
+
? "blank function call name"
|
|
576
|
+
: "invalid function call name";
|
|
577
|
+
return {
|
|
578
|
+
type: "error",
|
|
579
|
+
message: `google response contained ${subject} (${diagnostic.reason}; partIndex=${diagnostic.partIndex}; valueType=${diagnostic.valueType}) — cannot dispatch`,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* A candidate's `content` is claimed model output inside a well-formed frame, so it is governed by
|
|
585
|
+
* the #1332 nested-shape rule (fail closed) rather than #1240's root-frame padding rule (skip).
|
|
586
|
+
*
|
|
587
|
+
* Absence stays legal, and so does one encoding of it: an empty array is how a JSON writer with no
|
|
588
|
+
* distinct empty-object form spells an empty `content`, and it already behaves as "no parts". A
|
|
589
|
+
* NON-empty array is the opposite case — `content?.parts` silently reads `undefined` from it, so a
|
|
590
|
+
* candidate shaped `content: [{ parts: [...] }]` dropped its own text and completed as an empty
|
|
591
|
+
* turn.
|
|
592
|
+
*/
|
|
593
|
+
function diagnoseGoogleContent(content: unknown): InvalidGoogleShapeDiagnostic | undefined {
|
|
594
|
+
if (content === undefined || content === null || isGoogleRecord(content)) return undefined;
|
|
595
|
+
if (Array.isArray(content) && content.length === 0) return undefined;
|
|
596
|
+
return { reason: "content_not_object", valueType: googleStructuralValueType(content) };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* `content.parts` sits one rung below the candidate guard added in #1332, and both parsers
|
|
601
|
+
* consumed it unchecked: `for (const part of {})` throws `{} is not iterable`, and a `[null]`
|
|
602
|
+
* element throws on `part.thoughtSignature`. A `parts` that is absent or `null` keeps its existing
|
|
603
|
+
* skip — only a present, non-null container is validated.
|
|
604
|
+
*/
|
|
605
|
+
function diagnoseGoogleParts(parts: unknown): InvalidGoogleShapeDiagnostic | undefined {
|
|
606
|
+
if (!Array.isArray(parts)) {
|
|
607
|
+
return { reason: "parts_not_array", valueType: googleStructuralValueType(parts) };
|
|
608
|
+
}
|
|
609
|
+
for (let partIndex = 0; partIndex < parts.length; partIndex++) {
|
|
610
|
+
const part: unknown = parts[partIndex];
|
|
611
|
+
if (!isGoogleRecord(part)) {
|
|
612
|
+
return { reason: "part_not_object", partIndex, valueType: googleStructuralValueType(part) };
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return undefined;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function invalidGoogleShapeEvent(
|
|
619
|
+
diagnostic: InvalidGoogleShapeDiagnostic,
|
|
620
|
+
): Extract<AdapterEvent, { type: "error" }> {
|
|
621
|
+
const at = diagnostic.partIndex !== undefined ? `; partIndex=${diagnostic.partIndex}` : "";
|
|
622
|
+
// The subject names the rung that failed, so an operator reading a log can tell a broken
|
|
623
|
+
// candidate list from a well-formed candidate whose parts are broken. The candidate subject keeps
|
|
624
|
+
// the exact wording #1332 introduced as its prefix, so an existing grep still matches.
|
|
625
|
+
const subject = diagnostic.reason === "candidates_not_array" || diagnostic.reason === "candidate_not_object"
|
|
626
|
+
? "candidates"
|
|
627
|
+
: diagnostic.reason === "content_not_object"
|
|
628
|
+
? "content"
|
|
629
|
+
: "content parts";
|
|
630
|
+
return {
|
|
631
|
+
type: "error",
|
|
632
|
+
message: `google response contained invalid ${subject} (${diagnostic.reason}${at}; valueType=${diagnostic.valueType})`,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapter {
|
|
637
|
+
// Per-request closure: resolveAdapter builds a fresh adapter per request (server.ts), so buildRequest
|
|
638
|
+
// can stash the CCA model/session for parseStream's reasoning-replay observation.
|
|
639
|
+
let antigravityModel: string | undefined;
|
|
640
|
+
let antigravitySession: string | undefined;
|
|
641
|
+
// Vertex returns the same opaque Gemini thought signatures as CCA, but its replay namespace
|
|
642
|
+
// must stay transport-scoped: a signature minted by one Google backend must never be sent to
|
|
643
|
+
// another merely because the public model id and first prompt happen to match.
|
|
644
|
+
let vertexReplayModel: string | undefined;
|
|
645
|
+
let vertexReplaySession: string | undefined;
|
|
646
|
+
let restoreGoogleToolName = (name: string): string => name;
|
|
647
|
+
return {
|
|
648
|
+
name: "google",
|
|
649
|
+
|
|
650
|
+
// Vertex + Antigravity get Kiro-style retry/timeout + classified, redacted errors.
|
|
651
|
+
// Direct AI-Studio uses the canonical server transport (fetchWithTransientRetry), which
|
|
652
|
+
// retries transient 5xx responses through providerFetch while preserving multi-key pool
|
|
653
|
+
// 429 rotation and raw error formatting.
|
|
654
|
+
...(provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist"
|
|
655
|
+
? {
|
|
656
|
+
fetchResponse: (request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response> =>
|
|
657
|
+
(provider.googleMode === "cloud-code-assist" ? fetchAntigravityWithRetry : fetchVertexWithRetry)(request, ctx),
|
|
658
|
+
formatErrorBody: (status: number, _headers: Headers, payloadText: string): string =>
|
|
659
|
+
(provider.googleMode === "cloud-code-assist" ? safeAntigravityHttpErrorMessage : safeVertexHttpErrorMessage)(status, payloadText),
|
|
660
|
+
}
|
|
661
|
+
: {}),
|
|
662
|
+
|
|
663
|
+
async buildRequest(parsed: OcxParsedRequest) {
|
|
664
|
+
const routedModelId = provider.googleMode === "cloud-code-assist"
|
|
665
|
+
? resolveAntigravityEffortWireModel(
|
|
666
|
+
parsed.modelId,
|
|
667
|
+
mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning),
|
|
668
|
+
provider.baseUrl,
|
|
669
|
+
).wireModelId
|
|
670
|
+
: provider.googleMode === "vertex"
|
|
671
|
+
? parsed.modelId
|
|
672
|
+
: resolveDirectGeminiWireModelId(parsed.modelId, provider.directGeminiWireRenames !== false);
|
|
673
|
+
// AI Studio's `-tiered` spelling is wire-only; CCA aliases may migrate to another generation.
|
|
674
|
+
const identityModelId = provider.googleMode === "cloud-code-assist" ? routedModelId : parsed.modelId;
|
|
675
|
+
const { systemInstruction, contents } = messagesToGeminiFormat(
|
|
676
|
+
parsed,
|
|
677
|
+
identityModelId,
|
|
678
|
+
provider.googleMode === "cloud-code-assist",
|
|
679
|
+
);
|
|
680
|
+
const tools = toolsToGeminiFormat(parsed);
|
|
681
|
+
|
|
682
|
+
const body: Record<string, unknown> = { contents };
|
|
683
|
+
if (systemInstruction) body.systemInstruction = systemInstruction;
|
|
684
|
+
if (tools) body.tools = tools;
|
|
685
|
+
// Only meaningful with declarations on the wire: mode ANY with an empty
|
|
686
|
+
// catalog is a guaranteed upstream 400.
|
|
687
|
+
const toolConfig = tools ? toolChoiceToGeminiToolConfig(parsed) : undefined;
|
|
688
|
+
if (toolConfig) body.toolConfig = toolConfig;
|
|
689
|
+
|
|
690
|
+
const generationConfig: Record<string, unknown> = {};
|
|
691
|
+
if (parsed.options.maxOutputTokens) generationConfig.maxOutputTokens = parsed.options.maxOutputTokens;
|
|
692
|
+
if (parsed.options.temperature !== undefined) generationConfig.temperature = parsed.options.temperature;
|
|
693
|
+
if (parsed.options.topP !== undefined) generationConfig.topP = parsed.options.topP;
|
|
694
|
+
if (parsed.options.stopSequences) generationConfig.stopSequences = parsed.options.stopSequences;
|
|
695
|
+
// Effort → thinkingLevel follows the configured ladder: any model advertising reasoning
|
|
696
|
+
// efforts (registry preset or user config) sends the mapped level, so a picker-selected
|
|
697
|
+
// effort actually reaches the wire (gemini-3.1-pro-preview ships a ladder). The original
|
|
698
|
+
// gemini-3.5/3.6-flash direct-mode slice stays hardcoded so unladdered configs keep their
|
|
699
|
+
// current behavior; Vertex participates only through an explicitly configured ladder (the
|
|
700
|
+
// seed google-vertex entry ships none). Image models are excluded — thinkingConfig would
|
|
701
|
+
// suppress the responseModalities fallback below. CCA maps effort on its envelope path.
|
|
702
|
+
const thinkingEligible = provider.googleMode !== "cloud-code-assist"
|
|
703
|
+
&& !isImageCapableModel(parsed.modelId)
|
|
704
|
+
&& (configuredReasoningEfforts(provider, parsed.modelId) !== undefined
|
|
705
|
+
|| (provider.googleMode !== "vertex"
|
|
706
|
+
&& (parsed.modelId === "gemini-3.5-flash" || parsed.modelId === "gemini-3.6-flash")));
|
|
707
|
+
const thinkingLevel = thinkingEligible
|
|
708
|
+
? mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning)
|
|
709
|
+
: undefined;
|
|
710
|
+
if (thinkingLevel) generationConfig.thinkingConfig = { thinkingLevel };
|
|
711
|
+
if (!generationConfig.thinkingConfig && isImageCapableModel(parsed.modelId)) {
|
|
712
|
+
generationConfig.responseModalities = ["TEXT", "IMAGE"];
|
|
713
|
+
}
|
|
714
|
+
if (Object.keys(generationConfig).length > 0) body.generationConfig = generationConfig;
|
|
715
|
+
|
|
716
|
+
const ccaAlwaysSse = provider.googleMode === "cloud-code-assist";
|
|
717
|
+
const method = ccaAlwaysSse || parsed.stream ? "streamGenerateContent" : "generateContent";
|
|
718
|
+
const streamParam = ccaAlwaysSse || parsed.stream ? "?alt=sse" : "";
|
|
719
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
720
|
+
if (provider.headers) Object.assign(headers, provider.headers);
|
|
721
|
+
|
|
722
|
+
if (provider.googleMode === "cloud-code-assist") {
|
|
723
|
+
// Google Antigravity (Cloud Code Assist): wrap the flat Gemini body in the CCA envelope.
|
|
724
|
+
const token = provider.apiKey?.trim();
|
|
725
|
+
if (!token) throw new Error("google-antigravity oauth token missing — run ocx login google-antigravity");
|
|
726
|
+
const base = provider.baseUrl?.trim();
|
|
727
|
+
if (!base) throw new Error("google-antigravity requires a non-empty baseUrl");
|
|
728
|
+
const canonicalBase = canonicalAntigravityHttpsHost(base) ?? base;
|
|
729
|
+
if (!isAntigravityHttpsHost(canonicalBase)) {
|
|
730
|
+
throw new Error("google-antigravity requires an HTTPS baseUrl");
|
|
731
|
+
}
|
|
732
|
+
const url = `${canonicalBase.replace(/\/+$/, "")}/v1internal:${method}${streamParam}`;
|
|
733
|
+
const project = provider.project;
|
|
734
|
+
if (!project) throw new Error("Antigravity requires a discovered Cloud Code Assist project id (re-run `ocx login google-antigravity`).");
|
|
735
|
+
const sessionId = antigravitySessionId(parsed);
|
|
736
|
+
const mappedEffort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning);
|
|
737
|
+
const { wireModelId, thinkingLevel } = resolveAntigravityEffortWireModel(
|
|
738
|
+
parsed.modelId,
|
|
739
|
+
mappedEffort,
|
|
740
|
+
provider.baseUrl,
|
|
741
|
+
);
|
|
742
|
+
antigravityModel = wireModelId;
|
|
743
|
+
antigravitySession = sessionId;
|
|
744
|
+
// Effort → thinkingConfig for CCA (CLIProxyAPI proven: request.generationConfig.thinkingConfig).
|
|
745
|
+
// Suffix/compat IDs return thinkingLevel=undefined — the suffix IS the effort, no contradiction.
|
|
746
|
+
if (thinkingLevel) {
|
|
747
|
+
const gc = (body.generationConfig ?? {}) as Record<string, unknown>;
|
|
748
|
+
gc.thinkingConfig = { thinkingLevel };
|
|
749
|
+
body.generationConfig = gc;
|
|
750
|
+
}
|
|
751
|
+
// Reasoning continuity: Gemini models re-inject cached thoughtSignatures; Claude-on-Antigravity
|
|
752
|
+
// sanitizes signatures inline (no cache). Both guard against the upstream 400 on bad signatures.
|
|
753
|
+
// The real Antigravity client puts the session id ONLY at `request.sessionId` (camelCase,
|
|
754
|
+
// nested) — matching CLIProxyAPI `generateStableSessionID`. An extra top-level/snake_case
|
|
755
|
+
// spelling is a non-first-party key, so we send the single canonical location.
|
|
756
|
+
const draftRequest: Record<string, unknown> = { ...body, sessionId };
|
|
757
|
+
// Claude-on-Antigravity forces VALIDATED function calling (the real client always sets it).
|
|
758
|
+
if (/claude/i.test(wireModelId)) {
|
|
759
|
+
// VALIDATED would defeat a client's tool_choice "none": honor it by dropping the
|
|
760
|
+
// declarations instead, the wire shape of a tool-less Claude turn.
|
|
761
|
+
if (parsed.options.toolChoice === "none") {
|
|
762
|
+
delete draftRequest.tools;
|
|
763
|
+
delete draftRequest.toolConfig;
|
|
764
|
+
}
|
|
765
|
+
const existing = (draftRequest.toolConfig ?? {}) as Record<string, unknown>;
|
|
766
|
+
const fcc = (existing.functionCallingConfig ?? {}) as Record<string, unknown>;
|
|
767
|
+
draftRequest.toolConfig = { ...existing, functionCallingConfig: { ...fcc, mode: "VALIDATED" } };
|
|
768
|
+
}
|
|
769
|
+
const compiled = compileGoogleWireBody(draftRequest);
|
|
770
|
+
const request = compiled.body;
|
|
771
|
+
if (systemInstruction) {
|
|
772
|
+
request.preambleConfig = { mode: "SYSTEM_INSTRUCTION_MODE_REPLACE" };
|
|
773
|
+
}
|
|
774
|
+
restoreGoogleToolName = compiled.restoreToolName;
|
|
775
|
+
// Compile names before replay: signatures are keyed by the exact provider-visible name.
|
|
776
|
+
if (Array.isArray((request as { contents?: unknown[] }).contents)) {
|
|
777
|
+
const contents = (request as { contents: unknown[] }).contents;
|
|
778
|
+
const strippedModelTail = /claude/i.test(wireModelId) ? stripTrailingClaudePrefill(contents) : false;
|
|
779
|
+
if (antigravityUsesReplayCache(wireModelId)) {
|
|
780
|
+
applyAntigravityReplay(wireModelId, sessionId, contents);
|
|
781
|
+
} else {
|
|
782
|
+
sanitizeAntigravityClaudeSignatures(contents);
|
|
783
|
+
}
|
|
784
|
+
// Claude-on-Antigravity rejects assistant-tail (model-tail in Gemini terms) histories
|
|
785
|
+
// as prefill: "This model does not support assistant message prefill. The conversation
|
|
786
|
+
// must end with a user message." Context compaction, previous_response_id expansion,
|
|
787
|
+
// and interrupted-turn replay can all produce a model-tail history. Append a user
|
|
788
|
+
// "(continue)" nudge, mirroring the anthropic adapter's tail guard (src/adapters/anthropic.ts).
|
|
789
|
+
// When a trailing model turn was stripped, append even if the history now ends with user.
|
|
790
|
+
if (/claude/i.test(wireModelId)) {
|
|
791
|
+
const last = contents.length > 0 ? contents[contents.length - 1] as { role?: string } : undefined;
|
|
792
|
+
if (strippedModelTail || !last || last.role === "model") {
|
|
793
|
+
contents.push({ role: "user", parts: [{ text: "(continue)" }] });
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
const envelope = {
|
|
798
|
+
model: wireModelId,
|
|
799
|
+
// The envelope's `userAgent` field is a protocol constant ("antigravity"), distinct from
|
|
800
|
+
// the HTTP `User-Agent` header (the real IDE UA). CLIProxyAPI `geminiToAntigravity` hardcodes
|
|
801
|
+
// the body field; only the header carries the versioned client string.
|
|
802
|
+
userAgent: "antigravity",
|
|
803
|
+
requestType: "agent",
|
|
804
|
+
project,
|
|
805
|
+
requestId: `agent-${crypto.randomUUID()}`,
|
|
806
|
+
request,
|
|
807
|
+
};
|
|
808
|
+
headers["User-Agent"] = ANTIGRAVITY_REQUEST_UA;
|
|
809
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
810
|
+
if (/claude/i.test(wireModelId)) {
|
|
811
|
+
headers["anthropic-beta"] = "interleaved-thinking-2025-05-14";
|
|
812
|
+
}
|
|
813
|
+
return { url, method: "POST", headers, body: JSON.stringify(envelope) };
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (provider.googleMode === "vertex") {
|
|
817
|
+
const compiled = compileGoogleWireBody(body);
|
|
818
|
+
restoreGoogleToolName = compiled.restoreToolName;
|
|
819
|
+
const vertexProject = provider.project || process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT || "api-key";
|
|
820
|
+
const vertexLocation = provider.location || process.env.GOOGLE_CLOUD_LOCATION || "global";
|
|
821
|
+
vertexReplayModel = `vertex:${vertexProject}:${vertexLocation}:${parsed.modelId}`;
|
|
822
|
+
vertexReplaySession = vertexReplaySessionId(parsed);
|
|
823
|
+
// Compile names before replay so the cache matches the exact provider-visible
|
|
824
|
+
// functionCall identity. This is the same bounded TTL/LRU store used by CCA, with the
|
|
825
|
+
// transport prefix above preventing cross-backend signature reuse (#1254).
|
|
826
|
+
if (Array.isArray((compiled.body as { contents?: unknown[] }).contents)) {
|
|
827
|
+
applyAntigravityReplay(
|
|
828
|
+
vertexReplayModel,
|
|
829
|
+
vertexReplaySession,
|
|
830
|
+
(compiled.body as { contents: unknown[] }).contents,
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
// Vertex AI: project/location endpoint with GCP ADC, or x-goog-api-key fast path.
|
|
834
|
+
const apiKey = resolveVertexApiKey(provider.apiKey);
|
|
835
|
+
if (apiKey) {
|
|
836
|
+
const url = `https://aiplatform.googleapis.com/v1/publishers/google/models/${parsed.modelId}:${method}${streamParam}`;
|
|
837
|
+
headers["x-goog-api-key"] = apiKey;
|
|
838
|
+
return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
|
|
839
|
+
}
|
|
840
|
+
const project = provider.project || process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT;
|
|
841
|
+
if (!project) throw new Error("Vertex AI requires a project id (provider.project or GOOGLE_CLOUD_PROJECT/GCLOUD_PROJECT).");
|
|
842
|
+
const location = provider.location || process.env.GOOGLE_CLOUD_LOCATION;
|
|
843
|
+
if (!location) throw new Error("Vertex AI requires a location (provider.location or GOOGLE_CLOUD_LOCATION).");
|
|
844
|
+
const locationError = googleVertexLocationConfigError(location);
|
|
845
|
+
if (locationError) throw new Error(locationError);
|
|
846
|
+
const host = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
|
|
847
|
+
const url = `https://${host}/v1/projects/${project}/locations/${location}/publishers/google/models/${parsed.modelId}:${method}${streamParam}`;
|
|
848
|
+
const token = await getVertexAccessToken();
|
|
849
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
850
|
+
return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// ai-studio (default): Generative Language API + x-goog-api-key.
|
|
854
|
+
const url = `${provider.baseUrl}/v1beta/models/${routedModelId}:${method}${streamParam}`;
|
|
855
|
+
const apiKey = provider.apiKey?.trim();
|
|
856
|
+
if (!apiKey) throw new Error("google (AI Studio) requires a non-empty API key");
|
|
857
|
+
headers["x-goog-api-key"] = apiKey;
|
|
858
|
+
|
|
859
|
+
const compiled = compileGoogleWireBody(body);
|
|
860
|
+
restoreGoogleToolName = compiled.restoreToolName;
|
|
861
|
+
return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
|
|
862
|
+
},
|
|
863
|
+
|
|
864
|
+
async *parseStream(response: Response, budget: TranslatorBudget): AsyncGenerator<AdapterEvent> {
|
|
865
|
+
if (!response.body) {
|
|
866
|
+
yield { type: "error", message: "No response body" };
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
// Streaming responses are processed incrementally (SSE chunks), so the full body
|
|
870
|
+
// is never buffered — no Content-Length pre-check is needed here. Per-image size
|
|
871
|
+
// protection is enforced on each chunk via MAX_ENCODED_BYTES_PER_IMAGE before
|
|
872
|
+
// materializeInlineImage is called (see the inline.data check below).
|
|
873
|
+
|
|
874
|
+
const reader = response.body.getReader();
|
|
875
|
+
const decoder = new TextDecoder();
|
|
876
|
+
const budgetEncoder = new TextEncoder();
|
|
877
|
+
let buffer = "";
|
|
878
|
+
let bufferBytes = 0;
|
|
879
|
+
// Raw unterminated-line bytes, independent of TextDecoder's pending UTF-8
|
|
880
|
+
// state. `bufferBytes` is the decoded residual and can undercount by 1–3
|
|
881
|
+
// bytes when a chunk ends mid-character.
|
|
882
|
+
let incompleteLineBytes = 0;
|
|
883
|
+
let pendingUsage: OcxUsage | undefined;
|
|
884
|
+
let toolCallsStarted = 0;
|
|
885
|
+
let lastFinishReason: string | undefined;
|
|
886
|
+
let sawAnyFrame = false;
|
|
887
|
+
let sawTerminalSignal = false;
|
|
888
|
+
let pendingStreamThoughtSig: string | undefined;
|
|
889
|
+
|
|
890
|
+
const handleDataLine = async function* (line: string): AsyncGenerator<AdapterEvent, "continue" | "content" | "terminate"> {
|
|
891
|
+
const payload = line.slice(5).trim();
|
|
892
|
+
if (!payload) return "continue";
|
|
893
|
+
if (budgetEncoder.encode(payload).byteLength > sseFrameMaxBytes) {
|
|
894
|
+
yield { type: "error", message: `upstream SSE data frame exceeds ${sseFrameMaxBytes} bytes` };
|
|
895
|
+
return "terminate";
|
|
896
|
+
}
|
|
897
|
+
let emittedContentEvent = false;
|
|
898
|
+
|
|
899
|
+
let parsed: unknown;
|
|
900
|
+
try {
|
|
901
|
+
parsed = JSON.parse(payload);
|
|
902
|
+
} catch {
|
|
903
|
+
yield { type: "error", message: "malformed upstream SSE data frame" };
|
|
904
|
+
return "terminate";
|
|
905
|
+
}
|
|
906
|
+
// `JSON.parse("null")` returns null rather than throwing, so the catch above cannot cover
|
|
907
|
+
// it and the `chunk.error` read below crashed the stream (see openai-chat.ts). Skip such a
|
|
908
|
+
// frame rather than terminating, for the reason given there: it is padding between real
|
|
909
|
+
// frames, not a broken stream. Deliberately returns BEFORE `sawAnyFrame`, so a stream made
|
|
910
|
+
// only of non-record frames still fails the terminal-signal check below instead of
|
|
911
|
+
// completing empty. An unparseable frame stays terminal.
|
|
912
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
913
|
+
return "continue";
|
|
914
|
+
}
|
|
915
|
+
const chunk = parsed as Record<string, unknown>;
|
|
916
|
+
sawAnyFrame = true;
|
|
917
|
+
|
|
918
|
+
// Inline provider error inside a 200 stream → terminal error (see openai-chat.ts).
|
|
919
|
+
if (chunk.error) {
|
|
920
|
+
const err = chunk.error as { message?: string } | undefined;
|
|
921
|
+
// Clear-on-invalid: a signature rejection means our replayed thoughtSignatures are stale.
|
|
922
|
+
// Drop the cache entry so the next turn starts clean instead of re-injecting a bad sig.
|
|
923
|
+
const replayModel = provider.googleMode === "cloud-code-assist" ? antigravityModel : vertexReplayModel;
|
|
924
|
+
const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession;
|
|
925
|
+
if ((provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex")
|
|
926
|
+
&& replayModel && replaySession
|
|
927
|
+
&& /signature|invalid_argument|invalid argument/i.test(err?.message ?? "")) {
|
|
928
|
+
clearAntigravityReplay(replayModel, replaySession);
|
|
929
|
+
}
|
|
930
|
+
yield { type: "error", message: err?.message ?? "upstream error" };
|
|
931
|
+
return "terminate";
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Antigravity (CCA) nests the standard Gemini payload under `response`.
|
|
935
|
+
let root = chunk;
|
|
936
|
+
if (provider.googleMode === "cloud-code-assist") {
|
|
937
|
+
const wrapped = chunk.response;
|
|
938
|
+
if (!wrapped || typeof wrapped !== "object" || Array.isArray(wrapped)) {
|
|
939
|
+
yield { type: "error", message: "google-antigravity response missing response wrapper" };
|
|
940
|
+
return "terminate";
|
|
941
|
+
}
|
|
942
|
+
root = wrapped as Record<string, unknown>;
|
|
943
|
+
}
|
|
944
|
+
// usageMetadata is a top-level field independent of candidates; read it BEFORE the
|
|
945
|
+
// candidates guard so a usage-only final chunk is not dropped.
|
|
946
|
+
const usageMeta = root.usageMetadata as Record<string, number> | undefined;
|
|
947
|
+
if (usageMeta) {
|
|
948
|
+
// Accumulate usage; emit a single terminal `done` post-loop so usage is never
|
|
949
|
+
// dropped on EOF and the stream never yields two `done` events.
|
|
950
|
+
pendingUsage = usageFromGemini(usageMeta);
|
|
951
|
+
sawTerminalSignal = true;
|
|
952
|
+
}
|
|
953
|
+
const rawCandidates = root.candidates;
|
|
954
|
+
// `null` is an absence encoding, not corruption, and terminating on it is the #1219
|
|
955
|
+
// failure mode one rung in: a `{"candidates":null}` frame arriving between a content
|
|
956
|
+
// delta and the finish chunk killed a turn whose answer had already fully arrived. An
|
|
957
|
+
// absent key and an empty array are already skipped here; `null` joins them. A non-null
|
|
958
|
+
// non-array container is still claimed structure the parser cannot read, and stays
|
|
959
|
+
// terminal.
|
|
960
|
+
if (rawCandidates === undefined || rawCandidates === null) return "continue";
|
|
961
|
+
if (!Array.isArray(rawCandidates)) {
|
|
962
|
+
yield invalidGoogleShapeEvent({
|
|
963
|
+
reason: "candidates_not_array",
|
|
964
|
+
valueType: googleStructuralValueType(rawCandidates),
|
|
965
|
+
});
|
|
966
|
+
return "terminate";
|
|
967
|
+
}
|
|
968
|
+
if (rawCandidates.length === 0) return "continue";
|
|
969
|
+
const rawCandidate = rawCandidates[0];
|
|
970
|
+
if (!isGoogleRecord(rawCandidate)) {
|
|
971
|
+
// Unlike a root `data: null` keepalive, this is a claimed response candidate. Treat it
|
|
972
|
+
// as terminal protocol corruption so the turn cannot complete after silently losing
|
|
973
|
+
// a candidate or tool call (#1325).
|
|
974
|
+
yield invalidGoogleShapeEvent({
|
|
975
|
+
reason: "candidate_not_object",
|
|
976
|
+
valueType: googleStructuralValueType(rawCandidate),
|
|
977
|
+
});
|
|
978
|
+
return "terminate";
|
|
979
|
+
}
|
|
980
|
+
const candidate = rawCandidate as {
|
|
981
|
+
content?: unknown;
|
|
982
|
+
finishReason?: string;
|
|
983
|
+
};
|
|
984
|
+
|
|
985
|
+
if (typeof candidate.finishReason === "string" && candidate.finishReason) {
|
|
986
|
+
lastFinishReason = candidate.finishReason;
|
|
987
|
+
sawTerminalSignal = true;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// One rung below the candidate guard above, same rule: this is claimed content, not
|
|
991
|
+
// padding, so it fails closed rather than being iterated or silently dropped (#1325).
|
|
992
|
+
const rawContent: unknown = candidate.content;
|
|
993
|
+
const invalidContent = diagnoseGoogleContent(rawContent);
|
|
994
|
+
if (invalidContent) {
|
|
995
|
+
yield invalidGoogleShapeEvent(invalidContent);
|
|
996
|
+
return "terminate";
|
|
997
|
+
}
|
|
998
|
+
const rawParts: unknown = isGoogleRecord(rawContent) ? rawContent.parts : undefined;
|
|
999
|
+
let parts: GoogleResponsePart[] | undefined;
|
|
1000
|
+
if (rawParts !== undefined && rawParts !== null) {
|
|
1001
|
+
const invalidParts = diagnoseGoogleParts(rawParts);
|
|
1002
|
+
if (invalidParts) {
|
|
1003
|
+
yield invalidGoogleShapeEvent(invalidParts);
|
|
1004
|
+
return "terminate";
|
|
1005
|
+
}
|
|
1006
|
+
parts = rawParts as GoogleResponsePart[];
|
|
1007
|
+
const invalidFunctionCall = diagnoseGoogleFunctionCalls(parts);
|
|
1008
|
+
if (invalidFunctionCall) {
|
|
1009
|
+
yield invalidGoogleFunctionCallEvent(invalidFunctionCall);
|
|
1010
|
+
return "terminate";
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
// Record Gemini thought signatures for the next stateless tool-result turn. Vertex and
|
|
1014
|
+
// Antigravity use separate model namespaces so opaque provider state cannot cross routes.
|
|
1015
|
+
const replayModel = provider.googleMode === "cloud-code-assist" ? antigravityModel : vertexReplayModel;
|
|
1016
|
+
const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession;
|
|
1017
|
+
if ((provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex")
|
|
1018
|
+
&& parts && replayModel && replaySession) {
|
|
1019
|
+
pendingStreamThoughtSig = observeAntigravityReplay(
|
|
1020
|
+
replayModel,
|
|
1021
|
+
replaySession,
|
|
1022
|
+
parts as unknown[],
|
|
1023
|
+
pendingStreamThoughtSig,
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
if (parts) {
|
|
1027
|
+
for (const part of parts) {
|
|
1028
|
+
const sig = googlePartThoughtSignature(part);
|
|
1029
|
+
if (part.thought === true && sig && isLikelyRealThoughtSignature(sig)) {
|
|
1030
|
+
pendingStreamThoughtSig = sig;
|
|
1031
|
+
}
|
|
1032
|
+
const textEvent = googlePartTextEvent(part);
|
|
1033
|
+
if (textEvent) {
|
|
1034
|
+
emittedContentEvent = true;
|
|
1035
|
+
yield textEvent;
|
|
1036
|
+
}
|
|
1037
|
+
const inline = (part as { inlineData?: { mimeType?: string; data?: string } }).inlineData;
|
|
1038
|
+
if (inline && typeof inline.data === "string") {
|
|
1039
|
+
if (inline.data.length > MAX_ENCODED_BYTES_PER_IMAGE) {
|
|
1040
|
+
yield { type: "error", message: "inline image exceeds per-image size cap" };
|
|
1041
|
+
} else {
|
|
1042
|
+
try {
|
|
1043
|
+
const filePath = await materializeInlineImage(inline.data, imageBudget);
|
|
1044
|
+
const escapedPath = artifactMarkdownUrl(filePath);
|
|
1045
|
+
emittedContentEvent = true;
|
|
1046
|
+
yield { type: "text_delta", text: `\n\n` };
|
|
1047
|
+
} catch {
|
|
1048
|
+
yield { type: "error", message: "failed to materialize inline image" };
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
const functionCall = googleFunctionCall(part);
|
|
1053
|
+
if (functionCall) {
|
|
1054
|
+
const id = `call_${crypto.randomUUID().slice(0, 8)}`;
|
|
1055
|
+
toolCallsStarted++;
|
|
1056
|
+
emittedContentEvent = true;
|
|
1057
|
+
const restoredName = restoreGoogleToolName(functionCall.name);
|
|
1058
|
+
yield {
|
|
1059
|
+
type: "tool_call_start",
|
|
1060
|
+
id,
|
|
1061
|
+
name: restoredName,
|
|
1062
|
+
...googleToolCallMetadataFromPart(part, pendingStreamThoughtSig),
|
|
1063
|
+
};
|
|
1064
|
+
yield { type: "tool_call_delta", arguments: JSON.stringify(functionCall.args ?? {}) };
|
|
1065
|
+
yield { type: "tool_call_end" };
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
return emittedContentEvent ? "content" : "continue";
|
|
1070
|
+
};
|
|
1071
|
+
const imageBudget = createImageBudget();
|
|
1072
|
+
|
|
1073
|
+
try {
|
|
1074
|
+
while (true) {
|
|
1075
|
+
const { done, value } = await reader.read();
|
|
1076
|
+
if (done) break;
|
|
1077
|
+
const incoming = value ?? new Uint8Array();
|
|
1078
|
+
// Cap each incomplete line on raw bytes before decode and before waiting
|
|
1079
|
+
// for a newline — otherwise a single unterminated data: payload can grow
|
|
1080
|
+
// without bound, buffer.length is UTF-16 units, and a mid-character
|
|
1081
|
+
// decode residual undercounts the true line. Reset at each newline so
|
|
1082
|
+
// several sub-cap frames in one network chunk are not rejected as one
|
|
1083
|
+
// oversized frame.
|
|
1084
|
+
const lineScan = scanSseLineBytes(incompleteLineBytes, incoming);
|
|
1085
|
+
if (lineScan.maximum > sseFrameMaxBytes) {
|
|
1086
|
+
yield { type: "error", message: `upstream SSE data frame exceeds ${sseFrameMaxBytes} bytes` };
|
|
1087
|
+
try { await reader.cancel(); } catch { /* ignore */ }
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
incompleteLineBytes = lineScan.residual;
|
|
1091
|
+
const nextBuffer = buffer + decoder.decode(value, { stream: true });
|
|
1092
|
+
const nextBufferBytes = budgetEncoder.encode(nextBuffer).byteLength;
|
|
1093
|
+
const appendReservation = budget.reserveTransient(nextBufferBytes, { kind: "live_transient" });
|
|
1094
|
+
buffer = nextBuffer;
|
|
1095
|
+
appendReservation.commitRetained();
|
|
1096
|
+
budget.releaseRetained(bufferBytes, { kind: "live_transient" });
|
|
1097
|
+
bufferBytes = nextBufferBytes;
|
|
1098
|
+
|
|
1099
|
+
const lines = buffer.split("\n");
|
|
1100
|
+
buffer = lines.pop() ?? "";
|
|
1101
|
+
const residualBytes = budgetEncoder.encode(buffer).byteLength;
|
|
1102
|
+
const residualReservation = budget.reserveTransient(residualBytes, { kind: "live_transient" });
|
|
1103
|
+
residualReservation.commitRetained();
|
|
1104
|
+
budget.releaseRetained(bufferBytes, { kind: "live_transient" });
|
|
1105
|
+
bufferBytes = residualBytes;
|
|
1106
|
+
|
|
1107
|
+
let sawLiveness = false;
|
|
1108
|
+
let sawContentEvent = false;
|
|
1109
|
+
for (const line of lines) {
|
|
1110
|
+
if (line.startsWith("data:")) {
|
|
1111
|
+
const result = yield* handleDataLine(line);
|
|
1112
|
+
if (result === "terminate") return;
|
|
1113
|
+
if (result === "content") sawContentEvent = true;
|
|
1114
|
+
continue;
|
|
1115
|
+
}
|
|
1116
|
+
sawLiveness = true;
|
|
1117
|
+
if (line.startsWith(":") || !line.trim()) continue;
|
|
1118
|
+
debugDroppedFrame("google", line);
|
|
1119
|
+
}
|
|
1120
|
+
if (sawLiveness && !sawContentEvent) yield { type: "heartbeat" };
|
|
1121
|
+
}
|
|
1122
|
+
buffer += decoder.decode();
|
|
1123
|
+
if (buffer.trim().length > 0) {
|
|
1124
|
+
const residual = buffer.trim();
|
|
1125
|
+
if (residual.startsWith(":")) {
|
|
1126
|
+
yield { type: "heartbeat" };
|
|
1127
|
+
} else if (!residual.startsWith("data:")) {
|
|
1128
|
+
yield { type: "error", message: "upstream stream ended with an incomplete SSE frame — possible truncation" };
|
|
1129
|
+
return;
|
|
1130
|
+
} else if ((yield* handleDataLine(residual)) === "terminate") return;
|
|
1131
|
+
}
|
|
1132
|
+
// Fail-closed: a turn cut off mid tool call (MAX_TOKENS / MALFORMED_FUNCTION_CALL) surfaces
|
|
1133
|
+
// an error instead of a silently-incomplete done. Mirrors kiro-truncation.
|
|
1134
|
+
if ((provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist")
|
|
1135
|
+
&& isVertexTruncatedTurn(lastFinishReason, toolCallsStarted)) {
|
|
1136
|
+
yield { type: "error", message: vertexTruncationErrorMessage(lastFinishReason) };
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
if (!sawAnyFrame || !sawTerminalSignal) {
|
|
1140
|
+
yield { type: "error", message: "upstream stream ended without a terminal signal — possible truncation" };
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
const stopReason = lastFinishReason === "MAX_TOKENS"
|
|
1144
|
+
? "max_tokens"
|
|
1145
|
+
: ["SAFETY", "RECITATION", "BLOCKLIST", "PROHIBITED_CONTENT", "SPII"].includes(lastFinishReason ?? "")
|
|
1146
|
+
? "content_filter"
|
|
1147
|
+
: undefined;
|
|
1148
|
+
yield {
|
|
1149
|
+
type: "done",
|
|
1150
|
+
usage: pendingUsage,
|
|
1151
|
+
...(stopReason ? { stopReason } : {}),
|
|
1152
|
+
};
|
|
1153
|
+
} catch (error) {
|
|
1154
|
+
if (!isTranslatorBudgetExceededError(error)) throw error;
|
|
1155
|
+
try { await reader.cancel(error); } catch { /* already closed */ }
|
|
1156
|
+
yield {
|
|
1157
|
+
type: "error",
|
|
1158
|
+
status: 502,
|
|
1159
|
+
errorType: "upstream_error",
|
|
1160
|
+
code: "translation_buffer_limit",
|
|
1161
|
+
message: "upstream translation buffer exceeded the safe limit",
|
|
1162
|
+
};
|
|
1163
|
+
} finally {
|
|
1164
|
+
budget.releaseRetained(bufferBytes, { kind: "live_transient" });
|
|
1165
|
+
reader.releaseLock();
|
|
1166
|
+
}
|
|
1167
|
+
},
|
|
1168
|
+
|
|
1169
|
+
async parseResponse(response: Response, budget: TranslatorBudget): Promise<AdapterEvent[]> {
|
|
1170
|
+
// Cloud Code Assist exposes only the SSE transport. Unary callers still use this
|
|
1171
|
+
// buffered adapter entry point, so collect the exact same events parseStream emits
|
|
1172
|
+
// instead of maintaining a second CCA JSON parser.
|
|
1173
|
+
if (provider.googleMode === "cloud-code-assist") {
|
|
1174
|
+
const events: AdapterEvent[] = [];
|
|
1175
|
+
let previousTail: AdapterEvent | undefined;
|
|
1176
|
+
try {
|
|
1177
|
+
for await (const event of this.parseStream(response, budget)) {
|
|
1178
|
+
retainTranslatedEvent(event, budget, previousTail);
|
|
1179
|
+
events.push(event);
|
|
1180
|
+
previousTail = event;
|
|
1181
|
+
}
|
|
1182
|
+
return events;
|
|
1183
|
+
} catch (error) {
|
|
1184
|
+
for (const event of events) releaseTranslatedEvent(event, budget);
|
|
1185
|
+
if (!isTranslatorBudgetExceededError(error)) throw error;
|
|
1186
|
+
return [{
|
|
1187
|
+
type: "error",
|
|
1188
|
+
status: 502,
|
|
1189
|
+
errorType: "upstream_error",
|
|
1190
|
+
code: "translation_buffer_limit",
|
|
1191
|
+
message: "upstream translation buffer exceeded the safe limit",
|
|
1192
|
+
}];
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
// Reject oversized responses before JSON parse. Prefer Content-Length when
|
|
1196
|
+
// present and truthful; always stream-read with a hard byte cap so a missing
|
|
1197
|
+
// or lying Content-Length cannot force a full in-memory buffer + parse.
|
|
1198
|
+
const contentLength = Number(response.headers.get("content-length"));
|
|
1199
|
+
if (Number.isFinite(contentLength) && contentLength > MAX_RESPONSE_BYTES) {
|
|
1200
|
+
try { await response.body?.cancel(); } catch { /* ignore */ }
|
|
1201
|
+
return [{ type: "error", message: `google response too large (content-length ${contentLength} exceeds ${MAX_RESPONSE_BYTES} bytes)` }];
|
|
1202
|
+
}
|
|
1203
|
+
let rawText: string;
|
|
1204
|
+
let rawTextBytes = 0;
|
|
1205
|
+
try {
|
|
1206
|
+
const reader = response.body?.getReader();
|
|
1207
|
+
if (!reader) return [{ type: "error", message: "google response had no body" }];
|
|
1208
|
+
const chunks: Uint8Array[] = [];
|
|
1209
|
+
let total = 0;
|
|
1210
|
+
try {
|
|
1211
|
+
for (;;) {
|
|
1212
|
+
const { done, value } = await reader.read();
|
|
1213
|
+
if (done) break;
|
|
1214
|
+
total += value.byteLength;
|
|
1215
|
+
if (total > MAX_RESPONSE_BYTES) {
|
|
1216
|
+
await reader.cancel().catch(() => {});
|
|
1217
|
+
return [{ type: "error", message: `google response too large (exceeded ${MAX_RESPONSE_BYTES} bytes)` }];
|
|
1218
|
+
}
|
|
1219
|
+
budget.chargeRetained(value.byteLength, { kind: "retained_collectors" });
|
|
1220
|
+
chunks.push(value);
|
|
1221
|
+
}
|
|
1222
|
+
} finally {
|
|
1223
|
+
try { await reader.cancel(); } catch { /* ignore */ }
|
|
1224
|
+
reader.releaseLock();
|
|
1225
|
+
}
|
|
1226
|
+
const bytesReservation = budget.reserveTransient(total, { kind: "retained_collectors" });
|
|
1227
|
+
const bytes = new Uint8Array(total);
|
|
1228
|
+
let offset = 0;
|
|
1229
|
+
for (const chunk of chunks) {
|
|
1230
|
+
bytes.set(chunk, offset);
|
|
1231
|
+
offset += chunk.byteLength;
|
|
1232
|
+
}
|
|
1233
|
+
bytesReservation.commitRetained();
|
|
1234
|
+
budget.releaseRetained(total, { kind: "retained_collectors" });
|
|
1235
|
+
rawText = new TextDecoder().decode(bytes);
|
|
1236
|
+
rawTextBytes = new TextEncoder().encode(rawText).byteLength;
|
|
1237
|
+
const textReservation = budget.reserveTransient(rawTextBytes, { kind: "retained_collectors" });
|
|
1238
|
+
textReservation.commitRetained();
|
|
1239
|
+
budget.releaseRetained(total, { kind: "retained_collectors" });
|
|
1240
|
+
} catch (err) {
|
|
1241
|
+
return [{ type: "error", message: err instanceof Error ? err.message : "failed to read google response body" }];
|
|
1242
|
+
}
|
|
1243
|
+
let raw: Record<string, unknown>;
|
|
1244
|
+
let rawBytes = 0;
|
|
1245
|
+
try {
|
|
1246
|
+
raw = JSON.parse(rawText) as Record<string, unknown>;
|
|
1247
|
+
rawBytes = new TextEncoder().encode(JSON.stringify(raw)).byteLength;
|
|
1248
|
+
const rawReservation = budget.reserveTransient(rawBytes, { kind: "retained_collectors" });
|
|
1249
|
+
rawReservation.commitRetained();
|
|
1250
|
+
budget.releaseRetained(rawTextBytes, { kind: "retained_collectors" });
|
|
1251
|
+
} catch {
|
|
1252
|
+
budget.releaseRetained(rawTextBytes, { kind: "retained_collectors" });
|
|
1253
|
+
return [{ type: "error", message: "google response was not valid JSON" }];
|
|
1254
|
+
}
|
|
1255
|
+
const finish = (events: AdapterEvent[]): AdapterEvent[] => {
|
|
1256
|
+
retainTranslatedEventBatch(events, budget);
|
|
1257
|
+
budget.releaseRetained(rawBytes, { kind: "retained_collectors" });
|
|
1258
|
+
rawBytes = 0;
|
|
1259
|
+
return events;
|
|
1260
|
+
};
|
|
1261
|
+
if (raw.error) {
|
|
1262
|
+
const err = raw.error as { message?: string };
|
|
1263
|
+
return finish([{ type: "error", message: err.message ?? "upstream error" }]);
|
|
1264
|
+
}
|
|
1265
|
+
const json = raw;
|
|
1266
|
+
const events: AdapterEvent[] = [];
|
|
1267
|
+
|
|
1268
|
+
const rawCandidates: unknown = json.candidates;
|
|
1269
|
+
// Parity with the streaming path, which has rejected a non-array `candidates` since #1332.
|
|
1270
|
+
// Buffered accepted `"abc"` outright (`"abc".length` is 3, so the emptiness check below
|
|
1271
|
+
// passed and `candidates[0]` was the character `"a"`), and reported `{}`/`5` as an absent
|
|
1272
|
+
// candidate list rather than a malformed one.
|
|
1273
|
+
if (rawCandidates !== undefined && rawCandidates !== null && !Array.isArray(rawCandidates)) {
|
|
1274
|
+
return finish([invalidGoogleShapeEvent({
|
|
1275
|
+
reason: "candidates_not_array",
|
|
1276
|
+
valueType: googleStructuralValueType(rawCandidates),
|
|
1277
|
+
})]);
|
|
1278
|
+
}
|
|
1279
|
+
const candidates = rawCandidates as { finishReason?: string }[] | undefined;
|
|
1280
|
+
if (!candidates?.length) {
|
|
1281
|
+
return finish([{ type: "error", message: "google response contained no candidates" }]);
|
|
1282
|
+
}
|
|
1283
|
+
const rawCandidate: unknown = candidates[0];
|
|
1284
|
+
if (!isGoogleRecord(rawCandidate)) {
|
|
1285
|
+
// The streaming parser already treats this as terminal protocol corruption (#1325/#1332).
|
|
1286
|
+
// Buffered returned a bare `done`, so a claimed-but-malformed candidate was reported to
|
|
1287
|
+
// the caller as a successful empty turn.
|
|
1288
|
+
return finish([invalidGoogleShapeEvent({
|
|
1289
|
+
reason: "candidate_not_object",
|
|
1290
|
+
valueType: googleStructuralValueType(rawCandidate),
|
|
1291
|
+
})]);
|
|
1292
|
+
}
|
|
1293
|
+
const candidate = rawCandidate as { content?: unknown; finishReason?: string };
|
|
1294
|
+
let toolCallsStarted = 0;
|
|
1295
|
+
const imageBudget = createImageBudget();
|
|
1296
|
+
const rawContent: unknown = candidate.content;
|
|
1297
|
+
const invalidContent = diagnoseGoogleContent(rawContent);
|
|
1298
|
+
if (invalidContent) return finish([invalidGoogleShapeEvent(invalidContent)]);
|
|
1299
|
+
const rawParts: unknown = isGoogleRecord(rawContent) ? rawContent.parts : undefined;
|
|
1300
|
+
if (rawParts !== undefined && rawParts !== null) {
|
|
1301
|
+
const invalidParts = diagnoseGoogleParts(rawParts);
|
|
1302
|
+
if (invalidParts) return finish([invalidGoogleShapeEvent(invalidParts)]);
|
|
1303
|
+
const parts = rawParts as GoogleResponsePart[];
|
|
1304
|
+
const invalidFunctionCall = diagnoseGoogleFunctionCalls(parts);
|
|
1305
|
+
if (invalidFunctionCall) return finish([invalidGoogleFunctionCallEvent(invalidFunctionCall)]);
|
|
1306
|
+
// Non-streaming Google-family response: observe thought signatures for the next turn,
|
|
1307
|
+
// using the same transport-scoped namespace as the streaming path.
|
|
1308
|
+
const replayModel = vertexReplayModel;
|
|
1309
|
+
const replaySession = vertexReplaySession;
|
|
1310
|
+
if (provider.googleMode === "vertex" && replayModel && replaySession) {
|
|
1311
|
+
observeAntigravityReplay(replayModel, replaySession, parts as unknown[]);
|
|
1312
|
+
}
|
|
1313
|
+
let pendingThoughtSig: string | undefined;
|
|
1314
|
+
for (const part of parts) {
|
|
1315
|
+
const sig = googlePartThoughtSignature(part);
|
|
1316
|
+
if (part.thought === true && sig && isLikelyRealThoughtSignature(sig)) {
|
|
1317
|
+
pendingThoughtSig = sig;
|
|
1318
|
+
}
|
|
1319
|
+
const textEvent = googlePartTextEvent(part);
|
|
1320
|
+
if (textEvent) events.push(textEvent);
|
|
1321
|
+
const inline = (part as { inlineData?: { mimeType?: string; data?: string } }).inlineData;
|
|
1322
|
+
if (inline && typeof inline.data === "string") {
|
|
1323
|
+
if (inline.data.length > MAX_ENCODED_BYTES_PER_IMAGE) {
|
|
1324
|
+
events.push({ type: "error", message: "inline image exceeds per-image size cap" });
|
|
1325
|
+
} else {
|
|
1326
|
+
try {
|
|
1327
|
+
const filePath = await materializeInlineImage(inline.data, imageBudget);
|
|
1328
|
+
const escapedPath = artifactMarkdownUrl(filePath);
|
|
1329
|
+
events.push({ type: "text_delta", text: `\n\n` });
|
|
1330
|
+
} catch {
|
|
1331
|
+
events.push({ type: "error", message: "failed to materialize inline image" });
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
const functionCall = googleFunctionCall(part);
|
|
1336
|
+
if (functionCall) {
|
|
1337
|
+
const id = `call_${crypto.randomUUID().slice(0, 8)}`;
|
|
1338
|
+
toolCallsStarted++;
|
|
1339
|
+
events.push({
|
|
1340
|
+
type: "tool_call_start",
|
|
1341
|
+
id,
|
|
1342
|
+
name: restoreGoogleToolName(functionCall.name),
|
|
1343
|
+
...googleToolCallMetadataFromPart(part, pendingThoughtSig),
|
|
1344
|
+
});
|
|
1345
|
+
events.push({ type: "tool_call_delta", arguments: JSON.stringify(functionCall.args ?? {}) });
|
|
1346
|
+
events.push({ type: "tool_call_end" });
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// Fail-closed truncation, same as the stream path: a non-stream turn cut off mid tool call
|
|
1352
|
+
// (MAX_TOKENS / MALFORMED_FUNCTION_CALL) surfaces an error instead of a silent done.
|
|
1353
|
+
if (provider.googleMode === "vertex"
|
|
1354
|
+
&& isVertexTruncatedTurn(candidate.finishReason, toolCallsStarted)) {
|
|
1355
|
+
return finish([{ type: "error", message: vertexTruncationErrorMessage(candidate.finishReason) }]);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
const usage = json.usageMetadata as Record<string, number> | undefined;
|
|
1359
|
+
// Mirror the streaming path: a buffered turn cut off by the token limit or a content filter
|
|
1360
|
+
// must carry its stop reason, or the bridge sees a clean `done` and reports the truncated
|
|
1361
|
+
// turn as completed — and, on a compaction turn, installs the half-written summary as
|
|
1362
|
+
// replacement history (#422).
|
|
1363
|
+
const finishReason = candidate.finishReason as string | undefined;
|
|
1364
|
+
const stopReason = finishReason === "MAX_TOKENS"
|
|
1365
|
+
? "max_tokens"
|
|
1366
|
+
: ["SAFETY", "RECITATION", "BLOCKLIST", "PROHIBITED_CONTENT", "SPII"].includes(finishReason ?? "")
|
|
1367
|
+
? "content_filter"
|
|
1368
|
+
: undefined;
|
|
1369
|
+
events.push({
|
|
1370
|
+
type: "done",
|
|
1371
|
+
usage: usageFromGemini(usage),
|
|
1372
|
+
...(stopReason ? { stopReason } : {}),
|
|
1373
|
+
});
|
|
1374
|
+
return finish(events);
|
|
1375
|
+
},
|
|
1376
|
+
};
|
|
1377
|
+
}
|