Skip to content
Open
7 changes: 7 additions & 0 deletions src/brands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { TweakccConfig } from './types.js';
import { buildZaiTweakccConfig } from './zai.js';
import { buildMinimaxTweakccConfig } from './minimax.js';
import { buildOpenRouterTweakccConfig } from './openrouter.js';
import { buildNanoGPTTweakccConfig } from './nanogpt.js';
import { buildCCRouterTweakccConfig } from './ccrouter.js';
import { buildMirrorTweakccConfig } from './mirror.js';

Expand Down Expand Up @@ -31,6 +32,12 @@ const BRAND_PRESETS: Record<string, BrandPreset> = {
description: 'Light UI with teal/cyan accents and OpenRouter toolset label.',
buildTweakccConfig: buildOpenRouterTweakccConfig,
},
nanogpt: {
key: 'nanogpt',
label: 'NanoGPT Violet',
description: 'Dark UI with purple/violet accents and NanoGPT toolset label.',
buildTweakccConfig: buildNanoGPTTweakccConfig,
},
ccrouter: {
key: 'ccrouter',
label: 'CCRouter Sky',
Expand Down
198 changes: 198 additions & 0 deletions src/brands/nanogpt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
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);

// NanoGPT uses blue/purple/violet palette
const palette = {
base: '#1a1a2e',
surface: '#16213e',
panel: '#1f2544',
border: '#4a4a8a',
borderStrong: '#6a5acd',
text: '#e8e8f0',
textMuted: '#9090b0',
textDim: '#6a6a8a',
// Purple/violet palette
violet: '#8b5cf6', // Primary violet
violetBright: '#a78bfa', // Bright violet
violetDeep: '#6d28d9', // Deep violet
violetMuted: '#7c3aed', // Softer violet
purple: '#9333ea',
blue: '#3b82f6',
green: '#10b981',
red: '#ef4444',
orange: '#f59e0b',
};

const theme: Theme = {
name: 'NanoGPT Violet',
id: 'nanogpt-violet',
colors: {
autoAccept: rgb(palette.green),
bashBorder: rgb(palette.violet),
claude: rgb(palette.violetDeep),
claudeShimmer: lighten(palette.violet, 0.35),
claudeBlue_FOR_SYSTEM_SPINNER: rgb(palette.violet),
claudeBlueShimmer_FOR_SYSTEM_SPINNER: lighten(palette.violetMuted, 0.2),
permission: rgb(palette.purple),
permissionShimmer: lighten(palette.purple, 0.25),
planMode: rgb(palette.violetDeep),
ide: rgb(palette.violetMuted),
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.violetMuted, 0.12),
suggestion: rgb(palette.violetMuted),
remember: rgb(palette.violetDeep),
background: rgb(palette.base),
success: rgb(palette.green),
error: rgb(palette.red),
warning: rgb(palette.orange),
warningShimmer: lighten(palette.orange, 0.28),
diffAdded: mix(palette.base, palette.green, 0.2),
diffRemoved: mix(palette.base, palette.red, 0.2),
diffAddedDimmed: mix(palette.base, palette.green, 0.12),
diffRemovedDimmed: mix(palette.base, palette.red, 0.12),
diffAddedWord: mix(palette.base, palette.green, 0.42),
diffRemovedWord: mix(palette.base, palette.red, 0.42),
diffAddedWordDimmed: mix(palette.base, palette.green, 0.28),
diffRemovedWordDimmed: mix(palette.base, palette.red, 0.28),
red_FOR_SUBAGENTS_ONLY: rgb(palette.red),
blue_FOR_SUBAGENTS_ONLY: rgb(palette.blue),
green_FOR_SUBAGENTS_ONLY: rgb(palette.green),
yellow_FOR_SUBAGENTS_ONLY: rgb(palette.orange),
purple_FOR_SUBAGENTS_ONLY: rgb(palette.purple),
orange_FOR_SUBAGENTS_ONLY: rgb(palette.orange),
pink_FOR_SUBAGENTS_ONLY: lighten(palette.purple, 0.32),
cyan_FOR_SUBAGENTS_ONLY: rgb(palette.blue),
professionalBlue: rgb(palette.violet),
rainbow_red: rgb(palette.red),
rainbow_orange: rgb(palette.orange),
rainbow_yellow: lighten(palette.orange, 0.18),
rainbow_green: rgb(palette.green),
rainbow_blue: rgb(palette.blue),
rainbow_indigo: rgb(palette.violetDeep),
rainbow_violet: rgb(palette.purple),
rainbow_red_shimmer: lighten(palette.red, 0.35),
rainbow_orange_shimmer: lighten(palette.orange, 0.35),
rainbow_yellow_shimmer: lighten(palette.orange, 0.3),
rainbow_green_shimmer: lighten(palette.green, 0.35),
rainbow_blue_shimmer: lighten(palette.blue, 0.35),
rainbow_indigo_shimmer: lighten(palette.violetDeep, 0.35),
rainbow_violet_shimmer: lighten(palette.purple, 0.35),
clawd_body: rgb(palette.violetDeep),
clawd_background: rgb(palette.base),
userMessageBackground: rgb(palette.panel),
bashMessageBackgroundColor: rgb(palette.surface),
memoryBackgroundColor: mix(palette.panel, palette.violetMuted, 0.12),
rate_limit_fill: rgb(palette.violet),
rate_limit_empty: rgb(palette.borderStrong),
},
};

export const buildNanoGPTTweakccConfig = (): TweakccConfig => ({
ccVersion: '',
ccInstallationPath: null,
lastModified: new Date().toISOString(),
changesApplied: false,
hidePiebaldAnnouncement: true,
settings: {
themes: [theme, ...DEFAULT_THEMES],
thinkingVerbs: {
format: '{}... ',
verbs: [
'Computing',
'Processing',
'Inferring',
'Modeling',
'Generating',
'Tokenizing',
'Encoding',
'Decoding',
'Sampling',
'Orchestrating',
'Reasoning',
'Synthesizing',
'Optimizing',
'Transforming',
],
},
thinkingStyle: {
updateInterval: 120,
phases: ['·', '•', '◦', '•'],
reverseMirror: false,
},
userMessageDisplay: {
format: formatUserMessage(getUserLabel()),
styling: ['bold'],
foregroundColor: 'default',
backgroundColor: 'default',
borderStyle: 'topBottomSingle',
borderColor: rgb(palette.violet),
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: 'nanogpt',
allowedTools: '*',
},
],
defaultToolset: 'nanogpt',
planModeToolset: 'nanogpt',
},
});
2 changes: 1 addition & 1 deletion src/cli/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ COMMANDS

OPTIONS (create/quick)
--name <name> Variant name (becomes CLI command)
--provider <name> Provider: mirror | zai | minimax | openrouter | ccrouter
--provider <name> Provider: mirror | zai | minimax | openrouter | nanogpt | ccrouter
--api-key <key> Provider API key
--brand <preset> Theme: auto | none | mirror | zai | minimax
--no-team-mode Disable team mode (not recommended)
Expand Down
21 changes: 21 additions & 0 deletions src/core/wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ export const writeWrapper = (
ccrSecondary: '\x1b[38;5;45m', // Bright cyan
ccrAccent: '\x1b[38;5;33m', // Deep blue
ccrDim: '\x1b[38;5;31m', // Muted blue
// NanoGPT: Purple/Violet gradient
ngPrimary: '\x1b[38;5;135m', // Violet
ngSecondary: '\x1b[38;5;141m', // Light violet
ngAccent: '\x1b[38;5;99m', // Deep purple
ngDim: '\x1b[38;5;97m', // Muted purple
// Mirror: Silver/Chrome with electric blue
mirPrimary: '\x1b[38;5;252m', // Silver/light gray
mirSecondary: '\x1b[38;5;250m', // Platinum
Expand Down Expand Up @@ -138,6 +143,22 @@ export const writeWrapper = (
'CCMORT',
' __cc_show_label="0"',
' ;;',
' nanogpt)',
" cat <<'CCMNGPT'",
'',
`${C.ngPrimary} ███╗ ██╗ █████╗ ███╗ ██╗ ██████╗ ██████╗ ██████╗ ████████╗${C.reset}`,
`${C.ngPrimary} ████╗ ██║██╔══██╗████╗ ██║██╔═══██╗██╔════╝ ██╔══██╗╚══██╔══╝${C.reset}`,
`${C.ngSecondary} ██╔██╗ ██║███████║██╔██╗ ██║██║ ██║██║ ███╗██████╔╝ ██║${C.reset}`,
`${C.ngSecondary} ██║╚██╗██║██╔══██║██║╚██╗██║██║ ██║██║ ██║██╔═══╝ ██║${C.reset}`,
`${C.ngAccent} ██║ ╚████║██║ ██║██║ ╚████║╚██████╔╝╚██████╔╝██║ ██║${C.reset}`,
`${C.ngAccent} ╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝${C.reset}`,
'',
`${C.ngDim} ━━━━━━━━━━━━━━${C.ngPrimary}◆${C.ngDim}━━━━━━━━━━━━━━${C.reset}`,
`${C.ngSecondary} One API ${C.ngDim}━${C.ngSecondary} Many Models${C.reset}`,
'',
'CCMNGPT',
' __cc_show_label="0"',
' ;;',
' ccrouter)',
" cat <<'CCMCCR'",
'',
Expand Down
16 changes: 16 additions & 0 deletions src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,22 @@ const PROVIDERS: Record<string, ProviderTemplate> = {
authMode: 'authToken',
requiresModelMapping: true,
},
nanogpt: {
key: 'nanogpt',
label: 'NanoGPT',
description: 'NanoGPT gateway for OpenAI-compatible requests',
baseUrl: 'https://nano-gpt.com/api',
env: {
API_TIMEOUT_MS: DEFAULT_TIMEOUT_MS,
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1,
CC_MIRROR_SPLASH: 1,
CC_MIRROR_PROVIDER_LABEL: 'NanoGPT',
CC_MIRROR_SPLASH_STYLE: 'nanogpt',
},
apiKeyLabel: 'NanoGPT API key',
authMode: 'authToken',
requiresModelMapping: true,
},
ccrouter: {
key: 'ccrouter',
label: 'Claude Code Router',
Expand Down
5 changes: 5 additions & 0 deletions src/tui/components/ui/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ export const providerColors = {
border: 'cyan',
accent: 'cyanBright',
},
nanogpt: {
primary: 'magenta',
border: 'magenta',
accent: 'magentaBright',
},
ccrouter: {
primary: 'blue',
border: 'blue',
Expand Down
4 changes: 4 additions & 0 deletions src/tui/content/haikus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export const COMPLETION_HAIKUS: Record<string, HaikuLines[]> = {
['Many paths, one door—', 'OpenRouter finds the way.', 'Models at your core.'],
['Routes converge as one,', 'your choice echoes through the wire.', 'The journey begun.'],
],
nanogpt: [
['Nano sparks ignite,', 'vast models at your command.', 'Code takes its first flight.'],
['Violet streams flow,', 'NanoGPT powers your dreams.', 'Watch your projects grow.'],
],
ccrouter: [
['Local models shine,', "routed through the mirror's edge.", 'Your code, your design.'],
['Proxied through the night,', 'your models stand at the ready.', 'Code takes its first flight.'],
Expand Down
27 changes: 26 additions & 1 deletion src/tui/content/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,31 @@ export const PROVIDER_EDUCATION: Record<string, ProviderEducation> = {
setupNote: 'Create an account, add credits, then generate an API key. You must set model aliases.',
},

nanogpt: {
headline: 'NanoGPT — One API, Many Models',
tagline: 'Nano power, vast potential',
features: [
'Access to 100+ AI models',
'Pay-per-use or subscription pricing',
'Model flexibility',
'Violet-themed interface',
],
bestFor: 'Trying different models without multiple accounts',
models: {
opus: 'zai-org/glm-4.7:thinking',
sonnet: 'zai-org/glm-4.7:thinking',
haiku: 'zai-org/glm-4.7:thinking',
},
requiresMapping: true,
hasPromptPack: false,
setupLinks: {
subscribe: 'https://nano-gpt.com',
apiKey: 'https://nano-gpt.com/api',
docs: 'https://docs.nano-gpt.com',
},
setupNote: 'Create an account, add credits, then copy your API key from the API page.',
},

ccrouter: {
headline: 'Claude Code Router — Local Model Gateway',
tagline: 'Your models, your rules',
Expand Down Expand Up @@ -148,7 +173,7 @@ export const getProviderEducation = (providerKey: string): ProviderEducation | n
*/
export const PROVIDER_COMPARISON = {
fullySupported: ['mirror', 'zai', 'minimax'],
requiresMapping: ['openrouter'],
requiresMapping: ['openrouter', 'nanogpt'],
hasPromptPack: ['zai', 'minimax'],
localFirst: ['ccrouter'],
pureClaudeCode: ['mirror'],
Expand Down
5 changes: 5 additions & 0 deletions src/tui/screens/ApiKeyScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ const PROVIDER_LINKS: Record<string, ProviderLinkInfo> = {
subscribe: 'https://openrouter.ai/account',
note: 'Your OpenRouter key will be stored as ANTHROPIC_AUTH_TOKEN.',
},
nanogpt: {
apiKey: 'https://nano-gpt.com/api',
subscribe: 'https://nano-gpt.com',
note: 'Your NanoGPT key will be stored as ANTHROPIC_AUTH_TOKEN.',
},
ccrouter: {
apiKey: 'https://github.com/musistudio/claude-code-router',
subscribe: 'https://github.com/musistudio/claude-code-router#installation',
Expand Down
5 changes: 5 additions & 0 deletions src/tui/screens/ModelConfigScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ function getPlaceholder(providerKey: string | undefined, model: 'opus' | 'sonnet
sonnet: 'anthropic/claude-3.5-sonnet',
haiku: 'anthropic/claude-3-haiku',
},
nanogpt: {
opus: 'zai-org/glm-4.7:thinking',
sonnet: 'zai-org/glm-4.7:thinking',
haiku: 'zai-org/glm-4.7:thinking',
},
ccrouter: {
opus: 'deepseek,deepseek-reasoner',
sonnet: 'deepseek,deepseek-chat',
Expand Down
2 changes: 1 addition & 1 deletion src/tui/screens/VariantActionsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ interface VariantActionsScreenProps {
}

// Providers that require model mapping
const MODEL_MAPPING_PROVIDERS = ['openrouter', 'ccrouter'];
const MODEL_MAPPING_PROVIDERS = ['openrouter', 'nanogpt', 'ccrouter'];

export const VariantActionsScreen: React.FC<VariantActionsScreenProps> = ({
meta,
Expand Down
8 changes: 8 additions & 0 deletions test/e2e/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ export const PROVIDERS = [
expectedSplashStyle: 'openrouter',
colorCode: '\\x1b[38;5;43m', // Teal
},
{
key: 'nanogpt',
name: 'NanoGPT',
apiKey: 'test-nanogpt-key',
expectedThemeId: 'nanogpt-violet',
expectedSplashStyle: 'nanogpt',
colorCode: '\\x1b[38;5;135m', // Purple/violet
},
{
key: 'ccrouter',
name: 'Claude Code Router',
Expand Down
Loading