@bitkyc08/opencodex 2.80.0 → 2.81.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/README.md +3 -3
- package/gui/dist/assets/App-7yhoys8a.js +54 -0
- package/gui/dist/assets/Tray-EwAOsP0S.js +1 -0
- package/gui/dist/assets/index-RsoaAxXY.js +94 -0
- package/gui/dist/assets/quota-summary-D2lN8tDm.js +1 -0
- package/gui/dist/index.html +1 -1
- package/gui/dist/provider-icons/claude-green.svg +1 -0
- package/package.json +3 -2
- package/src/adapters/anthropic-model-contract.ts +8 -2
- package/src/adapters/anthropic.ts +7 -2
- package/src/adapters/codebuddy/mcp-server.ts +2 -25
- package/src/adapters/codebuddy/tool-bridge.ts +1 -7
- package/src/adapters/coding-agent/mcp-server.ts +33 -0
- package/src/adapters/coding-agent/protocol.ts +122 -17
- package/src/adapters/coding-agent/tool-catalog.ts +75 -0
- package/src/adapters/coding-agent/turn.ts +101 -44
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +2 -0
- package/src/adapters/devin/live-models.ts +2 -0
- package/src/adapters/kiro-retry.ts +9 -4
- package/src/adapters/openai-chat/messages.ts +7 -6
- package/src/adapters/physical-send.ts +8 -5
- package/src/adapters/qoder/adapter.ts +41 -6
- package/src/adapters/qoder/scaffold-guard.ts +2 -3
- package/src/adapters/zed.ts +16 -1
- package/src/claude/inbound-cache-stabilize.ts +4 -9
- package/src/claude/inbound.ts +1 -0
- package/src/claude/intercept/model-bindings.ts +1 -0
- package/src/claude/intercept/picker-listener.ts +71 -4
- package/src/cli/account-anthropic-threshold.ts +14 -5
- package/src/cli/account-api.ts +3 -1
- package/src/cli/account-extended.ts +13 -9
- package/src/cli/account-policy-dto.ts +1 -1
- package/src/cli/account-policy.ts +13 -4
- package/src/cli/account.ts +3 -3
- package/src/cli/capabilities-accounts.ts +2 -2
- package/src/cli/capabilities-agents-routing.ts +24 -0
- package/src/cli/capabilities-integrations.ts +16 -0
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/dispatch.ts +10 -1
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +11 -5
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/integration-preview.ts +8 -1
- package/src/cli/integrations.ts +32 -3
- package/src/cli/message-args.ts +34 -0
- package/src/cli/message-command.ts +43 -0
- package/src/cli/message-runtime.ts +6 -0
- package/src/cli/observe.ts +3 -1
- package/src/cli/provider.ts +29 -6
- package/src/cli/registry.ts +24 -2
- package/src/clients/config-export/commandcode.ts +131 -0
- package/src/clients/config-export/contracts.ts +12 -0
- package/src/clients/config-export.ts +64 -1
- package/src/codex/auth-api/account-list.ts +11 -3
- package/src/codex/auth-api/main-account-probe.ts +25 -8
- package/src/codex/auth-api/pool-mode-gate.ts +33 -9
- package/src/codex/auth-api/pool-quota-probe.ts +15 -1
- package/src/codex/auth-api/routes.ts +1 -0
- package/src/codex/catalog/auto-review.ts +13 -6
- package/src/codex/catalog/build-entries.ts +15 -7
- package/src/codex/catalog/control-plane.ts +45 -0
- package/src/codex/catalog/effort.ts +11 -1
- package/src/codex/catalog/gather-capture.ts +5 -3
- package/src/codex/catalog/parsing.ts +2 -1
- package/src/codex/catalog/provider-models.ts +15 -9
- package/src/codex/control-plane-models.ts +6 -0
- package/src/combos/jev-decision-contract.ts +25 -2
- package/src/combos/jev.ts +3 -2
- package/src/combos/types.ts +1 -1
- package/src/config/diagnostics.ts +27 -16
- package/src/config/load-degrade.ts +34 -0
- package/src/config/schema/anthropic-account-pool.ts +143 -0
- package/src/config/schema/config-schema.ts +7 -1
- package/src/config/schema/leaf-validators.ts +4 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +320 -184
- package/src/generated/model-metadata.ts +5 -4
- package/src/images/loop.ts +2 -1
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +7 -0
- package/src/integrations/registry.ts +8 -0
- package/src/integrations/state.ts +9 -2
- package/src/integrations/writer.ts +8 -2
- package/src/lib/lab-live-route-production.ts +9 -1
- package/src/lib/request-execution-budget.ts +236 -58
- package/src/lib/spend-pool-alias-validation.ts +79 -0
- package/src/lib/spend-pool-continuity.ts +34 -0
- package/src/lib/spend-reservation-ledger.ts +412 -50
- package/src/lib/state-store-registrations.ts +20 -3
- package/src/lib/upstream-retry.ts +17 -5
- package/src/lib/workflow-budget.ts +53 -11
- package/src/messaging/budget.ts +45 -0
- package/src/messaging/discovery.ts +57 -0
- package/src/messaging/envelope.ts +49 -0
- package/src/messaging/input.ts +37 -0
- package/src/messaging/rpc.ts +196 -0
- package/src/messaging/send.ts +58 -0
- package/src/messaging/socket.ts +72 -0
- package/src/messaging/types.ts +29 -0
- package/src/oauth/anthropic-account-refusal.ts +51 -17
- package/src/oauth/anthropic-account-threshold.ts +8 -2
- package/src/oauth/anthropic-continuity.ts +15 -1
- package/src/oauth/anthropic-model-quota.ts +120 -48
- package/src/oauth/anthropic-model-routes.ts +11 -3
- package/src/oauth/anthropic-oauth-definitions.ts +98 -0
- package/src/oauth/anthropic-pool-config.ts +30 -0
- package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
- package/src/oauth/anthropic-routing.ts +1202 -866
- package/src/oauth/anthropic-send-ownership.ts +41 -0
- package/src/oauth/anthropic.ts +4 -1
- package/src/oauth/generic-account-failover.ts +3 -1
- package/src/oauth/health.ts +8 -6
- package/src/oauth/index.ts +90 -173
- package/src/oauth/kiro.ts +22 -9
- package/src/oauth/login-cli.ts +2 -0
- package/src/oauth/model-discovery-auth.ts +43 -0
- package/src/oauth/pool-kernel.ts +11 -5
- package/src/oauth/pool-settings-capability.ts +5 -3
- package/src/oauth/store-anthropic-instance.ts +87 -0
- package/src/oauth/store.ts +67 -9
- package/src/oauth/token-guardian.ts +2 -0
- package/src/oauth/types.ts +2 -0
- package/src/protocols/plan-snapshot.ts +5 -0
- package/src/protocols/settings.ts +20 -5
- package/src/providers/anthropic-instance-id.ts +47 -0
- package/src/providers/anthropic-instance.ts +48 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
- package/src/providers/derive.ts +2 -0
- package/src/providers/kiro-models.ts +3 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/quota/account-cache.ts +37 -11
- package/src/providers/quota/anthropic-account-quota.ts +93 -0
- package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
- package/src/providers/quota/report-cache.ts +24 -1
- package/src/providers/quota/vendor-probes-oauth.ts +46 -14
- package/src/providers/quota.ts +22 -32
- package/src/providers/registry/entries-core.ts +35 -30
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +7 -4
- package/src/providers/registry/types.ts +3 -0
- package/src/providers/registry.ts +3 -3
- package/src/responses/spill-store.ts +5 -1
- package/src/responses/state/metrics.ts +87 -0
- package/src/responses/state/snapshot-codec.ts +6 -0
- package/src/responses/state/spill-queue.ts +1 -0
- package/src/responses/state/unforced-store-false.ts +55 -0
- package/src/responses/state.ts +50 -66
- package/src/router.ts +28 -4
- package/src/routing/compatibility/assemble.ts +1 -1
- package/src/routing/quota.ts +7 -3
- package/src/server/auth-cors.ts +5 -0
- package/src/server/chat-completions.ts +6 -1
- package/src/server/chat-native.ts +45 -25
- package/src/server/claude-messages.ts +16 -2
- package/src/server/gui-static.ts +38 -4
- package/src/server/index/spend-ledger-lifecycle.ts +10 -4
- package/src/server/index.ts +4 -2
- package/src/server/inference/context.ts +1 -1
- package/src/server/lifecycle.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +30 -6
- package/src/server/management/anthropic-account-threshold.ts +12 -9
- package/src/server/management/anthropic-pool-settings.ts +72 -0
- package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
- package/src/server/management/config-routes.ts +27 -7
- package/src/server/management/decision-routes.ts +2 -1
- package/src/server/management/integration-routes.ts +25 -2
- package/src/server/management/oauth-account-routes.ts +79 -98
- package/src/server/management/provider-instance-ownership.ts +44 -0
- package/src/server/management/provider-routes.ts +21 -28
- package/src/server/management/route-registry.ts +3 -1
- package/src/server/management/routing-profile-routes.ts +4 -4
- package/src/server/management/system-restart.ts +44 -14
- package/src/server/management/system-routes.ts +24 -2
- package/src/server/management/vision-sidecar-options.ts +9 -6
- package/src/server/management/web-search-sidecar-options.ts +51 -2
- package/src/server/management-auth.ts +6 -0
- package/src/server/messages-native-eligibility.ts +3 -2
- package/src/server/messages-native-oauth.ts +86 -41
- package/src/server/messages-native-selector.ts +39 -0
- package/src/server/messages-native.ts +84 -25
- package/src/server/request-log.ts +5 -0
- package/src/server/responses/adapter-continuation.ts +29 -17
- package/src/server/responses/adapter-dispatch.ts +47 -17
- package/src/server/responses/antigravity-validation-refusal.ts +20 -6
- package/src/server/responses/compact.ts +25 -8
- package/src/server/responses/compaction-recovery.ts +1 -1
- package/src/server/responses/core-combo-native.ts +4 -1
- package/src/server/responses/core-combo.ts +16 -11
- package/src/server/responses/core-normalize.ts +1 -0
- package/src/server/responses/core-options.ts +3 -1
- package/src/server/responses/core-replay.ts +5 -0
- package/src/server/responses/passthrough-delivery.ts +7 -3
- package/src/server/responses/passthrough-dispatch.ts +21 -8
- package/src/server/responses/request-prepare.ts +37 -5
- package/src/server/responses/request-send-budget.ts +74 -37
- package/src/server/responses/request-spend.ts +325 -33
- package/src/server/responses/request-transport.ts +132 -52
- package/src/server/responses/run-turn-execution.ts +11 -3
- package/src/server/responses/sidecar-execution.ts +96 -14
- package/src/server/responses-undeclared-tool-guard.ts +5 -1
- package/src/server/workflow-refusal.ts +30 -0
- package/src/service/ownership-mutation-lease.d.mts +1 -1
- package/src/service/ownership-mutation-lease.mjs +5 -7
- package/src/sidecar/anthropic-binding.ts +79 -0
- package/src/sidecar/auth.ts +76 -2
- package/src/types/anthropic-account-pool.ts +33 -0
- package/src/types/config.ts +12 -28
- package/src/types/provider.ts +5 -0
- package/src/types/tools.ts +7 -0
- package/src/types.ts +4 -0
- package/src/usage/cost.ts +3 -2
- package/src/usage/expected-prices.ts +66 -14
- package/src/vision/anthropic-describe.ts +16 -3
- package/src/vision/index.ts +3 -0
- package/src/vision/plan.ts +19 -6
- package/src/web-search/alpha-search.ts +7 -1
- package/src/web-search/anthropic-executor.ts +16 -4
- package/src/web-search/index.ts +7 -3
- package/src/web-search/loop.ts +27 -5
- package/src/web-search/passthrough-bridge.ts +4 -1
- package/src/web-search/sidecar-providers.ts +4 -3
- package/gui/dist/assets/App-MbDVfUeC.js +0 -54
- package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
- package/gui/dist/assets/index-UJt6npXk.js +0 -94
- package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
|
@@ -19,17 +19,11 @@
|
|
|
19
19
|
* No token, account id or body content is logged or returned in an error message.
|
|
20
20
|
*/
|
|
21
21
|
import type { OAuthAccessSnapshot } from "../oauth";
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
resolveAnthropicDispatchAccountId,
|
|
28
|
-
resolveAnthropicAccountForSession,
|
|
29
|
-
getEligibleAnthropicAccounts,
|
|
30
|
-
isAnthropicAccountPoolEnabled,
|
|
31
|
-
} from "../oauth/anthropic-routing";
|
|
32
|
-
import { resolveAnthropicModelRoute, routeCandidates, type AnthropicRouteDecision } from "../oauth/anthropic-model-routes";
|
|
22
|
+
import { anthropicRoutingFor } from "../oauth/anthropic-routing";
|
|
23
|
+
import { resolveAnthropicModelRouteForInstance, routeCandidates, type AnthropicRouteDecision } from "../oauth/anthropic-model-routes";
|
|
24
|
+
import { configuredAnthropicInstance, type AnthropicInstanceId } from "../providers/anthropic-instance";
|
|
25
|
+
import { resolveAnthropicMessagesUrl } from "../adapters/anthropic";
|
|
26
|
+
import { routedProviderConfig } from "../router";
|
|
33
27
|
import {
|
|
34
28
|
captureOAuthAccountSelection,
|
|
35
29
|
commitOAuthAccountSelection,
|
|
@@ -40,7 +34,6 @@ import type { TranslatorBudget } from "../lib/translator-budget";
|
|
|
40
34
|
import type { OcxConfig } from "../types";
|
|
41
35
|
import { relaySseWithPayloadRewrite } from "./sse-payload-rewrite";
|
|
42
36
|
|
|
43
|
-
const PROVIDER = "anthropic";
|
|
44
37
|
const MAX_SELECTION_ATTEMPTS = 3;
|
|
45
38
|
|
|
46
39
|
type Selection = NonNullable<ReturnType<typeof captureOAuthAccountSelection>>;
|
|
@@ -52,6 +45,9 @@ function isRec(value: unknown): value is Rec {
|
|
|
52
45
|
|
|
53
46
|
/** The committed selection and the credential snapshot one native request is served with. */
|
|
54
47
|
export interface NativeOAuthBinding {
|
|
48
|
+
readonly instance: AnthropicInstanceId;
|
|
49
|
+
/** Config-authorized physical destination, captured before any credential await. */
|
|
50
|
+
readonly routeTarget: string;
|
|
55
51
|
readonly selection: Selection;
|
|
56
52
|
readonly snapshot: OAuthAccessSnapshot;
|
|
57
53
|
/** Anthropic account UUID from the same stored generation, distinct from snapshot.accountId. */
|
|
@@ -74,17 +70,43 @@ export class NativeOAuthSelectionChangedError extends Error {
|
|
|
74
70
|
/** Invalid operator route configuration remains a request refusal, not an auth failure. */
|
|
75
71
|
export class NativeOAuthModelRouteError extends Error {}
|
|
76
72
|
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Current configured ownership and target; a revoked B never resolves orphan credentials.
|
|
75
|
+
*
|
|
76
|
+
* Adapter, auth mode and destination are read from the routed provider, the same normalization the
|
|
77
|
+
* settled route used to compute its target. Routing canonicalizes the primary row's adapter and auth
|
|
78
|
+
* mode from the registry, so a raw-field comparison would refuse an existing Pool 1 row the route
|
|
79
|
+
* already accepted. B's own raw-row shape is still enforced through `configuredAnthropicInstance`.
|
|
80
|
+
*/
|
|
81
|
+
function nativeOAuthRouteTarget(instance: AnthropicInstanceId, config: OcxConfig): string {
|
|
82
|
+
const provider = config.providers[instance];
|
|
83
|
+
if (configuredAnthropicInstance(config, instance) !== instance || !provider || provider.disabled === true) {
|
|
84
|
+
throw new NativeOAuthSelectionChangedError();
|
|
85
|
+
}
|
|
86
|
+
let routed: ReturnType<typeof routedProviderConfig>;
|
|
87
|
+
try {
|
|
88
|
+
routed = routedProviderConfig(instance, provider);
|
|
89
|
+
} catch {
|
|
90
|
+
// A destination the router now refuses is a moved route, not a different failure class.
|
|
91
|
+
throw new NativeOAuthSelectionChangedError();
|
|
92
|
+
}
|
|
93
|
+
if (routed.authMode !== "oauth" || routed.adapter !== "anthropic") throw new NativeOAuthSelectionChangedError();
|
|
94
|
+
return resolveAnthropicMessagesUrl(routed);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function currentRoute(instance: AnthropicInstanceId, config: OcxConfig, model?: string): AnthropicRouteDecision | null {
|
|
98
|
+
const resolved = model ? resolveAnthropicModelRouteForInstance(instance, config, model) : { decision: null };
|
|
79
99
|
if (resolved.error) throw new NativeOAuthModelRouteError(`Invalid Anthropic model routes: ${resolved.error}`);
|
|
80
100
|
return resolved.decision ? { ...resolved.decision, accounts: [...resolved.decision.accounts] } : null;
|
|
81
101
|
}
|
|
82
102
|
|
|
83
|
-
function routeIsCurrent(config: OcxConfig, model: string | undefined, decision: AnthropicRouteDecision | null): boolean {
|
|
84
|
-
return JSON.stringify(currentRoute(config, model)) === JSON.stringify(decision);
|
|
103
|
+
function routeIsCurrent(instance: AnthropicInstanceId, config: OcxConfig, model: string | undefined, decision: AnthropicRouteDecision | null): boolean {
|
|
104
|
+
return JSON.stringify(currentRoute(instance, config, model)) === JSON.stringify(decision);
|
|
85
105
|
}
|
|
86
106
|
|
|
87
107
|
export interface NativeOAuthBindingOptions {
|
|
108
|
+
/** Settled route destination; retries may never authorize a replacement destination. */
|
|
109
|
+
routeTarget?: string;
|
|
88
110
|
sessionKey?: string | null;
|
|
89
111
|
model?: string;
|
|
90
112
|
/** An account proposed by the shared pre-output refusal recovery policy. */
|
|
@@ -94,21 +116,34 @@ export interface NativeOAuthBindingOptions {
|
|
|
94
116
|
expectedRecoveryRouteDecision?: AnthropicRouteDecision | null;
|
|
95
117
|
}
|
|
96
118
|
|
|
119
|
+
/** Compatibility entrypoint: the original instance only. */
|
|
120
|
+
export function resolveNativeOAuthBinding(config: OcxConfig, options: NativeOAuthBindingOptions = {}): Promise<NativeOAuthBinding> {
|
|
121
|
+
return resolveNativeOAuthBindingForInstance("anthropic", config, options);
|
|
122
|
+
}
|
|
123
|
+
|
|
97
124
|
/** Resolve the shared pool selector and commit its exact credential generation for dispatch. */
|
|
98
|
-
export async function
|
|
125
|
+
export async function resolveNativeOAuthBindingForInstance(
|
|
126
|
+
instance: AnthropicInstanceId,
|
|
99
127
|
config: OcxConfig,
|
|
100
128
|
options: NativeOAuthBindingOptions = {},
|
|
101
129
|
): Promise<NativeOAuthBinding> {
|
|
130
|
+
const routeTarget = options.routeTarget ?? nativeOAuthRouteTarget(instance, config);
|
|
131
|
+
const routing = anthropicRoutingFor(instance);
|
|
132
|
+
const assertOwner = () => {
|
|
133
|
+
if (nativeOAuthRouteTarget(instance, config) !== routeTarget) throw new NativeOAuthSelectionChangedError();
|
|
134
|
+
};
|
|
102
135
|
const sessionKey = options.sessionKey ?? null;
|
|
103
136
|
const model = options.model;
|
|
104
137
|
for (let attempt = 0; attempt < MAX_SELECTION_ATTEMPTS; attempt++) {
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
const
|
|
138
|
+
assertOwner();
|
|
139
|
+
const routeDecision = currentRoute(instance, config, model);
|
|
140
|
+
const manualSelectionGeneration = routing.captureAnthropicManualSelectionGeneration();
|
|
141
|
+
const selection = captureOAuthAccountSelection(instance);
|
|
108
142
|
// Always ask admission first so pause/login/cooldown retains its typed local refusal.
|
|
109
|
-
const admittedId = await resolveAnthropicDispatchAccountId(config, sessionKey, routeDecision, model);
|
|
110
|
-
|
|
111
|
-
|
|
143
|
+
const admittedId = await routing.resolveAnthropicDispatchAccountId(config, sessionKey, routeDecision, model);
|
|
144
|
+
assertOwner();
|
|
145
|
+
if (!routeIsCurrent(instance, config, model, routeDecision)) continue;
|
|
146
|
+
const proposed = routing.resolveAnthropicAccountForSession(sessionKey, config, Date.now(), routeDecision, model);
|
|
112
147
|
// A lost selection commit discards a recovery proposal: a newer manual/policy choice wins.
|
|
113
148
|
const recoverySelectionMatches = !!options.expectedRecoverySelection
|
|
114
149
|
&& selection?.accountId === options.expectedRecoverySelection.accountId
|
|
@@ -118,27 +153,30 @@ export async function resolveNativeOAuthBinding(
|
|
|
118
153
|
const accountId = attempt === 0 && options.candidateAccountId && recoverySelectionMatches && recoveryRouteMatches
|
|
119
154
|
? options.candidateAccountId : admittedId;
|
|
120
155
|
if (!selection) continue;
|
|
121
|
-
if (!routeCandidates(getEligibleAnthropicAccounts(Date.now(), model), routeDecision).includes(accountId)) {
|
|
156
|
+
if (!routeCandidates(routing.getEligibleAnthropicAccounts(Date.now(), model), routeDecision).includes(accountId)) {
|
|
122
157
|
throw new NativeOAuthSelectionChangedError();
|
|
123
158
|
}
|
|
124
|
-
const candidate = await getAnthropicPoolAccessSnapshot(accountId);
|
|
125
|
-
|
|
126
|
-
|
|
159
|
+
const candidate = await routing.getAnthropicPoolAccessSnapshot(accountId);
|
|
160
|
+
assertOwner();
|
|
161
|
+
if (candidate.provider !== instance) throw new NativeOAuthSelectionChangedError();
|
|
162
|
+
if (!routeIsCurrent(instance, config, model, routeDecision)) continue;
|
|
163
|
+
const committed = await commitOAuthAccountSelection(instance, candidate.accountId, {
|
|
127
164
|
expectedSelection: selection,
|
|
128
165
|
expectedCredentialGeneration: candidate.generation,
|
|
129
166
|
requireUsableAccount: true,
|
|
130
167
|
});
|
|
168
|
+
assertOwner();
|
|
131
169
|
if (committed) {
|
|
132
|
-
if (!routeIsCurrent(config, model, routeDecision)
|
|
133
|
-
|| manualSelectionGeneration !== captureAnthropicManualSelectionGeneration()) continue;
|
|
134
|
-
if (!commitAnthropicSelectionRouting(candidate.accountId, selection, committed, {
|
|
170
|
+
if (!routeIsCurrent(instance, config, model, routeDecision)
|
|
171
|
+
|| manualSelectionGeneration !== routing.captureAnthropicManualSelectionGeneration()) continue;
|
|
172
|
+
if (!routing.commitAnthropicSelectionRouting(candidate.accountId, selection, committed, {
|
|
135
173
|
config, sessionKey, model, routeDecision,
|
|
136
174
|
reason: accountId === proposed.accountId ? proposed.reason : undefined,
|
|
137
175
|
expectedCredentialGeneration: candidate.generation,
|
|
138
176
|
})) continue;
|
|
139
|
-
const row = getAccountCredentialWithStatus(
|
|
140
|
-
if (!row || credentialGeneration(row.credential) !== candidate.generation) continue;
|
|
141
|
-
const binding = { selection: committed, snapshot: candidate, providerAccountUuid: row.credential.accountId, routeDecision, sessionKey, model, config, manualSelectionGeneration };
|
|
177
|
+
const row = getAccountCredentialWithStatus(instance, candidate.accountId);
|
|
178
|
+
if (!row || row.credential.access !== candidate.accessToken || credentialGeneration(row.credential) !== candidate.generation) continue;
|
|
179
|
+
const binding: NativeOAuthBinding = { instance, routeTarget, selection: committed, snapshot: candidate, providerAccountUuid: row.credential.accountId, routeDecision, sessionKey, model, config, manualSelectionGeneration };
|
|
142
180
|
if (nativeOAuthBindingIsCurrent(binding)) return binding;
|
|
143
181
|
}
|
|
144
182
|
}
|
|
@@ -150,12 +188,18 @@ export async function resolveNativeOAuthBinding(
|
|
|
150
188
|
* selection, plus the same usable credential generation. Checked before every physical send.
|
|
151
189
|
*/
|
|
152
190
|
export function nativeOAuthBindingIsCurrent(binding: NativeOAuthBinding): boolean {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
191
|
+
const { instance } = binding;
|
|
192
|
+
const routing = anthropicRoutingFor(instance);
|
|
193
|
+
try {
|
|
194
|
+
if (nativeOAuthRouteTarget(instance, binding.config) !== binding.routeTarget) return false;
|
|
195
|
+
} catch { return false; }
|
|
196
|
+
if (binding.snapshot.provider !== instance) return false;
|
|
197
|
+
if (binding.manualSelectionGeneration !== routing.captureAnthropicManualSelectionGeneration()
|
|
198
|
+
|| !routeIsCurrent(instance, binding.config, binding.model, binding.routeDecision)) return false;
|
|
199
|
+
const selected = captureOAuthAccountSelection(instance);
|
|
200
|
+
const row = getAccountCredentialWithStatus(instance, binding.snapshot.accountId);
|
|
201
|
+
const sessionRoute = routing.isAnthropicAccountPoolEnabled(binding.config) && binding.sessionKey
|
|
202
|
+
? routing.resolveAnthropicAccountForSession(binding.sessionKey, binding.config, Date.now(), binding.routeDecision, binding.model) : null;
|
|
159
203
|
// Another conversation may move the automatic active pointer without revoking this
|
|
160
204
|
// session's affinity. Manual selection clears affinity and precedes it in the selector.
|
|
161
205
|
const selectionCurrent = sessionRoute?.reason === "affinity"
|
|
@@ -163,8 +207,9 @@ export function nativeOAuthBindingIsCurrent(binding: NativeOAuthBinding): boolea
|
|
|
163
207
|
|| (selected?.accountId === binding.selection.accountId && selected?.revision === binding.selection.revision);
|
|
164
208
|
return selectionCurrent
|
|
165
209
|
&& !!row && !row.paused && !row.needsReauth && row.credential.expires > Date.now()
|
|
166
|
-
&& !getAnthropicAccountHealthSnapshot(binding.snapshot.accountId)
|
|
167
|
-
&& routeCandidates(getEligibleAnthropicAccounts(Date.now(), binding.model), binding.routeDecision).includes(binding.snapshot.accountId)
|
|
210
|
+
&& !routing.getAnthropicAccountHealthSnapshot(binding.snapshot.accountId)
|
|
211
|
+
&& routeCandidates(routing.getEligibleAnthropicAccounts(Date.now(), binding.model), binding.routeDecision).includes(binding.snapshot.accountId)
|
|
212
|
+
&& row.credential.access === binding.snapshot.accessToken
|
|
168
213
|
&& credentialGeneration(row.credential) === binding.snapshot.generation
|
|
169
214
|
&& row.credential.accountId === binding.providerAccountUuid;
|
|
170
215
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Pure caller-forward exclusion shared by Messages ingress and protocol preview. */
|
|
2
|
+
import { resolveInboundModel } from "../claude/inbound-model-options";
|
|
3
|
+
import { routeConcreteModel } from "../router";
|
|
4
|
+
import { configuredAnthropicInstance } from "../providers/anthropic-instance";
|
|
5
|
+
import { hasOwnProvider } from "../config/provider-name";
|
|
6
|
+
import type { OcxConfig } from "../types";
|
|
7
|
+
|
|
8
|
+
export function messagesSelectorTargetsSecondaryInstance(
|
|
9
|
+
config: OcxConfig,
|
|
10
|
+
model: string,
|
|
11
|
+
cc: OcxConfig["claudeCode"] = config.claudeCode,
|
|
12
|
+
): boolean {
|
|
13
|
+
const selector = resolveInboundModel(model, cc);
|
|
14
|
+
const slash = selector.indexOf("/");
|
|
15
|
+
if (slash <= 0) return false;
|
|
16
|
+
const qualifier = selector.slice(0, slash);
|
|
17
|
+
// An orphan or unmarked B selector must also never forward a caller's credential.
|
|
18
|
+
if (qualifier === "anthropic2") return true;
|
|
19
|
+
// Provider keys are case-sensitive and outrank aliases, including when disabled.
|
|
20
|
+
if (hasOwnProvider(config.providers, qualifier)) return false;
|
|
21
|
+
try {
|
|
22
|
+
// Concrete resolution handles configured provider aliases without picking a combo/account.
|
|
23
|
+
return routeConcreteModel(config, selector).providerName === "anthropic2";
|
|
24
|
+
} catch {
|
|
25
|
+
return config.providers.anthropic2?.alias?.trim().toLowerCase() === qualifier.toLowerCase();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A missing B row must not fall through the router's generic default-provider path. */
|
|
30
|
+
export function messagesSecondaryInstanceUnavailable(
|
|
31
|
+
config: OcxConfig,
|
|
32
|
+
model: string,
|
|
33
|
+
cc: OcxConfig["claudeCode"] = config.claudeCode,
|
|
34
|
+
): boolean {
|
|
35
|
+
if (!messagesSelectorTargetsSecondaryInstance(config, model, cc)) return false;
|
|
36
|
+
const provider = config.providers.anthropic2;
|
|
37
|
+
return !provider || provider.disabled === true
|
|
38
|
+
|| provider.authMode === "oauth" && configuredAnthropicInstance(config, "anthropic2") !== "anthropic2";
|
|
39
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createPhysicalSendReporter } from "../lib/request-execution-budget";
|
|
2
|
+
import { createInferenceSendBudget } from "./inference/context";
|
|
1
3
|
/**
|
|
2
4
|
* Managed native Messages lane (PF-08, behind `protocols.rollout.managedMessagesNative`).
|
|
3
5
|
*
|
|
@@ -17,16 +19,18 @@
|
|
|
17
19
|
*
|
|
18
20
|
* Loaded lazily by `claude-messages.ts` only when the switch is on and a route is eligible.
|
|
19
21
|
*/
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
22
|
+
import { resolveAnthropicModelRouteForInstance } from "../oauth/anthropic-model-routes";
|
|
23
|
+
import { bindAnthropicRefusalCredentialForSend, rotateAnthropicAccountOnResponseForInstance } from "../oauth/anthropic-account-refusal";
|
|
24
|
+
import { captureAnthropicPhysicalSendOwnership, anthropicPhysicalSendOwnershipIsCurrent,
|
|
25
|
+
type AnthropicPhysicalSendOwnership } from "../oauth/anthropic-send-ownership";
|
|
26
|
+
import { anthropicModelQuotaFor } from "../oauth/anthropic-model-quota";
|
|
23
27
|
import { captureConfigGeneration } from "../lib/state-store-sweeper";
|
|
24
|
-
import {
|
|
28
|
+
import { recordAnthropicAccountQuotaFromHeadersForInstance } from "../providers/quota";
|
|
25
29
|
import { credentialGeneration, getAccountCredentialWithStatus } from "../oauth/store";
|
|
26
30
|
import { ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST } from "../oauth/anthropic-routing";
|
|
27
31
|
import { enforceAnthropicImageLimits } from "../adapters/anthropic-image-guard";
|
|
28
32
|
import { normalizeAnthropicImages } from "../adapters/anthropic-image-normalize";
|
|
29
|
-
import { formatAnthropicErrorBody } from "../adapters/anthropic";
|
|
33
|
+
import { formatAnthropicErrorBody, resolveAnthropicMessagesUrl } from "../adapters/anthropic";
|
|
30
34
|
import {
|
|
31
35
|
anthropicMessagesNativeWireBody,
|
|
32
36
|
buildAnthropicMessagesPassthroughRequest,
|
|
@@ -43,6 +47,7 @@ import { isTranslatorBudgetExceededError, type TranslatorBudget } from "../lib/t
|
|
|
43
47
|
import {
|
|
44
48
|
applyReplayRefusalClientHeaders,
|
|
45
49
|
applyUpstreamRecoveryInit,
|
|
50
|
+
SendBudgetExhaustedError,
|
|
46
51
|
fetchWithResetRetry,
|
|
47
52
|
fetchWithTransientRetry,
|
|
48
53
|
isNonReplayableResponse,
|
|
@@ -67,12 +72,13 @@ import {
|
|
|
67
72
|
} from "../providers/key-failover";
|
|
68
73
|
import { stampApiKeyAccountLabel } from "../providers/label";
|
|
69
74
|
import { OAuthAccountPausedError, OAuthLoginRequiredError, publicOAuthAuthenticationErrorMessage } from "../oauth";
|
|
70
|
-
import { AnthropicAccountCooldownError, formatAnthropicProviderForLog,
|
|
75
|
+
import { AnthropicAccountCooldownError, formatAnthropicProviderForLog, anthropicRoutingFor } from "../oauth/anthropic-routing";
|
|
76
|
+
import { configuredAnthropicInstance } from "../providers/anthropic-instance";
|
|
71
77
|
import { resolveProtocolSettings } from "../protocols/settings";
|
|
72
78
|
import { addProtocolEntryReason, markProtocolBlocked } from "../protocols/trace";
|
|
73
79
|
import type { OcxProviderTransport } from "../providers/xai-transport";
|
|
74
80
|
import { preservesPhysicalComboProvider, resolveComboId } from "../combos";
|
|
75
|
-
import { captureRouteStaticPolicy, routeModel, type RouteResult } from "../router";
|
|
81
|
+
import { captureRouteStaticPolicy, routedProviderConfig, routeModel, type RouteResult } from "../router";
|
|
76
82
|
import { POLICY_NAMESPACE, resolvePolicyProfileId } from "../routing/profile";
|
|
77
83
|
import type { AnthropicClientIdentity } from "../adapters/anthropic/client-identity";
|
|
78
84
|
import type { OcxConfig, OcxProviderConfig, OcxUsage } from "../types";
|
|
@@ -88,11 +94,12 @@ import { beginInferenceAttempt } from "./inference/attempt";
|
|
|
88
94
|
import { createFinalRequestLog, type FinalRequestLogMeta } from "./inference/final-log";
|
|
89
95
|
import { registerTurn, unregisterTurn } from "./lifecycle";
|
|
90
96
|
import { nativeMessagesDeclineReason, type NativeMessagesSelector } from "./messages-native-eligibility";
|
|
97
|
+
import { messagesSecondaryInstanceUnavailable } from "./messages-native-selector";
|
|
91
98
|
import {
|
|
92
99
|
nativeOAuthBindingIsCurrent,
|
|
93
100
|
NativeOAuthSelectionChangedError,
|
|
94
101
|
NativeOAuthModelRouteError,
|
|
95
|
-
|
|
102
|
+
resolveNativeOAuthBindingForInstance,
|
|
96
103
|
restoreOAuthToolNamesInMessage,
|
|
97
104
|
restoreOAuthToolNamesInSse,
|
|
98
105
|
type NativeOAuthBinding,
|
|
@@ -300,10 +307,12 @@ async function prepareNativeBody(body: Rec, signal: AbortSignal): Promise<void>
|
|
|
300
307
|
*/
|
|
301
308
|
export async function handleNativeMessages(options: HandleNativeMessagesOptions): Promise<Response> {
|
|
302
309
|
const { req, config, logCtx, logIds, route, body, requestedModel, translatorBudget } = options;
|
|
310
|
+
const nativeInstance = configuredAnthropicInstance(config, route.providerName);
|
|
303
311
|
const requestedStream = body.stream === true;
|
|
304
312
|
logCtx.inboundProtocol = "messages";
|
|
305
313
|
logCtx.model = route.modelId;
|
|
306
314
|
logCtx.provider = route.providerName;
|
|
315
|
+
logCtx.spendPoolId = route.providerName;
|
|
307
316
|
logCtx.providerAdapter = route.provider.adapter;
|
|
308
317
|
logCtx.requestedModel = requestedModel;
|
|
309
318
|
if (route.routeReason === "model-alias" || route.modelId !== requestedModel) logCtx.requestedAlias = requestedModel;
|
|
@@ -353,11 +362,18 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
353
362
|
return null;
|
|
354
363
|
};
|
|
355
364
|
|
|
365
|
+
if (route.provider.authMode === "oauth" && !nativeInstance) {
|
|
366
|
+
return fail(401, "Configured Anthropic OAuth instance is unavailable", "authentication_error");
|
|
367
|
+
}
|
|
368
|
+
|
|
356
369
|
const toolScopeDenial = nativeMessagesToolScopeDenial(options.modelScope, route.providerName, requestedModel, body);
|
|
357
370
|
if (toolScopeDenial) {
|
|
358
371
|
finishLog(403, toolScopeDenial.message);
|
|
359
372
|
return admissionModelDeniedResponse(toolScopeDenial);
|
|
360
373
|
}
|
|
374
|
+
let routeTarget: string;
|
|
375
|
+
try { routeTarget = resolveAnthropicMessagesUrl(route.provider); }
|
|
376
|
+
catch { return fail(400, "Invalid Anthropic Messages destination", "invalid_request_error"); }
|
|
361
377
|
|
|
362
378
|
try {
|
|
363
379
|
await prepareNativeBody(body, req.signal);
|
|
@@ -391,7 +407,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
391
407
|
const oauthProvider = (binding: NativeOAuthBinding): OcxProviderConfig => ({ ...route.provider, apiKey: binding.snapshot.accessToken });
|
|
392
408
|
if (route.provider.authMode === "oauth") {
|
|
393
409
|
try {
|
|
394
|
-
oauthBinding = await
|
|
410
|
+
oauthBinding = await resolveNativeOAuthBindingForInstance(nativeInstance!, config, { routeTarget, sessionKey: options.sessionKey, model: route.modelId });
|
|
395
411
|
} catch (error) {
|
|
396
412
|
cleanupAbort();
|
|
397
413
|
upstream.abort();
|
|
@@ -411,6 +427,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
411
427
|
stampApiKeyAccountLabel(logCtx, route.providerName, activeProvider);
|
|
412
428
|
if (oauthBinding) logCtx.provider = formatAnthropicProviderForLog(route.providerName, oauthBinding.snapshot.accountId, config);
|
|
413
429
|
const spendTracker = attachRequestSpendTracker(req, logCtx);
|
|
430
|
+
const physicalBudget = createInferenceSendBudget(req, logCtx);
|
|
414
431
|
let activeRequest: AnthropicMessagesPassthroughRequest;
|
|
415
432
|
let retainedRequestBytes = 0;
|
|
416
433
|
const releaseRetainedRequest = () => {
|
|
@@ -473,6 +490,15 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
473
490
|
: Number.POSITIVE_INFINITY;
|
|
474
491
|
const transientSendAvailable = (): boolean => remainingTransientSends() > 0;
|
|
475
492
|
const selector: NativeMessagesSelector = options.selector ?? {};
|
|
493
|
+
const nativeOAuthRouteIsCurrent = (binding: NativeOAuthBinding): boolean => {
|
|
494
|
+
const provider = config.providers[binding.instance];
|
|
495
|
+
if (configuredAnthropicInstance(config, binding.instance) !== binding.instance || !provider) return false;
|
|
496
|
+
// Same normalization as the bound target (nativeOAuthRouteTarget): compare routed destinations.
|
|
497
|
+
let routed: OcxProviderConfig;
|
|
498
|
+
try { routed = routedProviderConfig(binding.instance, provider); } catch { return false; }
|
|
499
|
+
return resolveAnthropicMessagesUrl(routed) === routeTarget
|
|
500
|
+
&& nativeMessagesDeclineReason({ ...route, provider: routed }, body, config, selector) === undefined;
|
|
501
|
+
};
|
|
476
502
|
|
|
477
503
|
const send = async (recovery?: "rate-limit-429" | "oauth-account-403" | "key-429" | "key-401"): Promise<Response> => {
|
|
478
504
|
const remaining = remainingTransientSends();
|
|
@@ -493,12 +519,13 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
493
519
|
modelId: route.modelId,
|
|
494
520
|
dispatchOverride: async (_input, init, execute) => {
|
|
495
521
|
if (oauthBinding) {
|
|
522
|
+
if (!nativeOAuthRouteIsCurrent(oauthBinding)) throw new NativeOAuthSelectionChangedError();
|
|
496
523
|
// The OAuth twin of the key check below: re-resolve through the same selection
|
|
497
524
|
// owner when the committed account or its credential moved since the build.
|
|
498
525
|
for (let attempt = 0; !nativeOAuthBindingIsCurrent(oauthBinding); attempt++) {
|
|
499
526
|
if (attempt >= 3) throw new NativeOAuthSelectionChangedError();
|
|
500
527
|
try {
|
|
501
|
-
oauthBinding = await
|
|
528
|
+
oauthBinding = await resolveNativeOAuthBindingForInstance(nativeInstance!, config, { routeTarget, sessionKey: options.sessionKey, model: route.modelId });
|
|
502
529
|
} catch (error) {
|
|
503
530
|
if (error instanceof OAuthAccountPausedError || error instanceof OAuthLoginRequiredError
|
|
504
531
|
|| error instanceof AnthropicAccountCooldownError || error instanceof NativeOAuthModelRouteError) throw error;
|
|
@@ -519,31 +546,58 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
519
546
|
const headers = new Headers(wire.headers);
|
|
520
547
|
const encoding = new Headers(init.headers).get("accept-encoding");
|
|
521
548
|
if (!headers.has("accept-encoding") && encoding) headers.set("accept-encoding", encoding);
|
|
522
|
-
const
|
|
523
|
-
const
|
|
549
|
+
const sendingBinding = oauthBinding;
|
|
550
|
+
const snapshot = sendingBinding?.snapshot;
|
|
551
|
+
const providerAccountUuid = sendingBinding?.providerAccountUuid;
|
|
524
552
|
const writerGeneration = snapshot ? captureConfigGeneration() : 0;
|
|
525
|
-
const ownsBearer = snapshot &&
|
|
526
|
-
|
|
553
|
+
const ownsBearer = snapshot && snapshot.provider === nativeInstance
|
|
554
|
+
&& headers.get("authorization") === `Bearer ${snapshot.accessToken}` && !headers.has("x-api-key");
|
|
555
|
+
if (oauthBinding && (!ownsBearer || wire.url !== oauthBinding.routeTarget
|
|
556
|
+
|| !nativeOAuthRouteIsCurrent(oauthBinding) || !nativeOAuthBindingIsCurrent(oauthBinding))) {
|
|
557
|
+
throw new NativeOAuthSelectionChangedError();
|
|
558
|
+
}
|
|
559
|
+
const releaseFamily = snapshot && oauthBinding ? anthropicModelQuotaFor(oauthBinding.instance).claimAnthropicFamilyRevalidation(snapshot.accountId, route.modelId) : () => {};
|
|
527
560
|
if (!releaseFamily) throw new AnthropicAccountCooldownError(1);
|
|
528
561
|
let dispatched: Response;
|
|
562
|
+
const spendReport = createPhysicalSendReporter(physicalBudget, () => ({
|
|
563
|
+
poolId: logCtx.spendPoolId ?? route.providerName, identityId: logCtx.accountLogLabel,
|
|
564
|
+
}));
|
|
565
|
+
let sendOwner: AnthropicPhysicalSendOwnership | null = null;
|
|
529
566
|
try {
|
|
530
567
|
if (init.signal?.aborted) throw init.signal.reason;
|
|
531
|
-
|
|
568
|
+
// Capture before either accounting path so this request retains its start policy.
|
|
569
|
+
if (spendReport.beforeSend && !spendReport.beforeSend()) {
|
|
570
|
+
if (spendTracker.refusals) throw new NativeMessagesSpendRefusal();
|
|
571
|
+
throw new SendBudgetExhaustedError();
|
|
572
|
+
}
|
|
573
|
+
if (!physicalBudget.spendEnforced && !spendTracker.charge()) throw new NativeMessagesSpendRefusal();
|
|
532
574
|
noteProviderAttemptSend(logCtx, route.providerName, activeProvider, logCtx.usageLogInputTokens, transportRecovery ?? recovery);
|
|
575
|
+
// Last synchronous ownership reservation before the physical fetch. Retain this
|
|
576
|
+
// exact incarnation across its await; a returned response can never reserve one.
|
|
577
|
+
sendOwner = snapshot ? captureAnthropicPhysicalSendOwnership(snapshot) : null;
|
|
578
|
+
if (snapshot && !sendOwner) throw new NativeOAuthSelectionChangedError();
|
|
533
579
|
dispatched = await sendWithConnectionPolicy(
|
|
534
580
|
(activeProvider as OcxProviderTransport).fetch ?? execute,
|
|
535
581
|
wire.url,
|
|
536
582
|
applyUpstreamRecoveryInit({ ...init, method: "POST", headers, body: wire.body }, transportRecovery),
|
|
537
583
|
{ providerName: route.providerName, provider: activeProvider },
|
|
538
584
|
);
|
|
539
|
-
} finally {
|
|
540
|
-
|
|
585
|
+
} finally {
|
|
586
|
+
try { if (physicalBudget.spendEnforced) spendReport(1); }
|
|
587
|
+
finally {
|
|
588
|
+
try { spendReport.close?.(); }
|
|
589
|
+
finally { releaseFamily(); }
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (ownsBearer && snapshot && sendOwner && sendingBinding) {
|
|
541
593
|
try {
|
|
542
|
-
const current = getAccountCredentialWithStatus(
|
|
543
|
-
if (
|
|
594
|
+
const current = getAccountCredentialWithStatus(snapshot.provider, snapshot.accountId);
|
|
595
|
+
if (anthropicPhysicalSendOwnershipIsCurrent(sendOwner) && nativeOAuthRouteIsCurrent(sendingBinding)
|
|
596
|
+
&& current && !current.needsReauth && current.credential.access === snapshot.accessToken
|
|
597
|
+
&& credentialGeneration(current.credential) === snapshot.generation
|
|
544
598
|
&& current.credential.accountId === providerAccountUuid) {
|
|
545
|
-
|
|
546
|
-
|
|
599
|
+
bindAnthropicRefusalCredentialForSend(dispatched, sendOwner, providerAccountUuid);
|
|
600
|
+
recordAnthropicAccountQuotaFromHeadersForInstance(nativeInstance!, snapshot.accountId, dispatched.headers, writerGeneration, dispatched.status, route.modelId);
|
|
547
601
|
}
|
|
548
602
|
} catch { /* Passive observation must not fail the response. */ }
|
|
549
603
|
}
|
|
@@ -622,12 +676,13 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
622
676
|
const sendingBinding = oauthBinding;
|
|
623
677
|
const expectedRecoverySelection = sendingBinding.selection;
|
|
624
678
|
triedAccountIds.add(sendingBinding.snapshot.accountId);
|
|
625
|
-
const nextAccountId = await
|
|
679
|
+
const nextAccountId = await rotateAnthropicAccountOnResponseForInstance(sendingBinding.instance, response, {
|
|
626
680
|
config, accountId: sendingBinding.snapshot.accountId, model: route.modelId,
|
|
627
681
|
sessionKey: sendingBinding.sessionKey, decision: sendingBinding.routeDecision,
|
|
628
682
|
excludedAccountIds: triedAccountIds,
|
|
629
683
|
currentDecision: () => {
|
|
630
|
-
|
|
684
|
+
if (!nativeOAuthRouteIsCurrent(sendingBinding)) throw new NativeOAuthSelectionChangedError();
|
|
685
|
+
const resolved = resolveAnthropicModelRouteForInstance(sendingBinding.instance, config, route.modelId);
|
|
631
686
|
if (resolved.error) throw new NativeOAuthModelRouteError(`Invalid Anthropic model routes: ${resolved.error}`);
|
|
632
687
|
return resolved.decision;
|
|
633
688
|
},
|
|
@@ -637,7 +692,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
637
692
|
if (!nextAccountId) break;
|
|
638
693
|
const recovery = response.status === 403 ? "oauth-account-403" : "rate-limit-429";
|
|
639
694
|
discard(response);
|
|
640
|
-
oauthBinding = await
|
|
695
|
+
oauthBinding = await resolveNativeOAuthBindingForInstance(nativeInstance!, config, { routeTarget,
|
|
641
696
|
sessionKey: options.sessionKey, model: route.modelId, candidateAccountId: nextAccountId,
|
|
642
697
|
expectedRecoverySelection, expectedRecoveryRouteDecision: sendingBinding.routeDecision,
|
|
643
698
|
});
|
|
@@ -652,6 +707,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
652
707
|
upstream.abort();
|
|
653
708
|
if (req.signal.aborted) return fail(499, "Client cancelled request", "api_error");
|
|
654
709
|
const sendError = error instanceof UpstreamRetryEvidenceError ? error.cause : error;
|
|
710
|
+
if (sendError instanceof SendBudgetExhaustedError) return fail(429, sendError.message, "send_budget_exhausted", "send_budget_exhausted");
|
|
655
711
|
if (sendError instanceof NativeMessagesSpendRefusal) {
|
|
656
712
|
const refusal = workflowRefusalResponse("workflow-spend-exhausted", logCtx);
|
|
657
713
|
finishLog(429);
|
|
@@ -865,6 +921,7 @@ export function nativeMessagesCountBody(
|
|
|
865
921
|
): Rec | undefined {
|
|
866
922
|
if (typeof body.model !== "string") return undefined;
|
|
867
923
|
try {
|
|
924
|
+
if (messagesSecondaryInstanceUnavailable(config, body.model, cc)) return undefined;
|
|
868
925
|
const selectorId = resolveInboundModel(body.model, cc);
|
|
869
926
|
// Combo and policy selectors are never eligible, and routing them would advance round-robin
|
|
870
927
|
// state or run the policy evaluator for a request that sends nothing.
|
|
@@ -875,11 +932,13 @@ export function nativeMessagesCountBody(
|
|
|
875
932
|
route.providerName, route.modelId, route.provider, route.staticPolicy.effectiveAlias, "anthropic",
|
|
876
933
|
);
|
|
877
934
|
route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, "anthropic", route.staticPolicy);
|
|
935
|
+
const instance = configuredAnthropicInstance(config, route.providerName);
|
|
878
936
|
const selector: NativeMessagesSelector = {
|
|
879
937
|
...rows,
|
|
880
938
|
routeSelector: selectorId,
|
|
881
939
|
claudeCode: cc,
|
|
882
|
-
...(route.provider.authMode === "oauth"
|
|
940
|
+
...(route.provider.authMode === "oauth" && instance
|
|
941
|
+
? { oauthFailoverQuorum: anthropicRoutingFor(instance).hasAnthropicFailoverQuorum() } : {}),
|
|
883
942
|
};
|
|
884
943
|
if (nativeMessagesDeclineReason(route, body, config, selector) !== undefined) return undefined;
|
|
885
944
|
// The body without a credential: counting never resolves or refreshes an OAuth account.
|
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
type UsageStatus,
|
|
64
64
|
} from "../usage/log";
|
|
65
65
|
import type { RequestExecutionBudget } from "../lib/request-execution-budget";
|
|
66
|
+
import type { WorkflowSpendDenialDetail } from "../lib/workflow-budget";
|
|
66
67
|
import { attributeFinalRequest, attributeSealedAttempt } from "./request-log-failure-attribution";
|
|
67
68
|
import { debugAttemptDeliverySummary } from "../lib/debug";
|
|
68
69
|
import {
|
|
@@ -203,6 +204,10 @@ export interface RequestLogContext {
|
|
|
203
204
|
spendOutputCeilingTokens?: number;
|
|
204
205
|
/** Pre-send input estimate reserved for spend only; unlike usageLogInputTokens it never enters usage. */
|
|
205
206
|
spendInputEstimateTokens?: number;
|
|
207
|
+
/** Canonical provider-pool spend identity; independent of mutable, account-specific log labels. */
|
|
208
|
+
spendPoolId?: string;
|
|
209
|
+
/** Internal safe refusal detail for a request whose ceiling includes unidentified old pool spend. */
|
|
210
|
+
spendRefusalDetail?: WorkflowSpendDenialDetail;
|
|
206
211
|
/** Settles this request's durable spend entries from `addFinalRequestLog`. */
|
|
207
212
|
spendTracker?: RequestSpendSettlement;
|
|
208
213
|
attempts?: PersistedUsageAttempt[];
|