icebear0828 commited on
Commit
94a5578
·
1 Parent(s): a281808

feat: model list auto-sync + theme contrast fix

Browse files

- Backend: auto-write merged catalog back to models.yaml after dynamic fetch
- Frontend: poll /v1/models/catalog every 60s to pick up new models
- Theme: light mode bg #f6f8f6 → #ffffff for clearer dark/light contrast
- Remove unused legacy web/src/hooks/use-status.ts

CHANGELOG.md CHANGED
@@ -14,8 +14,11 @@
14
  - Model-aware 多计划账号路由:不同 plan(free/plus/business)的账号自动路由到各自支持的模型,business 账号可继续使用 gpt-5.4 等高端模型 (#57)
15
  - Structured Outputs 支持:`/v1/chat/completions` 支持 `response_format`(`json_object` / `json_schema`),Gemini 端点支持 `responseMimeType` + `responseSchema`,自动翻译为 Codex Responses API 的 `text.format`;`/v1/responses` 直通 `text` 字段
16
 
 
 
17
  ### Changed
18
 
 
19
  - 提取管道强化:`extract-fingerprint.ts` 新增 fallback 扫描(`.vite/build/*.js` 全文件回退)和 webview 模型发现(`webview/assets/*.js`),pattern 失败不再中断整个流程
20
  - 模型/别名自动添加降级为 semi-auto:后端已通过 `isCodexCompatibleId()` 自动合并新模型,`apply-update.ts` 不再自动写入 `models.yaml`(避免 `mutateYaml` 破坏 YAML 格式)
21
  - Codex Desktop 版本更新至 v26.309.31024 (build 962)
 
14
  - Model-aware 多计划账号路由:不同 plan(free/plus/business)的账号自动路由到各自支持的模型,business 账号可继续使用 gpt-5.4 等高端模型 (#57)
15
  - Structured Outputs 支持:`/v1/chat/completions` 支持 `response_format`(`json_object` / `json_schema`),Gemini 端点支持 `responseMimeType` + `responseSchema`,自动翻译为 Codex Responses API 的 `text.format`;`/v1/responses` 直通 `text` 字段
16
 
17
+ - 模型列表自动同步:后端动态 fetch 成功后自动回写 `config/models.yaml`,静态配置不再滞后;前端每 60s 轮询模型列表,新模型无需刷新页面即可选择
18
+
19
  ### Changed
20
 
21
+ - Light mode 背景色从 `#f6f8f6` 改为纯白 `#ffffff`,增大亮/暗主题视觉差异
22
  - 提取管道强化:`extract-fingerprint.ts` 新增 fallback 扫描(`.vite/build/*.js` 全文件回退)和 webview 模型发现(`webview/assets/*.js`),pattern 失败不再中断整个流程
23
  - 模型/别名自动添加降级为 semi-auto:后端已通过 `isCodexCompatibleId()` 自动合并新模型,`apply-update.ts` 不再自动写入 `models.yaml`(避免 `mutateYaml` 破坏 YAML 格式)
24
  - Codex Desktop 版本更新至 v26.309.31024 (build 962)
package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
  {
2
  "name": "codex-proxy",
3
- "version": "1.0.39",
4
  "lockfileVersion": 3,
5
  "requires": true,
6
  "packages": {
7
  "": {
8
  "name": "codex-proxy",
9
- "version": "1.0.39",
10
  "hasInstallScript": true,
11
  "dependencies": {
12
  "@hono/node-server": "^1.0.0",
 
1
  {
2
  "name": "codex-proxy",
3
+ "version": "1.0.45",
4
  "lockfileVersion": 3,
5
  "requires": true,
6
  "packages": {
7
  "": {
8
  "name": "codex-proxy",
9
+ "version": "1.0.45",
10
  "hasInstallScript": true,
11
  "dependencies": {
12
  "@hono/node-server": "^1.0.0",
shared/hooks/use-status.ts CHANGED
@@ -51,7 +51,7 @@ export function useStatus(accountCount: number) {
51
  const [selectedEffort, setSelectedEffort] = useState("medium");
52
  const [selectedSpeed, setSelectedSpeed] = useState<string | null>(null);
53
 
54
- const loadModels = useCallback(async () => {
55
  try {
56
  // Fetch full catalog for effort info
57
  const catalogResp = await fetch("/v1/models/catalog");
@@ -64,15 +64,25 @@ export function useStatus(accountCount: number) {
64
  const ids: string[] = data.data.map((m: { id: string }) => m.id);
65
  if (ids.length > 0) {
66
  setModels(ids);
67
- const defaultModel = catalogData.find((m) => m.isDefault)?.id ?? ids[0] ?? "";
68
- setSelectedModel(defaultModel);
 
 
 
 
 
 
 
 
69
  }
70
  } catch {
71
- setModels([]);
72
  }
73
  }, []);
74
 
75
  useEffect(() => {
 
 
76
  async function loadStatus() {
77
  try {
78
  const resp = await fetch("/auth/status");
@@ -80,13 +90,18 @@ export function useStatus(accountCount: number) {
80
  if (!data.authenticated) return;
81
  setBaseUrl(`${window.location.origin}/v1`);
82
  setApiKey(data.proxy_api_key || "any-string");
83
- await loadModels();
 
 
 
84
  } catch (err) {
85
  console.error("Status load error:", err);
86
  }
87
  }
88
  loadStatus();
89
- }, [loadModels, accountCount]);
 
 
90
 
91
  // Build model families — group catalog by family, excluding tier variants
92
  const modelFamilies = useMemo((): ModelFamily[] => {
 
51
  const [selectedEffort, setSelectedEffort] = useState("medium");
52
  const [selectedSpeed, setSelectedSpeed] = useState<string | null>(null);
53
 
54
+ const fetchModels = useCallback(async (isInitial: boolean) => {
55
  try {
56
  // Fetch full catalog for effort info
57
  const catalogResp = await fetch("/v1/models/catalog");
 
64
  const ids: string[] = data.data.map((m: { id: string }) => m.id);
65
  if (ids.length > 0) {
66
  setModels(ids);
67
+ if (isInitial) {
68
+ const defaultModel = catalogData.find((m) => m.isDefault)?.id ?? ids[0] ?? "";
69
+ setSelectedModel(defaultModel);
70
+ } else {
71
+ // On refresh: only reset if current selection is no longer available
72
+ setSelectedModel((prev) => {
73
+ if (ids.includes(prev)) return prev;
74
+ return catalogData.find((m) => m.isDefault)?.id ?? ids[0] ?? prev;
75
+ });
76
+ }
77
  }
78
  } catch {
79
+ if (isInitial) setModels([]);
80
  }
81
  }, []);
82
 
83
  useEffect(() => {
84
+ let intervalId: ReturnType<typeof setInterval> | null = null;
85
+
86
  async function loadStatus() {
87
  try {
88
  const resp = await fetch("/auth/status");
 
90
  if (!data.authenticated) return;
91
  setBaseUrl(`${window.location.origin}/v1`);
92
  setApiKey(data.proxy_api_key || "any-string");
93
+ await fetchModels(true);
94
+
95
+ // Refresh model list every 60s to pick up dynamic backend changes
96
+ intervalId = setInterval(() => { fetchModels(false); }, 60_000);
97
  } catch (err) {
98
  console.error("Status load error:", err);
99
  }
100
  }
101
  loadStatus();
102
+
103
+ return () => { if (intervalId) clearInterval(intervalId); };
104
+ }, [fetchModels, accountCount]);
105
 
106
  // Build model families — group catalog by family, excluding tier variants
107
  const modelFamilies = useMemo((): ModelFamily[] => {
src/models/model-store.ts CHANGED
@@ -9,7 +9,7 @@
9
  * Aliases always come from YAML (user-customizable), never from backend.
10
  */
11
 
12
- import { readFileSync } from "fs";
13
  import { resolve } from "path";
14
  import yaml from "js-yaml";
15
  import { getConfig } from "../config.js";
@@ -210,6 +210,45 @@ export function applyBackendModels(backendModels: BackendModelEntry[]): void {
210
  console.log(
211
  `[ModelStore] Merged ${filtered.length} backend (${skipped} non-codex skipped) + ${merged.length - filtered.length} static-only = ${merged.length} total models`,
212
  );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  }
214
 
215
  /**
 
9
  * Aliases always come from YAML (user-customizable), never from backend.
10
  */
11
 
12
+ import { readFileSync, writeFile } from "fs";
13
  import { resolve } from "path";
14
  import yaml from "js-yaml";
15
  import { getConfig } from "../config.js";
 
210
  console.log(
211
  `[ModelStore] Merged ${filtered.length} backend (${skipped} non-codex skipped) + ${merged.length - filtered.length} static-only = ${merged.length} total models`,
212
  );
213
+
214
+ // Auto-sync merged catalog back to models.yaml
215
+ syncStaticModels();
216
+ }
217
+
218
+ /**
219
+ * Write the current merged catalog back to config/models.yaml so it stays
220
+ * up-to-date as a fallback for future cold starts. Fire-and-forget.
221
+ */
222
+ function syncStaticModels(): void {
223
+ const configPath = resolve(getConfigDir(), "models.yaml");
224
+ const today = new Date().toISOString().slice(0, 10);
225
+
226
+ // Strip internal `source` field before serializing
227
+ const models = _catalog.map(({ source: _s, ...rest }) => rest);
228
+
229
+ const header = [
230
+ "# Codex model catalog",
231
+ "#",
232
+ "# Sources:",
233
+ "# 1. Static (below) — baseline for cold starts",
234
+ "# 2. Dynamic — fetched from /backend-api/codex/models, auto-synced here",
235
+ "#",
236
+ `# Last updated: ${today} (auto-synced by model-store)`,
237
+ "",
238
+ ].join("\n");
239
+
240
+ const body = yaml.dump(
241
+ { models, aliases: _aliases },
242
+ { lineWidth: 120, noRefs: true, sortKeys: false },
243
+ );
244
+
245
+ writeFile(configPath, header + body, "utf-8", (err) => {
246
+ if (err) {
247
+ console.warn(`[ModelStore] Failed to sync models.yaml: ${err.message}`);
248
+ } else {
249
+ console.log(`[ModelStore] Synced ${models.length} models to models.yaml`);
250
+ }
251
+ });
252
  }
253
 
254
  /**
web/src/hooks/use-status.ts DELETED
@@ -1,41 +0,0 @@
1
- import { useState, useEffect, useCallback } from "preact/hooks";
2
-
3
- export function useStatus(accountCount: number) {
4
- const [baseUrl, setBaseUrl] = useState("Loading...");
5
- const [apiKey, setApiKey] = useState("Loading...");
6
- const [models, setModels] = useState<string[]>(["codex"]);
7
- const [selectedModel, setSelectedModel] = useState("codex");
8
-
9
- const loadModels = useCallback(async () => {
10
- try {
11
- const resp = await fetch("/v1/models");
12
- const data = await resp.json();
13
- const ids: string[] = data.data.map((m: { id: string }) => m.id);
14
- if (ids.length > 0) {
15
- setModels(ids);
16
- const preferred = ids.find((n) => n === "codex");
17
- if (preferred) setSelectedModel(preferred);
18
- }
19
- } catch {
20
- setModels(["codex"]);
21
- }
22
- }, []);
23
-
24
- useEffect(() => {
25
- async function loadStatus() {
26
- try {
27
- const resp = await fetch("/auth/status");
28
- const data = await resp.json();
29
- if (!data.authenticated) return;
30
- setBaseUrl(`${window.location.origin}/v1`);
31
- setApiKey(data.proxy_api_key || "any-string");
32
- await loadModels();
33
- } catch (err) {
34
- console.error("Status load error:", err);
35
- }
36
- }
37
- loadStatus();
38
- }, [loadModels, accountCount]);
39
-
40
- return { baseUrl, apiKey, models, selectedModel, setSelectedModel };
41
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/tailwind.config.ts CHANGED
@@ -8,7 +8,7 @@ export default {
8
  colors: {
9
  primary: "rgb(var(--primary) / <alpha-value>)",
10
  "primary-hover": "rgb(var(--primary-hover) / <alpha-value>)",
11
- "bg-light": "#f6f8f6",
12
  "bg-dark": "var(--bg-dark, #0d1117)",
13
  "card-dark": "var(--card-dark, #161b22)",
14
  "border-dark": "var(--border-dark, #30363d)",
 
8
  colors: {
9
  primary: "rgb(var(--primary) / <alpha-value>)",
10
  "primary-hover": "rgb(var(--primary-hover) / <alpha-value>)",
11
+ "bg-light": "#ffffff",
12
  "bg-dark": "var(--bg-dark, #0d1117)",
13
  "card-dark": "var(--card-dark, #161b22)",
14
  "border-dark": "var(--border-dark, #30363d)",