diff --git a/apps/supercode-cli/server/.env.example b/apps/supercode-cli/server/.env.example index 3d7acf3..7eada24 100644 --- a/apps/supercode-cli/server/.env.example +++ b/apps/supercode-cli/server/.env.example @@ -62,6 +62,10 @@ MINIMAX_MODEL="MiniMax-M2" # ConcentraceAI CONCENTRATEAI_API_KEY="" +# Merge Dev Gateway (https://app.merge.dev) +MERGE_DEV_API_KEY="" +MERGE_DEV_MODEL="anthropic/claude-opus-4-8" + # ─── Web Search Tools ────────────────────────── # 🟡 Required for search/scrape tools diff --git a/apps/supercode-cli/server/package.json b/apps/supercode-cli/server/package.json index ecc5b5e..89623d1 100644 --- a/apps/supercode-cli/server/package.json +++ b/apps/supercode-cli/server/package.json @@ -1,6 +1,6 @@ { "name": "supercode-cli", - "version": "0.1.53", + "version": "0.1.54", "description": "AI-powered coding agent CLI", "main": "dist/main.js", "bin": { diff --git a/apps/supercode-cli/server/src/cli/ai/mergedev-service.ts b/apps/supercode-cli/server/src/cli/ai/mergedev-service.ts new file mode 100644 index 0000000..e22b137 --- /dev/null +++ b/apps/supercode-cli/server/src/cli/ai/mergedev-service.ts @@ -0,0 +1,242 @@ +import { createOpenAICompatible } from "@ai-sdk/openai-compatible" +import { streamText, stepCountIs, type ModelMessage, type LanguageModel, type LanguageModelUsage } from "ai" +import chalk from "chalk" +import { mergedevConfig } from "../../config/mergedev.config.ts" +import { recordUsage } from "../../lib/track-usage" +import { computeCost } from "../../lib/pricing" +import { isEmptyToolResult, isDeniedToolResult, summarizeToolResult, tcName } from "./tool-result" + +const HIGH_VALUE_MODELS = ["anthropic/claude-fable-5", "anthropic/claude-opus-4-8", "anthropic/claude-opus-4-7", "openai/gpt-5.5"] + +export class MergeDevService { + model: LanguageModel + readonly modelName: string + + constructor(modelName?: string) { + if (!mergedevConfig.apiKey) { + throw new Error( + "Merge Dev is not configured.\n\n Set MERGE_DEV_API_KEY in your environment:\n" + + " export MERGE_DEV_API_KEY=\n\n" + + " Get a key at: https://app.merge.dev/settings/api-keys", + ) + } + + this.modelName = modelName || mergedevConfig.model + + const client = createOpenAICompatible({ + name: "mergedev", + baseURL: mergedevConfig.baseUrl, + headers: { Authorization: `Bearer ${mergedevConfig.apiKey}` }, + }) + + this.model = client.chatModel(this.modelName) + } + + async sendMessage( + messages: ModelMessage[], + onChunk?: (chunk: string) => void, + tools?: any, + onToolCall?: any, + signal?: AbortSignal, + onReasoning?: (chunk: string) => void, + onToolResult?: (params: { toolName: string; args: unknown; result: string }) => void, + onStepFinish?: (params: { stepNumber: number; toolCalls: Array<{ toolName: string; args: unknown }>; toolResults: Array<{ toolName: string; args: unknown; result: string }> }) => void, + ) { + const streamAbortController = new AbortController() + const streamTimeout = setTimeout(() => streamAbortController.abort(), 120_000) + const signalHandler = signal ? () => streamAbortController.abort() : undefined + signalHandler && signal!.addEventListener("abort", signalHandler, { once: true }) + + try { + const systemMessages = messages.filter(m => m.role === "system") + const nonSystemMessages = messages.filter(m => m.role !== "system") + const system = systemMessages.map(m => m.content).join("\n") + + const hasTools = tools && Object.keys(tools).length > 0 + + if (!hasTools) { + const result = streamText({ + model: this.model, + messages: nonSystemMessages, + system, + abortSignal: streamAbortController.signal, + ...(!HIGH_VALUE_MODELS.includes(this.modelName) ? { maxOutputTokens: 8192 } : {}), + }) + + let fullResponse = "" + for await (const chunk of result.textStream) { + fullResponse += chunk + onChunk?.(chunk) + } + + const [finishReason, usage] = await Promise.all([ + result.finishReason, + result.usage, + ]) + + recordUsage({ + provider: "mergedev", + model: this.modelName, + inputTokens: usage.inputTokens ?? 0, + outputTokens: usage.outputTokens ?? 0, + cachedInputTokens: usage.inputTokenDetails?.cacheReadTokens ?? 0, + totalTokens: usage.totalTokens ?? 0, + costUsd: computeCost(this.modelName, usage.inputTokens ?? 0, usage.outputTokens ?? 0, usage.inputTokenDetails?.cacheReadTokens ?? 0), + durationMs: null, + }) + + return { + content: fullResponse, + finishReason, + usage, + } + } + + let fullResponse = "" + const seenStepResults: Array<{ toolName: string; result: string }> = [] + const deniedCounts = new Map() + let stopForDenialLoop = false + + const result = streamText({ + model: this.model, + messages: nonSystemMessages, + system, + tools, + ...(!HIGH_VALUE_MODELS.includes(this.modelName) ? { maxOutputTokens: 8192 } : {} as Record), + stopWhen: stepCountIs(8), + abortSignal: streamAbortController.signal, + prepareStep: async ({ messages }) => { + if (stopForDenialLoop) { + return { + messages: [ + ...messages, + { + role: "system" as const, + content: + "SYSTEM NOTICE: You have called the same permission-protected tool multiple " + + "times after the user denied it. Stop calling it. Respond to the user with " + + "what you have so far and ask for guidance.", + }, + ], + } + } + if (seenStepResults.length === 0) return undefined + const allEmpty = seenStepResults.every((r) => isEmptyToolResult(r.result)) + if (!allEmpty) return undefined + const summary = seenStepResults + .map((r) => `- ${r.toolName}: ${summarizeToolResult(r.result)}`) + .join("\n") + return { + messages: [ + ...messages, + { + role: "system" as const, + content: + "SYSTEM NOTICE: All tool calls so far have returned empty or error results. " + + "You have NO source material to answer with. Do NOT invent specifications, pricing, " + + "dates, leaderboard rankings, or any factual claims. Tell the user which tools failed " + + "and what you would need to proceed.\n\nTool outcomes:\n" + summary, + }, + ], + } + }, + onStepFinish: async (event) => { + if (event.toolCalls?.length) { + for (const tc of event.toolCalls) { + onToolCall?.({ toolName: tc.toolName, args: (tc as any).input as Record }) + } + } + const inputByCallId = new Map() + if (event.toolCalls?.length) { + for (const tc of event.toolCalls) { + const id = (tc as any).toolCallId + if (typeof id === "string") { + inputByCallId.set(id, (tc as any).input) + } + } + } + const toolResults = (event as any).toolResults as + | Array<{ toolName?: string; toolCallId?: string; input?: unknown; output?: unknown }> + | undefined + if (toolResults?.length) { + for (const tr of toolResults) { + const name = tcName(tr.toolName) ?? "unknown" + const out = (tr as any).output + const text = + typeof out === "string" + ? out + : out === undefined || out === null + ? "" + : JSON.stringify(out) + seenStepResults.push({ toolName: name, result: text }) + const args = tr.input ?? (tr.toolCallId ? inputByCallId.get(tr.toolCallId) : undefined) + if (onToolResult) { + onToolResult({ toolName: name, args, result: text }) + } + if (isDeniedToolResult(text)) { + const prev = deniedCounts.get(name) ?? 0 + const next = prev + 1 + deniedCounts.set(name, next) + if (next >= 2) stopForDenialLoop = true + } else { + deniedCounts.set(name, 0) + } + } + } + }, + }) + + for await (const chunk of result.textStream) { + fullResponse += chunk + onChunk?.(chunk) + } + + const [finishReason, usage] = await Promise.all([ + result.finishReason, + result.usage, + ]) + + recordUsage({ + provider: "mergedev", + model: this.modelName, + inputTokens: usage.inputTokens ?? 0, + outputTokens: usage.outputTokens ?? 0, + cachedInputTokens: usage.inputTokenDetails?.cacheReadTokens ?? 0, + totalTokens: usage.totalTokens ?? 0, + costUsd: computeCost(this.modelName, usage.inputTokens ?? 0, usage.outputTokens ?? 0, usage.inputTokenDetails?.cacheReadTokens ?? 0), + durationMs: null, + }) + + return { + content: fullResponse, + finishReason, + usage, + } + } catch (error: any) { + if (error?.name === "AbortError") throw error + const msg = error instanceof Error ? error.message : String(error) + const is5xx = /MergeDev (?:API )?5\d\d/.test(msg) || /status code 5\d\d/i.test(msg) + if (is5xx) { + const friendly = new Error( + `Merge Dev Gateway error (HTTP 5xx). This is upstream — not your request. ` + + `Try again, or run /model to switch providers.\n ${msg}`, + ) + console.error(chalk.red("MergeDev Service Error:"), friendly.message) + throw friendly + } + console.error(chalk.red("MergeDev Service Error:"), msg) + throw error + } finally { + clearTimeout(streamTimeout) + if (signalHandler) signal!.removeEventListener("abort", signalHandler as any) + } + } + + async getMessage(messages: ModelMessage[], tools?: any) { + let fullResponse = "" + const result = await this.sendMessage(messages, (chunk) => { + fullResponse += chunk + }) + return result.content + } +} diff --git a/apps/supercode-cli/server/src/cli/ai/provider.ts b/apps/supercode-cli/server/src/cli/ai/provider.ts index 4d1f1f1..4be5593 100644 --- a/apps/supercode-cli/server/src/cli/ai/provider.ts +++ b/apps/supercode-cli/server/src/cli/ai/provider.ts @@ -4,13 +4,15 @@ import { MinimaxService } from "./minimax-service.ts" import { OpenRouterService } from "./openrouter-service.ts" import { NvidiaService } from "./nvidia-service.ts" import { ConcentrateService } from "./concentrate-service.ts" +import { MergeDevService } from "./mergedev-service.ts" import { ServerProxyService } from "./server-proxy-service.ts" import { config } from "../../config/google.config.ts" import { minimaxConfig } from "../../config/minimax.config.ts" import { openRouterConfig } from "../../config/openrouter.config.ts" import { nvidiaConfig } from "../../config/nvidia.config.ts" +import { mergedevConfig } from "../../config/mergedev.config.ts" -export type ModelProvider = "google" | "minimax" | "openrouter" | "nvidia" | "concentrateai" +export type ModelProvider = "google" | "minimax" | "openrouter" | "nvidia" | "concentrateai" | "mergedev" export interface AIProvider { readonly name: string @@ -39,6 +41,7 @@ const providerMeta: Record string> = { @@ -47,6 +50,7 @@ const providerConfigs: Record string> = { openrouter: () => openRouterConfig.apiKey, nvidia: () => nvidiaConfig.apiKey, concentrateai: () => process.env.CONCENTRATEAI_API_KEY || "", + mergedev: () => mergedevConfig.apiKey, } export function createProvider(provider: ModelProvider, model?: string): AIProvider { @@ -103,6 +107,16 @@ export function createProvider(provider: ModelProvider, model?: string): AIProvi svc.sendMessage(messages, onChunk, tools, onToolCall, signal, onReasoning, onToolResult), } } + case "mergedev": { + const svc = new MergeDevService(model) + return { + name: "mergedev", + modelName: svc.modelName, + model: svc.model, + sendMessage: (messages, onChunk, tools, onToolCall, signal, onReasoning, onToolResult) => + svc.sendMessage(messages, onChunk, tools, onToolCall, signal, onReasoning, onToolResult), + } + } default: { throw new Error(`Provider "${provider}" is paused or unavailable`) } diff --git a/apps/supercode-cli/server/src/cli/commands/slashCommands/help.ts b/apps/supercode-cli/server/src/cli/commands/slashCommands/help.ts index 0f8bbcb..28ec416 100644 --- a/apps/supercode-cli/server/src/cli/commands/slashCommands/help.ts +++ b/apps/supercode-cli/server/src/cli/commands/slashCommands/help.ts @@ -13,6 +13,7 @@ const PROVIDERS = [ { name: "OpenRouter", models: ["openai/gpt-oss-120b:free", "deepseek/deepseek-v4-flash", "minimax/minimax-m3", "z-ai/glm-5.1", "moonshotai/kimi-k2.6"], note: "bring your own key" }, { name: "NVIDIA NIM", models: ["minimaxai/minimax-m3", "deepseek-ai/deepseek-v4-flash", "meta/llama-3.3-70b-instruct"], note: "free API" }, { name: "ConcentrateAI", models: ["deepseek-v4-flash", "kimi-k2-6", "glm-5.2", "glm-5.1", "minimax-m3", "openai/gpt-5.5"], note: "AI gateway" }, + { name: "Merge Dev Gateway", models: ["anthropic/claude-opus-4-8", "anthropic/claude-opus-4-7", "anthropic/claude-fable-5", "openai/gpt-5.5", "deepseek/deepseek-v4-flash"], note: "bring your own key" }, ] function renderCommands(): string { diff --git a/apps/supercode-cli/server/src/cli/commands/slashCommands/model.ts b/apps/supercode-cli/server/src/cli/commands/slashCommands/model.ts index c191d98..fc9a365 100644 --- a/apps/supercode-cli/server/src/cli/commands/slashCommands/model.ts +++ b/apps/supercode-cli/server/src/cli/commands/slashCommands/model.ts @@ -13,10 +13,10 @@ interface ModelEntry { } const MODELS: ModelEntry[] = [ - { value: "anthropic/claude-fable-5", label: "Fable 5", provider: "concentrateai", cost: "80x", desc: "Anthropic frontier" }, - { value: "anthropic/claude-opus-4-8", label: "Opus 4.8", provider: "concentrateai", cost: "40x", desc: "Anthropic reasoning" }, - { value: "anthropic/claude-opus-4-7", label: "Opus 4.7", provider: "concentrateai", cost: "40x", desc: "Anthropic GA" }, - { value: "openai/gpt-5.5", label: "GPT 5.5", provider: "concentrateai", cost: "50x", desc: "OpenAI frontier" }, + // { value: "anthropic/claude-fable-5", label: "Fable 5", provider: "concentrateai", cost: "80x", desc: "Anthropic frontier" }, + // { value: "anthropic/claude-opus-4-8", label: "Opus 4.8", provider: "concentrateai", cost: "40x", desc: "Anthropic reasoning" }, + // { value: "anthropic/claude-opus-4-7", label: "Opus 4.7", provider: "concentrateai", cost: "40x", desc: "Anthropic GA" }, + // { value: "openai/gpt-5.5", label: "GPT 5.5", provider: "concentrateai", cost: "50x", desc: "OpenAI frontier" }, { value: "glm-5.2", label: "GLM 5.2", provider: "concentrateai", cost: "0.5x", desc: "Latest GLM" }, // { value: "glm-5.1", label: "GLM 5.1", provider: "concentrateai", cost: "0.4x", desc: "Balanced multilingual" }, { value: "kimi-k2-6", label: "Kimi K2.6", provider: "concentrateai", cost: "0.8x", desc: "Long context" }, @@ -32,6 +32,11 @@ const MODELS: ModelEntry[] = [ { value: "minimax/minimax-m3", label: "MiniMax M3", provider: "openrouter", cost: "3.0x", desc: "Via OpenRouter" }, { value: "z-ai/glm-5.1", label: "GLM 5.1", provider: "openrouter", cost: "1.0x", desc: "Via OpenRouter" }, { value: "moonshotai/kimi-k2.6", label: "Kimi K2.6", provider: "openrouter", cost: "1.5x", desc: "Via OpenRouter" }, + { value: "anthropic/claude-opus-4-8", label: "Opus 4.8", provider: "mergedev", cost: "40x", desc: "Via Merge Dev" }, + { value: "anthropic/claude-opus-4-7", label: "Opus 4.7", provider: "mergedev", cost: "40x", desc: "Via Merge Dev" }, + { value: "anthropic/claude-fable-5", label: "Fable 5", provider: "mergedev", cost: "80x", desc: "Via Merge Dev" }, + { value: "openai/gpt-5.5", label: "GPT 5.5", provider: "mergedev", cost: "50x", desc: "Via Merge Dev" }, + { value: "deepseek/deepseek-v4-flash", label: "DeepSeek V4 Flash", provider: "mergedev", cost: "1.0x", desc: "Via Merge Dev" }, ] function renderModelBrowser(currentProvider: string, currentModel: string): void { @@ -53,6 +58,7 @@ function renderModelBrowser(currentProvider: string, currentModel: string): void ? "ConcentrateAI (default)" : providerKey === "google" ? "Google Gemini" : providerKey === "openrouter" ? "OpenRouter" + : providerKey === "mergedev" ? "Merge Dev Gateway" : "NVIDIA NIM" console.log(` ${sectionHeader(label, { accent: isDefault ? "amber" : "green", width: w - 4 })}`) for (const m of models) { @@ -122,6 +128,7 @@ export async function pickModel(): Promise<{ provider: ModelProvider; model?: st export function formatModelChange(p: ModelProvider, m?: string): string { const label = p === "concentrateai" ? "ConcentrateAI" : p === "google" ? "Gemini" : - p === "nvidia" ? "NVIDIA" : p === "minimax" ? "MiniMax" : "OpenRouter" + p === "nvidia" ? "NVIDIA" : p === "minimax" ? "MiniMax" : + p === "mergedev" ? "Merge Dev" : "OpenRouter" return `${label}${m ? ` · ${m}` : ""}` } diff --git a/apps/supercode-cli/server/src/config/mergedev.config.ts b/apps/supercode-cli/server/src/config/mergedev.config.ts new file mode 100644 index 0000000..059242a --- /dev/null +++ b/apps/supercode-cli/server/src/config/mergedev.config.ts @@ -0,0 +1,6 @@ +export const mergedevConfig = { + apiKey: process.env.MERGE_DEV_API_KEY || "", + model: process.env.MERGE_DEV_MODEL || "anthropic/claude-opus-4-8", + baseUrl: "https://api-gateway.merge.dev/v1/openai", + maxTokens: Number(process.env.MERGE_DEV_MAX_TOKENS) || 128000, +} diff --git a/apps/supercode-cli/server/src/index.ts b/apps/supercode-cli/server/src/index.ts index 17675fd..ade2248 100644 --- a/apps/supercode-cli/server/src/index.ts +++ b/apps/supercode-cli/server/src/index.ts @@ -40,6 +40,7 @@ const MODEL_MAX_TOKENS: Record = { "minimax-m3": 8192, "anthropic/claude-fable-5": 128000, "anthropic/claude-opus-4-7": 128000, + "anthropic/claude-opus-4-8": 128000, "openai/gpt-5.5": 128000, } function getModelMaxTokens(model: string): number { @@ -511,6 +512,113 @@ app.post("/api/ai/chat", async (req, res) => { res.end() break } + case "mergedev": { + const apiKey = process.env.MERGE_DEV_API_KEY + if (!apiKey) { res.status(500).json({ error: "Merge Dev not configured on server" }); return } + const modelName = modelParam || "anthropic/claude-opus-4-8" + const mdStart = Date.now() + const bodyObj: any = { + model: modelName, + messages: nonSystemMessages.map((m: any) => { + const msg: any = { + role: m.role, + content: m.content !== null && m.content !== undefined ? String(m.content) : "", + } + if (m.tool_calls) msg.tool_calls = m.tool_calls + if (m.tool_call_id) msg.tool_call_id = m.tool_call_id + return msg + }), + max_tokens: getModelMaxTokens(modelName), + temperature: 0.7, + stream: true, + } + if (system && nonSystemMessages.length > 0) { + bodyObj.messages = [{ role: "system", content: system }, ...bodyObj.messages] + } + if (tools) { + bodyObj.tools = Object.entries(tools).map(([name, fn]: [string, any]) => ({ + type: "function", + function: { name, description: fn.description || "", parameters: toolParams(fn) }, + })) + } + const response = await fetch("https://api-gateway.merge.dev/v1/openai/chat/completions", { + method: "POST", + headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, + body: JSON.stringify(bodyObj), + }) + if (!response.ok) { + const errText = await response.text().catch(() => "unknown error") + res.status(response.status).json({ error: `Merge Dev API ${response.status}: ${errText}` }) + return + } + const reader = response.body?.getReader() + if (!reader) { res.status(500).json({ error: "No response body" }); return } + const decoder = new TextDecoder() + let buffer = "" + let inputTokens = 0 + let outputTokens = 0 + let pendingToolCalls: Record = {} + while (true) { + const { done, value } = await reader.read() + if (done) break + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split("\n") + buffer = lines.pop() || "" + for (const line of lines) { + const trimmed = line.trim() + if (!trimmed || !trimmed.startsWith("data: ")) continue + const jsonStr = trimmed.slice(6) + if (jsonStr === "[DONE]") break + try { + const data = JSON.parse(jsonStr) + const delta = data.choices?.[0]?.delta + if (delta?.content) { + res.write(JSON.stringify({ type: "text", content: delta.content }) + "\n") + } + if (delta?.tool_calls) { + for (const tc of delta.tool_calls) { + const index = tc.index ?? 0 + if (!pendingToolCalls[index]) { + pendingToolCalls[index] = { id: "", name: "", args: "" } + } + if (tc.id) pendingToolCalls[index].id = tc.id + if (tc.function?.name) pendingToolCalls[index].name = tc.function.name + if (tc.function?.arguments) pendingToolCalls[index].args += tc.function.arguments + } + } + const finishReason = data.choices?.[0]?.finish_reason + if (finishReason === "tool_calls") { + for (const [, call] of Object.entries(pendingToolCalls)) { + if (call.name && call.args) { + try { + const parsed = JSON.parse(call.args) + res.write(JSON.stringify({ type: "tool-call", toolName: call.name, args: parsed, toolCallId: call.id }) + "\n") + } catch { /* skip malformed args */ } + } + } + pendingToolCalls = {} + } + if (data.usage) { + inputTokens = data.usage.prompt_tokens ?? 0 + outputTokens = data.usage.completion_tokens ?? 0 + } + } catch { /* skip malformed */ } + } + } + recordUsage({ + provider: "mergedev", model: modelName, + inputTokens, outputTokens, cachedInputTokens: 0, + totalTokens: inputTokens + outputTokens, + costUsd: computeCost(modelName, inputTokens, outputTokens, 0), + durationMs: Date.now() - mdStart, + }) + res.write(JSON.stringify({ + type: "finish", reason: "stop", + usage: { inputTokens, outputTokenDetails: { textTokens: outputTokens, reasoningTokens: 0 }, outputTokens, inputTokenDetails: { noCacheTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }, totalTokens: inputTokens + outputTokens } + }) + "\n") + res.end() + break + } case "concentrateai": { const { concentrateAiKey: forwardedKey } = req.body const apiKey = forwardedKey || process.env.CONCENTRATEAI_API_KEY @@ -737,6 +845,30 @@ app.post("/api/ai/generate-object", async (req, res) => { res.json({ object: result.object }) break } + case "mergedev": { + const apiKey = process.env.MERGE_DEV_API_KEY + if (!apiKey) { res.status(500).json({ error: "Merge Dev not configured on server" }); return } + const modelName = modelParam || "anthropic/claude-opus-4-8" + const response = await fetch("https://api-gateway.merge.dev/v1/openai/chat/completions", { + method: "POST", + headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, + body: JSON.stringify({ + model: modelName, + messages: [{ role: "user", content: prompt }], + max_tokens: getModelMaxTokens(modelName), + temperature: 0.7, + stream: false, + }), + }) + if (!response.ok) { + const errText = await response.text().catch(() => "unknown error") + res.status(response.status).json({ error: `Merge Dev API ${response.status}: ${errText}` }) + return + } + const data: any = await response.json() + res.json({ object: { content: data.choices?.[0]?.message?.content || "" } }) + break + } case "concentrateai": { const { concentrateAiKey: forwardedKey } = req.body const apiKey = forwardedKey || process.env.CONCENTRATEAI_API_KEY diff --git a/apps/supercode-cli/server/src/lib/cli-config.ts b/apps/supercode-cli/server/src/lib/cli-config.ts index 929598b..355957b 100644 --- a/apps/supercode-cli/server/src/lib/cli-config.ts +++ b/apps/supercode-cli/server/src/lib/cli-config.ts @@ -28,6 +28,7 @@ const API_KEY_ENV_MAP: Record = { openrouter: "OPENROUTER_API_KEY", nvidia: "NVIDIA_API_KEY", concentrateai: "CONCENTRATEAI_API_KEY", + mergedev: "MERGE_DEV_API_KEY", } export function getProviderApiKeys(): Partial> { diff --git a/apps/supercode-cli/server/src/lib/pricing.ts b/apps/supercode-cli/server/src/lib/pricing.ts index 3f29050..c258b4b 100644 --- a/apps/supercode-cli/server/src/lib/pricing.ts +++ b/apps/supercode-cli/server/src/lib/pricing.ts @@ -57,6 +57,7 @@ export function getProviderDisplayNameFromRaw(raw: string): string { google: "Google", nvidia: "NVIDIA", minimax: "MiniMax", + mergedev: "Merge Dev", } return map[raw] ?? raw } @@ -67,6 +68,7 @@ export const PROVIDER_COLORS: Record = { google: "#3b82f6", nvidia: "#75e02e", minimax: "#06b6d4", + mergedev: "#f59e0b", other: "#6b7280", }