@bitkyc08/opencodex 2.76.0 → 2.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/App-COdEeqSj.css +1 -0
- package/gui/dist/assets/App-DEpCJEP_.js +54 -0
- package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-gloIYNaW.js} +1 -1
- package/gui/dist/assets/index-Cd7ittMv.css +1 -0
- package/gui/dist/assets/index-DwJKFJm5.js +94 -0
- package/gui/dist/assets/quota-summary-DGcEJU3h.js +1 -0
- package/gui/dist/index.html +4 -3
- package/package.json +1 -1
- package/src/adapters/antigravity-refusal.ts +28 -0
- package/src/adapters/cursor/protobuf-request.ts +6 -0
- package/src/adapters/cursor/request-builder.ts +3 -0
- package/src/adapters/cursor/structured-output.ts +13 -0
- package/src/adapters/cursor/types.ts +2 -0
- package/src/adapters/ollama-native.ts +29 -2
- package/src/adapters/openai-responses/passthrough.ts +133 -103
- package/src/adapters/openai-responses/request-strips.ts +10 -4
- package/src/adapters/zed.ts +26 -4
- package/src/bridge/response-json.ts +14 -14
- package/src/bridge/sse.ts +15 -5
- package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
- package/src/claude/agents-inject.ts +4 -43
- package/src/claude/gateway-cache.ts +16 -13
- package/src/claude/inbound-model-options.ts +9 -2
- package/src/claude/intercept/cli-catalog.ts +27 -16
- package/src/claude/intercept/picker-bootstrap.ts +37 -16
- package/src/claude/intercept/picker-budget.ts +41 -0
- package/src/claude/outbound.ts +2 -0
- package/src/claude/subagent-force-status.ts +53 -0
- package/src/claude/subagent-model.ts +93 -0
- package/src/cli/account-api.ts +3 -0
- package/src/cli/account.ts +3 -1
- package/src/cli/agent.ts +10 -0
- package/src/cli/capabilities.ts +9 -0
- package/src/cli/chatgpt-command.ts +10 -7
- package/src/cli/claude.ts +22 -6
- package/src/cli/codex-shim-readiness.ts +89 -0
- package/src/cli/connect.ts +19 -0
- package/src/cli/dispatch.ts +3 -3
- package/src/cli/export-command.ts +20 -9
- package/src/cli/gui.ts +8 -3
- package/src/cli/help-catalog.ts +84 -0
- package/src/cli/help-models-context.ts +17 -0
- package/src/cli/help-navigation.ts +50 -0
- package/src/cli/help-recovery.ts +66 -0
- package/src/cli/help.ts +52 -8
- package/src/cli/models-runtime.ts +2 -1
- package/src/cli/opencode.ts +54 -10
- package/src/cli/provider.ts +3 -29
- package/src/cli/registry.ts +29 -5
- package/src/cli/root.ts +30 -9
- package/src/cli/runtime-api.ts +1 -1
- package/src/cli/tray-proxy.ts +3 -2
- package/src/clients/config-export/contracts.ts +100 -0
- package/src/clients/config-export/droid.ts +54 -2
- package/src/clients/config-export/kilo.ts +12 -2
- package/src/clients/config-export/model-metadata.ts +10 -4
- package/src/clients/config-export/reasoning-metadata.ts +47 -0
- package/src/clients/config-export.ts +46 -66
- package/src/clients/omo-role-models.ts +17 -3
- package/src/codex/account-credit-use.ts +4 -2
- package/src/codex/account-runtime-state.ts +21 -0
- package/src/codex/account-store.ts +11 -47
- package/src/codex/account-usability.ts +11 -8
- package/src/codex/auth-api/account-list.ts +73 -63
- package/src/codex/auth-api/main-account-probe.ts +25 -12
- package/src/codex/auth-api/pool-mode-gate.ts +6 -9
- package/src/codex/auth-api/pool-quota-probe.ts +2 -4
- package/src/codex/auth-api/reset-credit-service.ts +3 -2
- package/src/codex/auth-context.ts +30 -11
- package/src/codex/catalog/aggregation.ts +3 -0
- package/src/codex/catalog/model-hints.ts +2 -1
- package/src/codex/catalog/model-visibility.ts +6 -3
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +13 -4
- package/src/codex/catalog/retained-sync.ts +2 -1
- package/src/codex/chatgpt-refresh-failure.ts +56 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/main-account.ts +144 -11
- package/src/codex/quota-refresh-outcome.ts +17 -2
- package/src/codex/quota-types.ts +33 -1
- package/src/codex/quota.ts +56 -15
- package/src/codex/routing/cooldown-math.ts +10 -4
- package/src/codex/routing/selection.ts +11 -4
- package/src/codex/routing.ts +9 -9
- package/src/codex/shim-path-resolution.ts +133 -0
- package/src/codex/shim.ts +39 -39
- package/src/codex/subagent-model-fallback.ts +2 -1
- package/src/combos/failover.ts +82 -1
- package/src/combos/jev.ts +13 -3
- package/src/config/diagnostics.ts +4 -0
- package/src/config/load-degrade.ts +4 -0
- package/src/config/provider-validation.ts +3 -0
- package/src/config/schema/compaction-triggers.ts +8 -0
- package/src/config/schema/config-schema.ts +11 -0
- package/src/config/schema/leaf-validators.ts +5 -1
- package/src/config/subagent-models.ts +12 -0
- package/src/generated/compatibility-version.json +245 -153
- package/src/grok/session-identity.ts +33 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +4 -2
- package/src/integrations/state.ts +68 -6
- package/src/integrations/writer.ts +18 -4
- package/src/lib/gui-pair-capability.ts +17 -0
- package/src/lib/provider-client-headers.ts +62 -0
- package/src/oauth/anthropic-continuity.ts +65 -0
- package/src/oauth/anthropic-identity.ts +75 -0
- package/src/oauth/anthropic-routing.ts +3 -3
- package/src/oauth/anthropic.ts +7 -1
- package/src/oauth/generic-account-failover.ts +21 -0
- package/src/oauth/health.ts +32 -5
- package/src/oauth/index.ts +36 -27
- package/src/oauth/local-token-detect.ts +7 -0
- package/src/oauth/refresh-policy.ts +2 -0
- package/src/oauth/store.ts +55 -6
- package/src/oauth/types.ts +9 -0
- package/src/protocols/encoders/messages.ts +4 -3
- package/src/providers/antigravity-effort-families.ts +75 -0
- package/src/providers/antigravity-models.ts +54 -16
- package/src/providers/antigravity-wire-snapshot.ts +66 -0
- package/src/providers/derive.ts +2 -0
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/new-model-policy.ts +48 -10
- package/src/providers/registry/entries-extended.ts +3 -0
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/model-seeds.ts +5 -0
- package/src/providers/registry/types.ts +4 -0
- package/src/responses/compaction.ts +19 -0
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +9 -0
- package/src/server/chat-completions.ts +10 -0
- package/src/server/claude-messages.ts +18 -11
- package/src/server/droid-reasoning-default.ts +50 -0
- package/src/server/github-copilot-responses-repair.ts +3 -6
- package/src/server/grok-responses-control-frame.ts +2 -2
- package/src/server/grok-responses-snapshot-repair.ts +4 -5
- package/src/server/gui-session.ts +33 -11
- package/src/server/index/claude-intercept-lifecycle.ts +4 -0
- package/src/server/index/serve-options.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +6 -129
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/integration-routes.ts +64 -6
- package/src/server/management/link-routes.ts +20 -13
- package/src/server/management/model-row-export-metadata.ts +150 -0
- package/src/server/management/model-rows.ts +148 -9
- package/src/server/management/route-registry.ts +4 -3
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/subagent-model-routes.ts +214 -0
- package/src/server/relay.ts +14 -44
- package/src/server/request-log-generation-window.ts +38 -0
- package/src/server/request-log.ts +13 -0
- package/src/server/responses/adapter-dispatch.ts +100 -1
- package/src/server/responses/codex-auth-error.ts +10 -2
- package/src/server/responses/combo-stream-preflight.ts +29 -3
- package/src/server/responses/compaction-routing.ts +10 -1
- package/src/server/responses/core-auth.ts +57 -26
- package/src/server/responses/core-codex-account.ts +6 -3
- package/src/server/responses/core-combo-failure.ts +5 -1
- package/src/server/responses/core-combo-native.ts +7 -1
- package/src/server/responses/core-combo.ts +18 -1
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/fetch-helpers.ts +9 -0
- package/src/server/responses/passthrough-delivery.ts +1 -1
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/request-prepare.ts +5 -0
- package/src/server/responses/request-spend.ts +30 -30
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/responses-hosted-image-display.ts +70 -9
- package/src/server/responses-terminal-repair.ts +15 -2
- package/src/server/sse-frame-buffer.ts +38 -36
- package/src/server/sse-payload-rewrite.ts +78 -49
- package/src/server/ws-bridge.ts +1 -11
- package/src/service/windows-taskxml.ts +1 -2
- package/src/service-manager-probe.ts +36 -2
- package/src/types/config.ts +4 -0
- package/src/types/provider.ts +18 -0
- package/src/update/npm-invocation.mjs +35 -3
- package/src/update/proxy-liveness-probe.mjs +38 -8
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +7 -0
- package/src/usage/summary.ts +82 -1
- package/src/web-search/loop.ts +11 -8
- package/src/web-search/run-turn-loop.ts +10 -7
- package/gui/dist/assets/App-CddorHdA.js +0 -51
- package/gui/dist/assets/App-DMudtISW.css +0 -1
- package/gui/dist/assets/index-Btze1I3T.css +0 -1
- package/gui/dist/assets/index-CvnY7E6i.js +0 -86
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
|
@@ -30,9 +30,15 @@ export function codexModelAvailabilityErrorResponse(error: CodexModelAvailabilit
|
|
|
30
30
|
return formatErrorResponse(400, "invalid_request_error", error.message);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/** Actionable sign-in guidance without guessing which upstream revocation cause occurred. */
|
|
34
|
+
export const CODEX_MAIN_SIGN_IN_REQUIRED_MESSAGE =
|
|
35
|
+
"Codex main account needs sign-in. Run `codex login` to sign in again.";
|
|
36
|
+
|
|
33
37
|
export function nativeMainRefreshFailureResponse(error: unknown): Response {
|
|
34
38
|
if (error instanceof MainAccountTokenRefreshError && error.reason === "reauth") {
|
|
35
|
-
|
|
39
|
+
// Same sentence the quarantine produces on every later request, so the first refusal and the
|
|
40
|
+
// ones after it do not describe one dead credential two different ways.
|
|
41
|
+
return formatErrorResponse(401, "authentication_error", CODEX_MAIN_SIGN_IN_REQUIRED_MESSAGE);
|
|
36
42
|
}
|
|
37
43
|
if (error instanceof MainAccountTokenRefreshError
|
|
38
44
|
|| error instanceof MainAuthJsonChangedDuringRefreshError
|
|
@@ -98,7 +104,9 @@ export function mapCodexAuthContextErrorToResponse(
|
|
|
98
104
|
return codexModelAvailabilityErrorResponse(error);
|
|
99
105
|
}
|
|
100
106
|
if (error instanceof CodexPoolAuthenticationError || error instanceof CodexDirectAuthenticationError) {
|
|
101
|
-
return formatErrorResponse(401, "authentication_error",
|
|
107
|
+
return formatErrorResponse(401, "authentication_error",
|
|
108
|
+
error instanceof CodexPoolAuthenticationError && error.quarantinedMain
|
|
109
|
+
? CODEX_MAIN_SIGN_IN_REQUIRED_MESSAGE : error.message);
|
|
102
110
|
}
|
|
103
111
|
if (error instanceof CodexMainSubstitutionUnavailableError) {
|
|
104
112
|
return formatErrorResponse(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { codexAccountModelRefusal, codexAccountModelRefusalPayload, hasConflictingCodexModelRefusalEnvelopes, type CodexAccountModelRefusal } from "../../combos/failover";
|
|
1
2
|
import type { ResponsesTerminalStatus } from "../../bridge";
|
|
2
3
|
import { comboFailureDecision } from "../../combos";
|
|
3
4
|
import { httpStatusFromTerminalError } from "../../lib/errors";
|
|
@@ -68,6 +69,26 @@ function bareErrorStatus(payload: unknown): number | undefined {
|
|
|
68
69
|
});
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
/** Match the selected SSE error envelope before projection drops competing fields. */
|
|
73
|
+
function terminalModelRefusal(status: number, event: Record<string, unknown>): CodexAccountModelRefusal {
|
|
74
|
+
if (hasConflictingCodexModelRefusalEnvelopes(status, event)) return "ambiguous";
|
|
75
|
+
const outer = codexAccountModelRefusalPayload(status, event);
|
|
76
|
+
if (outer === "ambiguous") return outer;
|
|
77
|
+
const nested = event.response;
|
|
78
|
+
if (event.type !== "error" && nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
79
|
+
const nestedKind = codexAccountModelRefusalPayload(status, nested);
|
|
80
|
+
if (nestedKind === "ambiguous") return nestedKind;
|
|
81
|
+
const nestedError = (nested as Record<string, unknown>).error;
|
|
82
|
+
if (nestedError && typeof nestedError === "object" && !Array.isArray(nestedError)) {
|
|
83
|
+
return nestedKind;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (event.type === "error" && !Object.hasOwn(event, "error") && typeof event.message === "string") {
|
|
87
|
+
return codexAccountModelRefusal(status, event.message, { allowNestedResponse: false });
|
|
88
|
+
}
|
|
89
|
+
return outer;
|
|
90
|
+
}
|
|
91
|
+
|
|
71
92
|
function bareErrorIsRetryable(payload: unknown): boolean {
|
|
72
93
|
const status = bareErrorStatus(payload);
|
|
73
94
|
if (status === undefined || !payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
@@ -84,7 +105,9 @@ function bareErrorIsRetryable(payload: unknown): boolean {
|
|
|
84
105
|
const message = typeof error.message === "string"
|
|
85
106
|
? error.message
|
|
86
107
|
: typeof event.message === "string" ? event.message : "";
|
|
87
|
-
return comboFailureDecision(status, message, {
|
|
108
|
+
return comboFailureDecision(status, message, {
|
|
109
|
+
code, codexModelRefusal: terminalModelRefusal(status, event),
|
|
110
|
+
}) === "hop";
|
|
88
111
|
}
|
|
89
112
|
|
|
90
113
|
function retryableZeroOutputTerminal(payload: unknown): boolean {
|
|
@@ -226,7 +249,7 @@ function failedTerminalResponse(
|
|
|
226
249
|
|
|
227
250
|
export type ComboStreamPreflightResult =
|
|
228
251
|
| { kind: "accepted"; response: Response }
|
|
229
|
-
| { kind: "failed"; response: Response }
|
|
252
|
+
| { kind: "failed"; response: Response; codexModelRefusal?: CodexAccountModelRefusal }
|
|
230
253
|
/**
|
|
231
254
|
* The body errored mid-stream and `replayReadErrors` asked for the prefix back rather than
|
|
232
255
|
* a rethrow. `stage` is how far the inspection actually got; whether that permits a
|
|
@@ -322,7 +345,10 @@ export async function preflightComboStreamResponse(
|
|
|
322
345
|
|| retryableTerminalPayload?.type === "error")
|
|
323
346
|
&& !outputCommitted && retryableTerminalPayload) {
|
|
324
347
|
await reader.cancel("retrying zero-output combo stream terminal").catch(() => undefined);
|
|
325
|
-
|
|
348
|
+
const failed = failedTerminalResponse(response, retryableTerminalPayload, logCtx);
|
|
349
|
+
return { kind: "failed", response: failed,
|
|
350
|
+
...(failed.status === 400
|
|
351
|
+
? { codexModelRefusal: terminalModelRefusal(failed.status, retryableTerminalPayload) } : {}) };
|
|
326
352
|
}
|
|
327
353
|
if (next.done || terminalStatus !== undefined || outputCommitted
|
|
328
354
|
|| bufferedBytes >= COMBO_STREAM_PREFLIGHT_MAX_BYTES
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { OcxConfig } from "../../types";
|
|
2
2
|
import { isDeclaredReasoningEffort } from "../../reasoning-effort";
|
|
3
|
-
import { COMPACTION_TRIGGERS } from "../../config/schema/compaction-triggers";
|
|
3
|
+
import { COMPACTION_TRIGGERS, validCompactionSourceModels } from "../../config/schema/compaction-triggers";
|
|
4
4
|
import { routeConcreteModel, type RouteResult } from "../../router";
|
|
5
5
|
import { resolveComboId } from "../../combos/identifiers";
|
|
6
6
|
import { resolvePolicyProfileId } from "../../routing/profile";
|
|
@@ -57,6 +57,15 @@ export function applyCompactionRoutingOverride(
|
|
|
57
57
|
&& (typeof override.reasoningEffort !== "string" || !isDeclaredReasoningEffort(override.reasoningEffort))) return null;
|
|
58
58
|
const triggers = configuredTriggers(override.triggers);
|
|
59
59
|
if (!triggers) return null;
|
|
60
|
+
if (override.sourceModels !== undefined) {
|
|
61
|
+
if (!validCompactionSourceModels(override.sourceModels)) return null;
|
|
62
|
+
// Match the selector the conversation actually routed on: a synthetic `--fast` or
|
|
63
|
+
// effort suffix is ingress decoration, so `cheap--fast` is still the allowlisted `cheap`.
|
|
64
|
+
const source = sourceSelectorOf(config, raw.model);
|
|
65
|
+
if (!override.sourceModels.some(selector => selector.endsWith("/*")
|
|
66
|
+
? source.startsWith(selector.slice(0, -1)) && source.length > selector.length - 1
|
|
67
|
+
: source === selector)) return null;
|
|
68
|
+
}
|
|
60
69
|
if (options.endpoint !== "compact"
|
|
61
70
|
&& !(Array.isArray(raw.input) && raw.input.some(item => record(item)?.type === "compaction_trigger"))) return null;
|
|
62
71
|
|
|
@@ -57,6 +57,9 @@ export type ResponsesAuthResolution =
|
|
|
57
57
|
| { ok: false; response: Response };
|
|
58
58
|
|
|
59
59
|
|
|
60
|
+
export type CodexCredentialOwnershipOptions = Pick<HandleResponsesOptions,
|
|
61
|
+
"admission" | "callerDirectAuth" | "stripClaudeMainAuthForNoncanonicalForward" | "trustedClaudeMainAuth">;
|
|
62
|
+
|
|
60
63
|
/**
|
|
61
64
|
* The caller credential the final Codex auth resolution will be given, as far as the ROUTE
|
|
62
65
|
* decides it: a route change that may cross a credential domain drops the raw caller credential,
|
|
@@ -67,15 +70,53 @@ export type ResponsesAuthResolution =
|
|
|
67
70
|
* an HMAC of exactly this Authorization header. Two copies of this rule would put preview and
|
|
68
71
|
* final auth in different scopes the first time one of them changed.
|
|
69
72
|
*/
|
|
73
|
+
/**
|
|
74
|
+
* The stored native-main credential this process injected itself, when it is the bearer the
|
|
75
|
+
* resolution will see.
|
|
76
|
+
*
|
|
77
|
+
* `handleClaudeMessages` reads `~/.codex/auth.json` and attaches it so OpenAI-backed sidecars stay
|
|
78
|
+
* reachable on a translated turn. It is OUR credential, not the caller's -- the ingress says so
|
|
79
|
+
* where it sets `nativeCallerAuth` and `callerDirectAuth` to null for exactly this reason -- and
|
|
80
|
+
* every reader of the resolution's input headers has to agree about that, or one of them conclud
|
|
81
|
+
* the request owns a credential that in fact belongs to the Pool.
|
|
82
|
+
*/
|
|
83
|
+
export function injectedClaudeMainBearer(
|
|
84
|
+
route: Pick<RouteResult, "provider">,
|
|
85
|
+
options: CodexCredentialOwnershipOptions,
|
|
86
|
+
): { authorization: string; chatgptAccountId?: string } | undefined {
|
|
87
|
+
return options.stripClaudeMainAuthForNoncanonicalForward === true
|
|
88
|
+
&& isCanonicalOpenAiForwardProvider(route.provider)
|
|
89
|
+
? options.trustedClaudeMainAuth : undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function validatedCallerDirectHeaders(
|
|
93
|
+
config: OcxConfig,
|
|
94
|
+
route: Pick<RouteResult, "provider">,
|
|
95
|
+
options: CodexCredentialOwnershipOptions,
|
|
96
|
+
): Headers | undefined {
|
|
97
|
+
if (!options.callerDirectAuth || !isCanonicalOpenAiForwardProvider(route.provider)) return undefined;
|
|
98
|
+
const headers = new Headers({ authorization: options.callerDirectAuth.authorization });
|
|
99
|
+
if (options.callerDirectAuth.chatgptAccountId) headers.set("chatgpt-account-id", options.callerDirectAuth.chatgptAccountId);
|
|
100
|
+
return captureCallerDirectAuth(headers, config) ? headers : undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
70
103
|
export function codexRouteCredentialDomainHeaders(
|
|
71
104
|
req: Request,
|
|
72
105
|
route: RouteResult,
|
|
73
106
|
options: HandleResponsesOptions,
|
|
74
107
|
credentialDomainWasRewritten: boolean,
|
|
108
|
+
config: OcxConfig,
|
|
75
109
|
): Headers {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
110
|
+
const callerDirect = validatedCallerDirectHeaders(config, route, options);
|
|
111
|
+
if (callerDirect) {
|
|
112
|
+
const headers = new Headers(req.headers);
|
|
113
|
+
headers.set("authorization", callerDirect.get("authorization")!);
|
|
114
|
+
const accountId = callerDirect.get("chatgpt-account-id");
|
|
115
|
+
if (accountId) headers.set("chatgpt-account-id", accountId);
|
|
116
|
+
else headers.delete("chatgpt-account-id");
|
|
117
|
+
return headers;
|
|
118
|
+
}
|
|
119
|
+
const trustedClaudeMainForFinalRoute = injectedClaudeMainBearer(route, options);
|
|
79
120
|
if (trustedClaudeMainForFinalRoute) {
|
|
80
121
|
const claudeMainHeaders = new Headers(req.headers);
|
|
81
122
|
claudeMainHeaders.set("authorization", trustedClaudeMainForFinalRoute.authorization);
|
|
@@ -117,15 +158,25 @@ export function codexRouteCredentialDomainHeaders(
|
|
|
117
158
|
export function codexRouteCredentialOwnership(
|
|
118
159
|
authInputHeaders: Headers,
|
|
119
160
|
config: OcxConfig,
|
|
120
|
-
route: RouteResult,
|
|
121
|
-
options:
|
|
161
|
+
route: Pick<RouteResult, "provider" | "codexAccountMode">,
|
|
162
|
+
options: CodexCredentialOwnershipOptions,
|
|
122
163
|
): { substituteMainCredential: boolean; requestScopedMainCredential: boolean } {
|
|
123
164
|
const substituteMainCredential = options.admission?.source === "bearer"
|
|
124
165
|
&& (route.codexAccountMode !== undefined || isCanonicalOpenAiForwardProvider(route.provider));
|
|
125
166
|
return {
|
|
126
167
|
substituteMainCredential,
|
|
168
|
+
// A request-scoped main credential is one the CALLER owns: it exists for this request only,
|
|
169
|
+
// owns no Pool state, and must never be quarantined or refreshed on the Pool's behalf. The
|
|
170
|
+
// stored credential this process injected is the opposite of that in every respect, and
|
|
171
|
+
// `hasForwardableCodexBearer` cannot tell them apart -- it sees a Codex JWT with an account
|
|
172
|
+
// id either way. Counting ours as the caller's made the native main slot caller-owned on
|
|
173
|
+
// every translated Claude turn: `main` instead of `main-pool`, so an upstream 401 on a
|
|
174
|
+
// revoked session recorded no outcome, refreshed nothing, and retired nothing, and the next
|
|
175
|
+
// request sent the same dead token again.
|
|
127
176
|
requestScopedMainCredential: route.codexAccountMode !== undefined
|
|
128
177
|
&& !substituteMainCredential
|
|
178
|
+
&& (validatedCallerDirectHeaders(config, route, options) !== undefined
|
|
179
|
+
|| injectedClaudeMainBearer(route, options) === undefined)
|
|
129
180
|
&& hasForwardableCodexBearer(authInputHeaders, config),
|
|
130
181
|
};
|
|
131
182
|
}
|
|
@@ -149,6 +200,7 @@ export async function resolveResponsesCodexAuth(
|
|
|
149
200
|
route,
|
|
150
201
|
options,
|
|
151
202
|
credentialDomainWasRewritten,
|
|
203
|
+
config,
|
|
152
204
|
);
|
|
153
205
|
// A caller-auth transport that is not canonical OpenAI (keyless Cursor) consumes the
|
|
154
206
|
// caller's Authorization as its own upstream token. Keep that contract only for a clean
|
|
@@ -171,27 +223,6 @@ export async function resolveResponsesCodexAuth(
|
|
|
171
223
|
authInputHeaders = scoped;
|
|
172
224
|
}
|
|
173
225
|
}
|
|
174
|
-
// The caller's own Direct credential may cross an internal route change only to the
|
|
175
|
-
// canonical OpenAI transport, under a predicate deliberately STRICTER than plain
|
|
176
|
-
// unchanged-route Direct forwarding: a clean non-proxy bearer whose ChatGPT-domain
|
|
177
|
-
// marker is valid, with any explicit account header matching that marker. Unchanged
|
|
178
|
-
// routes keep their legacy rules; sidecar enrichment grants no primary authority.
|
|
179
|
-
if (options.callerDirectAuth && isCanonicalOpenAiForwardProvider(route.provider)) {
|
|
180
|
-
const directHeaders = new Headers({
|
|
181
|
-
authorization: options.callerDirectAuth.authorization,
|
|
182
|
-
...(options.callerDirectAuth.chatgptAccountId
|
|
183
|
-
? { "chatgpt-account-id": options.callerDirectAuth.chatgptAccountId } : {}),
|
|
184
|
-
});
|
|
185
|
-
if (captureCallerDirectAuth(directHeaders, config)) {
|
|
186
|
-
authInputHeaders = new Headers(authInputHeaders);
|
|
187
|
-
authInputHeaders.set("authorization", options.callerDirectAuth.authorization);
|
|
188
|
-
if (options.callerDirectAuth.chatgptAccountId) {
|
|
189
|
-
authInputHeaders.set("chatgpt-account-id", options.callerDirectAuth.chatgptAccountId);
|
|
190
|
-
} else {
|
|
191
|
-
authInputHeaders.delete("chatgpt-account-id");
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
226
|
// #1686: a caller that proved admission with a BEARER presented one of our own secrets.
|
|
196
227
|
// Refusing it here is what made the codex-cli `env_key` contract unusable against Direct.
|
|
197
228
|
// Admitting it is only safe because the stored main credential is substituted below, so
|
|
@@ -59,7 +59,7 @@ import {
|
|
|
59
59
|
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
|
|
60
60
|
import { isRequestExecutionBudget } from "../../lib/request-execution-budget";
|
|
61
61
|
import type { SingleUseDispatchPermit } from "../../lib/request-execution-budget";
|
|
62
|
-
import {
|
|
62
|
+
import { codexRouteCredentialOwnership, type CodexCredentialOwnershipOptions } from "./core-auth";
|
|
63
63
|
import { bindRouteReasoningReplayScope } from "./core-replay";
|
|
64
64
|
import {
|
|
65
65
|
conversationStateBindingFromAuth,
|
|
@@ -360,7 +360,7 @@ export interface CodexPoolAccountRetryArgs {
|
|
|
360
360
|
route: Pick<RouteResult, "providerName" | "modelId" | "provider" | "staticPolicy">;
|
|
361
361
|
parsed: OcxParsedRequest;
|
|
362
362
|
logCtx: RequestLogContext;
|
|
363
|
-
options: {
|
|
363
|
+
options: CodexCredentialOwnershipOptions & {
|
|
364
364
|
admission?: DataPlaneAdmission;
|
|
365
365
|
codexAuthPolicy?: CodexAuthPolicyConfig;
|
|
366
366
|
visionDescribeTerminal?: boolean;
|
|
@@ -641,10 +641,13 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
641
641
|
"pool",
|
|
642
642
|
{
|
|
643
643
|
excludeAccountId: firstAuthCtx.accountId,
|
|
644
|
+
signal: options.abortSignal,
|
|
644
645
|
admission: options.admission,
|
|
645
646
|
codexAuthPolicy: options.codexAuthPolicy,
|
|
646
647
|
modelId: route.modelId,
|
|
647
|
-
requestScopedMainCredential:
|
|
648
|
+
requestScopedMainCredential: codexRouteCredentialOwnership(callerAuthHeaders, config, {
|
|
649
|
+
provider: route.provider, codexAccountMode: "pool",
|
|
650
|
+
}, options).requestScopedMainCredential,
|
|
648
651
|
beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
|
|
649
652
|
resolveCodexModelEntitlements: entitlementResolver,
|
|
650
653
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { codexAccountModelRefusal, codexAccountModelRefusalHardStopCode, type CodexAccountModelRefusal } from "../../combos/failover";
|
|
1
2
|
import { parseRetryAfterMs } from "../../combos";
|
|
2
3
|
import type { ConsumedComboFailure, HandleResponsesOptions } from "./core-options";
|
|
3
4
|
import type { OcxUsage } from "../../types";
|
|
@@ -36,6 +37,7 @@ export async function consumeComboFailure(
|
|
|
36
37
|
const nonReplayable = isNonReplayableResponse(response);
|
|
37
38
|
const fallback = `Provider error ${response.status}`;
|
|
38
39
|
let classificationText = fallback;
|
|
40
|
+
let codexModelRefusal: CodexAccountModelRefusal | undefined;
|
|
39
41
|
let usage: OcxUsage | undefined;
|
|
40
42
|
let upstreamCode: string | undefined;
|
|
41
43
|
let upstreamMessage: string | undefined;
|
|
@@ -60,10 +62,11 @@ export async function consumeComboFailure(
|
|
|
60
62
|
&& isRateLimitOrQuotaFailureMessage(quotaMessage);
|
|
61
63
|
}
|
|
62
64
|
if (body.displaySafe && !body.truncated) {
|
|
65
|
+
if (response.status === 400) codexModelRefusal = codexAccountModelRefusal(response.status, body.text);
|
|
63
66
|
const normalized = normalizeUpstreamErrorText(body.text, fallback);
|
|
64
67
|
if (utf8Trusted || isCyberPolicyCode(normalized.code) || isCyberPolicyMessage(normalized.safeText)) {
|
|
65
68
|
classificationText = normalized.safeText;
|
|
66
|
-
upstreamCode = normalized.code;
|
|
69
|
+
upstreamCode = codexAccountModelRefusalHardStopCode(response.status, body.text) ?? normalized.code;
|
|
67
70
|
upstreamMessage = normalized.message;
|
|
68
71
|
upstreamType = normalized.type;
|
|
69
72
|
}
|
|
@@ -116,6 +119,7 @@ export async function consumeComboFailure(
|
|
|
116
119
|
response: failureResponse,
|
|
117
120
|
...(nonReplayable ? { nonReplayable: true } : {}),
|
|
118
121
|
classificationText,
|
|
122
|
+
...(codexModelRefusal !== undefined ? { codexModelRefusal } : {}),
|
|
119
123
|
...(normalizedUpstreamCode !== undefined ? { upstreamCode: normalizedUpstreamCode } : {}),
|
|
120
124
|
...(upstreamType !== undefined ? { upstreamType } : {}),
|
|
121
125
|
...(!cyberFailure && cooldownRetryAfter !== undefined ? { retryAfter: cooldownRetryAfter } : {}),
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
import { captureRouteStaticPolicy, routeConcreteModel } from "../../router";
|
|
35
35
|
import { comboDefaultEffort, concreteComboRequestBody, getCombo } from "../../combos";
|
|
36
36
|
import { supportedLadderFor } from "../effort-policy";
|
|
37
|
+
import { applyDroidReasoningDefault } from "../droid-reasoning-default";
|
|
37
38
|
import { resolveWireProtocolOverride } from "../adapter-resolve";
|
|
38
39
|
import { resolveOpenCodeGoTransport } from "../../providers/opencode-go-transport";
|
|
39
40
|
import { getOrAllocateRequestSessionLane } from "../request-log-conversation";
|
|
@@ -105,10 +106,11 @@ export function createComboProtocolLanes(input: {
|
|
|
105
106
|
config: OcxConfig;
|
|
106
107
|
logCtx: RequestLogContext;
|
|
107
108
|
admission: DataPlaneAdmission | undefined;
|
|
109
|
+
droidDefaultEffort?: string;
|
|
108
110
|
comboId: string;
|
|
109
111
|
targets: readonly ComboTarget[];
|
|
110
112
|
}): ComboProtocolLanes | undefined {
|
|
111
|
-
const { source, req, config, logCtx, admission, comboId, targets } = input;
|
|
113
|
+
const { source, req, config, logCtx, admission, droidDefaultEffort, comboId, targets } = input;
|
|
112
114
|
if (!source) return undefined;
|
|
113
115
|
const envelope = source.envelope;
|
|
114
116
|
const reject = resolveProtocolSettings(config).unrepresentable === "reject";
|
|
@@ -207,6 +209,10 @@ export function createComboProtocolLanes(input: {
|
|
|
207
209
|
const route = settle(target, targetRoute);
|
|
208
210
|
const body = nativeBody(target, route);
|
|
209
211
|
if (!route || !body) return undefined;
|
|
212
|
+
applyDroidReasoningDefault(body, droidDefaultEffort, {
|
|
213
|
+
provider: targetRoute.provider,
|
|
214
|
+
modelId: targetRoute.modelId,
|
|
215
|
+
});
|
|
210
216
|
applyComboEffort(body, config, comboId, target, targetRoute);
|
|
211
217
|
return { route, body, sendBudget: targetSendBudget };
|
|
212
218
|
},
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import type { CodexAccountModelRefusal } from "../../combos/failover";
|
|
1
2
|
import {
|
|
2
3
|
isCodexReasoningEffort,
|
|
3
4
|
isDeclaredReasoningEffort,
|
|
4
5
|
resolveEffortAtOrBelow,
|
|
5
6
|
} from "../../reasoning-effort";
|
|
7
|
+
import { resolveAdmissionModelScope, routeAllowedByScope } from "../admission-model-scope";
|
|
6
8
|
import { recordAttemptRequestedEffort } from "../request-log";
|
|
7
9
|
import {
|
|
8
10
|
CODEX_TEXT_GUARDED_BUDGET_POLICY,
|
|
@@ -65,6 +67,8 @@ import {
|
|
|
65
67
|
recoverEncryptedAgentTaskWithResult,
|
|
66
68
|
} from "./agent-task-recovery";
|
|
67
69
|
import { isThreadSpawnRequest, supportedLadderFor } from "../effort-policy";
|
|
70
|
+
import { applyDroidResponsesReasoningDefault } from "../droid-reasoning-default";
|
|
71
|
+
import { isPlainObject } from "../../lib/plain-data";
|
|
68
72
|
import {
|
|
69
73
|
clientCancelledResponse,
|
|
70
74
|
comboUnavailable,
|
|
@@ -327,6 +331,7 @@ export async function executeComboResponses(
|
|
|
327
331
|
config,
|
|
328
332
|
logCtx,
|
|
329
333
|
admission: options.admission,
|
|
334
|
+
droidDefaultEffort: options.droidDefaultEffort,
|
|
330
335
|
comboId,
|
|
331
336
|
targets: combo.targets,
|
|
332
337
|
});
|
|
@@ -572,6 +577,9 @@ export async function executeComboResponses(
|
|
|
572
577
|
candidates: choices.map(choice => choice.candidate),
|
|
573
578
|
fallback,
|
|
574
579
|
config,
|
|
580
|
+
isDestinationAllowed: (providerName, modelId) => routeAllowedByScope(
|
|
581
|
+
resolveAdmissionModelScope(config, options.admission), { providerName, modelId },
|
|
582
|
+
),
|
|
575
583
|
...(combo.decisionProvider ? { decisionProvider: combo.decisionProvider } : {}),
|
|
576
584
|
...(combo.decisionModel
|
|
577
585
|
? {
|
|
@@ -709,8 +717,13 @@ export async function executeComboResponses(
|
|
|
709
717
|
modelId: targetRoute.modelId,
|
|
710
718
|
});
|
|
711
719
|
const initialJevDecision = firstComboTarget ? jevDecision : undefined;
|
|
720
|
+
const childInput = options.droidDefaultEffort && isPlainObject(body) ? { ...body } : body;
|
|
721
|
+
applyDroidResponsesReasoningDefault(childInput, options.droidDefaultEffort, {
|
|
722
|
+
provider: targetRoute.provider,
|
|
723
|
+
modelId: targetRoute.modelId,
|
|
724
|
+
});
|
|
712
725
|
const childBody = concreteComboRequestBody(
|
|
713
|
-
|
|
726
|
+
childInput,
|
|
714
727
|
pick.target,
|
|
715
728
|
initialJevDecision ? initialJevDecision.effort : comboDefaultEffort(config, comboId),
|
|
716
729
|
initialJevDecision?.effort === null ? [] : targetReasoningEfforts,
|
|
@@ -773,6 +786,7 @@ export async function executeComboResponses(
|
|
|
773
786
|
const completedTarget = { provider: pick.target.provider, model: pick.target.model };
|
|
774
787
|
const writerGeneration = pick.writerGeneration;
|
|
775
788
|
let consumedChildFailure: ConsumedComboFailure | undefined;
|
|
789
|
+
let preflightModelRefusal: CodexAccountModelRefusal | undefined;
|
|
776
790
|
const callbackGate = createChildPassthroughCallbackGate({
|
|
777
791
|
...options,
|
|
778
792
|
onResponseComplete: model => {
|
|
@@ -895,6 +909,7 @@ export async function executeComboResponses(
|
|
|
895
909
|
callbackGate.discard();
|
|
896
910
|
terminalRecorder?.("failed", preflight.response.status);
|
|
897
911
|
response = preflight.response;
|
|
912
|
+
preflightModelRefusal = preflight.codexModelRefusal;
|
|
898
913
|
} else {
|
|
899
914
|
response = preflight.response;
|
|
900
915
|
if (nativePassthrough) markNativePassthroughSseResponse(response);
|
|
@@ -937,6 +952,7 @@ export async function executeComboResponses(
|
|
|
937
952
|
try {
|
|
938
953
|
failure = consumedChildFailure
|
|
939
954
|
?? await consumeComboFailure(response, options.abortSignal);
|
|
955
|
+
if (preflightModelRefusal !== undefined) failure = { ...failure, codexModelRefusal: preflightModelRefusal };
|
|
940
956
|
} catch (error) {
|
|
941
957
|
if (options.abortSignal?.aborted) {
|
|
942
958
|
retainCancelledAttempt();
|
|
@@ -981,6 +997,7 @@ export async function executeComboResponses(
|
|
|
981
997
|
? "stop"
|
|
982
998
|
: comboFailureDecision(failure.response.status, failure.classificationText, {
|
|
983
999
|
code: failure.upstreamCode,
|
|
1000
|
+
codexModelRefusal: failure.codexModelRefusal,
|
|
984
1001
|
});
|
|
985
1002
|
const wantsStream = (rawBody as { stream?: unknown } | null)?.stream === true;
|
|
986
1003
|
// Local byte admission has its own diagnostic; do not relabel it as an upstream refusal.
|
|
@@ -38,6 +38,7 @@ import { isInjectionDebugEnabled } from "../../lib/debug-settings";
|
|
|
38
38
|
import { injectionDebugLog } from "../../lib/injection-debug-log";
|
|
39
39
|
import { recordAttemptRequestedEffort } from "../request-log";
|
|
40
40
|
import type { ResolvedFastPolicy } from "../../providers/fastwire";
|
|
41
|
+
import { applyDroidResponsesReasoningDefault } from "../droid-reasoning-default";
|
|
41
42
|
import { applyXaiOauthFastModel, xaiOauthFastModelForDecision } from "../../providers/xai-fast-model";
|
|
42
43
|
|
|
43
44
|
/** Preview the billed xAI lane without mutating request state or resolving credentials. */
|
|
@@ -140,9 +141,15 @@ export async function applyFinalRouteRequestNormalization(args: {
|
|
|
140
141
|
inboundWire: InboundWire;
|
|
141
142
|
inboundTransport?: "websocket";
|
|
142
143
|
claudeGoAffinity?: HandleResponsesOptions["claudeGoAffinity"];
|
|
144
|
+
droidDefaultEffort?: HandleResponsesOptions["droidDefaultEffort"];
|
|
143
145
|
}): Promise<void> {
|
|
144
146
|
const { parsed, route, config, req, logCtx, inboundWire, inboundTransport } = args;
|
|
145
147
|
const effortSelector = prepareEffortNormalization(parsed, route);
|
|
148
|
+
if (applyDroidResponsesReasoningDefault(
|
|
149
|
+
parsed._rawBody,
|
|
150
|
+
args.droidDefaultEffort,
|
|
151
|
+
{ provider: route.provider, modelId: route.modelId },
|
|
152
|
+
)) parsed.options.reasoning = args.droidDefaultEffort;
|
|
146
153
|
|
|
147
154
|
// Only Anthropic message routes retain the Codex-facing selector. Other providers must keep
|
|
148
155
|
// their existing response.model contract even when their public and wire model ids differ.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CodexAccountModelRefusal } from "../../combos/failover";
|
|
1
2
|
import type { NativeResponseControl } from "./native-response-control";
|
|
2
3
|
import type { AdapterEvent, OcxUsage, OcxProviderContinuationState, OcxConfig } from "../../types";
|
|
3
4
|
import type { RouteResult } from "../../router";
|
|
@@ -24,6 +25,8 @@ import type { PolicyRequestScope } from "./policy-request-scope";
|
|
|
24
25
|
export interface ConsumedComboFailure {
|
|
25
26
|
response: Response;
|
|
26
27
|
classificationText: string;
|
|
28
|
+
/** Complete bounded-envelope evidence captured before display truncation; never serialized. */
|
|
29
|
+
codexModelRefusal?: CodexAccountModelRefusal;
|
|
27
30
|
/** Structured upstream `error.code` when present in the failure body. */
|
|
28
31
|
upstreamCode?: string;
|
|
29
32
|
/** Complete structured provider type, retained for conservative recovery classification. */
|
|
@@ -131,6 +134,8 @@ export interface HandleResponsesOptions {
|
|
|
131
134
|
* it. Omitted means a genuine Responses inbound.
|
|
132
135
|
*/
|
|
133
136
|
inboundWire?: InboundWire;
|
|
137
|
+
/** Droid's per-request effort default; each concrete combo or policy target applies it only if its ladder allows it. */
|
|
138
|
+
droidDefaultEffort?: string;
|
|
134
139
|
/** PF-07: the Chat source a combo child may send natively; set only by the Chat ingress. */
|
|
135
140
|
protocolSource?: import("./core-combo-native").ComboProtocolSource;
|
|
136
141
|
/** Internal transport identity for route-scoped upstream compatibility policy. */
|
|
@@ -192,8 +192,17 @@ export function sendWithConnectionPolicy(
|
|
|
192
192
|
const egressInit = redirectedToLoopback
|
|
193
193
|
? { proxy: false as const }
|
|
194
194
|
: decide ? providerEgressSendInit(egress, physicalFetch, input) : {};
|
|
195
|
+
// Bun can reset large native Codex string uploads before receiving headers while
|
|
196
|
+
// the identical UTF-8 buffer succeeds. Convert only at the final HTTP send so
|
|
197
|
+
// WebSocket selection still sees the serialized string and no retry is added.
|
|
198
|
+
const target = input instanceof Request ? input.url : String(input);
|
|
199
|
+
const body = init?.body;
|
|
200
|
+
const largeCodexBody = typeof body === "string"
|
|
201
|
+
&& /^https:\/\/chatgpt\.com\/backend-api\/codex\/responses(?:\/compact)?$/.test(target)
|
|
202
|
+
&& Buffer.byteLength(body, "utf8") >= 1024 * 1024;
|
|
195
203
|
return physicalFetch(input, {
|
|
196
204
|
...init,
|
|
205
|
+
...(largeCodexBody ? { body: Buffer.from(body, "utf8") } : {}),
|
|
197
206
|
headers,
|
|
198
207
|
redirect: "manual",
|
|
199
208
|
...(fresh ? { keepalive: false } : {}),
|
|
@@ -1329,7 +1329,7 @@ export async function deliverPassthroughResponse(
|
|
|
1329
1329
|
clientJson = imageDisplay.json(clientJson);
|
|
1330
1330
|
} catch (error) {
|
|
1331
1331
|
if (error instanceof RangeError) {
|
|
1332
|
-
return formatErrorResponse(502, "upstream_error", "hosted image result
|
|
1332
|
+
return formatErrorResponse(502, "upstream_error", "hosted image result exceeds local display limits");
|
|
1333
1333
|
}
|
|
1334
1334
|
throw error;
|
|
1335
1335
|
} finally { imageDisplay.dispose?.(); }
|
|
@@ -111,7 +111,7 @@ async function shouldHopPolicyCandidate(response: Response, signal?: AbortSignal
|
|
|
111
111
|
if (isPolicyCandidateRefusal(response)) return true;
|
|
112
112
|
try {
|
|
113
113
|
const inspected = await readBoundedResponseBody(response.clone(), { signal });
|
|
114
|
-
const text = inspected.displaySafe ? inspected.text : "";
|
|
114
|
+
const text = inspected.displaySafe && !inspected.truncated ? inspected.text : "";
|
|
115
115
|
return comboFailureDecision(response.status, text, { code: errorCodeFromText(text) }) === "hop";
|
|
116
116
|
} catch {
|
|
117
117
|
return false;
|
|
@@ -676,6 +676,7 @@ export async function prepareResponsesRequest(
|
|
|
676
676
|
route,
|
|
677
677
|
options,
|
|
678
678
|
credentialDomainWasRewritten,
|
|
679
|
+
config,
|
|
679
680
|
);
|
|
680
681
|
// Does the CALLER own the credential this request will authenticate with? Validated exactly
|
|
681
682
|
// the way final auth validates it: the route ownership predicate AND the caller-bearer check
|
|
@@ -736,6 +737,7 @@ export async function prepareResponsesRequest(
|
|
|
736
737
|
// recovery or a profile drain fences the physical identity, final auth drops main and preview
|
|
737
738
|
// has to drop it too. A hardcoded `true` would be wrong in the second case and `false` in
|
|
738
739
|
// the first.
|
|
740
|
+
requestOwnedMainCredential: previewRequestScopedMainCredential,
|
|
739
741
|
isMainAccountTokenLive: previewRequestScopedMainCredential
|
|
740
742
|
? () => previewRequestOwnedMainCredentialLive
|
|
741
743
|
: undefined,
|
|
@@ -962,6 +964,7 @@ export async function prepareResponsesRequest(
|
|
|
962
964
|
route,
|
|
963
965
|
options,
|
|
964
966
|
credentialDomainWasRewritten,
|
|
967
|
+
config,
|
|
965
968
|
);
|
|
966
969
|
const recoveryRequestScopedMainCredential = codexRouteCredentialOwnership(
|
|
967
970
|
recoveryAuthHeaders,
|
|
@@ -993,6 +996,7 @@ export async function prepareResponsesRequest(
|
|
|
993
996
|
const recoverySelectionOptions = {
|
|
994
997
|
nativeMainSelectionOnly: !recoveryNativeMainBlocked
|
|
995
998
|
&& recoverySelectionAdmission?.mainProfileDraining === true,
|
|
999
|
+
requestOwnedMainCredential: recoveryRequestScopedMainCredential,
|
|
996
1000
|
isMainAccountTokenLive: recoveryRequestScopedMainCredential
|
|
997
1001
|
? () => recoveryRequestOwnedMainCredentialLive
|
|
998
1002
|
: undefined,
|
|
@@ -1222,6 +1226,7 @@ export async function prepareResponsesRequest(
|
|
|
1222
1226
|
inboundWire,
|
|
1223
1227
|
inboundTransport: options.inboundTransport,
|
|
1224
1228
|
claudeGoAffinity: options.claudeGoAffinity,
|
|
1229
|
+
droidDefaultEffort: options.droidDefaultEffort,
|
|
1225
1230
|
});
|
|
1226
1231
|
try {
|
|
1227
1232
|
assertPolicyNormalizedRoute(policyScope, admittedPolicyRoute, route);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type { RequestSendObserver } from "../../lib/request-execution-budget";
|
|
3
|
-
import { sharedSpendLedger, type SpendReservationLedger } from "../../lib/spend-reservation-ledger";
|
|
3
|
+
import { sharedSpendLedger, type SpendReservationLedger, type SpendScopes } from "../../lib/spend-reservation-ledger";
|
|
4
4
|
import { SpendLedgerOwnerError } from "../../lib/spend-ledger-owner";
|
|
5
5
|
import { markLocalRequestLogRefusal, type RequestLogContext } from "../request-log";
|
|
6
|
-
import { recordWorkflowRefusalEvent, workflowDenialSummary } from "../../lib/workflow-budget";
|
|
6
|
+
import { recordWorkflowRefusalEvent, workflowDenialSummary, type WorkflowDenial } from "../../lib/workflow-budget";
|
|
7
7
|
|
|
8
8
|
/** The terminal usage a request reported, in the only two fields the ledger books. */
|
|
9
9
|
export interface TerminalSpendUsage {
|
|
@@ -81,44 +81,44 @@ export function createRequestSpendTracker(
|
|
|
81
81
|
// short of its limit forever and refuse nothing.
|
|
82
82
|
const alreadySent = options?.alreadySent === true;
|
|
83
83
|
const sendId = randomUUID();
|
|
84
|
-
const
|
|
84
|
+
const bookedLedger = ledger();
|
|
85
|
+
const scopes: SpendScopes = {
|
|
86
|
+
...(rootId !== undefined ? { rootId } : {}),
|
|
87
|
+
// The existing privacy-safe log label is aliased again by the ledger.
|
|
88
|
+
...(logCtx.accountLogLabel !== undefined ? { identityId: logCtx.accountLogLabel } : {}),
|
|
89
|
+
...(logCtx.provider !== undefined ? { poolId: logCtx.provider } : {}),
|
|
90
|
+
};
|
|
91
|
+
const policy = bookedLedger.policy;
|
|
92
|
+
const enforced = (scopes.rootId !== undefined && policy.root.maxTokens !== undefined)
|
|
93
|
+
|| (scopes.identityId !== undefined && policy.identity.maxTokens !== undefined)
|
|
94
|
+
|| (scopes.poolId !== undefined && policy.pool.maxTokens !== undefined);
|
|
95
|
+
const decision = bookedLedger.reserve({
|
|
85
96
|
sendId,
|
|
86
|
-
scopes
|
|
87
|
-
...(rootId !== undefined ? { rootId } : {}),
|
|
88
|
-
// Already the privacy-safe label the request log uses, and the ledger aliases it
|
|
89
|
-
// again on the way to disk. A raw credential never reaches either.
|
|
90
|
-
...(logCtx.accountLogLabel !== undefined ? { identityId: logCtx.accountLogLabel } : {}),
|
|
91
|
-
...(logCtx.provider !== undefined ? { poolId: logCtx.provider } : {}),
|
|
92
|
-
},
|
|
97
|
+
scopes,
|
|
93
98
|
inputTokens: logCtx.spendInputEstimateTokens ?? logCtx.usageLogInputTokens ?? 0,
|
|
94
99
|
outputCeilingTokens: logCtx.spendOutputCeilingTokens ?? 0,
|
|
95
100
|
...(alreadySent ? { alreadySent: true } : {}),
|
|
96
101
|
});
|
|
97
102
|
if (!decision.reserved) {
|
|
103
|
+
// An applicable ceiling cannot authorize an unbooked send, including when tracking
|
|
104
|
+
// capacity is full. Unconfigured/nonapplicable requests remain observe-only, and a
|
|
105
|
+
// physical send reported after dispatch cannot be refused retroactively.
|
|
106
|
+
if (alreadySent || !enforced) return true;
|
|
98
107
|
refusals += 1;
|
|
99
108
|
const denial = decision.denial;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (denial.reason !== "spend-limit-exceeded") return true;
|
|
111
|
-
// This send is refused, and the dispatch path that asked will report an exhausted send
|
|
112
|
-
// budget -- from there, that is all it can see. The row is where an operator actually
|
|
113
|
-
// looks, so the ceiling is named on it here: a locally assigned code wins in
|
|
114
|
-
// addFinalRequestLog, so the request that CROSSED the ceiling reads as a spend refusal
|
|
115
|
-
// rather than as the ordinary budget exhaustion it would otherwise be indistinguishable
|
|
116
|
-
// from. The event ring gets the same pair so /api/workflow-budget agrees with the row.
|
|
117
|
-
const detail = { scope: denial.scope, limit: denial.limit, projected: denial.projected };
|
|
118
|
-
const summary = workflowDenialSummary("workflow-spend-exhausted", detail);
|
|
109
|
+
const reason: WorkflowDenial = denial.reason === "duplicate-send-id"
|
|
110
|
+
? "workflow-send-replayed"
|
|
111
|
+
: denial.reason === "reserve-not-durable" || denial.reason === "journal-corrupt"
|
|
112
|
+
? "workflow-spend-undurable"
|
|
113
|
+
: denial.reason === "tracking-capacity-exhausted"
|
|
114
|
+
? "workflow-tracking-exhausted"
|
|
115
|
+
: "workflow-spend-exhausted";
|
|
116
|
+
const detail = denial.reason === "spend-limit-exceeded"
|
|
117
|
+
? { scope: denial.scope, limit: denial.limit, projected: denial.projected } : undefined;
|
|
118
|
+
const summary = workflowDenialSummary(reason, detail);
|
|
119
119
|
markLocalRequestLogRefusal(logCtx, summary.code);
|
|
120
120
|
logCtx.errorCode = summary.code;
|
|
121
|
-
recordWorkflowRefusalEvent(rootId,
|
|
121
|
+
recordWorkflowRefusalEvent(rootId, reason, Date.now(), detail);
|
|
122
122
|
return false;
|
|
123
123
|
}
|
|
124
124
|
live.push(sendId);
|