/** * Alibaba DashScope (ModelScope) provider for Oh My Pi * * Registers the DashScope OpenAI-compatible coding endpoint with all * configured Qwen3 / GLM-4 / Kimi models. * * Usage: * # Install from local path * omp plugin link ./omp-dashscope * * # Or install from npm (when published) * omp plugin install omp-dashscope * * # Configure via /login command * /login * # Select "Alibaba DashScope" and enter your API key * * # Or set via environment variable (takes precedence) * DASHSCOPE_API_KEY=sk-sp-... omp * * # Then switch models with /model or Ctrl+L * # Look for "dashscope/..." entries */ import type { ExtensionAPI, ProviderConfig } from "@oh-my-pi/pi-coding-agent"; import type { OAuthCredentials, OAuthLoginCallbacks } from "@oh-my-pi/pi-ai"; const DASHSCOPE_MODELS = [ { 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: { supportsStore: false, supportsDeveloperRole: false, supportsReasoningEffort: true, maxTokensField: "max_tokens", }, }, { 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: { supportsStore: false, supportsDeveloperRole: false, supportsReasoningEffort: true, maxTokensField: "max_tokens", }, }, { 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: { supportsStore: false, supportsDeveloperRole: false, supportsReasoningEffort: false, maxTokensField: "max_tokens", }, }, { 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: { supportsStore: false, supportsDeveloperRole: false, supportsReasoningEffort: false, maxTokensField: "max_tokens", }, }, { id: "glm-5", name: "GLM-5", reasoning: true, input: ["text"] as ["text"], contextWindow: 202752, maxTokens: 16384, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, compat: { supportsStore: false, supportsDeveloperRole: false, supportsReasoningEffort: true, maxTokensField: "max_tokens", }, }, { 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: { supportsStore: false, supportsDeveloperRole: false, supportsReasoningEffort: true, maxTokensField: "max_tokens", }, }, { 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", }, }, { 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: { supportsStore: false, supportsDeveloperRole: false, supportsReasoningEffort: true, maxTokensField: "max_tokens", }, }, ]; const DASHSCOPE_BASE_URL = "https://coding-intl.dashscope.aliyuncs.com/v1"; export default function (pi: ExtensionAPI) { pi.setLabel("DashScope Provider"); const oauthConfig: ProviderConfig["oauth"] = { name: "Alibaba DashScope", // DashScope uses a static API key, so return a plain string to persist it as // an `api_key` credential in auth storage. login: async (callbacks: OAuthLoginCallbacks): Promise => { const apiKey = await callbacks.onPrompt({ message: "DashScope API key (from https://modelstudio.console.alibabacloud.com):", placeholder: "sk-sp-...", }); if (!apiKey || apiKey.trim() === "") { throw new Error("No API key provided"); } return apiKey.trim(); }, // Compatibility for users who may already have older malformed oauth // credentials persisted from previous plugin versions. getApiKey: (credentials: OAuthCredentials): string => credentials.access || ((credentials as unknown as { accessToken?: string }).accessToken ?? ""), }; const providerConfig: ProviderConfig = { baseUrl: DASHSCOPE_BASE_URL, // Optional env-based fallback for non-/login users. // Keep unset by default so OAuth/login-only setups work without injecting // a literal "DASHSCOPE_API_KEY" auth header. apiKey: process.env.DASHSCOPE_API_KEY, api: "openai-completions", models: DASHSCOPE_MODELS, oauth: oauthConfig, }; // Register at extension load time for UI discovery pi.registerProvider("dashscope", providerConfig); // Keep extension-registered models alive across ModelRegistry.refresh() calls. // /model triggers refresh, which rebuilds from built-ins + models.yml and drops // runtime extension providers unless they are re-registered. pi.on("session_start", async (_event, ctx) => { const registry = ctx.modelRegistry as typeof ctx.modelRegistry & { __dashscopeRefreshWrapped__?: boolean; }; // Cleanup invalid legacy credentials (for example malformed oauth entries) // that make hasAuth() true but cannot yield a usable key. const stored = registry.authStorage.getAll().dashscope; const credentials = stored ? (Array.isArray(stored) ? stored : [stored]) : []; const hasUsableApiKey = credentials.some((credential) => { if (!credential || credential.type !== "api_key") return false; return typeof credential.key === "string" && credential.key.trim().length > 0; }); const hasUsableOauthKey = credentials.some((credential) => { if (!credential || credential.type !== "oauth") return false; const oauth = credential as unknown as { access?: unknown; accessToken?: unknown }; return ( (typeof oauth.access === "string" && oauth.access.trim().length > 0) || (typeof oauth.accessToken === "string" && oauth.accessToken.trim().length > 0) ); }); if (credentials.length > 0 && !hasUsableApiKey && !hasUsableOauthKey) { await registry.authStorage.remove("dashscope"); ctx.ui.notify("DashScope: removed invalid legacy credentials. Run /login to add your API key.", "warning"); } if (!registry.__dashscopeRefreshWrapped__) { const originalRefresh = registry.refresh.bind(registry); registry.refresh = async (...args: Parameters) => { try { await originalRefresh(...args); } finally { registry.registerProvider("dashscope", providerConfig); } }; registry.__dashscopeRefreshWrapped__ = true; } registry.registerProvider("dashscope", providerConfig); }); pi.registerCommand("dashscope-debug", { description: "Debug DashScope provider status", handler: async (_args, ctx) => { const registry = ctx.modelRegistry as typeof ctx.modelRegistry & { __dashscopeRefreshWrapped__?: boolean; }; const allModels = ctx.modelRegistry.getAll(); const dashscopeModels = allModels.filter((m) => m.provider === "dashscope"); const availableModels = ctx.modelRegistry .getAvailable() .filter((m) => m.provider === "dashscope"); const hasAuth = ctx.modelRegistry.authStorage.hasAuth("dashscope"); const info = [ `Total models: ${allModels.length}`, `DashScope models (all): ${dashscopeModels.length}`, `DashScope models (available): ${availableModels.length}`, `hasAuth(dashscope): ${hasAuth}`, `refresh wrapper active: ${registry.__dashscopeRefreshWrapped__ ? "yes" : "no"}`, `Env var DASHSCOPE_API_KEY: ${process.env.DASHSCOPE_API_KEY ? "set" : "not set"}`, `Models: ${dashscopeModels.map((m) => m.id).join(", ") || "none"}`, ].join("\n"); ctx.ui.notify(info, "info"); }, }); }