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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# 更新日志

## 1.2.45 - 2026-08-09

- 恢复聚合供应商模式,修复成员认证、模型交集、最小上下文窗口和失效成员清理。
- 恢复批量 `hi` 模型测试,逐个展示成功、失败、HTTP 状态和耗时。
- 批量 `hi` 测试后可一键删除全部测试失败的模型;删除前会询问确认,取消则保留原模型列表,确认后仍需保存才会写入配置。
- 供应商列表与详情页新增“取消使用”;取消后清理 `config.toml` / `auth.json` 中由供应商写入的字段,并保留其他 Codex 设置。
- 取消后的“无活动供应商”状态会持久保存,后续启动 Codex 不会自动重新写入供应商配置。

## 1.2.22 - 2026-06-28

- 修复启动 Codex 时会自动应用当前供应商配置的问题;现在只有手动点击“使用/切换供应商”才会切换供应商配置。
Expand Down
15 changes: 14 additions & 1 deletion apps/codex-plus-manager/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2722,6 +2722,7 @@ pub fn switch_relay_profile(
let store = SettingsStore::default();
let previous_active_relay_id = request.previous_active_relay_id;
let settings = normalize_settings_before_save(request.settings);
let cancelling = settings.active_relay_id.trim().is_empty();
log_manager_event(
"manager.switch_relay_profile.start",
json!({
Expand All @@ -2746,7 +2747,11 @@ pub fn switch_relay_profile(
}),
);
ok(
"供应商已切换。",
if cancelling {
"已取消当前供应商,并清理 Codex config.toml / auth.json 中的供应商写入信息。"
} else {
"供应商已切换。"
},
relay_switch_payload(result.settings, status, result.backup_path),
)
}
Expand Down Expand Up @@ -3420,6 +3425,10 @@ pub fn apply_relay_injection() -> CommandResult<RelayPayload> {
relay_payload(status, None),
);
}
if !settings.has_active_relay_profile() {
let status = codex_plus_core::relay_config::relay_status_from_home(&home);
return failed("当前没有使用中的供应商。", relay_payload(status, None));
}
prepare_codex_app_state_before_provider_switch(&home, "manager.apply_relay_injection.before");
let relay = settings.active_relay_profile();
log_relay_apply_request("manager.apply_relay_injection", &settings, &relay);
Expand Down Expand Up @@ -3571,6 +3580,10 @@ pub fn apply_pure_api_injection() -> CommandResult<RelayPayload> {
relay_payload(status, None),
);
}
if !settings.has_active_relay_profile() {
let status = codex_plus_core::relay_config::relay_status_from_home(&home);
return failed("当前没有使用中的供应商。", relay_payload(status, None));
}
prepare_codex_app_state_before_provider_switch(
&home,
"manager.apply_pure_api_injection.before",
Expand Down
241 changes: 202 additions & 39 deletions apps/codex-plus-manager/src/App.tsx

Large diffs are not rendered by default.

23 changes: 22 additions & 1 deletion apps/codex-plus-manager/src/i18n-en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@

// Plain strings: t("中文") -> EN_PLAIN["中文"].
export const EN_PLAIN: Record<string, string> = {
"批量 hi 测试": "Batch hi test",
"批量模型测试": "Batch model test",
"删除失败模型": "Delete failed models",
"当前没有测试失败的模型": "There are no failed models to delete",
"等待测试": "Waiting",
"测试中…": "Testing…",
"测试请求失败。": "Test request failed.",
"API Key 模式下扩展插件市场请求,尽量显示完整插件列表;官方/混合模式通常不需要。":
"Expands plugin marketplace requests in API Key mode to show the full plugin list. Usually unnecessary in official/mixed mode.",
"API Key 环境变量": "API Key environment variable",
Expand Down Expand Up @@ -245,8 +252,10 @@ export const EN_PLAIN: Record<string, string> = {
"会话删除、导出、项目移动和脚本能力": "Session deletion, export, project move and script capabilities",
"会话管理": "Session management",
"会话项目移动": "Session project move",
"取消使用": "Stop using",
"取消使用供应商": "Stop using provider",
"取消使用并清理 Codex 写入信息": "Stop using and clear Codex provider data",
"使用": "Use",
"使用中": "In use",
"例如 C:\\Program Files\\WindowsApps\\OpenAI.Codex...\\app": "e.g. C:\\Program Files\\WindowsApps\\OpenAI.Codex...\\app",
"例如 context7": "e.g. context7",
"例如 deepseek-v4-pro": "e.g. deepseek-v4-pro",
Expand Down Expand Up @@ -827,6 +836,14 @@ export const EN_PLAIN: Record<string, string> = {

// Interpolated strings: tf("前缀 {0}", [x]) -> EN_TEMPLATE["前缀 {0}"] with {0} filled.
export const EN_TEMPLATE: Record<string, string> = {
"删除失败模型 ({0})": "Delete failed models ({0})",
"删除 {0} 个测试失败的模型": "Delete {0} failed model(s)",
"删除测试失败的 {0} 个模型?确认后仍需点击保存才会写入配置。": "Delete the {0} failed model(s)? You will still need to click Save to write the change to the configuration.",
"HTTP {0} · {1} ms": "HTTP {0} · {1} ms",
"HTTP {0} · 失败 · {1} ms": "HTTP {0} · failed · {1} ms",
"模型测试完成:成功 {0} 个,失败 {1} 个。": "Model tests completed: {0} succeeded, {1} failed.",
"正在测试 {0}/{1}": "Testing {0}/{1}",
"请求失败 · {0} ms": "Request failed · {0} ms",
"作者:{0} · {1}": "Author: {0} · {1}",
"清单更新于 {0},安装后会保存到“我的主题”。": "Manifest updated {0}. Installed themes are saved under My themes.",
",清理 {0} 条失效任务索引": ", pruned {0} stale task index entry/entries",
Expand Down Expand Up @@ -963,6 +980,10 @@ export const EN_BACKEND: Record<string, string> = {
"配置文件已保存。": "Config file saved.",
"供应商切换锁已损坏,请重启管理器后再试。": "Provider switch lock is corrupted. Please restart the manager and try again.",
"供应商已切换。": "Provider switched.",
"已取消当前供应商,并清理 Codex config.toml / auth.json 中的供应商写入信息。":
"Current provider deactivated and its managed entries removed from Codex config.toml / auth.json.",
"当前没有使用中的供应商。": "No provider is currently active.",
"当前没有使用中的供应商": "No provider is currently active",
"当前供应商已不在配置列表中,已停止切换以避免覆盖用户改动。": "The current provider is no longer in the config list. Switching stopped to avoid overwriting user changes.",
"当前供应商配置已从 live 文件回填。": "Current provider config backfilled from live files.",
"工具与插件列表已读取。": "Tools & plugins list loaded.",
Expand Down
52 changes: 52 additions & 0 deletions apps/codex-plus-manager/src/relay-model-hi.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";

import { failedModelIds, runAllFetchedModelHiTests } from "./relay-model-hi.ts";

describe("batch model hi tests", () => {
it("collects only failed model ids from completed results", () => {
assert.deepEqual(
failedModelIds({
"model-ok": { status: "ok" },
"model-failed": { status: "failed" },
"model-running": { status: "running" },
"model-error": { status: "failed" },
}),
["model-failed", "model-error"],
);
});

it("tests every unique non-empty model returned by the current fetch", async () => {
const tested: string[] = [];

const outcomes = await runAllFetchedModelHiTests(
[" model-a ", "model-b", "", "model-a", "model-c"],
async (model) => {
tested.push(model);
return `${model}-ok`;
},
);

assert.deepEqual(tested, ["model-a", "model-b", "model-c"]);
assert.deepEqual(
outcomes.map((outcome) => outcome.model),
tested,
);
});

it("continues testing remaining fetched models after one request fails", async () => {
const tested: string[] = [];

const outcomes = await runAllFetchedModelHiTests(["model-a", "model-b", "model-c"], async (model) => {
tested.push(model);
if (model === "model-b") throw new Error("upstream failed");
return model;
});

assert.deepEqual(tested, ["model-a", "model-b", "model-c"]);
assert.deepEqual(
outcomes.map((outcome) => outcome.status),
["fulfilled", "rejected", "fulfilled"],
);
});
});
40 changes: 40 additions & 0 deletions apps/codex-plus-manager/src/relay-model-hi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export type ModelHiTestOutcome<T> =
| { model: string; status: "fulfilled"; value: T }
| { model: string; status: "rejected"; reason: unknown };

type ModelHiTestHooks<T> = {
onStart?: (model: string) => void;
onSettled?: (outcome: ModelHiTestOutcome<T>) => void;
};

export function normalizeFetchedModelIds(fetchedModels: readonly string[]): string[] {
return Array.from(new Set(fetchedModels.map((model) => model.trim()).filter(Boolean)));
}

export function failedModelIds(
results: Readonly<Record<string, { status: string }>>,
): string[] {
return Object.entries(results)
.filter(([, result]) => result.status === "failed")
.map(([model]) => model);
}

export async function runAllFetchedModelHiTests<T>(
fetchedModels: readonly string[],
testModel: (model: string) => Promise<T>,
hooks: ModelHiTestHooks<T> = {},
): Promise<ModelHiTestOutcome<T>[]> {
const outcomes: ModelHiTestOutcome<T>[] = [];
for (const model of normalizeFetchedModelIds(fetchedModels)) {
hooks.onStart?.(model);
let outcome: ModelHiTestOutcome<T>;
try {
outcome = { model, status: "fulfilled", value: await testModel(model) };
} catch (reason) {
outcome = { model, status: "rejected", reason };
}
outcomes.push(outcome);
hooks.onSettled?.(outcome);
}
return outcomes;
}
62 changes: 61 additions & 1 deletion apps/codex-plus-manager/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -974,7 +974,7 @@ body {
}

.relay-use-button {
min-width: 76px;
min-width: 92px;
}

.relay-card-extra {
Expand Down Expand Up @@ -1464,6 +1464,62 @@ body {
justify-self: start;
}

.relay-model-test-results {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 6px;
margin-top: 8px;
}

.relay-model-test-result {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
min-width: 0;
min-height: 34px;
border: 1px solid hsl(var(--border));
border-radius: 6px;
background: hsl(var(--background));
padding: 7px 9px;
}

.relay-model-test-result code {
min-width: 0;
overflow: hidden;
color: hsl(var(--foreground));
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}

.relay-model-test-result span {
flex: 0 0 auto;
color: hsl(var(--muted-foreground));
font-size: 12px;
white-space: nowrap;
}

.relay-model-test-result.running {
border-color: hsl(var(--primary) / 0.55);
}

.relay-model-test-result.ok {
border-color: hsl(156 64% 42% / 0.45);
}

.relay-model-test-result.ok span {
color: hsl(156 64% 42%);
}

.relay-model-test-result.failed {
border-color: hsl(0 72% 55% / 0.45);
}

.relay-model-test-result.failed span {
color: hsl(0 72% 55%);
}

.relay-field-official-key {
grid-column: 1;
}
Expand Down Expand Up @@ -3473,6 +3529,10 @@ body {
min-width: 0;
}

.relay-model-test-results {
grid-template-columns: 1fr;
}

.shell {
grid-template-columns: 64px minmax(0, 1fr);
}
Expand Down
14 changes: 12 additions & 2 deletions crates/codex-plus-core/src/launcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,9 @@ where
if settings.computer_use_guard_enabled {
hooks.ensure_computer_use_config(&settings).await?;
}
if settings.has_active_relay_profile() {
hooks.apply_active_relay_profile(&settings).await?;
}
match crate::codex_sqlite::sanitize_historical_model_suffixes(&home) {
Ok(result) if result.updated > 0 => {
let _ = crate::diagnostic_log::append_diagnostic_log(
Expand Down Expand Up @@ -418,6 +421,9 @@ fn relay_protocol_proxy_enabled(settings: &BackendSettings) -> bool {
}

fn remote_control_provider_proxy_enabled(settings: &BackendSettings) -> bool {
if !settings.has_active_relay_profile() {
return false;
}
let profile = settings.active_relay_profile();
profile.relay_mode == crate::settings::RelayMode::Official && profile.official_mix_api_key
}
Expand Down Expand Up @@ -549,10 +555,14 @@ impl LaunchHooks for DefaultLaunchHooks {
}

async fn apply_active_relay_profile(&self, settings: &BackendSettings) -> anyhow::Result<()> {
if !settings.relay_profiles_enabled {
if !settings.relay_profiles_enabled || !settings.has_active_relay_profile() {
return Ok(());
}
let profile = settings.active_relay_profile();
let profile = settings.active_relay_profile_with_aggregate_models();
if profile.relay_mode == crate::settings::RelayMode::Aggregate {
crate::relay_rotation::RelayRotationSelector::from_settings(settings)
.context("聚合供应商配置无效")?;
}
let home = crate::relay_config::default_codex_home_dir();
let common_config = crate::relay_config::normalize_config_text(
&[
Expand Down
18 changes: 10 additions & 8 deletions crates/codex-plus-core/src/model_catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,16 @@ pub async fn read_codex_model_catalog() -> Value {
let settings_path = crate::paths::default_settings_path();
if settings_path.exists() {
if let Ok(settings) = SettingsStore::new(settings_path).load() {
let profile = settings.active_relay_profile();
let catalog = relay_profile_model_catalog_value(&home, &profile);
if catalog
.get("models")
.and_then(Value::as_array)
.map_or(false, |m| !m.is_empty())
{
return catalog;
if settings.has_active_relay_profile() {
let profile = settings.active_relay_profile();
let catalog = relay_profile_model_catalog_value(&home, &profile);
if catalog
.get("models")
.and_then(Value::as_array)
.map_or(false, |m| !m.is_empty())
{
return catalog;
}
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions crates/codex-plus-core/src/protocol_proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,9 @@ pub async fn open_chat_completions_proxy_request(
original_user_agent: Option<&str>,
) -> anyhow::Result<UpstreamProxyResponse> {
let settings = SettingsStore::default().load().unwrap_or_default();
if !settings.has_active_relay_profile() {
anyhow::bail!("当前没有使用中的供应商");
}
let relay = settings.active_relay_profile();
if relay.protocol != RelayProtocol::ChatCompletions {
anyhow::bail!("当前中转未启用 Chat Completions 协议代理");
Expand Down
Loading