From 45b353d39aa172466305b5fdf4ea2f4e4239993d Mon Sep 17 00:00:00 2001 From: 0xGingi <0xgingi@0xgingi.com> Date: Sun, 25 Jan 2026 12:55:26 -0500 Subject: [PATCH] Add nano-gpt.com as a provider --- docs/cli/index.md | 3 +- docs/concepts/model-providers.md | 28 +++ docs/providers/index.md | 1 + docs/providers/models.md | 1 + docs/providers/nanogpt.md | 66 ++++++ docs/start/wizard.md | 11 + src/agents/model-auth.ts | 1 + src/agents/model-selection.ts | 1 + src/agents/models-config.providers.ts | 20 ++ src/agents/nanogpt-models.ts | 188 ++++++++++++++++++ src/cli/program.smoke.test.ts | 6 + src/cli/program/register.onboard.ts | 4 +- src/commands/auth-choice-options.test.ts | 12 ++ src/commands/auth-choice-options.ts | 11 + .../auth-choice.apply.api-providers.ts | 62 ++++++ .../auth-choice.preferred-provider.ts | 1 + src/commands/onboard-auth.config-core.ts | 81 ++++++++ src/commands/onboard-auth.credentials.ts | 13 ++ src/commands/onboard-auth.ts | 4 + .../local/auth-choice.ts | 21 ++ src/commands/onboard-types.ts | 2 + 21 files changed, 535 insertions(+), 2 deletions(-) create mode 100644 docs/providers/nanogpt.md create mode 100644 src/agents/nanogpt-models.ts diff --git a/docs/cli/index.md b/docs/cli/index.md index d23ee3a5e..cb6460d3f 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -297,7 +297,7 @@ Options: - `--non-interactive` - `--mode ` - `--flow ` (manual is an alias for advanced) -- `--auth-choice ` +- `--auth-choice ` - `--token-provider ` (non-interactive; used with `--auth-choice token`) - `--token ` (non-interactive; used with `--auth-choice token`) - `--token-profile-id ` (non-interactive; default: `:manual`) @@ -308,6 +308,7 @@ Options: - `--ai-gateway-api-key ` - `--moonshot-api-key ` - `--kimi-code-api-key ` +- `--nanogpt-api-key ` - `--gemini-api-key ` - `--zai-api-key ` - `--minimax-api-key ` diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index acbca6461..413740881 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -157,6 +157,34 @@ Moonshot uses OpenAI-compatible endpoints, so configure it as a custom provider: } ``` +### NanoGPT + +NanoGPT exposes OpenAI-compatible endpoints: + +- Provider: `nanogpt` +- Auth: `NANOGPT_API_KEY` +- Example model: `nanogpt/zai-org/glm-4.7` +- CLI: `clawdbot onboard --auth-choice nanogpt-api-key` + +```json5 +{ + agents: { + defaults: { model: { primary: "nanogpt/zai-org/glm-4.7" } } + }, + models: { + mode: "merge", + providers: { + nanogpt: { + baseUrl: "https://nano-gpt.com/api/v1", + apiKey: "${NANOGPT_API_KEY}", + api: "openai-completions", + models: [{ id: "zai-org/glm-4.7", name: "GLM 4.7" }] + } + } + } +} +``` + ### Kimi Code Kimi Code uses a dedicated endpoint and key (separate from Moonshot): diff --git a/docs/providers/index.md b/docs/providers/index.md index c4f020192..fa1ad06d2 100644 --- a/docs/providers/index.md +++ b/docs/providers/index.md @@ -36,6 +36,7 @@ See [Venice AI](/providers/venice). - [OpenAI (API + Codex)](/providers/openai) - [Anthropic (API + Claude Code CLI)](/providers/anthropic) - [Qwen (OAuth)](/providers/qwen) +- [NanoGPT](/providers/nanogpt) - [OpenRouter](/providers/openrouter) - [Vercel AI Gateway](/providers/vercel-ai-gateway) - [Moonshot AI (Kimi + Kimi Code)](/providers/moonshot) diff --git a/docs/providers/models.md b/docs/providers/models.md index e581740a7..20a880516 100644 --- a/docs/providers/models.md +++ b/docs/providers/models.md @@ -33,6 +33,7 @@ See [Venice AI](/providers/venice). - [OpenAI (API + Codex)](/providers/openai) - [Anthropic (API + Claude Code CLI)](/providers/anthropic) +- [NanoGPT](/providers/nanogpt) - [OpenRouter](/providers/openrouter) - [Vercel AI Gateway](/providers/vercel-ai-gateway) - [Moonshot AI (Kimi + Kimi Code)](/providers/moonshot) diff --git a/docs/providers/nanogpt.md b/docs/providers/nanogpt.md new file mode 100644 index 000000000..99c8c8895 --- /dev/null +++ b/docs/providers/nanogpt.md @@ -0,0 +1,66 @@ +--- +summary: "Use NanoGPT's OpenAI-compatible API in Clawdbot" +read_when: + - You want to use NanoGPT as a model provider + - You need a NanoGPT API key or base URL setup +--- +# NanoGPT + +NanoGPT exposes OpenAI-compatible endpoints. Clawdbot registers it as the +`nanogpt` provider. + +## Quick setup + +1) Set `NANOGPT_API_KEY` (or run the wizard below). +2) Run onboarding: + +```bash +clawdbot onboard --auth-choice nanogpt-api-key +``` + +The default model is set to: + +``` +nanogpt/zai-org/glm-4.7 +``` + +## Config example + +```json5 +{ + env: { NANOGPT_API_KEY: "sk-..." }, + agents: { + defaults: { + model: { primary: "nanogpt/zai-org/glm-4.7" }, + models: { "nanogpt/zai-org/glm-4.7": { alias: "GLM 4.7" } } + } + }, + models: { + mode: "merge", + providers: { + nanogpt: { + baseUrl: "https://nano-gpt.com/api/v1", + apiKey: "${NANOGPT_API_KEY}", + api: "openai-completions", + models: [ + { + id: "zai-org/glm-4.7", + name: "GLM 4.7", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 200000, + maxTokens: 65535 + } + ] + } + } + } +} +``` + +## Notes + +- Model refs use `nanogpt/`. +- If you enable a model allowlist (`agents.defaults.models`), add every model you plan to use. +- For the full provider catalog and configuration rules, see [Model providers](/concepts/model-providers). diff --git a/docs/start/wizard.md b/docs/start/wizard.md index 8d4866392..39ec3ca04 100644 --- a/docs/start/wizard.md +++ b/docs/start/wizard.md @@ -248,6 +248,17 @@ clawdbot onboard --non-interactive \ --gateway-bind loopback ``` +NanoGPT example: + +```bash +clawdbot onboard --non-interactive \ + --mode local \ + --auth-choice nanogpt-api-key \ + --nanogpt-api-key "$NANOGPT_API_KEY" \ + --gateway-port 18789 \ + --gateway-bind loopback +``` + OpenCode Zen example: ```bash diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index 680d0f53c..a54dc0ceb 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -285,6 +285,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null { venice: "VENICE_API_KEY", mistral: "MISTRAL_API_KEY", opencode: "OPENCODE_API_KEY", + nanogpt: "NANOGPT_API_KEY", }; const envVar = envMap[normalized]; if (!envVar) return null; diff --git a/src/agents/model-selection.ts b/src/agents/model-selection.ts index e05370edd..29ccadd2a 100644 --- a/src/agents/model-selection.ts +++ b/src/agents/model-selection.ts @@ -29,6 +29,7 @@ export function normalizeProviderId(provider: string): string { if (normalized === "z.ai" || normalized === "z-ai") return "zai"; if (normalized === "opencode-zen") return "opencode"; if (normalized === "qwen") return "qwen-portal"; + if (normalized === "nano-gpt") return "nanogpt"; return normalized; } diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index 996f09dd0..25efb900b 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -13,6 +13,11 @@ import { SYNTHETIC_MODEL_CATALOG, } from "./synthetic-models.js"; import { discoverVeniceModels, VENICE_BASE_URL } from "./venice-models.js"; +import { + buildNanoGptModelDefinition, + NANOGPT_BASE_URL, + NANOGPT_MODEL_CATALOG, +} from "./nanogpt-models.js"; type ModelsConfig = NonNullable; export type ProviderConfig = NonNullable[string]; @@ -359,6 +364,14 @@ async function buildOllamaProvider(): Promise { }; } +function buildNanoGptProvider(): ProviderConfig { + return { + baseUrl: NANOGPT_BASE_URL, + api: "openai-completions", + models: NANOGPT_MODEL_CATALOG.map(buildNanoGptModelDefinition), + }; +} + export async function resolveImplicitProviders(params: { agentDir: string; }): Promise { @@ -410,6 +423,13 @@ export async function resolveImplicitProviders(params: { }; } + const nanogptKey = + resolveEnvApiKeyVarName("nanogpt") ?? + resolveApiKeyFromProfiles({ provider: "nanogpt", store: authStore }); + if (nanogptKey) { + providers.nanogpt = { ...buildNanoGptProvider(), apiKey: nanogptKey }; + } + // Ollama provider - only add if explicitly configured const ollamaKey = resolveEnvApiKeyVarName("ollama") ?? diff --git a/src/agents/nanogpt-models.ts b/src/agents/nanogpt-models.ts new file mode 100644 index 000000000..ab63f1ee4 --- /dev/null +++ b/src/agents/nanogpt-models.ts @@ -0,0 +1,188 @@ +import type { ModelDefinitionConfig } from "../config/types.js"; + +export const NANOGPT_BASE_URL = "https://nano-gpt.com/api/v1"; +export const NANOGPT_DEFAULT_MODEL_ID = "zai-org/glm-4.7"; +export const NANOGPT_DEFAULT_MODEL_REF = `nanogpt/${NANOGPT_DEFAULT_MODEL_ID}`; +export const NANOGPT_DEFAULT_COST = { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, +}; + +export const NANOGPT_MODEL_CATALOG = [ + { + id: NANOGPT_DEFAULT_MODEL_ID, + name: "GLM 4.7", + reasoning: true, + input: ["text"], + contextWindow: 200000, + maxTokens: 65535, + }, + { + id: "zai-org/glm-4.7:thinking", + name: "GLM 4.7 Thinking", + reasoning: true, + input: ["text"], + contextWindow: 200000, + maxTokens: 65535, + }, + { + id: "zai-org/glm-4.7-original", + name: "GLM 4.7 Original", + reasoning: true, + input: ["text"], + contextWindow: 200000, + maxTokens: 65535, + }, + { + id: "zai-org/glm-4.7-original:thinking", + name: "GLM 4.7 Original Thinking", + reasoning: true, + input: ["text"], + contextWindow: 200000, + maxTokens: 65535, + }, + { + id: "zai-org/glm-4.7-flash", + name: "GLM 4.7 Flash", + reasoning: true, + input: ["text"], + contextWindow: 200000, + maxTokens: 128000, + }, + { + id: "zai-org/glm-4.7-flash:thinking", + name: "GLM 4.7 Flash Thinking", + reasoning: true, + input: ["text"], + contextWindow: 200000, + maxTokens: 128000, + }, + { + id: "zai-org/glm-4.7-flash-original", + name: "GLM 4.7 Flash Original", + reasoning: true, + input: ["text"], + contextWindow: 200000, + maxTokens: 128000, + }, + { + id: "zai-org/glm-4.7-flash-original:thinking", + name: "GLM 4.7 Flash Original Thinking", + reasoning: true, + input: ["text"], + contextWindow: 200000, + maxTokens: 128000, + }, + { + id: "minimax/minimax-m2.1", + name: "MiniMax M2.1", + reasoning: true, + input: ["text"], + contextWindow: 200000, + maxTokens: 131072, + }, + { + id: "Qwen/Qwen3-VL-235B-A22B-Instruct", + name: "Qwen3 VL 235B A22B Instruct", + reasoning: false, + input: ["text", "image"], + contextWindow: 128000, + maxTokens: 262144, + }, + { + id: "claude-opus-4-5-20251101", + name: "Claude 4.5 Opus", + reasoning: true, + input: ["text", "image"], + contextWindow: 200000, + maxTokens: 32000, + }, + { + id: "claude-opus-4-5-20251101:thinking", + name: "Claude 4.5 Opus Thinking", + reasoning: true, + input: ["text", "image"], + contextWindow: 200000, + maxTokens: 32000, + }, + { + id: "claude-sonnet-4-5-20250929", + name: "Claude Sonnet 4.5", + reasoning: false, + input: ["text", "image"], + contextWindow: 1000000, + maxTokens: 64000, + }, + { + id: "claude-sonnet-4-5-20250929-thinking", + name: "Claude Sonnet 4.5 Thinking", + reasoning: true, + input: ["text", "image"], + contextWindow: 1000000, + maxTokens: 64000, + }, + { + id: "claude-haiku-4-5-20251001", + name: "Claude Haiku 4.5", + reasoning: false, + input: ["text", "image"], + contextWindow: 200000, + maxTokens: 64000, + }, + { + id: "claude-3-5-haiku-20241022", + name: "Claude 3.5 Haiku", + reasoning: false, + input: ["text", "image"], + contextWindow: 200000, + maxTokens: 8192, + }, + { + id: "openai/gpt-5.2-chat", + name: "GPT 5.2 Chat", + reasoning: true, + input: ["text", "image"], + contextWindow: 400000, + maxTokens: 16384, + }, + { + id: "openai/gpt-5.2", + name: "GPT 5.2", + reasoning: true, + input: ["text", "image"], + contextWindow: 400000, + maxTokens: 128000, + }, + { + id: "openai/gpt-5.2-codex", + name: "GPT 5.2 Codex", + reasoning: true, + input: ["text", "image"], + contextWindow: 400000, + maxTokens: 128000, + }, + { + id: "openai/gpt-5.2-pro", + name: "GPT 5.2 Pro", + reasoning: true, + input: ["text", "image"], + contextWindow: 400000, + maxTokens: 128000, + }, +] as const; + +export type NanoGptCatalogEntry = (typeof NANOGPT_MODEL_CATALOG)[number]; + +export function buildNanoGptModelDefinition(entry: NanoGptCatalogEntry): ModelDefinitionConfig { + return { + id: entry.id, + name: entry.name, + reasoning: entry.reasoning, + input: [...entry.input], + cost: NANOGPT_DEFAULT_COST, + contextWindow: entry.contextWindow, + maxTokens: entry.maxTokens, + }; +} diff --git a/src/cli/program.smoke.test.ts b/src/cli/program.smoke.test.ts index f6b155554..ba6c965b9 100644 --- a/src/cli/program.smoke.test.ts +++ b/src/cli/program.smoke.test.ts @@ -176,6 +176,12 @@ describe("cli program (smoke)", () => { key: "sk-synthetic-test", field: "syntheticApiKey", }, + { + authChoice: "nanogpt-api-key", + flag: "--nanogpt-api-key", + key: "sk-nanogpt-test", + field: "nanogptApiKey", + }, { authChoice: "zai-api-key", flag: "--zai-api-key", diff --git a/src/cli/program/register.onboard.ts b/src/cli/program/register.onboard.ts index 281464b6f..c79c550f3 100644 --- a/src/cli/program/register.onboard.ts +++ b/src/cli/program/register.onboard.ts @@ -52,7 +52,7 @@ export function registerOnboardCommand(program: Command) { .option("--mode ", "Wizard mode: local|remote") .option( "--auth-choice ", - "Auth: setup-token|claude-cli|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|codex-cli|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip", + "Auth: setup-token|claude-cli|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|nanogpt-api-key|codex-cli|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip", ) .option( "--token-provider ", @@ -74,6 +74,7 @@ export function registerOnboardCommand(program: Command) { .option("--zai-api-key ", "Z.AI API key") .option("--minimax-api-key ", "MiniMax API key") .option("--synthetic-api-key ", "Synthetic API key") + .option("--nanogpt-api-key ", "NanoGPT API key") .option("--opencode-zen-api-key ", "OpenCode Zen API key") .option("--gateway-port ", "Gateway port") .option("--gateway-bind ", "Gateway bind: loopback|tailnet|lan|auto|custom") @@ -123,6 +124,7 @@ export function registerOnboardCommand(program: Command) { zaiApiKey: opts.zaiApiKey as string | undefined, minimaxApiKey: opts.minimaxApiKey as string | undefined, syntheticApiKey: opts.syntheticApiKey as string | undefined, + nanogptApiKey: opts.nanogptApiKey as string | undefined, opencodeZenApiKey: opts.opencodeZenApiKey as string | undefined, gatewayPort: typeof gatewayPort === "number" && Number.isFinite(gatewayPort) diff --git a/src/commands/auth-choice-options.test.ts b/src/commands/auth-choice-options.test.ts index db529761f..a9a661cd9 100644 --- a/src/commands/auth-choice-options.test.ts +++ b/src/commands/auth-choice-options.test.ts @@ -127,6 +127,18 @@ describe("buildAuthChoiceOptions", () => { expect(options.some((opt) => opt.value === "synthetic-api-key")).toBe(true); }); + it("includes NanoGPT auth choice", () => { + const store: AuthProfileStore = { version: 1, profiles: {} }; + const options = buildAuthChoiceOptions({ + store, + includeSkip: false, + includeClaudeCliIfMissing: true, + platform: "darwin", + }); + + expect(options.some((opt) => opt.value === "nanogpt-api-key")).toBe(true); + }); + it("includes Chutes OAuth auth choice", () => { const store: AuthProfileStore = { version: 1, profiles: {} }; const options = buildAuthChoiceOptions({ diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index f13eef365..e0af2ea01 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -21,6 +21,7 @@ export type AuthChoiceGroupId = | "opencode-zen" | "minimax" | "synthetic" + | "nanogpt" | "venice" | "qwen"; @@ -67,6 +68,12 @@ const AUTH_CHOICE_GROUP_DEFS: { hint: "Anthropic-compatible (multi-model)", choices: ["synthetic-api-key"], }, + { + value: "nanogpt", + label: "NanoGPT", + hint: "OpenAI-compatible API", + choices: ["nanogpt-api-key"], + }, { value: "venice", label: "Venice AI", @@ -202,6 +209,10 @@ export function buildAuthChoiceOptions(params: { label: "Venice AI API key", hint: "Privacy-focused inference (uncensored models)", }); + options.push({ + value: "nanogpt-api-key", + label: "NanoGPT API key", + }); options.push({ value: "github-copilot", label: "GitHub Copilot (GitHub device login)", diff --git a/src/commands/auth-choice.apply.api-providers.ts b/src/commands/auth-choice.apply.api-providers.ts index 8be02008b..8c688703b 100644 --- a/src/commands/auth-choice.apply.api-providers.ts +++ b/src/commands/auth-choice.apply.api-providers.ts @@ -17,6 +17,8 @@ import { applyKimiCodeProviderConfig, applyMoonshotConfig, applyMoonshotProviderConfig, + applyNanoGptConfig, + applyNanoGptProviderConfig, applyOpencodeZenConfig, applyOpencodeZenProviderConfig, applyOpenrouterConfig, @@ -32,6 +34,7 @@ import { MOONSHOT_DEFAULT_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF, SYNTHETIC_DEFAULT_MODEL_REF, + NANOGPT_DEFAULT_MODEL_REF, VENICE_DEFAULT_MODEL_REF, VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, setGeminiApiKey, @@ -39,6 +42,7 @@ import { setMoonshotApiKey, setOpencodeZenApiKey, setOpenrouterApiKey, + setNanoGptApiKey, setSyntheticApiKey, setVeniceApiKey, setVercelAiGatewayApiKey, @@ -83,6 +87,11 @@ export async function applyAuthChoiceApiProviders( authChoice = "synthetic-api-key"; } else if (params.opts.tokenProvider === "venice") { authChoice = "venice-api-key"; + } else if ( + params.opts.tokenProvider === "nanogpt" || + params.opts.tokenProvider === "nano-gpt" + ) { + authChoice = "nanogpt-api-key"; } else if (params.opts.tokenProvider === "opencode") { authChoice = "opencode-zen"; } @@ -463,6 +472,59 @@ export async function applyAuthChoiceApiProviders( return { config: nextConfig, agentModelOverride }; } + if (authChoice === "nanogpt-api-key") { + let hasCredential = false; + + if (!hasCredential && params.opts?.token && params.opts?.tokenProvider) { + const provider = params.opts.tokenProvider; + if (provider === "nanogpt" || provider === "nano-gpt") { + await setNanoGptApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); + hasCredential = true; + } + } + + const envKey = resolveEnvApiKey("nanogpt"); + if (!hasCredential && envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing NANOGPT_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setNanoGptApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } + } + + if (!hasCredential) { + const key = await params.prompter.text({ + message: "Enter NanoGPT API key", + validate: validateApiKeyInput, + }); + await setNanoGptApiKey(normalizeApiKeyInput(String(key)), params.agentDir); + } + + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "nanogpt:default", + provider: "nanogpt", + mode: "api_key", + }); + { + const applied = await applyDefaultModelChoice({ + config: nextConfig, + setDefaultModel: params.setDefaultModel, + defaultModel: NANOGPT_DEFAULT_MODEL_REF, + applyDefaultConfig: applyNanoGptConfig, + applyProviderConfig: applyNanoGptProviderConfig, + noteDefault: NANOGPT_DEFAULT_MODEL_REF, + noteAgentModel, + prompter: params.prompter, + }); + nextConfig = applied.config; + agentModelOverride = applied.agentModelOverride ?? agentModelOverride; + } + return { config: nextConfig, agentModelOverride }; + } + if (authChoice === "venice-api-key") { let hasCredential = false; diff --git a/src/commands/auth-choice.preferred-provider.ts b/src/commands/auth-choice.preferred-provider.ts index 6fe26b59a..c68effd25 100644 --- a/src/commands/auth-choice.preferred-provider.ts +++ b/src/commands/auth-choice.preferred-provider.ts @@ -20,6 +20,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial> = { "zai-api-key": "zai", "synthetic-api-key": "synthetic", "venice-api-key": "venice", + "nanogpt-api-key": "nanogpt", "github-copilot": "github-copilot", "copilot-proxy": "copilot-proxy", "minimax-cloud": "minimax", diff --git a/src/commands/onboard-auth.config-core.ts b/src/commands/onboard-auth.config-core.ts index 0d3a8523a..12e2f961e 100644 --- a/src/commands/onboard-auth.config-core.ts +++ b/src/commands/onboard-auth.config-core.ts @@ -10,6 +10,12 @@ import { VENICE_DEFAULT_MODEL_REF, VENICE_MODEL_CATALOG, } from "../agents/venice-models.js"; +import { + buildNanoGptModelDefinition, + NANOGPT_BASE_URL, + NANOGPT_DEFAULT_MODEL_REF, + NANOGPT_MODEL_CATALOG, +} from "../agents/nanogpt-models.js"; import type { ClawdbotConfig } from "../config/config.js"; import { OPENROUTER_DEFAULT_MODEL_REF, @@ -336,6 +342,81 @@ export function applySyntheticConfig(cfg: ClawdbotConfig): ClawdbotConfig { }; } +/** + * Apply NanoGPT provider configuration without changing the default model. + */ +export function applyNanoGptProviderConfig(cfg: ClawdbotConfig): ClawdbotConfig { + const models = { ...cfg.agents?.defaults?.models }; + models[NANOGPT_DEFAULT_MODEL_REF] = { + ...models[NANOGPT_DEFAULT_MODEL_REF], + alias: models[NANOGPT_DEFAULT_MODEL_REF]?.alias ?? "GLM 4.7", + }; + + const providers = { ...cfg.models?.providers }; + const existingProvider = providers.nanogpt; + const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : []; + const nanogptModels = NANOGPT_MODEL_CATALOG.map(buildNanoGptModelDefinition); + const mergedModels = [ + ...existingModels, + ...nanogptModels.filter( + (model) => !existingModels.some((existing) => existing.id === model.id), + ), + ]; + const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record< + string, + unknown + > as { apiKey?: string }; + const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined; + const normalizedApiKey = resolvedApiKey?.trim(); + providers.nanogpt = { + ...existingProviderRest, + baseUrl: NANOGPT_BASE_URL, + api: "openai-completions", + ...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}), + models: mergedModels.length > 0 ? mergedModels : nanogptModels, + }; + + return { + ...cfg, + agents: { + ...cfg.agents, + defaults: { + ...cfg.agents?.defaults, + models, + }, + }, + models: { + mode: cfg.models?.mode ?? "merge", + providers, + }, + }; +} + +/** + * Apply NanoGPT provider configuration AND set NanoGPT as the default model. + */ +export function applyNanoGptConfig(cfg: ClawdbotConfig): ClawdbotConfig { + const next = applyNanoGptProviderConfig(cfg); + const existingModel = next.agents?.defaults?.model; + return { + ...next, + agents: { + ...next.agents, + defaults: { + ...next.agents?.defaults, + model: { + ...(existingModel && "fallbacks" in (existingModel as Record) + ? { + fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks, + } + : undefined), + primary: NANOGPT_DEFAULT_MODEL_REF, + }, + }, + }, + }; +} + /** * Apply Venice provider configuration without changing the default model. * Registers Venice models and sets up the provider, but preserves existing model selection. diff --git a/src/commands/onboard-auth.credentials.ts b/src/commands/onboard-auth.credentials.ts index 0c7dff409..9faa1235a 100644 --- a/src/commands/onboard-auth.credentials.ts +++ b/src/commands/onboard-auth.credentials.ts @@ -99,6 +99,19 @@ export async function setSyntheticApiKey(key: string, agentDir?: string) { }); } +export async function setNanoGptApiKey(key: string, agentDir?: string) { + // Write to resolved agent dir so gateway finds credentials on startup. + upsertAuthProfile({ + profileId: "nanogpt:default", + credential: { + type: "api_key", + provider: "nanogpt", + key, + }, + agentDir: resolveAuthAgentDir(agentDir), + }); +} + export async function setVeniceApiKey(key: string, agentDir?: string) { // Write to resolved agent dir so gateway finds credentials on startup. upsertAuthProfile({ diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index b122d89cf..572f81024 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -2,6 +2,7 @@ export { SYNTHETIC_DEFAULT_MODEL_ID, SYNTHETIC_DEFAULT_MODEL_REF, } from "../agents/synthetic-models.js"; +export { NANOGPT_DEFAULT_MODEL_ID, NANOGPT_DEFAULT_MODEL_REF } from "../agents/nanogpt-models.js"; export { VENICE_DEFAULT_MODEL_ID, VENICE_DEFAULT_MODEL_REF } from "../agents/venice-models.js"; export { applyAuthProfileConfig, @@ -11,6 +12,8 @@ export { applyMoonshotProviderConfig, applyOpenrouterConfig, applyOpenrouterProviderConfig, + applyNanoGptConfig, + applyNanoGptProviderConfig, applySyntheticConfig, applySyntheticProviderConfig, applyVeniceConfig, @@ -41,6 +44,7 @@ export { setMoonshotApiKey, setOpencodeZenApiKey, setOpenrouterApiKey, + setNanoGptApiKey, setSyntheticApiKey, setVeniceApiKey, setVercelAiGatewayApiKey, diff --git a/src/commands/onboard-non-interactive/local/auth-choice.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts index 6762fb7d2..9015c8628 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.ts @@ -17,6 +17,7 @@ import { applyMinimaxApiConfig, applyMinimaxConfig, applyMoonshotConfig, + applyNanoGptConfig, applyOpencodeZenConfig, applyOpenrouterConfig, applySyntheticConfig, @@ -27,6 +28,7 @@ import { setKimiCodeApiKey, setMinimaxApiKey, setMoonshotApiKey, + setNanoGptApiKey, setOpencodeZenApiKey, setOpenrouterApiKey, setSyntheticApiKey, @@ -272,6 +274,25 @@ export async function applyNonInteractiveAuthChoice(params: { return applySyntheticConfig(nextConfig); } + if (authChoice === "nanogpt-api-key") { + const resolved = await resolveNonInteractiveApiKey({ + provider: "nanogpt", + cfg: baseConfig, + flagValue: opts.nanogptApiKey, + flagName: "--nanogpt-api-key", + envVar: "NANOGPT_API_KEY", + runtime, + }); + if (!resolved) return null; + if (resolved.source !== "profile") await setNanoGptApiKey(resolved.key); + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "nanogpt:default", + provider: "nanogpt", + mode: "api_key", + }); + return applyNanoGptConfig(nextConfig); + } + if ( authChoice === "minimax-cloud" || authChoice === "minimax-api" || diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index 84c15afc4..55a21553c 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -16,6 +16,7 @@ export type AuthChoice = | "moonshot-api-key" | "kimi-code-api-key" | "synthetic-api-key" + | "nanogpt-api-key" | "venice-api-key" | "codex-cli" | "apiKey" @@ -69,6 +70,7 @@ export type OnboardOptions = { zaiApiKey?: string; minimaxApiKey?: string; syntheticApiKey?: string; + nanogptApiKey?: string; veniceApiKey?: string; opencodeZenApiKey?: string; gatewayPort?: number;