Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions apps/codex-plus-manager/src-tauri/tests/windows_subsystem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,3 +365,14 @@ fn manager_update_install_keeps_visible_progress_bar() {
assert!(app_tsx.contains("completedTitle={t(\"上次更新结果\")}"));
assert!(app_tsx.contains("progress={updateInstallProgress}"));
}

#[test]
fn model_routing_provider_edits_keep_the_live_router_config() {
let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let app_tsx = manifest_dir.parent().unwrap().join("src/App.tsx");
let app_tsx = std::fs::read_to_string(&app_tsx).expect("read manager App.tsx");

assert!(app_tsx.contains("if (!switchSettings.modelRoutingEnabled)"));
assert!(app_tsx.contains("if (form.modelRoutingEnabled)"));
assert!(app_tsx.contains("await actions.switchRelayProfile(next, form.activeRelayId)"));
}
43 changes: 34 additions & 9 deletions apps/codex-plus-manager/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ type BackendSettings = {
providerSyncManualProviders: string[];
providerSyncLastSelectedProvider: string;
relayProfilesEnabled: boolean;
modelRoutingEnabled: boolean;
enhancementsEnabled: boolean;
computerUseGuardEnabled: boolean;
codexAppPluginMarketplaceUnlock: boolean;
Expand Down Expand Up @@ -760,6 +761,7 @@ const defaultSettings: BackendSettings = {
providerSyncManualProviders: [],
providerSyncLastSelectedProvider: "",
relayProfilesEnabled: true,
modelRoutingEnabled: false,
enhancementsEnabled: true,
computerUseGuardEnabled: false,
codexAppPluginMarketplaceUnlock: true,
Expand Down Expand Up @@ -2206,7 +2208,9 @@ export function App() {
showNotice(t("供应商配置可能不正确"), validationError, "failed");
return;
}
switchSettings = await snapshotActiveRelayFilesBeforeSwitch(switchSettings, previousActiveRelayId);
if (!switchSettings.modelRoutingEnabled) {
switchSettings = await snapshotActiveRelayFilesBeforeSwitch(switchSettings, previousActiveRelayId);
}
const selectedAfterSave = activeRelayProfile(switchSettings);
const command = relayProfileSwitchCommand(selectedAfterSave);

Expand Down Expand Up @@ -3260,6 +3264,22 @@ function RelayScreen({
</span>
<ToggleVisual />
</label>
<label className="switch-row relay-master-switch">
<input
checked={normalized.modelRoutingEnabled}
disabled={!normalized.relayProfilesEnabled}
onChange={(event) => {
const next = { ...normalized, modelRoutingEnabled: event.currentTarget.checked };
void saveRelaySettings(next);
}}
type="checkbox"
/>
<span>
<strong>{t("按模型自动路由供应商")}</strong>
<small>{t("开启后,模型列表中的模型自动走所属第三方供应商;未匹配模型走 ChatGPT 官方登录。重启 Codex++ 后生效。")}</small>
</span>
<ToggleVisual />
</label>
<div className="relay-add-row">
<Button
variant="secondary"
Expand Down Expand Up @@ -5375,14 +5395,18 @@ function RelayProfileDetail({
const next = isNew
? addRelayProfile(form, normalizedDraft)
: updateRelayProfile(form, profile.id, normalizedDraft);
await onFormChange(next);
if (isActive && relayProfileUsesLiveFiles(normalizedDraft)) {
await actions.saveRelayFile(
"config",
effectiveRelayConfigPreview(normalizedDraft, form, normalizedDraft),
true,
);
await actions.saveRelayFile("auth", normalizedDraft.authContents, true);
if (form.modelRoutingEnabled) {
await actions.switchRelayProfile(next, form.activeRelayId);
} else {
await onFormChange(next);
if (isActive && relayProfileUsesLiveFiles(normalizedDraft)) {
await actions.saveRelayFile(
"config",
effectiveRelayConfigPreview(normalizedDraft, form, normalizedDraft),
true,
);
await actions.saveRelayFile("auth", normalizedDraft.authContents, true);
}
}
onSaved?.();
};
Expand Down Expand Up @@ -7626,6 +7650,7 @@ function normalizeSettings(settings: BackendSettings): BackendSettings {
...defaultSettings,
...settings,
relayProfilesEnabled: settings.relayProfilesEnabled !== false,
modelRoutingEnabled: settings.modelRoutingEnabled === true,
computerUseGuardEnabled: settings.computerUseGuardEnabled === true,
codexAppImageOverlayOpacity: clampNumber(settings.codexAppImageOverlayOpacity || 35, 1, 100),
codexAppImageOverlayFitMode: normalizeImageOverlayFitMode(settings.codexAppImageOverlayFitMode),
Expand Down
36 changes: 35 additions & 1 deletion crates/codex-plus-core/src/launcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,9 @@ where
);
}
}
if settings.model_routing_enabled {
hooks.apply_active_relay_profile(&settings).await?;
}
let protocol_proxy_enabled = relay_protocol_proxy_enabled(&settings);
if protocol_proxy_enabled {
helper_port = crate::protocol_proxy::DEFAULT_PROTOCOL_PROXY_PORT;
Expand Down Expand Up @@ -536,6 +539,16 @@ impl LaunchHooks for DefaultLaunchHooks {
.collect::<Vec<_>>()
.join("\n\n"),
);
if settings.model_routing_enabled {
let router = crate::relay_config::model_router_profile(settings, &home);
crate::relay_config::apply_relay_profile_to_home_with_switch_rules_and_computer_use_guard(
&home,
&router,
&common_config,
settings.computer_use_guard_enabled,
)?;
return Ok(());
}
if profile.relay_mode == crate::settings::RelayMode::Official
&& !profile.official_mix_api_key
{
Expand Down Expand Up @@ -1004,6 +1017,7 @@ async fn handle_helper_connection(
let path = raw_path.split('?').next().unwrap_or(raw_path);
let request_user_agent = header_value_from_headers(&request_headers, "user-agent");
let request_content_type = header_value_from_headers(&request_headers, "content-type");
let request_proxy_headers = header_pairs_from_headers(&request_headers);
let remote_addr_text = remote_addr.map(|addr| addr.to_string());

let _ = crate::diagnostic_log::append_diagnostic_log(
Expand Down Expand Up @@ -1035,6 +1049,7 @@ async fn handle_helper_connection(
&mut stream,
&request_body,
request_user_agent.as_deref(),
&request_proxy_headers,
method,
path,
remote_addr_text,
Expand Down Expand Up @@ -1346,14 +1361,17 @@ async fn handle_protocol_proxy_connection(
stream: &mut tokio::net::TcpStream,
request_body: &str,
request_user_agent: Option<&str>,
request_headers: &[(String, String)],
method: &str,
path: &str,
remote_addr_text: Option<String>,
) -> anyhow::Result<()> {
let request_json = serde_json::from_str::<serde_json::Value>(request_body).ok();
let upstream = match crate::protocol_proxy::open_responses_proxy_request(
let upstream = match crate::protocol_proxy::open_responses_proxy_request_with_client_context(
request_body,
request_user_agent,
path,
request_headers,
)
.await
{
Expand Down Expand Up @@ -2074,6 +2092,22 @@ fn scan_chunked_body(encoded: &[u8]) -> Result<ChunkedBodyScan, HttpRequestReadE
ChunkedScanState::default().advance(encoded)
}

fn header_pairs_from_headers(headers: &str) -> Vec<(String, String)> {
headers
.lines()
.skip(1)
.filter_map(|line| {
let (name, value) = line.split_once(':')?;
let name = name.trim();
let value = value.trim();
if name.is_empty() || value.is_empty() {
return None;
}
Some((name.to_string(), value.to_string()))
})
.collect()
}

fn header_value_from_headers(headers: &str, header_name: &str) -> Option<String> {
headers
.lines()
Expand Down
Loading