Skip to content

Commit 6dd4ac4

Browse files
committed
feat: support glm
1 parent a0c946c commit 6dd4ac4

5 files changed

Lines changed: 169 additions & 22 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ During authentication, you will need a [CoreInfra AI Hub](https://hub.coreinfra.
2121
## Features
2222

2323
- **Up-to-date model list** - the catalog is loaded dynamically from the Hub API on every startup and always reflects its current state. Model capabilities (context limits, reasoning, tool use, etc.) are resolved from the models.dev catalog.
24-
- **OpenAI and Anthropic models** - both model families are supported, including GPT-5.x and Claude 4.x.
25-
- **Reasoning support** - `interleaved thinking` mode is enabled automatically for Anthropic models.
24+
- **All available models** - every model exposed by the Hub is supported.
25+
- **Reasoning support** - `interleaved thinking` mode is enabled automatically where applicable.
2626

2727
## Limitations
2828

README.ru.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ opencode providers login --provider coreinfra
2121
## Возможности
2222

2323
- **Актуальный список моделей** — каталог динамически загружается из API Hub при каждом запуске и всегда отражает его текущее состояние. Возможности моделей (контекст, reasoning, tool use и т.д.) определяются из каталога models.dev.
24-
- **Модели OpenAI и Anthropic**поддерживаются обе линейки, включая GPT-5.x и Claude 4.x.
25-
- **Поддержка reasoning**для моделей Anthropic автоматически включается режим `interleaved thinking`.
24+
- **Все доступные модели**поддерживается любая модель, предоставленная Hub.
25+
- **Поддержка reasoning**режим `interleaved thinking` включается автоматически там, где это применимо.
2626

2727
## Ограничения
2828

src/models.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ const CACHE_PATH = join(
1212
const MODELS_DEV_URL = "https://models.dev/api.json";
1313
const DEFAULT_HUB_BASE = "https://hub.coreinfra.ai";
1414
const HUB_URL = `${process.env.COREINFRA_HUB_BASE_URL ?? DEFAULT_HUB_BASE}/hub/api/prices`;
15-
const OPENAI_BASE = `${process.env.COREINFRA_HUB_BASE_URL ?? DEFAULT_HUB_BASE}/codex/api/v1`;
16-
const ANTHROPIC_BASE = `${process.env.COREINFRA_HUB_BASE_URL ?? DEFAULT_HUB_BASE}/claude/api/v1`;
15+
const OPENAI_BASE = `${process.env.COREINFRA_HUB_BASE_URL ?? DEFAULT_HUB_BASE}/openai/api/v1`;
16+
const ANTHROPIC_BASE = `${process.env.COREINFRA_HUB_BASE_URL ?? DEFAULT_HUB_BASE}/anthropic/api/v1`;
1717
const ANTHROPIC_BETA_HEADER =
1818
"interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14";
1919

@@ -34,11 +34,15 @@ const DEFAULT_COST = {
3434
type Protocol = "openai" | "anthropic";
3535

3636
// Providers we explicitly know how to route. Models from any provider not
37-
// listed here are skipped. Adding a provider is a one-line change here.
38-
const PROVIDERS = new Map<string, { protocol: Protocol }>([
39-
["openai", { protocol: "openai" }],
40-
["anthropic", { protocol: "anthropic" }],
41-
["deepseek", { protocol: "anthropic" }],
37+
// listed here are skipped. Adding a provider is a one-line change here. The
38+
// `npm` package selects the wire API: `@ai-sdk/openai` targets OpenAI's
39+
// Responses API (`/responses`), while `@ai-sdk/openai-compatible` targets the
40+
// Chat Completions API (`/chat/completions`).
41+
const PROVIDERS = new Map<string, { protocol: Protocol; npm: string }>([
42+
["openai", { protocol: "openai", npm: "@ai-sdk/openai" }],
43+
["anthropic", { protocol: "anthropic", npm: "@ai-sdk/anthropic" }],
44+
["deepseek", { protocol: "anthropic", npm: "@ai-sdk/anthropic" }],
45+
["zai", { protocol: "openai", npm: "@ai-sdk/openai-compatible" }],
4246
]);
4347

4448
type ModelDevEntry = {
@@ -49,6 +53,7 @@ type ModelDevEntry = {
4953
reasoning?: boolean;
5054
temperature?: boolean;
5155
tool_call?: boolean;
56+
interleaved?: boolean | { field: string };
5257
modalities?: { input?: string[]; output?: string[] };
5358
cost?: {
5459
input?: number;
@@ -193,7 +198,7 @@ export function buildConfigModels(
193198
name: entry?.name ?? hubModel.display_name,
194199
provider: {
195200
api: anthropic ? ANTHROPIC_BASE : OPENAI_BASE,
196-
npm: anthropic ? "@ai-sdk/anthropic" : "@ai-sdk/openai",
201+
npm: known.npm,
197202
},
198203
attachment: entry?.attachment ?? DEFAULT_CAPS.attachment,
199204
reasoning: entry?.reasoning ?? DEFAULT_CAPS.reasoning,
@@ -214,7 +219,9 @@ export function buildConfigModels(
214219
context: entry?.limit?.context ?? DEFAULT_LIMIT.context,
215220
output: entry?.limit?.output ?? DEFAULT_LIMIT.output,
216221
},
217-
interleaved: anthropic ? { field: "reasoning_content" } : true,
222+
interleaved:
223+
entry?.interleaved ??
224+
(anthropic ? { field: "reasoning_content" } : true),
218225
headers: anthropic ? { "anthropic-beta": ANTHROPIC_BETA_HEADER } : {},
219226
};
220227
}

test/index.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ describe("config hook", () => {
178178
id: "gpt-5.4-nano",
179179
name: "GPT-5.4 Nano",
180180
provider: {
181-
api: "https://hub.coreinfra.ai/codex/api/v1",
181+
api: "https://hub.coreinfra.ai/openai/api/v1",
182182
npm: "@ai-sdk/openai",
183183
},
184184
attachment: true,
@@ -196,7 +196,7 @@ describe("config hook", () => {
196196
id: "claude-sonnet-4-20250514",
197197
name: "Claude Sonnet 4",
198198
provider: {
199-
api: "https://hub.coreinfra.ai/claude/api/v1",
199+
api: "https://hub.coreinfra.ai/anthropic/api/v1",
200200
npm: "@ai-sdk/anthropic",
201201
},
202202
attachment: true,
@@ -217,7 +217,7 @@ describe("config hook", () => {
217217
id: "deepseek-v4-pro",
218218
name: "DeepSeek V4 Pro",
219219
provider: {
220-
api: "https://hub.coreinfra.ai/claude/api/v1",
220+
api: "https://hub.coreinfra.ai/anthropic/api/v1",
221221
npm: "@ai-sdk/anthropic",
222222
},
223223
attachment: false,
@@ -238,7 +238,7 @@ describe("config hook", () => {
238238
id: "deepseek-v4-flash",
239239
name: "DeepSeek V4 Flash",
240240
provider: {
241-
api: "https://hub.coreinfra.ai/claude/api/v1",
241+
api: "https://hub.coreinfra.ai/anthropic/api/v1",
242242
npm: "@ai-sdk/anthropic",
243243
},
244244
attachment: false,

test/models.test.ts

Lines changed: 145 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ describe("buildConfigModels", () => {
127127
id: "gpt-5.4-nano",
128128
name: "GPT-5.4 Nano",
129129
provider: {
130-
api: "https://hub.coreinfra.ai/codex/api/v1",
130+
api: "https://hub.coreinfra.ai/openai/api/v1",
131131
npm: "@ai-sdk/openai",
132132
},
133133
attachment: true,
@@ -149,7 +149,7 @@ describe("buildConfigModels", () => {
149149
id: "claude-sonnet-4-20250514",
150150
name: "Claude Sonnet 4",
151151
provider: {
152-
api: "https://hub.coreinfra.ai/claude/api/v1",
152+
api: "https://hub.coreinfra.ai/anthropic/api/v1",
153153
npm: "@ai-sdk/anthropic",
154154
},
155155
attachment: true,
@@ -174,7 +174,7 @@ describe("buildConfigModels", () => {
174174
id: "deepseek-v4-pro",
175175
name: "DeepSeek V4 Pro",
176176
provider: {
177-
api: "https://hub.coreinfra.ai/claude/api/v1",
177+
api: "https://hub.coreinfra.ai/anthropic/api/v1",
178178
npm: "@ai-sdk/anthropic",
179179
},
180180
attachment: false,
@@ -199,7 +199,7 @@ describe("buildConfigModels", () => {
199199
id: "deepseek-v4-flash",
200200
name: "DeepSeek V4 Flash",
201201
provider: {
202-
api: "https://hub.coreinfra.ai/claude/api/v1",
202+
api: "https://hub.coreinfra.ai/anthropic/api/v1",
203203
npm: "@ai-sdk/anthropic",
204204
},
205205
attachment: false,
@@ -240,7 +240,7 @@ describe("buildConfigModels", () => {
240240
id: "unknown-model",
241241
name: "Unknown Model",
242242
provider: {
243-
api: "https://hub.coreinfra.ai/codex/api/v1",
243+
api: "https://hub.coreinfra.ai/openai/api/v1",
244244
npm: "@ai-sdk/openai",
245245
},
246246
attachment: true,
@@ -385,6 +385,146 @@ describe("buildConfigModels", () => {
385385
expect(models).toEqual({});
386386
expect(warnings).toEqual([]);
387387
});
388+
389+
it("routes zai (GLM) through the openai chat-completions path", () => {
390+
const modelsDevData = {
391+
zai: {
392+
models: {
393+
"glm-5.2": {
394+
id: "glm-5.2",
395+
name: "GLM-5.2",
396+
limit: { context: 128000, output: 16000 },
397+
attachment: false,
398+
reasoning: true,
399+
temperature: true,
400+
tool_call: true,
401+
interleaved: { field: "reasoning_content" },
402+
modalities: { input: ["text"], output: ["text"] },
403+
cost: { input: 0.5, output: 2, cache_read: 0.05, cache_write: 0 },
404+
},
405+
"glm-4.7": {
406+
id: "glm-4.7",
407+
name: "GLM-4.7",
408+
limit: { context: 128000, output: 16000 },
409+
attachment: false,
410+
reasoning: true,
411+
temperature: true,
412+
tool_call: true,
413+
interleaved: { field: "reasoning_content" },
414+
modalities: { input: ["text"], output: ["text"] },
415+
cost: { input: 0.2, output: 0.8, cache_read: 0.02, cache_write: 0 },
416+
},
417+
},
418+
},
419+
};
420+
421+
const hubData = {
422+
providers: {
423+
zai: {
424+
models: {
425+
"glm-5.2": { display_name: "GLM-5.2" },
426+
"glm-4.7": { display_name: "GLM-4.7" },
427+
},
428+
},
429+
},
430+
};
431+
432+
const { models, warnings } = buildConfigModels(modelsDevData, hubData);
433+
434+
expect(warnings).toEqual([]);
435+
expect(Object.keys(models)).toHaveLength(2);
436+
437+
expect(models["glm-5.2"]).toEqual({
438+
id: "glm-5.2",
439+
name: "GLM-5.2",
440+
provider: {
441+
api: "https://hub.coreinfra.ai/openai/api/v1",
442+
// zai uses the openai-compatible SDK -> Chat Completions (/chat/completions),
443+
// NOT the openai SDK which would use the Responses API (/responses).
444+
npm: "@ai-sdk/openai-compatible",
445+
},
446+
attachment: false,
447+
reasoning: true,
448+
temperature: true,
449+
tool_call: true,
450+
modalities: { input: ["text"], output: ["text"] },
451+
cost: { input: 0.5, output: 2, cache_read: 0.05, cache_write: 0 },
452+
limit: { context: 128000, output: 16000 },
453+
interleaved: { field: "reasoning_content" },
454+
headers: {},
455+
});
456+
});
457+
458+
it("uses Responses API for openai but Chat Completions for zai", () => {
459+
const modelsDevData = {
460+
openai: { models: { "gpt-x": { name: "GPT X" } } },
461+
zai: { models: { "glm-x": { name: "GLM X" } } },
462+
};
463+
const hubData = {
464+
providers: {
465+
openai: { models: { "gpt-x": { display_name: "GPT X" } } },
466+
zai: { models: { "glm-x": { display_name: "GLM X" } } },
467+
},
468+
};
469+
const { models } = buildConfigModels(modelsDevData, hubData);
470+
471+
// openai -> @ai-sdk/openai (Responses API, /responses)
472+
expect(models["gpt-x"].provider.npm).toBe("@ai-sdk/openai");
473+
// zai -> @ai-sdk/openai-compatible (Chat Completions, /chat/completions)
474+
expect(models["glm-x"].provider.npm).toBe("@ai-sdk/openai-compatible");
475+
// both share the same hub base URL (the hub routes by model name)
476+
expect(models["gpt-x"].provider.api).toBe(models["glm-x"].provider.api);
477+
});
478+
479+
it("respects models.dev interleaved and falls back per protocol", () => {
480+
// openai-protocol zai model: explicit interleaved wins over protocol default
481+
const explicit = buildConfigModels(
482+
{
483+
zai: {
484+
models: {
485+
"glm-5.2": {
486+
name: "GLM-5.2",
487+
interleaved: { field: "reasoning_content" },
488+
},
489+
},
490+
},
491+
},
492+
{
493+
providers: {
494+
zai: { models: { "glm-5.2": { display_name: "GLM-5.2" } } },
495+
},
496+
},
497+
);
498+
expect(explicit.models["glm-5.2"].interleaved).toEqual({
499+
field: "reasoning_content",
500+
});
501+
502+
// openai-protocol zai model without interleaved -> default true
503+
const openaiFallback = buildConfigModels(
504+
{ zai: { models: { "glm-4.5": { name: "GLM-4.5" } } } },
505+
{
506+
providers: {
507+
zai: { models: { "glm-4.5": { display_name: "GLM-4.5" } } },
508+
},
509+
},
510+
);
511+
expect(openaiFallback.models["glm-4.5"].interleaved).toBe(true);
512+
513+
// anthropic-protocol model without interleaved -> default reasoning_content field
514+
const anthropicFallback = buildConfigModels(
515+
{ anthropic: { models: { "claude-x": { name: "Claude X" } } } },
516+
{
517+
providers: {
518+
anthropic: {
519+
models: { "claude-x": { display_name: "Claude X" } },
520+
},
521+
},
522+
},
523+
);
524+
expect(anthropicFallback.models["claude-x"].interleaved).toEqual({
525+
field: "reasoning_content",
526+
});
527+
});
388528
});
389529

390530
describe("fetchModelsDevData", () => {

0 commit comments

Comments
 (0)