diff --git a/README.md b/README.md index 2a0c659f0..e97ebdbb2 100644 --- a/README.md +++ b/README.md @@ -464,6 +464,7 @@ return config | Mistral | `MISTRAL_API_KEY` | | Groq | `GROQ_API_KEY` | | Cerebras | `CEREBRAS_API_KEY` | +| Synthetic | `SYNTHETIC_API_KEY` | | xAI | `XAI_API_KEY` | | OpenRouter | `OPENROUTER_API_KEY` | | Z.AI | `ZAI_API_KEY` | @@ -484,6 +485,7 @@ Use `/login` to authenticate with supported providers: - Perplexity - OpenCode Zen - Z.AI (GLM Coding Plan) +- Synthetic - MiniMax Coding Plan (International / China) ```bash diff --git a/docs/environment-variables.md b/docs/environment-variables.md index e2103135b..de69bae48 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -37,6 +37,7 @@ These are consumed via `getEnvApiKey()` (`packages/ai/src/stream.ts`) unless not | `GOOGLE_API_KEY` | Gemini image tool auth fallback | Using `gemini_image` tool without `GEMINI_API_KEY` | Used by coding-agent image tool fallback path | | `GROQ_API_KEY` | Groq auth | Using Groq models | | | `CEREBRAS_API_KEY` | Cerebras auth | Using Cerebras models | | +| `SYNTHETIC_API_KEY` | Synthetic auth | Using Synthetic models | | | `XAI_API_KEY` | xAI auth | Using xAI models | | | `OPENROUTER_API_KEY` | OpenRouter auth | Using OpenRouter models | Also used by image tool when preferred/auto provider is OpenRouter | | `MISTRAL_API_KEY` | Mistral auth | Using Mistral models | | diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 79126d603..2432604ff 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,7 @@ ## [Unreleased] - Added Synthetic provider +- Added API-key login helpers for Synthetic and Cerebras providers ## [12.10.0] - 2026-02-18 diff --git a/packages/ai/README.md b/packages/ai/README.md index d063e840d..2479313de 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -898,6 +898,7 @@ In Node.js environments, you can set environment variables to avoid passing API | Mistral | `MISTRAL_API_KEY` | | Groq | `GROQ_API_KEY` | | Cerebras | `CEREBRAS_API_KEY` | +| Synthetic | `SYNTHETIC_API_KEY` | | xAI | `XAI_API_KEY` | | OpenRouter | `OPENROUTER_API_KEY` | | zAI | `ZAI_API_KEY` | diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 2d3c2f768..e31628251 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -266,7 +266,7 @@ async function fetchKimiCodeModels(): Promise[]> { } } -const SYNTHETIC_BASE_URL = "https://api.synthetic.new/v1"; +const SYNTHETIC_BASE_URL = "https://api.synthetic.new/openai/v1"; interface SyntheticModelInfo { id: string; @@ -277,9 +277,22 @@ interface SyntheticModelInfo { } async function fetchSyntheticModels(): Promise[]> { - // Synthetic.new /models endpoint requires authentication - // Use SYNTHETIC_API_KEY env var if available, otherwise return fallback models - const apiKey = $env.SYNTHETIC_API_KEY; + // Synthetic.new /models endpoint requires authentication. + // Prefer SYNTHETIC_API_KEY, then fall back to agent.db (/login synthetic). + let apiKey = $env.SYNTHETIC_API_KEY; + if (!apiKey) { + try { + const storage = await CliAuthStorage.create(); + try { + const storedApiKey = storage.getApiKey("synthetic"); + if (storedApiKey) apiKey = storedApiKey; + } finally { + storage.close(); + } + } catch { + // Ignore missing/unreadable auth storage; fallback models will be used. + } + } if (apiKey) { try { console.log("Fetching models from Synthetic.new API..."); @@ -329,27 +342,109 @@ async function fetchSyntheticModels(): Promise[]> { } } - console.log("No Synthetic.new credentials found, using fallback models"); + console.log("No Synthetic credentials found (env or agent.db), using fallback models"); return getSyntheticFallbackModels(); } function getSyntheticFallbackModels(): Model<"openai-completions">[] { return [ { - id: "default", - name: "Synthetic Default", + id: "hf:moonshotai/Kimi-K2.5", + name: "moonshotai/Kimi-K2.5", api: "openai-completions", provider: "synthetic", baseUrl: SYNTHETIC_BASE_URL, - reasoning: true, - input: ["text", "image"], + reasoning: false, + input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 200000, + contextWindow: 262144, maxTokens: 8192, }, ]; } +const CEREBRAS_BASE_URL = "https://api.cerebras.ai/v1"; + +interface CerebrasModelInfo { + id: string; + name?: string; + context_length?: number; + max_completion_tokens?: number; + max_tokens?: number; + reasoning?: boolean; + modalities?: { + input?: string[]; + }; +} + +async function fetchCerebrasModels(): Promise[]> { + // Cerebras /models endpoint requires authentication. + // Prefer CEREBRAS_API_KEY, then fall back to agent.db (/login cerebras). + let apiKey = $env.CEREBRAS_API_KEY; + if (!apiKey) { + try { + const storage = await CliAuthStorage.create(); + try { + const storedApiKey = storage.getApiKey("cerebras"); + if (storedApiKey) apiKey = storedApiKey; + } finally { + storage.close(); + } + } catch { + // Ignore missing/unreadable auth storage; existing models will be used. + } + } + + if (!apiKey) { + console.log("No Cerebras credentials found (env or agent.db), will use previous models"); + return []; + } + + try { + console.log("Fetching models from Cerebras API..."); + const response = await fetch(`${CEREBRAS_BASE_URL}/models`, { + headers: { Authorization: `Bearer ${apiKey}` }, + }); + + if (!response.ok) { + console.warn(`Cerebras API returned ${response.status}, will use previous models`); + return []; + } + + const data = await response.json(); + const items = Array.isArray(data.data) ? (data.data as CerebrasModelInfo[]) : []; + const models: Model<"openai-completions">[] = []; + + for (const model of items) { + if (!model.id) continue; + + const supportsImage = model.modalities?.input?.includes("image") ?? false; + const reasoning = model.reasoning === true || model.id.toLowerCase().includes("reasoning"); + + models.push({ + id: model.id, + name: model.name || model.id, + api: "openai-completions", + provider: "cerebras", + baseUrl: CEREBRAS_BASE_URL, + reasoning, + input: supportsImage ? ["text", "image"] : ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: model.context_length || 131072, + maxTokens: model.max_completion_tokens || model.max_tokens || 32768, + }); + } + + models.sort((a, b) => a.id.localeCompare(b.id)); + console.log(`Fetched ${models.length} models from Cerebras API`); + return models; + } catch (error) { + console.error("Failed to fetch Cerebras models:", error); + return []; + } +} + + async function loadModelsDevData(): Promise { try { console.log("Fetching models from models.dev API..."); @@ -984,9 +1079,10 @@ async function generateModels() { const aiGatewayModels = await fetchAiGatewayModels(); const kimiCodeModels = await fetchKimiCodeModels(); const syntheticNewModels = await fetchSyntheticModels(); + const cerebrasModels = await fetchCerebrasModels(); // Combine models (models.dev has priority) - const allModels = [...modelsDevModels, ...openRouterModels, ...aiGatewayModels, ...kimiCodeModels, ...syntheticNewModels]; + const allModels = [...modelsDevModels, ...openRouterModels, ...aiGatewayModels, ...kimiCodeModels, ...syntheticNewModels, ...cerebrasModels]; // Fix incorrect cache pricing for Claude Opus 4.5 from models.dev // models.dev has 3x the correct pricing (1.5/18.75 instead of 0.5/6.25) diff --git a/packages/ai/src/models.json b/packages/ai/src/models.json index 575405b0c..e3972d13c 100644 --- a/packages/ai/src/models.json +++ b/packages/ai/src/models.json @@ -5302,6 +5302,44 @@ "contextWindow": 131000, "maxTokens": 32000 }, + "qwen-3-coder-480b": { + "id": "qwen-3-coder-480b", + "name": "qwen-3-coder-480b", + "api": "openai-completions", + "provider": "cerebras", + "baseUrl": "https://api.cerebras.ai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 131072, + "maxTokens": 32768 + }, + "zai-glm-4.6": { + "id": "zai-glm-4.6", + "name": "zai-glm-4.6", + "api": "openai-completions", + "provider": "cerebras", + "baseUrl": "https://api.cerebras.ai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 131072, + "maxTokens": 32768 + }, "zai-glm-4.7": { "id": "zai-glm-4.7", "name": "Z.AI GLM-4.7", @@ -9578,9 +9616,9 @@ "text" ], "cost": { - "input": 0.049999999999999996, - "output": 0.22, - "cacheRead": 0.024999999999999998, + "input": 0.06, + "output": 0.24, + "cacheRead": 0, "cacheWrite": 0 }, "contextWindow": 40960, @@ -9597,13 +9635,13 @@ "text" ], "cost": { - "input": 0.3, - "output": 1.2, - "cacheRead": 0.15, + "input": 0.45499999999999996, + "output": 1.8199999999999998, + "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 40960, - "maxTokens": 40960 + "contextWindow": 131072, + "maxTokens": 8192 }, "qwen/qwen3-235b-a22b-2507": { "id": "qwen/qwen3-235b-a22b-2507", @@ -9654,9 +9692,9 @@ "text" ], "cost": { - "input": 0.06, - "output": 0.22, - "cacheRead": 0.03, + "input": 0.08, + "output": 0.28, + "cacheRead": 0, "cacheWrite": 0 }, "contextWindow": 40960, @@ -9673,9 +9711,9 @@ "text" ], "cost": { - "input": 0.08, - "output": 0.33, - "cacheRead": 0.04, + "input": 0.09, + "output": 0.3, + "cacheRead": 0, "cacheWrite": 0 }, "contextWindow": 262144, @@ -10603,13 +10641,13 @@ "text" ], "cost": { - "input": 0.35, - "output": 1.55, - "cacheRead": 0.175, + "input": 0.55, + "output": 2, + "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 131072, - "maxTokens": 65536 + "contextWindow": 131000, + "maxTokens": 131000 }, "z-ai/glm-4.5-air": { "id": "z-ai/glm-4.5-air", @@ -10680,13 +10718,13 @@ "text" ], "cost": { - "input": 0.33999999999999997, - "output": 1.7, - "cacheRead": 0.16999999999999998, + "input": 0.35, + "output": 1.71, + "cacheRead": 0, "cacheWrite": 0 }, "contextWindow": 202752, - "maxTokens": 65536 + "maxTokens": 131072 }, "z-ai/glm-4.6:exacto": { "id": "z-ai/glm-4.6:exacto", @@ -13421,6 +13459,274 @@ } } }, + "synthetic": { + "hf:deepseek-ai/DeepSeek-R1-0528": { + "id": "hf:deepseek-ai/DeepSeek-R1-0528", + "name": "deepseek-ai/DeepSeek-R1-0528", + "api": "openai-completions", + "provider": "synthetic", + "baseUrl": "https://api.synthetic.new/openai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 131072, + "maxTokens": 8192 + }, + "hf:deepseek-ai/DeepSeek-V3": { + "id": "hf:deepseek-ai/DeepSeek-V3", + "name": "deepseek-ai/DeepSeek-V3", + "api": "openai-completions", + "provider": "synthetic", + "baseUrl": "https://api.synthetic.new/openai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 131072, + "maxTokens": 8192 + }, + "hf:deepseek-ai/DeepSeek-V3-0324": { + "id": "hf:deepseek-ai/DeepSeek-V3-0324", + "name": "deepseek-ai/DeepSeek-V3-0324", + "api": "openai-completions", + "provider": "synthetic", + "baseUrl": "https://api.synthetic.new/openai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 131072, + "maxTokens": 8192 + }, + "hf:deepseek-ai/DeepSeek-V3.2": { + "id": "hf:deepseek-ai/DeepSeek-V3.2", + "name": "deepseek-ai/DeepSeek-V3.2", + "api": "openai-completions", + "provider": "synthetic", + "baseUrl": "https://api.synthetic.new/openai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 162816, + "maxTokens": 8192 + }, + "hf:meta-llama/Llama-3.3-70B-Instruct": { + "id": "hf:meta-llama/Llama-3.3-70B-Instruct", + "name": "meta-llama/Llama-3.3-70B-Instruct", + "api": "openai-completions", + "provider": "synthetic", + "baseUrl": "https://api.synthetic.new/openai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 131072, + "maxTokens": 8192 + }, + "hf:MiniMaxAI/MiniMax-M2.1": { + "id": "hf:MiniMaxAI/MiniMax-M2.1", + "name": "MiniMaxAI/MiniMax-M2.1", + "api": "openai-completions", + "provider": "synthetic", + "baseUrl": "https://api.synthetic.new/openai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 196608, + "maxTokens": 8192 + }, + "hf:moonshotai/Kimi-K2-Instruct-0905": { + "id": "hf:moonshotai/Kimi-K2-Instruct-0905", + "name": "moonshotai/Kimi-K2-Instruct-0905", + "api": "openai-completions", + "provider": "synthetic", + "baseUrl": "https://api.synthetic.new/openai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 8192 + }, + "hf:moonshotai/Kimi-K2-Thinking": { + "id": "hf:moonshotai/Kimi-K2-Thinking", + "name": "moonshotai/Kimi-K2-Thinking", + "api": "openai-completions", + "provider": "synthetic", + "baseUrl": "https://api.synthetic.new/openai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 8192 + }, + "hf:moonshotai/Kimi-K2.5": { + "id": "hf:moonshotai/Kimi-K2.5", + "name": "moonshotai/Kimi-K2.5", + "api": "openai-completions", + "provider": "synthetic", + "baseUrl": "https://api.synthetic.new/openai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 8192 + }, + "hf:nvidia/Kimi-K2.5-NVFP4": { + "id": "hf:nvidia/Kimi-K2.5-NVFP4", + "name": "nvidia/Kimi-K2.5-NVFP4", + "api": "openai-completions", + "provider": "synthetic", + "baseUrl": "https://api.synthetic.new/openai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 8192 + }, + "hf:openai/gpt-oss-120b": { + "id": "hf:openai/gpt-oss-120b", + "name": "openai/gpt-oss-120b", + "api": "openai-completions", + "provider": "synthetic", + "baseUrl": "https://api.synthetic.new/openai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 131072, + "maxTokens": 8192 + }, + "hf:Qwen/Qwen3-235B-A22B-Thinking-2507": { + "id": "hf:Qwen/Qwen3-235B-A22B-Thinking-2507", + "name": "Qwen/Qwen3-235B-A22B-Thinking-2507", + "api": "openai-completions", + "provider": "synthetic", + "baseUrl": "https://api.synthetic.new/openai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 8192 + }, + "hf:Qwen/Qwen3-Coder-480B-A35B-Instruct": { + "id": "hf:Qwen/Qwen3-Coder-480B-A35B-Instruct", + "name": "Qwen/Qwen3-Coder-480B-A35B-Instruct", + "api": "openai-completions", + "provider": "synthetic", + "baseUrl": "https://api.synthetic.new/openai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 8192 + }, + "hf:zai-org/GLM-4.7": { + "id": "hf:zai-org/GLM-4.7", + "name": "zai-org/GLM-4.7", + "api": "openai-completions", + "provider": "synthetic", + "baseUrl": "https://api.synthetic.new/openai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 202752, + "maxTokens": 8192 + } + }, "openai-codex": { "gpt-5": { "id": "gpt-5", @@ -14488,273 +14794,5 @@ "contextWindow": 32768, "maxTokens": 8192 } - }, - "synthetic": { - "hf:deepseek-ai/DeepSeek-R1-0528": { - "id": "hf:deepseek-ai/DeepSeek-R1-0528", - "name": "deepseek-ai/DeepSeek-R1-0528", - "api": "openai-completions", - "provider": "synthetic", - "baseUrl": "https://api.synthetic.new/v1", - "reasoning": false, - "input": [ - "text" - ], - "cost": { - "input": 0, - "output": 0, - "cacheRead": 0, - "cacheWrite": 0 - }, - "contextWindow": 131072, - "maxTokens": 8192 - }, - "hf:deepseek-ai/DeepSeek-V3": { - "id": "hf:deepseek-ai/DeepSeek-V3", - "name": "deepseek-ai/DeepSeek-V3", - "api": "openai-completions", - "provider": "synthetic", - "baseUrl": "https://api.synthetic.new/v1", - "reasoning": false, - "input": [ - "text" - ], - "cost": { - "input": 0, - "output": 0, - "cacheRead": 0, - "cacheWrite": 0 - }, - "contextWindow": 131072, - "maxTokens": 8192 - }, - "hf:deepseek-ai/DeepSeek-V3-0324": { - "id": "hf:deepseek-ai/DeepSeek-V3-0324", - "name": "deepseek-ai/DeepSeek-V3-0324", - "api": "openai-completions", - "provider": "synthetic", - "baseUrl": "https://api.synthetic.new/v1", - "reasoning": false, - "input": [ - "text" - ], - "cost": { - "input": 0, - "output": 0, - "cacheRead": 0, - "cacheWrite": 0 - }, - "contextWindow": 131072, - "maxTokens": 8192 - }, - "hf:deepseek-ai/DeepSeek-V3.2": { - "id": "hf:deepseek-ai/DeepSeek-V3.2", - "name": "deepseek-ai/DeepSeek-V3.2", - "api": "openai-completions", - "provider": "synthetic", - "baseUrl": "https://api.synthetic.new/v1", - "reasoning": false, - "input": [ - "text" - ], - "cost": { - "input": 0, - "output": 0, - "cacheRead": 0, - "cacheWrite": 0 - }, - "contextWindow": 162816, - "maxTokens": 8192 - }, - "hf:meta-llama/Llama-3.3-70B-Instruct": { - "id": "hf:meta-llama/Llama-3.3-70B-Instruct", - "name": "meta-llama/Llama-3.3-70B-Instruct", - "api": "openai-completions", - "provider": "synthetic", - "baseUrl": "https://api.synthetic.new/v1", - "reasoning": false, - "input": [ - "text" - ], - "cost": { - "input": 0, - "output": 0, - "cacheRead": 0, - "cacheWrite": 0 - }, - "contextWindow": 131072, - "maxTokens": 8192 - }, - "hf:MiniMaxAI/MiniMax-M2.1": { - "id": "hf:MiniMaxAI/MiniMax-M2.1", - "name": "MiniMaxAI/MiniMax-M2.1", - "api": "openai-completions", - "provider": "synthetic", - "baseUrl": "https://api.synthetic.new/v1", - "reasoning": false, - "input": [ - "text" - ], - "cost": { - "input": 0, - "output": 0, - "cacheRead": 0, - "cacheWrite": 0 - }, - "contextWindow": 196608, - "maxTokens": 8192 - }, - "hf:moonshotai/Kimi-K2-Instruct-0905": { - "id": "hf:moonshotai/Kimi-K2-Instruct-0905", - "name": "moonshotai/Kimi-K2-Instruct-0905", - "api": "openai-completions", - "provider": "synthetic", - "baseUrl": "https://api.synthetic.new/v1", - "reasoning": false, - "input": [ - "text" - ], - "cost": { - "input": 0, - "output": 0, - "cacheRead": 0, - "cacheWrite": 0 - }, - "contextWindow": 262144, - "maxTokens": 8192 - }, - "hf:moonshotai/Kimi-K2-Thinking": { - "id": "hf:moonshotai/Kimi-K2-Thinking", - "name": "moonshotai/Kimi-K2-Thinking", - "api": "openai-completions", - "provider": "synthetic", - "baseUrl": "https://api.synthetic.new/v1", - "reasoning": false, - "input": [ - "text" - ], - "cost": { - "input": 0, - "output": 0, - "cacheRead": 0, - "cacheWrite": 0 - }, - "contextWindow": 262144, - "maxTokens": 8192 - }, - "hf:moonshotai/Kimi-K2.5": { - "id": "hf:moonshotai/Kimi-K2.5", - "name": "moonshotai/Kimi-K2.5", - "api": "openai-completions", - "provider": "synthetic", - "baseUrl": "https://api.synthetic.new/v1", - "reasoning": false, - "input": [ - "text" - ], - "cost": { - "input": 0, - "output": 0, - "cacheRead": 0, - "cacheWrite": 0 - }, - "contextWindow": 262144, - "maxTokens": 8192 - }, - "hf:nvidia/Kimi-K2.5-NVFP4": { - "id": "hf:nvidia/Kimi-K2.5-NVFP4", - "name": "nvidia/Kimi-K2.5-NVFP4", - "api": "openai-completions", - "provider": "synthetic", - "baseUrl": "https://api.synthetic.new/v1", - "reasoning": false, - "input": [ - "text" - ], - "cost": { - "input": 0, - "output": 0, - "cacheRead": 0, - "cacheWrite": 0 - }, - "contextWindow": 262144, - "maxTokens": 8192 - }, - "hf:openai/gpt-oss-120b": { - "id": "hf:openai/gpt-oss-120b", - "name": "openai/gpt-oss-120b", - "api": "openai-completions", - "provider": "synthetic", - "baseUrl": "https://api.synthetic.new/v1", - "reasoning": false, - "input": [ - "text" - ], - "cost": { - "input": 0, - "output": 0, - "cacheRead": 0, - "cacheWrite": 0 - }, - "contextWindow": 131072, - "maxTokens": 8192 - }, - "hf:Qwen/Qwen3-235B-A22B-Thinking-2507": { - "id": "hf:Qwen/Qwen3-235B-A22B-Thinking-2507", - "name": "Qwen/Qwen3-235B-A22B-Thinking-2507", - "api": "openai-completions", - "provider": "synthetic", - "baseUrl": "https://api.synthetic.new/v1", - "reasoning": false, - "input": [ - "text" - ], - "cost": { - "input": 0, - "output": 0, - "cacheRead": 0, - "cacheWrite": 0 - }, - "contextWindow": 262144, - "maxTokens": 8192 - }, - "hf:Qwen/Qwen3-Coder-480B-A35B-Instruct": { - "id": "hf:Qwen/Qwen3-Coder-480B-A35B-Instruct", - "name": "Qwen/Qwen3-Coder-480B-A35B-Instruct", - "api": "openai-completions", - "provider": "synthetic", - "baseUrl": "https://api.synthetic.new/v1", - "reasoning": false, - "input": [ - "text" - ], - "cost": { - "input": 0, - "output": 0, - "cacheRead": 0, - "cacheWrite": 0 - }, - "contextWindow": 262144, - "maxTokens": 8192 - }, - "hf:zai-org/GLM-4.7": { - "id": "hf:zai-org/GLM-4.7", - "name": "zai-org/GLM-4.7", - "api": "openai-completions", - "provider": "synthetic", - "baseUrl": "https://api.synthetic.new/v1", - "reasoning": false, - "input": [ - "text" - ], - "cost": { - "input": 0, - "output": 0, - "cacheRead": 0, - "cacheWrite": 0 - }, - "contextWindow": 202752, - "maxTokens": 8192 - } } } \ No newline at end of file diff --git a/packages/ai/src/provider-models/openai-compat.ts b/packages/ai/src/provider-models/openai-compat.ts index b0856d02a..25ced625f 100644 --- a/packages/ai/src/provider-models/openai-compat.ts +++ b/packages/ai/src/provider-models/openai-compat.ts @@ -586,7 +586,57 @@ export function kimiCodeModelManagerOptions( } // --------------------------------------------------------------------------- -// 10. GitHub Copilot +// 10. Synthetic +// --------------------------------------------------------------------------- + +export interface SyntheticModelManagerConfig { + apiKey?: string; + baseUrl?: string; +} + +export function syntheticModelManagerOptions( + config?: SyntheticModelManagerConfig, +): ModelManagerOptions<"openai-completions"> { + const apiKey = config?.apiKey; + const baseUrl = config?.baseUrl ?? "https://api.synthetic.new/openai/v1"; + const references = new Map( + (getBundledModels("synthetic") as Model<"openai-completions">[]).map(model => [model.id, model]), + ); + return { + providerId: "synthetic", + ...(apiKey && { + fetchDynamicModels: () => + fetchOpenAICompatibleModels({ + api: "openai-completions", + provider: "synthetic", + baseUrl, + apiKey, + mapModel: ( + entry: OpenAICompatibleModelRecord, + defaults: Model<"openai-completions">, + _context: OpenAICompatibleModelMapperContext<"openai-completions">, + ): Model<"openai-completions"> => { + const reference = references.get(defaults.id); + const referenceSupportsImage = reference?.input.includes("image") ?? false; + return { + ...(reference ? { ...reference, id: defaults.id, baseUrl } : defaults), + name: toModelName(entry.name, reference?.name ?? defaults.name), + reasoning: entry.supports_reasoning === true || (reference?.reasoning ?? false), + input: entry.supports_vision === true || referenceSupportsImage ? ["text", "image"] : ["text"], + contextWindow: toPositiveNumber( + entry.context_length, + reference?.contextWindow ?? defaults.contextWindow, + ), + maxTokens: toPositiveNumber(entry.max_tokens, reference?.maxTokens ?? 8192), + }; + }, + }), + }), + }; +} + +// --------------------------------------------------------------------------- +// 11. GitHub Copilot // --------------------------------------------------------------------------- export interface GithubCopilotModelManagerConfig { @@ -678,7 +728,7 @@ export function githubCopilotModelManagerOptions(config?: GithubCopilotModelMana } // --------------------------------------------------------------------------- -// 11. Anthropic +// 12. Anthropic // --------------------------------------------------------------------------- export interface AnthropicModelManagerConfig { diff --git a/packages/ai/src/providers/synthetic.ts b/packages/ai/src/providers/synthetic.ts index 08aec3cfa..88e07a024 100644 --- a/packages/ai/src/providers/synthetic.ts +++ b/packages/ai/src/providers/synthetic.ts @@ -2,8 +2,8 @@ * Synthetic provider - wraps OpenAI or Anthropic API based on format setting. * * Synthetic offers both OpenAI-compatible and Anthropic-compatible APIs: - * - OpenAI: https://api.synthetic.new/v1/chat/completions - * - Anthropic: https://api.synthetic.new/v1/messages + * - OpenAI: https://api.synthetic.new/openai/v1/chat/completions + * - Anthropic: https://api.synthetic.new/anthropic/v1/messages * * @see https://dev.synthetic.new/docs/api/overview */ @@ -15,8 +15,8 @@ import { streamOpenAICompletions } from "./openai-completions"; export type SyntheticApiFormat = "openai" | "anthropic"; -const SYNTHETIC_NEW_BASE_URL = "https://api.synthetic.new/v1"; -const SYNTHETIC_NEW_ANTHROPIC_BASE_URL = "https://api.synthetic.new"; +const SYNTHETIC_NEW_BASE_URL = "https://api.synthetic.new/openai/v1"; +const SYNTHETIC_NEW_ANTHROPIC_BASE_URL = "https://api.synthetic.new/anthropic"; // Default thinking budgets for Anthropic format (matches stream.ts) const DEFAULT_THINKING_BUDGETS = { @@ -28,7 +28,7 @@ const DEFAULT_THINKING_BUDGETS = { } as const; export interface SyntheticOptions extends SimpleStreamOptions { - /** API format: "openai" or "anthropic". Default: "anthropic" */ + /** API format: "openai" or "anthropic". Default: "openai" */ format?: SyntheticApiFormat; } @@ -42,7 +42,7 @@ export function streamSynthetic( options?: SyntheticOptions, ): AssistantMessageEventStream { const stream = new AssistantMessageEventStream(); - const format = options?.format ?? "anthropic"; + const format = options?.format ?? "openai"; // Async IIFE to handle stream piping (async () => { diff --git a/packages/ai/src/utils/oauth/cerebras.ts b/packages/ai/src/utils/oauth/cerebras.ts new file mode 100644 index 000000000..2b690d4b1 --- /dev/null +++ b/packages/ai/src/utils/oauth/cerebras.ts @@ -0,0 +1,59 @@ +/** + * Cerebras login flow. + * + * Cerebras provides OpenAI-compatible models via https://api.cerebras.ai/v1. + * + * This is not OAuth - it's a simple API key flow: + * 1. Open browser to Cerebras API key settings + * 2. User copies their API key + * 3. User pastes the API key into the CLI + */ + +import { validateOpenAICompatibleApiKey } from "./api-key-validation"; +import type { OAuthController } from "./types"; + +const AUTH_URL = "https://cloud.cerebras.ai/platform/"; +const API_BASE_URL = "https://api.cerebras.ai/v1"; +const VALIDATION_MODEL = "gpt-oss-120b"; + +/** + * Login to Cerebras. + * + * Opens browser to API keys page, prompts user to paste their API key. + * Returns the API key directly (not OAuthCredentials - this isn't OAuth). + */ +export async function loginCerebras(options: OAuthController): Promise { + if (!options.onPrompt) { + throw new Error("Cerebras login requires onPrompt callback"); + } + + options.onAuth?.({ + url: AUTH_URL, + instructions: "Copy your API key from the Cerebras dashboard", + }); + + const apiKey = await options.onPrompt({ + message: "Paste your Cerebras API key", + placeholder: "csk-...", + }); + + if (options.signal?.aborted) { + throw new Error("Login cancelled"); + } + + const trimmed = apiKey.trim(); + if (!trimmed) { + throw new Error("API key is required"); + } + + options.onProgress?.("Validating API key..."); + await validateOpenAICompatibleApiKey({ + provider: "Cerebras", + apiKey: trimmed, + baseUrl: API_BASE_URL, + model: VALIDATION_MODEL, + signal: options.signal, + }); + + return trimmed; +} diff --git a/packages/ai/src/utils/oauth/index.ts b/packages/ai/src/utils/oauth/index.ts index c3688d16e..9e409a491 100644 --- a/packages/ai/src/utils/oauth/index.ts +++ b/packages/ai/src/utils/oauth/index.ts @@ -26,10 +26,14 @@ import type { * - Google Cloud Code Assist (Gemini CLI) * - Antigravity (Gemini 3, Claude, GPT-OSS via Google Cloud) * - Kimi Code + * - Cerebras + * - Synthetic * - Perplexity (Pro/Max — desktop app extraction or manual cookie) */ // Anthropic export { loginAnthropic, refreshAnthropicToken } from "./anthropic"; +// Cerebras (API key) +export { loginCerebras } from "./cerebras"; // Cursor export { generateCursorAuthParams, @@ -60,6 +64,8 @@ export { loginOpenAICodex, refreshOpenAICodexToken } from "./openai-codex"; export { loginOpenCode } from "./opencode"; // Perplexity export { loginPerplexity } from "./perplexity"; +// Synthetic (API key) +export { loginSynthetic } from "./synthetic"; export * from "./types"; // Z.AI (API key) export { loginZai } from "./zai"; @@ -80,6 +86,11 @@ const builtInOAuthProviders: OAuthProviderInfo[] = [ name: "Kimi Code", available: true, }, + { + id: "cerebras", + name: "Cerebras", + available: true, + }, { id: "github-copilot", name: "GitHub Copilot", @@ -100,6 +111,11 @@ const builtInOAuthProviders: OAuthProviderInfo[] = [ name: "Cursor (Claude, GPT, etc.)", available: true, }, + { + id: "synthetic", + name: "Synthetic", + available: true, + }, { id: "opencode", name: "OpenCode Zen", @@ -197,6 +213,8 @@ export async function refreshOAuthToken( break; case "perplexity": case "opencode": + case "cerebras": + case "synthetic": case "zai": case "minimax-code": case "minimax-code-cn": diff --git a/packages/ai/src/utils/oauth/synthetic.ts b/packages/ai/src/utils/oauth/synthetic.ts new file mode 100644 index 000000000..4bfe506ea --- /dev/null +++ b/packages/ai/src/utils/oauth/synthetic.ts @@ -0,0 +1,60 @@ +/** + * Synthetic login flow. + * + * Synthetic provides OpenAI-compatible and Anthropic-compatible APIs via + * https://api.synthetic.new/openai/v1. + * + * This is not OAuth - it's a simple API key flow: + * 1. Open browser to Synthetic dashboard + * 2. User copies their API key + * 3. User pastes the API key into the CLI + */ + +import { validateOpenAICompatibleApiKey } from "./api-key-validation"; +import type { OAuthController } from "./types"; + +const AUTH_URL = "https://dev.synthetic.new/docs/api/overview"; +const API_BASE_URL = "https://api.synthetic.new/openai/v1"; +const VALIDATION_MODEL = "hf:moonshotai/Kimi-K2.5"; + +/** + * Login to Synthetic. + * + * Opens browser to API keys page, prompts user to paste their API key. + * Returns the API key directly (not OAuthCredentials - this isn't OAuth). + */ +export async function loginSynthetic(options: OAuthController): Promise { + if (!options.onPrompt) { + throw new Error("Synthetic login requires onPrompt callback"); + } + + options.onAuth?.({ + url: AUTH_URL, + instructions: "Copy your API key from the Synthetic dashboard", + }); + + const apiKey = await options.onPrompt({ + message: "Paste your Synthetic API key", + placeholder: "sk-...", + }); + + if (options.signal?.aborted) { + throw new Error("Login cancelled"); + } + + const trimmed = apiKey.trim(); + if (!trimmed) { + throw new Error("API key is required"); + } + + options.onProgress?.("Validating API key..."); + await validateOpenAICompatibleApiKey({ + provider: "Synthetic", + apiKey: trimmed, + baseUrl: API_BASE_URL, + model: VALIDATION_MODEL, + signal: options.signal, + }); + + return trimmed; +} diff --git a/packages/ai/src/utils/oauth/types.ts b/packages/ai/src/utils/oauth/types.ts index 8a9137051..618bec185 100644 --- a/packages/ai/src/utils/oauth/types.ts +++ b/packages/ai/src/utils/oauth/types.ts @@ -9,11 +9,13 @@ export type OAuthCredentials = { }; export type OAuthProvider = | "anthropic" + | "cerebras" | "github-copilot" | "google-gemini-cli" | "google-antigravity" | "kimi-code" | "openai-codex" + | "synthetic" | "opencode" | "zai" | "minimax-code" diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index c931b530a..8e26935f8 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added `/login` support for Cerebras and Synthetic API-key providers + ## [12.10.0] - 2026-02-18 ### Breaking Changes diff --git a/packages/coding-agent/src/config/model-registry.ts b/packages/coding-agent/src/config/model-registry.ts index 14b995e85..4b0dd3b5b 100644 --- a/packages/coding-agent/src/config/model-registry.ts +++ b/packages/coding-agent/src/config/model-registry.ts @@ -28,6 +28,7 @@ import { registerCustomApi, registerOAuthProvider, type SimpleStreamOptions, + syntheticModelManagerOptions, unregisterCustomApis, unregisterOAuthProviders, vercelAiGatewayModelManagerOptions, @@ -639,6 +640,7 @@ export class ModelRegistry { openrouterApiKey, vercelGatewayApiKey, kimiApiKey, + syntheticApiKey, githubCopilotApiKey, googleApiKey, cursorApiKey, @@ -656,6 +658,7 @@ export class ModelRegistry { this.getApiKeyForProvider("openrouter"), this.getApiKeyForProvider("vercel-ai-gateway"), this.getApiKeyForProvider("kimi-code"), + this.getApiKeyForProvider("synthetic"), this.getApiKeyForProvider("github-copilot"), this.getApiKeyForProvider("google"), this.getApiKeyForProvider("cursor"), @@ -745,6 +748,14 @@ export class ModelRegistry { }), ); } + if (isAuthenticated(syntheticApiKey)) { + options.push( + syntheticModelManagerOptions({ + apiKey: syntheticApiKey, + baseUrl: this.getProviderBaseUrl("synthetic"), + }), + ); + } if (isAuthenticated(githubCopilotApiKey)) { options.push( githubCopilotModelManagerOptions({ diff --git a/packages/coding-agent/src/config/model-resolver.ts b/packages/coding-agent/src/config/model-resolver.ts index 8f2fb6365..1d28a23d5 100644 --- a/packages/coding-agent/src/config/model-resolver.ts +++ b/packages/coding-agent/src/config/model-resolver.ts @@ -33,7 +33,7 @@ export const defaultModelPerProvider: Record = { "minimax-code-cn": "MiniMax-M2.5", opencode: "claude-sonnet-4-6", "kimi-code": "kimi-k2.5", - "synthetic": "hf:moonshotai/Kimi-K2.5" + synthetic: "hf:moonshotai/Kimi-K2.5", }; export interface ScopedModel { diff --git a/packages/coding-agent/src/session/auth-storage.ts b/packages/coding-agent/src/session/auth-storage.ts index a4b492b4c..4b5b5f6b3 100644 --- a/packages/coding-agent/src/session/auth-storage.ts +++ b/packages/coding-agent/src/session/auth-storage.ts @@ -13,6 +13,7 @@ import { kimiUsageProvider, loginAnthropic, loginAntigravity, + loginCerebras, loginCursor, loginGeminiCli, loginGitHubCopilot, @@ -22,6 +23,7 @@ import { loginOpenAICodex, loginOpenCode, loginPerplexity, + loginSynthetic, loginZai, type OAuthController, type OAuthCredentials, @@ -696,6 +698,11 @@ export class AuthStorage { await saveApiKeyCredential(apiKey); return; } + case "cerebras": { + const apiKey = await loginCerebras(ctrl); + await saveApiKeyCredential(apiKey); + return; + } case "zai": { const apiKey = await loginZai(ctrl); await saveApiKeyCredential(apiKey); @@ -711,6 +718,11 @@ export class AuthStorage { await saveApiKeyCredential(apiKey); return; } + case "synthetic": { + const apiKey = await loginSynthetic(ctrl); + await saveApiKeyCredential(apiKey); + return; + } default: { const customProvider = getOAuthProvider(provider); if (!customProvider) {