diff --git a/assets/inject/renderer-inject.js b/assets/inject/renderer-inject.js index 38b9cd243..f02f9587e 100644 --- a/assets/inject/renderer-inject.js +++ b/assets/inject/renderer-inject.js @@ -3331,9 +3331,10 @@ } function codexRemoteSessionProviderRequestMethod(method) { + // app-server restores persisted model/provider/reasoning for thread/resume only + // when the caller supplies none of those overrides. return [ "thread/start", - "thread/resume", "start-conversation", "start-thread-for-host", "thread-prewarm-start", @@ -3349,8 +3350,7 @@ if (!params || typeof params !== "object" || Array.isArray(params)) return params; const profile = codexRemoteSessionActiveProfile(); const pureApi = String(profile?.relayMode || "") === "pureApi"; - const isExtendedPureApiRequest = requestMethod === "thread/resume" || requestMethod === "turn/start"; - if (isExtendedPureApiRequest && !pureApi) return params; + if (requestMethod === "turn/start" && !pureApi) return params; const hasModelProvider = Object.prototype.hasOwnProperty.call(params, "modelProvider") || Object.prototype.hasOwnProperty.call(params, "model_provider"); if (requestMethod === "turn/start" && !hasModelProvider) return params; @@ -6566,7 +6566,7 @@ displayName: metadata?.displayName || modelName, description: metadata?.description || codexModelCatalog.provider_name || codexModelCatalog.model_provider || "Custom model", hidden: false, - isDefault: (codexModelCatalog.default_model || codexModelCatalog.model) === modelName, + isDefault: false, defaultReasoningEffort: metadata?.defaultReasoningEffort || "medium", supportedReasoningEfforts: modelReasoningEfforts(modelName), }; @@ -6675,13 +6675,6 @@ value.hidden_models = value.hidden_models.filter((name) => !names.includes(name)); if (value.hidden_models.length !== before) changed = true; } - if (value.defaultModel == null && names.length > 0) { - value.defaultModel = codexPlusModelDescriptor(names[0]); - changed = true; - } else if (typeof value.defaultModel === "string" && names.includes(value.defaultModel) && value.model == null) { - value.model = value.defaultModel; - changed = true; - } return changed; } @@ -6748,12 +6741,8 @@ changed = true; } }); - const nextValue = { - ...value, - available_models: availableModels, - default_model: names[0] || value.default_model, - }; - if (!changed && nextValue.default_model === value.default_model) return config; + if (!changed) return config; + const nextValue = { ...value, available_models: availableModels }; try { config.value = nextValue; } catch { diff --git a/crates/codex-plus-core/tests/cdp_bridge.rs b/crates/codex-plus-core/tests/cdp_bridge.rs index e18fda660..c50a1b705 100644 --- a/crates/codex-plus-core/tests/cdp_bridge.rs +++ b/crates/codex-plus-core/tests/cdp_bridge.rs @@ -1673,10 +1673,92 @@ fn injection_script_unlocks_custom_model_catalog() { assert!(!script.contains("function patchObjectGraphForModels")); assert!(!script.contains("window.dispatchEvent = function patchedCodexPlusDispatchEvent")); assert!(script.contains("String(name) === \"107580212\"")); + assert!(script.contains("isDefault: false")); + assert!(!script.contains("value.defaultModel = codexPlusModelDescriptor(names[0])")); + assert!(!script.contains("value.model = value.defaultModel")); + assert!(!script.contains("default_model: value.default_model || names[0]")); + assert!(!script.contains("default_model: names[0] || value.default_model")); assert!(script.contains("window.addEventListener(\"codex-message-from-view\"")); assert!(!script.contains("querySelectorAll(\"button, [role='menu']")); } +#[test] +fn model_whitelist_never_claims_the_host_default_model() { + let script = assets::injection_script(57321); + let start = script + .find("function codexPlusModelDescriptor(modelName)") + .expect("model descriptor patch should exist"); + let end = script[start..] + .find("\n function statsigClients()") + .map(|offset| start + offset) + .expect("model whitelist patch should have a stable end marker"); + let function_source = &script[start..end]; + let temp = tempfile::tempdir().expect("temp dir should be created"); + let harness_path = temp.path().join("model-default-harness.cjs"); + std::fs::write( + &harness_path, + format!( + r#" +const codexModelCatalog = {{ + default_model: "supplier-default", + model: "supplier-default", + provider_name: "Supplier", + model_provider: "custom", +}}; +const codexPlusModelMetadata = () => null; +const modelReasoningEfforts = () => []; +const applyCodexPlusModelMetadata = () => false; +const codexPlusModelNames = () => ["supplier-default", "extra-model"]; +{function_source} + +const missingDefault = {{ value: {{ available_models: ["native-model"] }} }}; +const patchedMissingDefault = patchStatsigModelDynamicConfig(missingDefault); +if (Object.prototype.hasOwnProperty.call(patchedMissingDefault.value, "default_model")) process.exit(2); +if (!patchedMissingDefault.value.available_models.includes("supplier-default")) process.exit(3); +if (!patchedMissingDefault.value.available_models.includes("extra-model")) process.exit(4); + +const existingDefault = {{ value: {{ available_models: ["native-model"], default_model: "thread-model" }} }}; +const patchedExistingDefault = patchStatsigModelDynamicConfig(existingDefault); +if (patchedExistingDefault.value.default_model !== "thread-model") process.exit(5); + +const missingContainerDefault = {{ + models: [{{ model: "native-model", isDefault: true }}], + availableModels: ["native-model"], +}}; +patchModelContainer(missingContainerDefault); +if (Object.prototype.hasOwnProperty.call(missingContainerDefault, "defaultModel")) process.exit(6); +if (Object.prototype.hasOwnProperty.call(missingContainerDefault, "model")) process.exit(7); +const injectedDefault = missingContainerDefault.models.find((item) => item.model === "supplier-default"); +if (!injectedDefault || injectedDefault.isDefault !== false) process.exit(8); + +const hostDefault = {{ model: "native-model" }}; +const hostModel = {{ model: "thread-model" }}; +const existingContainerDefault = {{ + models: [{{ model: "native-model", isDefault: true }}], + availableModels: ["native-model"], + defaultModel: hostDefault, + model: hostModel, +}}; +patchModelContainer(existingContainerDefault); +if (existingContainerDefault.defaultModel !== hostDefault) process.exit(9); +if (existingContainerDefault.model !== hostModel) process.exit(10); +"# + ), + ) + .expect("model default harness should be written"); + + let output = Command::new("node") + .arg(&harness_path) + .output() + .expect("node should run model default harness"); + assert!( + output.status.success(), + "node harness failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + #[test] fn injection_script_exposes_fast_service_tier_control() { let script = assets::injection_script(57321); @@ -1970,12 +2052,14 @@ fn injection_script_applies_fast_service_tier_contract() { assert_eq!(cases["openAiNormalizationDisabled"], true); assert_eq!(cases["refreshedCustomProviderOverride"], "refreshed_vendor"); assert_eq!(cases["refreshedOpenAiProvider"], "openai"); - assert_eq!(cases["refreshedPureApiResumeProvider"], "custom"); + assert_eq!(cases["refreshedPureApiResumeProvider"], "openai"); + assert_eq!(cases["refreshedPureApiResumeModel"], "gpt-5.6-terra"); assert_eq!(cases["failedRefreshProviderUnchanged"], "openai"); assert_eq!(cases["missingActiveProviderUnchanged"], true); assert_eq!(cases["missingActiveRecoveryUnscheduled"], true); assert_eq!(cases["pureApiThreadStartProvider"], "custom"); - assert_eq!(cases["pureApiThreadResumeProvider"], "custom"); + assert_eq!(cases["pureApiThreadResumeUnchanged"], true); + assert_eq!(cases["pureApiNativeResumeUnchanged"], true); assert_eq!(cases["pureApiTurnStartProvider"], "custom"); assert_eq!(cases["pureApiTurnWithoutProviderUnchanged"], true); assert_eq!(cases["pureApiOtherProviderUnchanged"], true); @@ -2495,8 +2579,10 @@ window.__codexSessionDeleteBridge = async (path) => {{ await appServerClient.sendRequest("thread/resume", {{ threadId: "thread-mobile-pure-api-refresh", modelProvider: "openai", + model: "gpt-5.6-terra", }}, {{ signal: "pure-api-switch" }}); const refreshedPureApiResumeProvider = appServerCalls.at(-1)?.params?.modelProvider || ""; +const refreshedPureApiResumeModel = appServerCalls.at(-1)?.params?.model || ""; delete window.__codexSessionDeleteBridge; api.setBackendSettings({{ relayProfilesEnabled: true, @@ -2530,10 +2616,13 @@ api.setBackendSettings({{ }}); const pureApiParams = {{ cwd: "C:/mobile", modelProvider: "openai" }}; const pureApiThreadStartProvider = api.applyProviderOverride("thread/start", pureApiParams)?.modelProvider; -const pureApiThreadResumeProvider = api.applyProviderOverride("thread/resume", {{ +const pureApiThreadResumeParams = {{ threadId: "thread-mobile-pure-api", model_provider: "openai", -}})?.modelProvider; +}}; +const pureApiThreadResumeUnchanged = api.applyProviderOverride("thread/resume", pureApiThreadResumeParams) === pureApiThreadResumeParams; +const pureApiNativeResumeParams = {{ threadId: "thread-mobile-native-resume" }}; +const pureApiNativeResumeUnchanged = api.applyProviderOverride("thread/resume", pureApiNativeResumeParams) === pureApiNativeResumeParams; const pureApiTurnStartProvider = api.applyProviderOverride("turn/start", {{ threadId: "thread-mobile-pure-api", modelProvider: "openai", @@ -2609,11 +2698,13 @@ process.stdout.write(JSON.stringify({{ refreshedCustomProviderOverride, refreshedOpenAiProvider, refreshedPureApiResumeProvider, + refreshedPureApiResumeModel, failedRefreshProviderUnchanged, missingActiveProviderUnchanged, missingActiveRecoveryUnscheduled, pureApiThreadStartProvider, - pureApiThreadResumeProvider, + pureApiThreadResumeUnchanged, + pureApiNativeResumeUnchanged, pureApiTurnStartProvider, pureApiTurnWithoutProviderUnchanged, pureApiOtherProviderUnchanged,