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:
parent
eb30d3afd4
commit
6793489b78
3 changed files with 274 additions and 1 deletions
172
packages/pi-ai/src/models.custom.ts
Normal file
172
packages/pi-ai/src/models.custom.ts
Normal 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;
|
||||
85
packages/pi-ai/src/models.test.ts
Normal file
85
packages/pi-ai/src/models.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue