@bitkyc08/opencodex 2.86.0 → 2.88.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/App-CHC89EXQ.js +58 -0
- package/gui/dist/assets/App-wT3mXqFz.css +1 -0
- package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-B5bC7AWL.js} +1 -1
- package/gui/dist/assets/index-B5bsvxor.js +94 -0
- package/gui/dist/assets/index-DPeB9HpV.css +1 -0
- package/gui/dist/assets/quota-summary-D8y2wTsB.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +9 -8
- package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
- package/src/adapters/anthropic/passthrough.ts +7 -23
- package/src/adapters/anthropic-progress.ts +84 -0
- package/src/adapters/anthropic-rejection-memo.ts +71 -0
- package/src/adapters/anthropic-replay.ts +135 -0
- package/src/adapters/anthropic-tool-catalog.ts +11 -0
- package/src/adapters/anthropic.ts +151 -33
- package/src/adapters/base.ts +2 -0
- package/src/adapters/claude-cli/adapter.ts +111 -23
- package/src/adapters/claude-cli/profiles.ts +2 -1
- package/src/adapters/claude-cli/stable-replay.ts +108 -0
- package/src/adapters/claude-cli/usage-admission.ts +230 -0
- package/src/adapters/coding-agent/protocol.ts +1 -1
- package/src/adapters/coding-agent/turn.ts +18 -2
- package/src/adapters/dsh-account.ts +97 -0
- package/src/adapters/google-progress-history.ts +53 -0
- package/src/adapters/google.ts +52 -9
- package/src/adapters/openai-chat/messages.ts +4 -3
- package/src/adapters/openai-chat/passthrough.ts +8 -2
- package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
- package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
- package/src/adapters/openai-responses/passthrough.ts +57 -125
- package/src/adapters/openai-responses/request-strips.ts +25 -3
- package/src/adapters/openai-responses/transport.ts +81 -0
- package/src/adapters/qoder/adapter.ts +4 -2
- package/src/adapters/registry.ts +5 -0
- package/src/adapters/zed.ts +26 -1
- package/src/advisor/activity.ts +55 -0
- package/src/advisor/budget.ts +62 -0
- package/src/advisor/consult.ts +4 -1
- package/src/advisor/context.ts +16 -4
- package/src/advisor/evidence-tool.ts +21 -0
- package/src/advisor/evidence.ts +117 -0
- package/src/advisor/runtime.ts +79 -31
- package/src/advisor/settings.ts +11 -2
- package/src/bridge/errors.ts +19 -1
- package/src/bridge/internal.ts +7 -1
- package/src/bridge/response-json.ts +18 -3
- package/src/bridge/sse.ts +59 -8
- package/src/bridge/terminal-refusal.ts +59 -0
- package/src/claude/desktop-3p-library.ts +32 -7
- package/src/claude/desktop-3p.ts +55 -13
- package/src/claude/desktop-first-party.ts +1 -1
- package/src/claude/desktop-picker-platform.ts +4 -0
- package/src/claude/desktop-picker.ts +15 -8
- package/src/claude/desktop-system-proxy.ts +50 -4
- package/src/claude/inbound-content-options.ts +77 -0
- package/src/claude/inbound.ts +33 -0
- package/src/claude/intercept/picker-ca-startup.ts +2 -1
- package/src/claude/intercept/picker-runtime.ts +6 -5
- package/src/claude/intercept/picker-trust-windows.ts +338 -0
- package/src/claude/intercept/picker-trust.ts +19 -0
- package/src/claude/intercept/port-policy.ts +30 -0
- package/src/claude/intercept/runtime.ts +97 -94
- package/src/claude/outbound.ts +46 -13
- package/src/cli/account-api.ts +1 -0
- package/src/cli/account-policy-dto.ts +6 -0
- package/src/cli/account-policy.ts +34 -5
- package/src/cli/account-preservation.ts +44 -0
- package/src/cli/account.ts +8 -2
- package/src/cli/advisor.ts +17 -3
- package/src/cli/capabilities-accounts.ts +20 -1
- package/src/cli/capabilities-agents-routing.ts +5 -2
- package/src/cli/capabilities-base.ts +38 -10
- package/src/cli/capabilities-observe-system.ts +63 -3
- package/src/cli/capabilities-provider-models.ts +11 -0
- package/src/cli/codex-cli-update.ts +151 -41
- package/src/cli/codex-provider.ts +44 -0
- package/src/cli/combo-input.ts +10 -1
- package/src/cli/combo-result.ts +1 -1
- package/src/cli/combo.ts +2 -1
- package/src/cli/cross-home-owner.ts +33 -16
- package/src/cli/dispatch.ts +25 -4
- package/src/cli/doctor.ts +6 -1
- package/src/cli/ensure-desktop-cold-start.ts +60 -7
- package/src/cli/help.ts +3 -2
- package/src/cli/index.ts +1 -1
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/log-history.ts +172 -0
- package/src/cli/loopback-family.ts +31 -0
- package/src/cli/models-runtime-subcommands.ts +1 -0
- package/src/cli/models-runtime.ts +72 -8
- package/src/cli/observe-stream.ts +2 -0
- package/src/cli/observe.ts +16 -4
- package/src/cli/provider.ts +27 -5
- package/src/cli/registry.ts +28 -8
- package/src/cli/runtime-api.ts +5 -3
- package/src/cli/system-command.ts +40 -8
- package/src/cli/uninstall-client-state.ts +32 -0
- package/src/cli/usage-compare.ts +309 -0
- package/src/cli/usage-model-search.ts +4 -4
- package/src/cli/usage-report.ts +2 -2
- package/src/cli/v2.ts +1 -1
- package/src/client/machine-listener.ts +1 -3
- package/src/client/runtime.ts +1 -0
- package/src/clients/client-path.ts +51 -0
- package/src/clients/config-export/contracts.ts +2 -1
- package/src/clients/config-export.ts +18 -44
- package/src/clients/gentle-shell-path.ts +87 -0
- package/src/codex/account-auto-switch.ts +2 -0
- package/src/codex/account-identity.ts +29 -0
- package/src/codex/account-label.ts +1 -1
- package/src/codex/account-lifecycle.ts +21 -5
- package/src/codex/account-preservation.ts +28 -0
- package/src/codex/account-use-remaining.ts +23 -0
- package/src/codex/app-server-processes.ts +97 -7
- package/src/codex/app-server-restart-service.ts +55 -4
- package/src/codex/auth-api/account-list.ts +4 -0
- package/src/codex/auth-api/account-pause-group.ts +6 -50
- package/src/codex/auth-api/account-preservation.ts +57 -0
- package/src/codex/auth-api/routes.ts +8 -0
- package/src/codex/auth-api/use-remaining-route.ts +36 -0
- package/src/codex/auth-api.ts +0 -1
- package/src/codex/auth-context.ts +2 -2
- package/src/codex/catalog/auto-review.ts +20 -3
- package/src/codex/catalog/build-entries.ts +9 -1
- package/src/codex/catalog/combo-member.ts +14 -5
- package/src/codex/catalog/configured-tiers.ts +1 -1
- package/src/codex/catalog/gather-capture.ts +2 -0
- package/src/codex/catalog/metadata.ts +11 -9
- package/src/codex/catalog/model-hints.ts +21 -16
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/parsing.ts +30 -86
- package/src/codex/catalog/provider-models.ts +36 -34
- package/src/codex/catalog/routed-gather.ts +14 -13
- package/src/codex/cli-install-provenance.ts +105 -17
- package/src/codex/cli-update-plan.ts +545 -0
- package/src/codex/config-write-lock.ts +23 -2
- package/src/codex/desktop-app/picker-freshness.ts +96 -0
- package/src/codex/desktop-app-restart.ts +21 -0
- package/src/codex/desktop-compatibility/installed-build.ts +24 -6
- package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
- package/src/codex/desktop-compatibility/runtime.ts +12 -3
- package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
- package/src/codex/features.ts +85 -45
- package/src/codex/inject/history-failure.ts +27 -0
- package/src/codex/inject/restore.ts +24 -2
- package/src/codex/inject/routing-classify.ts +4 -3
- package/src/codex/inject.ts +18 -27
- package/src/codex/main-account-hard-lock.ts +30 -1
- package/src/codex/native-profile-startup.ts +1 -1
- package/src/codex/ocx-compaction-history.ts +12 -10
- package/src/codex/paths.ts +86 -1
- package/src/codex/provider-routing.ts +174 -0
- package/src/codex/refresh.ts +2 -2
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/selection.ts +4 -3
- package/src/codex/routing/thread-affinity.ts +1 -1
- package/src/codex/signed-in-identity.ts +43 -0
- package/src/codex/subagent-model-fallback.ts +1 -1
- package/src/codex/sync.ts +6 -1
- package/src/codex/user-identity.ts +16 -5
- package/src/codex/websocket-activity.ts +161 -0
- package/src/codex/websocket-registry.ts +25 -1
- package/src/codex/windows-local-appdata.ts +84 -0
- package/src/combos/failover.ts +6 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +46 -10
- package/src/combos/types.ts +15 -0
- package/src/config/diagnostics.ts +33 -6
- package/src/config/live-reconcile.ts +38 -2
- package/src/config/load-degrade.ts +17 -0
- package/src/config/opencode-wire-upgrade.ts +54 -0
- package/src/config/opencode-zen-upgrade.ts +53 -0
- package/src/config/owner-registry.ts +22 -17
- package/src/config/provider-validation.ts +45 -2
- package/src/config/schema/config-schema.ts +22 -0
- package/src/config/schema/leaf-validators.ts +38 -9
- package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
- package/src/config/schema/native-reasoning-retention.ts +26 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +657 -297
- package/src/grok/sync.ts +1 -1
- package/src/images/loop.ts +116 -27
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +1 -0
- package/src/integrations/registry.ts +8 -0
- package/src/lib/codex-restart-contract.ts +15 -0
- package/src/lib/desktop-upstream-tunnel.ts +18 -53
- package/src/lib/empty-tool-catalog.ts +62 -0
- package/src/lib/errors.ts +19 -3
- package/src/lib/gcp-adc.ts +96 -9
- package/src/lib/gcp-credential-input.ts +12 -0
- package/src/lib/lab-live-route-production.ts +5 -0
- package/src/lib/opaque-proxy-tunnel.ts +217 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/request-failure-model.ts +2 -0
- package/src/oauth/account-quota-rank.ts +69 -2
- package/src/oauth/anthropic-account-refusal.ts +20 -2
- package/src/oauth/anthropic-oauth-definitions.ts +3 -1
- package/src/oauth/anthropic-routing.ts +1 -21
- package/src/oauth/anthropic.ts +38 -7
- package/src/oauth/callback-server.ts +27 -1
- package/src/oauth/chatgpt-device.ts +1 -1
- package/src/oauth/chatgpt-shared.ts +83 -0
- package/src/oauth/chatgpt.ts +8 -72
- package/src/oauth/dsh.ts +203 -0
- package/src/oauth/generic-account-failover.ts +40 -11
- package/src/oauth/generic-session-affinity.ts +125 -0
- package/src/oauth/index.ts +184 -109
- package/src/oauth/local-token-detect.ts +21 -1
- package/src/oauth/login-cli-errors.ts +44 -0
- package/src/oauth/login-cli.ts +69 -7
- package/src/oauth/login-flow-state.ts +371 -23
- package/src/oauth/meta-muse-errors.ts +41 -0
- package/src/oauth/meta-muse-keychain.ts +52 -0
- package/src/oauth/meta-muse.ts +51 -84
- package/src/oauth/model-discovery-auth.ts +3 -0
- package/src/oauth/pool-settings-capability.ts +4 -2
- package/src/oauth/refresh-flight-retirement.ts +48 -0
- package/src/oauth/session-affinity-key.ts +23 -0
- package/src/oauth/store-anthropic-instance.ts +9 -2
- package/src/oauth/store.ts +12 -5
- package/src/protocols/encoders/adapter-events.ts +6 -5
- package/src/protocols/encoders/messages.ts +9 -6
- package/src/protocols/provider-summary.ts +2 -1
- package/src/providers/api-key-cooldowns.ts +49 -0
- package/src/providers/api-key-resolve.ts +11 -6
- package/src/providers/api-key-response-ownership.ts +57 -0
- package/src/providers/api-key-selection.ts +31 -6
- package/src/providers/api-keys.ts +214 -3
- package/src/providers/claude-cli-usage.ts +96 -0
- package/src/providers/dsh-account-destination.ts +31 -0
- package/src/providers/dsh-account-models.ts +41 -0
- package/src/providers/gcp-credential-cleanup.ts +59 -0
- package/src/providers/key-failover.ts +54 -50
- package/src/providers/key-store.ts +7 -2
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/model-rename-migration.ts +2 -2
- package/src/providers/openai-tiers-destination.ts +11 -2
- package/src/providers/openai-tiers.ts +5 -12
- package/src/providers/opencode-go-transport.ts +3 -1
- package/src/providers/opencode-zen-rate-limit.ts +7 -2
- package/src/providers/quota/account-cache.ts +6 -5
- package/src/providers/quota/account-row.ts +34 -0
- package/src/providers/quota/antigravity.ts +28 -7
- package/src/providers/quota/dsh-account.ts +70 -0
- package/src/providers/quota/vendor-probes-oauth.ts +1 -93
- package/src/providers/quota-types.ts +2 -0
- package/src/providers/quota.ts +14 -20
- package/src/providers/registry/entries-core.ts +37 -8
- package/src/providers/registry/types.ts +1 -1
- package/src/providers/request-pacing.ts +77 -19
- package/src/providers/routed-config.ts +339 -0
- package/src/providers/service-tier.ts +2 -1
- package/src/quota/window-mapping.ts +5 -2
- package/src/responses/anthropic-progress.ts +34 -0
- package/src/responses/assistant-replay.ts +35 -0
- package/src/responses/compaction-history-framing.ts +65 -0
- package/src/responses/compaction.ts +6 -1
- package/src/responses/incomplete-reason.ts +31 -0
- package/src/responses/parser.ts +50 -5
- package/src/responses/reasoning-envelope.ts +4 -0
- package/src/responses/reasoning-replay-cache.ts +74 -6
- package/src/responses/reasoning-retention.ts +173 -0
- package/src/responses/retained-compaction.ts +46 -0
- package/src/responses/schema.ts +23 -0
- package/src/router.ts +8 -321
- package/src/routing/analytics.ts +7 -15
- package/src/routing/health.ts +37 -22
- package/src/routing/history/indexer.ts +6 -5
- package/src/routing/history/ingest.ts +6 -2
- package/src/routing/profile.ts +1 -1
- package/src/server/admission-model-scope.ts +17 -0
- package/src/server/auth-cors.ts +11 -0
- package/src/server/chat-completions.ts +7 -1
- package/src/server/chat-native-reasoning.ts +123 -0
- package/src/server/chat-native-sse.ts +110 -7
- package/src/server/chat-native.ts +76 -17
- package/src/server/claude-messages.ts +56 -414
- package/src/server/effort-policy.ts +1 -1
- package/src/server/images.ts +15 -2
- package/src/server/index/live-sideband.ts +3 -2
- package/src/server/index/model-alias-id.ts +36 -0
- package/src/server/index/optional-listeners.ts +2 -0
- package/src/server/index/serve-options.ts +20 -6
- package/src/server/index/startup-warnings.ts +2 -0
- package/src/server/index/websocket-handler.ts +18 -4
- package/src/server/index.ts +8 -8
- package/src/server/inference/advisor-diagnostics.ts +31 -0
- package/src/server/lifecycle.ts +13 -1
- package/src/server/management/advisor-routes.ts +15 -4
- package/src/server/management/claude-desktop-picker-routes.ts +1 -1
- package/src/server/management/combo-routes.ts +176 -50
- package/src/server/management/companion-routes.ts +1 -1
- package/src/server/management/config-routes.ts +75 -4
- package/src/server/management/cursor-integration-routes.ts +1 -1
- package/src/server/management/model-routes.ts +11 -6
- package/src/server/management/model-rows.ts +14 -0
- package/src/server/management/native-integration-routes.ts +2 -7
- package/src/server/management/native-reasoning-retention-routes.ts +41 -0
- package/src/server/management/oauth-account-routes.ts +87 -14
- package/src/server/management/provider-gcp-credential-routes.ts +29 -0
- package/src/server/management/provider-key-pacing-routes.ts +31 -0
- package/src/server/management/provider-overwrite-carry.ts +7 -0
- package/src/server/management/provider-routes.ts +25 -30
- package/src/server/management/route-registry.ts +10 -1
- package/src/server/management-api.ts +3 -1
- package/src/server/messages-native-tool-guard.ts +185 -0
- package/src/server/messages-native.ts +117 -24
- package/src/server/messages-shared.ts +431 -0
- package/src/server/relay.ts +1 -1
- package/src/server/request-log-terminal-status.ts +1 -1
- package/src/server/request-log.ts +6 -1
- package/src/server/responses/account-change-state.ts +8 -2
- package/src/server/responses/adapter-continuation.ts +47 -19
- package/src/server/responses/adapter-delivery.ts +72 -12
- package/src/server/responses/adapter-dispatch.ts +46 -3
- package/src/server/responses/advisor-retention.ts +117 -0
- package/src/server/responses/advisor-slot.ts +73 -41
- package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
- package/src/server/responses/codex-ws-wire.ts +15 -8
- package/src/server/responses/combo-stream-preflight.ts +3 -2
- package/src/server/responses/compact.ts +303 -125
- package/src/server/responses/compaction-recovery.ts +20 -10
- package/src/server/responses/compaction-retention.ts +61 -0
- package/src/server/responses/context-overflow.ts +11 -6
- package/src/server/responses/core-auth.ts +6 -0
- package/src/server/responses/core-codex-account.ts +11 -1
- package/src/server/responses/core-combo-failure.ts +2 -0
- package/src/server/responses/core-combo.ts +121 -36
- package/src/server/responses/core-errors.ts +3 -3
- package/src/server/responses/core-lifetime.ts +8 -4
- package/src/server/responses/core-opaque-recovery.ts +35 -0
- package/src/server/responses/core-options.ts +7 -0
- package/src/server/responses/core-pipeline.ts +141 -0
- package/src/server/responses/core-replay.ts +23 -3
- package/src/server/responses/core.ts +2 -115
- package/src/server/responses/fetch-helpers.ts +73 -19
- package/src/server/responses/main-hard-lock-routing.ts +109 -0
- package/src/server/responses/native-response-control.ts +4 -2
- package/src/server/responses/passthrough-delivery.ts +3 -1
- package/src/server/responses/passthrough-dispatch.ts +15 -13
- package/src/server/responses/passthrough-execution.ts +3 -2
- package/src/server/responses/request-prepare.ts +77 -12
- package/src/server/responses/request-sidecar-auth.ts +2 -2
- package/src/server/responses/request-transport.ts +104 -27
- package/src/server/responses/response-effects.ts +2 -0
- package/src/server/responses/response-tool-ownership.ts +21 -0
- package/src/server/responses/run-turn-execution.ts +96 -30
- package/src/server/responses/sidecar-execution.ts +63 -9
- package/src/server/responses/sidecar-send-budget.ts +2 -2
- package/src/server/responses/terminal-guard.ts +2 -16
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/ws-bridge.ts +2 -0
- package/src/service/auth-preflight.ts +71 -0
- package/src/service/cli.ts +4 -2
- package/src/service/definition.ts +97 -0
- package/src/service/guards.ts +1 -61
- package/src/service/health-sentinel-linux.ts +112 -0
- package/src/service/health-sentinel-main.ts +76 -57
- package/src/service/health.ts +5 -89
- package/src/service/launchd.ts +27 -7
- package/src/service/logging.ts +72 -9
- package/src/service/repair.ts +2 -1
- package/src/service/state.ts +7 -8
- package/src/service/systemd.ts +1 -1
- package/src/service/windows-guarded-manager.ts +2 -1
- package/src/service/windows-ops.ts +1 -1
- package/src/service/windows-scheduler.ts +2 -7
- package/src/service/windows-taskxml.ts +2 -2
- package/src/service/windows-wscript.ts +14 -0
- package/src/service.ts +4 -2
- package/src/sidecar/auth.ts +2 -0
- package/src/types/config.ts +64 -3
- package/src/types/opencode-go.ts +61 -0
- package/src/types/opencode-zen.ts +35 -0
- package/src/types/provider.ts +32 -6
- package/src/types/request.ts +31 -2
- package/src/types/wire.ts +31 -37
- package/src/types.ts +2 -0
- package/src/update/npm-invocation.mjs +12 -2
- package/src/usage/cost.ts +48 -12
- package/src/usage/expected-prices.ts +3 -0
- package/src/usage/ledger-retention.ts +2 -1
- package/src/usage/ledger-scanner.ts +6 -5
- package/src/usage/log.ts +17 -8
- package/src/usage/summary.ts +30 -11
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/usage/user-cost-overlays.ts +14 -19
- package/src/usage/user-cost-validation.ts +38 -0
- package/src/vision/eligibility.ts +13 -0
- package/src/web-search/anthropic-history.ts +59 -0
- package/src/web-search/loop.ts +236 -73
- package/src/web-search/progress-stream.ts +15 -2
- package/src/web-search/run-turn-loop.ts +9 -1
- package/gui/dist/assets/App-BjlttVLy.js +0 -58
- package/gui/dist/assets/App-CSJvmjNs.css +0 -1
- package/gui/dist/assets/index-BB6uK6tU.css +0 -1
- package/gui/dist/assets/index-BMssP4k3.js +0 -94
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
package/src/web-search/loop.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { bindAnthropicRejectionResponse } from "../adapters/anthropic-rejection-memo";
|
|
2
|
+
import { anthropicDeclaredTools } from "../adapters/anthropic-tool-catalog";
|
|
3
|
+
import { enableAnthropicReplayCapture, takeAnthropicReplay, bindAnthropicReplay, type AnthropicReplaySnapshot } from "../adapters/anthropic-replay";
|
|
4
|
+
import { anthropicSearchAssistantContent } from "./anthropic-history";
|
|
5
|
+
import type { createAnthropicThinkingRecovery } from "../server/responses/anthropic-thinking-recovery";
|
|
1
6
|
import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "../adapters/base";
|
|
2
7
|
import type { AdapterEvent, OcxConfig, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxProviderOpaqueToolCallMetadata, OcxThinkingContent, OcxUsage, RateLimitRetryPolicy } from "../types";
|
|
3
8
|
import { namespacedToolName, toolChoiceToolPredicate } from "../types";
|
|
@@ -6,6 +11,9 @@ import { cloneProviderOpaqueToolCallMetadata } from "../responses/provider-opaqu
|
|
|
6
11
|
import type { AttemptRecoveryKind } from "../usage/log";
|
|
7
12
|
import { isTruncatedStopReason } from "../responses/truncated-stop-reason";
|
|
8
13
|
import { bridgeToResponsesSSE } from "../bridge";
|
|
14
|
+
import { isTerminalRefusalIncomplete } from "../bridge/terminal-refusal";
|
|
15
|
+
import { guardEmptyToolCatalogStream, type ClientToolCatalogRestriction } from "../lib/empty-tool-catalog";
|
|
16
|
+
import { mergeUsage } from "../server/responses/empty-completion-guard";
|
|
9
17
|
import { runWebSearch, type SidecarOutcome, type SidecarOutcomeRecorder, type SidecarSettings } from "./executor";
|
|
10
18
|
import { runAnthropicWebSearch } from "./anthropic-executor";
|
|
11
19
|
import { runXaiWebSearch, type XaiSearchOptions } from "./xai-executor";
|
|
@@ -15,7 +23,7 @@ import type { WebSearchBackendId } from "./index";
|
|
|
15
23
|
import { pacingHeaderDeadline } from "./header-deadline";
|
|
16
24
|
import { redactSecretString } from "../lib/redact";
|
|
17
25
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
18
|
-
import { applyUpstreamRecoveryInit, fetchWithResetRetry, prepareSameTarget429Wait } from "../lib/upstream-retry";
|
|
26
|
+
import { applyUpstreamRecoveryInit, fetchWithResetRetry, prepareSameTarget429Wait, isNonReplayableResponse, SendBudgetExhaustedError } from "../lib/upstream-retry";
|
|
19
27
|
import { rateLimitRetryDelayMs } from "../providers/key-failover";
|
|
20
28
|
import {
|
|
21
29
|
isTranslatorBudgetExceededError,
|
|
@@ -23,7 +31,7 @@ import {
|
|
|
23
31
|
TranslatorBudgetExceededError,
|
|
24
32
|
} from "../lib/translator-budget";
|
|
25
33
|
import { formatWebSearchResults } from "./format-result";
|
|
26
|
-
import { parseStreamWithProgress, RoutedModelInactivityError, WebSearchStreamProtocolError } from "./progress-stream";
|
|
34
|
+
import { parseStreamWithProgress, RoutedModelInactivityError, WebSearchStreamProtocolError, WebSearchAdapterEventError } from "./progress-stream";
|
|
27
35
|
import { WEB_SEARCH_TOOL_NAME } from "./synthetic-tool";
|
|
28
36
|
|
|
29
37
|
const SSE_HEADERS = {
|
|
@@ -143,6 +151,18 @@ async function* replay(events: AdapterEvent[]): AsyncGenerator<AdapterEvent> {
|
|
|
143
151
|
for (const e of events) yield e;
|
|
144
152
|
}
|
|
145
153
|
|
|
154
|
+
function terminalUsage(events: AdapterEvent[]): OcxUsage | undefined {
|
|
155
|
+
for (const e of events) if (e.type === "done" || e.type === "incomplete") return e.usage;
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Hidden iterations add counters only: their checkpoint and raw wire usage are not the final terminal's. */
|
|
160
|
+
function countersOnly(usage: OcxUsage | undefined): OcxUsage | undefined {
|
|
161
|
+
if (!usage) return undefined;
|
|
162
|
+
const { contextTotalTokens: _context, rawUsage: _raw, ...counters } = usage;
|
|
163
|
+
return counters;
|
|
164
|
+
}
|
|
165
|
+
|
|
146
166
|
/**
|
|
147
167
|
* Collect the thinking block that preceded a web_search call in this iteration's events, so the
|
|
148
168
|
* replayed assistant turn can carry it. Anthropic extended thinking REQUIRES the assistant
|
|
@@ -189,7 +209,14 @@ function extractIterationThinking(events: AdapterEvent[]): OcxThinkingContent[]
|
|
|
189
209
|
};
|
|
190
210
|
|
|
191
211
|
for (const e of events) {
|
|
192
|
-
|
|
212
|
+
// Match the bridge: consecutive signature snapshots update one block. The next
|
|
213
|
+
// semantic event (including an empty thinking-block start) closes that block.
|
|
214
|
+
if (signature !== undefined && e.type !== "thinking_signature" && e.type !== "heartbeat") flushVisible();
|
|
215
|
+
if (e.type === "text_delta" && e.anthropicThinkingSignature) {
|
|
216
|
+
flushVisible();
|
|
217
|
+
flushRaw();
|
|
218
|
+
parts.push({ type: "thinking", thinking: e.text, signature: e.anthropicThinkingSignature });
|
|
219
|
+
} else if (e.type === "thinking_delta") {
|
|
193
220
|
flushRaw();
|
|
194
221
|
thinking += e.thinking;
|
|
195
222
|
} else if (e.type === "reasoning_raw_delta") {
|
|
@@ -197,7 +224,6 @@ function extractIterationThinking(events: AdapterEvent[]): OcxThinkingContent[]
|
|
|
197
224
|
rawReasoning += e.text;
|
|
198
225
|
} else if (e.type === "thinking_signature") {
|
|
199
226
|
signature = e.signature;
|
|
200
|
-
flushVisible();
|
|
201
227
|
} else if (e.type === "redacted_thinking") {
|
|
202
228
|
flushVisible();
|
|
203
229
|
flushRaw();
|
|
@@ -215,8 +241,8 @@ function normalizeQuery(q: string): string {
|
|
|
215
241
|
}
|
|
216
242
|
|
|
217
243
|
/**
|
|
218
|
-
*
|
|
219
|
-
*
|
|
244
|
+
* Developer nudge for the forced-answer pass. Anthropic keeps its first sent position in the
|
|
245
|
+
* request-local history; other providers append it only for that iteration. It tells the model to ground its final answer in the web results already
|
|
220
246
|
* gathered this turn. Citation wording is conditional — a failed/empty search still wants an answer,
|
|
221
247
|
* just without fabricated sources.
|
|
222
248
|
*/
|
|
@@ -233,10 +259,8 @@ function forcedAnswerNudge(): OcxMessage {
|
|
|
233
259
|
}
|
|
234
260
|
|
|
235
261
|
/**
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
* this turn says so explicitly rather than relying on the removal alone. Like {@link forcedAnswerNudge}
|
|
239
|
-
* it is iteration-local and never touches the persisted `messages`.
|
|
262
|
+
* Developer nudge for the ONE recovery pass after an empty forced answer. Tool calls are disabled;
|
|
263
|
+
* Anthropic retains its declared catalog and this nudge in request-local history for prefix fidelity.
|
|
240
264
|
*/
|
|
241
265
|
function forcedAnswerRetryNudge(): OcxMessage {
|
|
242
266
|
return {
|
|
@@ -250,17 +274,17 @@ function forcedAnswerRetryNudge(): OcxMessage {
|
|
|
250
274
|
};
|
|
251
275
|
}
|
|
252
276
|
|
|
253
|
-
function jsonError(status: number, message: string): Response {
|
|
254
|
-
return new Response(JSON.stringify({ error: { message, type: "upstream_error", code: null } }), {
|
|
277
|
+
function jsonError(status: number, message: string, event?: Extract<AdapterEvent, { type: "error" }>): Response {
|
|
278
|
+
return new Response(JSON.stringify({ error: { message, type: event?.errorType ?? "upstream_error", code: event?.code ?? null, ...(event?.retryable === false ? { retryable: false } : {}) } }), {
|
|
255
279
|
status,
|
|
256
|
-
headers: { "Content-Type": "application/json" },
|
|
280
|
+
headers: { "Content-Type": "application/json", ...(event?.retryable === false ? { "x-should-retry": "false" } : {}) },
|
|
257
281
|
});
|
|
258
282
|
}
|
|
259
283
|
|
|
260
284
|
/** Hard provider/parse failure inside an iteration. The eager first iteration converts it to a
|
|
261
285
|
* non-200 jsonError; later (already-streaming) iterations surface it as an in-stream error event. */
|
|
262
286
|
class LoopError extends Error {
|
|
263
|
-
constructor(readonly status: number, message: string) {
|
|
287
|
+
constructor(readonly status: number, message: string, readonly event?: Extract<AdapterEvent, { type: "error" }>) {
|
|
264
288
|
super(message);
|
|
265
289
|
this.name = "LoopError";
|
|
266
290
|
}
|
|
@@ -272,6 +296,7 @@ class LoopError extends Error {
|
|
|
272
296
|
*/
|
|
273
297
|
export interface WebSearchLoopDeps {
|
|
274
298
|
parsed: OcxParsedRequest;
|
|
299
|
+
clientToolCatalogRestriction?: ClientToolCatalogRestriction;
|
|
275
300
|
adapter: ProviderAdapter;
|
|
276
301
|
incomingMeta: IncomingMeta;
|
|
277
302
|
/**
|
|
@@ -297,6 +322,8 @@ export interface WebSearchLoopDeps {
|
|
|
297
322
|
settings: SidecarSettings;
|
|
298
323
|
maxSearches: number;
|
|
299
324
|
forceEmptyResponseId?: boolean;
|
|
325
|
+
/** Responses inbound only: deliver a non-retryable upstream refusal as `response.failed`. */
|
|
326
|
+
terminalRefusalAsFailed?: boolean;
|
|
300
327
|
abortSignal?: AbortSignal;
|
|
301
328
|
recordSidecarOutcome?: SidecarOutcomeRecorder;
|
|
302
329
|
beforeSidecarDispatch?: () => void;
|
|
@@ -320,10 +347,17 @@ export interface WebSearchLoopDeps {
|
|
|
320
347
|
onFirstOutput?: () => void;
|
|
321
348
|
/** Raw adapter usage at the terminal event, pre wire-normalization (see bridgeToResponsesSSE onUsage). */
|
|
322
349
|
onUsage?: (usage: OcxUsage | undefined) => void;
|
|
350
|
+
/**
|
|
351
|
+
* After an `on429` rotation, offered the usage of the hidden iterations the left credential
|
|
352
|
+
* served. Returning true means it was billed to that credential's attempt, so `onUsage` will not
|
|
353
|
+
* report it again.
|
|
354
|
+
*/
|
|
355
|
+
onRotatedUsage?: (usage: OcxUsage) => boolean;
|
|
323
356
|
/** Observe the exact adapter request selected for each routed-model iteration. */
|
|
324
357
|
onRequestBuilt?: (request: AdapterRequest) => void;
|
|
325
358
|
/** Request-scoped executor retains the core's selection binding across loop retries. */
|
|
326
|
-
|
|
359
|
+
thinkingRecovery?: ReturnType<typeof createAnthropicThinkingRecovery>;
|
|
360
|
+
fetchForRequest?: (request: AdapterRequest, parsed: OcxParsedRequest, adapter: ProviderAdapter, recovery?: AttemptRecoveryKind) => typeof globalThis.fetch;
|
|
327
361
|
/** Called before each routed-model dispatch in the loop, for attempt telemetry. Same-target 429 replays pass the `rate-limit-429` recovery kind. */
|
|
328
362
|
onAttemptSend?: (recovery?: AttemptRecoveryKind) => void;
|
|
329
363
|
/**
|
|
@@ -384,8 +418,11 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
384
418
|
: 300_000;
|
|
385
419
|
|
|
386
420
|
const messages: OcxMessage[] = [...parsed.context.messages];
|
|
421
|
+
const replaySnapshots = new Set<AnthropicReplaySnapshot>();
|
|
387
422
|
const loopT0 = Date.now();
|
|
388
|
-
const allTools = parsed.context.tools ?? [];
|
|
423
|
+
const allTools = adapter.name === "anthropic" ? anthropicDeclaredTools(parsed) : parsed.context.tools ?? [];
|
|
424
|
+
let forcedNudgeAdded = false;
|
|
425
|
+
let emptyNudgeAdded = false;
|
|
389
426
|
let searchesExecuted = 0;
|
|
390
427
|
let executedSearchCount = 0;
|
|
391
428
|
// Queries whose search already failed this turn — repeats are short-circuited so a model that keeps
|
|
@@ -407,6 +444,12 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
407
444
|
// One iteration beyond the forced answer is reserved for its empty-answer recovery below.
|
|
408
445
|
const HARD_CAP = maxSearches + 3;
|
|
409
446
|
let emptyAnswerRetries = 0;
|
|
447
|
+
// Every iteration re-sends the full context, but only the last one's terminal reaches the bridge.
|
|
448
|
+
// Search-calling passes and the discarded empty forced answer are summed here and added to what
|
|
449
|
+
// onUsage reports, so the request log meters what was actually sent. The client-facing terminal
|
|
450
|
+
// keeps the last iteration's usage: Codex and Claude Code size the context window from it, and
|
|
451
|
+
// a sum would read as a context several times larger and trigger early auto-compaction.
|
|
452
|
+
let hiddenUsage: OcxUsage | undefined;
|
|
410
453
|
let accountRefusalOutputStarted = false;
|
|
411
454
|
const connectTimeoutMs = deps.connectTimeoutMs ?? 200_000;
|
|
412
455
|
const routedModelStallTimeoutMs = deps.routedModelStallTimeoutMs ?? 200_000;
|
|
@@ -414,8 +457,11 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
414
457
|
interface IterationResponse {
|
|
415
458
|
response: Response;
|
|
416
459
|
responseAdapter: ProviderAdapter;
|
|
460
|
+
recover?: (error: WebSearchAdapterEventError) => Promise<IterationResponse | undefined>;
|
|
417
461
|
}
|
|
418
462
|
type IterationSplit = ReturnType<typeof scanEventsForWebSearch> & {
|
|
463
|
+
events: AdapterEvent[];
|
|
464
|
+
anthropicReplay?: AnthropicReplaySnapshot;
|
|
419
465
|
/**
|
|
420
466
|
* How many leading passthrough events were already delivered live this iteration. They are
|
|
421
467
|
* exactly the first N passthrough entries (live delivery stops before the first event that
|
|
@@ -430,6 +476,38 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
430
476
|
const rateLimitRetryPolicy = deps.retryOn429Policy ?? null;
|
|
431
477
|
let rateLimitRetries = 0;
|
|
432
478
|
|
|
479
|
+
const requireSuccessfulHeaders = async (prepared: IterationResponse): Promise<void> => {
|
|
480
|
+
if (!prepared.response.ok) {
|
|
481
|
+
if (deps.thinkingRecovery && prepared.responseAdapter.name === "anthropic") {
|
|
482
|
+
const event = await deps.thinkingRecovery.projectHttpFailure(prepared.response, signal);
|
|
483
|
+
throw new LoopError(event.status ?? prepared.response.status, event.message, event);
|
|
484
|
+
}
|
|
485
|
+
let body: Awaited<ReturnType<typeof readBoundedResponseBody>>;
|
|
486
|
+
try {
|
|
487
|
+
body = await readBoundedResponseBody(prepared.response, { signal });
|
|
488
|
+
} catch {
|
|
489
|
+
// The response status is authoritative even when its untrusted error body fails while
|
|
490
|
+
// being read (including a synchronous getReader() failure). Never route that failure
|
|
491
|
+
// through the adapter formatter or the generic transport error, which could expose its
|
|
492
|
+
// raw message. A parent/client cancellation still owns the request lifecycle as 499.
|
|
493
|
+
if (signal.aborted) throw new LoopError(499, "client closed request during web-search");
|
|
494
|
+
throw new LoopError(prepared.response.status, `Provider error ${prepared.response.status}`);
|
|
495
|
+
}
|
|
496
|
+
let formatted = "";
|
|
497
|
+
if (body.displaySafe && !body.truncated && body.text.trim() && prepared.responseAdapter.formatErrorBody) {
|
|
498
|
+
try {
|
|
499
|
+
formatted = prepared.responseAdapter.formatErrorBody(
|
|
500
|
+
prepared.response.status,
|
|
501
|
+
prepared.response.headers,
|
|
502
|
+
body.text,
|
|
503
|
+
).trim();
|
|
504
|
+
} catch { /* formatter hooks are best-effort; unsafe raw text is never the fallback */ }
|
|
505
|
+
}
|
|
506
|
+
const suffix = formatted ? `: ${formatted.slice(0, 400)}` : "";
|
|
507
|
+
throw new LoopError(prepared.response.status, `Provider error ${prepared.response.status}${suffix}`);
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
|
|
433
511
|
// Acquire one iteration's final response headers. The first call is drained eagerly so an initial
|
|
434
512
|
// connect/header/HTTP failure stays a non-2xx JSON response. Its successful BODY is deliberately
|
|
435
513
|
// left unread until the downstream Responses SSE bridge exists.
|
|
@@ -439,24 +517,30 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
439
517
|
* restart) before the `on429` key rotation.
|
|
440
518
|
*/
|
|
441
519
|
const prepareIterationEvents = async function* (forceAnswer: boolean): AsyncGenerator<AdapterEvent, IterationResponse> {
|
|
442
|
-
// On the forced-answer pass the model is asked to answer from the results already in
|
|
443
|
-
// `messages`. A weak model can still produce a thin answer that
|
|
444
|
-
// ignores what the search found, which reads to the user as "the search did nothing". Nudge it
|
|
445
|
-
// (iteration-locally — never mutate the shared `messages`) to actually use the gathered results.
|
|
446
|
-
// Only when a REAL search ran (executedSearchCount, not empty-query/limit/repeat placeholders).
|
|
447
|
-
let iterMessages: OcxMessage[] = forceAnswer && executedSearchCount > 0
|
|
448
|
-
? [...messages, forcedAnswerNudge()]
|
|
449
|
-
: messages;
|
|
450
|
-
// #1001 follow-up: the recovery pass for an empty forced answer. Removing every tool leaves the
|
|
451
|
-
// model nothing to call, and the extra developer turn asks it for the text it just failed to
|
|
452
|
-
// produce. `toolChoice: "none"` is what drops those definitions in the adapter, so the retry
|
|
453
|
-
// cannot repeat the same empty or tool-shaped response.
|
|
454
520
|
const recoveringEmptyAnswer = forceAnswer && emptyAnswerRetries > 0;
|
|
455
|
-
|
|
521
|
+
const preserveAnthropicPrefix = adapter.name === "anthropic";
|
|
522
|
+
let iterMessages = messages;
|
|
523
|
+
if (preserveAnthropicPrefix) {
|
|
524
|
+
// Once sent, each nudge is part of subsequent signatures' bound prefix. Keep its
|
|
525
|
+
// original position even when a forced-answer response makes another tool call.
|
|
526
|
+
if (forceAnswer && executedSearchCount > 0 && !forcedNudgeAdded) {
|
|
527
|
+
const nudge = forcedAnswerNudge();
|
|
528
|
+
if (nudge.role === "developer" && typeof nudge.content === "string") nudge.content = [{ type: "text", text: nudge.content }];
|
|
529
|
+
messages.push(nudge); forcedNudgeAdded = true;
|
|
530
|
+
}
|
|
531
|
+
if (recoveringEmptyAnswer && !emptyNudgeAdded) {
|
|
532
|
+
const nudge = forcedAnswerRetryNudge();
|
|
533
|
+
if (nudge.role === "developer" && typeof nudge.content === "string") nudge.content = [{ type: "text", text: nudge.content }];
|
|
534
|
+
messages.push(nudge); emptyNudgeAdded = true;
|
|
535
|
+
}
|
|
536
|
+
} else {
|
|
537
|
+
if (forceAnswer && executedSearchCount > 0) iterMessages = [...messages, forcedAnswerNudge()];
|
|
538
|
+
if (recoveringEmptyAnswer) iterMessages = [...iterMessages, forcedAnswerRetryNudge()];
|
|
539
|
+
}
|
|
456
540
|
const iterParsed: OcxParsedRequest = {
|
|
457
541
|
...parsed, stream: true,
|
|
458
542
|
...(recoveringEmptyAnswer ? { options: { ...parsed.options, toolChoice: "none" as const } } : {}),
|
|
459
|
-
context: { ...parsed.context, messages: iterMessages, tools: recoveringEmptyAnswer ? [] : allTools },
|
|
543
|
+
context: { ...parsed.context, messages: iterMessages, tools: recoveringEmptyAnswer && !preserveAnthropicPrefix ? [] : allTools },
|
|
460
544
|
};
|
|
461
545
|
// One cumulative header deadline spans key rotations and reset sends, excluding local pacing.
|
|
462
546
|
// clear() stops only its timer after final headers; the direct turn signal remains attached to
|
|
@@ -494,7 +578,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
494
578
|
cachedRequest = request;
|
|
495
579
|
cachedAdapter = requestAdapter;
|
|
496
580
|
}
|
|
497
|
-
const requestFetch = headerDeadline.pacedFetch(deps.fetchForRequest?.(request, iterParsed, requestAdapter) ?? routedProviderFetch);
|
|
581
|
+
const requestFetch = headerDeadline.pacedFetch(deps.fetchForRequest?.(request, iterParsed, requestAdapter, recovery) ?? routedProviderFetch);
|
|
498
582
|
let response: Response;
|
|
499
583
|
try {
|
|
500
584
|
if (requestAdapter.fetchResponse) {
|
|
@@ -537,9 +621,33 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
537
621
|
} finally {
|
|
538
622
|
request.releaseBodyObservation?.();
|
|
539
623
|
}
|
|
624
|
+
bindAnthropicRejectionResponse(request, response);
|
|
540
625
|
return { response, responseAdapter: requestAdapter };
|
|
541
626
|
};
|
|
542
627
|
|
|
628
|
+
const recover = async (error: WebSearchAdapterEventError): Promise<IterationResponse | undefined> => {
|
|
629
|
+
if (signal.aborted || !error.replaySafe || isNonReplayableResponse(prepared.response)
|
|
630
|
+
|| !deps.thinkingRecovery?.prepareEvent(error.event.message, prepared.responseAdapter.name, iterParsed, prepared.response)) return;
|
|
631
|
+
cachedRequest = undefined;
|
|
632
|
+
cachedAdapter = undefined;
|
|
633
|
+
// The successful headers ended the previous timer; the replacement owns a fresh deadline.
|
|
634
|
+
headerDeadline = pacingHeaderDeadline(connectTimeoutMs, signal);
|
|
635
|
+
try {
|
|
636
|
+
const replacement = await fetchOnce(prepared.responseAdapter, "opaque-blob-rejection");
|
|
637
|
+
headerDeadline.clear();
|
|
638
|
+
await requireSuccessfulHeaders(replacement);
|
|
639
|
+
return replacement;
|
|
640
|
+
} catch (error) {
|
|
641
|
+
if (signal.aborted) throw new LoopError(499, "client closed request during web-search");
|
|
642
|
+
if (headerDeadline.didExpire()) throw new LoopError(504,
|
|
643
|
+
`Provider response-header timeout after ${connectTimeoutMs}ms during web-search`);
|
|
644
|
+
if (error instanceof LoopError || error instanceof SendBudgetExhaustedError || isTranslatorBudgetExceededError(error)) throw error;
|
|
645
|
+
throw new LoopError(502, `Provider unreachable: ${error instanceof Error ? error.message : String(error)}`);
|
|
646
|
+
} finally {
|
|
647
|
+
headerDeadline.clear();
|
|
648
|
+
deps.onIterationEnd?.();
|
|
649
|
+
}
|
|
650
|
+
};
|
|
543
651
|
let prepared = await fetchOnce(adapter);
|
|
544
652
|
// Same-target 429 wait-and-retry (opt-in `retryOn429`) BEFORE key rotation: a primary-key
|
|
545
653
|
// rate-limit blip replays on the SAME key; rotation only runs after attempts exhaust.
|
|
@@ -586,6 +694,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
586
694
|
const rotated = await deps.on429(prepared.response.headers.get("retry-after"), prepared.response.headers,
|
|
587
695
|
iterParsed, prepared.response);
|
|
588
696
|
if (!rotated) break;
|
|
697
|
+
if (hiddenUsage && deps.onRotatedUsage?.(hiddenUsage)) hiddenUsage = undefined;
|
|
589
698
|
const retainRefusal = prepared.response.status === 403
|
|
590
699
|
&& deps.incomingMeta?.providerName === "google-antigravity" && rotated.recoveryKind === "oauth-account-403";
|
|
591
700
|
// Keep the bounded refusal readable until the sibling actually returns headers.
|
|
@@ -609,36 +718,20 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
609
718
|
prepared = replacement;
|
|
610
719
|
}
|
|
611
720
|
|
|
721
|
+
if (await deps.thinkingRecovery?.prepareHttp(prepared.response, prepared.responseAdapter.name, iterParsed)) {
|
|
722
|
+
try { void prepared.response.body?.cancel().catch(() => {}); } catch { /* closed */ }
|
|
723
|
+
cachedRequest = undefined;
|
|
724
|
+
cachedAdapter = undefined;
|
|
725
|
+
prepared = await fetchOnce(adapter, "opaque-blob-rejection");
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
|
|
612
729
|
// Final headers have arrived. Clear only the deadline timer before ANY body read.
|
|
613
730
|
headerDeadline.clear();
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
try {
|
|
617
|
-
body = await readBoundedResponseBody(prepared.response, { signal });
|
|
618
|
-
} catch {
|
|
619
|
-
// The response status is authoritative even when its untrusted error body fails while
|
|
620
|
-
// being read (including a synchronous getReader() failure). Never route that failure
|
|
621
|
-
// through the adapter formatter or the generic transport error, which could expose its
|
|
622
|
-
// raw message. A parent/client cancellation still owns the request lifecycle as 499.
|
|
623
|
-
if (signal.aborted) throw new LoopError(499, "client closed request during web-search");
|
|
624
|
-
throw new LoopError(prepared.response.status, `Provider error ${prepared.response.status}`);
|
|
625
|
-
}
|
|
626
|
-
let formatted = "";
|
|
627
|
-
if (body.displaySafe && !body.truncated && body.text.trim() && prepared.responseAdapter.formatErrorBody) {
|
|
628
|
-
try {
|
|
629
|
-
formatted = prepared.responseAdapter.formatErrorBody(
|
|
630
|
-
prepared.response.status,
|
|
631
|
-
prepared.response.headers,
|
|
632
|
-
body.text,
|
|
633
|
-
).trim();
|
|
634
|
-
} catch { /* formatter hooks are best-effort; unsafe raw text is never the fallback */ }
|
|
635
|
-
}
|
|
636
|
-
const suffix = formatted ? `: ${formatted.slice(0, 400)}` : "";
|
|
637
|
-
throw new LoopError(prepared.response.status, `Provider error ${prepared.response.status}${suffix}`);
|
|
638
|
-
}
|
|
639
|
-
return prepared;
|
|
731
|
+
await requireSuccessfulHeaders(prepared);
|
|
732
|
+
return { ...prepared, recover };
|
|
640
733
|
} catch (error) {
|
|
641
|
-
if (isTranslatorBudgetExceededError(error)) throw error;
|
|
734
|
+
if (isTranslatorBudgetExceededError(error) || error instanceof SendBudgetExhaustedError) throw error;
|
|
642
735
|
if (headerDeadline.didExpire()) {
|
|
643
736
|
throw new LoopError(504, `Provider response-header timeout after ${connectTimeoutMs}ms during web-search`);
|
|
644
737
|
}
|
|
@@ -675,13 +768,25 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
675
768
|
const events: AdapterEvent[] = [];
|
|
676
769
|
let liveWindowOpen = deps.streamRoutedModelOutput === true;
|
|
677
770
|
let streamedPassthroughCount = 0;
|
|
771
|
+
let anthropicReplay: AnthropicReplaySnapshot | undefined;
|
|
772
|
+
let releaseCapture = () => {};
|
|
678
773
|
try {
|
|
679
|
-
const parse =
|
|
774
|
+
const parse: ProviderAdapter["parseStream"] = (response, budget) => {
|
|
775
|
+
if (prepared.responseAdapter.name === "anthropic") releaseCapture = enableAnthropicReplayCapture(response);
|
|
776
|
+
return prepared.responseAdapter.parseStream(response, budget);
|
|
777
|
+
};
|
|
680
778
|
for await (const event of parseStreamWithProgress(prepared.response, parse, {
|
|
681
779
|
signal,
|
|
682
780
|
inactivityTimeoutMs: routedModelStallTimeoutMs,
|
|
683
781
|
translatorBudget,
|
|
782
|
+
preserveAdapterError: deps.thinkingRecovery !== undefined,
|
|
783
|
+
// A pass that fails was still billed; the error path reports it with the hidden passes.
|
|
784
|
+
onErrorUsage: usage => { hiddenUsage = mergeUsage(hiddenUsage, usage); },
|
|
684
785
|
})) {
|
|
786
|
+
if (event.type === "done") {
|
|
787
|
+
anthropicReplay = takeAnthropicReplay(event);
|
|
788
|
+
if (anthropicReplay) replaySnapshots.add(anthropicReplay);
|
|
789
|
+
}
|
|
685
790
|
if (event.type === "heartbeat") yield event;
|
|
686
791
|
// Kiro's explicit-completion protocol marks ordinary assistant text as commentary while
|
|
687
792
|
// it performs a bounded final-answer retry. That text is safe to surface immediately and
|
|
@@ -689,7 +794,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
689
794
|
// Codex show only `Working` until both Kiro attempts had finished (often 30-40 seconds).
|
|
690
795
|
// Tool events remain buffered below, so the decision to invoke the hosted sidecar is still
|
|
691
796
|
// atomic and no search call can escape before its stream has validated successfully.
|
|
692
|
-
else if (event.type === "text_delta" && event.phase === "commentary") { accountRefusalOutputStarted = true; yield event; }
|
|
797
|
+
else if (event.type === "text_delta" && event.phase === "commentary" && !event.anthropicThinkingSignature) { accountRefusalOutputStarted = true; yield event; }
|
|
693
798
|
else if (liveWindowOpen && LIVE_STREAMABLE.has(event.type)) {
|
|
694
799
|
// Live events are ALSO buffered: the scanner still needs them for thinking extraction
|
|
695
800
|
// and the forced-answer output check; only the terminal replay skips them (by count).
|
|
@@ -703,12 +808,21 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
703
808
|
}
|
|
704
809
|
}
|
|
705
810
|
} catch (error) {
|
|
706
|
-
if (isTranslatorBudgetExceededError(error)) throw error;
|
|
811
|
+
if (isTranslatorBudgetExceededError(error) || error instanceof SendBudgetExhaustedError) throw error;
|
|
707
812
|
if (signal.aborted) throw new LoopError(499, "client closed request during web-search");
|
|
813
|
+
if (error instanceof WebSearchAdapterEventError) {
|
|
814
|
+
const replacement = await prepared.recover?.(error);
|
|
815
|
+
if (replacement) return yield* consumeIterationEvents(replacement);
|
|
816
|
+
const event = isNonReplayableResponse(prepared.response)
|
|
817
|
+
? { ...error.event, message: "Provider stream error: upstream diagnostic withheld for a non-replayable failure", retryable: false as const }
|
|
818
|
+
: error.event;
|
|
819
|
+
throw new LoopError(event.status ?? 502, event.message, event);
|
|
820
|
+
}
|
|
821
|
+
if (error instanceof LoopError) throw error;
|
|
708
822
|
if (error instanceof RoutedModelInactivityError) throw new LoopError(504, error.message);
|
|
709
823
|
if (error instanceof WebSearchStreamProtocolError) throw new LoopError(502, error.message);
|
|
710
824
|
throw new LoopError(502, `Provider stream error: ${error instanceof Error ? error.message : String(error)}`);
|
|
711
|
-
}
|
|
825
|
+
} finally { releaseCapture(); }
|
|
712
826
|
|
|
713
827
|
const terminalIndexes = events.flatMap((event, index) =>
|
|
714
828
|
event.type === "done" || event.type === "incomplete" || event.type === "error" ? [index] : []);
|
|
@@ -722,7 +836,8 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
722
836
|
}
|
|
723
837
|
throw new LoopError(502, terminal.message);
|
|
724
838
|
}
|
|
725
|
-
|
|
839
|
+
deps.thinkingRecovery?.observeIteration(events);
|
|
840
|
+
return { ...scanEventsForWebSearch(events), streamedPassthroughCount, events, anthropicReplay };
|
|
726
841
|
};
|
|
727
842
|
|
|
728
843
|
// Execute one model-requested web_search call. The call may batch several queries (native
|
|
@@ -731,7 +846,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
731
846
|
// valid, and surface as ONE search cell carrying every attempted query. A real search (one that
|
|
732
847
|
// hits the sidecar) shows the spinner WHILE the batch runs. Empty/limit/repeat placeholders never
|
|
733
848
|
// emit a cell (matching the prior single-query behavior).
|
|
734
|
-
async function* runSearchCall(call: WebSearchCall, precedingThinking: OcxThinkingContent[] = []): AsyncGenerator<AdapterEvent> {
|
|
849
|
+
async function* runSearchCall(call: WebSearchCall, precedingThinking: OcxThinkingContent[] = [], appendAssistant = true): AsyncGenerator<AdapterEvent> {
|
|
735
850
|
const results: { query: string; outcome: SidecarOutcome }[] = [];
|
|
736
851
|
let beganCell = false;
|
|
737
852
|
if (call.queries.length === 0) {
|
|
@@ -808,7 +923,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
808
923
|
const callArgs: Record<string, unknown> = call.queries.length > 1
|
|
809
924
|
? { queries: call.queries }
|
|
810
925
|
: { query: call.queries[0] ?? "" };
|
|
811
|
-
messages.push({
|
|
926
|
+
if (appendAssistant) messages.push({
|
|
812
927
|
role: "assistant",
|
|
813
928
|
content: [
|
|
814
929
|
// Signed thinking must precede tool_use on replay (Anthropic extended thinking), and
|
|
@@ -867,7 +982,9 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
867
982
|
firstPrepared = await prepareIterationDrained(false);
|
|
868
983
|
} catch (e) {
|
|
869
984
|
if (abortSignal) abortSignal.removeEventListener("abort", linkAbort);
|
|
870
|
-
if (e instanceof LoopError) return jsonError(e.status, e.message);
|
|
985
|
+
if (e instanceof LoopError) return jsonError(e.status, e.message, e.event);
|
|
986
|
+
if (e instanceof SendBudgetExhaustedError) return jsonError(429, e.message,
|
|
987
|
+
{ type: "error", message: e.message, errorType: "rate_limit_error", code: e.code, retryable: false });
|
|
871
988
|
throw e;
|
|
872
989
|
}
|
|
873
990
|
|
|
@@ -914,7 +1031,10 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
914
1031
|
// limit-reached result instead of failing the turn (#6464: stripping
|
|
915
1032
|
// the declaration makes some models emit the raw call as visible
|
|
916
1033
|
// text).
|
|
917
|
-
|
|
1034
|
+
// A refused iteration ends the turn even when it carried a search call: running the
|
|
1035
|
+
// search would send the refused context back to the model (#7148).
|
|
1036
|
+
const shouldLoop = split.calls.length > 0 && !split.hasRealToolCall
|
|
1037
|
+
&& !split.passthrough.some(isTerminalRefusalIncomplete);
|
|
918
1038
|
if (!shouldLoop) {
|
|
919
1039
|
// #1001: a forced-answer pass that ends `done` must have produced
|
|
920
1040
|
// usable output — never a malformed tool call, and never silence.
|
|
@@ -944,9 +1064,10 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
944
1064
|
stopReason: terminalEvent.stopReason,
|
|
945
1065
|
eventTypes: [...new Set(split.passthrough.map(event => event.type))],
|
|
946
1066
|
}));
|
|
1067
|
+
hiddenUsage = mergeUsage(hiddenUsage, terminalEvent.usage);
|
|
947
1068
|
if (!split.hasMalformedToolCall && !split.hasRealToolCall && emptyAnswerRetries === 0) {
|
|
948
1069
|
emptyAnswerRetries++;
|
|
949
|
-
console.warn("[web-search-loop] empty forced answer — retrying once
|
|
1070
|
+
console.warn("[web-search-loop] empty forced answer — retrying once with tool calls disabled");
|
|
950
1071
|
yield { type: "heartbeat" };
|
|
951
1072
|
continue;
|
|
952
1073
|
}
|
|
@@ -966,13 +1087,45 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
966
1087
|
yield* replay(split.passthrough.slice(split.streamedPassthroughCount));
|
|
967
1088
|
return;
|
|
968
1089
|
}
|
|
1090
|
+
hiddenUsage = mergeUsage(hiddenUsage, terminalUsage(split.passthrough));
|
|
969
1091
|
// The thinking that led to the search belongs to the FIRST call's assistant replay turn.
|
|
970
|
-
const
|
|
1092
|
+
for (const event of split.passthrough.slice(split.streamedPassthroughCount)) {
|
|
1093
|
+
if (event.type === "text_delta" && event.anthropicThinkingSignature) yield event;
|
|
1094
|
+
}
|
|
1095
|
+
const preserveAnthropicHistory = prepared.responseAdapter.name === "anthropic";
|
|
1096
|
+
if (preserveAnthropicHistory) {
|
|
1097
|
+
if (!split.anthropicReplay) throw new LoopError(502, "Anthropic search history cannot be replayed losslessly");
|
|
1098
|
+
const message = { role: "assistant" as const,
|
|
1099
|
+
content: anthropicSearchAssistantContent(split.events), timestamp: Date.now() };
|
|
1100
|
+
const originalCalls = split.anthropicReplay.content.filter(block => block.type === "tool_use");
|
|
1101
|
+
const priorIds = new Set(messages.flatMap(message => message.role === "assistant"
|
|
1102
|
+
? message.content.filter(part => part.type === "toolCall").map(part => part.id)
|
|
1103
|
+
: message.role === "toolResult" ? [message.toolCallId] : []));
|
|
1104
|
+
if (originalCalls.some(block => priorIds.has(block.id as string))) {
|
|
1105
|
+
throw new LoopError(502, "Anthropic search replay tool identity changed");
|
|
1106
|
+
}
|
|
1107
|
+
const calls = message.content.filter(part => part.type === "toolCall");
|
|
1108
|
+
if (originalCalls.length !== calls.length || originalCalls.some((block, index) => block.id !== calls[index]?.id)) {
|
|
1109
|
+
throw new LoopError(502, "Anthropic search replay tool identity changed");
|
|
1110
|
+
}
|
|
1111
|
+
// The raw input is authoritative, including non-empty content_block_start input.
|
|
1112
|
+
for (const [index, call] of calls.entries()) call.arguments = originalCalls[index]!.input as Record<string, unknown>;
|
|
1113
|
+
for (const call of split.calls) {
|
|
1114
|
+
const original = originalCalls.find(block => block.id === call.id);
|
|
1115
|
+
if (!original) throw new LoopError(502, "Anthropic search replay tool identity changed");
|
|
1116
|
+
call.queries = parseQueries(JSON.stringify(original.input));
|
|
1117
|
+
}
|
|
1118
|
+
bindAnthropicReplay(message, split.anthropicReplay);
|
|
1119
|
+
messages.push(message);
|
|
1120
|
+
}
|
|
1121
|
+
const iterationThinking = preserveAnthropicHistory ? [] : extractIterationThinking(split.passthrough);
|
|
971
1122
|
for (const [callIndex, call] of split.calls.entries()) {
|
|
972
1123
|
accountRefusalOutputStarted = true;
|
|
973
|
-
yield* runSearchCall(call, callIndex === 0 ? iterationThinking : []);
|
|
1124
|
+
yield* runSearchCall(call, callIndex === 0 ? iterationThinking : [], !preserveAnthropicHistory);
|
|
974
1125
|
}
|
|
975
1126
|
} catch (e) {
|
|
1127
|
+
// The bridge reports no usage for either error; the iterations already sent still count.
|
|
1128
|
+
if (hiddenUsage) deps.onUsage?.(hiddenUsage);
|
|
976
1129
|
if (isTranslatorBudgetExceededError(e)) {
|
|
977
1130
|
yield {
|
|
978
1131
|
type: "error",
|
|
@@ -982,19 +1135,27 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
982
1135
|
message: "upstream translation buffer exceeded the safe limit",
|
|
983
1136
|
};
|
|
984
1137
|
} else {
|
|
985
|
-
yield { type: "error",
|
|
1138
|
+
if (e instanceof SendBudgetExhaustedError) yield { type: "error", status: 429,
|
|
1139
|
+
errorType: "rate_limit_error", code: e.code, message: e.message, retryable: false };
|
|
1140
|
+
else yield e instanceof LoopError && e.event ? e.event
|
|
1141
|
+
: e instanceof LoopError && deps.thinkingRecovery
|
|
1142
|
+
? { type: "error", status: e.status, errorType: "upstream_error", message: e.message }
|
|
1143
|
+
: { type: "error", message: e instanceof Error ? e.message : String(e) };
|
|
986
1144
|
}
|
|
987
1145
|
return;
|
|
988
1146
|
}
|
|
989
1147
|
}
|
|
1148
|
+
if (hiddenUsage) deps.onUsage?.(hiddenUsage);
|
|
990
1149
|
yield { type: "error", message: "web-search loop exceeded its iteration cap" };
|
|
991
1150
|
} finally {
|
|
1151
|
+
for (const snapshot of replaySnapshots) snapshot.release();
|
|
1152
|
+
replaySnapshots.clear();
|
|
992
1153
|
if (abortSignal) abortSignal.removeEventListener("abort", linkAbort);
|
|
993
1154
|
}
|
|
994
1155
|
}
|
|
995
1156
|
|
|
996
1157
|
const sse = bridgeToResponsesSSE(
|
|
997
|
-
produce(), parsed._responseModelId ?? parsed.modelId, toolNsMap, freeform, toolSearch, () => {
|
|
1158
|
+
guardEmptyToolCatalogStream(produce(), deps.clientToolCatalogRestriction, translatorBudget), parsed._responseModelId ?? parsed.modelId, toolNsMap, freeform, toolSearch, () => {
|
|
998
1159
|
const elapsed = Date.now() - loopT0;
|
|
999
1160
|
if (executedSearchCount > 0 || searchesExecuted > 0) {
|
|
1000
1161
|
console.warn(`[web-search-loop] cancelled — ${executedSearchCount} real searches, ${searchesExecuted - executedSearchCount} placeholders, ${elapsed}ms`);
|
|
@@ -1004,12 +1165,14 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
1004
1165
|
{
|
|
1005
1166
|
translatorBudget,
|
|
1006
1167
|
replayCacheScope: parsed._reasoningReplayScope,
|
|
1168
|
+
...(deps.clientToolCatalogRestriction ? { ...deps.clientToolCatalogRestriction, enforceDeclaredToolNames: false } : {}),
|
|
1007
1169
|
...(deps.forceEmptyResponseId ? { responseId: "" } : {}),
|
|
1170
|
+
...(deps.terminalRefusalAsFailed ? { terminalRefusalAsFailed: true } : {}),
|
|
1008
1171
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
1009
1172
|
hideRawReasoning: parsed.options.hideRawReasoning,
|
|
1010
1173
|
...(deps.stallTimeoutSec !== undefined ? { stallTimeoutSec: deps.stallTimeoutSec } : {}),
|
|
1011
1174
|
...(deps.onFirstOutput ? { onFirstOutput: deps.onFirstOutput } : {}),
|
|
1012
|
-
...(deps.onUsage ? { onUsage: deps.onUsage } : {}),
|
|
1175
|
+
...(deps.onUsage ? { onUsage: (usage: OcxUsage | undefined) => deps.onUsage?.(mergeUsage(countersOnly(hiddenUsage), usage)) } : {}),
|
|
1013
1176
|
...(deps.onCompletedResponse ? { onCompletedResponse: deps.onCompletedResponse } : {}),
|
|
1014
1177
|
},
|
|
1015
1178
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ProviderAdapter } from "../adapters/base";
|
|
2
|
-
import type { AdapterEvent } from "../types";
|
|
2
|
+
import type { AdapterEvent, OcxUsage } from "../types";
|
|
3
3
|
import {
|
|
4
4
|
isTranslatorBudgetExceededError,
|
|
5
5
|
TRANSLATOR_MAX_TURN_BYTES,
|
|
@@ -26,12 +26,22 @@ export class WebSearchStreamProtocolError extends Error {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/** Opt-in preserves an adapter refusal before it becomes an untyped parser exception. */
|
|
30
|
+
export class WebSearchAdapterEventError extends Error {
|
|
31
|
+
constructor(readonly event: Extract<AdapterEvent, { type: "error" }>, readonly replaySafe: boolean) {
|
|
32
|
+
super(event.message);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
29
36
|
export interface ParseStreamWithProgressOptions {
|
|
37
|
+
preserveAdapterError?: boolean;
|
|
30
38
|
inactivityTimeoutMs: number;
|
|
31
39
|
translatorBudget: TranslatorBudget;
|
|
32
40
|
signal?: AbortSignal;
|
|
33
41
|
/** Kept configurable for focused tests; production callers should use the 5 second default. */
|
|
34
42
|
postTerminalDrainTimeoutMs?: number;
|
|
43
|
+
/** Receives the usage an adapter `error` event carries before the stream fails on it. */
|
|
44
|
+
onErrorUsage?: (usage: OcxUsage) => void;
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
type ParseStream = ProviderAdapter["parseStream"];
|
|
@@ -247,6 +257,7 @@ export async function* parseStreamWithProgress(
|
|
|
247
257
|
|
|
248
258
|
resetInactivity();
|
|
249
259
|
|
|
260
|
+
let replaySafe = true;
|
|
250
261
|
const parserPump = (async (): Promise<void> => {
|
|
251
262
|
let heldTerminal: Extract<AdapterEvent, { type: "done" | "incomplete" }> | undefined;
|
|
252
263
|
try {
|
|
@@ -296,11 +307,13 @@ export async function* parseStreamWithProgress(
|
|
|
296
307
|
);
|
|
297
308
|
}
|
|
298
309
|
if (event.type === "error") {
|
|
310
|
+
if (event.usage) options.onErrorUsage?.(event.usage);
|
|
299
311
|
fail(event.code === "translation_buffer_limit"
|
|
300
312
|
? new TranslatorBudgetExceededError("live_transient", TRANSLATOR_MAX_TURN_BYTES)
|
|
301
|
-
: new Error(event.message));
|
|
313
|
+
: options.preserveAdapterError ? new WebSearchAdapterEventError(event, replaySafe) : new Error(event.message));
|
|
302
314
|
return;
|
|
303
315
|
}
|
|
316
|
+
replaySafe &&= event.type === "heartbeat" && event.replayUnsafe !== true;
|
|
304
317
|
if (event.type === "done" || event.type === "incomplete") {
|
|
305
318
|
heldTerminal = event;
|
|
306
319
|
// Response-byte inactivity ends once the adapter has produced a terminal event.
|