Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/ai/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,8 @@ Built-in providers resolve these env vars (Node.js; in browsers pass `apiKey` ex
| Hugging Face | `HF_TOKEN` |
| OpenCode Zen / OpenCode Go | `OPENCODE_API_KEY` |
| Kimi For Coding | `KIMI_API_KEY` |
| Qwen Token Plan | `QWEN_TOKEN_PLAN_API_KEY` |
| Qwen Token Plan (China) | `QWEN_TOKEN_PLAN_CN_API_KEY` |
| Xiaomi MiMo (API billing) | `XIAOMI_API_KEY` |
| Xiaomi MiMo Token Plan (China) | `XIAOMI_TOKEN_PLAN_CN_API_KEY` |
| Xiaomi MiMo Token Plan (Amsterdam) | `XIAOMI_TOKEN_PLAN_AMS_API_KEY` |
Expand Down
74 changes: 74 additions & 0 deletions packages/ai/scripts/generate-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1825,6 +1825,57 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
}
}

// Process Alibaba Cloud Model Studio Token Plan models
// Two regions (international / cn) with identical catalogs, separate
// endpoints and API keys (sk-sp- prefix). models.dev keys are
// "alibaba-token-plan[-cn]"; pi exposes them as "qwen-token-plan[-cn]".
const qwenTokenPlanCompat: OpenAICompletionsCompat = {
thinkingFormat: "qwen",
supportsDeveloperRole: false,
supportsStore: false,
};
const qwenTokenPlanVariants = [
{
source: "alibaba-token-plan",
provider: "qwen-token-plan",
baseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1",
},
{
source: "alibaba-token-plan-cn",
provider: "qwen-token-plan-cn",
baseUrl: "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1",
},
] as const;

for (const { source, provider, baseUrl } of qwenTokenPlanVariants) {
const providerModels = data[source]?.models;
if (!providerModels) continue;

for (const [modelId, model] of Object.entries(providerModels)) {
const m = model as ModelsDevModel;
if (m.tool_call !== true) continue;

models.push({
id: modelId,
name: m.name || modelId,
api: "openai-completions",
provider,
baseUrl,
compat: qwenTokenPlanCompat,
reasoning: m.reasoning === true,
input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
cost: {
input: m.cost?.input || 0,
output: m.cost?.output || 0,
cacheRead: m.cost?.cache_read || 0,
cacheWrite: m.cost?.cache_write || 0,
},
contextWindow: m.limit?.context || 4096,
maxTokens: m.limit?.output || 4096,
});
}
}

console.log(`Loaded ${models.length} tool-capable models from models.dev`);
return models;
} catch (error) {
Expand Down Expand Up @@ -2227,6 +2278,29 @@ async function generateModels() {
});
}

// Add qwen3.8-max-preview to Qwen Token Plan providers until models.dev includes it
for (const qwenTpProvider of ["qwen-token-plan", "qwen-token-plan-cn"] as const) {
if (!allModels.some((m) => m.provider === qwenTpProvider && m.id === "qwen3.8-max-preview")) {
const baseUrl =
qwenTpProvider === "qwen-token-plan"
? "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1"
: "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1";
allModels.push({
id: "qwen3.8-max-preview",
name: "Qwen3.8 Max Preview",
api: "openai-completions",
provider: qwenTpProvider,
baseUrl,
compat: { thinkingFormat: "qwen", supportsDeveloperRole: false, supportsStore: false } satisfies OpenAICompletionsCompat,
reasoning: true,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1000000,
maxTokens: 65536,
});
}
}

