diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 63d948c..65b204a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -31,7 +31,7 @@ If applicable, add screenshots to help explain your problem. - **OS**: [e.g., macOS 14.0, Ubuntu 22.04, Windows 11] - **Node.js version**: [e.g., 20.10.0] - **cc-mirror version**: [e.g., 1.0.0] -- **Provider**: [e.g., zai, minimax, openrouter, ccrouter] +- **Provider**: [e.g., zai, minimax, openrouter, ccrouter, mirror, vercel-ai-gateway] ## Additional Context diff --git a/AGENTS.md b/AGENTS.md index 1991a9e..307a914 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -102,12 +102,12 @@ Location: `~/.local/bin/` ### Provider Auth Modes -| Provider | Auth Mode | Key Variable | -| -------------------- | ---------- | --------------------------- | -| zai, minimax, custom | API Key | `ANTHROPIC_API_KEY` | -| openrouter | Auth Token | `ANTHROPIC_AUTH_TOKEN` | -| ccrouter | Optional | placeholder token | -| mirror | None | user authenticates normally | +| Provider | Auth Mode | Key Variable | +| ----------------------------- | ---------- | --------------------------- | +| zai, minimax, custom | API Key | `ANTHROPIC_API_KEY` | +| openrouter, vercel-ai-gateway | Auth Token | `ANTHROPIC_AUTH_TOKEN` | +| ccrouter | Optional | placeholder token | +| mirror | None | user authenticates normally | ### Model Mapping (env vars) diff --git a/README.md b/README.md index 517abf7..c3d3198 100644 --- a/README.md +++ b/README.md @@ -287,14 +287,14 @@ minimax # Run MiniMax variant ## CLI Options ``` ---provider mirror | zai | minimax | openrouter | ccrouter | custom +--provider mirror | zai | minimax | openrouter | ccrouter | vercel-ai-gateway | custom --name Variant name (becomes the CLI command) --api-key Provider API key --base-url Custom API endpoint --model-sonnet Map to sonnet model --model-opus Map to opus model --model-haiku Map to haiku model ---brand Theme: auto | zai | minimax | openrouter | ccrouter | mirror +--brand Theme: auto | zai | minimax | openrouter | ccrouter | mirror | vercel-ai-gateway --no-team-mode Disable team mode (not recommended) --no-tweak Skip tweakcc theme --no-prompt-pack Skip provider prompt pack diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 35a0496..0e6a087 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -64,7 +64,7 @@ src/ │ └── prompt-pack/ # System prompt overlays │ ├── providers/ # Provider templates -│ └── index.ts # zai, minimax, openrouter, ccrouter, mirror +│ └── index.ts # zai, minimax, openrouter, ccrouter, mirror, vercel-ai-gateway │ └── brands/ # Theme presets ├── index.ts # Brand registry @@ -72,7 +72,8 @@ src/ ├── minimax.ts # Coral theme ├── openrouter.ts # Teal theme ├── ccrouter.ts # Sky theme - └── mirror.ts # Silver/chrome theme + ├── mirror.ts # Silver/chrome theme + └── vercel.ts # Vercel dark theme ``` --- diff --git a/docs/features/mirror-claude.md b/docs/features/mirror-claude.md index 037af62..125cc11 100644 --- a/docs/features/mirror-claude.md +++ b/docs/features/mirror-claude.md @@ -70,20 +70,20 @@ No API key required at setup. When you run `mclaude`, authenticate via: ## 📊 Provider Comparison ``` -┌──────────────────────────────────────────────────────────────────────────────┐ -│ │ -│ Feature │ zai │ minimax │ openrouter │ mirror │ -│ ─────────────────────┼──────────┼──────────┼────────────┼─────────────────│ -│ Model │ GLM-4.7 │ M2.1 │ You choose │ Claude (native) │ -│ Auth Mode │ API Key │ API Key │ Auth Token │ OAuth or Key │ -│ ANTHROPIC_BASE_URL │ ✓ Set │ ✓ Set │ ✓ Set │ ✗ Not set │ -│ ANTHROPIC_API_KEY │ ✓ Set │ ✓ Set │ ✗ │ ✗ Not set │ -│ Model Mappings │ ✓ Auto │ ✓ Auto │ ✓ Required │ ✗ Not set │ -│ Prompt Pack │ ✓ Full │ ✓ Full │ ✗ │ ✗ Pure │ -│ Team Mode │ Optional │ Optional │ Optional │ ✓ Default │ -│ Config Isolation │ ✓ │ ✓ │ ✓ │ ✓ │ -│ │ -└──────────────────────────────────────────────────────────────────────────────┘ +┌─────────────────────────────────────────────────────────────────────────────────────────────┐ +│ │ +│ Feature │ zai │ minimax │ openrouter │ vercel-ai-gw │ mirror │ +│ ─────────────────────┼──────────┼──────────┼────────────┼──────────────┼─────────────────│ +│ Model │ GLM-4.7 │ M2.1 │ You choose │ You choose │ Claude (native) │ +│ Auth Mode │ API Key │ API Key │ Auth Token │ Auth Token │ OAuth or Key │ +│ ANTHROPIC_BASE_URL │ ✓ Set │ ✓ Set │ ✓ Set │ ✓ Set │ ✗ Not set │ +│ ANTHROPIC_API_KEY │ ✓ Set │ ✓ Set │ ✗ │ ✗ │ ✗ Not set │ +│ Model Mappings │ ✓ Auto │ ✓ Auto │ ✓ Required │ ✓ Required │ ✗ Not set │ +│ Prompt Pack │ ✓ Full │ ✓ Full │ ✗ │ ✗ │ ✗ Pure │ +│ Team Mode │ Optional │ Optional │ Optional │ Optional │ ✓ Default │ +│ Config Isolation │ ✓ │ ✓ │ ✓ │ ✓ │ ✓ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────────────────────┘ ``` --- @@ -99,7 +99,7 @@ No API key required at setup. When you run `mclaude`, authenticate via: ### Not For -- **Different models** - use OpenRouter or CCRouter for alternative LLMs +- **Different models** - use OpenRouter, Vercel AI Gateway, or CCRouter for alternative LLMs - **Cost savings** - Z.ai and MiniMax offer Coding Plan subscriptions - **Offline use** - Mirror requires Anthropic API access diff --git a/package-lock.json b/package-lock.json index 874903e..030a007 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cc-mirror", - "version": "1.0.2", + "version": "1.1.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cc-mirror", - "version": "1.0.2", + "version": "1.1.5", "license": "MIT", "dependencies": { "ink": "^6.6.0", @@ -92,7 +92,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1175,7 +1174,6 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1225,7 +1223,6 @@ "integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/types": "8.51.0", @@ -1465,7 +1462,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1821,7 +1817,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2621,7 +2616,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3644,7 +3638,6 @@ "resolved": "https://registry.npmjs.org/ink/-/ink-6.6.0.tgz", "integrity": "sha512-QDt6FgJxgmSxAelcOvOHUvFxbIUjVpCH5bx+Slvc5m7IEcpGt3dYwbz/L+oRnqEGeRvwy1tineKK4ect3nW1vQ==", "license": "MIT", - "peer": true, "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.1", "ansi-escapes": "^7.2.0", @@ -5338,7 +5331,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6273,7 +6265,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6511,7 +6502,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7080,7 +7070,6 @@ "integrity": "sha512-Zw/uYiiyF6pUT1qmKbZziChgNPRu+ZRneAsMUDU6IwmXdWt5JwcUfy2bvLOCUtz5UniaN/Zx5aFttZYbYc7O/A==", "devOptional": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/brands/index.ts b/src/brands/index.ts index 0c1db3d..e82a66d 100644 --- a/src/brands/index.ts +++ b/src/brands/index.ts @@ -4,6 +4,7 @@ import { buildMinimaxTweakccConfig } from './minimax.js'; import { buildOpenRouterTweakccConfig } from './openrouter.js'; import { buildCCRouterTweakccConfig } from './ccrouter.js'; import { buildMirrorTweakccConfig } from './mirror.js'; +import { buildVercelTweakccConfig } from './vercel.js'; export interface BrandPreset { key: string; @@ -43,6 +44,12 @@ const BRAND_PRESETS: Record = { description: 'Reflective silver/chrome theme for pure Claude Code experience.', buildTweakccConfig: buildMirrorTweakccConfig, }, + 'vercel-ai-gateway': { + key: 'vercel-ai-gateway', + label: 'Vercel AI Gateway', + description: 'Minimal dark theme with Vercel AI Gateway brand colors (black, white, cyan).', + buildTweakccConfig: buildVercelTweakccConfig, + }, }; export const listBrandPresets = (): BrandPreset[] => Object.values(BRAND_PRESETS); diff --git a/src/brands/vercel.ts b/src/brands/vercel.ts new file mode 100644 index 0000000..0fa6b78 --- /dev/null +++ b/src/brands/vercel.ts @@ -0,0 +1,199 @@ +import type { TweakccConfig, Theme } from './types.js'; +import { DEFAULT_THEMES } from './defaultThemes.js'; +import { formatUserMessage, getUserLabel } from './userLabel.js'; + +type Rgb = { r: number; g: number; b: number }; + +const clamp = (value: number) => Math.max(0, Math.min(255, Math.round(value))); + +const hexToRgb = (hex: string): Rgb => { + const normalized = hex.replace('#', '').trim(); + if (normalized.length === 3) { + const [r, g, b] = normalized.split(''); + return { + r: clamp(parseInt(r + r, 16)), + g: clamp(parseInt(g + g, 16)), + b: clamp(parseInt(b + b, 16)), + }; + } + if (normalized.length !== 6) { + throw new Error(`Unsupported hex color: ${hex}`); + } + return { + r: clamp(parseInt(normalized.slice(0, 2), 16)), + g: clamp(parseInt(normalized.slice(2, 4), 16)), + b: clamp(parseInt(normalized.slice(4, 6), 16)), + }; +}; + +const rgb = (hex: string) => { + const { r, g, b } = hexToRgb(hex); + return `rgb(${r},${g},${b})`; +}; + +const mix = (hexA: string, hexB: string, weight: number) => { + const a = hexToRgb(hexA); + const b = hexToRgb(hexB); + const w = Math.max(0, Math.min(1, weight)); + return `rgb(${clamp(a.r + (b.r - a.r) * w)},${clamp(a.g + (b.g - a.g) * w)},${clamp(a.b + (b.b - a.b) * w)})`; +}; + +const lighten = (hex: string, weight: number) => mix(hex, '#ffffff', weight); + +// Vercel's brand colors: black, white, and accent colors +const palette = { + // Base colors - Vercel's minimal dark aesthetic + base: '#000000', + surface: '#111111', + panel: '#1a1a1a', + border: '#333333', + borderStrong: '#444444', + text: '#ededed', + textMuted: '#a1a1a1', + textDim: '#666666', + // Vercel accent colors + white: '#ffffff', + blue: '#0070f3', // Vercel's primary blue + cyan: '#79ffe1', // Vercel's success/accent cyan + purple: '#7928ca', // Vercel's purple + pink: '#ff0080', // Vercel's pink/magenta + orange: '#f5a623', + red: '#ee0000', + green: '#0070f3', + yellow: '#f5a623', +}; + +const theme: Theme = { + name: 'Vercel AI Gateway', + id: 'vercel-ai-gateway', + colors: { + autoAccept: rgb(palette.cyan), + bashBorder: rgb(palette.border), + claude: rgb(palette.white), + claudeShimmer: rgb(palette.textMuted), + claudeBlue_FOR_SYSTEM_SPINNER: rgb(palette.blue), + claudeBlueShimmer_FOR_SYSTEM_SPINNER: lighten(palette.blue, 0.3), + permission: rgb(palette.cyan), + permissionShimmer: lighten(palette.cyan, 0.25), + planMode: rgb(palette.purple), + ide: rgb(palette.textMuted), + promptBorder: rgb(palette.border), + promptBorderShimmer: rgb(palette.borderStrong), + text: rgb(palette.text), + inverseText: rgb(palette.base), + inactive: rgb(palette.textDim), + subtle: mix(palette.base, palette.border, 0.3), + suggestion: rgb(palette.textMuted), + remember: rgb(palette.blue), + background: rgb(palette.base), + success: rgb(palette.cyan), + error: rgb(palette.red), + warning: rgb(palette.orange), + warningShimmer: lighten(palette.orange, 0.28), + diffAdded: mix(palette.base, palette.cyan, 0.15), + diffRemoved: mix(palette.base, palette.red, 0.15), + diffAddedDimmed: mix(palette.base, palette.cyan, 0.08), + diffRemovedDimmed: mix(palette.base, palette.red, 0.08), + diffAddedWord: mix(palette.base, palette.cyan, 0.35), + diffRemovedWord: mix(palette.base, palette.red, 0.35), + diffAddedWordDimmed: mix(palette.base, palette.cyan, 0.2), + diffRemovedWordDimmed: mix(palette.base, palette.red, 0.2), + red_FOR_SUBAGENTS_ONLY: rgb(palette.red), + blue_FOR_SUBAGENTS_ONLY: rgb(palette.blue), + green_FOR_SUBAGENTS_ONLY: rgb(palette.cyan), + yellow_FOR_SUBAGENTS_ONLY: rgb(palette.yellow), + purple_FOR_SUBAGENTS_ONLY: rgb(palette.purple), + orange_FOR_SUBAGENTS_ONLY: rgb(palette.orange), + pink_FOR_SUBAGENTS_ONLY: rgb(palette.pink), + cyan_FOR_SUBAGENTS_ONLY: rgb(palette.cyan), + professionalBlue: rgb(palette.blue), + rainbow_red: rgb(palette.red), + rainbow_orange: rgb(palette.orange), + rainbow_yellow: rgb(palette.yellow), + rainbow_green: rgb(palette.cyan), + rainbow_blue: rgb(palette.blue), + rainbow_indigo: rgb(palette.purple), + rainbow_violet: rgb(palette.pink), + rainbow_red_shimmer: lighten(palette.red, 0.35), + rainbow_orange_shimmer: lighten(palette.orange, 0.35), + rainbow_yellow_shimmer: lighten(palette.yellow, 0.3), + rainbow_green_shimmer: lighten(palette.cyan, 0.35), + rainbow_blue_shimmer: lighten(palette.blue, 0.35), + rainbow_indigo_shimmer: lighten(palette.purple, 0.35), + rainbow_violet_shimmer: lighten(palette.pink, 0.35), + clawd_body: rgb(palette.white), + clawd_background: rgb(palette.base), + userMessageBackground: rgb(palette.surface), + bashMessageBackgroundColor: rgb(palette.panel), + memoryBackgroundColor: mix(palette.surface, palette.blue, 0.08), + rate_limit_fill: rgb(palette.blue), + rate_limit_empty: rgb(palette.border), + }, +}; + +export const buildVercelTweakccConfig = (): TweakccConfig => ({ + ccVersion: '', + ccInstallationPath: null, + lastModified: new Date().toISOString(), + changesApplied: false, + hidePiebaldAnnouncement: true, + settings: { + themes: [theme, ...DEFAULT_THEMES], + thinkingVerbs: { + format: '{}... ', + verbs: [ + 'Deploying', + 'Building', + 'Routing', + 'Streaming', + 'Proxying', + 'Inferring', + 'Optimizing', + 'Caching', + 'Scaling', + 'Balancing', + 'Resolving', + 'Connecting', + 'Processing', + 'Analyzing', + ], + }, + thinkingStyle: { + updateInterval: 120, + phases: ['_', '-', '=', '-'], + reverseMirror: false, + }, + userMessageDisplay: { + format: formatUserMessage(getUserLabel()), + styling: ['bold'], + foregroundColor: 'default', + backgroundColor: 'default', + borderStyle: 'topBottomSingle', + borderColor: rgb(palette.border), + paddingX: 1, + paddingY: 0, + fitBoxToContent: true, + }, + inputBox: { + removeBorder: true, + }, + misc: { + showTweakccVersion: false, + showPatchesApplied: false, + expandThinkingBlocks: true, + enableConversationTitle: true, + hideStartupBanner: true, + hideCtrlGToEditPrompt: true, + hideStartupClawd: true, + increaseFileReadLimit: true, + }, + toolsets: [ + { + name: 'vercel', + allowedTools: '*', + }, + ], + defaultToolset: 'vercel', + planModeToolset: 'vercel', + }, +}); diff --git a/src/cli/help.ts b/src/cli/help.ts index 8520e7b..5d88ae8 100644 --- a/src/cli/help.ts +++ b/src/cli/help.ts @@ -34,9 +34,9 @@ COMMANDS OPTIONS (create/quick) --name Variant name (becomes CLI command) - --provider Provider: mirror | zai | minimax | openrouter | ccrouter + --provider Provider: mirror | zai | minimax | openrouter | ccrouter | vercel-ai-gateway --api-key Provider API key - --brand Theme: auto | none | mirror | zai | minimax + --brand Theme: auto | none | mirror | zai | minimax | vercel-ai-gateway --no-team-mode Disable team mode (not recommended) --tui / --no-tui Force TUI on/off diff --git a/src/cli/utils/modelOverrides.ts b/src/cli/utils/modelOverrides.ts index eb5578f..bc59ee8 100644 --- a/src/cli/utils/modelOverrides.ts +++ b/src/cli/utils/modelOverrides.ts @@ -22,7 +22,7 @@ export function getModelOverridesFromArgs(opts: ParsedArgs): ModelOverrides { } /** - * Ensure model mapping for providers that require it (e.g., OpenRouter, LiteLLM) + * Ensure model mapping for providers that require it (e.g., OpenRouter, Vercel AI Gateway) * Prompts for missing models if not in --yes mode */ export async function ensureModelMapping( @@ -38,7 +38,13 @@ export async function ensureModelMapping( haiku: (overrides.haiku ?? '').trim().length === 0, }; if (opts.yes && (missing.sonnet || missing.opus || missing.haiku)) { - throw new Error('OpenRouter/Local LLMs require --model-sonnet/--model-opus/--model-haiku'); + let errorMsg = 'This provider requires model mapping. Use --model-sonnet, --model-opus, --model-haiku.'; + if (providerKey === 'vercel-ai-gateway') { + errorMsg += + '\nFormat: provider/model (e.g., openai/gpt-5.1-codex-max, anthropic/claude-sonnet-4.5)' + + '\nBrowse models: vercel.com/ai-gateway/models'; + } + throw new Error(errorMsg); } if (!opts.yes) { if (missing.sonnet) overrides.sonnet = await requirePrompt('Default Sonnet model', overrides.sonnet); diff --git a/src/core/wrapper.ts b/src/core/wrapper.ts index a9aa7c6..163c604 100644 --- a/src/core/wrapper.ts +++ b/src/core/wrapper.ts @@ -71,6 +71,11 @@ export const writeWrapper = ( mirSecondary: '\x1b[38;5;250m', // Platinum mirAccent: '\x1b[38;5;45m', // Electric cyan mirDim: '\x1b[38;5;243m', // Muted silver + // Vercel: Black/White with cyan accents + verPrimary: '\x1b[38;5;255m', // White + verSecondary: '\x1b[38;5;252m', // Light gray + verAccent: '\x1b[38;5;51m', // Cyan (#79ffe1) + verDim: '\x1b[38;5;245m', // Muted gray // Default: White/Gray defPrimary: '\x1b[38;5;255m', // White defDim: '\x1b[38;5;245m', // Gray @@ -170,6 +175,22 @@ export const writeWrapper = ( 'CCMMIR', ' __cc_show_label="0"', ' ;;', + ' vercel-ai-gateway)', + " cat <<'CCMVER'", + '', + `${C.verPrimary} ██╗ ██╗███████╗██████╗ ██████╗███████╗██╗${C.reset}`, + `${C.verPrimary} ██║ ██║██╔════╝██╔══██╗██╔════╝██╔════╝██║${C.reset}`, + `${C.verSecondary} ██║ ██║█████╗ ██████╔╝██║ █████╗ ██║${C.reset}`, + `${C.verSecondary} ╚██╗ ██╔╝██╔══╝ ██╔══██╗██║ ██╔══╝ ██║${C.reset}`, + `${C.verAccent} ╚████╔╝ ███████╗██║ ██║╚██████╗███████╗███████╗${C.reset}`, + `${C.verAccent} ╚═══╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝╚══════╝╚══════╝${C.reset}`, + '', + `${C.verDim} ━━━━━━━━━━━━${C.verAccent}▲${C.verDim}━━━━━━━━━━━━${C.reset}`, + `${C.verSecondary} AI Gateway ${C.verDim}━${C.verSecondary} Any Model${C.reset}`, + '', + 'CCMVER', + ' __cc_show_label="0"', + ' ;;', ' *)', " cat <<'CCMGEN'", '', diff --git a/src/providers/index.ts b/src/providers/index.ts index ed941f9..aa2fa27 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -20,6 +20,12 @@ export interface ProviderTemplate { enablesTeamMode?: boolean; /** Skip prompt pack overlays (pure Claude experience) */ noPromptPack?: boolean; + /** Default model mappings for opus/sonnet/haiku */ + defaultModels?: { + opus?: string; + sonnet?: string; + haiku?: string; + }; } export interface ModelOverrides { @@ -117,6 +123,21 @@ const PROVIDERS: Record = { requiresModelMapping: false, // Models configured in ~/.claude-code-router/config.json credentialOptional: true, // No API key needed - CCRouter handles auth }, + 'vercel-ai-gateway': { + key: 'vercel-ai-gateway', + label: 'Vercel AI Gateway', + description: 'Unified gateway for multiple AI providers via Vercel', + baseUrl: 'https://ai-gateway.vercel.sh', + env: { + API_TIMEOUT_MS: DEFAULT_TIMEOUT_MS, + CC_MIRROR_SPLASH: 1, + CC_MIRROR_PROVIDER_LABEL: 'Vercel AI Gateway', + CC_MIRROR_SPLASH_STYLE: 'vercel-ai-gateway', + }, + apiKeyLabel: 'Vercel AI Gateway API key', + authMode: 'authToken', + requiresModelMapping: true, + }, custom: { key: 'custom', label: 'Custom', diff --git a/src/tui/content/education.ts b/src/tui/content/education.ts index 03632a0..b5285ff 100644 --- a/src/tui/content/education.ts +++ b/src/tui/content/education.ts @@ -15,7 +15,7 @@ export const EDUCATION = { 'and settings—completely independent from your main Claude Code.', '', 'Think of it as having multiple Claude Code "accounts", each pointing', - 'to a different AI backend: Z.ai, MiniMax, OpenRouter, or your own.', + 'to a different AI backend: Z.ai, MiniMax, OpenRouter, Vercel AI Gateway, or your own.', ], }, @@ -68,8 +68,8 @@ export const EDUCATION = { ' Sonnet → Default for most coding tasks', ' Haiku → Quick tasks, subagents, fast iteration', '', - 'When using providers like OpenRouter or LiteLLM, you map', - 'these aliases to actual model names (e.g., claude-3-opus).', + 'When using providers like OpenRouter, Vercel AI Gateway, or LiteLLM, you map', + 'these aliases to actual model names (e.g., anthropic/claude-opus-4.5).', ], }, diff --git a/src/tui/content/providers.ts b/src/tui/content/providers.ts index abf5807..896f1ac 100644 --- a/src/tui/content/providers.ts +++ b/src/tui/content/providers.ts @@ -21,6 +21,7 @@ export interface ProviderEducation { apiKey: string; docs?: string; github?: string; + models?: string; }; setupNote?: string; // Brief explanation of what this provider needs } @@ -134,6 +135,33 @@ export const PROVIDER_EDUCATION: Record = { }, setupNote: 'Uses normal Claude authentication. Sign in via OAuth or set ANTHROPIC_API_KEY.', }, + + 'vercel-ai-gateway': { + headline: 'Vercel AI Gateway — Unified AI Access', + tagline: 'One gateway, many models', + features: [ + 'Access multiple AI providers through Vercel', + 'Unified billing and usage tracking', + 'Built-in caching and rate limiting', + 'Minimal dark theme', + ], + bestFor: 'Teams already using Vercel who want unified AI access', + models: { + opus: 'openai/gpt-5.1-codex-max', + sonnet: 'google/gemini-3-pro-preview', + haiku: 'minimax/minimax-m2.1', + }, + requiresMapping: true, + hasPromptPack: false, + setupLinks: { + subscribe: 'https://vercel.com/ai-gateway', + apiKey: 'https://vercel.com/account/ai-gateway', + docs: 'https://vercel.com/docs/ai-gateway', + models: 'https://vercel.com/ai-gateway/models', + }, + setupNote: + 'Model format: provider/model (e.g., openai/gpt-5.1-codex-max). Browse models at vercel.com/ai-gateway/models', + }, }; /** @@ -148,10 +176,10 @@ export const getProviderEducation = (providerKey: string): ProviderEducation | n */ export const PROVIDER_COMPARISON = { fullySupported: ['mirror', 'zai', 'minimax'], - requiresMapping: ['openrouter'], + requiresMapping: ['openrouter', 'vercel-ai-gateway'], hasPromptPack: ['zai', 'minimax'], localFirst: ['ccrouter'], pureClaudeCode: ['mirror'], - teamModeDefault: ['mirror', 'zai', 'minimax', 'openrouter', 'ccrouter'], // All providers now have team mode by default + teamModeDefault: ['mirror', 'zai', 'minimax', 'openrouter', 'ccrouter', 'vercel-ai-gateway'], // All providers now have team mode by default recommended: ['mirror'], }; diff --git a/src/tui/screens/ApiKeyScreen.tsx b/src/tui/screens/ApiKeyScreen.tsx index fd1d1a4..88f454d 100644 --- a/src/tui/screens/ApiKeyScreen.tsx +++ b/src/tui/screens/ApiKeyScreen.tsx @@ -37,6 +37,11 @@ const PROVIDER_LINKS: Record = { subscribe: 'https://github.com/musistudio/claude-code-router#installation', note: 'No API key needed. Models are configured in ~/.claude-code-router/config.json', }, + 'vercel-ai-gateway': { + apiKey: 'https://vercel.com/account/ai-gateway', + subscribe: 'https://vercel.com/ai-gateway', + note: 'Your Vercel AI Gateway key will be stored as ANTHROPIC_AUTH_TOKEN.', + }, }; interface ApiKeyScreenProps { diff --git a/src/tui/screens/ModelConfigScreen.tsx b/src/tui/screens/ModelConfigScreen.tsx index e38bd2f..1136707 100644 --- a/src/tui/screens/ModelConfigScreen.tsx +++ b/src/tui/screens/ModelConfigScreen.tsx @@ -67,6 +67,11 @@ function getPlaceholder(providerKey: string | undefined, model: 'opus' | 'sonnet sonnet: 'deepseek,deepseek-chat', haiku: 'ollama,qwen2.5-coder:latest', }, + 'vercel-ai-gateway': { + opus: 'openai/gpt-5.1-codex-max', + sonnet: 'google/gemini-3-pro-preview', + haiku: 'minimax/minimax-m2.1', + }, }; const providerPlaceholders = placeholders[providerKey || ''] || placeholders.ccrouter; diff --git a/src/tui/screens/VariantActionsScreen.tsx b/src/tui/screens/VariantActionsScreen.tsx index 3a0902b..ad90516 100644 --- a/src/tui/screens/VariantActionsScreen.tsx +++ b/src/tui/screens/VariantActionsScreen.tsx @@ -30,7 +30,7 @@ interface VariantActionsScreenProps { } // Providers that require model mapping -const MODEL_MAPPING_PROVIDERS = ['openrouter', 'ccrouter']; +const MODEL_MAPPING_PROVIDERS = ['openrouter', 'ccrouter', 'vercel-ai-gateway']; export const VariantActionsScreen: React.FC = ({ meta, diff --git a/test/e2e/creation.test.ts b/test/e2e/creation.test.ts index 48b5401..d17c24e 100644 --- a/test/e2e/creation.test.ts +++ b/test/e2e/creation.test.ts @@ -19,7 +19,7 @@ test('E2E: Create variants for all providers', async (t) => { } }); - await t.test('creates all 4 provider variants with correct configuration', () => { + await t.test('creates all 5 provider variants with correct configuration', () => { withFakeNpm(() => { for (const provider of PROVIDERS) { const rootDir = makeTempDir(); diff --git a/test/e2e/providers.ts b/test/e2e/providers.ts index b4a3c1b..462045f 100644 --- a/test/e2e/providers.ts +++ b/test/e2e/providers.ts @@ -35,4 +35,12 @@ export const PROVIDERS = [ expectedSplashStyle: 'ccrouter', colorCode: '\\x1b[38;5;39m', // Sky blue }, + { + key: 'vercel-ai-gateway', + name: 'Vercel AI Gateway', + apiKey: 'test-vercel-key', + expectedThemeId: 'vercel-ai-gateway', + expectedSplashStyle: 'vercel-ai-gateway', + colorCode: '\\x1b[38;5;255m', // White + }, ]; diff --git a/test/provider-matrix.test.ts b/test/provider-matrix.test.ts index d94bf96..0bade82 100644 --- a/test/provider-matrix.test.ts +++ b/test/provider-matrix.test.ts @@ -45,6 +45,18 @@ test('Provider Feature Matrix', async (t) => { assert.equal(openrouter.authMode, 'authToken', 'openrouter should use authToken mode'); }); + await t.test('vercel-ai-gateway provider requires model mapping', () => { + const vercelAiGateway = getProvider('vercel-ai-gateway'); + assert.ok(vercelAiGateway, 'vercel-ai-gateway provider should exist'); + assert.ok(vercelAiGateway.requiresModelMapping, 'vercel-ai-gateway should require model mapping'); + assert.equal(vercelAiGateway.authMode, 'authToken', 'vercel-ai-gateway should use authToken mode'); + assert.equal( + vercelAiGateway.baseUrl, + 'https://ai-gateway.vercel.sh', + 'vercel-ai-gateway should have correct base URL' + ); + }); + await t.test('zai and minimax providers have splash styles', () => { const zai = getProvider('zai'); const minimax = getProvider('minimax');