@bitkyc08/opencodex 2.86.0 → 2.87.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-ZfYbLHkB.js +58 -0
- package/gui/dist/assets/App-wT3mXqFz.css +1 -0
- package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-DCd-A1az.js} +1 -1
- package/gui/dist/assets/index-CfYDMj34.js +94 -0
- package/gui/dist/assets/index-DPeB9HpV.css +1 -0
- package/gui/dist/assets/quota-summary-ChvmKGOk.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.ts +108 -21
- 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.ts +35 -8
- 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 +3 -1
- package/src/bridge/response-json.ts +18 -3
- package/src/bridge/sse.ts +58 -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 +11 -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 +38 -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 +17 -14
- 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/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 +12 -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/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 +52 -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 +608 -284
- package/src/grok/sync.ts +1 -1
- package/src/images/loop.ts +46 -8
- 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/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 +33 -7
- 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 +5 -4
- 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/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 +24 -2
- 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/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 +55 -414
- package/src/server/effort-policy.ts +1 -1
- package/src/server/images.ts +15 -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 +9 -1
- package/src/server/index/startup-warnings.ts +2 -0
- package/src/server/index.ts +1 -1
- package/src/server/inference/advisor-diagnostics.ts +31 -0
- 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 +101 -21
- 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 +13 -1
- package/src/server/responses/adapter-delivery.ts +19 -6
- package/src/server/responses/adapter-dispatch.ts +19 -2
- package/src/server/responses/advisor-retention.ts +117 -0
- package/src/server/responses/advisor-slot.ts +73 -41
- package/src/server/responses/codex-ws-wire.ts +15 -8
- package/src/server/responses/compact.ts +303 -125
- package/src/server/responses/compaction-recovery.ts +18 -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 +120 -36
- package/src/server/responses/core-errors.ts +3 -3
- package/src/server/responses/core-lifetime.ts +5 -2
- package/src/server/responses/core-opaque-recovery.ts +35 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-pipeline.ts +141 -0
- package/src/server/responses/core-replay.ts +18 -2
- 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/request-prepare.ts +76 -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 +1 -0
- package/src/server/responses/run-turn-execution.ts +96 -30
- package/src/server/responses/sidecar-execution.ts +20 -2
- package/src/server/responses/terminal-guard.ts +2 -16
- 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 +29 -0
- package/src/types/provider.ts +32 -6
- package/src/types/request.ts +23 -2
- package/src/types/wire.ts +29 -37
- package/src/types.ts +1 -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 +22 -9
- 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/loop.ts +55 -5
- package/src/web-search/progress-stream.ts +4 -1
- 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
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { OcxProviderConfig, OcxReasoningReplayScopeRef } from "../types";
|
|
2
|
+
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
3
|
+
import { chatReasoningReplayPolicy, isChatRecord } from "../adapters/openai-chat/reasoning-replay";
|
|
4
|
+
import { bindReasoningReplayScope, reasoningReplayDestinationIdentity,
|
|
5
|
+
reasoningReplayKeyCredentialIdentity, rememberReasoningForCall } from "../responses/reasoning-replay-cache";
|
|
6
|
+
import { reasoningReplayConversationIdFromResponsesRequest, sessionIdHeaderFromRequest } from "./request-log-conversation";
|
|
7
|
+
|
|
8
|
+
type Rec = Record<string, unknown>;
|
|
9
|
+
|
|
10
|
+
export function nativeChatReplayScope(headers: Headers): OcxReasoningReplayScopeRef | undefined {
|
|
11
|
+
const clientThreadId = reasoningReplayConversationIdFromResponsesRequest({
|
|
12
|
+
threadIdHeader: headers.get("thread-id"), sessionIdHeader: sessionIdHeaderFromRequest(headers),
|
|
13
|
+
});
|
|
14
|
+
return clientThreadId ? { clientThreadId } : undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Rebind before EVERY build: a key rotation must not retrieve another credential's reasoning. */
|
|
18
|
+
export function bindNativeChatReplayScope(
|
|
19
|
+
scope: OcxReasoningReplayScopeRef | undefined,
|
|
20
|
+
provider: OcxProviderConfig,
|
|
21
|
+
providerName: string,
|
|
22
|
+
modelId: string,
|
|
23
|
+
): void {
|
|
24
|
+
const providerDestinationIdentity = reasoningReplayDestinationIdentity(provider.baseUrl);
|
|
25
|
+
const credentialIdentity = reasoningReplayKeyCredentialIdentity(provider);
|
|
26
|
+
bindReasoningReplayScope(scope, providerDestinationIdentity && credentialIdentity ? {
|
|
27
|
+
providerName, modelId, adapterName: "openai-chat",
|
|
28
|
+
providerDestinationIdentity, credentialIdentity,
|
|
29
|
+
} : undefined);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Observe the native wire, never translate it or retain tool arguments/content. */
|
|
33
|
+
export function nativeChatReasoningCapture(
|
|
34
|
+
provider: OcxProviderConfig,
|
|
35
|
+
modelId: string,
|
|
36
|
+
scope: OcxReasoningReplayScopeRef | undefined,
|
|
37
|
+
budget: TranslatorBudget,
|
|
38
|
+
) {
|
|
39
|
+
const choices = new Map<number, { text: string; calls: Map<number, string> }>();
|
|
40
|
+
const accounting = { kind: "retained_collectors" as const };
|
|
41
|
+
// This optional fallback cannot retain more than the cache it feeds (256 KiB / 64 calls).
|
|
42
|
+
const maxBytes = 256 * 1024;
|
|
43
|
+
let bytes = 0;
|
|
44
|
+
let callCount = 0;
|
|
45
|
+
let disabled = !scope?.current || !chatReasoningReplayPolicy(provider, modelId).preserve;
|
|
46
|
+
const discard = () => {
|
|
47
|
+
budget.releaseRetained(bytes, accounting);
|
|
48
|
+
bytes = 0;
|
|
49
|
+
choices.clear();
|
|
50
|
+
disabled = true;
|
|
51
|
+
};
|
|
52
|
+
const remember = (text: string, calls: Iterable<string>) => {
|
|
53
|
+
for (const id of calls) rememberReasoningForCall(id, text, scope);
|
|
54
|
+
};
|
|
55
|
+
const observe = (value: Rec) => {
|
|
56
|
+
if (disabled || !Array.isArray(value.choices)) return;
|
|
57
|
+
for (const [ordinal, raw] of value.choices.entries()) {
|
|
58
|
+
if (!isChatRecord(raw) || !isChatRecord(raw.delta)) continue;
|
|
59
|
+
const index = typeof raw.index === "number" ? raw.index : ordinal;
|
|
60
|
+
if (!choices.has(index)) {
|
|
61
|
+
if (choices.size >= 64) { discard(); return; }
|
|
62
|
+
choices.set(index, { text: "", calls: new Map() });
|
|
63
|
+
}
|
|
64
|
+
const state = choices.get(index)!;
|
|
65
|
+
const delta = raw.delta;
|
|
66
|
+
const append = (previous: string, fragment: string): string | undefined => {
|
|
67
|
+
const added = Buffer.byteLength(fragment) * 2;
|
|
68
|
+
if (bytes + added > maxBytes) { discard(); return undefined; }
|
|
69
|
+
// Account the new retained fragment before concatenation; the transient reservation
|
|
70
|
+
// covers the old string while its replacement is allocated.
|
|
71
|
+
const reservation = budget.reserveTransient(Buffer.byteLength(previous) * 2, accounting);
|
|
72
|
+
try {
|
|
73
|
+
budget.chargeRetained(added, accounting);
|
|
74
|
+
bytes += added;
|
|
75
|
+
return previous + fragment;
|
|
76
|
+
} finally { reservation.release(); }
|
|
77
|
+
};
|
|
78
|
+
if (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0) {
|
|
79
|
+
const text = append(state.text, delta.reasoning_content);
|
|
80
|
+
if (text === undefined) return;
|
|
81
|
+
state.text = text;
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(delta.tool_calls)) {
|
|
84
|
+
for (const [toolOrdinal, tool] of delta.tool_calls.entries()) {
|
|
85
|
+
if (!isChatRecord(tool) || typeof tool.id !== "string" || tool.id.length === 0) continue;
|
|
86
|
+
const toolIndex = typeof tool.index === "number" ? tool.index : toolOrdinal;
|
|
87
|
+
if (!state.calls.has(toolIndex)) {
|
|
88
|
+
if (callCount >= 64) { discard(); return; }
|
|
89
|
+
callCount += 1;
|
|
90
|
+
}
|
|
91
|
+
const previous = state.calls.get(toolIndex) ?? "";
|
|
92
|
+
if (previous === tool.id) continue;
|
|
93
|
+
const id = append(previous, tool.id);
|
|
94
|
+
if (id === undefined) return;
|
|
95
|
+
state.calls.set(toolIndex, id);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// The completed choice is valid even if a caller dispatches its tool before [DONE].
|
|
99
|
+
if (typeof raw.finish_reason === "string" && raw.finish_reason.length > 0) {
|
|
100
|
+
remember(state.text, state.calls.values());
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
return {
|
|
105
|
+
observe,
|
|
106
|
+
finish(success: boolean) {
|
|
107
|
+
if (success && !disabled) {
|
|
108
|
+
for (const state of choices.values()) remember(state.text, state.calls.values());
|
|
109
|
+
}
|
|
110
|
+
discard();
|
|
111
|
+
},
|
|
112
|
+
json(value: Rec) {
|
|
113
|
+
if (disabled || !Array.isArray(value.choices)) return;
|
|
114
|
+
for (const choice of value.choices) {
|
|
115
|
+
if (!isChatRecord(choice) || !isChatRecord(choice.message)) continue;
|
|
116
|
+
const message = choice.message;
|
|
117
|
+
if (typeof message.reasoning_content !== "string" || !Array.isArray(message.tool_calls)) continue;
|
|
118
|
+
remember(message.reasoning_content, message.tool_calls.filter(isChatRecord)
|
|
119
|
+
.flatMap(tool => typeof tool.id === "string" ? [tool.id] : []));
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { chatCompletionsErrorBody } from "../chat/outbound";
|
|
2
|
-
import { classifyError, cyberPolicyErrorType, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../lib/errors";
|
|
2
|
+
import { classifyError, cyberPolicyErrorType, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode, UNDECLARED_TOOL_CALL_ERROR_CODE } from "../lib/errors";
|
|
3
3
|
import { redactSecretString } from "../lib/redact";
|
|
4
4
|
import {
|
|
5
5
|
isTranslatorBudgetExceededError,
|
|
@@ -16,6 +16,39 @@ function isRec(value: unknown): value is Rec {
|
|
|
16
16
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export const UNDECLARED_CHAT_TOOL_MESSAGE = "Upstream emitted a client tool call outside this request's declared tool catalog.";
|
|
20
|
+
|
|
21
|
+
function* chatToolCalls(value: Rec): Generator<{ key: string; name: string; snapshot: boolean; invalid?: boolean }> {
|
|
22
|
+
const choices = Array.isArray(value.choices) ? value.choices : [];
|
|
23
|
+
for (const [position, choice] of choices.entries()) {
|
|
24
|
+
if (!isRec(choice)) continue;
|
|
25
|
+
const choiceIndex = typeof choice.index === "number" ? choice.index : position;
|
|
26
|
+
for (const [payload, snapshot] of [[choice.delta, false], [choice.message, true]] as const) {
|
|
27
|
+
if (!isRec(payload)) continue;
|
|
28
|
+
const tools = Array.isArray(payload.tool_calls) ? payload.tool_calls : [];
|
|
29
|
+
for (const [index, tool] of tools.entries()) {
|
|
30
|
+
const fn = isRec(tool) && isRec(tool.function) ? tool.function : {};
|
|
31
|
+
const toolIndex = isRec(tool) && typeof tool.index === "number" ? tool.index : index;
|
|
32
|
+
yield {
|
|
33
|
+
key: `${choiceIndex}:tool:${toolIndex}`, name: typeof fn.name === "string" ? fn.name : "", snapshot,
|
|
34
|
+
invalid: !isRec(tool) || (tool.type !== undefined && tool.type !== "function"),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (payload.function_call !== undefined && payload.function_call !== null) {
|
|
38
|
+
const fn = isRec(payload.function_call) ? payload.function_call : {};
|
|
39
|
+
yield { key: `${choiceIndex}:legacy`, name: typeof fn.name === "string" ? fn.name : "", snapshot };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function chatResponseHasUndeclaredToolCalls(value: Rec, declared: ReadonlySet<string>): boolean {
|
|
46
|
+
for (const call of chatToolCalls(value)) {
|
|
47
|
+
if (call.invalid || !declared.has(call.name)) return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
19
52
|
export function usageFromChat(value: unknown): OcxUsage | undefined {
|
|
20
53
|
if (!isRec(value)) return undefined;
|
|
21
54
|
const prompt = typeof value.prompt_tokens === "number" ? value.prompt_tokens : undefined;
|
|
@@ -134,8 +167,11 @@ interface NativeChatSseOptions {
|
|
|
134
167
|
* `stallTimeoutSec` then resolves to disabled so a slow local model is not cut mid-turn.
|
|
135
168
|
*/
|
|
136
169
|
localUpstream?: boolean;
|
|
170
|
+
clientDeclaredEmptyToolCatalog?: boolean;
|
|
171
|
+
declaredLegacyFunctionNames?: ReadonlySet<string>;
|
|
137
172
|
onFirstOutput?: () => void;
|
|
138
173
|
onUsage: (usage: OcxUsage) => void;
|
|
174
|
+
onChunk?: (value: Rec) => void;
|
|
139
175
|
onTerminal?: (status: number, message?: string) => void;
|
|
140
176
|
onCancel?: () => void;
|
|
141
177
|
}
|
|
@@ -154,6 +190,29 @@ export function nativeChatSse(
|
|
|
154
190
|
}
|
|
155
191
|
});
|
|
156
192
|
let queuedBytes = 0;
|
|
193
|
+
const declared = options.declaredLegacyFunctionNames ?? new Set<string>();
|
|
194
|
+
const toolNames = new Map<string, string>();
|
|
195
|
+
const pendingToolNames = new Set<string>();
|
|
196
|
+
const heldFrames: string[] = [];
|
|
197
|
+
let heldBytes = 0;
|
|
198
|
+
let toolNameBytes = 0;
|
|
199
|
+
const hasPendingToolName = () => pendingToolNames.size > 0;
|
|
200
|
+
const inspectToolCalls = (value: Rec): boolean => {
|
|
201
|
+
for (const call of chatToolCalls(value)) {
|
|
202
|
+
if (call.invalid) return false;
|
|
203
|
+
const previous = toolNames.get(call.key);
|
|
204
|
+
const name = call.snapshot ? call.name : (previous ?? "") + call.name;
|
|
205
|
+
if (!declared.has(name) && ![...declared].some(allowed => allowed.startsWith(name))) return false;
|
|
206
|
+
const bytes = previous === undefined ? 128 + Buffer.byteLength(call.key) * 2 + Buffer.byteLength(name) * 2
|
|
207
|
+
: Math.max(0, Buffer.byteLength(name) - Buffer.byteLength(previous)) * 2;
|
|
208
|
+
options.translatorBudget.chargeRetained(bytes, scope);
|
|
209
|
+
toolNameBytes += bytes;
|
|
210
|
+
toolNames.set(call.key, name);
|
|
211
|
+
if (declared.has(name)) pendingToolNames.delete(call.key);
|
|
212
|
+
else pendingToolNames.add(call.key);
|
|
213
|
+
}
|
|
214
|
+
return true;
|
|
215
|
+
};
|
|
157
216
|
let sawFinish = false;
|
|
158
217
|
let sawDone = false;
|
|
159
218
|
let firstOutput = false;
|
|
@@ -191,7 +250,31 @@ export function nativeChatSse(
|
|
|
191
250
|
throw error;
|
|
192
251
|
}
|
|
193
252
|
};
|
|
194
|
-
const
|
|
253
|
+
const releaseHeld = () => {
|
|
254
|
+
options.translatorBudget.releaseRetained(heldBytes, scope);
|
|
255
|
+
heldBytes = 0;
|
|
256
|
+
heldFrames.length = 0;
|
|
257
|
+
};
|
|
258
|
+
const relay = (controller: ReadableStreamDefaultController<Uint8Array>, text: string) => {
|
|
259
|
+
if (hasPendingToolName()) {
|
|
260
|
+
const bytes = Buffer.byteLength(text) * 2;
|
|
261
|
+
options.translatorBudget.chargeRetained(bytes, scope);
|
|
262
|
+
heldBytes += bytes;
|
|
263
|
+
heldFrames.push(text);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
for (const frame of heldFrames) enqueue(controller, frame);
|
|
267
|
+
releaseHeld();
|
|
268
|
+
enqueue(controller, text);
|
|
269
|
+
};
|
|
270
|
+
const releaseBuffer = () => {
|
|
271
|
+
buffer.clear();
|
|
272
|
+
releaseHeld();
|
|
273
|
+
options.translatorBudget.releaseRetained(toolNameBytes, scope);
|
|
274
|
+
toolNameBytes = 0;
|
|
275
|
+
toolNames.clear();
|
|
276
|
+
pendingToolNames.clear();
|
|
277
|
+
};
|
|
195
278
|
const settle = (status: number, message?: string) => {
|
|
196
279
|
if (settled) return;
|
|
197
280
|
settled = true;
|
|
@@ -206,12 +289,12 @@ export function nativeChatSse(
|
|
|
206
289
|
type = "upstream_error",
|
|
207
290
|
) => {
|
|
208
291
|
const safeMessage = redactSecretString(message);
|
|
292
|
+
releaseBuffer();
|
|
209
293
|
try {
|
|
210
294
|
const frame = `data: ${JSON.stringify(chatCompletionsErrorBody(status, safeMessage, type, code))}\n\n`;
|
|
211
295
|
if (code === "translation_buffer_limit") controller.enqueue(encoder.encode(frame));
|
|
212
296
|
else enqueue(controller, frame);
|
|
213
297
|
} catch { /* controller may already be closed */ }
|
|
214
|
-
releaseBuffer();
|
|
215
298
|
settle(status, safeMessage);
|
|
216
299
|
try { void reader.cancel(new Error(safeMessage)).catch(() => {}); } catch { /* already closed */ }
|
|
217
300
|
try { controller.close(); } catch { /* already closed */ }
|
|
@@ -224,16 +307,20 @@ export function nativeChatSse(
|
|
|
224
307
|
const payload = sseDataPayload(block);
|
|
225
308
|
if (payload === null) {
|
|
226
309
|
if (performance.now() >= pullDeadline) throw stalled;
|
|
227
|
-
|
|
310
|
+
relay(controller, block + delimiter);
|
|
228
311
|
return;
|
|
229
312
|
}
|
|
230
313
|
const trimmed = payload.trim();
|
|
231
314
|
if (trimmed === "") {
|
|
232
315
|
if (performance.now() >= pullDeadline) throw stalled;
|
|
233
|
-
|
|
316
|
+
relay(controller, block + delimiter);
|
|
234
317
|
return;
|
|
235
318
|
}
|
|
236
319
|
if (trimmed === "[DONE]") {
|
|
320
|
+
if (hasPendingToolName()) {
|
|
321
|
+
fail(controller, UNDECLARED_CHAT_TOOL_MESSAGE, UNDECLARED_TOOL_CALL_ERROR_CODE);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
237
324
|
sawDone = true;
|
|
238
325
|
enqueue(controller, replaceSseDataPayload(block, "[DONE]") + delimiter);
|
|
239
326
|
releaseBuffer();
|
|
@@ -272,9 +359,20 @@ export function nativeChatSse(
|
|
|
272
359
|
return;
|
|
273
360
|
}
|
|
274
361
|
const usage = usageFromChat(parsed.usage);
|
|
362
|
+
options.onChunk?.(parsed);
|
|
275
363
|
if (usage) options.onUsage(usage);
|
|
364
|
+
if (options.clientDeclaredEmptyToolCatalog && !inspectToolCalls(parsed)) {
|
|
365
|
+
fail(controller, UNDECLARED_CHAT_TOOL_MESSAGE, UNDECLARED_TOOL_CALL_ERROR_CODE);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
276
368
|
const choices = Array.isArray(parsed.choices) ? parsed.choices : [];
|
|
277
369
|
const finished = choices.some(choice => isRec(choice) && typeof choice.finish_reason === "string" && choice.finish_reason.length > 0);
|
|
370
|
+
if (hasPendingToolName() && choices.some((choice, position) => isRec(choice)
|
|
371
|
+
&& typeof choice.finish_reason === "string" && choice.finish_reason.length > 0
|
|
372
|
+
&& [...pendingToolNames].some(key => key.startsWith(`${typeof choice.index === "number" ? choice.index : position}:`)))) {
|
|
373
|
+
fail(controller, UNDECLARED_CHAT_TOOL_MESSAGE, UNDECLARED_TOOL_CALL_ERROR_CODE);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
278
376
|
if (finished) sawFinish = true;
|
|
279
377
|
const hasText = choices.some(choice => {
|
|
280
378
|
if (!isRec(choice) || !isRec(choice.delta)) return false;
|
|
@@ -302,11 +400,11 @@ export function nativeChatSse(
|
|
|
302
400
|
const hasProgress = hasText || hasTool;
|
|
303
401
|
if (hasProgress || finished) pullDeadline = performance.now() + stallMs;
|
|
304
402
|
else if (performance.now() >= pullDeadline) throw stalled;
|
|
305
|
-
if (!firstOutput && hasProgress) {
|
|
403
|
+
if (!firstOutput && hasProgress && !hasPendingToolName()) {
|
|
306
404
|
firstOutput = true;
|
|
307
405
|
options.onFirstOutput?.();
|
|
308
406
|
}
|
|
309
|
-
|
|
407
|
+
relay(controller, replaceSseDataPayload(block, JSON.stringify(normalizedChunk(parsed, options.requestedModel))) + delimiter);
|
|
310
408
|
};
|
|
311
409
|
|
|
312
410
|
function onAbort() {
|
|
@@ -336,6 +434,7 @@ export function nativeChatSse(
|
|
|
336
434
|
if (next) {
|
|
337
435
|
buffer.compact();
|
|
338
436
|
processBlock(controller, next.block, next.delimiter);
|
|
437
|
+
if (heldFrames.length > 0 && !settled) continue;
|
|
339
438
|
return;
|
|
340
439
|
}
|
|
341
440
|
if (performance.now() >= pullDeadline) throw stalled;
|
|
@@ -370,6 +469,10 @@ export function nativeChatSse(
|
|
|
370
469
|
continue;
|
|
371
470
|
}
|
|
372
471
|
buffer.append(decoder.decode());
|
|
472
|
+
if (hasPendingToolName()) {
|
|
473
|
+
fail(controller, UNDECLARED_CHAT_TOOL_MESSAGE, UNDECLARED_TOOL_CALL_ERROR_CODE);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
373
476
|
if (buffer.tail().trim().length > 0) {
|
|
374
477
|
fail(controller, "upstream SSE ended with an unterminated event", "upstream_sse_unterminated");
|
|
375
478
|
return;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createPhysicalSendReporter } from "../lib/request-execution-budget";
|
|
2
2
|
import { createInferenceSendBudget } from "./inference/context";
|
|
3
|
+
import { observeAdvisorNativeReasoning } from "./inference/advisor-diagnostics";
|
|
3
4
|
import { buildOpenAIChatPassthroughRequest, createOpenAIChatAdapter } from "../adapters/openai-chat";
|
|
4
5
|
import type { AdapterRequest, ProviderAdapter } from "../adapters/base";
|
|
5
6
|
import { isNativeChatRouteEligible } from "./chat-native-eligibility";
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
isCyberPolicyCode,
|
|
19
20
|
isCyberPolicyMessage,
|
|
20
21
|
SEND_BUDGET_EXHAUSTED_CODE,
|
|
22
|
+
UNDECLARED_TOOL_CALL_ERROR_CODE,
|
|
21
23
|
} from "../lib/errors";
|
|
22
24
|
import { reportDispatchSends, type RequestExecutionBudget, type SingleUseDispatchPermit } from "../lib/request-execution-budget";
|
|
23
25
|
import type { AdmissionLease } from "../lib/admission";
|
|
@@ -54,10 +56,13 @@ import {
|
|
|
54
56
|
rateLimitRetryDelayMs,
|
|
55
57
|
rateLimitRetryPolicyFor,
|
|
56
58
|
rotateProviderTransportOn429,
|
|
59
|
+
rotateProviderTransportOn401,
|
|
57
60
|
transientRetryPolicyFor,
|
|
58
61
|
} from "../providers/key-failover";
|
|
59
62
|
import { fastPolicyForModel } from "../providers/service-tier";
|
|
63
|
+
import { RequestPacingQueueOverloadError } from "../providers/request-pacing";
|
|
60
64
|
import { stampApiKeyAccountLabel } from "../providers/label";
|
|
65
|
+
import { distributedKeyPhysicalFetch, distributedKeyResponseOwner } from "../providers/api-key-response-ownership";
|
|
61
66
|
import { providerApiKeySelectionIsCurrent, resolveCurrentProviderApiKeyTransport } from "../providers/api-key-selection";
|
|
62
67
|
import { enrichOpenCodeZenFreeTierMessage } from "../providers/opencode-zen-rate-limit";
|
|
63
68
|
import type { OcxProviderTransport } from "../providers/xai-transport";
|
|
@@ -74,7 +79,7 @@ import {
|
|
|
74
79
|
recordAttemptCredentialSource,
|
|
75
80
|
type RequestLogContext,
|
|
76
81
|
} from "./request-log";
|
|
77
|
-
import { jsonCompletionSse, nativeChatSse, structuredError, usageFromChat } from "./chat-native-sse";
|
|
82
|
+
import { chatResponseHasUndeclaredToolCalls, UNDECLARED_CHAT_TOOL_MESSAGE, jsonCompletionSse, nativeChatSse, structuredError, usageFromChat } from "./chat-native-sse";
|
|
78
83
|
import { beginInferenceAttempt, type InferenceAttempt } from "./inference/attempt";
|
|
79
84
|
import { createFinalRequestLog } from "./inference/final-log";
|
|
80
85
|
import { registerTurn, unregisterTurn } from "./lifecycle";
|
|
@@ -82,6 +87,7 @@ import { attachRequestSpendTracker } from "./responses/request-spend";
|
|
|
82
87
|
import { workflowRefusalResponse } from "./workflow-refusal";
|
|
83
88
|
import type { ComboProtocolSource } from "./responses/core-combo-native";
|
|
84
89
|
import type { ProtocolEnvelope } from "../protocols/envelope";
|
|
90
|
+
import { nativeChatReplayScope, bindNativeChatReplayScope, nativeChatReasoningCapture } from "./chat-native-reasoning";
|
|
85
91
|
|
|
86
92
|
export { isNativeChatRouteEligible, nativeChatDeclineReason } from "./chat-native-eligibility";
|
|
87
93
|
|
|
@@ -275,9 +281,15 @@ export async function runNativeChatAttempt(
|
|
|
275
281
|
): Promise<Response> {
|
|
276
282
|
const { req, config, logCtx, logIds, route, requestedModel, requestedStream, translatorBudget, finishLog } = execution;
|
|
277
283
|
const { sendBudget } = execution;
|
|
284
|
+
const clientDeclaredEmptyToolCatalog = Array.isArray(execution.chatBody.tools) && execution.chatBody.tools.length === 0;
|
|
285
|
+
const declaredLegacyFunctionNames = new Set<string>(clientDeclaredEmptyToolCatalog && Array.isArray(execution.chatBody.functions)
|
|
286
|
+
? execution.chatBody.functions.flatMap(fn => isRec(fn) && typeof fn.name === "string" && fn.name.length > 0 ? [fn.name] : [])
|
|
287
|
+
: []);
|
|
278
288
|
const { attempt } = attemptHandle;
|
|
279
|
-
|
|
289
|
+
let outputDelivered = false;
|
|
290
|
+
const notifyFirstOutput = execution.onFirstOutput
|
|
280
291
|
?? (logIds ? () => recordFirstOutput(logCtx, logIds.start) : undefined);
|
|
292
|
+
const onFirstOutput = () => { outputDelivered = true; notifyFirstOutput?.(); };
|
|
281
293
|
const fail = (status: number, message: string, type?: string, code?: string | null): Response => {
|
|
282
294
|
const safeMessage = redactSecretString(message);
|
|
283
295
|
finishLog(status, safeMessage);
|
|
@@ -313,14 +325,23 @@ export async function runNativeChatAttempt(
|
|
|
313
325
|
// assigned before the transport pin in core.ts.
|
|
314
326
|
// Transport variant: the bare picker answers with the persisted row, which for a built-in
|
|
315
327
|
// provider carries no adapter id or base URL until routedProviderConfig backfills it.
|
|
316
|
-
|
|
317
|
-
|
|
328
|
+
try {
|
|
329
|
+
const proactiveKeyProvider = selectProactiveApiKeyTransport(config, route.providerName, route.provider);
|
|
330
|
+
if (proactiveKeyProvider) route.provider = proactiveKeyProvider;
|
|
331
|
+
} catch (error) {
|
|
332
|
+
cleanupAbort(); upstream.abort();
|
|
333
|
+
if (!(error instanceof RequestPacingQueueOverloadError)) throw error;
|
|
334
|
+
const refusal = fail(429, error.message, "rate_limit_error");
|
|
335
|
+
refusal.headers.set("Retry-After", String(error.retryAfterSeconds));
|
|
336
|
+
return refusal;
|
|
337
|
+
}
|
|
318
338
|
let activeProvider: OcxProviderConfig = route.provider;
|
|
319
339
|
stampApiKeyAccountLabel(logCtx, route.providerName, activeProvider);
|
|
320
340
|
const spendTracker = sendBudget ? undefined : attachRequestSpendTracker(req, logCtx);
|
|
321
341
|
const physicalBudget = sendBudget ?? createInferenceSendBudget(req, logCtx);
|
|
322
342
|
let activeAdapter: ProviderAdapter = createOpenAIChatAdapter(activeProvider);
|
|
323
343
|
let activeRequest: AdapterRequest;
|
|
344
|
+
const replayScope = nativeChatReplayScope(req.headers);
|
|
324
345
|
let retainedRequestBytes = 0;
|
|
325
346
|
const releaseRetainedRequest = () => {
|
|
326
347
|
if (retainedRequestBytes === 0) return;
|
|
@@ -333,8 +354,9 @@ export async function runNativeChatAttempt(
|
|
|
333
354
|
retainedRequestBytes = bytes;
|
|
334
355
|
};
|
|
335
356
|
const buildActiveRequest = () => {
|
|
357
|
+
bindNativeChatReplayScope(replayScope, activeProvider, route.providerName, route.modelId);
|
|
336
358
|
recordAttemptCredentialSource(attempt, route.providerName, activeProvider, activeAdapter.name);
|
|
337
|
-
|
|
359
|
+
const request = buildOpenAIChatPassthroughRequest(
|
|
338
360
|
activeProvider,
|
|
339
361
|
execution.chatBody,
|
|
340
362
|
route.modelId,
|
|
@@ -342,7 +364,10 @@ export async function runNativeChatAttempt(
|
|
|
342
364
|
fastPolicyForModel(activeProvider, route.modelId, route.providerName, "chat"),
|
|
343
365
|
config.fastMode,
|
|
344
366
|
route.providerName,
|
|
367
|
+
replayScope,
|
|
345
368
|
);
|
|
369
|
+
observeAdvisorNativeReasoning(req.headers, logCtx, request.body);
|
|
370
|
+
return request;
|
|
346
371
|
};
|
|
347
372
|
try {
|
|
348
373
|
activeRequest = buildActiveRequest();
|
|
@@ -421,9 +446,10 @@ export async function runNativeChatAttempt(
|
|
|
421
446
|
} else if (!spendTracker?.charge()) throw new NativeChatSpendRefusal();
|
|
422
447
|
};
|
|
423
448
|
|
|
449
|
+
let responseOwnsDistributedKey = true;
|
|
424
450
|
const send = async (
|
|
425
451
|
request: AdapterRequest,
|
|
426
|
-
recovery?: "rate-limit-429" | "key-429" | UpstreamSendRecovery,
|
|
452
|
+
recovery?: "rate-limit-429" | "key-429" | "key-401" | UpstreamSendRecovery,
|
|
427
453
|
singleShot = false,
|
|
428
454
|
): Promise<Response> => {
|
|
429
455
|
try {
|
|
@@ -453,9 +479,16 @@ export async function runNativeChatAttempt(
|
|
|
453
479
|
onPhysicalDispatch: comboReceipt
|
|
454
480
|
? () => { if (!physicalBudget.spendEnforced) settleNonEnforcedSend(request.url); }
|
|
455
481
|
: undefined,
|
|
456
|
-
dispatchOverride: async (_input, init, execute) => {
|
|
457
|
-
|
|
458
|
-
|
|
482
|
+
dispatchOverride: async (_input, init, execute, reacquirePacing) => {
|
|
483
|
+
for (let attempt = 0; !providerApiKeySelectionIsCurrent(config, route.providerName, activeProvider); attempt++) {
|
|
484
|
+
if (outputDelivered) throw new Error("Provider key selection is unavailable during the native Chat turn");
|
|
485
|
+
if (attempt >= 3) throw new Error("Provider key selection changed repeatedly before native Chat dispatch");
|
|
486
|
+
let current = resolveCurrentProviderApiKeyTransport(config, route.providerName, activeProvider);
|
|
487
|
+
if (!current && activeProvider.requestPacing?.enabled
|
|
488
|
+
&& (activeProvider.requestPacing.keys || activeProvider.requestPacing.keySelection === "round-robin")) {
|
|
489
|
+
const fresh = resolveCurrentProviderApiKeyTransport(config, route.providerName, { ...activeProvider, _apiKeyAttempt: undefined });
|
|
490
|
+
if (fresh) current = selectProactiveApiKeyTransport(config, route.providerName, fresh) ?? fresh;
|
|
491
|
+
}
|
|
459
492
|
if (!current || !isNativeChatRouteEligible({ ...route, provider: current }, execution.chatBody, config)) {
|
|
460
493
|
throw new Error("Provider key selection is no longer available for native Chat");
|
|
461
494
|
}
|
|
@@ -467,6 +500,11 @@ export async function runNativeChatAttempt(
|
|
|
467
500
|
activeRequest = buildActiveRequest();
|
|
468
501
|
try { retainRequest(activeRequest); }
|
|
469
502
|
catch (error) { activeRequest.releaseBodyObservation?.(); throw error; }
|
|
503
|
+
if (current.requestPacing?.enabled
|
|
504
|
+
&& (current.requestPacing.keys || current.requestPacing.keySelection === "round-robin")) {
|
|
505
|
+
if (!reacquirePacing) throw new Error("Refreshed native Chat dispatch requires pacing readmission");
|
|
506
|
+
await reacquirePacing(current, route.modelId);
|
|
507
|
+
}
|
|
470
508
|
}
|
|
471
509
|
// The retry closure may still hold a pre-pacing request. Replace its entire
|
|
472
510
|
// wire shape, not just Authorization, and retain transport recovery flags.
|
|
@@ -495,7 +533,8 @@ export async function runNativeChatAttempt(
|
|
|
495
533
|
// A reselected provider transport is still a physical send: the connection policy
|
|
496
534
|
// and manual-redirect ownership wrap the selected implementation (#4992).
|
|
497
535
|
dispatched = await sendWithConnectionPolicy(
|
|
498
|
-
(activeProvider as OcxProviderTransport).fetch ?? execute,
|
|
536
|
+
distributedKeyPhysicalFetch((activeProvider as OcxProviderTransport).fetch ?? execute,
|
|
537
|
+
route.providerName, activeProvider, request.url),
|
|
499
538
|
request.url,
|
|
500
539
|
applyUpstreamRecoveryInit({
|
|
501
540
|
...init, method: request.method, headers, body: request.body,
|
|
@@ -510,6 +549,8 @@ export async function runNativeChatAttempt(
|
|
|
510
549
|
try { if (physicalBudget.spendEnforced) spendReport(1); }
|
|
511
550
|
finally { spendReport.close?.(); }
|
|
512
551
|
}
|
|
552
|
+
responseOwnsDistributedKey = !activeProvider._apiKeyAttempt?.distributed
|
|
553
|
+
|| distributedKeyResponseOwner(dispatched, config, route.providerName) !== null;
|
|
513
554
|
if (!dispatched.ok) await recordKeyAttemptFailure(logCtx, dispatched, init.signal ?? upstream.signal);
|
|
514
555
|
return dispatched;
|
|
515
556
|
},
|
|
@@ -566,8 +607,11 @@ export async function runNativeChatAttempt(
|
|
|
566
607
|
}
|
|
567
608
|
// Same reason as above, plus a second one: rotating here would write a cooldown against
|
|
568
609
|
// a key that rate-limited nothing, and that false signal outlives the request.
|
|
569
|
-
while (response.status === 429
|
|
570
|
-
|
|
610
|
+
while ((response.status === 429 || (response.status === 401 && activeProvider._apiKeyAttempt?.distributed))
|
|
611
|
+
&& responseOwnsDistributedKey && !isNonReplayableResponse(response) && hasKeyPoolFailover(activeProvider)) {
|
|
612
|
+
const failureStatus = response.status;
|
|
613
|
+
const rotate = failureStatus === 401 ? rotateProviderTransportOn401 : rotateProviderTransportOn429;
|
|
614
|
+
const rotated = rotate(config, route.providerName, activeProvider, {
|
|
571
615
|
retryAfter: response.headers.get("retry-after"),
|
|
572
616
|
now: Date.now(),
|
|
573
617
|
attemptedKey: activeProvider.apiKey,
|
|
@@ -586,7 +630,7 @@ export async function runNativeChatAttempt(
|
|
|
586
630
|
releaseRetainedRequest();
|
|
587
631
|
activeRequest = buildActiveRequest();
|
|
588
632
|
retainRequest(activeRequest);
|
|
589
|
-
response = await send(activeRequest, "key-429");
|
|
633
|
+
response = await send(activeRequest, failureStatus === 401 ? "key-401" : "key-429");
|
|
590
634
|
}
|
|
591
635
|
} catch (error) {
|
|
592
636
|
releaseRetainedRequest();
|
|
@@ -594,6 +638,11 @@ export async function runNativeChatAttempt(
|
|
|
594
638
|
upstream.abort();
|
|
595
639
|
if (req.signal.aborted) return fail(499, "Client cancelled request", "client_cancelled");
|
|
596
640
|
const sendError = error instanceof UpstreamRetryEvidenceError ? error.cause : error;
|
|
641
|
+
if (sendError instanceof RequestPacingQueueOverloadError) {
|
|
642
|
+
const refusal = fail(429, sendError.message, "rate_limit_error");
|
|
643
|
+
refusal.headers.set("Retry-After", String(sendError.retryAfterSeconds));
|
|
644
|
+
return refusal;
|
|
645
|
+
}
|
|
597
646
|
if (sendError instanceof SendBudgetExhaustedError) {
|
|
598
647
|
// A decision this process made, answered as the Responses path answers it: 429, not 502.
|
|
599
648
|
return fail(429, sendError.message, SEND_BUDGET_EXHAUSTED_CODE, SEND_BUDGET_EXHAUSTED_CODE);
|
|
@@ -704,6 +753,7 @@ export async function runNativeChatAttempt(
|
|
|
704
753
|
}
|
|
705
754
|
|
|
706
755
|
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
756
|
+
const reasoningCapture = nativeChatReasoningCapture(activeProvider, route.modelId, replayScope, translatorBudget);
|
|
707
757
|
if (contentType.includes("text/event-stream") && response.body) {
|
|
708
758
|
if (requestedStream) transferTurnToStream();
|
|
709
759
|
let terminalStatus: number | undefined;
|
|
@@ -740,7 +790,10 @@ export async function runNativeChatAttempt(
|
|
|
740
790
|
signal: upstream.signal,
|
|
741
791
|
stallTimeoutSec: config.stallTimeoutSec,
|
|
742
792
|
localUpstream: isLocalUpstream(activeProvider.baseUrl),
|
|
793
|
+
clientDeclaredEmptyToolCatalog,
|
|
794
|
+
declaredLegacyFunctionNames,
|
|
743
795
|
onFirstOutput,
|
|
796
|
+
onChunk: reasoningCapture.observe,
|
|
744
797
|
onUsage: usage => {
|
|
745
798
|
if (!recordKeyWireAttemptUsage(logCtx, usage)) {
|
|
746
799
|
logCtx.usage = usage;
|
|
@@ -748,6 +801,7 @@ export async function runNativeChatAttempt(
|
|
|
748
801
|
}
|
|
749
802
|
},
|
|
750
803
|
onTerminal: (status: number, message?: string) => {
|
|
804
|
+
reasoningCapture.finish(status < 400);
|
|
751
805
|
terminalStatus = status;
|
|
752
806
|
if (!requestedStream) return;
|
|
753
807
|
try {
|
|
@@ -758,8 +812,9 @@ export async function runNativeChatAttempt(
|
|
|
758
812
|
releaseStreamTurn();
|
|
759
813
|
}
|
|
760
814
|
},
|
|
761
|
-
|
|
762
|
-
|
|
815
|
+
onCancel: () => {
|
|
816
|
+
reasoningCapture.finish(false);
|
|
817
|
+
if (requestedStream) {
|
|
763
818
|
try {
|
|
764
819
|
cleanupAbort();
|
|
765
820
|
upstream.abort();
|
|
@@ -767,8 +822,8 @@ export async function runNativeChatAttempt(
|
|
|
767
822
|
} finally {
|
|
768
823
|
releaseStreamTurn();
|
|
769
824
|
}
|
|
770
|
-
}
|
|
771
|
-
}
|
|
825
|
+
}
|
|
826
|
+
},
|
|
772
827
|
});
|
|
773
828
|
if (requestedStream) {
|
|
774
829
|
return new Response(stream, {
|
|
@@ -833,6 +888,7 @@ export async function runNativeChatAttempt(
|
|
|
833
888
|
if (error) return fail(error.status ?? 502, error.message, error.type, error.code);
|
|
834
889
|
const completion = chatCompletionJson(parsedJson);
|
|
835
890
|
if (!completion) return fail(502, "upstream response contained no choices", "upstream_error");
|
|
891
|
+
reasoningCapture.json(completion);
|
|
836
892
|
const usage = usageFromChat(completion.usage);
|
|
837
893
|
if (usage) {
|
|
838
894
|
if (!recordKeyWireAttemptUsage(logCtx, usage)) {
|
|
@@ -840,6 +896,9 @@ export async function runNativeChatAttempt(
|
|
|
840
896
|
attempt.usage = usage;
|
|
841
897
|
}
|
|
842
898
|
}
|
|
899
|
+
if (clientDeclaredEmptyToolCatalog && chatResponseHasUndeclaredToolCalls(completion, declaredLegacyFunctionNames)) {
|
|
900
|
+
return fail(502, UNDECLARED_CHAT_TOOL_MESSAGE, "upstream_error", UNDECLARED_TOOL_CALL_ERROR_CODE);
|
|
901
|
+
}
|
|
843
902
|
onFirstOutput?.();
|
|
844
903
|
try {
|
|
845
904
|
const serialized = requestedStream
|