Spaces:
Paused
Paused
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 +3 -0
- package-lock.json +2 -2
- shared/hooks/use-status.ts +21 -6
- src/models/model-store.ts +40 -1
- web/src/hooks/use-status.ts +0 -41
- web/tailwind.config.ts +1 -1
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.
|
| 4 |
"lockfileVersion": 3,
|
| 5 |
"requires": true,
|
| 6 |
"packages": {
|
| 7 |
"": {
|
| 8 |
"name": "codex-proxy",
|
| 9 |
-
"version": "1.0.
|
| 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
|
| 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 |
-
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
| 84 |
} catch (err) {
|
| 85 |
console.error("Status load error:", err);
|
| 86 |
}
|
| 87 |
}
|
| 88 |
loadStatus();
|
| 89 |
-
|
|
|
|
|
|
|
| 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": "#
|
| 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)",
|