From 6793489b787b7ae399ada55179b97a03f685d972 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 24 Mar 2026 09:19:27 -0400 Subject: [PATCH] fix(pi-ai): restore alibaba-coding-plan provider via models.custom.ts (#2350) The alibaba-coding-plan provider (8 models) was silently dropped when models.generated.ts was regenerated from models.dev in PR #2118. This provider uses a proprietary DashScope endpoint not tracked by models.dev, so regeneration removes it every time. Add models.custom.ts for manually-maintained providers that don't exist in models.dev. The model registry (models.ts) now merges both generated and custom models at startup. Custom entries are additive and never overwrite generated ones. Restores: qwen3.5-plus, qwen3-max-2026-01-23, qwen3-coder-next, qwen3-coder-plus, MiniMax-M2.5, glm-5, glm-4.7, kimi-k2.5 Fixes #2339 Co-authored-by: Claude Opus 4.6 (1M context) --- packages/pi-ai/src/models.custom.ts | 172 ++++++++++++++++++++++++++++ packages/pi-ai/src/models.test.ts | 85 ++++++++++++++ packages/pi-ai/src/models.ts | 18 ++- 3 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 packages/pi-ai/src/models.custom.ts create mode 100644 packages/pi-ai/src/models.test.ts diff --git a/packages/pi-ai/src/models.custom.ts b/packages/pi-ai/src/models.custom.ts new file mode 100644 index 000000000..5dd136ac0 --- /dev/null +++ b/packages/pi-ai/src/models.custom.ts @@ -0,0 +1,172 @@ +// Manually-maintained model definitions for providers NOT tracked by models.dev. +// +// The auto-generated file (models.generated.ts) is rebuilt from the models.dev +// third-party catalog. Providers that use proprietary endpoints and are not +// listed on models.dev must be defined here so they survive regeneration. +// +// See: https://github.com/gsd-build/gsd-2/issues/2339 +// +// To add a custom provider: +// 1. Add its model definitions below following the existing pattern. +// 2. Add its API key mapping to env-api-keys.ts. +// 3. Add its provider name to KnownProvider in types.ts (if not already there). + +import type { Model } from "./types.js"; + +export const CUSTOM_MODELS = { + // ─── Alibaba Coding Plan ───────────────────────────────────────────── + // Direct Alibaba DashScope Coding Plan endpoint (OpenAI-compatible). + // NOT the same as alibaba/* models on OpenRouter — different endpoint & auth. + // Original PR: #295 | Fixes: #1003, #1055, #1057 + "alibaba-coding-plan": { + "qwen3.5-plus": { + id: "qwen3.5-plus", + name: "Qwen3.5 Plus", + api: "openai-completions", + provider: "alibaba-coding-plan", + baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 983616, + maxTokens: 65536, + compat: { thinkingFormat: "qwen", supportsDeveloperRole: false }, + } satisfies Model<"openai-completions">, + "qwen3-max-2026-01-23": { + id: "qwen3-max-2026-01-23", + name: "Qwen3 Max 2026-01-23", + api: "openai-completions", + provider: "alibaba-coding-plan", + baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 258048, + maxTokens: 32768, + compat: { thinkingFormat: "qwen", supportsDeveloperRole: false }, + } satisfies Model<"openai-completions">, + "qwen3-coder-next": { + id: "qwen3-coder-next", + name: "Qwen3 Coder Next", + api: "openai-completions", + provider: "alibaba-coding-plan", + baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 65536, + compat: { supportsDeveloperRole: false }, + } satisfies Model<"openai-completions">, + "qwen3-coder-plus": { + id: "qwen3-coder-plus", + name: "Qwen3 Coder Plus", + api: "openai-completions", + provider: "alibaba-coding-plan", + baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 997952, + maxTokens: 65536, + compat: { supportsDeveloperRole: false }, + } satisfies Model<"openai-completions">, + "MiniMax-M2.5": { + id: "MiniMax-M2.5", + name: "MiniMax M2.5", + api: "openai-completions", + provider: "alibaba-coding-plan", + baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 196608, + maxTokens: 65536, + compat: { + supportsStore: false, + supportsDeveloperRole: false, + supportsReasoningEffort: true, + maxTokensField: "max_tokens", + }, + } satisfies Model<"openai-completions">, + "glm-5": { + id: "glm-5", + name: "GLM-5", + api: "openai-completions", + provider: "alibaba-coding-plan", + baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 16384, + compat: { thinkingFormat: "qwen", supportsDeveloperRole: false }, + } satisfies Model<"openai-completions">, + "glm-4.7": { + id: "glm-4.7", + name: "GLM-4.7", + api: "openai-completions", + provider: "alibaba-coding-plan", + baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 169984, + maxTokens: 16384, + compat: { thinkingFormat: "qwen", supportsDeveloperRole: false }, + } satisfies Model<"openai-completions">, + "kimi-k2.5": { + id: "kimi-k2.5", + name: "Kimi K2.5", + api: "openai-completions", + provider: "alibaba-coding-plan", + baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 258048, + maxTokens: 32768, + compat: { thinkingFormat: "zai", supportsDeveloperRole: false }, + } satisfies Model<"openai-completions">, + }, +} as const; diff --git a/packages/pi-ai/src/models.test.ts b/packages/pi-ai/src/models.test.ts new file mode 100644 index 000000000..a98c32b40 --- /dev/null +++ b/packages/pi-ai/src/models.test.ts @@ -0,0 +1,85 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { getProviders, getModels, getModel } from "./models.js"; + +// ═══════════════════════════════════════════════════════════════════════════ +// Custom provider preservation (regression: #2339) +// +// Custom providers (like alibaba-coding-plan) are manually maintained and +// NOT sourced from models.dev. They must survive models.generated.ts +// regeneration by living in models.custom.ts. +// ═══════════════════════════════════════════════════════════════════════════ + +describe("model registry — custom providers", () => { + it("alibaba-coding-plan is a registered provider", () => { + const providers = getProviders(); + assert.ok( + providers.includes("alibaba-coding-plan"), + `Expected "alibaba-coding-plan" in providers, got: ${providers.join(", ")}`, + ); + }); + + it("alibaba-coding-plan has all expected models", () => { + const models = getModels("alibaba-coding-plan"); + const ids = models.map((m) => m.id).sort(); + const expected = [ + "MiniMax-M2.5", + "glm-4.7", + "glm-5", + "kimi-k2.5", + "qwen3-coder-next", + "qwen3-coder-plus", + "qwen3-max-2026-01-23", + "qwen3.5-plus", + ]; + assert.deepEqual(ids, expected); + }); + + it("alibaba-coding-plan models use the correct base URL", () => { + const models = getModels("alibaba-coding-plan"); + for (const model of models) { + assert.equal( + model.baseUrl, + "https://coding-intl.dashscope.aliyuncs.com/v1", + `Model ${model.id} has wrong baseUrl: ${model.baseUrl}`, + ); + } + }); + + it("alibaba-coding-plan models use openai-completions API", () => { + const models = getModels("alibaba-coding-plan"); + for (const model of models) { + assert.equal(model.api, "openai-completions", `Model ${model.id} has wrong api: ${model.api}`); + } + }); + + it("alibaba-coding-plan models have provider set correctly", () => { + const models = getModels("alibaba-coding-plan"); + for (const model of models) { + assert.equal( + model.provider, + "alibaba-coding-plan", + `Model ${model.id} has wrong provider: ${model.provider}`, + ); + } + }); + + it("getModel retrieves alibaba-coding-plan models by provider+id", () => { + // Use type assertion to test runtime behavior — alibaba-coding-plan may come + // from custom models rather than the generated file, so the narrow + // GeneratedProvider type doesn't include it until models.custom.ts is merged. + const model = getModel("alibaba-coding-plan" as any, "qwen3.5-plus" as any); + assert.ok(model, "Expected getModel to return a model for alibaba-coding-plan/qwen3.5-plus"); + assert.equal(model.id, "qwen3.5-plus"); + assert.equal(model.provider, "alibaba-coding-plan"); + }); +}); + +describe("model registry — custom models do not collide with generated models", () => { + it("generated providers still exist alongside custom providers", () => { + const providers = getProviders(); + // Spot-check a few generated providers + assert.ok(providers.includes("openai"), "openai should be in providers"); + assert.ok(providers.includes("anthropic"), "anthropic should be in providers"); + }); +}); diff --git a/packages/pi-ai/src/models.ts b/packages/pi-ai/src/models.ts index 8a4805ac1..ee488fbec 100644 --- a/packages/pi-ai/src/models.ts +++ b/packages/pi-ai/src/models.ts @@ -1,9 +1,10 @@ import { MODELS } from "./models.generated.js"; +import { CUSTOM_MODELS } from "./models.custom.js"; import type { Api, KnownProvider, Model, Usage } from "./types.js"; const modelRegistry: Map>> = new Map(); -// Initialize registry from MODELS on module load +// Initialize registry from auto-generated MODELS (models.dev catalog) for (const [provider, models] of Object.entries(MODELS)) { const providerModels = new Map>(); for (const [id, model] of Object.entries(models)) { @@ -12,6 +13,21 @@ for (const [provider, models] of Object.entries(MODELS)) { modelRegistry.set(provider, providerModels); } +// Merge manually-maintained custom providers that are NOT in models.dev. +// Custom models are additive — they never overwrite generated entries. +// See: https://github.com/gsd-build/gsd-2/issues/2339 +for (const [provider, models] of Object.entries(CUSTOM_MODELS)) { + if (!modelRegistry.has(provider)) { + modelRegistry.set(provider, new Map>()); + } + const providerModels = modelRegistry.get(provider)!; + for (const [id, model] of Object.entries(models)) { + if (!providerModels.has(id)) { + providerModels.set(id, model as Model); + } + } +} + /** Providers that have entries in the generated MODELS constant */ type GeneratedProvider = keyof typeof MODELS & KnownProvider;