Skip to content

Commit ccf3354

Browse files
thaolaptrinhclaude
andauthored
feat(models): dynamic open-source model catalog from CC provider API (#8)
* feat(models): dynamic open-source model catalog from CC provider API Replace the fully hand-maintained models.json catalog with a hybrid: the CC provider /provider/v1/models endpoint is now the source of truth for the model list, display names, and context windows (1h TTL cache, modeled on fetchLatestCliVersion), while models.json keeps what the API never returns — aliases, reasoning efforts, max output tokens — and doubles as the offline fallback. Closed models (claude-*, gpt-*, google/* and other closed orgs) are filtered out so the proxy only exposes open-source models available on the GO plan. New CC models now appear without a proxy release. - new src/translate/catalog.ts: merge (static-first order preserves the default model), isClosedModel, TTL + inflight-deduped refreshCatalog - models.ts/server.ts/opencode.ts read the live catalog; opencode setup fetches it when a key is available (env or saved auth) - models.json: closedModelOrgs blocklist + metadata/aliases for the new open models (GLM-5.3, Qwen 3.8 family, Grok 4.6, Hy4, Muse Spark, Fugu, Laguna, free MiniMax variants, ...) Co-Authored-By: Claude <noreply@anthropic.com> * refactor(catalog): review cleanup — drop dead fetchModelList, dedupe helpers Post-review of PR #8: - remove fetchModelList (no remaining callers after server.ts migrated to refreshCatalog) - extract displayNameFor() to collapse the three display-name fallback chains in catalog.ts - share the URL-filtering CC models fetch mock between test files via tests/helpers.ts Co-Authored-By: Claude <noreply@anthropic.com> * test(catalog): cover TTL expiry, retired models, malformed payloads Closes the four gaps flagged in the review of PR #8: - TTL expiry actually elapses and refetches (Date.now spy) - static entries survive when the API no longer lists them (retired upstream models keep static name/context, source: "static") - malformed payloads: data:null keeps the catalog; id-less and null items are skipped instead of discarding the whole refresh (also hardened the filter in catalog.ts for this); null context_length falls back to 128k - resolveAnthropicModel keeps the claude-* default pinned to the static first entry even when the API returns a reordered list 166 tests pass, typecheck clean. Co-Authored-By: Claude <noreply@anthropic.com> * fix(catalog): absorb duplicate ids within the API model list Self-review pass: an API payload listing the same API-only id twice would append it twice to the catalog (the `seen` set only tracked static ids). Now the second loop adds to `seen` as well; first occurrence wins. Regression test included. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2dbd904 commit ccf3354

10 files changed

Lines changed: 570 additions & 64 deletions

File tree

‎src/config.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export interface Config {
2727
* server actively blocks requests whose version looks stale or absent.
2828
*/
2929
export const DEFAULT_CC_VERSION = "0.40.3";
30+
export const DEFAULT_CC_API_BASE = "https://api.commandcode.ai";
3031
const CC_VERSION_REFRESH_MS = 24 * 60 * 60 * 1000;
3132

3233
let cachedVersion: string | null = null;
@@ -83,7 +84,7 @@ export function loadConfig(): Config {
8384
const host = cli.host || process.env.HOST || "127.0.0.1";
8485
const port = parseInt(cli.port || process.env.PORT || "8787", 10);
8586
const apiKey = cli["api-key"] || process.env.CC_API_KEY || readAuthKey();
86-
const ccApiBase = process.env.CC_API_BASE || "https://api.commandcode.ai";
87+
const ccApiBase = process.env.CC_API_BASE || DEFAULT_CC_API_BASE;
8788
const ccVersion = process.env.CC_CLI_VERSION || cachedVersion || DEFAULT_CC_VERSION;
8889
const logLevel = process.env.LOG_LEVEL || "info";
8990
// `*` is fine for a localhost proxy; restrict (e.g. to an origin or leave

‎src/models.json‎

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,63 @@
11
{
2+
"closedModelOrgs": ["anthropic", "openai", "google", "gemini"],
23
"builtin": [
34
"deepseek/deepseek-v4-pro",
45
"deepseek/deepseek-v4-flash",
6+
"deepseek/deepseek-v4-flash-vision-exp",
7+
"zai-org/GLM-5.3",
8+
"z-ai/glm-5.3-flash",
59
"zai-org/GLM-5.2",
610
"zai-org/GLM-5.2-Fast",
711
"zai-org/GLM-5.1",
812
"zai-org/GLM-5",
913
"MiniMaxAI/MiniMax-M3",
1014
"MiniMaxAI/MiniMax-M2.7",
15+
"minimax/minimax-m3-free",
16+
"minimax/minimax-m2.7-free",
1117
"MiniMaxAI/MiniMax-M2.5",
1218
"moonshotai/Kimi-K3",
1319
"moonshotai/Kimi-K2.7-Code",
1420
"moonshotai/Kimi-K2.7-Code-Highspeed",
1521
"moonshotai/Kimi-K2.6",
1622
"moonshotai/Kimi-K2.5",
23+
"Qwen/Qwen3.8-Max",
24+
"Qwen/Qwen3.8-Flash",
25+
"Qwen/Qwen3.8-27B",
1726
"Qwen/Qwen3.7-Max",
1827
"Qwen/Qwen3.7-Plus",
28+
"Qwen/Qwen3.7-Flash",
1929
"Qwen/Qwen3.6-Max-Preview",
2030
"Qwen/Qwen3.6-Plus",
2131
"stepfun/Step-3.7-Flash",
2232
"stepfun/Step-3.5-Flash",
2333
"xiaomi/mimo-v2.5-pro",
2434
"xiaomi/mimo-v2.5",
35+
"xai/grok-4.6",
2536
"xai/grok-4.5",
2637
"nvidia/nemotron-3-ultra-550b-a55b",
2738
"thinkingmachines/inkling",
28-
"tencent/Hy3"
39+
"thinkingmachines/inkling-small",
40+
"tencent/hy4-preview",
41+
"tencent/hy3-paid",
42+
"tencent/Hy3",
43+
"meta/muse-spark-1.2",
44+
"meta/muse-spark-1.2-contributor",
45+
"meta/muse-spark-1.1",
46+
"sakana/fugu-ultra",
47+
"poolside/laguna-s-2.1-free"
2948
],
3049
"shortAliases": {
3150
"deepseek-v4-pro": "deepseek/deepseek-v4-pro",
3251
"deepseek-v4": "deepseek/deepseek-v4-pro",
3352
"deepseek-pro": "deepseek/deepseek-v4-pro",
3453
"deepseek-v4-flash": "deepseek/deepseek-v4-flash",
3554
"deepseek-flash": "deepseek/deepseek-v4-flash",
55+
"deepseek-v4-flash-vision": "deepseek/deepseek-v4-flash-vision-exp",
56+
"deepseek-vision": "deepseek/deepseek-v4-flash-vision-exp",
57+
"glm-5.3": "zai-org/GLM-5.3",
58+
"glm5.3": "zai-org/GLM-5.3",
59+
"glm-5.3-flash": "z-ai/glm-5.3-flash",
60+
"glm5.3-flash": "z-ai/glm-5.3-flash",
3661
"glm-5.2": "zai-org/GLM-5.2",
3762
"glm5.2": "zai-org/GLM-5.2",
3863
"glm-5.2-fast": "zai-org/GLM-5.2-Fast",
@@ -41,8 +66,10 @@
4166
"glm-5": "zai-org/GLM-5",
4267
"minimax-m3": "MiniMaxAI/MiniMax-M3",
4368
"minimax3": "MiniMaxAI/MiniMax-M3",
69+
"minimax-m3-free": "minimax/minimax-m3-free",
4470
"minimax-m2.7": "MiniMaxAI/MiniMax-M2.7",
4571
"minimax2.7": "MiniMaxAI/MiniMax-M2.7",
72+
"minimax-m2.7-free": "minimax/minimax-m2.7-free",
4673
"minimax-m2.5": "MiniMaxAI/MiniMax-M2.5",
4774
"minimax2.5": "MiniMaxAI/MiniMax-M2.5",
4875
"kimi-k3": "moonshotai/Kimi-K3",
@@ -56,14 +83,22 @@
5683
"kimi2.6": "moonshotai/Kimi-K2.6",
5784
"kimi-k2.5": "moonshotai/Kimi-K2.5",
5885
"kimi2.5": "moonshotai/Kimi-K2.5",
86+
"qwen3.8-max": "Qwen/Qwen3.8-Max",
87+
"qwen-3.8-max": "Qwen/Qwen3.8-Max",
88+
"qwen3.8-flash": "Qwen/Qwen3.8-Flash",
89+
"qwen-3.8-flash": "Qwen/Qwen3.8-Flash",
90+
"qwen3.8-27b": "Qwen/Qwen3.8-27B",
91+
"qwen-3.8-27b": "Qwen/Qwen3.8-27B",
5992
"qwen3.7-max": "Qwen/Qwen3.7-Max",
6093
"qwen-3.7-max": "Qwen/Qwen3.7-Max",
6194
"qwen3.7-plus": "Qwen/Qwen3.7-Plus",
6295
"qwen-3.7-plus": "Qwen/Qwen3.7-Plus",
63-
"qwen-3.6-max": "Qwen/Qwen3.6-Max-Preview",
96+
"qwen3.7-flash": "Qwen/Qwen3.7-Flash",
97+
"qwen-3.7-flash": "Qwen/Qwen3.7-Flash",
6498
"qwen3.6-max": "Qwen/Qwen3.6-Max-Preview",
65-
"qwen-3.6-plus": "Qwen/Qwen3.6-Plus",
99+
"qwen-3.6-max": "Qwen/Qwen3.6-Max-Preview",
66100
"qwen3.6-plus": "Qwen/Qwen3.6-Plus",
101+
"qwen-3.6-plus": "Qwen/Qwen3.6-Plus",
67102
"step-3.7-flash": "stepfun/Step-3.7-Flash",
68103
"step3.7": "stepfun/Step-3.7-Flash",
69104
"step-3.5-flash": "stepfun/Step-3.5-Flash",
@@ -72,73 +107,125 @@
72107
"mimo-pro": "xiaomi/mimo-v2.5-pro",
73108
"mimo-v2.5": "xiaomi/mimo-v2.5",
74109
"mimo2.5": "xiaomi/mimo-v2.5",
110+
"grok-4.6": "xai/grok-4.6",
111+
"grok4.6": "xai/grok-4.6",
75112
"grok-4.5": "xai/grok-4.5",
76113
"grok4.5": "xai/grok-4.5",
77114
"nemotron": "nvidia/nemotron-3-ultra-550b-a55b",
78115
"nemotron-3-ultra": "nvidia/nemotron-3-ultra-550b-a55b",
79116
"inkling": "thinkingmachines/inkling",
80-
"hy3": "tencent/Hy3"
117+
"inkling-small": "thinkingmachines/inkling-small",
118+
"hy4": "tencent/hy4-preview",
119+
"hy4-preview": "tencent/hy4-preview",
120+
"hy3": "tencent/Hy3",
121+
"muse-spark": "meta/muse-spark-1.2",
122+
"muse-spark-1.2": "meta/muse-spark-1.2",
123+
"muse-spark-contributor": "meta/muse-spark-1.2-contributor",
124+
"muse-spark-1.1": "meta/muse-spark-1.1",
125+
"fugu": "sakana/fugu-ultra",
126+
"fugu-ultra": "sakana/fugu-ultra",
127+
"laguna": "poolside/laguna-s-2.1-free"
81128
},
82129
"contextWindows": {
83130
"deepseek/deepseek-v4-pro": 1048576,
84131
"deepseek/deepseek-v4-flash": 1048576,
132+
"deepseek/deepseek-v4-flash-vision-exp": 1048576,
133+
"zai-org/GLM-5.3": 1000000,
134+
"z-ai/glm-5.3-flash": 1048576,
85135
"zai-org/GLM-5.2": 1048576,
86136
"zai-org/GLM-5.2-Fast": 1048576,
87137
"zai-org/GLM-5.1": 200000,
88138
"zai-org/GLM-5": 200000,
89139
"MiniMaxAI/MiniMax-M3": 1048576,
90140
"MiniMaxAI/MiniMax-M2.7": 204800,
141+
"minimax/minimax-m3-free": 1048576,
142+
"minimax/minimax-m2.7-free": 197000,
91143
"MiniMaxAI/MiniMax-M2.5": 200000,
92144
"moonshotai/Kimi-K3": 1048576,
93145
"moonshotai/Kimi-K2.7-Code": 256000,
94146
"moonshotai/Kimi-K2.7-Code-Highspeed": 262000,
95147
"moonshotai/Kimi-K2.6": 256000,
96148
"moonshotai/Kimi-K2.5": 256000,
149+
"Qwen/Qwen3.8-Max": 1000000,
150+
"Qwen/Qwen3.8-Flash": 1000000,
151+
"Qwen/Qwen3.8-27B": 262144,
97152
"Qwen/Qwen3.7-Max": 1048576,
98153
"Qwen/Qwen3.7-Plus": 1048576,
154+
"Qwen/Qwen3.7-Flash": 1048576,
99155
"Qwen/Qwen3.6-Max-Preview": 262144,
100156
"Qwen/Qwen3.6-Plus": 1048576,
101157
"stepfun/Step-3.7-Flash": 256000,
102158
"stepfun/Step-3.5-Flash": 1048576,
103159
"xiaomi/mimo-v2.5-pro": 1048576,
104160
"xiaomi/mimo-v2.5": 1048576,
161+
"xai/grok-4.6": 500000,
105162
"xai/grok-4.5": 500000,
106163
"nvidia/nemotron-3-ultra-550b-a55b": 1048576,
107164
"thinkingmachines/inkling": 256000,
108-
"tencent/Hy3": 262144
165+
"thinkingmachines/inkling-small": 1000000,
166+
"tencent/hy4-preview": 1048576,
167+
"tencent/hy3-paid": 262144,
168+
"tencent/Hy3": 262144,
169+
"meta/muse-spark-1.2": 1048576,
170+
"meta/muse-spark-1.2-contributor": 1048576,
171+
"meta/muse-spark-1.1": 1048576,
172+
"sakana/fugu-ultra": 1000000,
173+
"poolside/laguna-s-2.1-free": 256000
109174
},
110175
"modelNames": {
111176
"deepseek/deepseek-v4-pro": "DeepSeek V4 Pro",
112177
"deepseek/deepseek-v4-flash": "DeepSeek V4 Flash",
178+
"deepseek/deepseek-v4-flash-vision-exp": "DeepSeek V4 Flash Vision (exp)",
179+
"zai-org/GLM-5.3": "GLM-5.3",
180+
"z-ai/glm-5.3-flash": "GLM-5.3 Flash",
113181
"zai-org/GLM-5.2": "GLM-5.2",
114182
"zai-org/GLM-5.2-Fast": "GLM-5.2 Fast",
115183
"zai-org/GLM-5.1": "GLM-5.1",
116184
"zai-org/GLM-5": "GLM-5",
117185
"MiniMaxAI/MiniMax-M3": "MiniMax M3",
118186
"MiniMaxAI/MiniMax-M2.7": "MiniMax M2.7",
187+
"minimax/minimax-m3-free": "MiniMax M3 (Free)",
188+
"minimax/minimax-m2.7-free": "MiniMax M2.7 (Free)",
119189
"MiniMaxAI/MiniMax-M2.5": "MiniMax M2.5",
120190
"moonshotai/Kimi-K3": "Kimi K3",
121191
"moonshotai/Kimi-K2.7-Code": "Kimi K2.7 Code",
122192
"moonshotai/Kimi-K2.7-Code-Highspeed": "Kimi K2.7 Code Highspeed",
123193
"moonshotai/Kimi-K2.6": "Kimi K2.6",
124194
"moonshotai/Kimi-K2.5": "Kimi K2.5",
195+
"Qwen/Qwen3.8-Max": "Qwen 3.8 Max",
196+
"Qwen/Qwen3.8-Flash": "Qwen 3.8 Flash",
197+
"Qwen/Qwen3.8-27B": "Qwen 3.8 27B",
125198
"Qwen/Qwen3.7-Max": "Qwen 3.7 Max",
126199
"Qwen/Qwen3.7-Plus": "Qwen 3.7 Plus",
200+
"Qwen/Qwen3.7-Flash": "Qwen 3.7 Flash",
127201
"Qwen/Qwen3.6-Max-Preview": "Qwen 3.6 Max Preview",
128202
"Qwen/Qwen3.6-Plus": "Qwen 3.6 Plus",
129203
"stepfun/Step-3.7-Flash": "Step 3.7 Flash",
130204
"stepfun/Step-3.5-Flash": "Step 3.5 Flash",
131205
"xiaomi/mimo-v2.5-pro": "MiMo V2.5 Pro",
132206
"xiaomi/mimo-v2.5": "MiMo V2.5",
207+
"xai/grok-4.6": "Grok 4.6",
133208
"xai/grok-4.5": "Grok 4.5",
134209
"nvidia/nemotron-3-ultra-550b-a55b": "Nemotron 3 Ultra",
135210
"thinkingmachines/inkling": "Inkling",
136-
"tencent/Hy3": "Hy3"
211+
"thinkingmachines/inkling-small": "Inkling Small",
212+
"tencent/hy4-preview": "Tencent Hy4 Preview",
213+
"tencent/hy3-paid": "Tencent Hy3",
214+
"tencent/Hy3": "Hy3",
215+
"meta/muse-spark-1.2": "Muse Spark 1.2",
216+
"meta/muse-spark-1.2-contributor": "Muse Spark 1.2 Contributor",
217+
"meta/muse-spark-1.1": "Muse Spark 1.1",
218+
"sakana/fugu-ultra": "Fugu Ultra",
219+
"poolside/laguna-s-2.1-free": "Laguna S 2.1 (Free)"
137220
},
138221
"reasoningEfforts": {
139222
"deepseek/deepseek-v4-pro": ["high", "max"],
140223
"deepseek/deepseek-v4-flash": ["high", "max"],
224+
"deepseek/deepseek-v4-flash-vision-exp": ["high", "max"],
225+
"zai-org/GLM-5.3": ["high", "max"],
226+
"z-ai/glm-5.3-flash": ["high", "max"],
141227
"zai-org/GLM-5.2": ["high", "max"],
228+
"xai/grok-4.6": ["low", "medium", "high"],
142229
"xai/grok-4.5": ["low", "medium", "high"]
143230
},
144231
"maxOutputTokens": {

‎src/server.ts‎

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
AnthropicStreamEncoder,
99
buildAnthropicResponse,
1010
} from "@/translate/anthropic.js";
11-
import { getDefaultModels, fetchModelList } from "@/translate/models.js";
11+
import { refreshCatalog, getCatalog } from "@/translate/catalog.js";
1212
import type { CCEvent } from "@/translate/types.js";
1313
import { formatSSE, formatSSEDone, formatAnthropicSSE } from "@/stream.js";
1414
import { sendToCC, collectEvents, UpstreamError } from "@/upstream.js";
@@ -26,13 +26,8 @@ import type { AnthropicRequest, AnthropicSSERecord } from "@/translate/anthropic
2626
// ──────────────────────────────────────────
2727

2828
let config: Config;
29-
let modelList: string[] = getDefaultModels();
3029
let corsOrigin = "*";
3130

32-
function updateModelList(models: string[]): void {
33-
if (models.length > 0) modelList = models;
34-
}
35-
3631
// ──────────────────────────────────────────
3732
// Request body parser
3833
// ──────────────────────────────────────────
@@ -251,28 +246,29 @@ function handleModels(req: http.IncomingMessage, res: http.ServerResponse): void
251246
const isAnthropic = req.headers["anthropic-version"] !== undefined;
252247

253248
if (isAnthropic) {
254-
const items = modelList;
249+
const catalog = getCatalog();
250+
const items = catalog.models;
255251
const data = {
256-
data: items.map((id: string) => ({
257-
id,
252+
data: items.map((m) => ({
253+
id: m.id,
258254
type: "model" as const,
259-
display_name: id,
255+
display_name: m.displayName,
260256
created_at: new Date().toISOString(),
261257
max_input_tokens: null as number | null,
262258
max_tokens: null as number | null,
263259
capabilities: null,
264260
})),
265261
has_more: false,
266-
first_id: items.length > 0 ? items[0] : null,
267-
last_id: items.length > 0 ? items[items.length - 1] : null,
262+
first_id: items.length > 0 ? items[0].id : null,
263+
last_id: items.length > 0 ? items[items.length - 1].id : null,
268264
};
269265
sendJson(res, 200, data);
270266
return;
271267
}
272268

273269
const data = {
274270
object: "list",
275-
data: modelList.map((id: string) => ({
271+
data: getCatalog().ids.map((id: string) => ({
276272
id,
277273
object: "model",
278274
created: Math.floor(Date.now() / 1000),
@@ -586,15 +582,12 @@ export function createServer(cfg: Config): http.Server {
586582
config = cfg;
587583
corsOrigin = cfg.corsOrigin;
588584

589-
// Start fetching model list in background (only if we have a key to use).
585+
// Refresh the model catalog from the CC provider API in background (only if
586+
// we have a key to use). On failure the static fallback stays in place.
590587
if (cfg.apiKey) {
591-
fetchModelList(cfg.ccApiBase, cfg.apiKey)
592-
.then((models) => {
593-
if (models.length > 0) updateModelList(models);
594-
})
595-
.catch(() => {
596-
/* keep defaults */
597-
});
588+
refreshCatalog(cfg.ccApiBase, cfg.apiKey).catch(() => {
589+
/* keep defaults */
590+
});
598591
}
599592

600593
const routes: RouteEntry[] = [

0 commit comments

Comments
 (0)