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) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher 2026-03-24 09:19:27 -04:00 committed by GitHub
parent eb30d3afd4
commit 6793489b78
3 changed files with 274 additions and 1 deletions

View file

@ -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;

View file

@ -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");
});
});

View file

@ -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<string, Map<string, Model<Api>>> = 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<string, Model<Api>>();
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<string, Model<Api>>());
}
const providerModels = modelRegistry.get(provider)!;
for (const [id, model] of Object.entries(models)) {
if (!providerModels.has(id)) {
providerModels.set(id, model as Model<Api>);
}
}
}
/** Providers that have entries in the generated MODELS constant */
type GeneratedProvider = keyof typeof MODELS & KnownProvider;