Skip to content

Commit 1b78bff

Browse files
MaxwellMaxwell
authored andcommitted
release: v1.0.9
1 parent 6ed1b72 commit 1b78bff

16 files changed

Lines changed: 312 additions & 38 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
# 更新日志
22

33

4+
## v1.0.9 (2026-07-10)
5+
6+
### Bug 修复
7+
8+
- 修复 Codex 合并到 ChatGPT.app 后,直连模式 config.toml 残留 `model = "provider:model"` 格式导致新版报 "Model provider not found" 的问题,切换直连时自动移除带 provider 前缀的 model 值
9+
- 修复直连模式历史对话切换时模型选择组件不稳定,以及带 provider 前缀的模型请求被错误转发到本地 helper 的问题
10+
411
## v1.0.8 (2026-07-02)
512

613
### Bug 修复

Cargo.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ members = [
88
]
99

1010
[workspace.package]
11-
version = "1.0.8"
11+
version = "1.0.9"
1212
edition = "2024"
1313
repository = "https://github.com/Jasoncasper/CodexMate"
1414

apps/codexmate-manager/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codexmate-manager",
3-
"version": "1.0.8",
3+
"version": "1.0.9",
44
"private": true,
55
"type": "module",
66
"scripts": {

apps/codexmate-manager/src-tauri/src/commands.rs

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,22 @@ fn sync_all_session_providers(home: &Path) -> codexmate_data::ProviderSyncResult
382382
codexmate_data::run_provider_sync(Some(home))
383383
}
384384

