/** * Alibaba DashScope (ModelStudio) provider for pi.dev * * Registers the DashScope OpenAI-compatible coding endpoint with all * configured Qwen3 / GLM-4 / Kimi models. * * Usage: * # Install from local path * pi install ./pi-dashscope * * # Or test in-place without installing * pi -e ./pi-dashscope * * # Configure your API key interactively * /dashscope-configure * * # Or set via environment variable (takes precedence over saved config) * DASHSCOPE_API_KEY=sk-sp-... pi * * # Then switch models with /model or Ctrl+L * # Look for "dashscope/..." entries */ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; const CONFIG_FILE = join(homedir(), ".pi", "agent", "dashscope.json"); function readStoredKey(): string | undefined { try { if (existsSync(CONFIG_FILE)) { const data = JSON.parse(readFileSync(CONFIG_FILE, "utf-8")); return typeof data.apiKey === "string" ? data.apiKey : undefined; } } catch { // ignore malformed config } return undefined; } function saveKey(apiKey: string): void { const dir = join(homedir(), ".pi", "agent"); mkdirSync(dir, { recursive: true }); writeFileSync(CONFIG_FILE, JSON.stringify({ apiKey }, null, 2), "utf-8"); } const DASHSCOPE_MODELS = [ // ── Qwen 3.5 Plus ────────────────────────────────────────────────────── // Extended thinking enabled | 960K context | 64K output { id: "qwen3.5-plus", name: "Qwen 3.5 Plus", reasoning: true, input: ["text"] as ["text"], contextWindow: 983616, maxTokens: 65536, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, compat: { thinkingFormat: "qwen", supportsDeveloperRole: false }, }, // ── Qwen3 Max 2026-01-23 ─────────────────────────────────────────────── // Extended thinking enabled | 252K context | 32K output { id: "qwen3-max-2026-01-23", name: "Qwen3 Max (2026-01-23)", reasoning: true, input: ["text"] as ["text"], contextWindow: 258048, maxTokens: 32768, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, compat: { thinkingFormat: "qwen", supportsDeveloperRole: false }, }, // ── Qwen3 Coder Plus ─────────────────────────────────────────────────── // No thinking | 975K context | 64K output { id: "qwen3-coder-plus", name: "Qwen3 Coder Plus", reasoning: false, input: ["text"] as ["text"], contextWindow: 997952, maxTokens: 65536, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, compat: { supportsDeveloperRole: false }, }, // ── Qwen3 Coder Next ─────────────────────────────────────────────────── // No thinking | 200K context | 64K output { id: "qwen3-coder-next", name: "Qwen3 Coder Next", reasoning: false, input: ["text"] as ["text"], contextWindow: 204800, maxTokens: 65536, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, compat: { supportsDeveloperRole: false }, }, // ── GLM-4.7 ──────────────────────────────────────────────────────────── // Extended thinking enabled | 166K context | 16K output { id: "glm-4.7", name: "GLM-4.7", reasoning: true, input: ["text"] as ["text"], contextWindow: 169984, maxTokens: 16384, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, compat: { thinkingFormat: "qwen", supportsDeveloperRole: false }, }, { id: "glm-5", name: "GLM-5", reasoning: true, input: ["text"] as ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 202752, maxTokens: 16384, compat: { thinkingFormat: "qwen", supportsDeveloperRole: false }, }, { id: "minimax-m2.5", name: "Minimax M2.5", reasoning: true, input: ["text"] as ["text"], contextWindow: 196608, maxTokens: 65536, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, compat: { supportsStore: false, supportsDeveloperRole: false, supportsReasoningEffort: true, maxTokensField: "max_tokens", }, }, // ── Kimi K2.5 ───────────────────────────────────────────────────────── // Extended thinking enabled | 252K context | 32K output { id: "kimi-k2.5", name: "Kimi K2.5", reasoning: true, input: ["text"] as ["text"], contextWindow: 258048, maxTokens: 32768, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, compat: { thinkingFormat: "zai", supportsDeveloperRole: false }, }, ]; const DASHSCOPE_BASE_URL = "https://coding-intl.dashscope.aliyuncs.com/v1"; export default function (pi: ExtensionAPI) { // Resolve API key: env var takes precedence, then saved config, then env var name // (pi resolves env var name strings automatically, so "BAILIAN_API_KEY" is a // valid apiKey value that tells pi to look up process.env.BAILIAN_API_KEY). const resolvedKey = process.env.DASHSCOPE_API_KEY ?? readStoredKey() ?? "DASHSCOPE_API_KEY"; pi.registerProvider("dashscope", { baseUrl: DASHSCOPE_BASE_URL, apiKey: resolvedKey, api: "openai-completions", models: DASHSCOPE_MODELS, }); pi.on("session_start", async (_event, ctx) => { const hasKey = process.env.DASHSCOPE_API_KEY !== undefined || readStoredKey() !== undefined; if (!hasKey) { ctx.ui.notify( "DashScope: No API key configured. Run /dashscope-configure to set up.", "warning", ); } }); pi.registerCommand("dashscope-configure", { description: "Configure your Alibaba DashScope API key", handler: async (_args, ctx) => { const apiKey = await ctx.ui.input( "DashScope API key (from https://modelstudio.console.alibabacloud.com):", "sk-sp-...", ); if (!apiKey || apiKey.trim() === "") { ctx.ui.notify("No API key provided. Configuration cancelled.", "warning"); return; } const trimmedKey = apiKey.trim(); saveKey(trimmedKey); // Update the live model registry directly — pi.registerProvider() only // queues to pendingProviderRegistrations which is drained once at startup. ctx.modelRegistry.registerProvider("dashscope", { baseUrl: DASHSCOPE_BASE_URL, apiKey: trimmedKey, api: "openai-completions", models: DASHSCOPE_MODELS, }); ctx.ui.notify("DashScope API key saved and applied successfully!", "success"); }, }); }