@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005
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/bin/ocx.mjs +11 -10
- package/gui/dist/assets/App-CppMz-6w.js +54 -0
- package/gui/dist/assets/App-lCIslZ1L.css +1 -0
- package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
- package/gui/dist/assets/index-DQt9srNi.js +94 -0
- package/gui/dist/assets/index-DmQB8D_R.css +1 -0
- package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
- package/gui/dist/index.html +4 -3
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +19 -2
- package/src/adapters/anthropic/client-identity.ts +13 -4
- package/src/adapters/anthropic/native-client-preamble.ts +31 -0
- package/src/adapters/anthropic/passthrough.ts +103 -25
- package/src/adapters/antigravity-refusal.ts +28 -0
- package/src/adapters/cursor/protobuf-request.ts +6 -0
- package/src/adapters/cursor/request-builder.ts +3 -0
- package/src/adapters/cursor/structured-output.ts +13 -0
- package/src/adapters/cursor/types.ts +2 -0
- package/src/adapters/devin/cloud-direct/chat.ts +7 -2
- package/src/adapters/devin/trajectory.ts +49 -0
- package/src/adapters/devin.ts +13 -0
- package/src/adapters/ollama-native.ts +62 -19
- package/src/adapters/openai-responses/passthrough.ts +133 -103
- package/src/adapters/openai-responses/request-strips.ts +10 -4
- package/src/adapters/zed.ts +26 -4
- package/src/bridge/response-json.ts +14 -14
- package/src/bridge/sse.ts +15 -5
- package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
- package/src/claude/agents-inject.ts +4 -43
- package/src/claude/gateway-cache.ts +16 -13
- package/src/claude/inbound-model-options.ts +9 -2
- package/src/claude/intercept/cli-catalog.ts +27 -16
- package/src/claude/intercept/client-hello.ts +103 -0
- package/src/claude/intercept/model-bindings.ts +3 -2
- package/src/claude/intercept/picker-bootstrap.ts +37 -16
- package/src/claude/intercept/picker-budget.ts +41 -0
- package/src/claude/intercept/picker-listener.ts +279 -66
- package/src/claude/outbound.ts +2 -0
- package/src/claude/subagent-force-status.ts +53 -0
- package/src/claude/subagent-model.ts +93 -0
- package/src/cli/access-audio-input.ts +84 -0
- package/src/cli/access-audio-live.ts +99 -0
- package/src/cli/access-audio.ts +123 -0
- package/src/cli/access-data-client.ts +127 -0
- package/src/cli/access-data-plane.ts +104 -0
- package/src/cli/access-data-response.ts +76 -0
- package/src/cli/access.ts +65 -2
- package/src/cli/account-api.ts +33 -6
- package/src/cli/account-auth.ts +111 -42
- package/src/cli/account-key-quota.ts +118 -0
- package/src/cli/account-main.ts +11 -4
- package/src/cli/account-next-actions.ts +63 -0
- package/src/cli/account-policy-dto.ts +68 -0
- package/src/cli/account-policy.ts +167 -0
- package/src/cli/account-target.ts +8 -0
- package/src/cli/account.ts +37 -9
- package/src/cli/agent-runtime-settings.ts +130 -0
- package/src/cli/agent-settings.ts +101 -0
- package/src/cli/agent.ts +29 -2
- package/src/cli/alias.ts +7 -2
- package/src/cli/aside-profiles.ts +2 -2
- package/src/cli/capabilities-access-remote.ts +147 -0
- package/src/cli/capabilities-accounts.ts +195 -0
- package/src/cli/capabilities-agents-routing.ts +414 -0
- package/src/cli/capabilities-base.ts +783 -0
- package/src/cli/capabilities-command.ts +1 -0
- package/src/cli/capabilities-integrations.ts +264 -0
- package/src/cli/capabilities-lab.ts +214 -0
- package/src/cli/capabilities-observe-system.ts +662 -0
- package/src/cli/capabilities-provider-models.ts +302 -0
- package/src/cli/capabilities.ts +30 -1190
- package/src/cli/capability-types.ts +54 -0
- package/src/cli/catalog-command-result.ts +57 -0
- package/src/cli/chatgpt-command.ts +35 -9
- package/src/cli/claude-agent-startup-sync.ts +17 -0
- package/src/cli/claude-desktop-profile.ts +100 -0
- package/src/cli/claude-desktop.ts +39 -6
- package/src/cli/claude.ts +22 -6
- package/src/cli/codex-shim-autorestore.ts +2 -1
- package/src/cli/codex-shim-readiness.ts +109 -0
- package/src/cli/combo-input.ts +88 -0
- package/src/cli/combo-result.ts +52 -0
- package/src/cli/combo-stats.ts +123 -0
- package/src/cli/combo.ts +37 -14
- package/src/cli/companion-timeline.ts +142 -0
- package/src/cli/companion-usage.ts +146 -0
- package/src/cli/companion.ts +12 -3
- package/src/cli/config-command.ts +24 -8
- package/src/cli/connect.ts +19 -0
- package/src/cli/dispatch.ts +55 -82
- package/src/cli/doctor.ts +22 -4
- package/src/cli/export-command.ts +20 -9
- package/src/cli/gui.ts +8 -3
- package/src/cli/help-catalog.ts +101 -0
- package/src/cli/help-models-context.ts +17 -0
- package/src/cli/help-navigation.ts +50 -0
- package/src/cli/help-recovery.ts +66 -0
- package/src/cli/help.ts +69 -11
- package/src/cli/index.ts +38 -27
- package/src/cli/injection-follow.ts +35 -0
- package/src/cli/inspect.ts +5 -0
- package/src/cli/integration-aside-sync.ts +69 -0
- package/src/cli/integration-cursor.ts +106 -0
- package/src/cli/integration-input.ts +60 -0
- package/src/cli/integration-journal.ts +49 -0
- package/src/cli/integration-plan-dto.ts +156 -0
- package/src/cli/integration-preview.ts +215 -0
- package/src/cli/integrations.ts +24 -18
- package/src/cli/json-input.ts +134 -0
- package/src/cli/link.ts +32 -4
- package/src/cli/local-sync-result.ts +41 -0
- package/src/cli/log-follow.ts +84 -0
- package/src/cli/log-view-filter.ts +146 -0
- package/src/cli/logout-command.ts +44 -0
- package/src/cli/model-picker-ordering.ts +137 -0
- package/src/cli/models-custom-input.ts +107 -0
- package/src/cli/models-custom-runtime.ts +112 -0
- package/src/cli/models-order.ts +194 -0
- package/src/cli/models-runtime-subcommands.ts +2 -0
- package/src/cli/models-runtime.ts +9 -1
- package/src/cli/models.ts +66 -81
- package/src/cli/observe-stream.ts +142 -0
- package/src/cli/observe.ts +75 -34
- package/src/cli/opencode.ts +54 -10
- package/src/cli/provider-batch.ts +77 -0
- package/src/cli/provider-lifecycle-runtime.ts +170 -0
- package/src/cli/provider-result.ts +124 -0
- package/src/cli/provider-runtime.ts +15 -12
- package/src/cli/provider-settings.ts +143 -0
- package/src/cli/provider.ts +116 -64
- package/src/cli/registry.ts +90 -24
- package/src/cli/remote-workspace-hub.ts +83 -0
- package/src/cli/remote-workspace.ts +6 -1
- package/src/cli/restart-failure.ts +27 -0
- package/src/cli/root.ts +42 -9
- package/src/cli/route-policy-write.ts +137 -0
- package/src/cli/route-policy.ts +8 -0
- package/src/cli/runtime-api.ts +57 -80
- package/src/cli/secret-args.ts +149 -0
- package/src/cli/settings-result.ts +30 -0
- package/src/cli/storage.ts +77 -7
- package/src/cli/system-command.ts +60 -2
- package/src/cli/system-restart-client.ts +20 -3
- package/src/cli/system-settings-parity.ts +83 -0
- package/src/cli/tray-proxy.ts +3 -2
- package/src/cli/update-restart-candidate.ts +21 -0
- package/src/cli/update-restart-child.ts +81 -0
- package/src/cli/update-restart-home.ts +38 -0
- package/src/cli/update-restart-transport.ts +193 -0
- package/src/cli/update-restart.ts +260 -0
- package/src/cli/usage-model-search.ts +58 -0
- package/src/cli/usage-report.ts +13 -5
- package/src/cli/v2-input.ts +66 -0
- package/src/cli/v2-local-output.ts +37 -0
- package/src/cli/v2-runtime.ts +90 -0
- package/src/cli/v2.ts +102 -144
- package/src/clients/config-export/contracts.ts +100 -0
- package/src/clients/config-export/droid.ts +54 -2
- package/src/clients/config-export/dsh.ts +21 -0
- package/src/clients/config-export/kilo.ts +12 -2
- package/src/clients/config-export/model-metadata.ts +10 -4
- package/src/clients/config-export/reasoning-metadata.ts +47 -0
- package/src/clients/config-export.ts +53 -67
- package/src/clients/omo-role-models.ts +17 -3
- package/src/codex/account-credit-use.ts +4 -2
- package/src/codex/account-runtime-state.ts +21 -0
- package/src/codex/account-store.ts +11 -47
- package/src/codex/account-usability.ts +11 -8
- package/src/codex/auth-api/account-list.ts +73 -63
- package/src/codex/auth-api/main-account-probe.ts +25 -12
- package/src/codex/auth-api/pool-mode-gate.ts +6 -9
- package/src/codex/auth-api/pool-quota-probe.ts +2 -4
- package/src/codex/auth-api/reset-credit-service.ts +3 -2
- package/src/codex/auth-context.ts +30 -11
- package/src/codex/catalog/aggregation.ts +3 -0
- package/src/codex/catalog/build-entries.ts +1 -1
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/heal-observation.ts +66 -0
- package/src/codex/catalog/model-hints.ts +2 -1
- package/src/codex/catalog/model-visibility.ts +6 -3
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +13 -4
- package/src/codex/catalog/publication-observer.ts +19 -0
- package/src/codex/catalog/remote.ts +13 -15
- package/src/codex/catalog/restore.ts +19 -6
- package/src/codex/catalog/retained-sync.ts +60 -11
- package/src/codex/catalog/routed-removal.ts +116 -0
- package/src/codex/catalog/write-audit-contract.ts +21 -0
- package/src/codex/catalog/write-audit.ts +171 -0
- package/src/codex/catalog-self-heal.ts +284 -0
- package/src/codex/catalog-write-serialization.ts +71 -7
- package/src/codex/chatgpt-refresh-failure.ts +56 -0
- package/src/codex/codex-home-owner.ts +124 -0
- package/src/codex/convergence.ts +57 -16
- package/src/codex/features.ts +8 -1
- package/src/codex/inject/restore.ts +46 -4
- package/src/codex/inject.ts +11 -1
- package/src/codex/internal/catalog-writer.ts +120 -13
- package/src/codex/journal.ts +49 -13
- package/src/codex/main-account.ts +144 -11
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/quota-refresh-outcome.ts +17 -2
- package/src/codex/quota-types.ts +36 -1
- package/src/codex/quota.ts +67 -16
- package/src/codex/refresh.ts +20 -9
- package/src/codex/routing/cooldown-math.ts +10 -4
- package/src/codex/routing/selection.ts +11 -4
- package/src/codex/routing.ts +9 -9
- package/src/codex/runtime.ts +7 -1
- package/src/codex/shim-diagnostics.ts +80 -0
- package/src/codex/shim-fingerprint.ts +7 -2
- package/src/codex/shim-migration.ts +153 -0
- package/src/codex/shim-overlay.ts +509 -0
- package/src/codex/shim-path-resolution.ts +145 -0
- package/src/codex/shim-probe.ts +4 -3
- package/src/codex/shim-state-file.ts +93 -7
- package/src/codex/shim.ts +54 -107
- package/src/codex/subagent-model-fallback.ts +2 -1
- package/src/codex/sync.ts +24 -4
- package/src/combos/failover.ts +101 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/jev.ts +13 -3
- package/src/combos/resolve.ts +12 -0
- package/src/config/atomic-write.ts +13 -8
- package/src/config/diagnostics.ts +23 -1
- package/src/config/load-degrade.ts +7 -0
- package/src/config/provider-validation.ts +3 -0
- package/src/config/schema/chatgpt-desktop.ts +22 -0
- package/src/config/schema/compaction-triggers.ts +8 -0
- package/src/config/schema/config-schema.ts +43 -14
- package/src/config/schema/leaf-validators.ts +8 -3
- package/src/config/subagent-models.ts +12 -0
- package/src/generated/compatibility-version.json +645 -237
- package/src/grok/session-identity.ts +33 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/merge.ts +13 -5
- package/src/integrations/mutation-plan.ts +50 -5
- package/src/integrations/omp-yaml-source.ts +181 -1
- package/src/integrations/ownership-policy.ts +66 -0
- package/src/integrations/registry.ts +50 -1
- package/src/integrations/state.ts +106 -19
- package/src/integrations/target.ts +25 -2
- package/src/integrations/writer.ts +107 -35
- package/src/lib/gui-pair-capability.ts +17 -0
- package/src/lib/package-tree-integrity.ts +42 -1
- package/src/lib/provider-client-headers.ts +62 -0
- package/src/lib/proxy-env.ts +3 -0
- package/src/lib/test-home-guard.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +19 -8
- package/src/oauth/anthropic-continuity.ts +65 -0
- package/src/oauth/anthropic-identity.ts +75 -0
- package/src/oauth/anthropic-routing.ts +20 -15
- package/src/oauth/anthropic.ts +34 -3
- package/src/oauth/generic-account-failover.ts +21 -0
- package/src/oauth/health.ts +32 -5
- package/src/oauth/index.ts +42 -35
- package/src/oauth/local-token-detect.ts +7 -0
- package/src/oauth/pool-settings-capability.ts +9 -2
- package/src/oauth/refresh-policy.ts +2 -0
- package/src/oauth/store.ts +55 -6
- package/src/oauth/types.ts +9 -0
- package/src/protocols/encoders/messages.ts +4 -3
- package/src/protocols/plan-snapshot.ts +1 -1
- package/src/protocols/settings.ts +28 -4
- package/src/providers/antigravity-effort-families.ts +75 -0
- package/src/providers/antigravity-models.ts +54 -16
- package/src/providers/antigravity-wire-snapshot.ts +66 -0
- package/src/providers/derive.ts +2 -0
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/new-model-policy.ts +48 -10
- package/src/providers/registry/entries-extended.ts +3 -0
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/model-seeds.ts +5 -0
- package/src/providers/registry/types.ts +4 -0
- package/src/responses/compaction.ts +19 -0
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +9 -0
- package/src/server/caller-session-identity.ts +32 -0
- package/src/server/chat-completions.ts +10 -0
- package/src/server/claude-messages.ts +29 -15
- package/src/server/droid-reasoning-default.ts +50 -0
- package/src/server/github-copilot-responses-repair.ts +3 -6
- package/src/server/grok-responses-control-frame.ts +2 -2
- package/src/server/grok-responses-snapshot-repair.ts +4 -5
- package/src/server/gui-session.ts +33 -11
- package/src/server/index/claude-intercept-lifecycle.ts +4 -0
- package/src/server/index/serve-options.ts +13 -7
- package/src/server/index.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +12 -134
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +8 -1
- package/src/server/management/cursor-integration-routes.ts +3 -3
- package/src/server/management/integration-routes.ts +69 -11
- package/src/server/management/link-routes.ts +20 -13
- package/src/server/management/model-routes.ts +3 -3
- package/src/server/management/model-row-export-metadata.ts +150 -0
- package/src/server/management/model-rows.ts +148 -9
- package/src/server/management/native-integration-routes.ts +2 -2
- package/src/server/management/oauth-account-routes.ts +96 -30
- package/src/server/management/route-registry.ts +29 -27
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/subagent-model-routes.ts +214 -0
- package/src/server/messages-native-eligibility.ts +3 -6
- package/src/server/messages-native-oauth.ts +82 -35
- package/src/server/messages-native.ts +78 -16
- package/src/server/relay.ts +14 -44
- package/src/server/request-log-generation-window.ts +38 -0
- package/src/server/request-log.ts +13 -0
- package/src/server/responses/adapter-dispatch.ts +152 -28
- package/src/server/responses/codex-auth-error.ts +10 -2
- package/src/server/responses/combo-stream-preflight.ts +29 -3
- package/src/server/responses/compaction-routing.ts +10 -1
- package/src/server/responses/core-auth.ts +67 -30
- package/src/server/responses/core-codex-account.ts +6 -3
- package/src/server/responses/core-combo-failure.ts +5 -1
- package/src/server/responses/core-combo-native.ts +7 -1
- package/src/server/responses/core-combo.ts +64 -7
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/fetch-helpers.ts +9 -0
- package/src/server/responses/passthrough-delivery.ts +1 -1
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/request-prepare.ts +5 -0
- package/src/server/responses/request-spend.ts +30 -30
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/responses-hosted-image-display.ts +70 -9
- package/src/server/responses-terminal-repair.ts +15 -2
- package/src/server/sse-frame-buffer.ts +38 -36
- package/src/server/sse-payload-rewrite.ts +78 -49
- package/src/server/ws-bridge.ts +1 -11
- package/src/service/cli.ts +6 -1
- package/src/service/ownership-mutation-lease.d.mts +26 -0
- package/src/service/ownership-mutation-lease.mjs +76 -1
- package/src/service/windows-taskxml.ts +1 -2
- package/src/service-manager-probe.ts +36 -2
- package/src/types/config.ts +6 -0
- package/src/types/provider.ts +28 -10
- package/src/update/index.ts +2 -1
- package/src/update/npm-invocation.mjs +35 -3
- package/src/update/proxy-liveness-probe.mjs +38 -8
- package/src/update/update-failure-guidance.d.mts +6 -0
- package/src/update/update-failure-guidance.mjs +30 -5
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +7 -0
- package/src/usage/summary.ts +82 -1
- package/src/web-search/loop.ts +11 -8
- package/src/web-search/run-turn-loop.ts +10 -7
- package/gui/dist/assets/App-CddorHdA.js +0 -51
- package/gui/dist/assets/App-DMudtISW.css +0 -1
- package/gui/dist/assets/index-Btze1I3T.css +0 -1
- package/gui/dist/assets/index-CvnY7E6i.js +0 -86
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
|
@@ -2,19 +2,15 @@
|
|
|
2
2
|
* Anthropic OAuth on the managed native Messages lane (PF-10, behind
|
|
3
3
|
* `protocols.rollout.managedMessagesNativeOAuth`).
|
|
4
4
|
*
|
|
5
|
-
* Credential.
|
|
6
|
-
* the same
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* proposal against the captured selection so a concurrent manual switch wins. Nothing here picks
|
|
10
|
-
* an account of its own, and nothing runs at planning time: the planner and eligibility read
|
|
11
|
-
* config only, and this module is reached from the lane's dispatch alone.
|
|
5
|
+
* Credential. Shared Anthropic routing chooses the account for the session and model;
|
|
6
|
+
* the same generation-fenced selection commit as the Responses transport admits it. A lost
|
|
7
|
+
* commit re-evaluates routing so a concurrent manual switch or policy edit wins. Selection,
|
|
8
|
+
* token resolution and affinity binding run at dispatch only, never during planning.
|
|
12
9
|
*
|
|
13
|
-
* Pools.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* lane-change 409 is distinct from the shared local authentication/pause/cooldown refusals.
|
|
10
|
+
* Pools. Native Messages uses the OAuth owner's strategy, model allowlist, session affinity,
|
|
11
|
+
* pause and cooldown admission. Pre-output refusal recovery proposes a replacement account;
|
|
12
|
+
* this module admits and binds its exact generation before a rebuilt request may send.
|
|
13
|
+
* A lane-change 409 remains distinct from typed local authentication/pause/cooldown refusals.
|
|
18
14
|
*
|
|
19
15
|
* Tool names. An OAuth request carries client tool names under the Claude OAuth prefix, as the
|
|
20
16
|
* adapter sends them; the answer's `tool_use` names are mapped back here, for exactly the names
|
|
@@ -25,12 +21,15 @@
|
|
|
25
21
|
import type { OAuthAccessSnapshot } from "../oauth";
|
|
26
22
|
import {
|
|
27
23
|
commitAnthropicSelectionRouting,
|
|
24
|
+
captureAnthropicManualSelectionGeneration,
|
|
28
25
|
getAnthropicPoolAccessSnapshot,
|
|
29
26
|
getAnthropicAccountHealthSnapshot,
|
|
30
27
|
resolveAnthropicDispatchAccountId,
|
|
31
|
-
|
|
28
|
+
resolveAnthropicAccountForSession,
|
|
29
|
+
getEligibleAnthropicAccounts,
|
|
32
30
|
isAnthropicAccountPoolEnabled,
|
|
33
31
|
} from "../oauth/anthropic-routing";
|
|
32
|
+
import { resolveAnthropicModelRoute, routeCandidates, type AnthropicRouteDecision } from "../oauth/anthropic-model-routes";
|
|
34
33
|
import {
|
|
35
34
|
captureOAuthAccountSelection,
|
|
36
35
|
commitOAuthAccountSelection,
|
|
@@ -57,9 +56,14 @@ export interface NativeOAuthBinding {
|
|
|
57
56
|
readonly snapshot: OAuthAccessSnapshot;
|
|
58
57
|
/** Anthropic account UUID from the same stored generation, distinct from snapshot.accountId. */
|
|
59
58
|
readonly providerAccountUuid?: string;
|
|
59
|
+
readonly routeDecision: AnthropicRouteDecision | null;
|
|
60
|
+
readonly sessionKey: string | null;
|
|
61
|
+
readonly model?: string;
|
|
62
|
+
readonly config: OcxConfig;
|
|
63
|
+
readonly manualSelectionGeneration: number;
|
|
60
64
|
}
|
|
61
65
|
|
|
62
|
-
/** The selection moved or became
|
|
66
|
+
/** The selection moved or became ineligible while resolving. Maps to a 409 retry. */
|
|
63
67
|
export class NativeOAuthSelectionChangedError extends Error {
|
|
64
68
|
constructor() {
|
|
65
69
|
super("OAuth account selection changed; retry the request");
|
|
@@ -67,57 +71,100 @@ export class NativeOAuthSelectionChangedError extends Error {
|
|
|
67
71
|
}
|
|
68
72
|
}
|
|
69
73
|
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
/** Invalid operator route configuration remains a request refusal, not an auth failure. */
|
|
75
|
+
export class NativeOAuthModelRouteError extends Error {}
|
|
76
|
+
|
|
77
|
+
function currentRoute(config: OcxConfig, model?: string): AnthropicRouteDecision | null {
|
|
78
|
+
const resolved = model ? resolveAnthropicModelRoute(config, model) : { decision: null };
|
|
79
|
+
if (resolved.error) throw new NativeOAuthModelRouteError(`Invalid Anthropic model routes: ${resolved.error}`);
|
|
80
|
+
return resolved.decision ? { ...resolved.decision, accounts: [...resolved.decision.accounts] } : null;
|
|
72
81
|
}
|
|
73
82
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
function routeIsCurrent(config: OcxConfig, model: string | undefined, decision: AnthropicRouteDecision | null): boolean {
|
|
84
|
+
return JSON.stringify(currentRoute(config, model)) === JSON.stringify(decision);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface NativeOAuthBindingOptions {
|
|
88
|
+
sessionKey?: string | null;
|
|
89
|
+
model?: string;
|
|
90
|
+
/** An account proposed by the shared pre-output refusal recovery policy. */
|
|
91
|
+
candidateAccountId?: string;
|
|
92
|
+
/** Selection captured before asynchronous refusal classification or throttle wait. */
|
|
93
|
+
expectedRecoverySelection?: Selection | null;
|
|
94
|
+
expectedRecoveryRouteDecision?: AnthropicRouteDecision | null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Resolve the shared pool selector and commit its exact credential generation for dispatch. */
|
|
98
|
+
export async function resolveNativeOAuthBinding(
|
|
99
|
+
config: OcxConfig,
|
|
100
|
+
options: NativeOAuthBindingOptions = {},
|
|
101
|
+
): Promise<NativeOAuthBinding> {
|
|
102
|
+
const sessionKey = options.sessionKey ?? null;
|
|
103
|
+
const model = options.model;
|
|
80
104
|
for (let attempt = 0; attempt < MAX_SELECTION_ATTEMPTS; attempt++) {
|
|
105
|
+
const routeDecision = currentRoute(config, model);
|
|
106
|
+
const manualSelectionGeneration = captureAnthropicManualSelectionGeneration();
|
|
81
107
|
const selection = captureOAuthAccountSelection(PROVIDER);
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
108
|
+
// Always ask admission first so pause/login/cooldown retains its typed local refusal.
|
|
109
|
+
const admittedId = await resolveAnthropicDispatchAccountId(config, sessionKey, routeDecision, model);
|
|
110
|
+
if (!routeIsCurrent(config, model, routeDecision)) continue;
|
|
111
|
+
const proposed = resolveAnthropicAccountForSession(sessionKey, config, Date.now(), routeDecision, model);
|
|
112
|
+
// A lost selection commit discards a recovery proposal: a newer manual/policy choice wins.
|
|
113
|
+
const recoverySelectionMatches = !!options.expectedRecoverySelection
|
|
114
|
+
&& selection?.accountId === options.expectedRecoverySelection.accountId
|
|
115
|
+
&& selection?.revision === options.expectedRecoverySelection.revision;
|
|
116
|
+
const recoveryRouteMatches = options.expectedRecoveryRouteDecision === undefined
|
|
117
|
+
|| JSON.stringify(options.expectedRecoveryRouteDecision) === JSON.stringify(routeDecision);
|
|
118
|
+
const accountId = attempt === 0 && options.candidateAccountId && recoverySelectionMatches && recoveryRouteMatches
|
|
119
|
+
? options.candidateAccountId : admittedId;
|
|
86
120
|
if (!selection) continue;
|
|
121
|
+
if (!routeCandidates(getEligibleAnthropicAccounts(Date.now(), model), routeDecision).includes(accountId)) {
|
|
122
|
+
throw new NativeOAuthSelectionChangedError();
|
|
123
|
+
}
|
|
87
124
|
const candidate = await getAnthropicPoolAccessSnapshot(accountId);
|
|
125
|
+
if (!routeIsCurrent(config, model, routeDecision)) continue;
|
|
88
126
|
const committed = await commitOAuthAccountSelection(PROVIDER, candidate.accountId, {
|
|
89
127
|
expectedSelection: selection,
|
|
90
128
|
expectedCredentialGeneration: candidate.generation,
|
|
91
129
|
requireUsableAccount: true,
|
|
92
130
|
});
|
|
93
131
|
if (committed) {
|
|
132
|
+
if (!routeIsCurrent(config, model, routeDecision)
|
|
133
|
+
|| manualSelectionGeneration !== captureAnthropicManualSelectionGeneration()) continue;
|
|
94
134
|
if (!commitAnthropicSelectionRouting(candidate.accountId, selection, committed, {
|
|
95
|
-
config,
|
|
96
|
-
|
|
135
|
+
config, sessionKey, model, routeDecision,
|
|
136
|
+
reason: accountId === proposed.accountId ? proposed.reason : undefined,
|
|
97
137
|
expectedCredentialGeneration: candidate.generation,
|
|
98
138
|
})) continue;
|
|
99
|
-
if (pooled(config)) break;
|
|
100
139
|
const row = getAccountCredentialWithStatus(PROVIDER, candidate.accountId);
|
|
101
140
|
if (!row || credentialGeneration(row.credential) !== candidate.generation) continue;
|
|
102
|
-
const binding = { selection: committed, snapshot: candidate, providerAccountUuid: row.credential.accountId };
|
|
141
|
+
const binding = { selection: committed, snapshot: candidate, providerAccountUuid: row.credential.accountId, routeDecision, sessionKey, model, config, manualSelectionGeneration };
|
|
103
142
|
if (nativeOAuthBindingIsCurrent(binding)) return binding;
|
|
104
143
|
}
|
|
105
|
-
// Re-read after a lost commit or a health change during credential resolution.
|
|
106
144
|
}
|
|
107
145
|
throw new NativeOAuthSelectionChangedError();
|
|
108
146
|
}
|
|
109
147
|
|
|
110
148
|
/**
|
|
111
|
-
* Whether a binding may still be sent:
|
|
112
|
-
*
|
|
149
|
+
* Whether a binding may still be sent: a live pooled session affinity or the same committed
|
|
150
|
+
* selection, plus the same usable credential generation. Checked before every physical send.
|
|
113
151
|
*/
|
|
114
152
|
export function nativeOAuthBindingIsCurrent(binding: NativeOAuthBinding): boolean {
|
|
153
|
+
if (binding.manualSelectionGeneration !== captureAnthropicManualSelectionGeneration()
|
|
154
|
+
|| !routeIsCurrent(binding.config, binding.model, binding.routeDecision)) return false;
|
|
115
155
|
const selected = captureOAuthAccountSelection(PROVIDER);
|
|
116
156
|
const row = getAccountCredentialWithStatus(PROVIDER, binding.snapshot.accountId);
|
|
117
|
-
|
|
118
|
-
|
|
157
|
+
const sessionRoute = isAnthropicAccountPoolEnabled(binding.config) && binding.sessionKey
|
|
158
|
+
? resolveAnthropicAccountForSession(binding.sessionKey, binding.config, Date.now(), binding.routeDecision, binding.model) : null;
|
|
159
|
+
// Another conversation may move the automatic active pointer without revoking this
|
|
160
|
+
// session's affinity. Manual selection clears affinity and precedes it in the selector.
|
|
161
|
+
const selectionCurrent = sessionRoute?.reason === "affinity"
|
|
162
|
+
&& sessionRoute.accountId === binding.snapshot.accountId
|
|
163
|
+
|| (selected?.accountId === binding.selection.accountId && selected?.revision === binding.selection.revision);
|
|
164
|
+
return selectionCurrent
|
|
119
165
|
&& !!row && !row.paused && !row.needsReauth && row.credential.expires > Date.now()
|
|
120
166
|
&& !getAnthropicAccountHealthSnapshot(binding.snapshot.accountId)
|
|
167
|
+
&& routeCandidates(getEligibleAnthropicAccounts(Date.now(), binding.model), binding.routeDecision).includes(binding.snapshot.accountId)
|
|
121
168
|
&& credentialGeneration(row.credential) === binding.snapshot.generation
|
|
122
169
|
&& row.credential.accountId === binding.providerAccountUuid;
|
|
123
170
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* A Messages request whose settled route is a proxy-managed key on the `anthropic` adapter is
|
|
5
5
|
* sent as Messages: the source body (after the ingress's managed-client steps) cut to a field
|
|
6
6
|
* allowlist, the wire model, and the provider's own key. With `managedMessagesNativeOAuth` on
|
|
7
|
-
* (PF-10), an
|
|
7
|
+
* (PF-10), an Anthropic OAuth account is sent the same way with the access token the
|
|
8
8
|
* existing OAuth selection resolves at dispatch (`messages-native-oauth.ts`). Modelled on the native Chat lane and
|
|
9
9
|
* built on the same shared pieces — attempt row, finish-once final log, spend tracker, proactive
|
|
10
10
|
* key selection, key failover and 429 replay, connection policy — so nothing the Responses
|
|
@@ -17,6 +17,13 @@
|
|
|
17
17
|
*
|
|
18
18
|
* Loaded lazily by `claude-messages.ts` only when the switch is on and a route is eligible.
|
|
19
19
|
*/
|
|
20
|
+
import { resolveAnthropicModelRoute } from "../oauth/anthropic-model-routes";
|
|
21
|
+
import { bindAnthropicRefusalCredential, rotateAnthropicAccountOnResponse } from "../oauth/anthropic-account-refusal";
|
|
22
|
+
import { claimAnthropicFamilyRevalidation } from "../oauth/anthropic-model-quota";
|
|
23
|
+
import { captureConfigGeneration } from "../lib/state-store-sweeper";
|
|
24
|
+
import { recordAnthropicAccountQuotaFromHeaders } from "../providers/quota";
|
|
25
|
+
import { credentialGeneration, getAccountCredentialWithStatus } from "../oauth/store";
|
|
26
|
+
import { ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST } from "../oauth/anthropic-routing";
|
|
20
27
|
import { enforceAnthropicImageLimits } from "../adapters/anthropic-image-guard";
|
|
21
28
|
import { normalizeAnthropicImages } from "../adapters/anthropic-image-normalize";
|
|
22
29
|
import { formatAnthropicErrorBody } from "../adapters/anthropic";
|
|
@@ -84,6 +91,7 @@ import { nativeMessagesDeclineReason, type NativeMessagesSelector } from "./mess
|
|
|
84
91
|
import {
|
|
85
92
|
nativeOAuthBindingIsCurrent,
|
|
86
93
|
NativeOAuthSelectionChangedError,
|
|
94
|
+
NativeOAuthModelRouteError,
|
|
87
95
|
resolveNativeOAuthBinding,
|
|
88
96
|
restoreOAuthToolNamesInMessage,
|
|
89
97
|
restoreOAuthToolNamesInSse,
|
|
@@ -150,6 +158,7 @@ export interface HandleNativeMessagesOptions {
|
|
|
150
158
|
*/
|
|
151
159
|
callerAnthropicBeta?: string | null;
|
|
152
160
|
clientIdentity?: AnthropicClientIdentity;
|
|
161
|
+
sessionKey?: string | null;
|
|
153
162
|
}
|
|
154
163
|
|
|
155
164
|
type FinishLog = (status: number, message?: string, meta?: FinalRequestLogMeta) => void;
|
|
@@ -373,7 +382,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
373
382
|
const oauthProvider = (binding: NativeOAuthBinding): OcxProviderConfig => ({ ...route.provider, apiKey: binding.snapshot.accessToken });
|
|
374
383
|
if (route.provider.authMode === "oauth") {
|
|
375
384
|
try {
|
|
376
|
-
oauthBinding = await resolveNativeOAuthBinding(config);
|
|
385
|
+
oauthBinding = await resolveNativeOAuthBinding(config, { sessionKey: options.sessionKey, model: route.modelId });
|
|
377
386
|
} catch (error) {
|
|
378
387
|
cleanupAbort();
|
|
379
388
|
upstream.abort();
|
|
@@ -381,6 +390,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
381
390
|
const localRefusal = localOAuthFailure(error);
|
|
382
391
|
if (localRefusal) return localRefusal;
|
|
383
392
|
if (error instanceof NativeOAuthSelectionChangedError) return fail(409, error.message, "api_error");
|
|
393
|
+
if (error instanceof NativeOAuthModelRouteError) return fail(400, error.message, "invalid_request_error");
|
|
384
394
|
return fail(401, publicOAuthAuthenticationErrorMessage(error), "authentication_error");
|
|
385
395
|
}
|
|
386
396
|
} else {
|
|
@@ -455,7 +465,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
455
465
|
const transientSendAvailable = (): boolean => remainingTransientSends() > 0;
|
|
456
466
|
const selector: NativeMessagesSelector = options.selector ?? {};
|
|
457
467
|
|
|
458
|
-
const send = async (recovery?: "rate-limit-429" | "key-429" | "key-401"): Promise<Response> => {
|
|
468
|
+
const send = async (recovery?: "rate-limit-429" | "oauth-account-403" | "key-429" | "key-401"): Promise<Response> => {
|
|
459
469
|
const remaining = remainingTransientSends();
|
|
460
470
|
if (requestTransientPolicy && remaining <= 0) {
|
|
461
471
|
throw new Error("native Messages transient send budget exhausted before recovery dispatch");
|
|
@@ -479,10 +489,10 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
479
489
|
for (let attempt = 0; !nativeOAuthBindingIsCurrent(oauthBinding); attempt++) {
|
|
480
490
|
if (attempt >= 3) throw new NativeOAuthSelectionChangedError();
|
|
481
491
|
try {
|
|
482
|
-
oauthBinding = await resolveNativeOAuthBinding(config);
|
|
492
|
+
oauthBinding = await resolveNativeOAuthBinding(config, { sessionKey: options.sessionKey, model: route.modelId });
|
|
483
493
|
} catch (error) {
|
|
484
494
|
if (error instanceof OAuthAccountPausedError || error instanceof OAuthLoginRequiredError
|
|
485
|
-
|| error instanceof AnthropicAccountCooldownError) throw error;
|
|
495
|
+
|| error instanceof AnthropicAccountCooldownError || error instanceof NativeOAuthModelRouteError) throw error;
|
|
486
496
|
throw new NativeOAuthSelectionChangedError();
|
|
487
497
|
}
|
|
488
498
|
rebuildFor(oauthProvider(oauthBinding));
|
|
@@ -500,15 +510,34 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
500
510
|
const headers = new Headers(wire.headers);
|
|
501
511
|
const encoding = new Headers(init.headers).get("accept-encoding");
|
|
502
512
|
if (!headers.has("accept-encoding") && encoding) headers.set("accept-encoding", encoding);
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
513
|
+
const snapshot = oauthBinding?.snapshot;
|
|
514
|
+
const providerAccountUuid = oauthBinding?.providerAccountUuid;
|
|
515
|
+
const writerGeneration = snapshot ? captureConfigGeneration() : 0;
|
|
516
|
+
const ownsBearer = snapshot && headers.get("authorization") === `Bearer ${snapshot.accessToken}` && !headers.has("x-api-key");
|
|
517
|
+
const releaseFamily = snapshot ? claimAnthropicFamilyRevalidation(snapshot.accountId, route.modelId) : () => {};
|
|
518
|
+
if (!releaseFamily) throw new AnthropicAccountCooldownError(1);
|
|
519
|
+
let dispatched: Response;
|
|
520
|
+
try {
|
|
521
|
+
if (init.signal?.aborted) throw init.signal.reason;
|
|
522
|
+
if (!spendTracker.charge()) throw new NativeMessagesSpendRefusal();
|
|
523
|
+
noteProviderAttemptSend(logCtx, route.providerName, activeProvider, logCtx.usageLogInputTokens, transportRecovery ?? recovery);
|
|
524
|
+
dispatched = await sendWithConnectionPolicy(
|
|
525
|
+
(activeProvider as OcxProviderTransport).fetch ?? execute,
|
|
526
|
+
wire.url,
|
|
527
|
+
applyUpstreamRecoveryInit({ ...init, method: "POST", headers, body: wire.body }, transportRecovery),
|
|
528
|
+
{ providerName: route.providerName, provider: activeProvider },
|
|
529
|
+
);
|
|
530
|
+
} finally { releaseFamily(); }
|
|
531
|
+
if (ownsBearer && snapshot) {
|
|
532
|
+
try {
|
|
533
|
+
const current = getAccountCredentialWithStatus("anthropic", snapshot.accountId);
|
|
534
|
+
if (current && !current.needsReauth && credentialGeneration(current.credential) === snapshot.generation
|
|
535
|
+
&& current.credential.accountId === providerAccountUuid) {
|
|
536
|
+
bindAnthropicRefusalCredential(dispatched, snapshot, providerAccountUuid);
|
|
537
|
+
recordAnthropicAccountQuotaFromHeaders(snapshot.accountId, dispatched.headers, writerGeneration, dispatched.status, route.modelId);
|
|
538
|
+
}
|
|
539
|
+
} catch { /* Passive observation must not fail the response. */ }
|
|
540
|
+
}
|
|
512
541
|
if (!dispatched.ok) await recordKeyAttemptFailure(logCtx, dispatched, init.signal ?? upstream.signal);
|
|
513
542
|
return dispatched;
|
|
514
543
|
},
|
|
@@ -544,7 +573,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
544
573
|
rebuildFor(rotated);
|
|
545
574
|
response = await send("key-401");
|
|
546
575
|
}
|
|
547
|
-
const retryPolicy = rateLimitRetryPolicyFor(activeProvider);
|
|
576
|
+
const retryPolicy = oauthBinding ? undefined : rateLimitRetryPolicyFor(activeProvider);
|
|
548
577
|
let retries = 0;
|
|
549
578
|
// A refusal this proxy synthesized for a reset replay is not a provider rate limit.
|
|
550
579
|
while (
|
|
@@ -577,6 +606,37 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
577
606
|
rebuildFor(rotated);
|
|
578
607
|
response = await send("key-429");
|
|
579
608
|
}
|
|
609
|
+
const oauthRetryKey = {};
|
|
610
|
+
const triedAccountIds = new Set<string>();
|
|
611
|
+
let oauthFailovers = 0;
|
|
612
|
+
while (oauthBinding && (response.status === 429 || response.status === 403)) {
|
|
613
|
+
const sendingBinding = oauthBinding;
|
|
614
|
+
const expectedRecoverySelection = sendingBinding.selection;
|
|
615
|
+
triedAccountIds.add(sendingBinding.snapshot.accountId);
|
|
616
|
+
const nextAccountId = await rotateAnthropicAccountOnResponse(response, {
|
|
617
|
+
config, accountId: sendingBinding.snapshot.accountId, model: route.modelId,
|
|
618
|
+
sessionKey: sendingBinding.sessionKey, decision: sendingBinding.routeDecision,
|
|
619
|
+
excludedAccountIds: triedAccountIds,
|
|
620
|
+
currentDecision: () => {
|
|
621
|
+
const resolved = resolveAnthropicModelRoute(config, route.modelId);
|
|
622
|
+
if (resolved.error) throw new NativeOAuthModelRouteError(`Invalid Anthropic model routes: ${resolved.error}`);
|
|
623
|
+
return resolved.decision;
|
|
624
|
+
},
|
|
625
|
+
signal: upstream.signal, requestKey: oauthRetryKey,
|
|
626
|
+
canRetry: transientSendAvailable() && oauthFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
|
|
627
|
+
});
|
|
628
|
+
if (!nextAccountId) break;
|
|
629
|
+
const recovery = response.status === 403 ? "oauth-account-403" : "rate-limit-429";
|
|
630
|
+
discard(response);
|
|
631
|
+
oauthBinding = await resolveNativeOAuthBinding(config, {
|
|
632
|
+
sessionKey: options.sessionKey, model: route.modelId, candidateAccountId: nextAccountId,
|
|
633
|
+
expectedRecoverySelection, expectedRecoveryRouteDecision: sendingBinding.routeDecision,
|
|
634
|
+
});
|
|
635
|
+
oauthFailovers++;
|
|
636
|
+
rebuildFor(oauthProvider(oauthBinding));
|
|
637
|
+
logCtx.provider = formatAnthropicProviderForLog(route.providerName, oauthBinding.snapshot.accountId, config);
|
|
638
|
+
response = await send(recovery);
|
|
639
|
+
}
|
|
580
640
|
} catch (error) {
|
|
581
641
|
releaseRetainedRequest();
|
|
582
642
|
cleanupAbort();
|
|
@@ -591,6 +651,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
591
651
|
const localRefusal = localOAuthFailure(sendError);
|
|
592
652
|
if (localRefusal) return localRefusal;
|
|
593
653
|
if (sendError instanceof NativeOAuthSelectionChangedError) return fail(409, sendError.message, "api_error");
|
|
654
|
+
if (sendError instanceof NativeOAuthModelRouteError) return fail(400, sendError.message, "invalid_request_error");
|
|
594
655
|
if (sendError instanceof NativeOpaqueStateRefusal) {
|
|
595
656
|
logCtx.errorCode = "unsupported_feature";
|
|
596
657
|
return fail(400, sendError.message, "invalid_request_error", "unsupported_feature");
|
|
@@ -791,6 +852,7 @@ export function nativeMessagesCountBody(
|
|
|
791
852
|
cc: OcxConfig["claudeCode"],
|
|
792
853
|
body: Rec,
|
|
793
854
|
rows: Pick<NativeMessagesSelector, "effortRow" | "fastRow">,
|
|
855
|
+
clientIdentity?: AnthropicClientIdentity,
|
|
794
856
|
): Rec | undefined {
|
|
795
857
|
if (typeof body.model !== "string") return undefined;
|
|
796
858
|
try {
|
|
@@ -812,7 +874,7 @@ export function nativeMessagesCountBody(
|
|
|
812
874
|
};
|
|
813
875
|
if (nativeMessagesDeclineReason(route, body, config, selector) !== undefined) return undefined;
|
|
814
876
|
// The body without a credential: counting never resolves or refreshes an OAuth account.
|
|
815
|
-
return anthropicMessagesNativeWireBody(route.provider, route.modelId, body).wireBody;
|
|
877
|
+
return anthropicMessagesNativeWireBody(route.provider, route.modelId, body, { clientIdentity }).wireBody;
|
|
816
878
|
} catch {
|
|
817
879
|
return undefined;
|
|
818
880
|
}
|
package/src/server/relay.ts
CHANGED
|
@@ -30,8 +30,9 @@ import {
|
|
|
30
30
|
joinSseFrameBytes,
|
|
31
31
|
MAX_CLIENT_SSE_FRAME_BYTES,
|
|
32
32
|
SseFrameCountLimitError,
|
|
33
|
+
sseDelimiterLengthAt,
|
|
33
34
|
} from "./sse-frame-buffer";
|
|
34
|
-
import { replaceSseDataPayload, sseDataPayload } from "./sse-payload-rewrite";
|
|
35
|
+
import { replaceSseDataPayload, splitSseBlock, sseDataPayload } from "./sse-payload-rewrite";
|
|
35
36
|
import { createBoundedResponseLogBody } from "./response-log-body";
|
|
36
37
|
import { clientWireLogOf } from "./inference/client-wire";
|
|
37
38
|
import { recordClientWireRequestLog } from "./inference/client-wire-log";
|
|
@@ -355,10 +356,11 @@ export function createSseTerminalOutputBoundary(
|
|
|
355
356
|
frames: ReturnType<BoundedSseFrameBuffer["feed"]>,
|
|
356
357
|
): Uint8Array => {
|
|
357
358
|
if (disposed || terminal || frames.length === 0) return EMPTY_BYTES;
|
|
358
|
-
|
|
359
|
+
const completedFrames = frames.reduce((count, frame) => count + (frame.continuation ? 0 : 1), 0);
|
|
360
|
+
if (maxFrames !== undefined && completedFrames > maxFrames - framesSeen) {
|
|
359
361
|
throw new SseFrameCountLimitError(maxFrames);
|
|
360
362
|
}
|
|
361
|
-
framesSeen +=
|
|
363
|
+
framesSeen += completedFrames;
|
|
362
364
|
const output: Uint8Array[] = [];
|
|
363
365
|
const appendOutput = (...parts: Uint8Array[]): void => {
|
|
364
366
|
for (const part of parts) {
|
|
@@ -596,17 +598,7 @@ export function relaySseWithFailedTail(
|
|
|
596
598
|
});
|
|
597
599
|
}
|
|
598
600
|
|
|
599
|
-
export
|
|
600
|
-
const match = buffer.match(/\r?\n\r?\n/);
|
|
601
|
-
if (!match || match.index === undefined) return null;
|
|
602
|
-
return {
|
|
603
|
-
block: buffer.slice(0, match.index),
|
|
604
|
-
delimiter: match[0],
|
|
605
|
-
rest: buffer.slice(match.index + match[0].length),
|
|
606
|
-
};
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
export { sseDataPayload } from "./sse-payload-rewrite";
|
|
601
|
+
export { nextSseBlock, sseDataPayload } from "./sse-payload-rewrite";
|
|
610
602
|
|
|
611
603
|
type JsonRecord = Record<string, unknown>;
|
|
612
604
|
|
|
@@ -698,9 +690,8 @@ function stripCodexSafetyBufferingField(block: string, parsed: unknown): string
|
|
|
698
690
|
}
|
|
699
691
|
|
|
700
692
|
function rewritePolicyTerminalBlock(block: string, payload: string): string {
|
|
701
|
-
const newline = block.includes("\r\n") ? "\r\n" : "\n";
|
|
702
693
|
const rewritten = replaceSseDataPayload(block, payload);
|
|
703
|
-
const lines = rewritten
|
|
694
|
+
const { newline, lines } = splitSseBlock(rewritten);
|
|
704
695
|
let eventRewritten = false;
|
|
705
696
|
const withEvent = lines.map(line => {
|
|
706
697
|
if (!eventRewritten && line.startsWith("event:")) {
|
|
@@ -1156,31 +1147,6 @@ function outputBearingSseEvent(event: { type?: unknown; output_index?: unknown }
|
|
|
1156
1147
|
.test(event.type);
|
|
1157
1148
|
}
|
|
1158
1149
|
|
|
1159
|
-
function delimiterLengthAt(
|
|
1160
|
-
index: number,
|
|
1161
|
-
length: number,
|
|
1162
|
-
byteAt: (index: number) => number,
|
|
1163
|
-
): number | 0 | undefined {
|
|
1164
|
-
const first = byteAt(index);
|
|
1165
|
-
if (first === 10) {
|
|
1166
|
-
if (index + 1 >= length) return undefined;
|
|
1167
|
-
const second = byteAt(index + 1);
|
|
1168
|
-
if (second === 10) return 2;
|
|
1169
|
-
if (second !== 13) return 0;
|
|
1170
|
-
if (index + 2 >= length) return undefined;
|
|
1171
|
-
return byteAt(index + 2) === 10 ? 3 : 0;
|
|
1172
|
-
}
|
|
1173
|
-
if (first !== 13) return 0;
|
|
1174
|
-
if (index + 1 >= length) return undefined;
|
|
1175
|
-
if (byteAt(index + 1) !== 10) return 0;
|
|
1176
|
-
if (index + 2 >= length) return undefined;
|
|
1177
|
-
const third = byteAt(index + 2);
|
|
1178
|
-
if (third === 10) return 3;
|
|
1179
|
-
if (third !== 13) return 0;
|
|
1180
|
-
if (index + 3 >= length) return undefined;
|
|
1181
|
-
return byteAt(index + 3) === 10 ? 4 : 0;
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
1150
|
/**
|
|
1185
1151
|
* Per-chunk SSE inspection state machine shared by consumeForInspection,
|
|
1186
1152
|
* consumeForResponseLogMetadata, and the eager bounded relay (relay-eager.ts).
|
|
@@ -1202,6 +1168,7 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
|
|
|
1202
1168
|
let sawTerminal = false;
|
|
1203
1169
|
let disposed = false;
|
|
1204
1170
|
let delimiterTail: Uint8Array = EMPTY_BYTES;
|
|
1171
|
+
let pendingLineFeed = false;
|
|
1205
1172
|
let candidate: Uint8Array = EMPTY_BYTES;
|
|
1206
1173
|
let candidateBytes = 0;
|
|
1207
1174
|
let discardingOversizedFrame = false;
|
|
@@ -1228,6 +1195,7 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
|
|
|
1228
1195
|
|
|
1229
1196
|
const clearFrameState = (): void => {
|
|
1230
1197
|
delimiterTail = EMPTY_BYTES;
|
|
1198
|
+
pendingLineFeed = false;
|
|
1231
1199
|
candidate = EMPTY_BYTES;
|
|
1232
1200
|
candidateBytes = 0;
|
|
1233
1201
|
discardingOversizedFrame = false;
|
|
@@ -1504,15 +1472,17 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
|
|
|
1504
1472
|
retainCandidateSlice(chunk.subarray(Math.max(0, start - tailLength), end - tailLength));
|
|
1505
1473
|
}
|
|
1506
1474
|
};
|
|
1507
|
-
let index = 0;
|
|
1508
|
-
|
|
1475
|
+
let index = pendingLineFeed && byteAt(0) === 10 ? 1 : 0;
|
|
1476
|
+
if (totalLength > 0) pendingLineFeed = false;
|
|
1477
|
+
let retainedThrough = index;
|
|
1509
1478
|
while (index < totalLength) {
|
|
1510
|
-
const delimiterLength =
|
|
1479
|
+
const delimiterLength = sseDelimiterLengthAt(index, totalLength, byteAt);
|
|
1511
1480
|
if (delimiterLength === undefined) break;
|
|
1512
1481
|
if (delimiterLength > 0) {
|
|
1513
1482
|
retainRange(retainedThrough, index);
|
|
1514
1483
|
completeCandidate();
|
|
1515
1484
|
index += delimiterLength;
|
|
1485
|
+
pendingLineFeed = index === totalLength && byteAt(index - 1) === 13;
|
|
1516
1486
|
retainedThrough = index;
|
|
1517
1487
|
continue;
|
|
1518
1488
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { RequestLogContext } from "./request-log";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generation window for the decode-rate estimate (#4038, #6309).
|
|
5
|
+
*
|
|
6
|
+
* `firstOutputMs` marks the first VISIBLE output delta. For a reasoning model that is the end of
|
|
7
|
+
* the reasoning phase, yet `outputTokens` includes the reasoning tokens, so
|
|
8
|
+
* `outputTokens / (durationMs - firstOutputMs)` divides the whole output by only part of the time
|
|
9
|
+
* that produced it. The window recorded here starts when the model starts producing ANY output
|
|
10
|
+
* item (reasoning included) and ends at the last output delta, so trailing stream bookkeeping
|
|
11
|
+
* after the final token does not count either.
|
|
12
|
+
*
|
|
13
|
+
* Event vocabulary: Anthropic `content_block_start` / `content_block_delta`, and Responses
|
|
14
|
+
* `response.output_item.added` / `response.*.delta`. Anything else leaves the window untouched.
|
|
15
|
+
*/
|
|
16
|
+
export function recordGenerationEvent(logCtx: RequestLogContext, type: unknown, now = Date.now()): void {
|
|
17
|
+
if (typeof type !== "string" || !Number.isFinite(now)) return;
|
|
18
|
+
if (type === "content_block_start" || type === "response.output_item.added") {
|
|
19
|
+
logCtx.generationStartedAt ??= now;
|
|
20
|
+
} else if (type === "content_block_delta" || (type.startsWith("response.") && type.endsWith(".delta"))) {
|
|
21
|
+
logCtx.generationStartedAt ??= now;
|
|
22
|
+
logCtx.lastOutputAt = now;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Request-relative `genStartMs` / `lastOutputMs`, present only when both ends were observed. */
|
|
27
|
+
export function generationWindowFields(
|
|
28
|
+
logCtx: Pick<RequestLogContext, "generationStartedAt" | "lastOutputAt">,
|
|
29
|
+
requestStartedAt: number,
|
|
30
|
+
): { genStartMs?: number; lastOutputMs?: number } {
|
|
31
|
+
const startedAt = logCtx.generationStartedAt;
|
|
32
|
+
const lastAt = logCtx.lastOutputAt;
|
|
33
|
+
if (startedAt === undefined || lastAt === undefined || !Number.isFinite(requestStartedAt)) return {};
|
|
34
|
+
return {
|
|
35
|
+
genStartMs: Math.max(0, startedAt - requestStartedAt),
|
|
36
|
+
lastOutputMs: Math.max(0, lastAt - requestStartedAt),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -85,6 +85,7 @@ import {
|
|
|
85
85
|
normalizePersistedJevDecision,
|
|
86
86
|
type PersistedJevDecisionV1,
|
|
87
87
|
} from "../usage/jev-stats";
|
|
88
|
+
import { generationWindowFields, recordGenerationEvent } from "./request-log-generation-window";
|
|
88
89
|
import type { RequestMetricsRecorder } from "./request-metrics";
|
|
89
90
|
import type {
|
|
90
91
|
CacheDiagnosticDraft,
|
|
@@ -136,6 +137,9 @@ export interface RequestLogContext {
|
|
|
136
137
|
affinityMoveReasons?: CodexAffinityReason[];
|
|
137
138
|
/** TTFT: ms from request start to the first non-empty model output delta (WP4, devlog 040). */
|
|
138
139
|
firstOutputMs?: number;
|
|
140
|
+
/** Epoch ms of the first output item and of the last output delta; see request-log-generation-window.ts. */
|
|
141
|
+
generationStartedAt?: number;
|
|
142
|
+
lastOutputAt?: number;
|
|
139
143
|
/** Best-effort chat/session correlation for Logs grouping (#330). Opaque; omit when unknown. */
|
|
140
144
|
conversationId?: string;
|
|
141
145
|
surface?: "claude" | "claude-desktop" | "grok";
|
|
@@ -282,6 +286,9 @@ export interface RequestLogEntry {
|
|
|
282
286
|
provider: string;
|
|
283
287
|
/** TTFT: ms from request start to the first non-empty model output delta; unset for non-streaming/tool-only. */
|
|
284
288
|
firstOutputMs?: number;
|
|
289
|
+
/** Request-relative generation window (#6309): first output item of any kind, then the last output delta. */
|
|
290
|
+
genStartMs?: number;
|
|
291
|
+
lastOutputMs?: number;
|
|
285
292
|
surface?: "claude" | "claude-desktop" | "grok";
|
|
286
293
|
/**
|
|
287
294
|
* Set when the proxy answered this turn locally and sent nothing upstream. Without it a zero-send
|
|
@@ -453,6 +460,8 @@ export function requestLogEntryFromPersistedUsage(entry: PersistedUsageEntry): R
|
|
|
453
460
|
model: entry.model,
|
|
454
461
|
provider: entry.provider,
|
|
455
462
|
...(entry.firstOutputMs !== undefined ? { firstOutputMs: entry.firstOutputMs } : {}),
|
|
463
|
+
...(entry.genStartMs !== undefined ? { genStartMs: entry.genStartMs } : {}),
|
|
464
|
+
...(entry.lastOutputMs !== undefined ? { lastOutputMs: entry.lastOutputMs } : {}),
|
|
456
465
|
...(isKnownUsageSurface(entry.surface) ? { surface: entry.surface } : {}),
|
|
457
466
|
...(entry.conversationId ? { conversationId: entry.conversationId } : {}),
|
|
458
467
|
...(isCodexUsageAccountLogLabel(entry.accountLogLabel)
|
|
@@ -668,6 +677,8 @@ export function addRequestLog(entry: RequestLogEntry) {
|
|
|
668
677
|
status: entry.status,
|
|
669
678
|
durationMs: entry.durationMs,
|
|
670
679
|
...(entry.firstOutputMs !== undefined ? { firstOutputMs: entry.firstOutputMs } : {}),
|
|
680
|
+
...(entry.genStartMs !== undefined ? { genStartMs: entry.genStartMs } : {}),
|
|
681
|
+
...(entry.lastOutputMs !== undefined ? { lastOutputMs: entry.lastOutputMs } : {}),
|
|
671
682
|
usageStatus: entry.usageStatus,
|
|
672
683
|
...(entry.usage ? { usage: entry.usage } : {}),
|
|
673
684
|
...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}),
|
|
@@ -1056,6 +1067,7 @@ export function inspectResponseLogSsePayloadParsed(
|
|
|
1056
1067
|
if (!payload || payload.trim() === "[DONE]") return;
|
|
1057
1068
|
const debugEnabled = isUsageDebugEnabled();
|
|
1058
1069
|
const sseAlreadyMarked = logCtx.usageDebugBodyKind === "sse";
|
|
1070
|
+
if (parsed !== undefined) recordGenerationEvent(logCtx, (parsed as { type?: unknown } | null)?.type);
|
|
1059
1071
|
if (parsed !== undefined) applyResponseLogMetadata(logCtx, parsed);
|
|
1060
1072
|
else logCtx.activeTierMetadata?.markResponseUnparseable();
|
|
1061
1073
|
captureUpstreamErrorParsed(logCtx, payload, parsed);
|
|
@@ -1571,6 +1583,7 @@ export function addFinalRequestLog(
|
|
|
1571
1583
|
status: effectiveStatus,
|
|
1572
1584
|
durationMs,
|
|
1573
1585
|
...(logCtx.firstOutputMs !== undefined ? { firstOutputMs: logCtx.firstOutputMs } : {}),
|
|
1586
|
+
...generationWindowFields(logCtx, start),
|
|
1574
1587
|
...(errorCode ? { errorCode } : {}),
|
|
1575
1588
|
...(meta?.terminalStatus ? { terminalStatus: meta.terminalStatus } : {}),
|
|
1576
1589
|
...(closeReason ? { closeReason } : {}),
|