@bitkyc08/opencodex 1.9.5 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.zh-CN.md CHANGED
@@ -1,27 +1,16 @@
1
- <div align="center">
2
-
3
- <picture>
4
- <source media="(prefers-color-scheme: dark)" srcset="assets/logo-dark.png">
5
- <img alt="opencodex" src="assets/logo-light.png" width="96" height="96">
6
- </picture>
7
-
8
- # opencodex (`ocx`)
9
-
10
- **面向 [OpenAI Codex](https://openai.com/codex) 的通用 provider proxy —— 在 Codex CLI、App 和 SDK 中使用任意 LLM。**
11
-
12
- [English](README.md) · [한국어](README.ko.md) · **简体中文**
13
-
14
- 📖 **[完整文档 →](https://lidge-jun.github.io/opencodex/zh-cn/)**
1
+ <p align="center">
2
+ <img src="assets/banner.png" alt="opencodex — 让 Codex 接入任意 LLM" width="820">
3
+ </p>
15
4
 
16
- </div>
5
+ <p align="center">
6
+ <a href="README.md">English</a> · <a href="README.ko.md">한국어</a> · <b>简体中文</b> · 📖 <a href="https://lidge-jun.github.io/opencodex/zh-cn/"><b>完整文档 →</b></a>
7
+ </p>
17
8
 
18
9
  <p align="center">
19
- <img src="assets/dashboard.png" alt="opencodex 控制台 —— 展示代理状态、provider 与可用模型的深色控制面板" width="820">
10
+ <img src="assets/architecture.png" alt="opencodex 架构 Codex CLI 通过 opencodex 代理路由到任意 LLM 提供商" width="820">
20
11
  </p>
21
12
 
22
- Codex 只能使用 Responses API(`/v1/responses`)。opencodex 位于 Codex 与你的 LLM
23
- provider 之间,实时翻译两者之间的协议 —— 包括 streaming、工具调用、推理(reasoning)和图像
24
- —— 并且是双向的。
13
+ Codex 只认 Responses API(`/v1/responses`)。opencodex 做的事情很简单:架在 Codex 和你的 LLM provider 中间,把协议实时翻译过去——streaming、tool 调用、reasoning、图片,全都覆盖,双向通信。
25
14
 
26
15
  ```
27
16
  Codex CLI / App / SDK ──/v1/responses──▶ opencodex ──▶ Any provider
@@ -30,24 +19,34 @@ Codex CLI / App / SDK ──/v1/responses──▶ opencodex ──▶ Any provi
30
19
  OpenRouter · Azure · DeepSeek · GLM · …and OpenAI itself
31
20
  ```
32
21
 
22
+ ## 支持平台
23
+
24
+ | 操作系统 | 状态 | 服务管理 |
25
+ |---|---|---|
26
+ | macOS (arm64 / x64) | 完整支持 | launchd |
27
+ | Linux (x64 / arm64) | 完整支持 | systemd(用户级) |
28
+ | Windows (x64) | 完整支持 | Task Scheduler |
29
+
30
+ 需要 [Bun](https://bun.sh) 1.1+。三个平台都原生运行(Windows 不需要 WSL)。
31
+
33
32
  ## 快速开始
34
33
 
35
34
  ```bash
36
- # Install
37
- npm install -g @bitkyc08/opencodex # or: bun install -g @bitkyc08/opencodex
35
+ # 安装
36
+ npm install -g @bitkyc08/opencodex # 或者: bun install -g @bitkyc08/opencodex
38
37
 
39
- # Interactive setup (writes config + injects into Codex)
38
+ # 交互式初始化(写入配置 + 注入 Codex
40
39
  ocx init
41
40
 
42
- # Start the proxy
41
+ # 启动代理
43
42
  ocx start
44
43
 
45
- # Use Codex normally it now routes through opencodex
44
+ # 正常使用 Codex —— 请求已经通过 opencodex 路由
46
45
  codex "Write a hello world in Rust"
47
46
  ```
48
47
 
49
48
  <details>
50
- <summary><b>没有 <a href="https://bun.sh">bun</a>?</b> —— 先安装它(opencodex 运行在 bun 上)</summary>
49
+ <summary><b>还没装 <a href="https://bun.sh">bun</a>?</b> —— 先装一下(opencodex 跑在 bun 上)</summary>
51
50
 
52
51
  <br/>
53
52
 
@@ -59,37 +58,51 @@ curl -fsSL https://bun.sh/install | bash
59
58
  powershell -c "irm bun.sh/install.ps1 | iex"
60
59
  ```
61
60
 
62
- 然后重新运行 `npm install -g @bitkyc08/opencodex`。(`ocx` 可执行文件是 bun 原生的,因此 bun 必须在你的 `PATH` 中。)
61
+ 装完之后重新跑 `npm install -g @bitkyc08/opencodex`。(`ocx` bun 原生二进制,所以 bun 必须在你的 `PATH` 里。)
63
62
 
64
63
  </details>
65
64
 
66
- 使用 `provider/model` 形式指定一个具体的已路由模型:
65
+ ## 亮点
66
+
67
+ - **一个代理,20+ provider。** Anthropic、Google、xAI、Kimi、Ollama Cloud、Groq、Azure、DeepSeek、OpenRouter……装一次就全通了。
68
+ - **5 种 adapter 覆盖一切。** Anthropic Messages、Google Gemini、Azure、OpenAI Responses 直通,以及**所有 OpenAI 兼容 Chat Completions** 端点——不管你用什么 LLM,总有一个 adapter 能接上。
69
+ - **三种认证方式,随你挑。** OAuth 登录(xAI / Anthropic / Kimi,token 自动刷新)、转发 `codex login`、或直接粘贴 API key(支持 `${ENV_VARS}`)。内置 18 家 provider 的 API key 目录(含 **Ollama Cloud**)。
70
+ - **即插即用 Codex 全家桶。** 自动向 `~/.codex/config.toml` 注入 `[model_providers.opencodex]`,并写入共享模型目录——路由模型直接出现在 Codex 的模型选择器里,CLI、TUI、App、SDK 全部适用。
71
+ - **Subagent 控制。** 在 `subagentModels` 或 Web 仪表盘中,把最多 5 个路由/原生模型置顶到 Codex 的 `spawn_agent` 选择器。
72
+ - **Sidecar 能力加持。** 非 OpenAI 模型也能拥有真正的**网页搜索**和**图片理解**——通过你的 ChatGPT 登录借用一个 `gpt-5.4-mini` 来实现。
73
+ - **Web 仪表盘。** 管理 provider、OAuth 登录、模型选择、请求日志,都在浏览器里完成。
74
+ - **HTTP/SSE 为默认,WebSocket 按需开启。** 只有显式设置 `"websockets": true` 时,代理才会广告 `supports_websockets`。
75
+ - **干净退出,零残留。** `ocx stop`(或仪表盘的 Stop 按钮)会关闭代理、停止后台服务(如果有的话)、并将 Codex 恢复为原始配置。之后 `codex` 命令就像从未安装过 opencodex 一样正常工作。
76
+
77
+ ## 添加 Provider
78
+
79
+ 最简单的方式:用 Web 仪表盘。
67
80
 
68
81
  ```bash
69
- codex -m "anthropic/claude-opus-4-8" "Explain this stack trace"
70
- codex -m "ollama-cloud/glm-5.2" "Write a SQL migration"
82
+ ocx gui # 在浏览器中打开 localhost:10100
71
83
  ```
72
84
 
73
- ## 亮点
85
+ 仪表盘提供 20+ 内置 provider 模板(Anthropic、Google、xAI、Kimi、Ollama Cloud、Groq、DeepSeek、OpenRouter 等等)。选一个,填入 API key 或用 OAuth 登录,保存即可。opencodex 会自动发现该 provider 支持的模型,并同步到 Codex 的模型选择器中。
86
+
87
+ 如果你更习惯手动配置,直接编辑 `~/.opencodex/config.json`,在 `providers` 对象中添加一项即可。详见下方[配置](#配置)章节。
88
+
89
+ ## 模型路由
74
90
 
75
- - **五种 adapter**,覆盖 Anthropic Messages、Google Gemini、Azure、OpenAI Responses 直通(passthrough),
76
- 以及**所有 OpenAI 兼容的 Chat Completions** 端点。
77
- - **OAuth、API key 或 ChatGPT 转发(forward)。** 用你的 xAI / Anthropic / Kimi 账户登录(token
78
- 自动刷新)、转发你的 `codex login`,或直接粘贴一个 key(支持 `${ENV_VARS}`)。内置一份 18 家 provider 的
79
- API-key 目录(含 **Ollama Cloud**)。
80
- - **无缝接入 Codex CLI、TUI、App 和 SDK。** 向 `$CODEX_HOME/config.toml`(默认 `~/.codex/config.toml`)
81
- 注入一个 `[model_providers.opencodex]` 表,并写入共享的 Codex 模型目录,让已路由的模型出现在
82
- Codex 模型选择器中。
83
- - **Subagent 控制。** 通过 `subagentModels` 或 Web 仪表盘,将最多 5 个路由或原生模型优先展示在
84
- Codex 的 `spawn_agent` 选择器中。
85
- - **默认 HTTP/SSE,WebSocket 需要显式开启。** 代理有 Responses WebSocket 端点,但只有设置
86
- `"websockets": true` 时才会广告 `supports_websockets`。
87
- - **Sidecars。** 通过基于你 ChatGPT 登录的 `gpt-5.4-mini`,为非 OpenAI 模型提供真正的**网页搜索**和**图像理解**能力。
88
- - **Web 仪表盘**,用于管理 provider、OAuth 登录、模型选择和请求日志。
89
-
90
- ## Providers 与 adapters
91
-
92
- | Provider | Adapter | Auth |
91
+ 通过 `provider/model` 格式指定路由模型,在 Codex 中直接使用:
92
+
93
+ ```bash
94
+ codex -m "anthropic/claude-opus-4-8" "解释这个 stack trace"
95
+ codex -m "google/gemini-2.5-pro" "重构这段代码"
96
+ codex -m "xai/grok-4" "写一个 SQL migration"
97
+ codex -m "ollama-cloud/glm-5.2" "生成单元测试"
98
+ codex -m "deepseek/deepseek-r1" "分析这个性能瓶颈"
99
+ ```
100
+
101
+ 不指定 provider 前缀时,Codex 使用你配置的 `defaultProvider` `defaultModel`。
102
+
103
+ ## Provider adapter
104
+
105
+ | Provider | Adapter | 认证方式 |
93
106
  |---|---|---|
94
107
  | OpenAI(ChatGPT 登录) | `openai-responses` | 转发(无需 key) |
95
108
  | OpenAI(API key) | `openai-responses` | key |
@@ -105,22 +118,24 @@ codex -m "ollama-cloud/glm-5.2" "Write a SQL migration"
105
118
  ## CLI
106
119
 
107
120
  ```bash
108
- ocx init # interactive setup
109
- ocx start [--port 10100] # start the proxy
110
- ocx stop # stop + restore native Codex
111
- ocx restore # restore without stopping (alias: ocx eject)
112
- ocx sync # refresh models + re-inject into Codex
113
- ocx status # is the proxy running?
114
- ocx login <xai|anthropic|kimi> # OAuth login
115
- ocx logout <provider> # remove a stored login
116
- ocx gui # open the web dashboard
117
- ocx service <install|start|stop|status|uninstall> # run as a background service
118
- ocx update # update opencodex to the latest published version
121
+ ocx init # 交互式初始化
122
+ ocx start [--port 10100] # 启动代理
123
+ ocx stop # 停止并恢复原生 Codex 配置
124
+ ocx restore # 仅恢复,不停止(别名:ocx eject
125
+ ocx sync # 刷新模型列表 + 重新注入 Codex
126
+ ocx status # 查看代理是否在运行
127
+ ocx login <xai|anthropic|kimi> # OAuth 登录
128
+ ocx logout <provider> # 移除已保存的登录
129
+ ocx gui # 打开 Web 仪表盘
130
+ ocx service <install|start|stop|status|uninstall> # 后台服务(launchd/systemd/schtasks)
131
+ ocx update # 更新到最新版
119
132
  ```
120
133
 
121
134
  ## 配置
122
135
 
123
- 配置文件位于 `~/.opencodex/config.json`。最小示例:
136
+ 配置文件路径:`~/.opencodex/config.json`。
137
+
138
+ **云端 provider 示例:**
124
139
 
125
140
  ```json
126
141
  {
@@ -143,18 +158,34 @@ ocx update # update opencodex to the latest published versio
143
158
  }
144
159
  ```
145
160
 
146
- WebSocket 传输默认关闭。只有当你希望 Codex 广告并使用 Responses WebSocket 路径而不是 HTTP/SSE 时,
147
- 才需要设置 `"websockets": true`。
161
+ **本地 provider 示例(Ollama / vLLM / LM Studio):**
162
+
163
+ ```json
164
+ {
165
+ "port": 10100,
166
+ "defaultProvider": "local",
167
+ "providers": {
168
+ "local": {
169
+ "adapter": "openai-chat",
170
+ "baseUrl": "http://localhost:11434/v1",
171
+ "apiKey": "",
172
+ "defaultModel": "qwen3:32b"
173
+ }
174
+ }
175
+ }
176
+ ```
177
+
178
+ 本地 provider 的 `apiKey` 通常留空。只要你的本地服务暴露了 OpenAI 兼容的 Chat Completions 端点,opencodex 就能直接对接。
179
+
180
+ WebSocket 传输默认关闭。只有当你希望 Codex 使用 Responses WebSocket 而不是 HTTP/SSE 时,才需要设置 `"websockets": true`。
148
181
 
149
- 每个字段的说明请参阅 **[配置参考](https://lidge-jun.github.io/opencodex/zh-cn/reference/configuration/)**。
182
+ 每个字段的详细说明参阅 **[配置参考](https://lidge-jun.github.io/opencodex/zh-cn/reference/configuration/)**。
150
183
 
151
184
  ## 文档
152
185
 
153
- 公开文档 —— 安装、providers、路由、sidecars、Codex 集成、Codex App 模型选择器,
154
- 以及 CLI/配置参考 —— 是位于 [`docs-site/`](./docs-site) 下的一个 Astro 站点,
155
- 并发布于 **[lidge-jun.github.io/opencodex](https://lidge-jun.github.io/opencodex/zh-cn/)**。
186
+ 完整文档——安装、provider 配置、路由、sidecar、Codex 集成、Codex App 模型选择器、CLI/配置参考——由 [`docs-site/`](./docs-site) 目录下的 Astro 站点构建,发布在 **[lidge-jun.github.io/opencodex](https://lidge-jun.github.io/opencodex/zh-cn/)**。
156
187
 
157
- 维护者 source of truth 位于 [`structure/`](./structure),历史调查/诊断笔记保留在 [`docs/`](./docs)。
188
+ 维护者 source of truth 位于 [`structure/`](./structure),历史调查和诊断笔记保留在 [`docs/`](./docs)。
158
189
 
159
190
  ## 开发
160
191
 
@@ -162,11 +193,11 @@ WebSocket 传输默认关闭。只有当你希望 Codex 广告并使用 Response
162
193
  git clone https://github.com/lidge-jun/opencodex.git
163
194
  cd opencodex
164
195
  bun install
165
- bun run dev # start the proxy in dev mode
166
- bun x tsc --noEmit # typecheck
196
+ bun run dev # 以开发模式启动代理
197
+ bun x tsc --noEmit # 类型检查
167
198
  ```
168
199
 
169
- 请参阅 **[贡献指南](https://lidge-jun.github.io/opencodex/zh-cn/contributing/)**。
200
+ 参阅 **[贡献指南](https://lidge-jun.github.io/opencodex/zh-cn/contributing/)**。
170
201
 
171
202
  ## 许可证
172
203
 
@@ -0,0 +1 @@
1
+ :root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;--bg:var(--lightningcss-light,#f6f7f9)var(--lightningcss-dark,#0c0d11);--rail:var(--lightningcss-light,#fff)var(--lightningcss-dark,#101218);--surface:var(--lightningcss-light,#fff)var(--lightningcss-dark,#15171d);--raised:var(--lightningcss-light,#f1f2f5)var(--lightningcss-dark,#1c1f27);--raised-hover:var(--lightningcss-light,#e8eaee)var(--lightningcss-dark,#242833);--border:var(--lightningcss-light,#e2e4e9)var(--lightningcss-dark,#2a2e39);--border-soft:var(--lightningcss-light,#ededf1)var(--lightningcss-dark,#20242d);--hover:var(--lightningcss-light,#11131c09)var(--lightningcss-dark,#ffffff06);--text:var(--lightningcss-light,#16181d)var(--lightningcss-dark,#edeef2);--muted:var(--lightningcss-light,#5b6270)var(--lightningcss-dark,#a3a9b5);--faint:var(--lightningcss-light,#868d9b)var(--lightningcss-dark,#6b7280);--accent:var(--lightningcss-light,#4f46e5)var(--lightningcss-dark,#6366f1);--accent-hover:var(--lightningcss-light,#4338ca)var(--lightningcss-dark,#818cf8);--accent-ink:#fff;--accent-soft:var(--lightningcss-light,#4f46e51a)var(--lightningcss-dark,#6366f129);--accent-ring:var(--lightningcss-light,#4f46e566)var(--lightningcss-dark,#6366f180);--green:var(--lightningcss-light,#047857)var(--lightningcss-dark,#34d399);--green-soft:var(--lightningcss-light,#0596691a)var(--lightningcss-dark,#34d39921);--red:var(--lightningcss-light,#b91c1c)var(--lightningcss-dark,#f87171);--red-soft:var(--lightningcss-light,#b91c1c17)var(--lightningcss-dark,#f8717121);--amber:var(--lightningcss-light,#b45309)var(--lightningcss-dark,#fbbf24);--amber-soft:var(--lightningcss-light,#b453091a)var(--lightningcss-dark,#fbbf2421);--radius:8px;--radius-sm:5px;--radius-xs:4px;--font:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui, "Helvetica Neue", sans-serif;--mono:ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Code", Menlo, Consolas, monospace;--shadow:0 1px 2px var(--lightningcss-light,#1018280f)var(--lightningcss-dark,#00000080), 0 10px 28px var(--lightningcss-light,#10182812)var(--lightningcss-dark,#0000004d);--shadow-sm:0 1px 2px var(--lightningcss-light,#1018280f)var(--lightningcss-dark,#0006)}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}:root[data-theme=light]{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}:root[data-theme=dark]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}*{box-sizing:border-box}html,body,#root{height:100%}body{background:var(--bg);color:var(--text);font-family:var(--font);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;margin:0;font-size:14px;line-height:1.5}a{color:var(--accent-hover);text-decoration:none}a:hover{text-decoration:underline}code,.mono{font-family:var(--mono);font-size:.92em}h1,h2,h3,h4{letter-spacing:-.01em;margin:0;font-weight:650}::selection{background:var(--accent-soft)}input[type=checkbox],input[type=radio]{accent-color:var(--accent)}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-thumb{background:var(--border);border:2px solid var(--bg);border-radius:99px}::-webkit-scrollbar-thumb:hover{background:var(--faint)}:focus-visible{outline:2px solid var(--accent-ring);outline-offset:2px;border-radius:4px}.app{grid-template-columns:232px 1fr;min-height:100dvh;display:grid}.sidebar{border-right:1px solid var(--border);background:var(--rail);flex-direction:column;align-self:start;gap:4px;height:100dvh;padding:18px 14px;display:flex;position:sticky;top:0}.brand{align-items:center;gap:10px;padding:6px 8px 14px;display:flex}.brand-logo{background:var(--text);flex-shrink:0;width:26px;height:26px;-webkit-mask:url(/logo.png) 50%/contain no-repeat;mask:url(/logo.png) 50%/contain no-repeat}.brand .name{letter-spacing:-.02em;font-size:15px;font-weight:700;line-height:26px}.brand .ver{font-family:var(--mono);color:var(--muted);background:var(--raised);border:1px solid var(--border);border-radius:99px;align-self:center;padding:2px 6px;font-size:10px;line-height:1}.nav-item{border-radius:var(--radius-sm);text-align:left;cursor:pointer;width:100%;color:var(--muted);font:inherit;background:0 0;border:none;align-items:center;gap:10px;padding:8px 10px;font-size:13.5px;font-weight:500;transition:background .12s,color .12s;display:flex}.nav-item:hover{background:var(--raised);color:var(--text)}.nav-item.active{background:var(--accent-soft);color:var(--text)}.nav-item svg{width:17px;height:17px;color:var(--faint);flex-shrink:0}.nav-item.active svg{color:var(--accent)}.sidebar-foot{flex-direction:column;gap:2px;margin-top:auto;padding-top:12px;display:flex}.sidebar-link{color:var(--muted);border-radius:var(--radius-sm);align-items:center;gap:9px;padding:8px 10px;font-size:13px;display:flex}.sidebar-link:hover{background:var(--raised);color:var(--text);text-decoration:none}.sidebar-link svg{width:16px;height:16px}.theme-toggle{text-align:left;cursor:pointer;width:100%;color:var(--muted);font:inherit;border-radius:var(--radius-sm);background:0 0;border:none;align-items:center;gap:9px;padding:8px 10px;font-size:13px;transition:background .12s,color .12s;display:flex}.theme-toggle:hover{background:var(--raised);color:var(--text)}.theme-toggle svg{flex-shrink:0;width:16px;height:16px}.theme-toggle .mode{text-transform:capitalize}.stop-toggle{color:var(--red)}.stop-toggle:hover{background:var(--red-soft);color:var(--red)}.stop-toggle:disabled{opacity:.5;cursor:default}.main{min-width:0}.main-inner{max-width:980px;margin:0 auto;padding:32px 36px 64px}.page-head{justify-content:space-between;align-items:center;gap:16px;margin-bottom:6px;display:flex}.page-head h2{font-size:19px}.page-sub{color:var(--muted);max-width:70ch;margin:4px 0 22px;font-size:13.5px}.page-sub b{color:var(--text);font-weight:600}.btn{border-radius:var(--radius-sm);font:inherit;cursor:pointer;white-space:nowrap;border:1px solid #0000;justify-content:center;align-items:center;gap:7px;padding:7px 14px;font-size:13px;font-weight:550;transition:background .12s,border-color .12s,opacity .12s;display:inline-flex}.btn svg{width:15px;height:15px}.btn:disabled{opacity:.55;cursor:default}.btn-primary{background:var(--accent);color:var(--accent-ink)}.btn-primary:hover:not(:disabled){background:var(--accent-hover)}.btn-ghost{background:var(--raised);color:var(--text);border-color:var(--border)}.btn-ghost:hover:not(:disabled){background:var(--raised-hover)}.btn-danger{color:var(--red);background:0 0;border-color:#f871714d}.btn-danger:hover:not(:disabled){background:var(--red-soft)}.btn-sm{border-radius:var(--radius-xs);padding:4px 9px;font-size:12px}.btn-icon{padding:5px}.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius)}.panel{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:18px}.panel-accent{background:linear-gradient(180deg, var(--accent-soft), transparent 120%), var(--surface);border-color:#7c5cff47}.stat-row{grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:28px;display:grid}.stat{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:14px 16px;transition:border-color .12s}.stat:hover{border-color:var(--accent-ring)}.stat .label{color:var(--muted);text-transform:uppercase;letter-spacing:.05em;align-items:center;gap:6px;margin-bottom:9px;font-size:11px;font-weight:600;display:flex}.stat .label svg{width:14px;height:14px}.stat .value{letter-spacing:-.02em;font-size:24px;font-weight:700;line-height:1.1}.stat .value.mono{font-family:var(--mono);font-size:19px}.model-group-head{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;align-items:baseline;gap:8px;margin:0 0 8px;font-size:12px;font-weight:600;display:flex}.model-group-head .count{font-family:var(--mono);text-transform:none;letter-spacing:0;color:var(--faint);font-weight:500}.model-grid{grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px;display:grid}.model-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);padding:10px 12px;transition:border-color .12s,background .12s}.model-card:hover{border-color:var(--accent-ring);background:var(--hover)}.model-card .id{font-family:var(--mono);letter-spacing:-.01em;color:var(--text);font-size:13px;font-weight:600}.badge{font-size:11px;font-weight:600;font-family:var(--mono);letter-spacing:.01em;border-radius:99px;align-items:center;gap:5px;padding:2px 8px;display:inline-flex}.badge-accent{background:var(--accent-soft);color:var(--accent-hover)}.badge-green{background:var(--green-soft);color:var(--green)}.badge-amber{background:var(--amber-soft);color:var(--amber)}.badge-muted{background:var(--raised);color:var(--muted);border:1px solid var(--border)}.dot{border-radius:50%;flex-shrink:0;width:7px;height:7px}.dot-green{background:var(--green);box-shadow:0 0 0 3px var(--green-soft)}.dot-red{background:var(--red);box-shadow:0 0 0 3px var(--red-soft)}.tbl{border-collapse:collapse;width:100%;font-size:13px}.tbl thead th{text-align:left;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;border-bottom:1px solid var(--border);padding:9px 12px;font-size:11.5px;font-weight:600}.tbl tbody td{border-bottom:1px solid var(--border-soft);padding:10px 12px}.tbl tbody tr:last-child td{border-bottom:none}.tbl tbody tr:hover td{background:var(--hover)}.tbl .num{text-align:right;font-family:var(--mono)}.tbl-wrap{border:1px solid var(--border);border-radius:var(--radius);overflow-x:auto}.input,textarea.input{border-radius:var(--radius-sm);background:var(--raised);border:1px solid var(--border);width:100%;color:var(--text);font:inherit;padding:8px 11px;font-size:13px;transition:border-color .12s}.input::placeholder{color:var(--faint)}.input:focus{border-color:var(--accent);outline:none}textarea.input{resize:vertical;font-family:var(--mono);line-height:1.55}.field-label{color:var(--muted);margin-bottom:5px;font-size:12px;font-weight:500;display:block}select.input{appearance:none}.switch{cursor:pointer;background:var(--lightningcss-light,#c5c9d2)var(--lightningcss-dark,#3a3f4b);border:none;border-radius:99px;flex-shrink:0;width:34px;height:19px;padding:0;transition:background .15s;position:relative}.switch.on{background:var(--accent)}.switch:disabled{opacity:.6;cursor:default}.switch .knob{background:#fff;border-radius:50%;width:15px;height:15px;transition:left .15s;position:absolute;top:2px;left:2px;box-shadow:0 1px 2px #1018284d}.switch.on .knob{left:17px}.muted{color:var(--muted)}.faint{color:var(--faint)}.row{align-items:center;gap:10px;display:flex}.spread{justify-content:space-between;align-items:center;gap:12px;display:flex}.stack{flex-direction:column;display:flex}.chip{font-family:var(--mono);background:var(--raised);border:1px solid var(--border);border-radius:var(--radius-xs);color:var(--text);padding:1px 7px;font-size:12px}.empty{text-align:center;border:1px dashed var(--border);border-radius:var(--radius);color:var(--muted);padding:56px 20px}.empty svg{width:30px;height:30px;color:var(--faint);margin-bottom:12px}.empty .title{color:var(--text);margin-bottom:6px;font-weight:600}.notice{border-radius:var(--radius-sm);align-items:center;gap:8px;margin-bottom:14px;padding:9px 12px;font-size:13px;display:flex}.notice svg{flex-shrink:0;width:15px;height:15px}.notice-ok{background:var(--green-soft);color:var(--green)}.notice-err{background:var(--red-soft);color:var(--red)}.h-section{color:var(--text);align-items:center;gap:8px;margin:30px 0 12px;font-size:13px;font-weight:600;display:flex}.h-section .count{color:var(--muted);font-weight:500;font-family:var(--mono);font-size:12px}.spin{border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;width:14px;height:14px;animation:.7s linear infinite spin;display:inline-block}@keyframes spin{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}.modal-overlay{-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);z-index:50;background:var(--lightningcss-light,#11131c73)var(--lightningcss-dark,#0009);justify-content:center;align-items:flex-start;padding:8vh 16px;display:flex;position:fixed;inset:0}.modal-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);width:100%;max-width:520px;box-shadow:var(--shadow);max-height:84vh;padding:20px;overflow-y:auto}.modal-head{justify-content:space-between;align-items:center;margin-bottom:16px;display:flex}.modal-head h3{font-size:16px}.list-row{text-align:left;border-radius:var(--radius-sm);border:1px solid var(--border);background:var(--raised);cursor:pointer;width:100%;color:var(--text);font:inherit;justify-content:space-between;align-items:center;gap:10px;padding:11px 13px;transition:background .12s,border-color .12s;display:flex}.list-row:hover{background:var(--raised-hover);border-color:var(--accent-ring)}.list-row .title{font-size:14px;font-weight:600}.list-row .sub{color:var(--muted);margin-top:2px;font-size:12px}.prov-card{justify-content:space-between;align-items:flex-start;gap:12px;padding:15px 16px;display:flex}.link-btn{color:var(--accent-hover);font:inherit;cursor:pointer;background:0 0;border:none;padding:6px 2px;font-size:13px;text-decoration:underline}@media (width<=760px){.app{grid-template-columns:1fr}.sidebar{z-index:20;border-right:none;border-bottom:1px solid var(--border);background:var(--rail);flex-flow:wrap;align-items:center;gap:0;min-width:0;height:auto;padding:0 10px;position:sticky;top:0}.brand{flex:auto;order:1;width:auto;padding:10px 4px}.sidebar-foot{flex-direction:row;flex:none;order:2;gap:4px;margin:0;padding:0}.sidebar-foot .sidebar-link{display:none}.theme-toggle{justify-content:center;min-width:44px;min-height:44px;padding:8px}.theme-toggle .mode{display:none}.sidebar nav{overscroll-behavior-x:contain;border-top:1px solid var(--border-soft);scrollbar-width:none;flex-direction:row;flex:100%;order:3;gap:2px;min-width:0;margin:0;padding:4px 0 8px;display:flex;overflow-x:auto}.sidebar nav::-webkit-scrollbar{display:none}.nav-item{white-space:nowrap;width:auto;min-height:44px;padding:9px 14px;font-size:14px}.main-inner{padding:22px 18px 48px}.stat-row{grid-template-columns:repeat(2,minmax(0,1fr))}.tbl{min-width:460px}}