@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,1980 @@
|
|
|
1
|
+
import type { AdapterRequest, ProviderAdapter } from "./base";
|
|
2
|
+
import type { AdapterEvent, OcxAssistantMessage, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTextContent, OcxThinkingContent, OcxToolCall, OcxUsage } from "../types";
|
|
3
|
+
import { isAllowedToolChoice, modelInList, namespacedToolName, resolveToolChoiceWireName, toolChoiceToolPredicate } from "../types";
|
|
4
|
+
import { mapReasoningEffort, modelRecordValue } from "../reasoning-effort";
|
|
5
|
+
import { debugProviderDiagnostic } from "../lib/debug";
|
|
6
|
+
import { sseFieldValue } from "../lib/sse-decoder";
|
|
7
|
+
import { isDebugEnabled } from "../lib/debug-settings";
|
|
8
|
+
import { isCyberPolicyCode } from "../lib/errors";
|
|
9
|
+
import { redactSecretString } from "../lib/redact";
|
|
10
|
+
import { contentPartsToText } from "./image";
|
|
11
|
+
import { identifyRoutedModel } from "./identity";
|
|
12
|
+
import { peekReasoningForCall } from "../responses/reasoning-replay-cache";
|
|
13
|
+
import { buildNonOpenAIToolCatalogNudgeForTools, shouldInjectNonOpenAIToolCatalogNudge } from "./tool-catalog-nudge";
|
|
14
|
+
import { openRouterProviderPayload, resolveOpenRouterRouting } from "../providers/openrouter-routing";
|
|
15
|
+
import {
|
|
16
|
+
canForwardForeignServiceTierForChatModel,
|
|
17
|
+
fastPolicyForModel,
|
|
18
|
+
supportsServiceTierForModel,
|
|
19
|
+
} from "../providers/service-tier";
|
|
20
|
+
import {
|
|
21
|
+
canonicalFastTierMarker,
|
|
22
|
+
createAdapterTierMetadata,
|
|
23
|
+
decideTier,
|
|
24
|
+
type AdapterTierMetadata,
|
|
25
|
+
type ResolvedFastPolicy,
|
|
26
|
+
} from "../providers/fastwire";
|
|
27
|
+
import { openaiChatCompletionsUrl } from "./openai-chat-url";
|
|
28
|
+
import { stripResponsesOnlyEncryptedMarker } from "./responses-tool-schema";
|
|
29
|
+
import {
|
|
30
|
+
isTranslatorBudgetExceededError,
|
|
31
|
+
retainTranslatedEventBatch,
|
|
32
|
+
TRANSLATOR_MAX_SSE_EVENT_BYTES,
|
|
33
|
+
type TranslatorBudget,
|
|
34
|
+
} from "../lib/translator-budget";
|
|
35
|
+
|
|
36
|
+
// Providers may opt into stripping one trailing "[...]" group from the wire model id.
|
|
37
|
+
// Z.AI needs this because its OpenAI path rejects glm-5.2[1m] with 400 code 1211;
|
|
38
|
+
// unflagged OpenAI-compatible providers and the Anthropic adapter keep ids verbatim.
|
|
39
|
+
export function stripBracketedModelSuffix(modelId: string): string {
|
|
40
|
+
const suffixEnd = modelId.trimEnd().length;
|
|
41
|
+
if (suffixEnd === 0 || modelId[suffixEnd - 1] !== "]") return modelId;
|
|
42
|
+
|
|
43
|
+
let suffixStart = -1;
|
|
44
|
+
for (let i = suffixEnd - 2; i >= 0 && modelId[i] !== "]"; i--) {
|
|
45
|
+
if (modelId[i] === "[") suffixStart = i;
|
|
46
|
+
}
|
|
47
|
+
return suffixStart === -1 ? modelId : modelId.slice(0, suffixStart);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const CHAT_PASSTHROUGH_FIELDS = [
|
|
51
|
+
"audio",
|
|
52
|
+
"frequency_penalty",
|
|
53
|
+
"logit_bias",
|
|
54
|
+
"logprobs",
|
|
55
|
+
"max_completion_tokens",
|
|
56
|
+
"max_tokens",
|
|
57
|
+
"metadata",
|
|
58
|
+
"modalities",
|
|
59
|
+
"n",
|
|
60
|
+
"prediction",
|
|
61
|
+
"presence_penalty",
|
|
62
|
+
"reasoning_effort",
|
|
63
|
+
"response_format",
|
|
64
|
+
"seed",
|
|
65
|
+
"stop",
|
|
66
|
+
"store",
|
|
67
|
+
"temperature",
|
|
68
|
+
"tool_choice",
|
|
69
|
+
"tools",
|
|
70
|
+
"top_logprobs",
|
|
71
|
+
"top_p",
|
|
72
|
+
"user",
|
|
73
|
+
"web_search_options",
|
|
74
|
+
] as const;
|
|
75
|
+
|
|
76
|
+
function openAIChatTransport(provider: OcxProviderConfig): {
|
|
77
|
+
url: string;
|
|
78
|
+
headers: Record<string, string>;
|
|
79
|
+
hasCredential: boolean;
|
|
80
|
+
} {
|
|
81
|
+
const hasCredential = typeof provider.apiKey === "string" && provider.apiKey.trim().length > 0;
|
|
82
|
+
if ((provider.authMode === "key" || provider.authMode === "oauth") && !provider.keyOptional && !hasCredential) {
|
|
83
|
+
throw new Error(`${provider.adapter} requires a non-empty credential (authMode: ${provider.authMode})`);
|
|
84
|
+
}
|
|
85
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
86
|
+
if (hasCredential) headers.Authorization = `Bearer ${provider.apiKey}`;
|
|
87
|
+
if (provider.headers) Object.assign(headers, provider.headers);
|
|
88
|
+
return { url: openaiChatCompletionsUrl(provider.baseUrl), headers, hasCredential };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build a provider request from an inbound Chat Completions body without translating it
|
|
93
|
+
* through the Responses contract. This is deliberately a whitelist: Chat-only caller
|
|
94
|
+
* fields retain their exact wire representation, while provider capability gates remain
|
|
95
|
+
* centralized beside the ordinary openai-chat adapter.
|
|
96
|
+
*/
|
|
97
|
+
export function buildOpenAIChatPassthroughRequest(
|
|
98
|
+
provider: OcxProviderConfig,
|
|
99
|
+
rawBody: Record<string, unknown>,
|
|
100
|
+
modelId: string,
|
|
101
|
+
stream: boolean,
|
|
102
|
+
fastPolicy: ResolvedFastPolicy = fastPolicyForModel(provider, modelId, undefined, "chat"),
|
|
103
|
+
fastMode?: boolean,
|
|
104
|
+
): AdapterRequest {
|
|
105
|
+
const { url, headers, hasCredential } = openAIChatTransport(provider);
|
|
106
|
+
|
|
107
|
+
const body: Record<string, unknown> = {
|
|
108
|
+
model: provider.modelSuffixBracketStrip ? stripBracketedModelSuffix(modelId) : modelId,
|
|
109
|
+
messages: rawBody.messages,
|
|
110
|
+
stream,
|
|
111
|
+
};
|
|
112
|
+
for (const field of CHAT_PASSTHROUGH_FIELDS) {
|
|
113
|
+
if (rawBody[field] !== undefined) body[field] = rawBody[field];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const openRouterRouting = resolveOpenRouterRouting(provider, modelId);
|
|
117
|
+
if (openRouterRouting) body.provider = openRouterProviderPayload(openRouterRouting);
|
|
118
|
+
|
|
119
|
+
if (modelInList(provider.noTemperatureModels, modelId)) delete body.temperature;
|
|
120
|
+
if (modelInList(provider.noTopPModels, modelId)) delete body.top_p;
|
|
121
|
+
if (modelInList(provider.noPenaltyModels, modelId)) {
|
|
122
|
+
delete body.presence_penalty;
|
|
123
|
+
delete body.frequency_penalty;
|
|
124
|
+
}
|
|
125
|
+
// Exact match, unlike the gates above: `noStructuredOutputModels` is documented as
|
|
126
|
+
// "only an exact requested-model match omits the field" (#1424), and the Responses
|
|
127
|
+
// ingress enforces exactly that. A prefix match here would strip response_format from
|
|
128
|
+
// `<listed>:<tag>` siblings the operator never opted out, silently returning prose.
|
|
129
|
+
if (provider.noStructuredOutputModels?.includes(modelId)) delete body.response_format;
|
|
130
|
+
|
|
131
|
+
// Run the same complete Fast policy as the translated Chat path, including explicit
|
|
132
|
+
// fastMode and foreign-tier handling. On inherited canonical Fast, the passthrough still
|
|
133
|
+
// retains the caller's exact spelling; forced Fast uses the policy-owned wire value.
|
|
134
|
+
const callerTier = typeof rawBody.service_tier === "string" ? rawBody.service_tier : undefined;
|
|
135
|
+
const tierDecision = decideTier(fastPolicy, fastMode, callerTier);
|
|
136
|
+
if (tierDecision.kind === "set") {
|
|
137
|
+
body.service_tier = fastMode === undefined && canonicalFastTierMarker(callerTier) !== undefined
|
|
138
|
+
? callerTier
|
|
139
|
+
: tierDecision.value;
|
|
140
|
+
} else if (tierDecision.kind === "forward-caller" && rawBody.service_tier !== undefined) {
|
|
141
|
+
body.service_tier = rawBody.service_tier;
|
|
142
|
+
}
|
|
143
|
+
if (provider.promptCacheKey && rawBody.prompt_cache_key !== undefined) {
|
|
144
|
+
body.prompt_cache_key = rawBody.prompt_cache_key;
|
|
145
|
+
}
|
|
146
|
+
if (Array.isArray(rawBody.tools) && rawBody.tools.length > 0) {
|
|
147
|
+
if (provider.parallelToolCalls === true) {
|
|
148
|
+
body.parallel_tool_calls = rawBody.parallel_tool_calls !== false;
|
|
149
|
+
} else if (provider.parallelToolCalls === false
|
|
150
|
+
&& (provider.baseUrl === "https://integrate.api.nvidia.com/v1" || provider.pinParallelToolCallsFalse === true)) {
|
|
151
|
+
body.parallel_tool_calls = false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (stream) {
|
|
155
|
+
const callerOptions = rawBody.stream_options !== null
|
|
156
|
+
&& typeof rawBody.stream_options === "object"
|
|
157
|
+
&& !Array.isArray(rawBody.stream_options)
|
|
158
|
+
? rawBody.stream_options as Record<string, unknown>
|
|
159
|
+
: {};
|
|
160
|
+
body.stream_options = { ...callerOptions, include_usage: true };
|
|
161
|
+
} else if (rawBody.stream_options !== undefined) {
|
|
162
|
+
body.stream_options = rawBody.stream_options;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const bodyJson = JSON.stringify(body);
|
|
166
|
+
|
|
167
|
+
if (isDebugEnabled()) {
|
|
168
|
+
let host = "upstream";
|
|
169
|
+
try { host = new URL(url).host; } catch { /* keep fallback */ }
|
|
170
|
+
debugProviderDiagnostic("openai-chat", "passthrough-request", {
|
|
171
|
+
host,
|
|
172
|
+
model: body.model,
|
|
173
|
+
stream,
|
|
174
|
+
messageCount: Array.isArray(body.messages) ? body.messages.length : 0,
|
|
175
|
+
toolCount: Array.isArray(body.tools) ? body.tools.length : 0,
|
|
176
|
+
hasCredential,
|
|
177
|
+
bodyBytes: new TextEncoder().encode(bodyJson).length,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { url, method: "POST", headers, body: bodyJson };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 260715 (issue #126): surface upstream error detail through the web-search sidecar loop.
|
|
185
|
+
// loop.ts only appends a suffix to "Provider error N" when the adapter exposes
|
|
186
|
+
// formatErrorBody; without it, strict OpenAI-compatible backends (NVIDIA NIM pydantic
|
|
187
|
+
// validation, "This model only supports single tool-calls at once!", etc.) were reduced
|
|
188
|
+
// to a bare status code. JSON-only extraction: recognized string fields are returned,
|
|
189
|
+
// HTML/non-JSON bodies yield "" so raw markup is never echoed to the client.
|
|
190
|
+
export function formatOpenAIChatErrorBody(status: number, _headers: Headers, payloadText: string): string {
|
|
191
|
+
let parsed: unknown;
|
|
192
|
+
try {
|
|
193
|
+
parsed = JSON.parse(payloadText);
|
|
194
|
+
} catch {
|
|
195
|
+
return "";
|
|
196
|
+
}
|
|
197
|
+
const detail = extractErrorDetail(parsed);
|
|
198
|
+
if (!detail) return "";
|
|
199
|
+
return redactSecretString(detail).slice(0, 400);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function extractErrorDetail(parsed: unknown): string | undefined {
|
|
203
|
+
if (typeof parsed === "string") return parsed.trim() || undefined;
|
|
204
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
205
|
+
const obj = parsed as Record<string, unknown>;
|
|
206
|
+
const err = obj.error;
|
|
207
|
+
if (typeof err === "string" && err.trim()) return err.trim();
|
|
208
|
+
if (err !== null && typeof err === "object" && !Array.isArray(err)) {
|
|
209
|
+
const msg = (err as Record<string, unknown>).message;
|
|
210
|
+
if (typeof msg === "string" && msg.trim()) return msg.trim();
|
|
211
|
+
}
|
|
212
|
+
const det = obj.detail;
|
|
213
|
+
if (typeof det === "string" && det.trim()) return det.trim();
|
|
214
|
+
if (Array.isArray(det)) {
|
|
215
|
+
const msgs = det
|
|
216
|
+
.map(item => (item !== null && typeof item === "object" && typeof (item as Record<string, unknown>).msg === "string"
|
|
217
|
+
? ((item as Record<string, unknown>).msg as string).trim()
|
|
218
|
+
: ""))
|
|
219
|
+
.filter(m => m.length > 0);
|
|
220
|
+
if (msgs.length > 0) return msgs.join("; ");
|
|
221
|
+
}
|
|
222
|
+
if (typeof obj.message === "string" && obj.message.trim()) return obj.message.trim();
|
|
223
|
+
if (typeof obj.title === "string" && obj.title.trim()) return obj.title.trim();
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function unwrapChatCompletionPayload(json: Record<string, unknown>): Record<string, unknown> {
|
|
228
|
+
if ((json.error !== undefined && json.error !== null) || Array.isArray(json.choices)) return json;
|
|
229
|
+
const data = json.data;
|
|
230
|
+
return data !== null && typeof data === "object" && !Array.isArray(data)
|
|
231
|
+
? data as Record<string, unknown>
|
|
232
|
+
: json;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
interface OpenAIChatError {
|
|
236
|
+
message?: unknown;
|
|
237
|
+
code?: unknown;
|
|
238
|
+
type?: unknown;
|
|
239
|
+
status?: unknown;
|
|
240
|
+
metadata?: unknown;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function safeUpstreamRequestId(metadata: unknown): string | undefined {
|
|
244
|
+
if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) return undefined;
|
|
245
|
+
const record = metadata as Record<string, unknown>;
|
|
246
|
+
const value = record.request_id ?? record.requestId;
|
|
247
|
+
if (typeof value !== "string") return undefined;
|
|
248
|
+
const requestId = value.trim();
|
|
249
|
+
return /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(requestId)
|
|
250
|
+
&& redactSecretString(requestId) === requestId
|
|
251
|
+
? requestId
|
|
252
|
+
: undefined;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function upstreamErrorEvent(
|
|
256
|
+
error: unknown,
|
|
257
|
+
usage?: OcxUsage,
|
|
258
|
+
): Extract<AdapterEvent, { type: "error" }> {
|
|
259
|
+
const details = error !== null && typeof error === "object" && !Array.isArray(error)
|
|
260
|
+
? error as OpenAIChatError
|
|
261
|
+
: undefined;
|
|
262
|
+
const rawMessage = typeof error === "string"
|
|
263
|
+
? error.trim() || "upstream error"
|
|
264
|
+
: typeof details?.message === "string" ? details.message : "upstream error";
|
|
265
|
+
const safeMessage = redactSecretString(rawMessage);
|
|
266
|
+
const requestId = safeUpstreamRequestId(details?.metadata);
|
|
267
|
+
const message = requestId !== undefined && !safeMessage.includes(requestId)
|
|
268
|
+
? `${safeMessage} (request ID: ${requestId})`
|
|
269
|
+
: safeMessage;
|
|
270
|
+
const code = typeof details?.code === "string"
|
|
271
|
+
? details.code
|
|
272
|
+
: typeof details?.code === "number" && Number.isFinite(details.code) && Number.isInteger(details.code)
|
|
273
|
+
? String(details.code)
|
|
274
|
+
: undefined;
|
|
275
|
+
const errorType = typeof details?.type === "string" ? details.type : undefined;
|
|
276
|
+
const codeStatus = typeof details?.code === "number"
|
|
277
|
+
&& Number.isInteger(details.code)
|
|
278
|
+
&& details.code >= 100
|
|
279
|
+
&& details.code <= 599
|
|
280
|
+
? details.code
|
|
281
|
+
: undefined;
|
|
282
|
+
const status = isCyberPolicyCode(code)
|
|
283
|
+
? 400
|
|
284
|
+
: typeof details?.status === "number" && Number.isInteger(details.status)
|
|
285
|
+
? details.status
|
|
286
|
+
: codeStatus;
|
|
287
|
+
return {
|
|
288
|
+
type: "error",
|
|
289
|
+
message,
|
|
290
|
+
...(usage !== undefined ? { usage } : {}),
|
|
291
|
+
...(code !== undefined ? { code } : {}),
|
|
292
|
+
...(errorType !== undefined ? { errorType } : {}),
|
|
293
|
+
...(status !== undefined ? { status } : {}),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function stopReasonFor(finishReason: unknown): "max_tokens" | "content_filter" | undefined {
|
|
298
|
+
return finishReason === "length"
|
|
299
|
+
? "max_tokens"
|
|
300
|
+
: finishReason === "content_filter"
|
|
301
|
+
? "content_filter"
|
|
302
|
+
: undefined;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function reasoningTextFrom(record: Record<string, unknown>): string | undefined {
|
|
306
|
+
return typeof record.reasoning_content === "string" && record.reasoning_content.length > 0
|
|
307
|
+
? record.reasoning_content
|
|
308
|
+
: typeof record.reasoning === "string" && record.reasoning.length > 0
|
|
309
|
+
? record.reasoning
|
|
310
|
+
: undefined;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function invalidChoicesEvent(usage?: OcxUsage): Extract<AdapterEvent, { type: "error" }> {
|
|
314
|
+
return {
|
|
315
|
+
type: "error",
|
|
316
|
+
message: "upstream response contained invalid choices",
|
|
317
|
+
...(usage !== undefined ? { usage } : {}),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function invalidToolCallsEvent(
|
|
322
|
+
rawToolCalls: unknown,
|
|
323
|
+
mode: "stream" | "response",
|
|
324
|
+
usage?: OcxUsage,
|
|
325
|
+
diagnosticOverride?: InvalidToolCallDiagnostic,
|
|
326
|
+
): Extract<AdapterEvent, { type: "error" }> {
|
|
327
|
+
// The streamed accumulator knows things a rescan cannot: which field on which pending call
|
|
328
|
+
// was actually rejected. Without the override, a stream carrying accepted padding on call 0
|
|
329
|
+
// and a real defect on call 1 blames call 0, because the stateless scan stops at the first
|
|
330
|
+
// structurally odd value it sees.
|
|
331
|
+
const diagnostic = diagnosticOverride ?? diagnoseInvalidToolCalls(rawToolCalls, mode);
|
|
332
|
+
const detail = diagnostic
|
|
333
|
+
? ` (${diagnostic.reason}${diagnostic.callIndex !== undefined ? `; callIndex=${diagnostic.callIndex}` : ""}; valueType=${diagnostic.valueType})`
|
|
334
|
+
: "";
|
|
335
|
+
return {
|
|
336
|
+
type: "error",
|
|
337
|
+
status: 502,
|
|
338
|
+
errorType: "upstream_error",
|
|
339
|
+
message: `upstream response contained invalid tool calls${detail}`,
|
|
340
|
+
...(usage !== undefined ? { usage } : {}),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* A streamed tool call is only dispatchable once the upstream has named the function.
|
|
346
|
+
*
|
|
347
|
+
* The OpenAI streaming convention puts `function.name` in the first chunk for a tool-call
|
|
348
|
+
* index and leaves later chunks carrying only `arguments` deltas, so a stream that never
|
|
349
|
+
* sends a name is non-conforming for every provider rather than quirky for one. The
|
|
350
|
+
* reference implementations accumulate such a call with an empty name and let the caller
|
|
351
|
+
* fail; we sit at the boundary where it would become a Codex tool-call contract event, so
|
|
352
|
+
* the equivalent is to refuse to emit it.
|
|
353
|
+
*
|
|
354
|
+
* Failing closed rather than dropping is deliberate, and matches #1325: a claimed tool call
|
|
355
|
+
* that silently disappears can leave the matching result orphaned on the next turn. Naming
|
|
356
|
+
* it ourselves is worse still — the id is synthesizable because it is an opaque correlation
|
|
357
|
+
* handle, but a function name is a guess at intent.
|
|
358
|
+
*/
|
|
359
|
+
function unnamedToolCallEvent(usage?: OcxUsage): Extract<AdapterEvent, { type: "error" }> {
|
|
360
|
+
return {
|
|
361
|
+
type: "error",
|
|
362
|
+
message: "upstream streamed a tool call without a function name — cannot dispatch",
|
|
363
|
+
...(usage !== undefined ? { usage } : {}),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
368
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
type InvalidToolCallReason =
|
|
372
|
+
| "tool_calls_not_array"
|
|
373
|
+
| "tool_call_not_object"
|
|
374
|
+
| "tool_call_id_invalid"
|
|
375
|
+
| "tool_call_function_not_object"
|
|
376
|
+
| "tool_call_function_name_invalid"
|
|
377
|
+
| "tool_call_function_name_blank"
|
|
378
|
+
| "tool_call_function_arguments_invalid";
|
|
379
|
+
|
|
380
|
+
type InvalidToolCallDiagnostic = {
|
|
381
|
+
reason: InvalidToolCallReason;
|
|
382
|
+
callIndex?: number;
|
|
383
|
+
valueType: string;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
type InvalidFieldShape =
|
|
387
|
+
| {
|
|
388
|
+
kind: "object";
|
|
389
|
+
knownKeys: string[];
|
|
390
|
+
knownFieldTypes: Record<string, string>;
|
|
391
|
+
hasUnknownKeys: boolean;
|
|
392
|
+
}
|
|
393
|
+
| {
|
|
394
|
+
kind: "array";
|
|
395
|
+
length: number;
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const SAFE_TOOL_CALL_SHAPE_KEYS = [
|
|
399
|
+
"name",
|
|
400
|
+
"type",
|
|
401
|
+
"value",
|
|
402
|
+
"function",
|
|
403
|
+
"arguments",
|
|
404
|
+
"id",
|
|
405
|
+
"index",
|
|
406
|
+
] as const;
|
|
407
|
+
const SAFE_TOOL_CALL_SHAPE_KEY_SET = new Set<string>(SAFE_TOOL_CALL_SHAPE_KEYS);
|
|
408
|
+
|
|
409
|
+
function structuralValueType(value: unknown): string {
|
|
410
|
+
return value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function invalidToolCallField(rawToolCalls: unknown, diagnostic: InvalidToolCallDiagnostic): unknown {
|
|
414
|
+
if (diagnostic.reason === "tool_calls_not_array") return rawToolCalls;
|
|
415
|
+
if (!Array.isArray(rawToolCalls) || diagnostic.callIndex === undefined) return undefined;
|
|
416
|
+
|
|
417
|
+
const rawToolCall = rawToolCalls[diagnostic.callIndex];
|
|
418
|
+
if (diagnostic.reason === "tool_call_not_object") return rawToolCall;
|
|
419
|
+
if (!isRecord(rawToolCall)) return undefined;
|
|
420
|
+
if (diagnostic.reason === "tool_call_function_not_object") return rawToolCall.function;
|
|
421
|
+
|
|
422
|
+
const rawFunction = rawToolCall.function;
|
|
423
|
+
switch (diagnostic.reason) {
|
|
424
|
+
case "tool_call_id_invalid":
|
|
425
|
+
return rawToolCall.id;
|
|
426
|
+
case "tool_call_function_name_invalid":
|
|
427
|
+
return isRecord(rawFunction) ? rawFunction.name : undefined;
|
|
428
|
+
case "tool_call_function_arguments_invalid":
|
|
429
|
+
return isRecord(rawFunction) ? rawFunction.arguments : undefined;
|
|
430
|
+
default:
|
|
431
|
+
return undefined;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function fingerprintInvalidField(value: unknown): InvalidFieldShape | undefined {
|
|
436
|
+
if (Array.isArray(value)) return { kind: "array", length: value.length };
|
|
437
|
+
if (!isRecord(value)) return undefined;
|
|
438
|
+
|
|
439
|
+
const knownKeys: string[] = [];
|
|
440
|
+
const knownFieldTypes: Record<string, string> = {};
|
|
441
|
+
for (const key of SAFE_TOOL_CALL_SHAPE_KEYS) {
|
|
442
|
+
if (!Object.hasOwn(value, key)) continue;
|
|
443
|
+
knownKeys.push(key);
|
|
444
|
+
knownFieldTypes[key] = structuralValueType(value[key]);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
let hasUnknownKeys = false;
|
|
448
|
+
for (const key of Object.keys(value)) {
|
|
449
|
+
if (!SAFE_TOOL_CALL_SHAPE_KEY_SET.has(key)) {
|
|
450
|
+
hasUnknownKeys = true;
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return { kind: "object", knownKeys, knownFieldTypes, hasUnknownKeys };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Streamed string fields are absent when null or undefined (#1731): OpenAI-compatible
|
|
459
|
+
* streamers repeat already-sent `id`/`name`/`arguments` as null on continuation deltas.
|
|
460
|
+
* The accumulator and this diagnostic share this predicate so they cannot disagree about
|
|
461
|
+
* which delta was the invalid one.
|
|
462
|
+
*/
|
|
463
|
+
function isInvalidStreamStringField(value: unknown): boolean {
|
|
464
|
+
return value != null && typeof value !== "string";
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Explain only the rejected wire shape, never its values. This diagnostic exists so provider
|
|
469
|
+
* compatibility can be tightened from evidence without retaining tool arguments or credentials.
|
|
470
|
+
*/
|
|
471
|
+
function diagnoseInvalidToolCalls(
|
|
472
|
+
rawToolCalls: unknown,
|
|
473
|
+
mode: "stream" | "response",
|
|
474
|
+
): InvalidToolCallDiagnostic | undefined {
|
|
475
|
+
if (!Array.isArray(rawToolCalls)) {
|
|
476
|
+
return { reason: "tool_calls_not_array", valueType: rawToolCalls === null ? "null" : typeof rawToolCalls };
|
|
477
|
+
}
|
|
478
|
+
for (let callIndex = 0; callIndex < rawToolCalls.length; callIndex++) {
|
|
479
|
+
const rawToolCall = rawToolCalls[callIndex];
|
|
480
|
+
if (!isRecord(rawToolCall)) {
|
|
481
|
+
return {
|
|
482
|
+
reason: "tool_call_not_object",
|
|
483
|
+
callIndex,
|
|
484
|
+
valueType: rawToolCall === null ? "null" : Array.isArray(rawToolCall) ? "array" : typeof rawToolCall,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
if (mode === "stream") {
|
|
488
|
+
// The streamed path validates the pieces it is about to store (#1531): a present
|
|
489
|
+
// `function` must be a record, and a present `name`/`arguments`/`id` must be a string.
|
|
490
|
+
// Blank names are caught later at flush, not here, so they are not diagnosed on this
|
|
491
|
+
// branch. Describe exactly that boundary rather than tightening compatibility in a
|
|
492
|
+
// diagnostic change.
|
|
493
|
+
// #1731: "present" means the same thing here as in the accumulator — null and undefined
|
|
494
|
+
// are both absent, because some OpenAI-compatible streamers repeat already-sent fields
|
|
495
|
+
// as null on continuation deltas. A separate predicate here would diagnose accepted
|
|
496
|
+
// padding as the failure and point compatibility work at the wrong delta.
|
|
497
|
+
const streamFunction = (rawToolCall as { function?: unknown }).function;
|
|
498
|
+
if (streamFunction !== undefined && streamFunction !== null) {
|
|
499
|
+
if (!isRecord(streamFunction)) {
|
|
500
|
+
return {
|
|
501
|
+
reason: "tool_call_function_not_object",
|
|
502
|
+
callIndex,
|
|
503
|
+
valueType: Array.isArray(streamFunction) ? "array" : typeof streamFunction,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
if (isInvalidStreamStringField(streamFunction.name)) {
|
|
507
|
+
return { reason: "tool_call_function_name_invalid", callIndex, valueType: typeof streamFunction.name };
|
|
508
|
+
}
|
|
509
|
+
if (isInvalidStreamStringField(streamFunction.arguments)) {
|
|
510
|
+
return { reason: "tool_call_function_arguments_invalid", callIndex, valueType: typeof streamFunction.arguments };
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (isInvalidStreamStringField(rawToolCall.id)) {
|
|
514
|
+
return { reason: "tool_call_id_invalid", callIndex, valueType: typeof rawToolCall.id };
|
|
515
|
+
}
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
// Precedence must mirror the buffered validator below, or a payload with more than one
|
|
519
|
+
// problem is reported under the wrong reason and sends compatibility work after the wrong
|
|
520
|
+
// shape. That validator checks the `function` container first (`!isRecord(rawToolCall) ||
|
|
521
|
+
// !isRecord(rawToolCall.function)`), then id/name/arguments types together, and only then
|
|
522
|
+
// the blank name.
|
|
523
|
+
if (!isRecord(rawToolCall.function)) {
|
|
524
|
+
return {
|
|
525
|
+
reason: "tool_call_function_not_object",
|
|
526
|
+
callIndex,
|
|
527
|
+
valueType: rawToolCall.function === null ? "null" : Array.isArray(rawToolCall.function) ? "array" : typeof rawToolCall.function,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
if (typeof rawToolCall.id !== "string") {
|
|
531
|
+
return { reason: "tool_call_id_invalid", callIndex, valueType: typeof rawToolCall.id };
|
|
532
|
+
}
|
|
533
|
+
if (typeof rawToolCall.function.name !== "string") {
|
|
534
|
+
return { reason: "tool_call_function_name_invalid", callIndex, valueType: typeof rawToolCall.function.name };
|
|
535
|
+
}
|
|
536
|
+
if (typeof rawToolCall.function.arguments !== "string") {
|
|
537
|
+
return { reason: "tool_call_function_arguments_invalid", callIndex, valueType: typeof rawToolCall.function.arguments };
|
|
538
|
+
}
|
|
539
|
+
// Last, matching the validator: #1531 also rejects a blank or whitespace-only name here,
|
|
540
|
+
// because such a call cannot select a dispatch target. Reporting it as `name_invalid`
|
|
541
|
+
// would claim a type problem for a correctly-typed value, so it gets its own code.
|
|
542
|
+
if (rawToolCall.function.name.trim().length === 0) {
|
|
543
|
+
return { reason: "tool_call_function_name_blank", callIndex, valueType: "string" };
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return undefined;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function logInvalidToolCalls(
|
|
550
|
+
mode: "stream" | "response",
|
|
551
|
+
rawToolCalls: unknown,
|
|
552
|
+
diagnosticOverride?: InvalidToolCallDiagnostic,
|
|
553
|
+
): void {
|
|
554
|
+
if (!isDebugEnabled()) return;
|
|
555
|
+
const diagnostic = diagnosticOverride ?? diagnoseInvalidToolCalls(rawToolCalls, mode);
|
|
556
|
+
if (!diagnostic) return;
|
|
557
|
+
const fieldShape = fingerprintInvalidField(invalidToolCallField(rawToolCalls, diagnostic));
|
|
558
|
+
debugProviderDiagnostic("openai-chat", "invalid-tool-calls", {
|
|
559
|
+
mode,
|
|
560
|
+
...diagnostic,
|
|
561
|
+
...(fieldShape ? { fieldShape } : {}),
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function developerSystemText(message: OcxMessage): string | undefined {
|
|
566
|
+
if (message.role !== "developer") return undefined;
|
|
567
|
+
if (typeof message.content === "string") return message.content;
|
|
568
|
+
if (message.content.some(part => part.type === "image")) return undefined;
|
|
569
|
+
return message.content.map(part => (part as OcxTextContent).text).join("");
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function isNativeOpenAIChatTarget(provider: OcxProviderConfig): boolean {
|
|
573
|
+
try {
|
|
574
|
+
return new URL(provider.baseUrl).hostname === "api.openai.com";
|
|
575
|
+
} catch {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Chat-completions image_url parts for images carried inside a tool result (issue #888). role:"tool"
|
|
582
|
+
* content is text-only on every chat provider, so these ride in a follow-up user message instead of
|
|
583
|
+
* being flattened to the "[image]" marker the model can't actually see. Data URLs and remote https
|
|
584
|
+
* URLs are both valid in image_url.url, unlike Gemini inline_data which needs base64.
|
|
585
|
+
*/
|
|
586
|
+
function toolResultTextForWire(content: string | OcxContentPart[]): string {
|
|
587
|
+
if (typeof content === "string") return content;
|
|
588
|
+
const text = content.filter((p) => p.type === "text").map((p) => (p as OcxTextContent).text).join("");
|
|
589
|
+
if (text) {
|
|
590
|
+
const untransportableImages = content.filter((p) => p.type === "image" && !p.imageUrl).length;
|
|
591
|
+
return `${text}${"[image]".repeat(untransportableImages)}`;
|
|
592
|
+
}
|
|
593
|
+
return contentPartsToText(content);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function toolResultImageChatParts(content: string | OcxContentPart[]): unknown[] {
|
|
597
|
+
if (typeof content === "string") return [];
|
|
598
|
+
const parts: unknown[] = [];
|
|
599
|
+
for (const p of content) {
|
|
600
|
+
if (p.type !== "image" || !p.imageUrl) continue;
|
|
601
|
+
parts.push({ type: "image_url", image_url: { url: p.imageUrl, ...(p.detail ? { detail: p.detail } : {}) } });
|
|
602
|
+
}
|
|
603
|
+
return parts;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] {
|
|
607
|
+
const out: unknown[] = [];
|
|
608
|
+
const { context, options } = parsed;
|
|
609
|
+
const replayCacheScope = parsed._reasoningReplayScope;
|
|
610
|
+
|
|
611
|
+
interface PendingToolCall { id: string; name: string }
|
|
612
|
+
let pendingToolCalls: PendingToolCall[] = [];
|
|
613
|
+
let deferredBarrierMessages: unknown[] = [];
|
|
614
|
+
let pendingToolResultImageParts: unknown[] = [];
|
|
615
|
+
let mintedIdSeq = 0;
|
|
616
|
+
const seenWireCallIds = new Set<string>();
|
|
617
|
+
|
|
618
|
+
const mintCallId = (): string => {
|
|
619
|
+
let id = "";
|
|
620
|
+
do {
|
|
621
|
+
id = `call_ocx_minted_${++mintedIdSeq}`;
|
|
622
|
+
} while (seenWireCallIds.has(id));
|
|
623
|
+
seenWireCallIds.add(id);
|
|
624
|
+
return id;
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
const releaseDeferredBarriers = (): void => {
|
|
628
|
+
if (deferredBarrierMessages.length === 0) return;
|
|
629
|
+
out.push(...deferredBarrierMessages);
|
|
630
|
+
deferredBarrierMessages = [];
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
const flushToolResultImages = (): void => {
|
|
634
|
+
if (pendingToolResultImageParts.length === 0) return;
|
|
635
|
+
out.push({
|
|
636
|
+
role: "user",
|
|
637
|
+
content: [
|
|
638
|
+
{ type: "text", text: "[ocx] image output from the preceding tool result(s):" },
|
|
639
|
+
...pendingToolResultImageParts,
|
|
640
|
+
],
|
|
641
|
+
});
|
|
642
|
+
pendingToolResultImageParts = [];
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
const flushPendingToolCalls = (): void => {
|
|
646
|
+
if (pendingToolCalls.length === 0) return;
|
|
647
|
+
for (const call of pendingToolCalls) {
|
|
648
|
+
out.push({
|
|
649
|
+
role: "tool",
|
|
650
|
+
tool_call_id: call.id,
|
|
651
|
+
content: `[ocx] no tool result was recorded for "${call.name}"; execution status unknown — do not treat this as success, failure, or user-provided input.`,
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
pendingToolCalls = [];
|
|
655
|
+
flushToolResultImages();
|
|
656
|
+
releaseDeferredBarriers();
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
const nativeOpenAI = isNativeOpenAIChatTarget(provider);
|
|
660
|
+
const toolCatalogNudge = shouldInjectNonOpenAIToolCatalogNudge(provider)
|
|
661
|
+
? buildNonOpenAIToolCatalogNudgeForTools(context.tools, options.toolChoice)
|
|
662
|
+
: undefined;
|
|
663
|
+
const developerSystemParts = nativeOpenAI
|
|
664
|
+
? []
|
|
665
|
+
: context.messages
|
|
666
|
+
.map(developerSystemText)
|
|
667
|
+
.filter((part): part is string => part !== undefined && part.length > 0);
|
|
668
|
+
const systemParts = [
|
|
669
|
+
...(context.systemPrompt ?? []),
|
|
670
|
+
...developerSystemParts,
|
|
671
|
+
...(toolCatalogNudge ? [toolCatalogNudge] : []),
|
|
672
|
+
];
|
|
673
|
+
if (systemParts.length > 0) {
|
|
674
|
+
const wireModelId = provider.modelSuffixBracketStrip
|
|
675
|
+
? stripBracketedModelSuffix(parsed.modelId)
|
|
676
|
+
: parsed.modelId;
|
|
677
|
+
const sys = identifyRoutedModel(systemParts.join("\n\n"), wireModelId);
|
|
678
|
+
out.push({ role: "system", content: sys });
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
for (const msg of context.messages) {
|
|
682
|
+
switch (msg.role) {
|
|
683
|
+
case "user":
|
|
684
|
+
case "developer": {
|
|
685
|
+
const parts = typeof msg.content === "string" ? undefined : msg.content as OcxContentPart[];
|
|
686
|
+
const hasImages = parts?.some(p => p.type === "image") ?? false;
|
|
687
|
+
let chatMsg: Record<string, unknown>;
|
|
688
|
+
if (msg.role === "developer" && !hasImages) {
|
|
689
|
+
if (!nativeOpenAI) break;
|
|
690
|
+
const text = typeof msg.content === "string"
|
|
691
|
+
? msg.content
|
|
692
|
+
: parts!.map(p => (p as OcxTextContent).text).join("");
|
|
693
|
+
chatMsg = { role: "developer", content: text };
|
|
694
|
+
} else if (typeof msg.content === "string") {
|
|
695
|
+
chatMsg = { role: "user", content: msg.content };
|
|
696
|
+
} else if (!hasImages) {
|
|
697
|
+
chatMsg = { role: "user", content: parts!.map(p => (p as OcxTextContent).text).join("") };
|
|
698
|
+
} else {
|
|
699
|
+
const chatParts = parts!.map(p => p.type === "image"
|
|
700
|
+
? { type: "image_url", image_url: { url: p.imageUrl, ...(p.detail ? { detail: p.detail } : {}) } }
|
|
701
|
+
: { type: "text", text: (p as OcxTextContent).text });
|
|
702
|
+
chatMsg = { role: "user", content: chatParts };
|
|
703
|
+
}
|
|
704
|
+
if (pendingToolCalls.length > 0) deferredBarrierMessages.push(chatMsg);
|
|
705
|
+
else out.push(chatMsg);
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
case "assistant": {
|
|
709
|
+
const aMsg = msg as OcxAssistantMessage;
|
|
710
|
+
const textParts = aMsg.content.filter(p => p.type === "text") as OcxTextContent[];
|
|
711
|
+
const thinkingParts = aMsg.content.filter(p => p.type === "thinking") as OcxThinkingContent[];
|
|
712
|
+
const toolCalls = aMsg.content.filter(p => p.type === "toolCall") as OcxToolCall[];
|
|
713
|
+
const chatMsg: Record<string, unknown> = { role: "assistant" };
|
|
714
|
+
if (textParts.length > 0) chatMsg.content = textParts.map(p => p.text).join("");
|
|
715
|
+
let reasoningContent = thinkingParts.map(p => p.thinking).join("");
|
|
716
|
+
if (
|
|
717
|
+
reasoningContent.length === 0
|
|
718
|
+
&& toolCalls.length > 0
|
|
719
|
+
&& modelInList(provider.preserveReasoningContentModels, parsed.modelId)
|
|
720
|
+
) {
|
|
721
|
+
const cached = toolCalls
|
|
722
|
+
.map(tc => (tc.id ? peekReasoningForCall(tc.id, replayCacheScope) : undefined))
|
|
723
|
+
.filter((text): text is string => typeof text === "string" && text.length > 0);
|
|
724
|
+
// Parallel calls share one preceding reasoning block, which is
|
|
725
|
+
// recorded under every call id — join unique texts only.
|
|
726
|
+
if (cached.length > 0) {
|
|
727
|
+
reasoningContent = [...new Set(cached)].join("\n");
|
|
728
|
+
} else if (modelInList(provider.requiresReasoningPlaceholderModels ?? provider.preserveReasoningContentModels, parsed.modelId)) {
|
|
729
|
+
// Fallback (extends #950, closes #1193): the replay cache is
|
|
730
|
+
// bounded (64 entries / 256 KiB / 1 h TTL) and always misses on
|
|
731
|
+
// long sessions, and some tool rounds carry no recorded reasoning
|
|
732
|
+
// at all. DeepSeek thinking mode rejects ANY tool_call assistant
|
|
733
|
+
// message missing reasoning_content with HTTP 400, so inject a
|
|
734
|
+
// minimal placeholder rather than emit a bare continuation the
|
|
735
|
+
// upstream will reject. Scoped to requiresReasoningPlaceholderModels
|
|
736
|
+
// (defaulting to the preserve list): preserve-listed providers with
|
|
737
|
+
// toggleable thinking (MiniMax low effort) opt out with `[]` so
|
|
738
|
+
// non-thinking histories are never given a fabricated placeholder.
|
|
739
|
+
reasoningContent = " ";
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
if (reasoningContent.length > 0 && modelInList(provider.preserveReasoningContentModels, parsed.modelId)) {
|
|
743
|
+
chatMsg.reasoning_content = reasoningContent;
|
|
744
|
+
}
|
|
745
|
+
if (chatMsg.content === undefined && toolCalls.length === 0 && chatMsg.reasoning_content === undefined) break;
|
|
746
|
+
flushPendingToolCalls();
|
|
747
|
+
const wireToolCalls = toolCalls.map(tc => {
|
|
748
|
+
let id = tc.id;
|
|
749
|
+
if (!id) id = mintCallId();
|
|
750
|
+
else seenWireCallIds.add(id);
|
|
751
|
+
return { tc, id };
|
|
752
|
+
});
|
|
753
|
+
if (wireToolCalls.length > 0) {
|
|
754
|
+
chatMsg.tool_calls = wireToolCalls.map(({ tc, id }) => ({
|
|
755
|
+
id,
|
|
756
|
+
type: "function",
|
|
757
|
+
function: { name: namespacedToolName(tc.namespace, tc.name), arguments: JSON.stringify(tc.arguments) },
|
|
758
|
+
}));
|
|
759
|
+
if (!chatMsg.content) chatMsg.content = emptyAssistantContent(provider);
|
|
760
|
+
}
|
|
761
|
+
if (chatMsg.reasoning_content !== undefined && chatMsg.content === undefined && chatMsg.tool_calls === undefined) {
|
|
762
|
+
chatMsg.content = emptyAssistantContent(provider);
|
|
763
|
+
}
|
|
764
|
+
out.push(chatMsg);
|
|
765
|
+
pendingToolCalls = wireToolCalls.map(({ tc, id }) => ({ id, name: namespacedToolName(tc.namespace, tc.name) }));
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
768
|
+
case "toolResult": {
|
|
769
|
+
let toolCallId = msg.toolCallId;
|
|
770
|
+
const matchIdx = toolCallId ? pendingToolCalls.findIndex(c => c.id === toolCallId) : -1;
|
|
771
|
+
if (matchIdx >= 0 && toolCallId) {
|
|
772
|
+
out.push({
|
|
773
|
+
role: "tool",
|
|
774
|
+
tool_call_id: toolCallId,
|
|
775
|
+
content: toolResultTextForWire(msg.content),
|
|
776
|
+
});
|
|
777
|
+
pendingToolResultImageParts.push(...toolResultImageChatParts(msg.content));
|
|
778
|
+
pendingToolCalls.splice(matchIdx, 1);
|
|
779
|
+
if (pendingToolCalls.length === 0) {
|
|
780
|
+
flushToolResultImages();
|
|
781
|
+
releaseDeferredBarriers();
|
|
782
|
+
}
|
|
783
|
+
} else {
|
|
784
|
+
if (!toolCallId) toolCallId = `call_orphan_${out.length}`;
|
|
785
|
+
flushPendingToolCalls();
|
|
786
|
+
const name = safeToolName(msg.toolName);
|
|
787
|
+
const cachedReasoning =
|
|
788
|
+
toolCallId && modelInList(provider.preserveReasoningContentModels, parsed.modelId)
|
|
789
|
+
? peekReasoningForCall(toolCallId, replayCacheScope)
|
|
790
|
+
: undefined;
|
|
791
|
+
// Same fallback as the main-assistant path: never emit a bare orphan
|
|
792
|
+
// tool_call continuation on a thinking-mode provider — inject a
|
|
793
|
+
// placeholder when the replay cache missed (the bounded cache can
|
|
794
|
+
// always miss on long sessions), or DeepSeek thinking mode 400s.
|
|
795
|
+
// Gate on the preserve list too: reasoning_content is only ever
|
|
796
|
+
// serialized for preserve-listed models, so a requires-only custom
|
|
797
|
+
// entry must not fabricate it on this path (P2 on #1205).
|
|
798
|
+
// `||` (not `??`): the cache never stores empty strings, but treat a
|
|
799
|
+
// falsy hit as a miss so the placeholder still fires.
|
|
800
|
+
const orphanReasoning =
|
|
801
|
+
cachedReasoning
|
|
802
|
+
|| (modelInList(provider.preserveReasoningContentModels, parsed.modelId)
|
|
803
|
+
&& modelInList(provider.requiresReasoningPlaceholderModels ?? provider.preserveReasoningContentModels, parsed.modelId)
|
|
804
|
+
? " "
|
|
805
|
+
: undefined);
|
|
806
|
+
out.push({
|
|
807
|
+
role: "assistant",
|
|
808
|
+
content: emptyAssistantContent(provider),
|
|
809
|
+
...(orphanReasoning ? { reasoning_content: orphanReasoning } : {}),
|
|
810
|
+
tool_calls: [{
|
|
811
|
+
id: toolCallId,
|
|
812
|
+
type: "function",
|
|
813
|
+
function: { name, arguments: "{}" },
|
|
814
|
+
}],
|
|
815
|
+
});
|
|
816
|
+
seenWireCallIds.add(toolCallId);
|
|
817
|
+
out.push({
|
|
818
|
+
role: "tool",
|
|
819
|
+
tool_call_id: toolCallId,
|
|
820
|
+
content: toolResultTextForWire(msg.content),
|
|
821
|
+
});
|
|
822
|
+
pendingToolResultImageParts.push(...toolResultImageChatParts(msg.content));
|
|
823
|
+
flushToolResultImages();
|
|
824
|
+
}
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
flushPendingToolCalls();
|
|
831
|
+
releaseDeferredBarriers();
|
|
832
|
+
return out;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function safeToolName(name: string | undefined): string {
|
|
836
|
+
const raw = name && name.trim().length > 0 ? name : "tool_result";
|
|
837
|
+
const sanitized = raw.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
838
|
+
return sanitized;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const ZEN_SCHEMA_MAP_KEYS = new Set(["properties", "$defs", "definitions"]);
|
|
842
|
+
const ZEN_DROPPED_SCHEMA_KEYS = new Set(["encrypted"]);
|
|
843
|
+
|
|
844
|
+
function sanitizeZenSchemaMap(value: unknown): unknown {
|
|
845
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return sanitizeZenToolParameters(value);
|
|
846
|
+
const out: Record<string, unknown> = {};
|
|
847
|
+
for (const [name, child] of Object.entries(value as Record<string, unknown>)) {
|
|
848
|
+
out[name] = sanitizeZenToolParameters(child);
|
|
849
|
+
}
|
|
850
|
+
return out;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function sanitizeZenToolParameters(value: unknown): unknown {
|
|
854
|
+
if (Array.isArray(value)) return value.map(sanitizeZenToolParameters);
|
|
855
|
+
if (!value || typeof value !== "object") return value;
|
|
856
|
+
const input = value as Record<string, unknown>;
|
|
857
|
+
const out: Record<string, unknown> = {};
|
|
858
|
+
for (const [key, child] of Object.entries(input)) {
|
|
859
|
+
if (ZEN_DROPPED_SCHEMA_KEYS.has(key)) continue;
|
|
860
|
+
if (key === "required" && Array.isArray(child) && child.length === 0) continue;
|
|
861
|
+
if (key === "type" && Array.isArray(child)) {
|
|
862
|
+
const nonNull = child.filter(entry => entry !== "null");
|
|
863
|
+
if (child.includes("null")) out.nullable = true;
|
|
864
|
+
if (nonNull.length > 0) out.type = nonNull[0];
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
out[key] = ZEN_SCHEMA_MAP_KEYS.has(key) ? sanitizeZenSchemaMap(child) : sanitizeZenToolParameters(child);
|
|
868
|
+
}
|
|
869
|
+
return out;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function ensureZenRootObjectSchema(schema: unknown): Record<string, unknown> {
|
|
873
|
+
const obj = schema && typeof schema === "object" && !Array.isArray(schema)
|
|
874
|
+
? schema as Record<string, unknown>
|
|
875
|
+
: {};
|
|
876
|
+
const compositionKeys = ["oneOf", "anyOf", "allOf"] as const;
|
|
877
|
+
const hasComposition = compositionKeys.some(key => Array.isArray(obj[key]));
|
|
878
|
+
const rootType = obj.type;
|
|
879
|
+
const rootObjectType = rootType === "object" || (Array.isArray(rootType) && rootType.includes("object"));
|
|
880
|
+
if (!hasComposition) {
|
|
881
|
+
const base = sanitizeZenToolParameters(obj) as Record<string, unknown>;
|
|
882
|
+
return rootObjectType && base.type === "object" ? base : { ...base, type: "object" };
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const props: Record<string, unknown> = {};
|
|
886
|
+
const required = new Set<string>();
|
|
887
|
+
if (obj.properties && typeof obj.properties === "object") {
|
|
888
|
+
Object.assign(props, sanitizeZenSchemaMap(obj.properties) as Record<string, unknown>);
|
|
889
|
+
}
|
|
890
|
+
if (Array.isArray(obj.required)) {
|
|
891
|
+
for (const entry of obj.required) if (typeof entry === "string") required.add(entry);
|
|
892
|
+
}
|
|
893
|
+
for (const key of compositionKeys) {
|
|
894
|
+
const variants = obj[key];
|
|
895
|
+
if (!Array.isArray(variants)) continue;
|
|
896
|
+
const mergeRequired = key === "allOf";
|
|
897
|
+
for (const variant of variants) {
|
|
898
|
+
if (!variant || typeof variant !== "object" || Array.isArray(variant)) continue;
|
|
899
|
+
const rec = variant as Record<string, unknown>;
|
|
900
|
+
if (rec.properties && typeof rec.properties === "object") {
|
|
901
|
+
Object.assign(props, sanitizeZenSchemaMap(rec.properties) as Record<string, unknown>);
|
|
902
|
+
}
|
|
903
|
+
if (mergeRequired && Array.isArray(rec.required)) {
|
|
904
|
+
for (const entry of rec.required) if (typeof entry === "string") required.add(entry);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
const merged = sanitizeZenToolParameters(obj) as Record<string, unknown>;
|
|
910
|
+
delete merged.oneOf;
|
|
911
|
+
delete merged.anyOf;
|
|
912
|
+
delete merged.allOf;
|
|
913
|
+
merged.type = "object";
|
|
914
|
+
if (Object.keys(props).length > 0) merged.properties = props;
|
|
915
|
+
if (required.size > 0) merged.required = [...required];
|
|
916
|
+
return merged;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function shouldSanitizeZenToolParameters(provider: OcxProviderConfig): boolean {
|
|
920
|
+
const baseUrl = provider.baseUrl.replace(/\/+$/, "");
|
|
921
|
+
return baseUrl === "https://opencode.ai/zen/v1"
|
|
922
|
+
|| baseUrl === "https://opencode.ai/zen/go/v1";
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function isXaiSchemaTarget(provider: OcxProviderConfig): boolean {
|
|
926
|
+
try {
|
|
927
|
+
// Public api.x.ai accepts native root object unions. Only the Grok CLI proxy
|
|
928
|
+
// 400s on a root oneOf/anyOf, so flattening/omitting is scoped to that host.
|
|
929
|
+
return new URL(provider.baseUrl).hostname === "cli-chat-proxy.grok.com";
|
|
930
|
+
} catch {
|
|
931
|
+
return false;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const VOLCENGINE_ARK_HOSTNAMES = new Set([
|
|
936
|
+
"ark.cn-beijing.volces.com",
|
|
937
|
+
"ark.ap-southeast.volces.com",
|
|
938
|
+
]);
|
|
939
|
+
|
|
940
|
+
function isVolcengineArkPaygChatTarget(provider: OcxProviderConfig): boolean {
|
|
941
|
+
try {
|
|
942
|
+
const url = new URL(provider.baseUrl);
|
|
943
|
+
const pathname = url.pathname.replace(/\/+$/, "") || "/";
|
|
944
|
+
return VOLCENGINE_ARK_HOSTNAMES.has(url.hostname) && pathname === "/api/v3";
|
|
945
|
+
} catch {
|
|
946
|
+
return false;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function emptyAssistantContent(provider: OcxProviderConfig): string | { type: "text"; text: string }[] {
|
|
951
|
+
return isVolcengineArkPaygChatTarget(provider) ? [{ type: "text", text: "" }] : "";
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function ensureRootObjectType(parameters: unknown): Record<string, unknown> {
|
|
955
|
+
if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) {
|
|
956
|
+
return { type: "object", properties: {} };
|
|
957
|
+
}
|
|
958
|
+
const obj = parameters as Record<string, unknown>;
|
|
959
|
+
if (obj.type === "object") return obj;
|
|
960
|
+
return { ...obj, type: "object" };
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function isXaiObjectSchema(value: unknown): value is Record<string, unknown> {
|
|
964
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function stringRequiredFields(value: unknown): string[] {
|
|
968
|
+
return Array.isArray(value)
|
|
969
|
+
? value.filter((item): item is string => typeof item === "string")
|
|
970
|
+
: [];
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/** Variant keys the merger can keep. Anything else is refused, not silently dropped. */
|
|
974
|
+
const XAI_VARIANT_MERGE_KEYS = new Set([
|
|
975
|
+
"type",
|
|
976
|
+
"properties",
|
|
977
|
+
"required",
|
|
978
|
+
"additionalProperties",
|
|
979
|
+
"description",
|
|
980
|
+
"title",
|
|
981
|
+
"$comment",
|
|
982
|
+
"$defs",
|
|
983
|
+
"definitions",
|
|
984
|
+
]);
|
|
985
|
+
|
|
986
|
+
function decodeJsonPointerToken(token: string): string {
|
|
987
|
+
return token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function lookupLocalJsonPointer(root: unknown, ref: string): unknown {
|
|
991
|
+
if (ref === "#" || ref === "#/") return root;
|
|
992
|
+
if (!ref.startsWith("#/")) return undefined;
|
|
993
|
+
let current: unknown = root;
|
|
994
|
+
for (const token of ref.slice(2).split("/").map(decodeJsonPointerToken)) {
|
|
995
|
+
if (!isXaiObjectSchema(current) || !Object.hasOwn(current, token)) return undefined;
|
|
996
|
+
current = current[token];
|
|
997
|
+
}
|
|
998
|
+
return current;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/** Resolve local `#/` `$ref`s. Unresolvable or cyclic refs return undefined. */
|
|
1002
|
+
function resolveXaiSchemaRefs(
|
|
1003
|
+
schema: unknown,
|
|
1004
|
+
root: Record<string, unknown>,
|
|
1005
|
+
stack: Set<string> = new Set(),
|
|
1006
|
+
): unknown | undefined {
|
|
1007
|
+
if (!isXaiObjectSchema(schema)) return schema;
|
|
1008
|
+
if (typeof schema.$ref === "string") {
|
|
1009
|
+
const ref = schema.$ref;
|
|
1010
|
+
if (stack.has(ref)) return undefined;
|
|
1011
|
+
const target = lookupLocalJsonPointer(root, ref);
|
|
1012
|
+
if (target === undefined) return undefined;
|
|
1013
|
+
stack.add(ref);
|
|
1014
|
+
const resolvedTarget = resolveXaiSchemaRefs(target, root, stack);
|
|
1015
|
+
stack.delete(ref);
|
|
1016
|
+
if (resolvedTarget === undefined) return undefined;
|
|
1017
|
+
const rest: Record<string, unknown> = { ...schema };
|
|
1018
|
+
delete rest.$ref;
|
|
1019
|
+
if (Object.keys(rest).length === 0) return resolvedTarget;
|
|
1020
|
+
const resolvedRest = resolveXaiSchemaRefs(rest, root, stack);
|
|
1021
|
+
if (resolvedRest === undefined || !isXaiObjectSchema(resolvedTarget) || !isXaiObjectSchema(resolvedRest)) {
|
|
1022
|
+
return undefined;
|
|
1023
|
+
}
|
|
1024
|
+
return composeXaiObjectSchemas(resolvedTarget, resolvedRest);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const resolved: Record<string, unknown> = {};
|
|
1028
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
1029
|
+
if ((key === "oneOf" || key === "anyOf") && Array.isArray(value)) {
|
|
1030
|
+
const items: unknown[] = [];
|
|
1031
|
+
for (const item of value) {
|
|
1032
|
+
const next = resolveXaiSchemaRefs(item, root, stack);
|
|
1033
|
+
if (next === undefined) return undefined;
|
|
1034
|
+
items.push(next);
|
|
1035
|
+
}
|
|
1036
|
+
resolved[key] = items;
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
if (key === "properties" && isXaiObjectSchema(value)) {
|
|
1040
|
+
const properties: Record<string, unknown> = {};
|
|
1041
|
+
for (const [name, property] of Object.entries(value)) {
|
|
1042
|
+
const next = resolveXaiSchemaRefs(property, root, stack);
|
|
1043
|
+
if (next === undefined) return undefined;
|
|
1044
|
+
properties[name] = next;
|
|
1045
|
+
}
|
|
1046
|
+
resolved[key] = properties;
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
resolved[key] = value;
|
|
1050
|
+
}
|
|
1051
|
+
return resolved;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function xaiVariantIsConcreteObject(variant: Record<string, unknown>): boolean {
|
|
1055
|
+
if (variant.type !== undefined && variant.type !== "object") return false;
|
|
1056
|
+
return Object.keys(variant).every(key => XAI_VARIANT_MERGE_KEYS.has(key));
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function variantProperties(variant: Record<string, unknown>): Record<string, unknown> {
|
|
1060
|
+
return isXaiObjectSchema(variant.properties) ? variant.properties : {};
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Independent per-property anyOf is lossless only when every property name exists
|
|
1065
|
+
* on every variant (absence is meaningful under xAI's default additionalProperties:
|
|
1066
|
+
* false, and promoting a branch-local key also tightens explicit-true variants)
|
|
1067
|
+
* and at most one of those shared properties has a conflicting schema.
|
|
1068
|
+
*/
|
|
1069
|
+
function xaiPropertyMergeIsLossless(variants: Record<string, unknown>[]): boolean {
|
|
1070
|
+
const names = new Set<string>();
|
|
1071
|
+
const props = variants.map(variant => {
|
|
1072
|
+
const properties = variantProperties(variant);
|
|
1073
|
+
for (const name of Object.keys(properties)) names.add(name);
|
|
1074
|
+
return properties;
|
|
1075
|
+
});
|
|
1076
|
+
let schemaConflicts = 0;
|
|
1077
|
+
for (const name of names) {
|
|
1078
|
+
const values = props.map(property => property[name]);
|
|
1079
|
+
if (values.some(value => value === undefined)) return false;
|
|
1080
|
+
if (values.some(value => JSON.stringify(value) !== JSON.stringify(values[0]))) schemaConflicts += 1;
|
|
1081
|
+
}
|
|
1082
|
+
return schemaConflicts <= 1;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
function xaiRequiredSetsMatch(variants: Record<string, unknown>[]): boolean {
|
|
1086
|
+
const serialized = variants.map(variant => [...stringRequiredFields(variant.required)].sort().join("\0"));
|
|
1087
|
+
return serialized.every(value => value === serialized[0]);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function mergeXaiAdditionalProperties(
|
|
1091
|
+
variants: Record<string, unknown>[],
|
|
1092
|
+
): { ok: true; value?: unknown } | { ok: false } {
|
|
1093
|
+
const values = variants.map(variant => variant.additionalProperties);
|
|
1094
|
+
const explicit = values.filter(value => value !== undefined);
|
|
1095
|
+
if (explicit.length === 0) return { ok: true };
|
|
1096
|
+
if (explicit.length !== values.length) return { ok: false };
|
|
1097
|
+
const hasFalse = explicit.some(value => value === false);
|
|
1098
|
+
const permissive = explicit.filter(value => value !== false);
|
|
1099
|
+
if (hasFalse && permissive.length > 0) return { ok: false };
|
|
1100
|
+
if (hasFalse) return { ok: true, value: false };
|
|
1101
|
+
const unique: unknown[] = [];
|
|
1102
|
+
const seen = new Set<string>();
|
|
1103
|
+
for (const value of permissive) {
|
|
1104
|
+
const key = JSON.stringify(value);
|
|
1105
|
+
if (seen.has(key)) continue;
|
|
1106
|
+
seen.add(key);
|
|
1107
|
+
unique.push(value);
|
|
1108
|
+
}
|
|
1109
|
+
if (unique.length !== 1) return { ok: false };
|
|
1110
|
+
return { ok: true, value: unique[0] };
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/** Compose root siblings into a branch so properties/required are not overwritten. */
|
|
1114
|
+
function composeXaiObjectSchemas(
|
|
1115
|
+
inherited: Record<string, unknown>,
|
|
1116
|
+
branch: Record<string, unknown>,
|
|
1117
|
+
): Record<string, unknown> {
|
|
1118
|
+
const composed: Record<string, unknown> = { ...inherited, ...branch };
|
|
1119
|
+
const inheritedProps = isXaiObjectSchema(inherited.properties) ? inherited.properties : undefined;
|
|
1120
|
+
const branchProps = isXaiObjectSchema(branch.properties) ? branch.properties : undefined;
|
|
1121
|
+
if (inheritedProps || branchProps) {
|
|
1122
|
+
const properties: Record<string, unknown> = { ...(inheritedProps ?? {}) };
|
|
1123
|
+
for (const [name, value] of Object.entries(branchProps ?? {})) {
|
|
1124
|
+
const inheritedValue = inheritedProps?.[name];
|
|
1125
|
+
properties[name] = inheritedValue !== undefined && JSON.stringify(inheritedValue) !== JSON.stringify(value)
|
|
1126
|
+
? { allOf: [inheritedValue, value] }
|
|
1127
|
+
: value;
|
|
1128
|
+
}
|
|
1129
|
+
composed.properties = properties;
|
|
1130
|
+
}
|
|
1131
|
+
const required = [...new Set([
|
|
1132
|
+
...stringRequiredFields(inherited.required),
|
|
1133
|
+
...stringRequiredFields(branch.required),
|
|
1134
|
+
])];
|
|
1135
|
+
if (required.length > 0) composed.required = required;
|
|
1136
|
+
else delete composed.required;
|
|
1137
|
+
return composed;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function expandXaiRootObjectSchemas(schema: unknown): Record<string, unknown>[] | undefined {
|
|
1141
|
+
if (!isXaiObjectSchema(schema)) return undefined;
|
|
1142
|
+
const compositionKey = ["oneOf", "anyOf"].find(key => Array.isArray(schema[key]));
|
|
1143
|
+
if (!compositionKey) {
|
|
1144
|
+
if (schema.type !== undefined && schema.type !== "object") return undefined;
|
|
1145
|
+
return [{ ...schema, type: "object" }];
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
const siblings = Object.fromEntries(Object.entries(schema).filter(([key]) => key !== compositionKey));
|
|
1149
|
+
const branches = schema[compositionKey];
|
|
1150
|
+
if (!Array.isArray(branches)) return undefined;
|
|
1151
|
+
const expanded: Record<string, unknown>[] = [];
|
|
1152
|
+
for (const branch of branches) {
|
|
1153
|
+
const variants = expandXaiRootObjectSchemas(branch);
|
|
1154
|
+
if (!variants) return undefined;
|
|
1155
|
+
for (const variant of variants) expanded.push(composeXaiObjectSchemas(siblings, variant));
|
|
1156
|
+
}
|
|
1157
|
+
return expanded.length > 0 ? expanded : undefined;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function mergeXaiPropertySchemas(values: unknown[]): unknown {
|
|
1161
|
+
const unique: unknown[] = [];
|
|
1162
|
+
const serialized = new Set<string>();
|
|
1163
|
+
for (const value of values) {
|
|
1164
|
+
const key = JSON.stringify(value);
|
|
1165
|
+
if (serialized.has(key)) continue;
|
|
1166
|
+
serialized.add(key);
|
|
1167
|
+
unique.push(value);
|
|
1168
|
+
}
|
|
1169
|
+
return unique.length === 1 ? unique[0] : { anyOf: unique };
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* The Grok CLI proxy rejects a function parameter schema whose root remains oneOf/anyOf.
|
|
1174
|
+
* Flatten only when the merge is lossless: local $refs resolve, every variant is a concrete
|
|
1175
|
+
* object whose keys we can preserve, required sets match, additionalProperties does not change
|
|
1176
|
+
* meaning, every property name exists on every variant, and at most one property schema
|
|
1177
|
+
* differs. Otherwise omit the tool rather than emit a weaker schema.
|
|
1178
|
+
*/
|
|
1179
|
+
function normalizeXaiToolParameters(parameters: unknown): Record<string, unknown> | undefined {
|
|
1180
|
+
if (!isXaiObjectSchema(parameters)) return undefined;
|
|
1181
|
+
const resolved = resolveXaiSchemaRefs(parameters, parameters);
|
|
1182
|
+
if (!isXaiObjectSchema(resolved)) return undefined;
|
|
1183
|
+
|
|
1184
|
+
const normalizedRoot = { ...resolved };
|
|
1185
|
+
delete normalizedRoot.$schema;
|
|
1186
|
+
|
|
1187
|
+
const variants = expandXaiRootObjectSchemas(normalizedRoot);
|
|
1188
|
+
if (!variants) return undefined;
|
|
1189
|
+
if (variants.length === 1) {
|
|
1190
|
+
return xaiVariantIsConcreteObject(variants[0]) ? variants[0] : undefined;
|
|
1191
|
+
}
|
|
1192
|
+
if (!variants.every(xaiVariantIsConcreteObject) || !xaiRequiredSetsMatch(variants)) return undefined;
|
|
1193
|
+
const additionalProperties = mergeXaiAdditionalProperties(variants);
|
|
1194
|
+
if (!additionalProperties.ok) return undefined;
|
|
1195
|
+
if (!xaiPropertyMergeIsLossless(variants)) return undefined;
|
|
1196
|
+
|
|
1197
|
+
const metadata = Object.fromEntries(Object.entries(normalizedRoot).filter(([key]) => key !== "oneOf" && key !== "anyOf" && key !== "type"));
|
|
1198
|
+
delete metadata.properties;
|
|
1199
|
+
delete metadata.required;
|
|
1200
|
+
delete metadata.additionalProperties;
|
|
1201
|
+
|
|
1202
|
+
const propertyValues = new Map<string, unknown[]>();
|
|
1203
|
+
for (const variant of variants) {
|
|
1204
|
+
if (!variant.properties || typeof variant.properties !== "object" || Array.isArray(variant.properties)) continue;
|
|
1205
|
+
for (const [name, value] of Object.entries(variant.properties as Record<string, unknown>)) {
|
|
1206
|
+
const values = propertyValues.get(name) ?? [];
|
|
1207
|
+
values.push(value);
|
|
1208
|
+
propertyValues.set(name, values);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
const properties = Object.fromEntries(
|
|
1213
|
+
[...propertyValues].map(([name, values]) => [name, mergeXaiPropertySchemas(values)]),
|
|
1214
|
+
);
|
|
1215
|
+
const required = stringRequiredFields(variants[0]?.required);
|
|
1216
|
+
|
|
1217
|
+
return {
|
|
1218
|
+
...metadata,
|
|
1219
|
+
type: "object",
|
|
1220
|
+
properties,
|
|
1221
|
+
...(required.length > 0 ? { required } : {}),
|
|
1222
|
+
...("value" in additionalProperties ? { additionalProperties: additionalProperties.value } : {}),
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function toolsToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] | undefined {
|
|
1227
|
+
if (!parsed.context.tools || parsed.context.tools.length === 0) return undefined;
|
|
1228
|
+
const tools = parsed.context.tools.filter(toolChoiceToolPredicate(parsed.options.toolChoice, parsed.context.tools));
|
|
1229
|
+
if (tools.length === 0) return undefined;
|
|
1230
|
+
const xaiTarget = isXaiSchemaTarget(provider);
|
|
1231
|
+
const formatted = tools.flatMap(t => {
|
|
1232
|
+
const parameters = stripResponsesOnlyEncryptedMarker(xaiTarget
|
|
1233
|
+
? normalizeXaiToolParameters(t.parameters)
|
|
1234
|
+
: ensureRootObjectType(t.parameters));
|
|
1235
|
+
|
|
1236
|
+
if (parameters === undefined) return [];
|
|
1237
|
+
return [{
|
|
1238
|
+
type: "function",
|
|
1239
|
+
function: {
|
|
1240
|
+
name: namespacedToolName(t.namespace, t.name),
|
|
1241
|
+
...(t.description ? { description: t.description } : {}),
|
|
1242
|
+
parameters,
|
|
1243
|
+
...(t.strict !== undefined ? { strict: t.strict } : {}),
|
|
1244
|
+
},
|
|
1245
|
+
}];
|
|
1246
|
+
});
|
|
1247
|
+
return formatted.length > 0 ? formatted : undefined;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function toolsToChatFormatForProvider(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] | undefined {
|
|
1251
|
+
const base = toolsToChatFormat(parsed, provider);
|
|
1252
|
+
if (!base || !shouldSanitizeZenToolParameters(provider)) return base;
|
|
1253
|
+
return base.map(tool => {
|
|
1254
|
+
if (!tool || typeof tool !== "object") return tool;
|
|
1255
|
+
const functionDef = (tool as { function?: Record<string, unknown> }).function;
|
|
1256
|
+
if (!functionDef || typeof functionDef !== "object") return tool;
|
|
1257
|
+
return {
|
|
1258
|
+
...tool,
|
|
1259
|
+
function: {
|
|
1260
|
+
...functionDef,
|
|
1261
|
+
parameters: ensureZenRootObjectSchema(functionDef.parameters ?? {}),
|
|
1262
|
+
},
|
|
1263
|
+
};
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
function toolChoiceToChatFormat(
|
|
1268
|
+
tc: OcxParsedRequest["options"]["toolChoice"],
|
|
1269
|
+
tools: OcxParsedRequest["context"]["tools"],
|
|
1270
|
+
provider: OcxProviderConfig,
|
|
1271
|
+
): unknown {
|
|
1272
|
+
if (!tc) return undefined;
|
|
1273
|
+
if (isAllowedToolChoice(tc)) {
|
|
1274
|
+
if (tc.mode === "required" && tc.allowedTools.length === 1 && isNativeOpenAIChatTarget(provider)) {
|
|
1275
|
+
return { type: "function", function: { name: resolveToolChoiceWireName(tools, tc.allowedTools[0]) } };
|
|
1276
|
+
}
|
|
1277
|
+
return tc.mode === "required" ? "required" : "auto";
|
|
1278
|
+
}
|
|
1279
|
+
if (tc === "auto" || tc === "none" || tc === "required") return tc;
|
|
1280
|
+
if ("name" in tc) return { type: "function", function: { name: resolveToolChoiceWireName(tools, tc.name) } };
|
|
1281
|
+
return undefined;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function usageFromOpenAIChat(usage: Record<string, unknown> | undefined): OcxUsage | undefined {
|
|
1285
|
+
if (!usage) return undefined;
|
|
1286
|
+
const promptDetails = usage.prompt_tokens_details as Record<string, number> | undefined;
|
|
1287
|
+
const completionDetails = usage.completion_tokens_details as Record<string, number> | undefined;
|
|
1288
|
+
return {
|
|
1289
|
+
inputTokens: typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : 0,
|
|
1290
|
+
outputTokens: typeof usage.completion_tokens === "number" ? usage.completion_tokens : 0,
|
|
1291
|
+
...(promptDetails?.cached_tokens !== undefined ? { cachedInputTokens: promptDetails.cached_tokens } : {}),
|
|
1292
|
+
...(completionDetails?.reasoning_tokens !== undefined ? { reasoningOutputTokens: completionDetails.reasoning_tokens } : {}),
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function resolveMaxTokens(provider: OcxProviderConfig, parsed: OcxParsedRequest): number | undefined {
|
|
1297
|
+
return parsed.options.maxOutputTokens
|
|
1298
|
+
?? modelRecordValue(provider.modelMaxOutputTokens, parsed.modelId)
|
|
1299
|
+
?? provider.defaultMaxOutputTokens;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
function thinkingBudgetForEffort(parsed: OcxParsedRequest, reasoningEffort: string, maxOutputTokens?: number): number | undefined {
|
|
1303
|
+
if (parsed.options.reasoning === "minimal") return 0;
|
|
1304
|
+
const maxBudget = maxOutputTokens ?? 32768;
|
|
1305
|
+
const fractions: Record<string, number> = {
|
|
1306
|
+
low: 0.20,
|
|
1307
|
+
medium: 0.50,
|
|
1308
|
+
high: 0.75,
|
|
1309
|
+
xhigh: 0.90,
|
|
1310
|
+
max: 1.0,
|
|
1311
|
+
};
|
|
1312
|
+
const fraction = fractions[reasoningEffort];
|
|
1313
|
+
return fraction === undefined ? undefined : Math.max(1, Math.floor(maxBudget * fraction));
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function canSerializeOpenAIChatServiceTier(
|
|
1317
|
+
provider: OcxProviderConfig,
|
|
1318
|
+
modelId: string,
|
|
1319
|
+
serviceTier: unknown,
|
|
1320
|
+
tierDecision?: OcxParsedRequest["options"]["tierDecision"],
|
|
1321
|
+
): boolean {
|
|
1322
|
+
if (serviceTier === undefined) return false;
|
|
1323
|
+
if (tierDecision !== undefined) {
|
|
1324
|
+
return tierDecision.kind === "set" || tierDecision.kind === "forward-caller";
|
|
1325
|
+
}
|
|
1326
|
+
// No decision from the router means this call did not go through the tier state machine, so
|
|
1327
|
+
// ask that machine rather than re-deriving a looser answer beside it. The previous fallback
|
|
1328
|
+
// returned true whenever foreign forwarding was allowed at all, which let a caller tier
|
|
1329
|
+
// reach the wire in cases `decideTier` would have dropped — the two paths disagreeing is
|
|
1330
|
+
// precisely the bug, so there is now only one authority.
|
|
1331
|
+
const callerTier = typeof serviceTier === "string" ? serviceTier : undefined;
|
|
1332
|
+
const decision = decideTier(fastPolicyForModel(provider, modelId, undefined, "chat"), undefined, callerTier);
|
|
1333
|
+
return decision.kind === "set" || decision.kind === "forward-caller";
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAdapter {
|
|
1337
|
+
return {
|
|
1338
|
+
name: "openai-chat",
|
|
1339
|
+
|
|
1340
|
+
formatErrorBody: formatOpenAIChatErrorBody,
|
|
1341
|
+
|
|
1342
|
+
buildRequest(parsed: OcxParsedRequest) {
|
|
1343
|
+
const { url, headers, hasCredential } = openAIChatTransport(provider);
|
|
1344
|
+
const messages = messagesToChatFormat(parsed, provider);
|
|
1345
|
+
const tools = toolsToChatFormatForProvider(parsed, provider);
|
|
1346
|
+
const toolChoice = toolChoiceToChatFormat(parsed.options.toolChoice, parsed.context.tools, provider);
|
|
1347
|
+
|
|
1348
|
+
const body: Record<string, unknown> = {
|
|
1349
|
+
model: provider.modelSuffixBracketStrip ? stripBracketedModelSuffix(parsed.modelId) : parsed.modelId,
|
|
1350
|
+
messages,
|
|
1351
|
+
stream: parsed.stream,
|
|
1352
|
+
};
|
|
1353
|
+
// A policy-produced canonical decision has already passed capability validation. Without
|
|
1354
|
+
// that decision, a canonical caller value still requires an explicit true capability;
|
|
1355
|
+
// unclassified Chat routes remain behind the caller-forwarding opt-in.
|
|
1356
|
+
const serviceTier = parsed.options.serviceTier;
|
|
1357
|
+
const tierDecision = parsed.options.tierDecision;
|
|
1358
|
+
const canSerializeServiceTier = canSerializeOpenAIChatServiceTier(
|
|
1359
|
+
provider,
|
|
1360
|
+
parsed.modelId,
|
|
1361
|
+
serviceTier,
|
|
1362
|
+
tierDecision,
|
|
1363
|
+
);
|
|
1364
|
+
if (canSerializeServiceTier && serviceTier !== undefined) {
|
|
1365
|
+
body.service_tier = serviceTier;
|
|
1366
|
+
}
|
|
1367
|
+
if (modelInList(provider.reasoningSplitModels, parsed.modelId)) body.reasoning_split = true;
|
|
1368
|
+
const maxTokens = resolveMaxTokens(provider, parsed);
|
|
1369
|
+
const openRouterRouting = resolveOpenRouterRouting(provider, parsed.modelId);
|
|
1370
|
+
if (openRouterRouting) body.provider = openRouterProviderPayload(openRouterRouting);
|
|
1371
|
+
if (tools) body.tools = tools;
|
|
1372
|
+
if (tools && toolChoice !== undefined) {
|
|
1373
|
+
body.tool_choice = modelInList(provider.autoToolChoiceOnlyModels, parsed.modelId)
|
|
1374
|
+
? (toolChoice === "none" ? "none" : "auto")
|
|
1375
|
+
: toolChoice;
|
|
1376
|
+
}
|
|
1377
|
+
if (maxTokens !== undefined) body.max_tokens = maxTokens;
|
|
1378
|
+
if (parsed.options.temperature !== undefined && !modelInList(provider.noTemperatureModels, parsed.modelId)) {
|
|
1379
|
+
body.temperature = parsed.options.temperature;
|
|
1380
|
+
}
|
|
1381
|
+
if (parsed.options.topP !== undefined && !modelInList(provider.noTopPModels, parsed.modelId)) {
|
|
1382
|
+
body.top_p = parsed.options.topP;
|
|
1383
|
+
}
|
|
1384
|
+
if (parsed.options.stopSequences !== undefined) body.stop = parsed.options.stopSequences;
|
|
1385
|
+
const reasoningDisabled = modelInList(provider.noReasoningModels, parsed.modelId);
|
|
1386
|
+
const reasoningEffort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning);
|
|
1387
|
+
const nativeOpenAI = isNativeOpenAIChatTarget(provider);
|
|
1388
|
+
let reasoningLog: AdapterRequest["reasoningLog"];
|
|
1389
|
+
if (!reasoningDisabled && provider.reasoningWireFormat === "gateway-object" && parsed.options.reasoning === "none") {
|
|
1390
|
+
if (nativeOpenAI) {
|
|
1391
|
+
body.reasoning_effort = "none";
|
|
1392
|
+
reasoningLog = {
|
|
1393
|
+
effectiveEffort: "none",
|
|
1394
|
+
wireField: "reasoning_effort",
|
|
1395
|
+
wireValue: "none",
|
|
1396
|
+
};
|
|
1397
|
+
} else {
|
|
1398
|
+
body.reasoning = { enabled: false };
|
|
1399
|
+
reasoningLog = {
|
|
1400
|
+
effectiveEffort: "none",
|
|
1401
|
+
wireField: "reasoning.enabled",
|
|
1402
|
+
wireValue: false,
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
} else if (reasoningEffort !== undefined) {
|
|
1406
|
+
if (provider.reasoningWireFormat === "gateway-object") {
|
|
1407
|
+
if (nativeOpenAI) {
|
|
1408
|
+
body.reasoning_effort = reasoningEffort;
|
|
1409
|
+
reasoningLog = {
|
|
1410
|
+
effectiveEffort: reasoningEffort,
|
|
1411
|
+
wireField: "reasoning_effort",
|
|
1412
|
+
wireValue: reasoningEffort,
|
|
1413
|
+
};
|
|
1414
|
+
} else {
|
|
1415
|
+
body.reasoning = { enabled: true, effort: reasoningEffort };
|
|
1416
|
+
reasoningLog = {
|
|
1417
|
+
effectiveEffort: reasoningEffort,
|
|
1418
|
+
wireField: "reasoning.effort",
|
|
1419
|
+
wireValue: reasoningEffort,
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
} else if (modelInList(provider.thinkingBudgetModels, parsed.modelId)) {
|
|
1423
|
+
const budget = thinkingBudgetForEffort(parsed, reasoningEffort, maxTokens);
|
|
1424
|
+
if (budget !== undefined) {
|
|
1425
|
+
body.thinking_budget = budget;
|
|
1426
|
+
reasoningLog = {
|
|
1427
|
+
effectiveEffort: parsed.options.reasoning === "minimal" ? "minimal" : reasoningEffort,
|
|
1428
|
+
wireField: "thinking_budget",
|
|
1429
|
+
wireValue: budget,
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
} else if (modelInList(provider.thinkingToggleModels, parsed.modelId)) {
|
|
1433
|
+
if (reasoningEffort === "enabled" || reasoningEffort === "disabled" || reasoningEffort === "adaptive") {
|
|
1434
|
+
body.thinking = { type: reasoningEffort };
|
|
1435
|
+
reasoningLog = {
|
|
1436
|
+
effectiveEffort: reasoningEffort,
|
|
1437
|
+
wireField: "thinking.type",
|
|
1438
|
+
wireValue: reasoningEffort,
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
} else {
|
|
1442
|
+
body.reasoning_effort = reasoningEffort;
|
|
1443
|
+
reasoningLog = {
|
|
1444
|
+
effectiveEffort: reasoningEffort,
|
|
1445
|
+
wireField: "reasoning_effort",
|
|
1446
|
+
wireValue: reasoningEffort,
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
if (parsed.options.presencePenalty !== undefined && !modelInList(provider.noPenaltyModels, parsed.modelId)) {
|
|
1451
|
+
body.presence_penalty = parsed.options.presencePenalty;
|
|
1452
|
+
}
|
|
1453
|
+
if (parsed.options.frequencyPenalty !== undefined && !modelInList(provider.noPenaltyModels, parsed.modelId)) {
|
|
1454
|
+
body.frequency_penalty = parsed.options.frequencyPenalty;
|
|
1455
|
+
}
|
|
1456
|
+
if (provider.promptCacheKey && parsed.options.promptCacheKey !== undefined) {
|
|
1457
|
+
body.prompt_cache_key = parsed.options.promptCacheKey;
|
|
1458
|
+
}
|
|
1459
|
+
// Structured-output support varies by the physical upstream model even when one
|
|
1460
|
+
// gateway exposes a uniform OpenAI-compatible endpoint. Keep the #1137 translation
|
|
1461
|
+
// as the default, but let an exact model opt out instead of forcing a provider-wide
|
|
1462
|
+
// rollback that would silently return prose for siblings that support JSON Schema.
|
|
1463
|
+
if (!provider.noStructuredOutputModels?.includes(parsed.modelId)) {
|
|
1464
|
+
const textFormat = parsed.options.textFormat;
|
|
1465
|
+
if (textFormat?.type === "json_object") {
|
|
1466
|
+
body.response_format = { type: "json_object" };
|
|
1467
|
+
} else if (textFormat?.type === "json_schema") {
|
|
1468
|
+
body.response_format = {
|
|
1469
|
+
type: "json_schema",
|
|
1470
|
+
json_schema: {
|
|
1471
|
+
name: textFormat.name ?? "response",
|
|
1472
|
+
...(textFormat.description !== undefined ? { description: textFormat.description } : {}),
|
|
1473
|
+
...(textFormat.schema !== undefined ? { schema: textFormat.schema } : {}),
|
|
1474
|
+
...(textFormat.strict !== undefined ? { strict: textFormat.strict } : {}),
|
|
1475
|
+
},
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
if (tools) {
|
|
1481
|
+
if (provider.parallelToolCalls === false) {
|
|
1482
|
+
// NIM documents the Boolean defaulting to false and kimi rejects true; pin the
|
|
1483
|
+
// wire bit so Codex cannot opt in via request.options. Other opted-out providers
|
|
1484
|
+
// omit the field by default so strict OpenAI-compatible hosts never see an
|
|
1485
|
+
// unsupported knob, but a self-hosted gateway that DOES honor the field and keeps
|
|
1486
|
+
// emitting parallel calls without it can opt in via pinParallelToolCallsFalse.
|
|
1487
|
+
if (provider.baseUrl === "https://integrate.api.nvidia.com/v1"
|
|
1488
|
+
|| provider.pinParallelToolCallsFalse === true) {
|
|
1489
|
+
body.parallel_tool_calls = false;
|
|
1490
|
+
}
|
|
1491
|
+
} else if (provider.parallelToolCalls === true) {
|
|
1492
|
+
body.parallel_tool_calls = parsed.options.parallelToolCalls !== false;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
if (parsed.stream) body.stream_options = { include_usage: true };
|
|
1496
|
+
|
|
1497
|
+
const bodyJson = JSON.stringify(body);
|
|
1498
|
+
const actualServiceTier = typeof body.service_tier === "string" ? body.service_tier : null;
|
|
1499
|
+
const tierLog = createAdapterTierMetadata(
|
|
1500
|
+
parsed.options.tierObservation,
|
|
1501
|
+
parsed.options.tierDecision,
|
|
1502
|
+
actualServiceTier === null ? null : "service-tier",
|
|
1503
|
+
actualServiceTier,
|
|
1504
|
+
);
|
|
1505
|
+
if (isDebugEnabled()) {
|
|
1506
|
+
let host = "upstream";
|
|
1507
|
+
try { host = new URL(url).host; } catch { /* keep fallback */ }
|
|
1508
|
+
debugProviderDiagnostic("openai-chat", "request", {
|
|
1509
|
+
host,
|
|
1510
|
+
model: body.model,
|
|
1511
|
+
stream: parsed.stream,
|
|
1512
|
+
messageCount: Array.isArray(messages) ? messages.length : 0,
|
|
1513
|
+
toolCount: Array.isArray(tools) ? tools.length : 0,
|
|
1514
|
+
hasCredential,
|
|
1515
|
+
bodyBytes: new TextEncoder().encode(bodyJson).length,
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
return {
|
|
1520
|
+
url,
|
|
1521
|
+
method: "POST",
|
|
1522
|
+
headers,
|
|
1523
|
+
body: bodyJson,
|
|
1524
|
+
...(reasoningLog ? { reasoningLog } : {}),
|
|
1525
|
+
...(tierLog ? { tierLog } : {}),
|
|
1526
|
+
};
|
|
1527
|
+
},
|
|
1528
|
+
|
|
1529
|
+
async *parseStream(
|
|
1530
|
+
response: Response,
|
|
1531
|
+
budget: TranslatorBudget,
|
|
1532
|
+
tierMetadata?: AdapterTierMetadata,
|
|
1533
|
+
): AsyncGenerator<AdapterEvent> {
|
|
1534
|
+
if (!response.body) {
|
|
1535
|
+
yield { type: "error", message: "No response body" };
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
const reader = response.body.getReader();
|
|
1540
|
+
const decoder = new TextDecoder();
|
|
1541
|
+
const budgetEncoder = new TextEncoder();
|
|
1542
|
+
let buffer = "";
|
|
1543
|
+
let bufferBytes = 0;
|
|
1544
|
+
interface PendingToolCall {
|
|
1545
|
+
key: string;
|
|
1546
|
+
id: string;
|
|
1547
|
+
name: string;
|
|
1548
|
+
args: string;
|
|
1549
|
+
argsBytes: number;
|
|
1550
|
+
/**
|
|
1551
|
+
* Whether this call has ever received `arguments` as an actual string, empty included.
|
|
1552
|
+
* An empty string still counts: it proves the upstream sent the field with the right
|
|
1553
|
+
* wire type, which is what a later malformed repeat of that field would be padding for.
|
|
1554
|
+
* A canonical NAME is not evidence about the ARGUMENTS field and must not stand in.
|
|
1555
|
+
*/
|
|
1556
|
+
sawArgumentsString: boolean;
|
|
1557
|
+
}
|
|
1558
|
+
const pendingToolCalls: PendingToolCall[] = [];
|
|
1559
|
+
let toolCallSeq = 0;
|
|
1560
|
+
const closeToolCalls = (): PendingToolCall[] => {
|
|
1561
|
+
const calls = [...pendingToolCalls];
|
|
1562
|
+
for (const call of calls) budget.closeCall(call.key);
|
|
1563
|
+
pendingToolCalls.length = 0;
|
|
1564
|
+
return calls;
|
|
1565
|
+
};
|
|
1566
|
+
const pendingToolCallsAreCompleteJsonObjects = (): boolean =>
|
|
1567
|
+
pendingToolCalls.length > 0 && pendingToolCalls.every(call => {
|
|
1568
|
+
if (call.name.trim().length === 0 || !call.sawArgumentsString || call.args.length === 0) return false;
|
|
1569
|
+
try {
|
|
1570
|
+
const parsed = JSON.parse(call.args) as unknown;
|
|
1571
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed);
|
|
1572
|
+
} catch {
|
|
1573
|
+
return false;
|
|
1574
|
+
}
|
|
1575
|
+
});
|
|
1576
|
+
// Returns "terminate" when a pending call cannot be dispatched, so every flush site
|
|
1577
|
+
// stops the turn instead of emitting an unusable call. `closeToolCalls()` runs first,
|
|
1578
|
+
// so budget reservations are released for every pending call even on the early return.
|
|
1579
|
+
const flushToolCalls = function* (): Generator<AdapterEvent, "continue" | "terminate"> {
|
|
1580
|
+
for (const call of closeToolCalls()) {
|
|
1581
|
+
// Ingest already proved `name` is a string; the typeof guard keeps this branch
|
|
1582
|
+
// total so a future ingest change cannot turn a malformed name into a throw.
|
|
1583
|
+
if (typeof call.name !== "string" || call.name.trim().length === 0) {
|
|
1584
|
+
debugProviderDiagnostic("openai-chat", "tool-call-unnamed", {
|
|
1585
|
+
hadId: call.id.length > 0,
|
|
1586
|
+
argsBytes: call.argsBytes,
|
|
1587
|
+
});
|
|
1588
|
+
yield unnamedToolCallEvent(pendingUsage);
|
|
1589
|
+
return "terminate";
|
|
1590
|
+
}
|
|
1591
|
+
if (!call.id) call.id = `call_${++toolCallSeq}`;
|
|
1592
|
+
yield { type: "tool_call_start", id: call.id, name: call.name };
|
|
1593
|
+
if (call.args.length > 0) yield { type: "tool_call_delta", arguments: call.args };
|
|
1594
|
+
yield { type: "tool_call_end" };
|
|
1595
|
+
}
|
|
1596
|
+
return "continue";
|
|
1597
|
+
};
|
|
1598
|
+
const terminateWithError = function* (
|
|
1599
|
+
event: Extract<AdapterEvent, { type: "error" }>,
|
|
1600
|
+
): Generator<AdapterEvent, "terminate"> {
|
|
1601
|
+
closeToolCalls();
|
|
1602
|
+
yield event;
|
|
1603
|
+
return "terminate";
|
|
1604
|
+
};
|
|
1605
|
+
let pendingUsage: OcxUsage | undefined;
|
|
1606
|
+
let finishReason: string | undefined;
|
|
1607
|
+
let sawUserFacingOutput = false;
|
|
1608
|
+
|
|
1609
|
+
const handleDataLine = function* (line: string): Generator<AdapterEvent, "continue" | "terminate"> {
|
|
1610
|
+
const rawPayload = sseFieldValue(line, "data");
|
|
1611
|
+
if (rawPayload === null) return "continue";
|
|
1612
|
+
const payload = rawPayload.trim();
|
|
1613
|
+
if (payload.length === 0) return "continue";
|
|
1614
|
+
if (payload === "[DONE]") {
|
|
1615
|
+
if ((yield* flushToolCalls()) === "terminate") return "terminate";
|
|
1616
|
+
const stopReason = stopReasonFor(finishReason);
|
|
1617
|
+
yield { type: "done", usage: pendingUsage, ...(stopReason ? { stopReason } : {}) };
|
|
1618
|
+
return "terminate";
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
let parsed: unknown;
|
|
1622
|
+
try {
|
|
1623
|
+
parsed = JSON.parse(payload);
|
|
1624
|
+
} catch {
|
|
1625
|
+
tierMetadata?.markResponseUnparseable();
|
|
1626
|
+
yield { type: "error", message: "malformed upstream SSE data frame" };
|
|
1627
|
+
return "terminate";
|
|
1628
|
+
}
|
|
1629
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return "continue";
|
|
1630
|
+
const chunk = parsed as Record<string, unknown>;
|
|
1631
|
+
if (Object.hasOwn(chunk, "service_tier")) {
|
|
1632
|
+
tierMetadata?.observeResponseServiceTier(chunk.service_tier);
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
if (chunk.error !== undefined && chunk.error !== null) {
|
|
1636
|
+
const event = upstreamErrorEvent(chunk.error, pendingUsage);
|
|
1637
|
+
debugProviderDiagnostic("openai-chat", "stream-error", { message: event.message });
|
|
1638
|
+
return yield* terminateWithError(event);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
if (chunk.usage) pendingUsage = usageFromOpenAIChat(chunk.usage as Record<string, unknown>);
|
|
1642
|
+
|
|
1643
|
+
const choices = chunk.choices;
|
|
1644
|
+
if (choices === undefined) return "continue";
|
|
1645
|
+
if (!Array.isArray(choices)) return yield* terminateWithError(invalidChoicesEvent(pendingUsage));
|
|
1646
|
+
if (choices.length === 0) return "continue";
|
|
1647
|
+
const rawChoice = choices[0];
|
|
1648
|
+
if (rawChoice === null || typeof rawChoice !== "object" || Array.isArray(rawChoice)) {
|
|
1649
|
+
return yield* terminateWithError(invalidChoicesEvent(pendingUsage));
|
|
1650
|
+
}
|
|
1651
|
+
const choice = rawChoice as {
|
|
1652
|
+
delta?: Record<string, unknown>;
|
|
1653
|
+
finish_reason?: string;
|
|
1654
|
+
error?: unknown;
|
|
1655
|
+
};
|
|
1656
|
+
if (choice.finish_reason === "error") {
|
|
1657
|
+
const event = upstreamErrorEvent(choice.error, pendingUsage);
|
|
1658
|
+
debugProviderDiagnostic("openai-chat", "stream-error", { message: event.message });
|
|
1659
|
+
return yield* terminateWithError(event);
|
|
1660
|
+
}
|
|
1661
|
+
if (typeof choice.finish_reason === "string" && choice.finish_reason) finishReason = choice.finish_reason;
|
|
1662
|
+
const delta = choice.delta;
|
|
1663
|
+
if (delta) {
|
|
1664
|
+
const reasoningText = reasoningTextFrom(delta);
|
|
1665
|
+
if (reasoningText !== undefined) yield { type: "reasoning_raw_delta", text: reasoningText };
|
|
1666
|
+
if (typeof delta.content === "string" && delta.content.length > 0) {
|
|
1667
|
+
sawUserFacingOutput = true;
|
|
1668
|
+
yield { type: "text_delta", text: delta.content };
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
const rawToolCalls = delta.tool_calls;
|
|
1672
|
+
if (rawToolCalls !== undefined && rawToolCalls !== null) {
|
|
1673
|
+
// A non-null claimed tool-call payload is not benign padding. Dropping it can leave the
|
|
1674
|
+
// matching result permanently orphaned, so malformed nested shapes fail closed
|
|
1675
|
+
// through the adapter error channel instead of escaping as TypeError (#1325). Null is
|
|
1676
|
+
// tolerated as absent because OpenAI-compatible providers may emit it as stream padding.
|
|
1677
|
+
if (!Array.isArray(rawToolCalls)) {
|
|
1678
|
+
logInvalidToolCalls("stream", rawToolCalls);
|
|
1679
|
+
return yield* terminateWithError(invalidToolCallsEvent(rawToolCalls, "stream", pendingUsage));
|
|
1680
|
+
}
|
|
1681
|
+
for (let callIndex = 0; callIndex < rawToolCalls.length; callIndex++) {
|
|
1682
|
+
const rawToolCall: unknown = rawToolCalls[callIndex];
|
|
1683
|
+
if (!isRecord(rawToolCall)) {
|
|
1684
|
+
const diagnostic: InvalidToolCallDiagnostic = {
|
|
1685
|
+
reason: "tool_call_not_object",
|
|
1686
|
+
callIndex,
|
|
1687
|
+
valueType: rawToolCall === null ? "null" : Array.isArray(rawToolCall) ? "array" : typeof rawToolCall,
|
|
1688
|
+
};
|
|
1689
|
+
logInvalidToolCalls("stream", rawToolCalls, diagnostic);
|
|
1690
|
+
return yield* terminateWithError(invalidToolCallsEvent(rawToolCalls, "stream", pendingUsage, diagnostic));
|
|
1691
|
+
}
|
|
1692
|
+
// This is upstream JSON, so every field is validated before it is stored: a
|
|
1693
|
+
// malformed value must fail closed through the #1325 channel here rather than
|
|
1694
|
+
// escaping later as a TypeError from string handling at flush time.
|
|
1695
|
+
const rawFunction = rawToolCall.function;
|
|
1696
|
+
if (rawFunction !== undefined && rawFunction !== null && !isRecord(rawFunction)) {
|
|
1697
|
+
const diagnostic: InvalidToolCallDiagnostic = {
|
|
1698
|
+
reason: "tool_call_function_not_object",
|
|
1699
|
+
callIndex,
|
|
1700
|
+
valueType: Array.isArray(rawFunction) ? "array" : typeof rawFunction,
|
|
1701
|
+
};
|
|
1702
|
+
logInvalidToolCalls("stream", rawToolCalls, diagnostic);
|
|
1703
|
+
return yield* terminateWithError(invalidToolCallsEvent(rawToolCalls, "stream", pendingUsage, diagnostic));
|
|
1704
|
+
}
|
|
1705
|
+
const fnRecord = isRecord(rawFunction) ? rawFunction : undefined;
|
|
1706
|
+
const rawName = fnRecord?.name;
|
|
1707
|
+
const rawArguments = fnRecord?.arguments;
|
|
1708
|
+
const rawId = rawToolCall.id;
|
|
1709
|
+
const idDelta = typeof rawId === "string" ? rawId : "";
|
|
1710
|
+
const rawIndex = rawToolCall.index;
|
|
1711
|
+
|
|
1712
|
+
// Resolve the pending call BEFORE judging the fields. Some OpenAI-compatible
|
|
1713
|
+
// streamers repeat an already-sent field as a non-string placeholder on a
|
|
1714
|
+
// continuation delta; judging first meant the whole stream died with a 502 even
|
|
1715
|
+
// though the value being repeated was already held in canonical form.
|
|
1716
|
+
const key = typeof rawIndex === "number"
|
|
1717
|
+
? `i:${rawIndex}`
|
|
1718
|
+
: idDelta
|
|
1719
|
+
? `id:${idDelta}`
|
|
1720
|
+
: pendingToolCalls[pendingToolCalls.length - 1]?.key;
|
|
1721
|
+
let call = key !== undefined ? pendingToolCalls.find(c => c.key === key) : undefined;
|
|
1722
|
+
if (!call && idDelta) call = pendingToolCalls.find(c => c.id === idDelta);
|
|
1723
|
+
if (!call) {
|
|
1724
|
+
call = {
|
|
1725
|
+
key: key ?? `seq:${pendingToolCalls.length}`,
|
|
1726
|
+
id: "",
|
|
1727
|
+
name: "",
|
|
1728
|
+
args: "",
|
|
1729
|
+
argsBytes: 0,
|
|
1730
|
+
sawArgumentsString: false,
|
|
1731
|
+
};
|
|
1732
|
+
pendingToolCalls.push(call);
|
|
1733
|
+
budget.openCall(call.key);
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
// Tolerance is per FIELD, keyed on that field's own provenance. A canonical name
|
|
1737
|
+
// says nothing about whether `arguments` was ever sent as a string, so it cannot
|
|
1738
|
+
// authorize a malformed arguments value — that would silently drop a real
|
|
1739
|
+
// argument payload the model intended to send.
|
|
1740
|
+
const rejection: InvalidToolCallDiagnostic | undefined =
|
|
1741
|
+
isInvalidStreamStringField(rawName) && call.name.trim() === ""
|
|
1742
|
+
? { reason: "tool_call_function_name_invalid", callIndex, valueType: typeof rawName }
|
|
1743
|
+
: isInvalidStreamStringField(rawArguments) && !call.sawArgumentsString
|
|
1744
|
+
? { reason: "tool_call_function_arguments_invalid", callIndex, valueType: typeof rawArguments }
|
|
1745
|
+
: isInvalidStreamStringField(rawId) && call.id === ""
|
|
1746
|
+
? { reason: "tool_call_id_invalid", callIndex, valueType: typeof rawId }
|
|
1747
|
+
: undefined;
|
|
1748
|
+
if (rejection) {
|
|
1749
|
+
logInvalidToolCalls("stream", rawToolCalls, rejection);
|
|
1750
|
+
return yield* terminateWithError(invalidToolCallsEvent(rawToolCalls, "stream", pendingUsage, rejection));
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
if (idDelta && !call.id) call.id = idDelta;
|
|
1754
|
+
if (typeof rawName === "string" && rawName && !call.name) call.name = rawName;
|
|
1755
|
+
if (typeof rawArguments === "string") call.sawArgumentsString = true;
|
|
1756
|
+
// Tool-call deltas are BUFFERED until a terminal signal, so this adapter can
|
|
1757
|
+
// consume upstream frames for a long time while yielding nothing. The Responses
|
|
1758
|
+
// bridge reads adapter activity, not socket activity, so a model that streams a
|
|
1759
|
+
// large argument payload looks identical to a hung upstream and the stall
|
|
1760
|
+
// watchdog can abort a turn that was progressing normally.
|
|
1761
|
+
//
|
|
1762
|
+
// Found while investigating #2156, but it is NOT that bug: a stall abort emits
|
|
1763
|
+
// `response.incomplete` with `upstream_stall_timeout` from the bridge, whereas
|
|
1764
|
+
// that report shows the adapter's own end-of-stream error after `reader.read()`
|
|
1765
|
+
// returned EOF with tool calls still pending. Different path, different frame.
|
|
1766
|
+
//
|
|
1767
|
+
// A heartbeat is invisible downstream — the bridge consumes it to re-arm the
|
|
1768
|
+
// watchdog and emits nothing — which is the same remedy the Cursor, Anthropic,
|
|
1769
|
+
// Google, and Kiro adapters already use for their own silent phases.
|
|
1770
|
+
yield { type: "heartbeat" };
|
|
1771
|
+
if (typeof rawArguments === "string" && rawArguments) {
|
|
1772
|
+
const previousBytes = call.argsBytes;
|
|
1773
|
+
const nextBytes = previousBytes + budgetEncoder.encode(rawArguments).byteLength;
|
|
1774
|
+
const scope = { kind: "tool_args" as const, callId: call.key };
|
|
1775
|
+
const reservation = budget.reserveTransient(nextBytes, scope);
|
|
1776
|
+
try {
|
|
1777
|
+
call.args += rawArguments;
|
|
1778
|
+
reservation.commitRetained();
|
|
1779
|
+
budget.releaseRetained(previousBytes, scope);
|
|
1780
|
+
call.argsBytes = nextBytes;
|
|
1781
|
+
} catch (error) {
|
|
1782
|
+
reservation.release();
|
|
1783
|
+
throw error;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
if (typeof choice.finish_reason === "string" && choice.finish_reason) {
|
|
1791
|
+
if ((yield* flushToolCalls()) === "terminate") return "terminate";
|
|
1792
|
+
}
|
|
1793
|
+
return "continue";
|
|
1794
|
+
};
|
|
1795
|
+
|
|
1796
|
+
try {
|
|
1797
|
+
while (true) {
|
|
1798
|
+
const { done, value } = await reader.read();
|
|
1799
|
+
if (done) break;
|
|
1800
|
+
const decoded = decoder.decode(value, { stream: true });
|
|
1801
|
+
const nextBufferBytes = bufferBytes + budgetEncoder.encode(decoded).byteLength;
|
|
1802
|
+
if (nextBufferBytes > TRANSLATOR_MAX_SSE_EVENT_BYTES) {
|
|
1803
|
+
throw new Error(`translation SSE event exceeded ${TRANSLATOR_MAX_SSE_EVENT_BYTES} bytes`, {
|
|
1804
|
+
cause: { code: "translation_buffer_limit" },
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
const appendReservation = budget.reserveTransient(nextBufferBytes, { kind: "live_transient" });
|
|
1808
|
+
try {
|
|
1809
|
+
buffer += decoded;
|
|
1810
|
+
appendReservation.commitRetained();
|
|
1811
|
+
budget.releaseRetained(bufferBytes, { kind: "live_transient" });
|
|
1812
|
+
} catch (error) {
|
|
1813
|
+
appendReservation.release();
|
|
1814
|
+
throw error;
|
|
1815
|
+
}
|
|
1816
|
+
bufferBytes = nextBufferBytes;
|
|
1817
|
+
|
|
1818
|
+
const lines = buffer.split("\n");
|
|
1819
|
+
buffer = lines.pop() ?? "";
|
|
1820
|
+
const residualBytes = budgetEncoder.encode(buffer).byteLength;
|
|
1821
|
+
const residualReservation = budget.reserveTransient(residualBytes, { kind: "live_transient" });
|
|
1822
|
+
residualReservation.commitRetained();
|
|
1823
|
+
budget.releaseRetained(bufferBytes, { kind: "live_transient" });
|
|
1824
|
+
bufferBytes = residualBytes;
|
|
1825
|
+
|
|
1826
|
+
for (const line of lines) {
|
|
1827
|
+
if ((yield* handleDataLine(line)) === "terminate") return;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
if (buffer.length > 0) {
|
|
1832
|
+
if ((yield* handleDataLine(buffer)) === "terminate") return;
|
|
1833
|
+
}
|
|
1834
|
+
const sawFinish = finishReason !== undefined;
|
|
1835
|
+
if (!sawFinish && pendingToolCalls.length > 0) {
|
|
1836
|
+
// Some OpenAI-compatible gateways close immediately after a complete function-call
|
|
1837
|
+
// delta and omit both terminal conventions. Keep the default fail-closed policy, and
|
|
1838
|
+
// let an opted-in provider recover only calls whose assembled argument payload is a
|
|
1839
|
+
// complete JSON object. A partial JSON prefix still takes the truncation path below.
|
|
1840
|
+
if (provider.openaiChatEofTolerance === true && pendingToolCallsAreCompleteJsonObjects()) {
|
|
1841
|
+
if ((yield* flushToolCalls()) === "terminate") return;
|
|
1842
|
+
yield { type: "done", usage: pendingUsage };
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
debugProviderDiagnostic("openai-chat", "stream-truncated", {
|
|
1846
|
+
finishReason: null,
|
|
1847
|
+
hadUsage: pendingUsage !== undefined,
|
|
1848
|
+
pendingToolCalls: pendingToolCalls.length,
|
|
1849
|
+
});
|
|
1850
|
+
yield { type: "error", message: "upstream stream ended mid tool call without a terminal signal — possible truncation" };
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
if (!sawFinish && !sawUserFacingOutput) {
|
|
1854
|
+
debugProviderDiagnostic("openai-chat", "stream-truncated", {
|
|
1855
|
+
finishReason: finishReason ?? null,
|
|
1856
|
+
hadUsage: pendingUsage !== undefined,
|
|
1857
|
+
});
|
|
1858
|
+
yield { type: "error", message: "upstream stream ended without a terminal signal ([DONE] or finish_reason) — possible truncation" };
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
if ((yield* flushToolCalls()) === "terminate") return;
|
|
1862
|
+
const stopReason = stopReasonFor(finishReason);
|
|
1863
|
+
yield { type: "done", usage: pendingUsage, ...(stopReason ? { stopReason } : {}) };
|
|
1864
|
+
} catch (error) {
|
|
1865
|
+
if (isTranslatorBudgetExceededError(error)
|
|
1866
|
+
|| (error instanceof Error && (error.cause as { code?: unknown } | undefined)?.code === "translation_buffer_limit")) {
|
|
1867
|
+
yield {
|
|
1868
|
+
type: "error",
|
|
1869
|
+
status: 502,
|
|
1870
|
+
errorType: "upstream_error",
|
|
1871
|
+
code: "translation_buffer_limit",
|
|
1872
|
+
message: "upstream translation buffer exceeded the safe limit",
|
|
1873
|
+
};
|
|
1874
|
+
try { await reader.cancel(error); } catch { /* already closed */ }
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
throw error;
|
|
1878
|
+
} finally {
|
|
1879
|
+
budget.releaseRetained(bufferBytes, { kind: "live_transient" });
|
|
1880
|
+
closeToolCalls();
|
|
1881
|
+
reader.releaseLock();
|
|
1882
|
+
}
|
|
1883
|
+
},
|
|
1884
|
+
|
|
1885
|
+
async parseResponse(
|
|
1886
|
+
response: Response,
|
|
1887
|
+
budget: TranslatorBudget,
|
|
1888
|
+
tierMetadata?: AdapterTierMetadata,
|
|
1889
|
+
): Promise<AdapterEvent[]> {
|
|
1890
|
+
let parsed: unknown;
|
|
1891
|
+
try {
|
|
1892
|
+
parsed = await response.json();
|
|
1893
|
+
} catch (error) {
|
|
1894
|
+
tierMetadata?.markResponseUnparseable();
|
|
1895
|
+
throw error;
|
|
1896
|
+
}
|
|
1897
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1898
|
+
tierMetadata?.markResponseUnparseable();
|
|
1899
|
+
throw new Error("upstream response was not a JSON object");
|
|
1900
|
+
}
|
|
1901
|
+
const json = parsed as Record<string, unknown>;
|
|
1902
|
+
if (Object.hasOwn(json, "service_tier")) {
|
|
1903
|
+
tierMetadata?.observeResponseServiceTier(json.service_tier);
|
|
1904
|
+
}
|
|
1905
|
+
const responseBytes = new TextEncoder().encode(JSON.stringify(json)).byteLength;
|
|
1906
|
+
budget.chargeRetained(responseBytes, { kind: "retained_collectors" });
|
|
1907
|
+
try {
|
|
1908
|
+
const payload = unwrapChatCompletionPayload(json);
|
|
1909
|
+
const usage = usageFromOpenAIChat(payload.usage as Record<string, unknown> | undefined);
|
|
1910
|
+
if (json.success === false && payload.error === undefined) {
|
|
1911
|
+
return [{
|
|
1912
|
+
type: "error",
|
|
1913
|
+
message: "upstream reported failure without an error payload",
|
|
1914
|
+
...(usage ? { usage } : {}),
|
|
1915
|
+
}];
|
|
1916
|
+
}
|
|
1917
|
+
if (payload.error !== undefined && payload.error !== null) return [upstreamErrorEvent(payload.error, usage)];
|
|
1918
|
+
|
|
1919
|
+
const events: AdapterEvent[] = [];
|
|
1920
|
+
const choices = payload.choices as {
|
|
1921
|
+
message?: Record<string, unknown>;
|
|
1922
|
+
finish_reason?: unknown;
|
|
1923
|
+
error?: OpenAIChatError;
|
|
1924
|
+
}[] | undefined;
|
|
1925
|
+
if (!Array.isArray(choices) || choices.length === 0) {
|
|
1926
|
+
return [{ type: "error", message: "upstream response contained no choices", ...(usage ? { usage } : {}) }];
|
|
1927
|
+
}
|
|
1928
|
+
const rawChoice = choices[0];
|
|
1929
|
+
if (rawChoice === null || typeof rawChoice !== "object" || Array.isArray(rawChoice)) {
|
|
1930
|
+
return [invalidChoicesEvent(usage)];
|
|
1931
|
+
}
|
|
1932
|
+
const choice = rawChoice;
|
|
1933
|
+
if (choice.finish_reason === "error") return [upstreamErrorEvent(choice.error, usage)];
|
|
1934
|
+
if (!choice.message) return [{ type: "error", message: "upstream response contained no choices", ...(usage ? { usage } : {}) }];
|
|
1935
|
+
|
|
1936
|
+
const msg = choice.message;
|
|
1937
|
+
const reasoningText = reasoningTextFrom(msg);
|
|
1938
|
+
if (reasoningText !== undefined) events.push({ type: "reasoning_raw_delta", text: reasoningText });
|
|
1939
|
+
if (typeof msg.content === "string") events.push({ type: "text_delta", text: msg.content });
|
|
1940
|
+
const rawToolCalls = msg.tool_calls;
|
|
1941
|
+
if (rawToolCalls !== undefined && rawToolCalls !== null) {
|
|
1942
|
+
if (!Array.isArray(rawToolCalls)) {
|
|
1943
|
+
logInvalidToolCalls("response", rawToolCalls);
|
|
1944
|
+
return [invalidToolCallsEvent(rawToolCalls, "response", usage)];
|
|
1945
|
+
}
|
|
1946
|
+
for (const rawToolCall of rawToolCalls) {
|
|
1947
|
+
if (!isRecord(rawToolCall) || !isRecord(rawToolCall.function)) {
|
|
1948
|
+
logInvalidToolCalls("response", rawToolCalls);
|
|
1949
|
+
return [invalidToolCallsEvent(rawToolCalls, "response", usage)];
|
|
1950
|
+
}
|
|
1951
|
+
const id = rawToolCall.id;
|
|
1952
|
+
const name = rawToolCall.function.name;
|
|
1953
|
+
const args = rawToolCall.function.arguments;
|
|
1954
|
+
// A blank name is as undispatchable as a missing one, so it fails closed here
|
|
1955
|
+
// for the same reason the streamed path refuses it. Trimmed length, not `!name`:
|
|
1956
|
+
// a whitespace-only function name is not a legitimate tool-call shape either.
|
|
1957
|
+
if (typeof id !== "string" || typeof name !== "string" || typeof args !== "string"
|
|
1958
|
+
|| name.trim().length === 0) {
|
|
1959
|
+
logInvalidToolCalls("response", rawToolCalls);
|
|
1960
|
+
return [invalidToolCallsEvent(rawToolCalls, "response", usage)];
|
|
1961
|
+
}
|
|
1962
|
+
events.push({ type: "tool_call_start", id, name });
|
|
1963
|
+
events.push({ type: "tool_call_delta", arguments: args });
|
|
1964
|
+
events.push({ type: "tool_call_end" });
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
const stopReason = stopReasonFor(choice.finish_reason);
|
|
1968
|
+
events.push({
|
|
1969
|
+
type: "done",
|
|
1970
|
+
usage,
|
|
1971
|
+
...(stopReason ? { stopReason } : {}),
|
|
1972
|
+
});
|
|
1973
|
+
retainTranslatedEventBatch(events, budget);
|
|
1974
|
+
return events;
|
|
1975
|
+
} finally {
|
|
1976
|
+
budget.releaseRetained(responseBytes, { kind: "retained_collectors" });
|
|
1977
|
+
}
|
|
1978
|
+
},
|
|
1979
|
+
};
|
|
1980
|
+
}
|