diff --git a/apps/supercode-cli/server/package.json b/apps/supercode-cli/server/package.json index 716a419..b18c183 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.94", + "version": "0.1.95", "description": "AI-powered coding agent CLI", "main": "dist/main.js", "bin": { diff --git a/apps/supercode-cli/server/prisma/seed.ts b/apps/supercode-cli/server/prisma/seed.ts index e5f7c0b..0b1e66f 100644 --- a/apps/supercode-cli/server/prisma/seed.ts +++ b/apps/supercode-cli/server/prisma/seed.ts @@ -162,10 +162,14 @@ const PLANS = [ ] const MODELS = [ - // ── Spark / Spark Premium (minTier: "spark") ── + // ── Spark / Spark Premium (minTier: "spark") — all open / cloud free models ── + // Spark Premium uses the same open catalog (higher limits/credits); gating is by tier order. { slug: "deepseek-v4-flash", displayName: "DeepSeek V4 Flash", provider: "deepseek", minTier: "spark", inputPrice: 0.15, outputPrice: 0.60, cachedPrice: 0 }, { slug: "deepseek/deepseek-v4-flash", displayName: "DeepSeek V4 Flash (OR)", provider: "openrouter", minTier: "spark", inputPrice: 0.15, outputPrice: 0.60, cachedPrice: 0 }, + { slug: "hy3", displayName: "Hunyuan Hy3", provider: "supercode", minTier: "spark", inputPrice: 0.15, outputPrice: 0.60, cachedPrice: 0 }, { slug: "MiniMax-M3", displayName: "MiniMax M3", provider: "minimax", minTier: "spark", inputPrice: 0.20, outputPrice: 0.80, cachedPrice: 0.04 }, + // CLI cloud picker uses this slug; keep both so plan-gate matches either form + { slug: "minimax-m3", displayName: "MiniMax M3 (cloud)", provider: "supercode", minTier: "spark", inputPrice: 0.20, outputPrice: 0.80, cachedPrice: 0.04 }, { slug: "mimo-v2.5", displayName: "MiMo v2.5", provider: "orcarouter", minTier: "spark", inputPrice: 0.15, outputPrice: 0.60, cachedPrice: 0 }, { slug: "kimi-k2-6", displayName: "Kimi K2.6", provider: "openrouter", minTier: "spark", inputPrice: 0.15, outputPrice: 0.60, cachedPrice: 0 }, { slug: "kimi-k2-7-code", displayName: "Kimi K2.7 Code", provider: "openrouter", minTier: "spark", inputPrice: 0.25, outputPrice: 1.00, cachedPrice: 0 }, diff --git a/apps/supercode-cli/server/src/cli/ai/chat/chat.ts b/apps/supercode-cli/server/src/cli/ai/chat/chat.ts index c0097f1..f94b37e 100644 --- a/apps/supercode-cli/server/src/cli/ai/chat/chat.ts +++ b/apps/supercode-cli/server/src/cli/ai/chat/chat.ts @@ -129,6 +129,26 @@ function isYashDewasthale(): boolean { ) } +// Get user's plan tier for display +async function getUserPlanTier(): Promise { + if (!currentUser) return "" + try { + const prisma = (await import("src/lib/prisma")).default + const subscription = await prisma.subscription.findFirst({ + where: { + userId: currentUser.id, + status: { in: ["active", "trialing"] }, + }, + include: { plan: true }, + orderBy: { createdAt: "desc" }, + }) + if (!subscription?.plan) return "" + return subscription.plan.tier + } catch { + return "" + } +} + export async function initConversation(userId: string, conversationId: string | null = null, mode = "chat") { const thinking = createThinking("loading conversation") const conversation = await getOrCreateConversation(conversationId, mode) @@ -1956,6 +1976,9 @@ export async function chatLoop( footer.setModel(provider.modelName) footer.setContextWindow(contextWindow) footer.setTokens(0) + // Set plan tier in footer + const planTier = await getUserPlanTier() + if (planTier) footer.setPlan(planTier) footer.mount() // Re-mount on terminal resize so the status row tracks the new bottom row. 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 645270f..1898e2d 100644 --- a/apps/supercode-cli/server/src/cli/commands/slashCommands/model.ts +++ b/apps/supercode-cli/server/src/cli/commands/slashCommands/model.ts @@ -4,6 +4,9 @@ import chalk from "chalk" import { theme, heavyDivider, CARET } from "src/cli/utils/tui.ts" import { type ModelProvider, providerMeta } from "src/cli/ai/provider.ts" import { getCliConfig, saveCliConfig, saveProviderApiKey, getByokSessionKey } from "src/lib/cli-config.ts" +import { getStoredToken } from "src/lib/token" +import { getCurrentUser } from "src/lib/api-client" +import prisma from "src/lib/prisma" interface ModelEntry { value: string @@ -13,6 +16,33 @@ interface ModelEntry { desc: string } +// ── Tier detection ────────────────────────────────────────────── +export type UserTier = "spark" | "spark-premium" | "pro" | "ultra" | "none" + +async function getUserTier(): Promise { + try { + const token = await getStoredToken() + if (!token?.access_token) return "none" + + const result = await getCurrentUser() + if (!result.ok) return "none" + + const subscription = await prisma.subscription.findFirst({ + where: { + userId: result.user.id, + status: { in: ["active", "trialing"] }, + }, + include: { plan: true }, + orderBy: { createdAt: "desc" }, + }) + + if (!subscription?.plan) return "none" + return subscription.plan.tier as UserTier + } catch { + return "none" + } +} + const SECTION_CLOUD = "__section_cloud__" const SECTION_BYOK = "__section_byok__" const SECTION_CONCENTRATEAI = "__section_concentrateai__" @@ -22,7 +52,6 @@ const SECTION_MINIMAX = "__section_minimax__" const SECTION_NVIDIA = "__section_nvidia__" const SECTION_OPENROUTER = "__section_openrouter__" const SECTION_ORCAROUTER = "__section_orcarouter__" -const SECTION_CLOUD_PREMIUM = "__section_cloud_premium__" export const ALL_SECTIONS = new Set([ SECTION_CLOUD, SECTION_BYOK, @@ -30,7 +59,6 @@ export const ALL_SECTIONS = new Set([ SECTION_GOOGLE, SECTION_MINIMAX, SECTION_NVIDIA, SECTION_OPENROUTER, SECTION_ORCAROUTER, - SECTION_CLOUD_PREMIUM, ]) const SECTION_LABELS: Record = { @@ -43,21 +71,24 @@ const SECTION_LABELS: Record = { [SECTION_NVIDIA]: "NVIDIA NIM", [SECTION_OPENROUTER]: "OpenRouter", [SECTION_ORCAROUTER]: "OrcaRouter", - [SECTION_CLOUD_PREMIUM]: "Supercode Cloud Premium", } -const isMainSection = (v: string) => v === SECTION_CLOUD || v === SECTION_BYOK || v === SECTION_CLOUD_PREMIUM +const isMainSection = (v: string) => v === SECTION_CLOUD || v === SECTION_BYOK // Models available through the Supercode cloud proxy (no API key needed) export const CLOUD_MODELS: ModelEntry[] = [ - { value: "deepseek-v4-flash", label: "DeepSeek V4 Flash", provider: "supercode", cost: "free", desc: "Fast & capable" }, - // { value: "glm-5.2", label: "GLM 5.2", provider: "supercode", cost: "free", desc: "Latest GLM" }, - // { value: "glm-5.1", label: "GLM 5.1", provider: "supercode", cost: "free", desc: "Stable & reliable" }, - // { value: "kimi-k2-6", label: "Kimi K2.6", provider: "supercode", cost: "free", desc: "Long context" }, - // { value: "minimax-m3", label: "MiniMax M3", provider: "supercode", cost: "free", desc: "Fast & smart" }, - { value: "hy3", label: "Hunyuan Hy3", provider: "supercode", cost: "free", desc: "Tencent flagship" }, - { value: "mimo-v2.5", label: "Mimo v2.5", provider: "supercode", cost: "free", desc: "Novita" }, - // { value: "fireworks/nemotron-3-ultra-nvfp4", label: "Nemotron 3 Ultra NVFP4", provider: "supercode", cost: "free", desc: "Fireworks" }, + { value: "deepseek-v4-flash", label: "DeepSeek V4 Flash", provider: "supercode", cost: "", desc: "Fast & capable" }, + { value: "kimi-k2-6", label: "Kimi K2.6", provider: "supercode", cost: "", desc: "Long context" }, + { value: "kimi-k2-7-code", label: "Kimi K2.7 Code", provider: "supercode", cost: "", desc: "Code specialist" }, + { value: "kimi-k3", label: "Kimi K3", provider: "supercode", cost: "", desc: "Moonshot latest" }, + { value: "minimax-m3", label: "MiniMax M3", provider: "supercode", cost: "", desc: "Fast & smart" }, + { value: "glm-5.2", label: "GLM 5.2", provider: "supercode", cost: "", desc: "Latest GLM" }, + { value: "glm-5.1", label: "GLM 5.1", provider: "supercode", cost: "", desc: "Stable & reliable" }, + { value: "mimo-v2.5", label: "Mimo v2.5", provider: "supercode", cost: "", desc: "Novita" }, + { value: "hy3", label: "Hunyuan Hy3", provider: "supercode", cost: "", desc: "Tencent flagship" }, + { value: "gemini-2.5-flash", label: "Gemini 2.5 Flash", provider: "supercode", cost: "", desc: "Google smart & fast" }, + { value: "meta/llama-3.3-70b-instruct", label: "Llama 3.3 70B", provider: "supercode", cost: "", desc: "Open weights" }, + { value: "orcarouter/auto", label: "OrcaRouter Auto", provider: "supercode", cost: "", desc: "Auto-pick cheapest" }, ] // Models available when you bring your own API key (BYOK) @@ -154,7 +185,7 @@ export const BYOK_MODELS: ModelEntry[] = [ { value: "openai/gpt-4.1-nano", label: "GPT-4.1 Nano", provider: "openrouter", cost: "0.3x", desc: "Tiny & fast" }, { value: "openai/o3-mini", label: "o3-mini", provider: "openrouter", cost: "3x", desc: "Reasoning mini" }, { value: "openai/o4-mini", label: "o4-mini", provider: "openrouter", cost: "3x", desc: "Reasoning v4 mini" }, - { value: "openai/gpt-oss-120b:free", label: "GPT OSS 120B", provider: "openrouter", cost: "free", desc: "Open-weight free" }, + { value: "openai/gpt-oss-120b:free", label: "GPT OSS 120B", provider: "openrouter", cost: "", desc: "Open-weight" }, { value: "x-ai/grok-3", label: "Grok 3", provider: "openrouter", cost: "10x", desc: "xAI flagship" }, { value: "x-ai/grok-3-mini", label: "Grok 3 Mini", provider: "openrouter", cost: "5x", desc: "Compact Grok" }, { value: "x-ai/grok-3-mini-fast", label: "Grok 3 Mini Fast", provider: "openrouter", cost: "5x", desc: "Fast Grok" }, @@ -201,28 +232,56 @@ export const BYOK_MODELS: ModelEntry[] = [ { value: "kimi/kimi-k3", label: "Kimi K3", provider: "orcarouter", cost: "", desc: "Moonshot latest" }, { value: "kimi/kimi-k2.6", label: "Kimi K2.6", provider: "orcarouter", cost: "", desc: "Long context" }, { value: "minimax/minimax-m3", label: "MiniMax M3", provider: "orcarouter", cost: "", desc: "Fast & smart" }, - { value: "orcarouter/auto", label: "OrcaRouter Auto", provider: "orcarouter", cost: "0x", desc: "Auto-pick cheapest" }, + { value: "orcarouter/auto", label: "OrcaRouter Auto", provider: "orcarouter", cost: "", desc: "Auto-pick cheapest" }, ] -// Set of premium cloud models that require Supercode Cloud Premium +// Set of premium cloud models that require Pro or higher tier (frontier models) export const PREMIUM_CLOUD_MODELS = new Set([ - "glm-5.2", - "kimi-k2-7-code", - "kimi-k3", - "minimax-m3", + "anthropic/claude-sonnet-4.6", + "anthropic/claude-opus-4.7", + "anthropic/claude-opus-4-8", + "openai/gpt-5.5", + "grok/grok-4-fast-reasoning", + "gemini-2.5-pro", + "deepseek/deepseek-reasoner", ]) -export const MODELS: ModelEntry[] = [ - { value: SECTION_CLOUD, label: "Supercode Cloud", provider: "supercode", cost: "", desc: "" }, - ...CLOUD_MODELS, - { value: SECTION_CLOUD_PREMIUM, label: "Supercode Cloud Premium", provider: "supercode", cost: "", desc: "" }, - { value: "glm-5.2", label: "GLM 5.2", provider: "supercode", cost: "", desc: "Latest GLM" }, - { value: "kimi-k2-7-code", label: "Kimi K2.7 Code", provider: "supercode", cost: "", desc: "Code specialist" }, - { value: "kimi-k3", label: "Kimi K3", provider: "supercode", cost: "", desc: "Moonshot latest" }, - { value: "minimax-m3", label: "MiniMax M3", provider: "supercode", cost: "", desc: "Fast & smart" }, - { value: SECTION_BYOK, label: "Bring Your Own Key", provider: "supercode", cost: "", desc: "" }, - ...BYOK_MODELS, -] +// Tier-organized model lists +export const TIER_MODELS: Record = { + "spark": [ + { value: SECTION_CLOUD, label: "Supercode Cloud", provider: "supercode", cost: "", desc: "" }, + ...CLOUD_MODELS, + { value: SECTION_BYOK, label: "Bring Your Own Key", provider: "supercode", cost: "", desc: "" }, + ...BYOK_MODELS, + ], + "spark-premium": [ + { value: SECTION_CLOUD, label: "Supercode Cloud", provider: "supercode", cost: "", desc: "" }, + ...CLOUD_MODELS, + { value: SECTION_BYOK, label: "Bring Your Own Key", provider: "supercode", cost: "", desc: "" }, + ...BYOK_MODELS, + ], + "pro": [ + { value: SECTION_CLOUD, label: "Supercode Cloud", provider: "supercode", cost: "", desc: "" }, + ...CLOUD_MODELS, + { value: SECTION_BYOK, label: "Bring Your Own Key", provider: "supercode", cost: "", desc: "" }, + ...BYOK_MODELS, + ], + "ultra": [ + { value: SECTION_CLOUD, label: "Supercode Cloud", provider: "supercode", cost: "", desc: "" }, + ...CLOUD_MODELS, + { value: SECTION_BYOK, label: "Bring Your Own Key", provider: "supercode", cost: "", desc: "" }, + ...BYOK_MODELS, + ], + "none": [ + { value: SECTION_CLOUD, label: "Supercode Cloud", provider: "supercode", cost: "", desc: "" }, + ...CLOUD_MODELS, + { value: SECTION_BYOK, label: "Bring Your Own Key", provider: "supercode", cost: "", desc: "" }, + ...BYOK_MODELS, + ], +} + +// Default model list (fallback) +export const MODELS: ModelEntry[] = TIER_MODELS["none"] export class ModelPicker { items: ModelEntry[] = MODELS @@ -377,28 +436,13 @@ export class ModelPicker { const name = chalk.hex( isCurrent ? theme.green : theme.greenGlow, )(m.label.padEnd(22)) - const cost = chalk.hex( - m.cost === "free" ? theme.greenGlow : theme.muted, - )(m.cost.padEnd(6)) + const cost = chalk.hex(theme.muted)(m.cost.padEnd(6)) const desc = chalk.hex(theme.muted)(m.desc.padEnd(20)) const marker = isCurrent ? ` ${chalk.bgHex(theme.amber).hex(theme.black).bold(" current ")}` : "" - const isPremium = PREMIUM_CLOUD_MODELS.has(m.value) - const cloudTag = - m.provider === "supercode" && !isCurrent && !isPremium - ? ` ${chalk.bgHex(theme.green).hex(theme.black).bold(" CLOUD ")}` - : "" - const premiumTag = - isPremium && !isCurrent - ? ` ${chalk.bgHex(theme.amber).hex(theme.black).bold(" PREMIUM ")}` - : "" - const freeTag = - !isCurrent && m.cost === "free" && m.provider !== "supercode" - ? ` ${chalk.bgHex(theme.green).hex(theme.black).bold(" FREE ")}` - : "" - - const label = `${prefix} ${name} ${cost}${desc}${marker}${cloudTag}${premiumTag}${freeTag}` + + const label = `${prefix} ${name} ${cost}${desc}${marker}` if (isSelected) { const bg = chalk.bgHex(theme.greenDeep) @@ -507,9 +551,12 @@ export async function pickModel( const currentProvider = stored?.provider || "supercode" const currentModel = stored?.model || "deepseek-v4-flash" + // Get user tier for model filtering + const userTier = await getUserTier() + const picker = new ModelPicker() if (providerFilter) { - picker.items = MODELS.filter((m) => m.provider === providerFilter) + picker.items = TIER_MODELS[userTier].filter((m) => m.provider === providerFilter) if (opts?.allowCustom) { picker.items.push({ value: "__custom__", @@ -520,6 +567,8 @@ export async function pickModel( }) } picker.setFilter("") + } else { + picker.items = TIER_MODELS[userTier] } const cols = process.stdout.columns ?? 80 @@ -608,15 +657,23 @@ export async function pickModel( return { provider: selected.provider, model: trimmed } } - // Premium cloud models require Supercode Cloud Premium - if (PREMIUM_CLOUD_MODELS.has(selected.value)) { + // Premium cloud models (frontier) require Pro or higher tier + const frontierModels = new Set([ + "anthropic/claude-sonnet-4.6", + "anthropic/claude-opus-4.7", + "anthropic/claude-opus-4-8", + "openai/gpt-5.5", + "grok/grok-4-fast-reasoning", + "gemini-2.5-pro", + "deepseek/deepseek-reasoner", + ]) + if (frontierModels.has(selected.value) && userTier !== "pro" && userTier !== "ultra") { process.stdout.write("\n") process.stdout.write( - ` ${chalk.hex(theme.amber)("◆")} ${chalk.hex(theme.green).bold("Supercode Cloud Premium")} + ` ${chalk.hex(theme.amber)("◆")} ${chalk.hex(theme.green).bold("Pro Plan Required")} - ${chalk.hex(theme.muted)("This model is available on Supercode Cloud Premium.")} - ${chalk.hex(theme.muted)("Upgrade your plan to access premium models.")} - ${chalk.hex(theme.muted)("→ https://supercode.ai/pricing")} + ${chalk.hex(theme.muted)("This model requires a Pro or Ultra subscription.")} + ${chalk.hex(theme.muted)("Run /upgrade to subscribe.")} ` ) diff --git a/apps/supercode-cli/server/src/cli/utils/tui.ts b/apps/supercode-cli/server/src/cli/utils/tui.ts index 5be3304..d270f67 100644 --- a/apps/supercode-cli/server/src/cli/utils/tui.ts +++ b/apps/supercode-cli/server/src/cli/utils/tui.ts @@ -785,6 +785,7 @@ export class PersistentStatusBar { private state = { mode: "chat", model: "", + plan: "", connectionType: "", cumulativeTokens: 0, contextWindow: 0, @@ -889,6 +890,10 @@ export class PersistentStatusBar { this.update({ model }) } + setPlan(plan: string) { + this.update({ plan }) + } + setConnectionType(type: string) { this.update({ connectionType: type }) } @@ -982,7 +987,7 @@ export class PersistentStatusBar { parts.push( this.state.isStreaming ? ansiColor(theme.greenDim, "esc interrupt") - : ansiColor(theme.greenDim, "tab mode"), + : ansiColor(theme.greenDim, this.state.plan || "tab mode"), ) const inner = parts.join(sep) diff --git a/apps/supercode-cli/server/src/lib/model-access.ts b/apps/supercode-cli/server/src/lib/model-access.ts index fa88926..aa485ad 100644 --- a/apps/supercode-cli/server/src/lib/model-access.ts +++ b/apps/supercode-cli/server/src/lib/model-access.ts @@ -24,6 +24,10 @@ export function tierIndex(tier: string): number { /** * Checks whether a model slug is allowed for the user's plan tier. * Model gating is backed by the `Model` table's `minTier` field. + * + * Tier ladder: spark < spark-premium < pro < ultra + * Open models use minTier "spark" so both Spark and Spark Premium get them. + * Spark Premium is the paid open-models tier (higher limits/credits, same catalog). */ export async function isModelAllowedForTier( modelSlug: string, @@ -31,9 +35,17 @@ export async function isModelAllowedForTier( ): Promise { try { const models = await ensureCache() - const model = models.find( - (m) => m.slug === modelSlug || modelSlug.includes(m.slug), - ) + const normalized = modelSlug.trim().toLowerCase() + const model = models.find((m) => { + const slug = m.slug.toLowerCase() + return ( + slug === normalized || + normalized === slug || + normalized.endsWith(`/${slug}`) || + normalized.includes(slug) || + slug.includes(normalized) + ) + }) if (!model) return false return tierIndex(userTier) >= tierIndex(model.minTier) } catch (error) { @@ -43,8 +55,11 @@ export async function isModelAllowedForTier( } export function getUpgradeSuggestion(userTier: string): string { + if (tierIndex(userTier) < tierIndex("spark-premium")) { + return "To access more open models and higher limits, run /upgrade (Spark Premium)" + } if (tierIndex(userTier) < tierIndex("pro")) { - return "To access premium models, run /upgrade" + return "To access premium models (Claude, GPT, etc.), run /upgrade" } if (tierIndex(userTier) < tierIndex("ultra")) { return "To access all models unrestricted, run /upgrade"