@bitkyc08/opencodex 2.79.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/cursor/live-transport.ts +5 -3
- package/src/adapters/cursor/native-exec-fs.ts +12 -9
- package/src/adapters/cursor/native-exec.ts +13 -5
- package/src/adapters/cursor/protobuf-request.ts +13 -7
- package/src/adapters/cursor/request-builder.ts +23 -10
- package/src/adapters/cursor/tool-definitions.ts +9 -6
- package/src/adapters/cursor/tool-guidance.ts +29 -8
- package/src/adapters/cursor/tool-naming.ts +40 -7
- package/src/adapters/cursor/tool-wording.ts +46 -0
- package/src/adapters/cursor.ts +3 -0
- package/src/adapters/devin/live-models.ts +2 -0
- package/src/adapters/devin.ts +25 -11
- package/src/adapters/kiro-retry.ts +9 -4
- package/src/adapters/openai-chat/messages.ts +7 -4
- 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 +10 -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/autostart-health.ts +3 -3
- 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/codex/journal.ts +36 -5
- package/src/codex/shim-overlay.ts +9 -1
- package/src/combos/jev-decision-contract.ts +25 -2
- package/src/combos/jev.ts +6 -3
- 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 +347 -199
- 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 +16 -16
- 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-scope.ts +4 -1
- 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/combo-requested-effort.ts +14 -0
- 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 +21 -18
- 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/desktop-startup.ts +100 -3
- 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/header-deadline.ts +67 -0
- package/src/web-search/index.ts +7 -3
- package/src/web-search/loop.ts +32 -10
- package/src/web-search/passthrough-bridge.ts +4 -1
- package/src/web-search/sidecar-providers.ts +4 -3
- package/gui/dist/assets/App-BOUa-ek3.js +0 -54
- package/gui/dist/assets/Tray-DUtzB4zF.js +0 -1
- package/gui/dist/assets/index-DwHNDF4s.js +0 -94
- package/gui/dist/assets/quota-summary-BuL4WIBo.js +0 -1
package/src/responses/state.ts
CHANGED
|
@@ -21,6 +21,9 @@ import {
|
|
|
21
21
|
import { collectReferencedSpillFileNames, snapshotReferencedSpillFileNames } from "./state/spill-inspect";
|
|
22
22
|
import { selectSnapshotEntries, snapshotPayload } from "./state/snapshot-select";
|
|
23
23
|
import { clientCarriedPrefixLength, providerIssuedIdentity } from "./state/replay-fingerprint";
|
|
24
|
+
import { hasPendingClientToolCall, allowsUnforcedStoreFalseReplay } from "./state/unforced-store-false";
|
|
25
|
+
import { computeResponseStateMetrics, type ResponseStateMetrics } from "./state/metrics";
|
|
26
|
+
export type { ResponseStateMetrics } from "./state/metrics";
|
|
24
27
|
export type { ResponseStateTempRecoveryResult, ResponseStateTempRecoveryOptions } from "./state/temp-recovery";
|
|
25
28
|
export type { ResponseSpillDirInspection } from "./spill-store";
|
|
26
29
|
export { recoverStaleResponseStateTemps, reclaimAbandonedResponseStateTemps, inspectAbandonedResponseStateTemps, sweepAbandonedResponseStateTemps } from "./state/temp-recovery";
|
|
@@ -118,6 +121,7 @@ export interface ResidentResponseState {
|
|
|
118
121
|
/** Index in `items` where provider output begins; see clientCarriedPrefixLength. */
|
|
119
122
|
providerOutputStart?: number;
|
|
120
123
|
providers?: OcxProviderContinuationState;
|
|
124
|
+
unforcedStoreFalse?: boolean;
|
|
121
125
|
sizeBytes: number;
|
|
122
126
|
}
|
|
123
127
|
|
|
@@ -128,6 +132,7 @@ export interface SpilledResponseState {
|
|
|
128
132
|
/** Mirrors the spilled payload boundary so a spilled entry keeps its anchor. */
|
|
129
133
|
providerOutputStart?: number;
|
|
130
134
|
providers?: OcxProviderContinuationState;
|
|
135
|
+
unforcedStoreFalse?: boolean;
|
|
131
136
|
spill: ResponseSpillRef;
|
|
132
137
|
sizeBytes: number;
|
|
133
138
|
}
|
|
@@ -315,6 +320,7 @@ function measureResidentEntry(id: string, entry: ResidentInput): ResidentRespons
|
|
|
315
320
|
items: entry.items,
|
|
316
321
|
...(entry.providerOutputStart !== undefined ? { providerOutputStart: entry.providerOutputStart } : {}),
|
|
317
322
|
...(entry.providers ? { providers: entry.providers } : {}),
|
|
323
|
+
...(entry.unforcedStoreFalse ? { unforcedStoreFalse: entry.unforcedStoreFalse } : {}),
|
|
318
324
|
});
|
|
319
325
|
return sizeBytes === null ? null : { kind: "resident", ...entry, sizeBytes };
|
|
320
326
|
}
|
|
@@ -417,7 +423,9 @@ function swapResidentForSpill(id: string, expected: ResidentResponseState, ref:
|
|
|
417
423
|
kind: "spill",
|
|
418
424
|
createdAt: expected.createdAt,
|
|
419
425
|
...(expected.clientThreadId ? { clientThreadId: expected.clientThreadId } : {}),
|
|
426
|
+
...(expected.providerOutputStart !== undefined ? { providerOutputStart: expected.providerOutputStart } : {}),
|
|
420
427
|
...(expected.providers ? { providers: expected.providers } : {}),
|
|
428
|
+
...(expected.unforcedStoreFalse ? { unforcedStoreFalse: expected.unforcedStoreFalse } : {}),
|
|
421
429
|
spill: ref,
|
|
422
430
|
};
|
|
423
431
|
const next: SpilledResponseState = { ...base, sizeBytes: stubSize(id, base) };
|
|
@@ -441,6 +449,7 @@ function replaceSpillEntryAtomically(
|
|
|
441
449
|
items: candidate.items,
|
|
442
450
|
...(candidate.providerOutputStart !== undefined ? { providerOutputStart: candidate.providerOutputStart } : {}),
|
|
443
451
|
...(candidate.providers ? { providers: candidate.providers } : {}),
|
|
452
|
+
...(candidate.unforcedStoreFalse ? { unforcedStoreFalse: candidate.unforcedStoreFalse } : {}),
|
|
444
453
|
});
|
|
445
454
|
const base: Omit<SpilledResponseState, "sizeBytes"> = {
|
|
446
455
|
kind: "spill",
|
|
@@ -448,6 +457,7 @@ function replaceSpillEntryAtomically(
|
|
|
448
457
|
...(candidate.clientThreadId ? { clientThreadId: candidate.clientThreadId } : {}),
|
|
449
458
|
...(candidate.providerOutputStart !== undefined ? { providerOutputStart: candidate.providerOutputStart } : {}),
|
|
450
459
|
...(candidate.providers ? { providers: candidate.providers } : {}),
|
|
460
|
+
...(candidate.unforcedStoreFalse ? { unforcedStoreFalse: candidate.unforcedStoreFalse } : {}),
|
|
451
461
|
spill: ref,
|
|
452
462
|
};
|
|
453
463
|
const next: SpilledResponseState = { ...base, sizeBytes: stubSize(id, base) };
|
|
@@ -533,6 +543,7 @@ function admitOversizedCandidate(
|
|
|
533
543
|
items: candidate.items,
|
|
534
544
|
...(candidate.providerOutputStart !== undefined ? { providerOutputStart: candidate.providerOutputStart } : {}),
|
|
535
545
|
...(candidate.providers ? { providers: candidate.providers } : {}),
|
|
546
|
+
...(candidate.unforcedStoreFalse ? { unforcedStoreFalse: candidate.unforcedStoreFalse } : {}),
|
|
536
547
|
});
|
|
537
548
|
// Enforce the ceiling against the REAL envelope: the spill payload adds
|
|
538
549
|
// the {version, responseId, ...} wrapper, so a candidate within the
|
|
@@ -547,7 +558,9 @@ function admitOversizedCandidate(
|
|
|
547
558
|
kind: "spill",
|
|
548
559
|
createdAt: candidate.createdAt,
|
|
549
560
|
...(candidate.clientThreadId ? { clientThreadId: candidate.clientThreadId } : {}),
|
|
561
|
+
...(candidate.providerOutputStart !== undefined ? { providerOutputStart: candidate.providerOutputStart } : {}),
|
|
550
562
|
...(candidate.providers ? { providers: candidate.providers } : {}),
|
|
563
|
+
...(candidate.unforcedStoreFalse ? { unforcedStoreFalse: candidate.unforcedStoreFalse } : {}),
|
|
551
564
|
spill: ref,
|
|
552
565
|
};
|
|
553
566
|
const next: SpilledResponseState = { ...base, sizeBytes: stubSize(id, base) };
|
|
@@ -935,6 +948,7 @@ function pruneResponses(at = now()): void {
|
|
|
935
948
|
items: entry.items,
|
|
936
949
|
...(entry.providerOutputStart !== undefined ? { providerOutputStart: entry.providerOutputStart } : {}),
|
|
937
950
|
...(entry.providers ? { providers: entry.providers } : {}),
|
|
951
|
+
...(entry.unforcedStoreFalse ? { unforcedStoreFalse: entry.unforcedStoreFalse } : {}),
|
|
938
952
|
});
|
|
939
953
|
if (swapResidentForSpill(oldestId, entry, ref)) noteSpillWriteSuccess();
|
|
940
954
|
} catch (error) {
|
|
@@ -994,6 +1008,7 @@ export function evictOldestResponseContinuationForBudget(): number {
|
|
|
994
1008
|
items: entry.items,
|
|
995
1009
|
...(entry.providerOutputStart !== undefined ? { providerOutputStart: entry.providerOutputStart } : {}),
|
|
996
1010
|
...(entry.providers ? { providers: entry.providers } : {}),
|
|
1011
|
+
...(entry.unforcedStoreFalse ? { unforcedStoreFalse: entry.unforcedStoreFalse } : {}),
|
|
997
1012
|
});
|
|
998
1013
|
if (swapResidentForSpill(id, entry, ref)) noteSpillWriteSuccess();
|
|
999
1014
|
} catch (error) {
|
|
@@ -1030,6 +1045,15 @@ function materializeEntry(
|
|
|
1030
1045
|
schedulePersist();
|
|
1031
1046
|
return { ok: false, failure };
|
|
1032
1047
|
}
|
|
1048
|
+
// A restricted snapshot stub must agree with the validated spill's provenance.
|
|
1049
|
+
if (entry.unforcedStoreFalse && (result.payload.unforcedStoreFalse !== true
|
|
1050
|
+
|| entry.providerOutputStart === undefined || entry.providerOutputStart > result.payload.items.length
|
|
1051
|
+
|| entry.providerOutputStart !== result.payload.providerOutputStart)) {
|
|
1052
|
+
spillCounters.readFailures += 1;
|
|
1053
|
+
replaceWithSpillFailure(id, entry);
|
|
1054
|
+
schedulePersist();
|
|
1055
|
+
return { ok: false, failure: { code: "previous_response_not_found", reason: "spill_corrupt" } };
|
|
1056
|
+
}
|
|
1033
1057
|
const state = measureResidentEntry(id, {
|
|
1034
1058
|
createdAt: result.payload.createdAt,
|
|
1035
1059
|
...(result.payload.clientThreadId ? { clientThreadId: result.payload.clientThreadId } : {}),
|
|
@@ -1038,6 +1062,7 @@ function materializeEntry(
|
|
|
1038
1062
|
? { providerOutputStart: result.payload.providerOutputStart }
|
|
1039
1063
|
: {}),
|
|
1040
1064
|
...(result.payload.providers ? { providers: result.payload.providers } : {}),
|
|
1065
|
+
...(result.payload.unforcedStoreFalse ? { unforcedStoreFalse: result.payload.unforcedStoreFalse } : {}),
|
|
1041
1066
|
});
|
|
1042
1067
|
if (!state) {
|
|
1043
1068
|
spillCounters.readFailures += 1;
|
|
@@ -1075,6 +1100,15 @@ export function expandPreviousResponseInput(body: unknown, clientThreadId?: stri
|
|
|
1075
1100
|
replayScopeMismatchDrops += 1;
|
|
1076
1101
|
return body;
|
|
1077
1102
|
}
|
|
1103
|
+
const clientInput = inputItems(request.input);
|
|
1104
|
+
const stored = materialized.state.items;
|
|
1105
|
+
const carried = clientCarriedPrefixLength(stored, clientInput);
|
|
1106
|
+
if (
|
|
1107
|
+
materialized.state.unforcedStoreFalse
|
|
1108
|
+
&& !allowsUnforcedStoreFalseReplay(stored, materialized.state.providerOutputStart, clientInput, carried)
|
|
1109
|
+
) {
|
|
1110
|
+
return body;
|
|
1111
|
+
}
|
|
1078
1112
|
// The client already replayed this history verbatim. Prepending the stored copy would
|
|
1079
1113
|
// double it, and the doubled turn is stored again, so the next turn triples (#1412 saw
|
|
1080
1114
|
// 127k of real context reach 1.3M tokens this way).
|
|
@@ -1087,10 +1121,7 @@ export function expandPreviousResponseInput(body: unknown, clientThreadId?: stri
|
|
|
1087
1121
|
// There is no invariant that provider output always carries ids, so an entry whose output
|
|
1088
1122
|
// has none simply never skips.
|
|
1089
1123
|
{
|
|
1090
|
-
const clientInput = inputItems(request.input);
|
|
1091
|
-
const stored = materialized.state.items;
|
|
1092
1124
|
const anchor = materialized.state.providerOutputStart;
|
|
1093
|
-
const carried = clientCarriedPrefixLength(stored, clientInput);
|
|
1094
1125
|
if (
|
|
1095
1126
|
carried === stored.length
|
|
1096
1127
|
&& anchor !== undefined
|
|
@@ -1110,9 +1141,9 @@ export function expandPreviousResponseInput(body: unknown, clientThreadId?: stri
|
|
|
1110
1141
|
}
|
|
1111
1142
|
const expanded = {
|
|
1112
1143
|
...request,
|
|
1113
|
-
input: [...
|
|
1144
|
+
input: [...stored, ...clientInput],
|
|
1114
1145
|
};
|
|
1115
|
-
replayedInputPrefixLengths.set(expanded,
|
|
1146
|
+
replayedInputPrefixLengths.set(expanded, stored.length);
|
|
1116
1147
|
return expanded;
|
|
1117
1148
|
}
|
|
1118
1149
|
|
|
@@ -1156,29 +1187,6 @@ export function previousResponseProviderState(responseId: string | undefined): O
|
|
|
1156
1187
|
return providers ? structuredClone(providers) : undefined;
|
|
1157
1188
|
}
|
|
1158
1189
|
|
|
1159
|
-
export interface ResponseStateMetrics {
|
|
1160
|
-
count: number;
|
|
1161
|
-
residentCount: number;
|
|
1162
|
-
spillStubCount: number;
|
|
1163
|
-
tombstoneCount: number;
|
|
1164
|
-
totalBytes: number;
|
|
1165
|
-
spillPayloadBytes: number;
|
|
1166
|
-
largestBytes: number;
|
|
1167
|
-
oldestAgeMs: number;
|
|
1168
|
-
spillWrites: number;
|
|
1169
|
-
spillWriteFailures: number;
|
|
1170
|
-
spillWriteStatus: ResponseSpillWriteStatus;
|
|
1171
|
-
spillWriteConsecutiveFailures: number;
|
|
1172
|
-
spillLastWriteFailureCode: ResponseSpillWriteFailureCode | null;
|
|
1173
|
-
spillLastWriteFailureOrigin: ResponseSpillWriteFailureOrigin | null;
|
|
1174
|
-
spillAclRetryReturnedTimeouts: number;
|
|
1175
|
-
spillAclTimeoutMemoRefusals: number;
|
|
1176
|
-
spillLastWriteFailureAt: number | null;
|
|
1177
|
-
spillLastWriteSuccessAt: number | null;
|
|
1178
|
-
spillReadFailures: number;
|
|
1179
|
-
replayScopeMismatchDrops: number;
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
1190
|
/**
|
|
1183
1191
|
* Observe-only snapshot of the in-RAM continuation store, surfaced via GET /api/system/memory.
|
|
1184
1192
|
* Additive and side-effect free — it does NOT lazy-load the disk snapshot, prune, or evict — so a
|
|
@@ -1189,50 +1197,23 @@ export interface ResponseStateMetrics {
|
|
|
1189
1197
|
* store (JS heap) or in the runtime allocator (native).
|
|
1190
1198
|
*/
|
|
1191
1199
|
export function responseStateMetrics(): ResponseStateMetrics {
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
let spillStubCount = 0;
|
|
1197
|
-
let tombstoneCount = 0;
|
|
1198
|
-
let spillPayloadBytes = 0;
|
|
1199
|
-
for (const state of states.values()) {
|
|
1200
|
-
const bytes = state.sizeBytes;
|
|
1201
|
-
if (bytes > largestBytes) largestBytes = bytes;
|
|
1202
|
-
if (state.createdAt < oldestCreatedAt) oldestCreatedAt = state.createdAt;
|
|
1203
|
-
if (state.kind === "resident") {
|
|
1204
|
-
residentCount += 1;
|
|
1205
|
-
} else if (state.kind === "spill") {
|
|
1206
|
-
spillStubCount += 1;
|
|
1207
|
-
spillPayloadBytes += state.spill.payloadBytes;
|
|
1208
|
-
} else tombstoneCount += 1;
|
|
1209
|
-
}
|
|
1210
|
-
return {
|
|
1211
|
-
count: states.size,
|
|
1212
|
-
residentCount,
|
|
1213
|
-
spillStubCount,
|
|
1214
|
-
tombstoneCount,
|
|
1200
|
+
return computeResponseStateMetrics({
|
|
1201
|
+
states: states.values(),
|
|
1202
|
+
stateCount: states.size,
|
|
1203
|
+
now: now(),
|
|
1215
1204
|
totalBytes: responseContinuationRetainedStoreSnapshot().bytes,
|
|
1216
|
-
spillPayloadBytes,
|
|
1217
|
-
largestBytes,
|
|
1218
|
-
oldestAgeMs: states.size > 0 ? at - oldestCreatedAt : 0,
|
|
1219
1205
|
spillWrites: spillCounters.writes,
|
|
1220
1206
|
spillWriteFailures: spillCounters.writeFailures,
|
|
1221
|
-
spillWriteStatus: spillWriteHealth.consecutiveFailures > 0
|
|
1222
|
-
? "degraded"
|
|
1223
|
-
: spillWriteHealth.lastSuccessAt !== null
|
|
1224
|
-
? "healthy"
|
|
1225
|
-
: "initial",
|
|
1226
1207
|
spillWriteConsecutiveFailures: spillWriteHealth.consecutiveFailures,
|
|
1227
|
-
|
|
1228
|
-
|
|
1208
|
+
spillLastFailureCode: spillWriteHealth.lastFailureCode,
|
|
1209
|
+
spillLastFailureOrigin: spillWriteHealth.lastFailureOrigin,
|
|
1210
|
+
spillLastSuccessAt: spillWriteHealth.lastSuccessAt,
|
|
1229
1211
|
spillAclRetryReturnedTimeouts: spillCounters.aclRetryReturnedTimeouts,
|
|
1230
1212
|
spillAclTimeoutMemoRefusals: spillCounters.aclTimeoutMemoRefusals,
|
|
1231
|
-
|
|
1232
|
-
spillLastWriteSuccessAt: spillWriteHealth.lastSuccessAt,
|
|
1213
|
+
spillLastFailureAt: spillWriteHealth.lastFailureAt,
|
|
1233
1214
|
spillReadFailures: spillCounters.readFailures,
|
|
1234
1215
|
replayScopeMismatchDrops,
|
|
1235
|
-
};
|
|
1216
|
+
});
|
|
1236
1217
|
}
|
|
1237
1218
|
|
|
1238
1219
|
/**
|
|
@@ -1256,7 +1237,7 @@ export function rememberResponseState(
|
|
|
1256
1237
|
requestBody: unknown,
|
|
1257
1238
|
response: { id?: unknown; output?: unknown; status?: unknown; incomplete_details?: unknown },
|
|
1258
1239
|
providerState?: OcxProviderContinuationState | string,
|
|
1259
|
-
opts?: { force?: boolean; clientThreadId?: string },
|
|
1240
|
+
opts?: { force?: boolean; clientThreadId?: string; retainForToolContinuation?: boolean },
|
|
1260
1241
|
): void {
|
|
1261
1242
|
if (!requestBody || typeof requestBody !== "object" || Array.isArray(requestBody)) return;
|
|
1262
1243
|
const request = requestBody as Record<string, unknown>;
|
|
@@ -1266,8 +1247,10 @@ export function rememberResponseState(
|
|
|
1266
1247
|
// The passthrough branch records with force so those chains can be expanded locally; the
|
|
1267
1248
|
// store stays in-memory under RESPONSE_TTL_MS, so this is a proxy-internal continuation cache, not
|
|
1268
1249
|
// real server-side response storage.
|
|
1269
|
-
if (request.store === false && !opts?.force) return;
|
|
1270
1250
|
if (typeof response.id !== "string" || !Array.isArray(response.output)) return;
|
|
1251
|
+
// Only an explicitly opted-in adapter may retain pending client calls despite store:false.
|
|
1252
|
+
const unforcedStoreFalse = request.store === false && !opts?.force;
|
|
1253
|
+
if (unforcedStoreFalse && !(opts?.retainForToolContinuation === true && hasPendingClientToolCall(response.output))) return;
|
|
1271
1254
|
if (response.status === "incomplete") {
|
|
1272
1255
|
const details = response.incomplete_details;
|
|
1273
1256
|
if (!details || typeof details !== "object" || Array.isArray(details)
|
|
@@ -1300,6 +1283,7 @@ export function rememberResponseState(
|
|
|
1300
1283
|
// incomplete agent turn on the Cursor side (we suspended without a real mcpResult), so its
|
|
1301
1284
|
// checkpoint must not be reused — but the conversation id string itself is still valid.
|
|
1302
1285
|
...(Object.keys(normalizedProviderState).length > 0 ? { providers: normalizedProviderState } : {}),
|
|
1286
|
+
...(unforcedStoreFalse ? { unforcedStoreFalse: true } : {}),
|
|
1303
1287
|
});
|
|
1304
1288
|
enforceAppOwnedMemoryBudget();
|
|
1305
1289
|
schedulePersist();
|
package/src/router.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CODEX_INTERNAL_OPENAI_MODELS } from "./codex/control-plane-models";
|
|
1
2
|
import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "./types";
|
|
2
3
|
import { createHash } from "node:crypto";
|
|
3
4
|
import { peekAuthStore } from "./oauth/store";
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
} from "./combos";
|
|
13
14
|
import type { NormalizedComboConfig } from "./combos/types";
|
|
14
15
|
import { hasOwnProvider } from "./config/provider-name";
|
|
16
|
+
import { anthropicInstanceRowShapeMatches } from "./providers/anthropic-instance-id";
|
|
15
17
|
import { providerUsesKeyAuthOverride, resolveProviderApiKey } from "./providers/key-store";
|
|
16
18
|
import { captureProviderApiKeySelection } from "./providers/api-key-selection-capture";
|
|
17
19
|
import { assertProviderDestinationAllowed } from "./lib/destination-policy";
|
|
@@ -53,6 +55,14 @@ import { evaluatePolicyProfile, type PolicyRequestEvidence } from "./routing/eva
|
|
|
53
55
|
import { assemblePolicyCandidateEvidence } from "./routing/compatibility/assemble";
|
|
54
56
|
import { resolveModelPolicy, type ResolvedModelPolicy } from "./providers/resolved-model-policy";
|
|
55
57
|
|
|
58
|
+
/** Explicit Pool 2 admission refusal; ingress must not recover it through default routing. */
|
|
59
|
+
export class AnthropicSecondaryInstanceUnavailableError extends Error {
|
|
60
|
+
constructor() {
|
|
61
|
+
super("Anthropic Pool 2 requires an enabled configured provider with valid OAuth ownership");
|
|
62
|
+
this.name = "AnthropicSecondaryInstanceUnavailableError";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
56
66
|
export class UnknownRoutingPolicyError extends Error {
|
|
57
67
|
constructor(readonly profileId: string) {
|
|
58
68
|
super(`Unknown routing policy: ${profileId}`);
|
|
@@ -521,9 +531,18 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
|
|
|
521
531
|
return resolved;
|
|
522
532
|
}
|
|
523
533
|
|
|
534
|
+
/**
|
|
535
|
+
* Rows eligible for bare-model inference (configured default model, model list, model alias).
|
|
536
|
+
*
|
|
537
|
+
* The marked builtin Pool 2 row is excluded: it seeds the same catalog as `anthropic`, so letting it
|
|
538
|
+
* compete would move a bare `claude-*` request onto Pool 2 by insertion order, or make a shared alias
|
|
539
|
+
* ambiguous. Pool 2 is reached only through `anthropic2/<model>`, its provider alias, or an explicit
|
|
540
|
+
* `defaultProvider`. An unmarked custom row named `anthropic2` keeps its ordinary custom meaning.
|
|
541
|
+
*/
|
|
524
542
|
function activeProviderEntries(config: OcxConfig): [string, OcxProviderConfig][] {
|
|
525
543
|
return Object.entries(config.providers)
|
|
526
|
-
.filter(([name, provider]) => name !== LEGACY_CHATGPT_PROVIDER_ID && provider.disabled !== true
|
|
544
|
+
.filter(([name, provider]) => name !== LEGACY_CHATGPT_PROVIDER_ID && provider.disabled !== true
|
|
545
|
+
&& !(name === "anthropic2" && anthropicInstanceRowShapeMatches(name, provider)));
|
|
527
546
|
}
|
|
528
547
|
|
|
529
548
|
export class NoEnabledOpenAiProviderError extends Error {
|
|
@@ -564,9 +583,6 @@ export function comboRouteDecisionTrace(
|
|
|
564
583
|
});
|
|
565
584
|
}
|
|
566
585
|
|
|
567
|
-
// Codex uses a small number of control-plane model ids that are not part of the public GPT/o
|
|
568
|
-
// naming families. Keep this exact: a broad `codex-*` rule could capture a third-party model.
|
|
569
|
-
const CODEX_INTERNAL_OPENAI_MODELS = new Set(["codex-auto-review"]);
|
|
570
586
|
const MAX_BLOCKED_MODEL_REDIRECT_EDGES = 5;
|
|
571
587
|
|
|
572
588
|
interface BlockedModelRedirectState {
|
|
@@ -620,6 +636,9 @@ function routeResult(
|
|
|
620
636
|
routeReason: string,
|
|
621
637
|
redirectState: BlockedModelRedirectState,
|
|
622
638
|
): RouteResult {
|
|
639
|
+
if (providerName === "anthropic2" && provider.authMode === "oauth" && !anthropicInstanceRowShapeMatches(providerName, provider)) {
|
|
640
|
+
throw new AnthropicSecondaryInstanceUnavailableError();
|
|
641
|
+
}
|
|
623
642
|
const qualified = resolveBlockedModelRedirect(config, `${providerName}/${modelId}`);
|
|
624
643
|
const bare = resolveBlockedModelRedirect(config, modelId);
|
|
625
644
|
const crossTarget = config && (
|
|
@@ -813,6 +832,11 @@ function routeModelInternal(
|
|
|
813
832
|
if (slash > 0) {
|
|
814
833
|
const requestedProvider = modelId.slice(0, slash);
|
|
815
834
|
const requestedLower = requestedProvider.toLowerCase();
|
|
835
|
+
// Literal B is a reserved instance selector even before its row exists. Keep other
|
|
836
|
+
// qualifiers, including exact uppercase custom keys, on the ordinary routing path.
|
|
837
|
+
if (requestedProvider === "anthropic2" && !hasOwnProvider(config.providers, requestedProvider)) {
|
|
838
|
+
throw new AnthropicSecondaryInstanceUnavailableError();
|
|
839
|
+
}
|
|
816
840
|
let provName: string | undefined;
|
|
817
841
|
|
|
818
842
|
if (hasOwnProvider(config.providers, requestedProvider)) {
|
|
@@ -76,7 +76,7 @@ export function assemblePolicyCandidateEvidence(
|
|
|
76
76
|
quota: quotaEvidenceForCandidate({
|
|
77
77
|
provider: candidate.provider,
|
|
78
78
|
model: candidate.model,
|
|
79
|
-
}),
|
|
79
|
+
}, config),
|
|
80
80
|
cost: costEvidenceForCandidate({
|
|
81
81
|
provider: candidate.provider,
|
|
82
82
|
model: candidate.model,
|
package/src/routing/quota.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
import { configuredAnthropicInstance } from "../providers/anthropic-instance";
|
|
1
3
|
/**
|
|
2
4
|
* Quota-aware policy scoring (RI-07).
|
|
3
5
|
*
|
|
@@ -98,7 +100,7 @@ export function codexPoolQuotaEvidence(accounts: readonly CodexPoolQuotaAccount[
|
|
|
98
100
|
};
|
|
99
101
|
}
|
|
100
102
|
|
|
101
|
-
export function quotaEvidenceForCandidate(input: QuotaEvidenceInput): RouteQuotaEvidence {
|
|
103
|
+
export function quotaEvidenceForCandidate(input: QuotaEvidenceInput, config?: Pick<OcxConfig, "providers">): RouteQuotaEvidence {
|
|
102
104
|
if (input.provider === "openai" && input.codexAccountId) {
|
|
103
105
|
// listAccountQuotas() is the reconciled quota snapshot: config-generation
|
|
104
106
|
// reconciliation prunes removed accounts. Other eligibility dimensions
|
|
@@ -113,8 +115,10 @@ export function quotaEvidenceForCandidate(input: QuotaEvidenceInput): RouteQuota
|
|
|
113
115
|
return codexAccountQuotaEvidence(input.codexAccountId, input.codexAccountPlan);
|
|
114
116
|
}
|
|
115
117
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
+
const instance = config ? configuredAnthropicInstance(config, input.provider)
|
|
119
|
+
: input.provider === "anthropic" ? "anthropic" : undefined;
|
|
120
|
+
if (instance && input.accountRef) {
|
|
121
|
+
const quota = getCachedProviderAccountQuota(instance, input.accountRef);
|
|
118
122
|
if (quota) {
|
|
119
123
|
const family = anthropicFamilyWindow(input.model, quota.customWindows ?? []);
|
|
120
124
|
const percents = [quota.fiveHourPercent, quota.weeklyPercent, quota.monthlyPercent, family?.percent]
|
package/src/server/auth-cors.ts
CHANGED
|
@@ -38,6 +38,7 @@ import { redactSecretString } from "../lib/redact";
|
|
|
38
38
|
import { DECLARABLE_HOSTED_TOOL_TYPES } from "../responses/hosted-tool-policy";
|
|
39
39
|
import { effectiveGoogleMode, getProviderRegistryEntry, providerCodexAccountMode, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../providers/registry";
|
|
40
40
|
import { providerConfigSeed } from "../providers/derive";
|
|
41
|
+
import { anthropicOAuthInstanceConfigError } from "../config/schema/anthropic-account-pool";
|
|
41
42
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
42
43
|
import { openRouterRoutingConfigError } from "../providers/openrouter-routing";
|
|
43
44
|
import { modelAutoCompactTokenLimitsConfigError } from "../providers/auto-compact-budget";
|
|
@@ -785,6 +786,8 @@ export function providerManagementConfigError(
|
|
|
785
786
|
return `provider ${name} must not include codexAccountMode`;
|
|
786
787
|
}
|
|
787
788
|
const typed = provider as unknown as OcxProviderConfig;
|
|
789
|
+
const markerError = anthropicOAuthInstanceConfigError({ providers: { [name]: provider } });
|
|
790
|
+
if (markerError) return markerError;
|
|
788
791
|
// Every write path (POST, PUT, reload, both PATCH passes) funnels through here, so a PATCH
|
|
789
792
|
// that changes authMode/baseUrl/adapter under a retained tlsProfile is refused before it
|
|
790
793
|
// persists a row the config schema would later reject as document-fatal.
|
|
@@ -1057,6 +1060,8 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
|
|
|
1057
1060
|
modelVercelGatewayRouting: "editor",
|
|
1058
1061
|
authMode: "editor",
|
|
1059
1062
|
oauthAccountFailover: "editor",
|
|
1063
|
+
anthropicAccountPool: "editor",
|
|
1064
|
+
anthropicOAuthInstance: "editor",
|
|
1060
1065
|
keyOptional: "editor",
|
|
1061
1066
|
freeTier: "editor",
|
|
1062
1067
|
note: "editor",
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
UPSTREAM_RESET_REPLAY_REFUSED_CODE,
|
|
33
33
|
} from "../lib/upstream-retry";
|
|
34
34
|
import { estimateTokens } from "../lib/token-estimate";
|
|
35
|
-
import { captureRouteStaticPolicy, NoEligiblePolicyCandidateError, UnknownRoutingPolicyError, routeModel } from "../router";
|
|
35
|
+
import { AnthropicSecondaryInstanceUnavailableError, captureRouteStaticPolicy, NoEligiblePolicyCandidateError, UnknownRoutingPolicyError, routeModel } from "../router";
|
|
36
36
|
import { evidenceFromBody } from "../routing/request-evidence";
|
|
37
37
|
import { resolveWireProtocolOverride } from "./adapter-resolve";
|
|
38
38
|
import { resolveOpenCodeGoTransport } from "../providers/opencode-go-transport";
|
|
@@ -242,6 +242,11 @@ async function handleChatCompletionsWithBudget(
|
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
244
|
} catch (err) {
|
|
245
|
+
if (err instanceof AnthropicSecondaryInstanceUnavailableError) {
|
|
246
|
+
logCtx.requestedModel = requestedModel;
|
|
247
|
+
if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 401, { closeReason: "non_stream" });
|
|
248
|
+
return chatCompletionsErrorResponse(401, err.message, "authentication_error");
|
|
249
|
+
}
|
|
245
250
|
if (err instanceof AdmissionModelDeniedError) {
|
|
246
251
|
logCtx.requestedModel = requestedModel;
|
|
247
252
|
if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 403, { closeReason: "non_stream" });
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createPhysicalSendReporter } from "../lib/request-execution-budget";
|
|
2
|
+
import { createInferenceSendBudget } from "./inference/context";
|
|
1
3
|
import { buildOpenAIChatPassthroughRequest, createOpenAIChatAdapter } from "../adapters/openai-chat";
|
|
2
4
|
import type { AdapterRequest, ProviderAdapter } from "../adapters/base";
|
|
3
5
|
import { isNativeChatRouteEligible } from "./chat-native-eligibility";
|
|
@@ -17,7 +19,7 @@ import {
|
|
|
17
19
|
isCyberPolicyMessage,
|
|
18
20
|
SEND_BUDGET_EXHAUSTED_CODE,
|
|
19
21
|
} from "../lib/errors";
|
|
20
|
-
import type
|
|
22
|
+
import { reportDispatchSends, type RequestExecutionBudget, type SingleUseDispatchPermit } from "../lib/request-execution-budget";
|
|
21
23
|
import type { AdmissionLease } from "../lib/admission";
|
|
22
24
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
23
25
|
import { redactSecretString } from "../lib/redact";
|
|
@@ -198,6 +200,7 @@ export interface NativeChatExecution extends HandleNativeChatOptions {
|
|
|
198
200
|
* the parent row, replace the one the final log settles.
|
|
199
201
|
*/
|
|
200
202
|
sendBudget?: RequestExecutionBudget;
|
|
203
|
+
comboDispatchPermit?: SingleUseDispatchPermit;
|
|
201
204
|
/** Replaces the request-relative first-output mark; a combo child records its own. */
|
|
202
205
|
onFirstOutput?: () => void;
|
|
203
206
|
/** The lease a streamed body holds; defaults to `logIds.turnAdmissionLease`. */
|
|
@@ -254,6 +257,7 @@ export function createNativeChatComboSource(input: {
|
|
|
254
257
|
translatorBudget: input.translatorBudget,
|
|
255
258
|
finishLog: child.finishLog,
|
|
256
259
|
sendBudget: child.sendBudget,
|
|
260
|
+
comboDispatchPermit: child.comboDispatchPermit,
|
|
257
261
|
onFirstOutput: child.onFirstOutput,
|
|
258
262
|
...(child.turnAdmissionLease ? { turnAdmissionLease: child.turnAdmissionLease } : {}),
|
|
259
263
|
}, child.attemptHandle),
|
|
@@ -314,6 +318,7 @@ export async function runNativeChatAttempt(
|
|
|
314
318
|
let activeProvider: OcxProviderConfig = route.provider;
|
|
315
319
|
stampApiKeyAccountLabel(logCtx, route.providerName, activeProvider);
|
|
316
320
|
const spendTracker = sendBudget ? undefined : attachRequestSpendTracker(req, logCtx);
|
|
321
|
+
const physicalBudget = sendBudget ?? createInferenceSendBudget(req, logCtx);
|
|
317
322
|
let activeAdapter: ProviderAdapter = createOpenAIChatAdapter(activeProvider);
|
|
318
323
|
let activeRequest: AdapterRequest;
|
|
319
324
|
let retainedRequestBytes = 0;
|
|
@@ -450,30 +455,45 @@ export async function runNativeChatAttempt(
|
|
|
450
455
|
const encoding = new Headers(init.headers).get("accept-encoding");
|
|
451
456
|
if (!headers.has("accept-encoding") && encoding) headers.set("accept-encoding", encoding);
|
|
452
457
|
if (init.signal?.aborted) throw init.signal.reason;
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
+
const spendReport = createPhysicalSendReporter(physicalBudget, () => ({
|
|
459
|
+
poolId: logCtx.spendPoolId ?? route.providerName, identityId: logCtx.accountLogLabel,
|
|
460
|
+
}), execution.comboDispatchPermit);
|
|
461
|
+
let dispatched: Response;
|
|
462
|
+
try {
|
|
463
|
+
// Capture this request's spend policy at its first physical boundary, including
|
|
464
|
+
// observe-only starts; later configuration changes belong to later requests.
|
|
465
|
+
if (spendReport.beforeSend && !spendReport.beforeSend()) {
|
|
466
|
+
if ((logCtx.spendTracker as { refusals?: number } | undefined)?.refusals) throw new NativeChatSpendRefusal();
|
|
467
|
+
throw new SendBudgetExhaustedError();
|
|
458
468
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
469
|
+
if (!physicalBudget.spendEnforced && sendBudget) {
|
|
470
|
+
// Backstop for sends the helper cannot see coming (a reset replay). The first
|
|
471
|
+
// report settles the combo's booking; each later one is charged and booked.
|
|
472
|
+
if (physicalSends > 0 && sendBudget.remainingBaseSends(sharedSendCap) <= 0) {
|
|
473
|
+
throw new SendBudgetExhaustedError(safeHostLabel(request.url));
|
|
474
|
+
}
|
|
475
|
+
physicalSends += 1;
|
|
476
|
+
reportDispatchSends(sendBudget, 1, execution.comboDispatchPermit);
|
|
477
|
+
} else if (!physicalBudget.spendEnforced && !spendTracker?.charge()) throw new NativeChatSpendRefusal();
|
|
478
|
+
noteProviderAttemptSend(logCtx, route.providerName, activeProvider, logCtx.usageLogInputTokens, transportRecovery ?? recovery);
|
|
479
|
+
// A reselected provider transport is still a physical send: the connection policy
|
|
480
|
+
// and manual-redirect ownership wrap the selected implementation (#4992).
|
|
481
|
+
dispatched = await sendWithConnectionPolicy(
|
|
482
|
+
(activeProvider as OcxProviderTransport).fetch ?? execute,
|
|
483
|
+
request.url,
|
|
484
|
+
applyUpstreamRecoveryInit({
|
|
485
|
+
...init, method: request.method, headers, body: request.body,
|
|
486
|
+
}, transportRecovery),
|
|
487
|
+
// Reselection can replace the provider transport and the wire shape, so the
|
|
488
|
+
// egress route is bound to the provider this send actually uses. Omitting it
|
|
489
|
+
// here would let a provider transport bypass its configured route entirely,
|
|
490
|
+
// because that transport wins over the executor that carries the binding.
|
|
491
|
+
{ providerName: route.providerName, provider: activeProvider },
|
|
492
|
+
);
|
|
493
|
+
} finally {
|
|
494
|
+
try { if (physicalBudget.spendEnforced) spendReport(1); }
|
|
495
|
+
finally { spendReport.close?.(); }
|
|
496
|
+
}
|
|
477
497
|
if (!dispatched.ok) await recordKeyAttemptFailure(logCtx, dispatched, init.signal ?? upstream.signal);
|
|
478
498
|
return dispatched;
|
|
479
499
|
},
|
|
@@ -558,7 +578,7 @@ export async function runNativeChatAttempt(
|
|
|
558
578
|
upstream.abort();
|
|
559
579
|
if (req.signal.aborted) return fail(499, "Client cancelled request", "client_cancelled");
|
|
560
580
|
const sendError = error instanceof UpstreamRetryEvidenceError ? error.cause : error;
|
|
561
|
-
if (
|
|
581
|
+
if (sendError instanceof SendBudgetExhaustedError) {
|
|
562
582
|
// A decision this process made, answered as the Responses path answers it: 429, not 502.
|
|
563
583
|
return fail(429, sendError.message, SEND_BUDGET_EXHAUSTED_CODE, SEND_BUDGET_EXHAUSTED_CODE);
|
|
564
584
|
}
|
|
@@ -80,7 +80,9 @@ import { upstreamWireForAdapter } from "../protocols/contract";
|
|
|
80
80
|
import { createProtocolEnvelope, type ProtocolEnvelope } from "../protocols/envelope";
|
|
81
81
|
import { featuresFromMessagesBody, type ProtocolFeature } from "../protocols/features";
|
|
82
82
|
import { credentialDomainFor, messagesBodyHasOpaqueState } from "../protocols/opaque-state";
|
|
83
|
-
import {
|
|
83
|
+
import { anthropicRoutingFor, anthropicSessionKeyFromParts } from "../oauth/anthropic-routing";
|
|
84
|
+
import { configuredAnthropicInstance } from "../providers/anthropic-instance";
|
|
85
|
+
import { messagesSelectorTargetsSecondaryInstance, messagesSecondaryInstanceUnavailable } from "./messages-native-selector";
|
|
84
86
|
import { checkRepresentable, unrepresentableMessage } from "../protocols/guard";
|
|
85
87
|
import { requestPathForLane } from "../protocols/path";
|
|
86
88
|
import { resolveApiSurfaceSettings, resolveProtocolSettings } from "../protocols/settings";
|
|
@@ -244,6 +246,7 @@ function wantsNativePassthrough(
|
|
|
244
246
|
): model is string {
|
|
245
247
|
if (cc?.nativePassthrough === false) return false;
|
|
246
248
|
if (typeof model !== "string" || !/^(claude|anthropic)/i.test(model)) return false;
|
|
249
|
+
if (messagesSelectorTargetsSecondaryInstance(config, model, cc)) return false;
|
|
247
250
|
// Authorization and x-api-key both belong to the upstream on this branch. An exposed listener
|
|
248
251
|
// therefore requires the dedicated admission header even though the routed Messages surface
|
|
249
252
|
// keeps accepting all three legacy admission forms.
|
|
@@ -961,6 +964,11 @@ async function handleClaudeMessagesWithBudget(
|
|
|
961
964
|
recordProtocolShadowPlan(logCtx, config, { inbound: "messages", model: requestedModel });
|
|
962
965
|
return await anthropicNativePassthrough(req, config, logCtx, logIds, anthropicBody, "/v1/messages");
|
|
963
966
|
}
|
|
967
|
+
if (isRec(anthropicBody) && typeof anthropicBody.model === "string"
|
|
968
|
+
&& messagesSecondaryInstanceUnavailable(config, anthropicBody.model, cc)) {
|
|
969
|
+
if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 401, { closeReason: "non_stream" });
|
|
970
|
+
return anthropicErrorResponse(401, "Configured Anthropic OAuth instance is unavailable", "authentication_error");
|
|
971
|
+
}
|
|
964
972
|
// Only Anthropic holds message-thread state; the error makes Claude Code resend the full turn.
|
|
965
973
|
if (carriesMessageThread(anthropicBody)) {
|
|
966
974
|
logCtx.errorCode = "claude_thread_unsupported";
|
|
@@ -1148,10 +1156,16 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1148
1156
|
|
|
1149
1157
|
// PF-08: a managed-key Anthropic route sends its Messages body natively. The caller-forward
|
|
1150
1158
|
// passthrough above was decided on the caller's own credential and never reaches this point.
|
|
1159
|
+
const settledInstance = configuredAnthropicInstance(config, settledRoute?.providerName);
|
|
1160
|
+
if (settledRoute?.providerName === "anthropic2" && settledRoute.provider.authMode === "oauth" && !settledInstance) {
|
|
1161
|
+
if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 401, { closeReason: "non_stream" });
|
|
1162
|
+
return anthropicErrorResponse(401, "Configured Anthropic OAuth instance is unavailable", "authentication_error");
|
|
1163
|
+
}
|
|
1151
1164
|
const nativeSelector: NativeMessagesSelector = {
|
|
1152
1165
|
effortRow: !!effortRow, fastRow: !!fastRow, routeSelector: String(internalBody.model ?? ""), claudeCode: cc,
|
|
1153
1166
|
// PF-10: a stored second OAuth account turns on the bridge's rotation, so it stays there.
|
|
1154
|
-
...(settledRoute?.provider.authMode === "oauth"
|
|
1167
|
+
...(settledRoute?.provider.authMode === "oauth" && settledInstance
|
|
1168
|
+
? { oauthFailoverQuorum: anthropicRoutingFor(settledInstance).hasAnthropicFailoverQuorum() } : {}),
|
|
1155
1169
|
};
|
|
1156
1170
|
const nativeDecline = settledRoute && isRec(anthropicBody)
|
|
1157
1171
|
? nativeMessagesDeclineReason(settledRoute, anthropicBody, config, nativeSelector)
|