// Add "auto" alias for openrouter/auto
if (!allModels.some(m => m.provider === "openrouter" && m.id === "auto")) {
allModels.push({
Expand Down
2 changes: 2 additions & 0 deletions packages/ai/src/env-api-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ function getApiKeyEnvVars(provider: string): readonly string[] | undefined {

const envMap: Record<string, string> = {
"ant-ling": "ANT_LING_API_KEY",
"qwen-token-plan": "QWEN_TOKEN_PLAN_API_KEY",
"qwen-token-plan-cn": "QWEN_TOKEN_PLAN_CN_API_KEY",
openai: "OPENAI_API_KEY",
"azure-openai-responses": "AZURE_OPENAI_API_KEY",
nvidia: "NVIDIA_API_KEY",
Expand Down
4 changes: 4 additions & 0 deletions packages/ai/src/models.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import { OPENAI_CODEX_MODELS } from "./providers/openai-codex.models.ts";
import { OPENCODE_MODELS } from "./providers/opencode.models.ts";
import { OPENCODE_GO_MODELS } from "./providers/opencode-go.models.ts";
import { OPENROUTER_MODELS } from "./providers/openrouter.models.ts";
import { QWEN_TOKEN_PLAN_MODELS } from "./providers/qwen-token-plan.models.ts";
import { QWEN_TOKEN_PLAN_CN_MODELS } from "./providers/qwen-token-plan-cn.models.ts";
import { TOGETHER_MODELS } from "./providers/together.models.ts";
import { VERCEL_AI_GATEWAY_MODELS } from "./providers/vercel-ai-gateway.models.ts";
import { XAI_MODELS } from "./providers/xai.models.ts";
Expand Down Expand Up @@ -64,6 +66,8 @@ export const MODELS = {
"opencode": OPENCODE_MODELS,
"opencode-go": OPENCODE_GO_MODELS,
"openrouter": OPENROUTER_MODELS,
"qwen-token-plan": QWEN_TOKEN_PLAN_MODELS,
"qwen-token-plan-cn": QWEN_TOKEN_PLAN_CN_MODELS,
"together": TOGETHER_MODELS,
"vercel-ai-gateway": VERCEL_AI_GATEWAY_MODELS,
"xai": XAI_MODELS,
Expand Down
4 changes: 4 additions & 0 deletions packages/ai/src/providers/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import { opencodeProvider } from "./opencode.ts";
import { opencodeGoProvider } from "./opencode-go.ts";
import { openrouterProvider } from "./openrouter.ts";
import { openrouterImagesProvider } from "./openrouter-images.ts";
import { qwenTokenPlanProvider } from "./qwen-token-plan.ts";
import { qwenTokenPlanCnProvider } from "./qwen-token-plan-cn.ts";
import { radiusProvider } from "./radius.ts";
import { togetherProvider } from "./together.ts";
import { vercelAIGatewayProvider } from "./vercel-ai-gateway.ts";
Expand Down Expand Up @@ -103,6 +105,8 @@ export function builtinProviders(): Provider[] {
opencodeProvider(),
opencodeGoProvider(),
openrouterProvider(),
qwenTokenPlanProvider(),
qwenTokenPlanCnProvider(),
radiusProvider(),
togetherProvider(),
vercelAIGatewayProvider(),
Expand Down
68 changes: 68 additions & 0 deletions packages/ai/src/providers/qwen-token-plan-cn.models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update

import values from "./data/qwen-token-plan-cn.json" with { type: "json" };
import type { Model } from "../types.ts";

export const QWEN_TOKEN_PLAN_CN_MODELS = values as {
"MiniMax-M2.5": Model<"openai-completions"> & {
id: "MiniMax-M2.5";
provider: "qwen-token-plan-cn";
};
"deepseek-v3.2": Model<"openai-completions"> & {
id: "deepseek-v3.2";
provider: "qwen-token-plan-cn";
};
"deepseek-v4-flash": Model<"openai-completions"> & {
id: "deepseek-v4-flash";
provider: "qwen-token-plan-cn";
};
"deepseek-v4-pro": Model<"openai-completions"> & {
id: "deepseek-v4-pro";
provider: "qwen-token-plan-cn";
};
"glm-5": Model<"openai-completions"> & {
id: "glm-5";
provider: "qwen-token-plan-cn";
};
"glm-5.1": Model<"openai-completions"> & {
id: "glm-5.1";
provider: "qwen-token-plan-cn";
};
"glm-5.2": Model<"openai-completions"> & {
id: "glm-5.2";
provider: "qwen-token-plan-cn";
};
"kimi-k2.5": Model<"openai-completions"> & {
id: "kimi-k2.5";
provider: "qwen-token-plan-cn";
};
"kimi-k2.6": Model<"openai-completions"> & {
id: "kimi-k2.6";
provider: "qwen-token-plan-cn";
};
"kimi-k2.7-code": Model<"openai-completions"> & {
id: "kimi-k2.7-code";
provider: "qwen-token-plan-cn";
};
"qwen3.6-flash": Model<"openai-completions"> & {
id: "qwen3.6-flash";
provider: "qwen-token-plan-cn";
};
"qwen3.6-plus": Model<"openai-completions"> & {
id: "qwen3.6-plus";
provider: "qwen-token-plan-cn";
};
"qwen3.7-max": Model<"openai-completions"> & {
id: "qwen3.7-max";
provider: "qwen-token-plan-cn";
};
"qwen3.7-plus": Model<"openai-completions"> & {
id: "qwen3.7-plus";
provider: "qwen-token-plan-cn";
};
"qwen3.8-max-preview": Model<"openai-completions"> & {
id: "qwen3.8-max-preview";
provider: "qwen-token-plan-cn";
};
};
15 changes: 15 additions & 0 deletions packages/ai/src/providers/qwen-token-plan-cn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { QWEN_TOKEN_PLAN_CN_MODELS } from "./qwen-token-plan-cn.models.ts";

export function qwenTokenPlanCnProvider(): Provider<"openai-completions"> {
return createProvider({
id: "qwen-token-plan-cn",
name: "Qwen Token Plan CN",
baseUrl: "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1",
auth: { apiKey: envApiKeyAuth("Qwen Token Plan CN API key", ["QWEN_TOKEN_PLAN_CN_API_KEY"]) },
models: Object.values(QWEN_TOKEN_PLAN_CN_MODELS),
api: openAICompletionsApi(),
});
}
68 changes: 68 additions & 0 deletions packages/ai/src/providers/qwen-token-plan.models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update

import values from "./data/qwen-token-plan.json" with { type: "json" };
import type { Model } from "../types.ts";

export const QWEN_TOKEN_PLAN_MODELS = values as {
"MiniMax-M2.5": Model<"openai-completions"> & {
id: "MiniMax-M2.5";
provider: "qwen-token-plan";
};
"deepseek-v3.2": Model<"openai-completions"> & {
id: "deepseek-v3.2";
provider: "qwen-token-plan";
};
"deepseek-v4-flash": Model<"openai-completions"> & {
id: "deepseek-v4-flash";
provider: "qwen-token-plan";
};
"deepseek-v4-pro": Model<"openai-completions"> & {
id: "deepseek-v4-pro";
provider: "qwen-token-plan";
};
"glm-5": Model<"openai-completions"> & {
id: "glm-5";
provider: "qwen-token-plan";
};
"glm-5.1": Model<"openai-completions"> & {
id: "glm-5.1";
provider: "qwen-token-plan";
};
"glm-5.2": Model<"openai-completions"> & {
id: "glm-5.2";
provider: "qwen-token-plan";
};
"kimi-k2.5": Model<"openai-completions"> & {
id: "kimi-k2.5";
provider: "qwen-token-plan";
};
"kimi-k2.6": Model<"openai-completions"> & {
id: "kimi-k2.6";
provider: "qwen-token-plan";
};
"kimi-k2.7-code": Model<"openai-completions"> & {
id: "kimi-k2.7-code";
provider: "qwen-token-plan";
};
"qwen3.6-flash": Model<"openai-completions"> & {
id: "qwen3.6-flash";
provider: "qwen-token-plan";
};
"qwen3.6-plus": Model<"openai-completions"> & {
id: "qwen3.6-plus";
provider: "qwen-token-plan";
};
"qwen3.7-max": Model<"openai-completions"> & {
id: "qwen3.7-max";
provider: "qwen-token-plan";
};
"qwen3.7-plus": Model<"openai-completions"> & {
id: "qwen3.7-plus";
provider: "qwen-token-plan";
};
"qwen3.8-max-preview": Model<"openai-completions"> & {
id: "qwen3.8-max-preview";
provider: "qwen-token-plan";
};
};
15 changes: 15 additions & 0 deletions packages/ai/src/providers/qwen-token-plan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { QWEN_TOKEN_PLAN_MODELS } from "./qwen-token-plan.models.ts";

export function qwenTokenPlanProvider(): Provider<"openai-completions"> {
return createProvider({
id: "qwen-token-plan",
name: "Qwen Token Plan",
baseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1",
auth: { apiKey: envApiKeyAuth("Qwen Token Plan API key", ["QWEN_TOKEN_PLAN_API_KEY"]) },
models: Object.values(QWEN_TOKEN_PLAN_MODELS),
api: openAICompletionsApi(),
});
}
2 changes: 2 additions & 0 deletions packages/ai/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ export type KnownProvider =
| "kimi-coding"
| "cloudflare-workers-ai"
| "cloudflare-ai-gateway"
| "qwen-token-plan"
| "qwen-token-plan-cn"
| "xiaomi"
| "xiaomi-token-plan-cn"
| "xiaomi-token-plan-ams"
Expand Down
3 changes: 3 additions & 0 deletions packages/ai/src/utils/overflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type { AssistantMessage } from "../types.ts";
* - Xiaomi MiMo: Truncates input to fill contextWindow exactly, then returns finish_reason "length"
* with output=0 (no room left to generate). Detected via stopReason "length" + zero output +
* input filling the context window.
* - DashScope/Qwen: "Range of input length should be [1, X]" (HTTP 400 invalid_parameter_error)
* - Ollama: Some deployments truncate silently, others return errors like "prompt too long; exceeded max context length by X tokens"
*/
const OVERFLOW_PATTERNS = [
Expand All @@ -54,6 +55,7 @@ const OVERFLOW_PATTERNS = [
/prompt has [\d,]+ tokens?, but the configured context size is [\d,]+ tokens?/i, // DS4 server
/model_context_window_exceeded/i, // z.ai non-standard finish_reason surfaced as error text
/prompt too long; exceeded (?:max )?context length/i, // Ollama explicit overflow error
/range of input length should be/i, // DashScope / Qwen Token Plan
/context[_ ]length[_ ]exceeded/i, // Generic fallback
/too many tokens/i, // Generic fallback
/token limit exceeded/i, // Generic fallback
Expand Down Expand Up @@ -101,6 +103,7 @@ const NON_OVERFLOW_PATTERNS = [
* - LM Studio: "greater than the context length"
* - Kimi For Coding: "exceeded model token limit: X (requested: Y)"
* - DS4: "Prompt has X tokens, but the configured context size is Y tokens"
* - DashScope/Qwen: "Range of input length should be [1, X]"
*
* **Unreliable detection:**
* - z.ai: Sometimes accepts overflow silently (detectable via usage.input > contextWindow),
Expand Down
24 changes: 24 additions & 0 deletions packages/ai/test/abort.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,30 @@ describe("AI Providers Abort Tests", () => {
});
});

describe.skipIf(!process.env.QWEN_TOKEN_PLAN_API_KEY)("Qwen Token Plan Provider Abort", () => {
const llm = getModel("qwen-token-plan", "qwen3.7-max");

it("should abort mid-stream", { retry: 3 }, async () => {
await testAbortSignal(llm);
});

it("should handle immediate abort", { retry: 3 }, async () => {
await testImmediateAbort(llm);
});
});

describe.skipIf(!process.env.QWEN_TOKEN_PLAN_CN_API_KEY)("Qwen Token Plan (CN) Provider Abort", () => {
const llm = getModel("qwen-token-plan-cn", "qwen3.7-max");

it("should abort mid-stream", { retry: 3 }, async () => {
await testAbortSignal(llm);
});

it("should handle immediate abort", { retry: 3 }, async () => {
await testImmediateAbort(llm);
});
});

describe.skipIf(!process.env.KIMI_API_KEY)("Kimi For Coding Provider Abort", () => {
const llm = getModel("kimi-coding", "kimi-for-coding");

Expand Down
Loading