@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
|
@@ -364,6 +364,7 @@ export async function deliverPassthroughResponse(
|
|
|
364
364
|
| "subagentFallbackAccountId"
|
|
365
365
|
| "clientRequestedStream"
|
|
366
366
|
| "translatorBudget"
|
|
367
|
+
| "inboundWire"
|
|
367
368
|
>,
|
|
368
369
|
transportState: Pick<ResponsesTransport, "requestBindings">,
|
|
369
370
|
sidecarState: Pick<ResponsesSidecarAuth, "openAiSidecar">,
|
|
@@ -428,7 +429,8 @@ export async function deliverPassthroughResponse(
|
|
|
428
429
|
normalizeFunctionCompletionJson,
|
|
429
430
|
} = nativeExchange;
|
|
430
431
|
const { commitReasoningReplayServingRoute, recordTerminalOutcomes } = responseEffects;
|
|
431
|
-
const { parsed, route, subagentQuotaFailureModel, clientRequestedStream, translatorBudget } = requestState;
|
|
432
|
+
const { parsed, route, subagentQuotaFailureModel, clientRequestedStream, translatorBudget, inboundWire } = requestState;
|
|
433
|
+
const enforceDeclaredToolNames = inboundWire !== "chat" && inboundWire !== "anthropic";
|
|
432
434
|
const { openAiSidecar } = sidecarState;
|
|
433
435
|
const { requestBindings } = transportState;
|
|
434
436
|
|
|
@@ -637,6 +639,7 @@ export async function deliverPassthroughResponse(
|
|
|
637
639
|
route.provider.webSearchBridge?.backend,
|
|
638
640
|
config,
|
|
639
641
|
openAiSidecar,
|
|
642
|
+
route.providerName,
|
|
640
643
|
);
|
|
641
644
|
const webSearchBridgePlan = planPassthroughWebSearchBridge(parsed, route.provider, {
|
|
642
645
|
providerName: route.providerName,
|
|
@@ -836,6 +839,7 @@ export async function deliverPassthroughResponse(
|
|
|
836
839
|
providerExecutedCallTypes,
|
|
837
840
|
declaredBareWireToolNames,
|
|
838
841
|
recoverableBareCustomWireToolNames,
|
|
842
|
+
enforceDeclaredToolNames,
|
|
839
843
|
)
|
|
840
844
|
: undefined,
|
|
841
845
|
grokUpstreamEchoEnabled
|
|
@@ -1293,7 +1297,7 @@ export async function deliverPassthroughResponse(
|
|
|
1293
1297
|
if (grokUpstreamEchoEnabled) {
|
|
1294
1298
|
clientJson = stripGrokUpstreamEnvelopeEchoFromResponsesJson(clientJson);
|
|
1295
1299
|
}
|
|
1296
|
-
// #1700: same
|
|
1300
|
+
// #1700: same inbound-wire refusal policy as the SSE relay above. Both the plain JSON answer and
|
|
1297
1301
|
// the reframed-SSE branch below are built from this body, so one check covers them. This
|
|
1298
1302
|
// runs BEFORE the continuation cache write below: a refused turn must not become state a
|
|
1299
1303
|
// later `previous_response_id` replay can expand from.
|
|
@@ -1312,7 +1316,7 @@ export async function deliverPassthroughResponse(
|
|
|
1312
1316
|
return undefined;
|
|
1313
1317
|
}
|
|
1314
1318
|
})();
|
|
1315
|
-
if (undeclared !== undefined) {
|
|
1319
|
+
if (enforceDeclaredToolNames && undeclared !== undefined) {
|
|
1316
1320
|
return formatErrorResponse(502, "upstream_error", undeclaredToolCallMessage(undeclared));
|
|
1317
1321
|
}
|
|
1318
1322
|
clientJson = normalizeDefaultNamespaceInJson(
|
|
@@ -163,6 +163,7 @@ import { ambiguousResendAllowanceFor, selfContainedResponsesBody } from "./reset
|
|
|
163
163
|
import { upstreamErrorMessageFromPayload, ENCRYPTED_FUNCTION_OUTPUT_REJECTION } from "../../lib/errors";
|
|
164
164
|
import { isTransientConsoleGoUploadRejection } from "../../providers/opencode-zen-rate-limit";
|
|
165
165
|
import { planReasoningEffortDowngrade } from "../../providers/reasoning-metadata";
|
|
166
|
+
import { unboundPoolSpendRefusalResponse } from "../workflow-refusal";
|
|
166
167
|
|
|
167
168
|
/** Prepares and recovers one native Responses exchange before client commitment. */
|
|
168
169
|
export async function preparePassthroughExchange(
|
|
@@ -214,6 +215,7 @@ export async function preparePassthroughExchange(
|
|
|
214
215
|
ResponsesSendBudget,
|
|
215
216
|
| "remainingTransientSendBudget"
|
|
216
217
|
| "noteTransientSends"
|
|
218
|
+
| "transientSendReporter"
|
|
217
219
|
| "recoverySendAllowance"
|
|
218
220
|
| "recoveryClassFor"
|
|
219
221
|
| "sendBudgetExhausted"
|
|
@@ -249,6 +251,7 @@ export async function preparePassthroughExchange(
|
|
|
249
251
|
const {
|
|
250
252
|
remainingTransientSendBudget,
|
|
251
253
|
noteTransientSends,
|
|
254
|
+
transientSendReporter,
|
|
252
255
|
recoverySendAllowance,
|
|
253
256
|
recoveryClassFor,
|
|
254
257
|
sendBudgetExhausted,
|
|
@@ -794,7 +797,10 @@ export async function preparePassthroughExchange(
|
|
|
794
797
|
*/
|
|
795
798
|
const sendAmbiguousReplacement = (
|
|
796
799
|
signal: AbortSignal = upstream.signal,
|
|
797
|
-
): Promise<Response> =>
|
|
800
|
+
): Promise<Response> => {
|
|
801
|
+
const report = transientSendReporter();
|
|
802
|
+
let started = false;
|
|
803
|
+
const run = () => fetchWithHeaderTimeout(
|
|
798
804
|
request.url,
|
|
799
805
|
applyUpstreamRecoveryInit({
|
|
800
806
|
method: request.method,
|
|
@@ -829,11 +835,17 @@ export async function preparePassthroughExchange(
|
|
|
829
835
|
// Charged to the SAME request counter every other send goes through. The
|
|
830
836
|
// replacement is bought here rather than by a nested retry helper, so there is
|
|
831
837
|
// one charge for one send and no per-layer counter to reconcile.
|
|
832
|
-
|
|
838
|
+
if (report.beforeSend?.() === false) throw new SendBudgetExhaustedError(safeHostLabel(request.url));
|
|
839
|
+
started = true;
|
|
833
840
|
},
|
|
834
841
|
}),
|
|
835
842
|
route.provider.authMode === "forward",
|
|
836
843
|
);
|
|
844
|
+
return (report.execute ? report.execute(run) : run()).finally(() => {
|
|
845
|
+
try { if (started) report(1); }
|
|
846
|
+
finally { report.close?.(); }
|
|
847
|
+
});
|
|
848
|
+
};
|
|
837
849
|
/**
|
|
838
850
|
* Refuse a built body that exceeds the operator's configured ceiling, before it is sent.
|
|
839
851
|
*
|
|
@@ -891,7 +903,8 @@ export async function preparePassthroughExchange(
|
|
|
891
903
|
releaseUpstreamHostAdmission(nativeHostState.lease);
|
|
892
904
|
nativeHostState.lease = null;
|
|
893
905
|
releaseCodexAuthContextProbeLease(admissionState.authCtx);
|
|
894
|
-
return
|
|
906
|
+
return unboundPoolSpendRefusalResponse(logCtx)
|
|
907
|
+
?? formatErrorResponse(429, "request_send_budget_exhausted", err.message);
|
|
895
908
|
}
|
|
896
909
|
const refusal = unwrapUpstreamRetryEvidenceError(err);
|
|
897
910
|
// Pacing may outlive the selected account's admission. No fetch occurred, so do
|
|
@@ -991,7 +1004,7 @@ export async function preparePassthroughExchange(
|
|
|
991
1004
|
.then(adoptObservedResponse);
|
|
992
1005
|
},
|
|
993
1006
|
{ abortSignal: upstream.signal, label: safeHostLabel(request.url),
|
|
994
|
-
attempts: remainingTransientSendBudget(transientSendAttempts()), onSendsConsumed:
|
|
1007
|
+
attempts: remainingTransientSendBudget(transientSendAttempts()), onSendsConsumed: transientSendReporter(),
|
|
995
1008
|
claimAmbiguousResend: claimPreHeaderResend,
|
|
996
1009
|
},
|
|
997
1010
|
);
|
|
@@ -1094,7 +1107,7 @@ export async function preparePassthroughExchange(
|
|
|
1094
1107
|
.then(adoptObservedResponse);
|
|
1095
1108
|
},
|
|
1096
1109
|
{ abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: allowance.attempts,
|
|
1097
|
-
onSendsConsumed:
|
|
1110
|
+
onSendsConsumed: transientSendReporter(allowance.permit), claimAmbiguousResend: claimPreHeaderResend },
|
|
1098
1111
|
);
|
|
1099
1112
|
} catch (err) {
|
|
1100
1113
|
return { failed: transportFailureResponse(err) };
|
|
@@ -1226,7 +1239,7 @@ export async function preparePassthroughExchange(
|
|
|
1226
1239
|
},
|
|
1227
1240
|
{ abortSignal: upstream.signal, label: safeHostLabel(request.url),
|
|
1228
1241
|
attempts: remainingTransientSendBudget(transientSendAttempts()),
|
|
1229
|
-
onSendsConsumed:
|
|
1242
|
+
onSendsConsumed: transientSendReporter(), claimAmbiguousResend: claimPreHeaderResend },
|
|
1230
1243
|
);
|
|
1231
1244
|
} catch (err) {
|
|
1232
1245
|
return transportFailureResponse(err);
|
|
@@ -1354,7 +1367,7 @@ export async function preparePassthroughExchange(
|
|
|
1354
1367
|
.then(adoptObservedResponse);
|
|
1355
1368
|
},
|
|
1356
1369
|
{ abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: remainingTransientSendBudget(transientSendAttempts()),
|
|
1357
|
-
onSendsConsumed:
|
|
1370
|
+
onSendsConsumed: transientSendReporter(), claimAmbiguousResend: claimPreHeaderResend },
|
|
1358
1371
|
);
|
|
1359
1372
|
} catch (err) {
|
|
1360
1373
|
return transportFailureResponse(err);
|
|
@@ -1489,7 +1502,7 @@ export async function preparePassthroughExchange(
|
|
|
1489
1502
|
.then(adoptObservedResponse);
|
|
1490
1503
|
},
|
|
1491
1504
|
{ abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: remainingTransientSendBudget(transientSendAttempts()),
|
|
1492
|
-
onSendsConsumed:
|
|
1505
|
+
onSendsConsumed: transientSendReporter(), claimAmbiguousResend: claimPreHeaderResend },
|
|
1493
1506
|
);
|
|
1494
1507
|
} catch (err) {
|
|
1495
1508
|
return transportFailureResponse(err);
|
|
@@ -64,6 +64,7 @@ import type { OcxParsedRequest } from "../../types";
|
|
|
64
64
|
import { buildToolBridgeMaps } from "./collaboration";
|
|
65
65
|
import { parseRequest } from "../../responses/parser";
|
|
66
66
|
import { anthropicSessionKeyFromParts } from "../../oauth/anthropic-routing";
|
|
67
|
+
import { configuredAnthropicInstance } from "../../providers/anthropic-instance";
|
|
67
68
|
import { isTranslatorBudgetExceededError } from "../../lib/translator-budget";
|
|
68
69
|
import { bindTurnTerminationScope, rememberDeliveredFinalAnswer } from "../../responses/turn-termination";
|
|
69
70
|
import { observeCacheDiagnosticInbound, rebindCacheDiagnosticBody, requestLogSpeedLabel, readConfiguredCodexServiceTier } from "../request-log";
|
|
@@ -74,6 +75,7 @@ import {
|
|
|
74
75
|
routeCompactionModel,
|
|
75
76
|
routeModel,
|
|
76
77
|
NoEligiblePolicyCandidateError,
|
|
78
|
+
AnthropicSecondaryInstanceUnavailableError,
|
|
77
79
|
} from "../../router";
|
|
78
80
|
import { evidenceFromBody } from "../../routing/request-evidence";
|
|
79
81
|
import { OPENAI_CODEX_PROVIDER_ID, isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
@@ -522,6 +524,23 @@ export async function prepareResponsesRequest(
|
|
|
522
524
|
// or a subagent fallback rewrites it, so a refusal names the client's own
|
|
523
525
|
// request rather than a destination it never asked for.
|
|
524
526
|
const inboundSelector = parsed.modelId;
|
|
527
|
+
const selectorTargetsSecondary = (selector: string): boolean => {
|
|
528
|
+
const slash = selector.indexOf("/");
|
|
529
|
+
if (slash <= 0) return false;
|
|
530
|
+
const qualifier = selector.slice(0, slash);
|
|
531
|
+
// Match router precedence: exact custom keys (including uppercase names) win over aliases.
|
|
532
|
+
if (Object.hasOwn(config.providers, qualifier)) return qualifier === "anthropic2";
|
|
533
|
+
return qualifier === "anthropic2"
|
|
534
|
+
|| config.providers.anthropic2?.alias?.trim().toLowerCase() === qualifier.toLowerCase();
|
|
535
|
+
};
|
|
536
|
+
const explicitlyRequestsSecondary = selectorTargetsSecondary(inboundSelector);
|
|
537
|
+
const secondaryProvider = config.providers.anthropic2;
|
|
538
|
+
// Reserve an explicit B selector before an absent row can default-route it to A.
|
|
539
|
+
// Combo parents have already dispatched above; concrete children retain their own target.
|
|
540
|
+
if (explicitlyRequestsSecondary && (!secondaryProvider || secondaryProvider.disabled === true
|
|
541
|
+
|| secondaryProvider.authMode === "oauth" && configuredAnthropicInstance(config, "anthropic2") !== "anthropic2")) {
|
|
542
|
+
return formatErrorResponse(401, "authentication_error", "Anthropic Pool 2 requires an enabled configured provider");
|
|
543
|
+
}
|
|
525
544
|
const admissionScope = resolveAdmissionModelScope(config, options.admission);
|
|
526
545
|
const captureInboundRoutePolicy = (candidate: RouteResult, captureRequestPolicy = true): RouteResult => {
|
|
527
546
|
// Every route this request path produces passes through here: the direct
|
|
@@ -553,11 +572,21 @@ export async function prepareResponsesRequest(
|
|
|
553
572
|
// no canonical OpenAI route for (#2901). Only the initial compaction route
|
|
554
573
|
// may fall back to the configured default provider; combo attempts and the
|
|
555
574
|
// later fallback/recovery re-routes keep the ordinary reservation.
|
|
556
|
-
const routeInbound = (modelId: string) =>
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
575
|
+
const routeInbound = (modelId: string) => {
|
|
576
|
+
const resolved = concreteSelection
|
|
577
|
+
? routeConcreteModel(config, modelId)
|
|
578
|
+
: parsed._compactionRequest === true
|
|
579
|
+
? routeCompactionModel(config, modelId, evidenceFromBody(parsed._rawBody))
|
|
580
|
+
: routeModel(config, modelId, evidenceFromBody(parsed._rawBody));
|
|
581
|
+
// A Pool 2 selector may leave Pool 2 only through an operator-configured cross-provider
|
|
582
|
+
// blocked-model redirect, which the router marks as a credential-domain rewrite. Any other
|
|
583
|
+
// destination means the selector fell through to default resolution, which stays refused.
|
|
584
|
+
const operatorRedirect = resolved.routeReason === "blocked-model-redirect" && resolved.credentialDomainRewrite === true;
|
|
585
|
+
if (selectorTargetsSecondary(modelId) && resolved.providerName !== "anthropic2" && !operatorRedirect) {
|
|
586
|
+
throw new Error("Anthropic Pool 2 selector cannot use the default provider");
|
|
587
|
+
}
|
|
588
|
+
return resolved;
|
|
589
|
+
};
|
|
561
590
|
const resolveRoute = (modelId: string) => captureInboundRoutePolicy(routeInbound(modelId));
|
|
562
591
|
// The phase's destination. Resolved through the admission-scoped resolver every other route
|
|
563
592
|
// uses, and it fails closed exactly like the shadow target: falling back to the native model
|
|
@@ -644,6 +673,9 @@ export async function prepareResponsesRequest(
|
|
|
644
673
|
logCtx.routeDecision = route.routeDecision;
|
|
645
674
|
logCtx.policyEligibility = route.policyEligibility;
|
|
646
675
|
} catch (err) {
|
|
676
|
+
if (err instanceof AnthropicSecondaryInstanceUnavailableError) {
|
|
677
|
+
return formatErrorResponse(401, "authentication_error", err.message);
|
|
678
|
+
}
|
|
647
679
|
const policyRefusal = policyCandidateRefusalResponse(err);
|
|
648
680
|
if (policyRefusal) return policyRefusal;
|
|
649
681
|
if (err instanceof AdmissionModelDeniedError) return admissionModelDeniedResponse(err);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import { createInferenceSendBudget } from "../inference/context";
|
|
1
2
|
import type { ResponsesRequestContext } from "./core-options";
|
|
2
|
-
import { createRequestExecutionBudget, isRequestExecutionBudget } from "../../lib/request-execution-budget";
|
|
3
|
+
import { claimDispatchSpendProof, createPhysicalSendReporter, createRequestExecutionBudget, isRequestExecutionBudget, reportDispatchSends } from "../../lib/request-execution-budget";
|
|
3
4
|
import {
|
|
4
5
|
chargeWorkflowSends,
|
|
5
6
|
workflowSendCeilingReached,
|
|
6
7
|
workflowSpendCeilingReached,
|
|
7
8
|
} from "../../lib/workflow-budget";
|
|
8
|
-
import { workflowRefusalResponse } from "../workflow-refusal";
|
|
9
|
+
import { poolContinuityRefusalReason, workflowRefusalResponse } from "../workflow-refusal";
|
|
9
10
|
import type { AttemptRecoveryKind, AttemptRecoveryWithheld } from "../../usage/log";
|
|
10
11
|
import { noteAttemptRecoveryWithheld, noteAttemptSend } from "../request-log";
|
|
11
12
|
import { TRANSIENT_RETRY_MAX_ATTEMPTS } from "../../lib/upstream-retry";
|
|
@@ -52,17 +53,27 @@ export function createResponsesSendBudget(
|
|
|
52
53
|
// fresh default of 3. It is now a holder carried on options, so a combo child inherits the
|
|
53
54
|
// parent's spend instead of starting over per target -- both halves of the measured
|
|
54
55
|
// amplification in #4546.
|
|
55
|
-
const sendBudget = options.sendBudget ??
|
|
56
|
+
const sendBudget = options.sendBudget ?? createInferenceSendBudget(req, logCtx);
|
|
56
57
|
// The root workflow is the user-visible task. A per-request cap cannot bound a fan-out that
|
|
57
58
|
// sends once per child seven hundred times, so every send charged to the request is charged
|
|
58
59
|
// to the root as well (#4546).
|
|
59
60
|
const workflowRootId = req.headers.get("x-codex-parent-thread-id")?.trim() || undefined;
|
|
60
|
-
const
|
|
61
|
+
const inheritedPermit = options.compactionRecoveryPermit ?? options.comboDispatchPermit;
|
|
62
|
+
let pendingHopPermit: SingleUseDispatchPermit | undefined = options.compactionRecoveryPermit;
|
|
63
|
+
const recordTransientSends = (used: number, permit?: SingleUseDispatchPermit): void => {
|
|
61
64
|
const charged = Math.max(0, used);
|
|
62
|
-
sendBudget
|
|
65
|
+
reportDispatchSends(sendBudget, charged, permit);
|
|
63
66
|
options.onCompactionRecoverySendsReported?.(charged);
|
|
64
67
|
chargeWorkflowSends(workflowRootId, charged);
|
|
65
68
|
};
|
|
69
|
+
const noteTransientSends = (used: number): void => recordTransientSends(used, pendingHopPermit ?? inheritedPermit);
|
|
70
|
+
// Capture at helper creation, before another leg can replace the pending handoff.
|
|
71
|
+
const transientSendReporter = (permit = pendingHopPermit ?? inheritedPermit) =>
|
|
72
|
+
createPhysicalSendReporter(sendBudget, () => ({ poolId: logCtx.spendPoolId ?? logCtx.provider,
|
|
73
|
+
identityId: logCtx.accountLogLabel }), permit, charged => {
|
|
74
|
+
options.onCompactionRecoverySendsReported?.(charged);
|
|
75
|
+
chargeWorkflowSends(workflowRootId, charged);
|
|
76
|
+
});
|
|
66
77
|
// Refused before any dispatch, and deliberately not by evicting the root's ledger entry:
|
|
67
78
|
// dropping the record to make room would hand the fan-out a fresh allowance, which is the
|
|
68
79
|
// laundering this ceiling exists to stop. The client is told the task needs a new grant
|
|
@@ -78,7 +89,17 @@ export function createResponsesSendBudget(
|
|
|
78
89
|
// returns -- a refusal an operator cannot tell from an ordinary budget exhaustion, on a
|
|
79
90
|
// ceiling they configured themselves. Asked before dispatch, it names the scope and the
|
|
80
91
|
// number instead. Returns undefined and touches no ledger when no ceiling is configured.
|
|
81
|
-
|
|
92
|
+
// Passthrough reports sends after they leave. Invalid or conflicting identity evidence
|
|
93
|
+
// still refuses here as well as in reserve(), including requests with no workflow root.
|
|
94
|
+
// Positive unbound balances alone are instead overlaid on each candidate pool below.
|
|
95
|
+
const continuityRefusal = poolContinuityRefusalReason();
|
|
96
|
+
if (continuityRefusal) {
|
|
97
|
+
return workflowRefusalResponse(continuityRefusal, logCtx, undefined, workflowRootId);
|
|
98
|
+
}
|
|
99
|
+
const prepaid = isRequestExecutionBudget(sendBudget)
|
|
100
|
+
? claimDispatchSpendProof(sendBudget, options.compactionRecoveryPermit ?? options.comboDispatchPermit) : undefined;
|
|
101
|
+
const admissionPolicy = isRequestExecutionBudget(sendBudget) ? sendBudget.spendAdmissionPolicy : undefined;
|
|
102
|
+
const spentCeiling = workflowSpendCeilingReached(workflowRootId, undefined, logCtx.spendPoolId ?? logCtx.provider, prepaid, admissionPolicy);
|
|
82
103
|
if (spentCeiling) {
|
|
83
104
|
return workflowRefusalResponse(
|
|
84
105
|
"workflow-spend-exhausted",
|
|
@@ -157,7 +178,6 @@ export function createResponsesSendBudget(
|
|
|
157
178
|
* refused and the request would answer with a synthetic 502 in place of the real 429 the hop
|
|
158
179
|
* was recovering from.
|
|
159
180
|
*/
|
|
160
|
-
let pendingHopPermit: SingleUseDispatchPermit | undefined = options.compactionRecoveryPermit;
|
|
161
181
|
/**
|
|
162
182
|
* The budget an adapter's OWN dispatch ladder reserves against.
|
|
163
183
|
*
|
|
@@ -171,19 +191,21 @@ export function createResponsesSendBudget(
|
|
|
171
191
|
* The hop hands its reservation down through `pendingHopPermit`, the same seam the
|
|
172
192
|
* passthrough ladder already uses, and this view spends it on the adapter's FIRST
|
|
173
193
|
* reservation. Every later send in that ladder is a new physical send and is charged
|
|
174
|
-
* normally.
|
|
175
|
-
*
|
|
176
|
-
*
|
|
194
|
+
* normally. The refundable view below keeps an unused hop open until executor invocation;
|
|
195
|
+
* its adapter release hands back an abandoned replay. The ordinary view retains early
|
|
196
|
+
* settlement for existing callers.
|
|
177
197
|
*/
|
|
198
|
+
const claimHopPermit = (): SingleUseDispatchPermit | undefined => {
|
|
199
|
+
const permit = pendingHopPermit;
|
|
200
|
+
pendingHopPermit = undefined;
|
|
201
|
+
return permit;
|
|
202
|
+
};
|
|
178
203
|
const adapterDispatchBudget: RequestExecutionBudget | undefined = adapterSendBudget === undefined
|
|
179
|
-
? undefined
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return permit;
|
|
185
|
-
},
|
|
186
|
-
});
|
|
204
|
+
? undefined : adapterDispatchBudgetView(adapterSendBudget, { claimHopPermit });
|
|
205
|
+
// Opt in only where the adapter's executor is the confirmation boundary. Ordinary callers
|
|
206
|
+
// keep the existing early settlement contract, including already-settled permit fallback.
|
|
207
|
+
const refundableAdapterDispatchBudget: RequestExecutionBudget | undefined = adapterSendBudget === undefined
|
|
208
|
+
? undefined : adapterDispatchBudgetView(adapterSendBudget, { claimHopPermit }, true);
|
|
187
209
|
/**
|
|
188
210
|
* How many sends a recovery leg may make, and the permit that authorises the last one.
|
|
189
211
|
*
|
|
@@ -268,6 +290,7 @@ export function createResponsesSendBudget(
|
|
|
268
290
|
return {
|
|
269
291
|
workflowRootId,
|
|
270
292
|
noteTransientSends,
|
|
293
|
+
transientSendReporter,
|
|
271
294
|
remainingTransientSendBudget,
|
|
272
295
|
/**
|
|
273
296
|
* Physical sends this logical request has already made.
|
|
@@ -279,6 +302,7 @@ export function createResponsesSendBudget(
|
|
|
279
302
|
get sendsUsed(): number { return sendBudget.used; },
|
|
280
303
|
adapterSendBudget,
|
|
281
304
|
adapterDispatchBudget,
|
|
305
|
+
refundableAdapterDispatchBudget,
|
|
282
306
|
noteAdapterPhysicalSend,
|
|
283
307
|
noteAdapterRecoveryWithheld,
|
|
284
308
|
sendBudgetExhausted,
|
|
@@ -312,8 +336,17 @@ export type ResponsesSendBudget = Exclude<ReturnType<typeof createResponsesSendB
|
|
|
312
336
|
function adapterDispatchBudgetView(
|
|
313
337
|
budget: RequestExecutionBudget,
|
|
314
338
|
hop: { claimHopPermit: () => SingleUseDispatchPermit | undefined },
|
|
339
|
+
refundableHop = false,
|
|
315
340
|
): RequestExecutionBudget {
|
|
316
341
|
return {
|
|
342
|
+
get physicalStarted() { return budget.physicalStarted; },
|
|
343
|
+
get physicalLimit() { return budget.physicalLimit; },
|
|
344
|
+
get spendEnforced() { return budget.spendEnforced; },
|
|
345
|
+
get spendPolicyStarted() { return budget.spendPolicyStarted; },
|
|
346
|
+
get spendAdmissionPolicy() { return budget.spendAdmissionPolicy; },
|
|
347
|
+
startRequest: target => budget.startRequest?.(target),
|
|
348
|
+
claimPhysicalSend: () => budget.claimPhysicalSend?.(),
|
|
349
|
+
beginSpendProducer: () => budget.beginSpendProducer?.(),
|
|
317
350
|
get used(): number { return budget.used; },
|
|
318
351
|
set used(next: number) { budget.used = next; },
|
|
319
352
|
logicalRequestId: budget.logicalRequestId,
|
|
@@ -327,6 +360,7 @@ function adapterDispatchBudgetView(
|
|
|
327
360
|
claimAmbiguousResend: (limit: number): boolean => budget.claimAmbiguousResend?.(limit) === true,
|
|
328
361
|
get ambiguousResendSpent(): boolean { return budget.ambiguousResendSpent === true; },
|
|
329
362
|
reserveDispatch(intent: DispatchIntent): DispatchDecision {
|
|
363
|
+
const refundable = refundableHop || budget.spendEnforced === true;
|
|
330
364
|
// A dispatch whose upstream state is unknown is refused on its own merits. A hop that
|
|
331
365
|
// already paid does not make an unsafe replay safe, so that check stays with the budget.
|
|
332
366
|
if (intent.replaySafe !== false) {
|
|
@@ -338,30 +372,33 @@ function adapterDispatchBudgetView(
|
|
|
338
372
|
hopPermit.release();
|
|
339
373
|
return budget.reserveDispatch({ ...intent, sendClass: hopPermit.sendClass });
|
|
340
374
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
375
|
+
if (hopPermit !== undefined && (refundable || hopPermit.assumeCharge())) {
|
|
376
|
+
let settled = false;
|
|
377
|
+
let executionPermit = hopPermit;
|
|
378
|
+
const confirm = (): boolean => {
|
|
379
|
+
if (settled) return false;
|
|
380
|
+
settled = true;
|
|
381
|
+
// Admission can still reject this replay. Close its external booking only when
|
|
382
|
+
// the adapter invokes its executor, leaving release() able to refund it until then.
|
|
383
|
+
if (!refundable || hopPermit.assumeCharge()) return true;
|
|
384
|
+
// Preserve the old fallback for a permit another leg already settled.
|
|
385
|
+
const fresh = budget.reserveDispatch(intent);
|
|
386
|
+
if (!fresh.allowed) return false;
|
|
387
|
+
executionPermit = fresh.permit;
|
|
388
|
+
return fresh.permit.assumeCharge();
|
|
389
|
+
};
|
|
348
390
|
return {
|
|
349
391
|
allowed: true,
|
|
350
392
|
permit: {
|
|
351
393
|
sendClass: hopPermit.sendClass,
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
spent = true;
|
|
360
|
-
return true;
|
|
394
|
+
execute: run => executionPermit.execute ? executionPermit.execute(run) : run(),
|
|
395
|
+
use: confirm,
|
|
396
|
+
assumeCharge: confirm,
|
|
397
|
+
release: (): void => {
|
|
398
|
+
if (!refundable || settled) return;
|
|
399
|
+
settled = true;
|
|
400
|
+
hopPermit.release();
|
|
361
401
|
},
|
|
362
|
-
// The hop's charge is already settled and belongs to the leg that asked for it,
|
|
363
|
-
// so there is nothing here to refund.
|
|
364
|
-
release: (): void => {},
|
|
365
402
|
},
|
|
366
403
|
};
|
|
367
404
|
}
|