diff --git a/src/codex/subagent-model-fallback.ts b/src/codex/subagent-model-fallback.ts index 7110d4cf50..27eea1e53d 100644 --- a/src/codex/subagent-model-fallback.ts +++ b/src/codex/subagent-model-fallback.ts @@ -7,7 +7,7 @@ */ import { existsSync, readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; -import { DEFAULT_SUBAGENT_MODELS, hasOwnProvider } from "../config"; +import { hasOwnProvider } from "../config"; import { isRateLimitOrQuotaFailureMessage } from "../lib/errors"; import type { OcxParsedRequest, OcxConfig } from "../types"; import { slugsEquivalent } from "../providers/slug-codec"; @@ -609,22 +609,9 @@ export function applySubagentModelFallback( resolvedFallbackChain?: readonly string[] | null, ): { from?: string; to?: string; skipped?: string[] } | null { if (!isThreadSpawnRequest(headers)) return null; - const configuredFallbackChain = resolvedFallbackChain === undefined + const fallbackChain = resolvedFallbackChain === undefined ? resolveSubagentFallbackChain(parsed, config) : resolvedFallbackChain; - // Native-only encrypted V2 tasks need a readable ChatGPT backend even when the - // operator configured no fallback chain. Keep ordinary routed spawns unchanged. - // - // Not when encrypted-task recovery is enabled: that operator chose to decrypt the - // assignment and stay on the routed model. The synthesized chain would reroute the - // spawn to native in this first pass, before recovery runs, and recovery's own - // caller-auth / proxy-secret / token-validity gates would never execute - // (tests/agent-task-recovery-security.test.ts went 13/13 -> 2/13 when #3239 landed - // without this guard). A configured chain keeps its existing precedence. - const fallbackChain = configuredFallbackChain === null && nativeFallbackOnly - && config.agentTaskRecovery?.enabled !== true - ? normalizedChain(parsed.modelId, config, [], DEFAULT_SUBAGENT_MODELS) - : configuredFallbackChain; if (!fallbackChain) return null; const selection = selectAvailableSubagentModel( parsed.modelId, diff --git a/tests/subagent-model-fallback.test.ts b/tests/subagent-model-fallback.test.ts index 510c294d80..75b2359ad5 100644 --- a/tests/subagent-model-fallback.test.ts +++ b/tests/subagent-model-fallback.test.ts @@ -1056,56 +1056,6 @@ describe("subagent model fallback chain", () => { expect((parsed._rawBody as { model?: string }).model).toBe("alibaba-token-plan/qwen3.8-max"); }); - test("encrypted routed spawn gets an automatic native fallback when none is configured", () => { - const config = cfg({ - subagentModelFallback: undefined, - defaultProvider: "xai", - }); - const parsed = { - modelId: "xai/grok-4.5", - options: {}, - context: { messages: [] }, - _rawBody: { model: "xai/grok-4.5" }, - }; - const result = applySubagentModelFallback( - parsed as never, - new Headers({ "x-openai-subagent": "collab_spawn" }), - config, - "pool-a", - Date.now(), - true, - ); - expect(result?.to).toBe("gpt-5.5"); - expect(parsed.modelId).toBe("gpt-5.5"); - }); - - test("the synthesized native chain yields to enabled encrypted-task recovery", () => { - // With recovery on, the first fallback pass must leave the routed spawn alone so - // recoverEncryptedAgentTask runs (and its security gates fire); rerouting here - // would bypass them. - const config = cfg({ - subagentModelFallback: undefined, - defaultProvider: "xai", - agentTaskRecovery: { enabled: true }, - }); - const parsed = { - modelId: "xai/grok-4.5", - options: {}, - context: { messages: [] }, - _rawBody: { model: "xai/grok-4.5" }, - }; - const result = applySubagentModelFallback( - parsed as never, - new Headers({ "x-openai-subagent": "collab_spawn" }), - config, - "pool-a", - Date.now(), - true, - ); - expect(result).toBeNull(); - expect(parsed.modelId).toBe("xai/grok-4.5"); - }); - test("applySubagentModelFallback is a no-op for main turns", () => { updateAccountQuota("pool-a", 95); const parsed = {