385+
fn prepare_direct_mode_state(home: &Path) -> anyhow::Result<codexmate_data::ProviderSyncResult> {
386+
let config_path = home.join("config.toml");
387+
let existing = std::fs::read_to_string(&config_path).unwrap_or_default();
388+
let mut doc = codexmate_core::relay_config::parse_toml_document(&existing)?;
389+
codexmate_core::relay_config::set_openai_model_provider_for_direct_mode(&mut doc);
390+
let content = codexmate_core::relay_config::ensure_trailing_newline(doc.to_string());
391+
std::fs::create_dir_all(home)?;
392+
std::fs::write(&config_path, content.as_bytes())?;
393+
394+
let sync = sync_all_session_providers(home);
395+
if sync.status != codexmate_data::ProviderSyncStatus::Synced {
396+
anyhow::bail!("{}", sync.message);
397+
}
398+
Ok(sync)
399+
}
400+
385401
#[allow(dead_code)]
386402
fn strip_model_provider_from_toml(contents: &str) -> String {
387403
let Ok(mut doc) = codexmate_core::relay_config::parse_toml_document(contents) else {
@@ -823,6 +839,14 @@ pub fn get_codex_mode() -> CommandResult<Value> {
823839
let mode = std::fs::read_to_string(&config_path)
824840
.map(|contents| codex_mode_from_config(&contents))
825841
.unwrap_or("direct");
842+
if mode == "direct" {
843+
if let Err(error) = prepare_direct_mode_state(&home) {
844+
return failed(
845+
&format!("直连模式配置修复失败:{error}"),
846+
json!({"mode": "direct"}),
847+
);
848+
}
849+
}
826850
CommandResult {
827851
status: "ok".to_string(),
828852
message: format!(
@@ -914,10 +938,23 @@ pub fn restart_codex(mode: String, request: LaunchRequest) -> CommandResult<Valu
914938
codexmate_core::watcher::stop_codex_processes();
915939
codexmate_core::watcher::stop_launcher_processes();
916940
std::thread::sleep(std::time::Duration::from_millis(800));
941+
let home = codexmate_core::relay_config::default_codex_home_dir();
917942

918943
if mode == "direct" {
944+
if let Err(error) = prepare_direct_mode_state(&home) {
945+
return failed(
946+
&format!("直连模式配置修复失败:{error}"),
947+
json!({"mode": "direct"}),
948+
);
949+
}
919950
let app_path = if request.app_path.trim().is_empty() {
920-
"/Applications/Codex.app".to_string()
951+
let settings = SettingsStore::default().load().unwrap_or_default();
952+
codexmate_core::app_paths::resolve_codex_app_dir_with_saved(
953+
None,
954+
Some(settings.codex_app_path.as_str()),
955+
)
956+
.map(|path| path.to_string_lossy().to_string())
957+
.unwrap_or_else(|| "/Applications/ChatGPT.app".to_string())
921958
} else {
922959
request.app_path.trim().to_string()
923960
};
@@ -1268,7 +1305,9 @@ base_url = "http://127.0.0.1:57321/v1"
12681305
assert!(direct.contains("model_provider = \"openai\""));
12691306
assert!(direct.contains("[model_providers.custom]"));
12701307
assert!(direct.contains("base_url = \"http://127.0.0.1:57321/v1\""));
1271-
assert!(direct.contains("model = \"deepseek-v4-flash:deepseek-v4-flash\""));
1308+
// 直连模式必须移除 "provider:model" 格式的 model 值,否则新版 Codex 报
1309+
// "Model provider 'xxx' not found"
1310+
assert!(!direct.contains("model = \"deepseek-v4-flash:deepseek-v4-flash\""));
12721311
}
12731312

12741313
#[test]
@@ -1303,6 +1342,39 @@ base_url = "http://127.0.0.1:57321/v1"
13031342
assert_eq!(codex_mode_from_config(config), "proxy");
13041343
}
13051344

1345+
#[test]
1346+
fn direct_mode_preparation_repairs_historical_provider() {
1347+
let temp = tempfile::tempdir().unwrap();
1348+
std::fs::write(
1349+
temp.path().join("config.toml"),
1350+
"model_provider = \"openai\"\n",
1351+
)
1352+
.unwrap();
1353+
let db = rusqlite::Connection::open(temp.path().join("state_5.sqlite")).unwrap();
1354+
db.execute(
1355+
"CREATE TABLE threads (id TEXT PRIMARY KEY, model_provider TEXT)",
1356+
[],
1357+
)
1358+
.unwrap();
1359+
db.execute("INSERT INTO threads VALUES ('thread-1', 'custom')", [])
1360+
.unwrap();
1361+
drop(db);
1362+
1363+
let sync = prepare_direct_mode_state(temp.path()).unwrap();
1364+
1365+
assert_eq!(sync.status, codexmate_data::ProviderSyncStatus::Synced);
1366+
assert_eq!(sync.sqlite_rows_updated, 1);
1367+
let db = rusqlite::Connection::open(temp.path().join("state_5.sqlite")).unwrap();
1368+
let provider: String = db
1369+
.query_row(
1370+
"SELECT model_provider FROM threads WHERE id = 'thread-1'",
1371+
[],
1372+
|row| row.get(0),
1373+
)
1374+
.unwrap();
1375+
assert_eq!(provider, "openai");
1376+
}
1377+
13061378
#[test]
13071379
fn sync_all_session_providers_updates_rollouts_at_session_root() {
13081380
let temp = tempfile::tempdir().unwrap();

apps/codexmate-manager/src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "CodexMate",
4-
"version": "1.0.8",
4+
"version": "1.0.9",
55
"identifier": "com.codexmate.manager",
66
"build": {
77
"beforeDevCommand": "npm run vite:dev",

assets/inject/renderer-inject.js

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@
3535
return uniqueValues([modelCatalog.default_model, modelCatalog.model].concat(Array.isArray(modelCatalog.models) ? modelCatalog.models : []));
3636
}
3737

38+
function isProxyMode() {
39+
var provider = typeof modelCatalog.model_provider === "string" ? modelCatalog.model_provider.trim().toLowerCase() : "";
40+
return provider === "custom" || provider === "codexmate";
41+
}
42+
3843
function loadModelCatalog(force) {
3944
if (!force && modelCatalogPromise) return modelCatalogPromise;
4045
if (!force && modelCatalogLoadedAt && Date.now() - modelCatalogLoadedAt < 10000) return Promise.resolve(modelCatalog);
@@ -81,7 +86,7 @@
8186
var existing = {};
8287
for (var i = 0; i < arr.length; i++) { if (arr[i] && arr[i].model) existing[arr[i].model] = true; }
8388
// 代理模式:隐藏 GPT 模型和已禁用模型
84-
var inProxy = !!modelCatalog.model_provider;
89+
var inProxy = isProxyMode();
8590
for (var i = 0; i < arr.length; i++) {
8691
if (arr[i] && arr[i].model) {
8792
var isGpt = /^(gpt|o[1-9]|codex-)/.test(arr[i].model);
@@ -106,7 +111,7 @@
106111
}
107112

108113
function patchModelContainer(value) {
109-
if (!value || typeof value !== "object") return false;
114+
if (!isProxyMode() || !value || typeof value !== "object") return false;
110115
var changed = false;
111116
if (patchModelArray(value.models, "defaultModel" in value || "availableModels" in value)) changed = true;
112117
if (patchModelNameArray(value.models)) changed = true;
@@ -139,6 +144,7 @@
139144
Response.prototype.json = async function patchedJson() {
140145
var payload = await originalJson.apply(this, arguments);
141146
if (!modelUnlockEnabled()) return payload;
147+
if (!isProxyMode()) return payload;
142148
if (!modelNames().length) await loadModelCatalog();
143149
if (!payload || typeof payload !== "object") return payload;
144150
try { patchModelContainer(payload); patchObjectGraph(payload, new WeakSet(), 0); } catch (_) {}
@@ -167,7 +173,7 @@
167173
}
168174

169175
function patchMsgData(data) {
170-
if (!data || data.type !== "mcp-response") return false;
176+
if (!isProxyMode() || !data || data.type !== "mcp-response") return false;
171177
var message = data.message || data.response;
172178
var requestId = message && message.id != null ? String(message.id) : "";
173179
if (modelListRequestIds.size > 0 && requestId && !modelListRequestIds.has(requestId)) return false;
@@ -238,7 +244,7 @@
238244
var originalFetch = window.fetch.bind(window);
239245
window.fetch = async function patchedFetch(input, init) {
240246
try {
241-
if (!modelUnlockEnabled() || appServerModelRequestMethod(input) !== "responses") {
247+
if (!modelUnlockEnabled() || !isProxyMode() || appServerModelRequestMethod(input) !== "responses") {
242248
return originalFetch(input, init);
243249
}
244250
if (!modelNames().length) await loadModelCatalog();
@@ -273,7 +279,7 @@
273279
};
274280
xhr.send = function patchedSend(body) {
275281
try {
276-
if (modelUnlockEnabled() && typeof body === "string" && body.trim()) {
282+
if (modelUnlockEnabled() && isProxyMode() && typeof body === "string" && body.trim()) {
277283
var payload = JSON.parse(body);
278284
var routedPayload = findScopedModelPayload(payload, 0);
279285
if (routedPayload) {
@@ -315,6 +321,7 @@
315321
}
316322

317323
function patchStatsigConfig(config) {
324+
if (!isProxyMode()) return config;
318325
var value = config && config.value;
319326
if (!value || typeof value !== "object") return config;
320327
var changed = false;
@@ -373,12 +380,15 @@
373380
// ===== patchWhitelist (CodexMate flow) =====
374381
function patchWhitelist() {
375382
if (!modelUnlockEnabled()) return;
383+
if (modelCatalog.status === "loading") {
384+
loadModelCatalog();
385+
return;
386+
}
387+
if (!isProxyMode()) return;
376388
installJsonPatch();
377389
installAppServerModelRequestPatch();
378390
installXhrModelRequestPatch();
379391
installMsgPatch();
380-
// 直连模式不注入代理模型到白名单
381-
if (!modelCatalog.model_provider) return;
382392
if (!modelNames().length) { loadModelCatalog(); return; }
383393
patchStatsig();
384394
patchReactState();
@@ -424,7 +434,7 @@
424434
document.body.appendChild(el);
425435
function updateModeLabel() {
426436
loadModelCatalog().then(function () {
427-
label.textContent = modelCatalog.model_provider ? "代理模式" : "直连模式";
437+
label.textContent = isProxyMode() ? "代理模式" : "直连模式";
428438
}).catch(function () {});
429439
}
430440
function poll() {
@@ -443,7 +453,7 @@
443453
function scanLightweight() { injectStyles(); addStatus(); }
444454
function scanDeferred() {
445455
patchWhitelist();
446-
setInterval(function () { if (modelNames().length) { patchStatsig(); patchReactState(); } }, 3000);
456+
setInterval(function () { if (isProxyMode() && modelNames().length) { patchStatsig(); patchReactState(); } }, 3000);
447457
}
448458

449459
function runSafe(fn) { try { fn(); } catch (_) {} }

assets/inject/renderer-inject.test.mjs

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ function createElementStub() {
1515
};
1616
}
1717

18-
async function createHarness(catalog) {
18+
async function createHarness(catalog, responsePayload = {}) {
1919
const fetchCalls = [];
2020
const xhrCalls = [];
2121
const bridgeCalls = [];
@@ -25,7 +25,7 @@ async function createHarness(catalog) {
2525
};
2626
function Response() {}
2727
Response.prototype.json = async function json() {
28-
return {};
28+
return JSON.parse(JSON.stringify(responsePayload));
2929
};
3030
class Request {
3131
constructor(url, init = {}) {
@@ -64,6 +64,8 @@ async function createHarness(catalog) {
6464
URL,
6565
fetch: originalFetch,
6666
globalThis: {},
67+
addEventListener() {},
68+
dispatchEvent() { return true; },
6769
localStorage: { getItem: () => "{}" },
6870
setInterval() {},
6971
requestAnimationFrame: (fn) => fn(),
@@ -109,11 +111,46 @@ assert.equal(
109111
"official models should not be routed to the local helper",
110112
);
111113

114+
const direct = await createHarness(
115+
{
116+
status: "ok",
117+
model: "gpt-5.6",
118+
default_model: "gpt-5.6",
119+
model_provider: "openai",
120+
provider_name: "OpenAI",
121+
models: ["gpt-5.6"],
122+
sources: [],
123+
},
124+
{
125+
models: [{ model: "gpt-5.6", hidden: false }],
126+
availableModels: ["gpt-5.6"],
127+
defaultModel: null,
128+
},
129+
);
130+
const directModelPayload = await new direct.sandbox.Response().json();
131+
assert.deepEqual(
132+
directModelPayload,
133+
{
134+
models: [{ model: "gpt-5.6", hidden: false }],
135+
availableModels: ["gpt-5.6"],
136+
defaultModel: null,
137+
},
138+
"direct mode should not mutate the native model payload",
139+
);
140+
await direct.sandbox.fetch("https://chatgpt.com/backend-api/responses", {
141+
body: JSON.stringify({ model: "custom:gpt-5.6", input: "hello" }),
142+
});
143+
assert.equal(
144+
direct.fetchCalls.at(-1).input,
145+
"https://chatgpt.com/backend-api/responses",
146+
"direct mode should not route scoped models to the local helper",
147+
);
148+
112149
const managed = await createHarness({
113150
status: "ok",
114151
model: "",
115152
default_model: "",
116-
model_provider: "codexmate",
153+
model_provider: "custom",
117154
provider_name: "CodexMate",
118155
models: ["deepseek-v4-pro:gpt-5.4-mini"],
119156
sources: [],

0 commit comments

Comments
 (0)