Merge b2fa1aa778 into 09be5d45d5
This commit is contained in:
commit
e4257acb3e
@ -297,7 +297,7 @@ Options:
|
|||||||
- `--non-interactive`
|
- `--non-interactive`
|
||||||
- `--mode <local|remote>`
|
- `--mode <local|remote>`
|
||||||
- `--flow <quickstart|advanced|manual>` (manual is an alias for advanced)
|
- `--flow <quickstart|advanced|manual>` (manual is an alias for advanced)
|
||||||
- `--auth-choice <setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip>`
|
- `--auth-choice <setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|nanogpt-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip>`
|
||||||
- `--token-provider <id>` (non-interactive; used with `--auth-choice token`)
|
- `--token-provider <id>` (non-interactive; used with `--auth-choice token`)
|
||||||
- `--token <token>` (non-interactive; used with `--auth-choice token`)
|
- `--token <token>` (non-interactive; used with `--auth-choice token`)
|
||||||
- `--token-profile-id <id>` (non-interactive; default: `<provider>:manual`)
|
- `--token-profile-id <id>` (non-interactive; default: `<provider>:manual`)
|
||||||
@ -308,6 +308,7 @@ Options:
|
|||||||
- `--ai-gateway-api-key <key>`
|
- `--ai-gateway-api-key <key>`
|
||||||
- `--moonshot-api-key <key>`
|
- `--moonshot-api-key <key>`
|
||||||
- `--kimi-code-api-key <key>`
|
- `--kimi-code-api-key <key>`
|
||||||
|
- `--nanogpt-api-key <key>`
|
||||||
- `--gemini-api-key <key>`
|
- `--gemini-api-key <key>`
|
||||||
- `--zai-api-key <key>`
|
- `--zai-api-key <key>`
|
||||||
- `--minimax-api-key <key>`
|
- `--minimax-api-key <key>`
|
||||||
|
|||||||
@ -158,6 +158,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
|
||||||
|
|
||||||
Kimi Code uses a dedicated endpoint and key (separate from Moonshot):
|
Kimi Code uses a dedicated endpoint and key (separate from Moonshot):
|
||||||
|
|||||||
@ -36,6 +36,7 @@ See [Venice AI](/providers/venice).
|
|||||||
- [OpenAI (API + Codex)](/providers/openai)
|
- [OpenAI (API + Codex)](/providers/openai)
|
||||||
- [Anthropic (API + Claude Code CLI)](/providers/anthropic)
|
- [Anthropic (API + Claude Code CLI)](/providers/anthropic)
|
||||||
- [Qwen (OAuth)](/providers/qwen)
|
- [Qwen (OAuth)](/providers/qwen)
|
||||||
|
- [NanoGPT](/providers/nanogpt)
|
||||||
- [OpenRouter](/providers/openrouter)
|
- [OpenRouter](/providers/openrouter)
|
||||||
- [Vercel AI Gateway](/providers/vercel-ai-gateway)
|
- [Vercel AI Gateway](/providers/vercel-ai-gateway)
|
||||||
- [Moonshot AI (Kimi + Kimi Code)](/providers/moonshot)
|
- [Moonshot AI (Kimi + Kimi Code)](/providers/moonshot)
|
||||||
|
|||||||
@ -33,6 +33,7 @@ See [Venice AI](/providers/venice).
|
|||||||
|
|
||||||
- [OpenAI (API + Codex)](/providers/openai)
|
- [OpenAI (API + Codex)](/providers/openai)
|
||||||
- [Anthropic (API + Claude Code CLI)](/providers/anthropic)
|
- [Anthropic (API + Claude Code CLI)](/providers/anthropic)
|
||||||
|
- [NanoGPT](/providers/nanogpt)
|
||||||
- [OpenRouter](/providers/openrouter)
|
- [OpenRouter](/providers/openrouter)
|
||||||
- [Vercel AI Gateway](/providers/vercel-ai-gateway)
|
- [Vercel AI Gateway](/providers/vercel-ai-gateway)
|
||||||
- [Moonshot AI (Kimi + Kimi Code)](/providers/moonshot)
|
- [Moonshot AI (Kimi + Kimi Code)](/providers/moonshot)
|
||||||
|
|||||||
84
docs/providers/nanogpt.md
Normal file
84
docs/providers/nanogpt.md
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
---
|
||||||
|
summary: "Use NanoGPT's OpenAI-compatible API in Moltbot"
|
||||||
|
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. Moltbot registers it as the
|
||||||
|
`nanogpt` provider.
|
||||||
|
|
||||||
|
## Quick setup
|
||||||
|
|
||||||
|
### Option 1: Browser login (recommended)
|
||||||
|
|
||||||
|
Use the device flow to authenticate via your browser:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
moltbot models auth login-nanogpt
|
||||||
|
```
|
||||||
|
|
||||||
|
This opens your browser, you approve access, and Moltbot receives your API key automatically.
|
||||||
|
|
||||||
|
Add `--set-default` to also set NanoGPT as your default model:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
moltbot models auth login-nanogpt --set-default
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: API key
|
||||||
|
|
||||||
|
1) Set `NANOGPT_API_KEY` (or run the wizard below).
|
||||||
|
2) Run onboarding:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
moltbot 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/<modelId>`.
|
||||||
|
- 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).
|
||||||
@ -251,6 +251,17 @@ openclaw onboard --non-interactive \
|
|||||||
--gateway-bind loopback
|
--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:
|
OpenCode Zen example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@ -286,6 +286,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
|
|||||||
venice: "VENICE_API_KEY",
|
venice: "VENICE_API_KEY",
|
||||||
mistral: "MISTRAL_API_KEY",
|
mistral: "MISTRAL_API_KEY",
|
||||||
opencode: "OPENCODE_API_KEY",
|
opencode: "OPENCODE_API_KEY",
|
||||||
|
nanogpt: "NANOGPT_API_KEY",
|
||||||
};
|
};
|
||||||
const envVar = envMap[normalized];
|
const envVar = envMap[normalized];
|
||||||
if (!envVar) return null;
|
if (!envVar) return null;
|
||||||
|
|||||||
@ -29,6 +29,7 @@ export function normalizeProviderId(provider: string): string {
|
|||||||
if (normalized === "z.ai" || normalized === "z-ai") return "zai";
|
if (normalized === "z.ai" || normalized === "z-ai") return "zai";
|
||||||
if (normalized === "opencode-zen") return "opencode";
|
if (normalized === "opencode-zen") return "opencode";
|
||||||
if (normalized === "qwen") return "qwen-portal";
|
if (normalized === "qwen") return "qwen-portal";
|
||||||
|
if (normalized === "nano-gpt") return "nanogpt";
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,11 @@ import {
|
|||||||
SYNTHETIC_MODEL_CATALOG,
|
SYNTHETIC_MODEL_CATALOG,
|
||||||
} from "./synthetic-models.js";
|
} from "./synthetic-models.js";
|
||||||
import { discoverVeniceModels, VENICE_BASE_URL } from "./venice-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<OpenClawConfig["models"]>;
|
type ModelsConfig = NonNullable<OpenClawConfig["models"]>;
|
||||||
export type ProviderConfig = NonNullable<ModelsConfig["providers"]>[string];
|
export type ProviderConfig = NonNullable<ModelsConfig["providers"]>[string];
|
||||||
@ -388,6 +393,14 @@ async function buildOllamaProvider(): Promise<ProviderConfig> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildNanoGptProvider(): ProviderConfig {
|
||||||
|
return {
|
||||||
|
baseUrl: NANOGPT_BASE_URL,
|
||||||
|
api: "openai-completions",
|
||||||
|
models: NANOGPT_MODEL_CATALOG.map(buildNanoGptModelDefinition),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function resolveImplicitProviders(params: {
|
export async function resolveImplicitProviders(params: {
|
||||||
agentDir: string;
|
agentDir: string;
|
||||||
}): Promise<ModelsConfig["providers"]> {
|
}): Promise<ModelsConfig["providers"]> {
|
||||||
@ -439,6 +452,13 @@ export async function resolveImplicitProviders(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nanogptKey =
|
||||||
|
resolveEnvApiKeyVarName("nanogpt") ??
|
||||||
|
resolveApiKeyFromProfiles({ provider: "nanogpt", store: authStore });
|
||||||
|
if (nanogptKey) {
|
||||||
|
providers.nanogpt = { ...buildNanoGptProvider(), apiKey: nanogptKey };
|
||||||
|
}
|
||||||
|
|
||||||
const xiaomiKey =
|
const xiaomiKey =
|
||||||
resolveEnvApiKeyVarName("xiaomi") ??
|
resolveEnvApiKeyVarName("xiaomi") ??
|
||||||
resolveApiKeyFromProfiles({ provider: "xiaomi", store: authStore });
|
resolveApiKeyFromProfiles({ provider: "xiaomi", store: authStore });
|
||||||
|
|||||||
284
src/agents/nanogpt-models.ts
Normal file
284
src/agents/nanogpt-models.ts
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
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: "moonshotai/kimi-k2.5",
|
||||||
|
name: "Kimi K2.5",
|
||||||
|
reasoning: false,
|
||||||
|
input: ["text", "image"],
|
||||||
|
contextWindow: 256000,
|
||||||
|
maxTokens: 65536,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "moonshotai/kimi-k2.5:thinking",
|
||||||
|
name: "Kimi K2.5 Thinking",
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text", "image"],
|
||||||
|
contextWindow: 256000,
|
||||||
|
maxTokens: 65536,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "moonshotai/kimi-k2.5-original",
|
||||||
|
name: "Kimi K2.5 (Official API)",
|
||||||
|
reasoning: false,
|
||||||
|
input: ["text", "image"],
|
||||||
|
contextWindow: 256000,
|
||||||
|
maxTokens: 65536,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "moonshotai/kimi-k2.5-original:thinking",
|
||||||
|
name: "Kimi K2.5 Thinking (Official API)",
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text", "image"],
|
||||||
|
contextWindow: 256000,
|
||||||
|
maxTokens: 65536,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "deepseek-v3.2-original",
|
||||||
|
name: "DeepSeek V3.2 Original",
|
||||||
|
reasoning: false,
|
||||||
|
input: ["text"],
|
||||||
|
contextWindow: 128000,
|
||||||
|
maxTokens: 65536,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "deepseek-v3.2-thinking-original",
|
||||||
|
name: "DeepSeek V3.2 Thinking Original",
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text"],
|
||||||
|
contextWindow: 128000,
|
||||||
|
maxTokens: 65536,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "deepseek-v3.2-speciale-original",
|
||||||
|
name: "DeepSeek V3.2 Speciale Original",
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text"],
|
||||||
|
contextWindow: 128000,
|
||||||
|
maxTokens: 128000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "deepseek-ai/deepseek-v3.2-exp",
|
||||||
|
name: "DeepSeek V3.2 Exp",
|
||||||
|
reasoning: false,
|
||||||
|
input: ["text"],
|
||||||
|
contextWindow: 163840,
|
||||||
|
maxTokens: 65536,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "deepseek-ai/deepseek-v3.2-exp-thinking",
|
||||||
|
name: "DeepSeek V3.2 Exp Thinking",
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text"],
|
||||||
|
contextWindow: 163840,
|
||||||
|
maxTokens: 65536,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "deepseek/deepseek-v3.2-speciale",
|
||||||
|
name: "DeepSeek V3.2 Speciale",
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text"],
|
||||||
|
contextWindow: 163000,
|
||||||
|
maxTokens: 65536,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "deepseek/deepseek-v3.2",
|
||||||
|
name: "DeepSeek V3.2",
|
||||||
|
reasoning: false,
|
||||||
|
input: ["text"],
|
||||||
|
contextWindow: 163000,
|
||||||
|
maxTokens: 65536,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "deepseek/deepseek-v3.2:thinking",
|
||||||
|
name: "DeepSeek V3.2 Thinking",
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text"],
|
||||||
|
contextWindow: 163000,
|
||||||
|
maxTokens: 65536,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ import type { Command } from "commander";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
githubCopilotLoginCommand,
|
githubCopilotLoginCommand,
|
||||||
|
nanogptLoginCommand,
|
||||||
modelsAliasesAddCommand,
|
modelsAliasesAddCommand,
|
||||||
modelsAliasesListCommand,
|
modelsAliasesListCommand,
|
||||||
modelsAliasesRemoveCommand,
|
modelsAliasesRemoveCommand,
|
||||||
@ -363,6 +364,25 @@ export function registerModelsCli(program: Command) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
auth
|
||||||
|
.command("login-nanogpt")
|
||||||
|
.description("Login to NanoGPT via browser device flow (TTY required)")
|
||||||
|
.option("--profile-id <id>", "Auth profile id (default: nanogpt:default)")
|
||||||
|
.option("--yes", "Overwrite existing profile without prompting", false)
|
||||||
|
.option("--set-default", "Set NanoGPT as the default model", false)
|
||||||
|
.action(async (opts) => {
|
||||||
|
await runModelsCommand(async () => {
|
||||||
|
await nanogptLoginCommand(
|
||||||
|
{
|
||||||
|
profileId: opts.profileId as string | undefined,
|
||||||
|
yes: Boolean(opts.yes),
|
||||||
|
setDefault: Boolean(opts.setDefault),
|
||||||
|
},
|
||||||
|
defaultRuntime,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const order = auth.command("order").description("Manage per-agent auth profile order overrides");
|
const order = auth.command("order").description("Manage per-agent auth profile order overrides");
|
||||||
|
|
||||||
order
|
order
|
||||||
|
|||||||
@ -176,6 +176,12 @@ describe("cli program (smoke)", () => {
|
|||||||
key: "sk-synthetic-test",
|
key: "sk-synthetic-test",
|
||||||
field: "syntheticApiKey",
|
field: "syntheticApiKey",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
authChoice: "nanogpt-api-key",
|
||||||
|
flag: "--nanogpt-api-key",
|
||||||
|
key: "sk-nanogpt-test",
|
||||||
|
field: "nanogptApiKey",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
authChoice: "zai-api-key",
|
authChoice: "zai-api-key",
|
||||||
flag: "--zai-api-key",
|
flag: "--zai-api-key",
|
||||||
|
|||||||
@ -52,7 +52,7 @@ export function registerOnboardCommand(program: Command) {
|
|||||||
.option("--mode <mode>", "Wizard mode: local|remote")
|
.option("--mode <mode>", "Wizard mode: local|remote")
|
||||||
.option(
|
.option(
|
||||||
"--auth-choice <choice>",
|
"--auth-choice <choice>",
|
||||||
"Auth: setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|xiaomi-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip",
|
"Auth: setup-token|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|venice-api-key|gemini-api-key|zai-api-key|xiaomi-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip",
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"--token-provider <id>",
|
"--token-provider <id>",
|
||||||
@ -75,6 +75,7 @@ export function registerOnboardCommand(program: Command) {
|
|||||||
.option("--xiaomi-api-key <key>", "Xiaomi API key")
|
.option("--xiaomi-api-key <key>", "Xiaomi API key")
|
||||||
.option("--minimax-api-key <key>", "MiniMax API key")
|
.option("--minimax-api-key <key>", "MiniMax API key")
|
||||||
.option("--synthetic-api-key <key>", "Synthetic API key")
|
.option("--synthetic-api-key <key>", "Synthetic API key")
|
||||||
|
.option("--nanogpt-api-key <key>", "NanoGPT API key")
|
||||||
.option("--venice-api-key <key>", "Venice API key")
|
.option("--venice-api-key <key>", "Venice API key")
|
||||||
.option("--opencode-zen-api-key <key>", "OpenCode Zen API key")
|
.option("--opencode-zen-api-key <key>", "OpenCode Zen API key")
|
||||||
.option("--gateway-port <port>", "Gateway port")
|
.option("--gateway-port <port>", "Gateway port")
|
||||||
@ -126,6 +127,7 @@ export function registerOnboardCommand(program: Command) {
|
|||||||
xiaomiApiKey: opts.xiaomiApiKey as string | undefined,
|
xiaomiApiKey: opts.xiaomiApiKey as string | undefined,
|
||||||
minimaxApiKey: opts.minimaxApiKey as string | undefined,
|
minimaxApiKey: opts.minimaxApiKey as string | undefined,
|
||||||
syntheticApiKey: opts.syntheticApiKey as string | undefined,
|
syntheticApiKey: opts.syntheticApiKey as string | undefined,
|
||||||
|
nanogptApiKey: opts.nanogptApiKey as string | undefined,
|
||||||
veniceApiKey: opts.veniceApiKey as string | undefined,
|
veniceApiKey: opts.veniceApiKey as string | undefined,
|
||||||
opencodeZenApiKey: opts.opencodeZenApiKey as string | undefined,
|
opencodeZenApiKey: opts.opencodeZenApiKey as string | undefined,
|
||||||
gatewayPort:
|
gatewayPort:
|
||||||
|
|||||||
@ -85,6 +85,18 @@ describe("buildAuthChoiceOptions", () => {
|
|||||||
expect(options.some((opt) => opt.value === "synthetic-api-key")).toBe(true);
|
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", () => {
|
it("includes Chutes OAuth auth choice", () => {
|
||||||
const store: AuthProfileStore = { version: 1, profiles: {} };
|
const store: AuthProfileStore = { version: 1, profiles: {} };
|
||||||
const options = buildAuthChoiceOptions({
|
const options = buildAuthChoiceOptions({
|
||||||
|
|||||||
@ -20,6 +20,7 @@ export type AuthChoiceGroupId =
|
|||||||
| "opencode-zen"
|
| "opencode-zen"
|
||||||
| "minimax"
|
| "minimax"
|
||||||
| "synthetic"
|
| "synthetic"
|
||||||
|
| "nanogpt"
|
||||||
| "venice"
|
| "venice"
|
||||||
| "qwen";
|
| "qwen";
|
||||||
|
|
||||||
@ -66,6 +67,12 @@ const AUTH_CHOICE_GROUP_DEFS: {
|
|||||||
hint: "Anthropic-compatible (multi-model)",
|
hint: "Anthropic-compatible (multi-model)",
|
||||||
choices: ["synthetic-api-key"],
|
choices: ["synthetic-api-key"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: "nanogpt",
|
||||||
|
label: "NanoGPT",
|
||||||
|
hint: "OpenAI-compatible API",
|
||||||
|
choices: ["nanogpt-api-key"],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: "venice",
|
value: "venice",
|
||||||
label: "Venice AI",
|
label: "Venice AI",
|
||||||
@ -154,6 +161,10 @@ export function buildAuthChoiceOptions(params: {
|
|||||||
label: "Venice AI API key",
|
label: "Venice AI API key",
|
||||||
hint: "Privacy-focused inference (uncensored models)",
|
hint: "Privacy-focused inference (uncensored models)",
|
||||||
});
|
});
|
||||||
|
options.push({
|
||||||
|
value: "nanogpt-api-key",
|
||||||
|
label: "NanoGPT API key",
|
||||||
|
});
|
||||||
options.push({
|
options.push({
|
||||||
value: "github-copilot",
|
value: "github-copilot",
|
||||||
label: "GitHub Copilot (GitHub device login)",
|
label: "GitHub Copilot (GitHub device login)",
|
||||||
|
|||||||
@ -17,6 +17,8 @@ import {
|
|||||||
applyKimiCodeProviderConfig,
|
applyKimiCodeProviderConfig,
|
||||||
applyMoonshotConfig,
|
applyMoonshotConfig,
|
||||||
applyMoonshotProviderConfig,
|
applyMoonshotProviderConfig,
|
||||||
|
applyNanoGptConfig,
|
||||||
|
applyNanoGptProviderConfig,
|
||||||
applyOpencodeZenConfig,
|
applyOpencodeZenConfig,
|
||||||
applyOpencodeZenProviderConfig,
|
applyOpencodeZenProviderConfig,
|
||||||
applyOpenrouterConfig,
|
applyOpenrouterConfig,
|
||||||
@ -34,6 +36,7 @@ import {
|
|||||||
MOONSHOT_DEFAULT_MODEL_REF,
|
MOONSHOT_DEFAULT_MODEL_REF,
|
||||||
OPENROUTER_DEFAULT_MODEL_REF,
|
OPENROUTER_DEFAULT_MODEL_REF,
|
||||||
SYNTHETIC_DEFAULT_MODEL_REF,
|
SYNTHETIC_DEFAULT_MODEL_REF,
|
||||||
|
NANOGPT_DEFAULT_MODEL_REF,
|
||||||
VENICE_DEFAULT_MODEL_REF,
|
VENICE_DEFAULT_MODEL_REF,
|
||||||
VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
|
VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
|
||||||
XIAOMI_DEFAULT_MODEL_REF,
|
XIAOMI_DEFAULT_MODEL_REF,
|
||||||
@ -42,6 +45,7 @@ import {
|
|||||||
setMoonshotApiKey,
|
setMoonshotApiKey,
|
||||||
setOpencodeZenApiKey,
|
setOpencodeZenApiKey,
|
||||||
setOpenrouterApiKey,
|
setOpenrouterApiKey,
|
||||||
|
setNanoGptApiKey,
|
||||||
setSyntheticApiKey,
|
setSyntheticApiKey,
|
||||||
setVeniceApiKey,
|
setVeniceApiKey,
|
||||||
setVercelAiGatewayApiKey,
|
setVercelAiGatewayApiKey,
|
||||||
@ -89,6 +93,11 @@ export async function applyAuthChoiceApiProviders(
|
|||||||
authChoice = "synthetic-api-key";
|
authChoice = "synthetic-api-key";
|
||||||
} else if (params.opts.tokenProvider === "venice") {
|
} else if (params.opts.tokenProvider === "venice") {
|
||||||
authChoice = "venice-api-key";
|
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") {
|
} else if (params.opts.tokenProvider === "opencode") {
|
||||||
authChoice = "opencode-zen";
|
authChoice = "opencode-zen";
|
||||||
}
|
}
|
||||||
@ -517,6 +526,59 @@ export async function applyAuthChoiceApiProviders(
|
|||||||
return { config: nextConfig, agentModelOverride };
|
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") {
|
if (authChoice === "venice-api-key") {
|
||||||
let hasCredential = false;
|
let hasCredential = false;
|
||||||
|
|
||||||
|
|||||||
@ -21,6 +21,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
|
|||||||
"xiaomi-api-key": "xiaomi",
|
"xiaomi-api-key": "xiaomi",
|
||||||
"synthetic-api-key": "synthetic",
|
"synthetic-api-key": "synthetic",
|
||||||
"venice-api-key": "venice",
|
"venice-api-key": "venice",
|
||||||
|
"nanogpt-api-key": "nanogpt",
|
||||||
"github-copilot": "github-copilot",
|
"github-copilot": "github-copilot",
|
||||||
"copilot-proxy": "copilot-proxy",
|
"copilot-proxy": "copilot-proxy",
|
||||||
"minimax-cloud": "minimax",
|
"minimax-cloud": "minimax",
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
export { githubCopilotLoginCommand } from "../providers/github-copilot-auth.js";
|
export { githubCopilotLoginCommand } from "../providers/github-copilot-auth.js";
|
||||||
|
export { nanogptLoginCommand } from "../providers/nanogpt-auth.js";
|
||||||
export {
|
export {
|
||||||
modelsAliasesAddCommand,
|
modelsAliasesAddCommand,
|
||||||
modelsAliasesListCommand,
|
modelsAliasesListCommand,
|
||||||
|
|||||||
@ -11,6 +11,12 @@ import {
|
|||||||
VENICE_DEFAULT_MODEL_REF,
|
VENICE_DEFAULT_MODEL_REF,
|
||||||
VENICE_MODEL_CATALOG,
|
VENICE_MODEL_CATALOG,
|
||||||
} from "../agents/venice-models.js";
|
} from "../agents/venice-models.js";
|
||||||
|
import {
|
||||||
|
buildNanoGptModelDefinition,
|
||||||
|
NANOGPT_BASE_URL,
|
||||||
|
NANOGPT_DEFAULT_MODEL_REF,
|
||||||
|
NANOGPT_MODEL_CATALOG,
|
||||||
|
} from "../agents/nanogpt-models.js";
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import {
|
import {
|
||||||
OPENROUTER_DEFAULT_MODEL_REF,
|
OPENROUTER_DEFAULT_MODEL_REF,
|
||||||
@ -409,6 +415,81 @@ export function applyXiaomiConfig(cfg: OpenClawConfig): OpenClawConfig {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply NanoGPT provider configuration without changing the default model.
|
||||||
|
*/
|
||||||
|
export function applyNanoGptProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||||
|
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: OpenClawConfig): OpenClawConfig {
|
||||||
|
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<string, unknown>)
|
||||||
|
? {
|
||||||
|
fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks,
|
||||||
|
}
|
||||||
|
: undefined),
|
||||||
|
primary: NANOGPT_DEFAULT_MODEL_REF,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply Venice provider configuration without changing the default model.
|
* Apply Venice provider configuration without changing the default model.
|
||||||
* Registers Venice models and sets up the provider, but preserves existing model selection.
|
* Registers Venice models and sets up the provider, but preserves existing model selection.
|
||||||
|
|||||||
@ -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) {
|
export async function setVeniceApiKey(key: string, agentDir?: string) {
|
||||||
// Write to resolved agent dir so gateway finds credentials on startup.
|
// Write to resolved agent dir so gateway finds credentials on startup.
|
||||||
upsertAuthProfile({
|
upsertAuthProfile({
|
||||||
|
|||||||
@ -2,6 +2,7 @@ export {
|
|||||||
SYNTHETIC_DEFAULT_MODEL_ID,
|
SYNTHETIC_DEFAULT_MODEL_ID,
|
||||||
SYNTHETIC_DEFAULT_MODEL_REF,
|
SYNTHETIC_DEFAULT_MODEL_REF,
|
||||||
} from "../agents/synthetic-models.js";
|
} 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 { VENICE_DEFAULT_MODEL_ID, VENICE_DEFAULT_MODEL_REF } from "../agents/venice-models.js";
|
||||||
export {
|
export {
|
||||||
applyAuthProfileConfig,
|
applyAuthProfileConfig,
|
||||||
@ -11,6 +12,8 @@ export {
|
|||||||
applyMoonshotProviderConfig,
|
applyMoonshotProviderConfig,
|
||||||
applyOpenrouterConfig,
|
applyOpenrouterConfig,
|
||||||
applyOpenrouterProviderConfig,
|
applyOpenrouterProviderConfig,
|
||||||
|
applyNanoGptConfig,
|
||||||
|
applyNanoGptProviderConfig,
|
||||||
applySyntheticConfig,
|
applySyntheticConfig,
|
||||||
applySyntheticProviderConfig,
|
applySyntheticProviderConfig,
|
||||||
applyVeniceConfig,
|
applyVeniceConfig,
|
||||||
@ -43,6 +46,7 @@ export {
|
|||||||
setMoonshotApiKey,
|
setMoonshotApiKey,
|
||||||
setOpencodeZenApiKey,
|
setOpencodeZenApiKey,
|
||||||
setOpenrouterApiKey,
|
setOpenrouterApiKey,
|
||||||
|
setNanoGptApiKey,
|
||||||
setSyntheticApiKey,
|
setSyntheticApiKey,
|
||||||
setVeniceApiKey,
|
setVeniceApiKey,
|
||||||
setVercelAiGatewayApiKey,
|
setVercelAiGatewayApiKey,
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
applyMinimaxApiConfig,
|
applyMinimaxApiConfig,
|
||||||
applyMinimaxConfig,
|
applyMinimaxConfig,
|
||||||
applyMoonshotConfig,
|
applyMoonshotConfig,
|
||||||
|
applyNanoGptConfig,
|
||||||
applyOpencodeZenConfig,
|
applyOpencodeZenConfig,
|
||||||
applyOpenrouterConfig,
|
applyOpenrouterConfig,
|
||||||
applySyntheticConfig,
|
applySyntheticConfig,
|
||||||
@ -24,6 +25,7 @@ import {
|
|||||||
setKimiCodeApiKey,
|
setKimiCodeApiKey,
|
||||||
setMinimaxApiKey,
|
setMinimaxApiKey,
|
||||||
setMoonshotApiKey,
|
setMoonshotApiKey,
|
||||||
|
setNanoGptApiKey,
|
||||||
setOpencodeZenApiKey,
|
setOpencodeZenApiKey,
|
||||||
setOpenrouterApiKey,
|
setOpenrouterApiKey,
|
||||||
setSyntheticApiKey,
|
setSyntheticApiKey,
|
||||||
@ -311,6 +313,25 @@ export async function applyNonInteractiveAuthChoice(params: {
|
|||||||
return applySyntheticConfig(nextConfig);
|
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 === "venice-api-key") {
|
if (authChoice === "venice-api-key") {
|
||||||
const resolved = await resolveNonInteractiveApiKey({
|
const resolved = await resolveNonInteractiveApiKey({
|
||||||
provider: "venice",
|
provider: "venice",
|
||||||
|
|||||||
@ -16,6 +16,7 @@ export type AuthChoice =
|
|||||||
| "moonshot-api-key"
|
| "moonshot-api-key"
|
||||||
| "kimi-code-api-key"
|
| "kimi-code-api-key"
|
||||||
| "synthetic-api-key"
|
| "synthetic-api-key"
|
||||||
|
| "nanogpt-api-key"
|
||||||
| "venice-api-key"
|
| "venice-api-key"
|
||||||
| "codex-cli"
|
| "codex-cli"
|
||||||
| "apiKey"
|
| "apiKey"
|
||||||
@ -71,6 +72,7 @@ export type OnboardOptions = {
|
|||||||
xiaomiApiKey?: string;
|
xiaomiApiKey?: string;
|
||||||
minimaxApiKey?: string;
|
minimaxApiKey?: string;
|
||||||
syntheticApiKey?: string;
|
syntheticApiKey?: string;
|
||||||
|
nanogptApiKey?: string;
|
||||||
veniceApiKey?: string;
|
veniceApiKey?: string;
|
||||||
opencodeZenApiKey?: string;
|
opencodeZenApiKey?: string;
|
||||||
gatewayPort?: number;
|
gatewayPort?: number;
|
||||||
|
|||||||
185
src/providers/nanogpt-auth.ts
Normal file
185
src/providers/nanogpt-auth.ts
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
import { intro, note, outro, spinner } from "@clack/prompts";
|
||||||
|
|
||||||
|
import { ensureAuthProfileStore, upsertAuthProfile } from "../agents/auth-profiles.js";
|
||||||
|
import { updateConfig } from "../commands/models/shared.js";
|
||||||
|
import {
|
||||||
|
applyAuthProfileConfig,
|
||||||
|
applyNanoGptConfig,
|
||||||
|
setNanoGptApiKey,
|
||||||
|
} from "../commands/onboard-auth.js";
|
||||||
|
import { logConfigUpdated } from "../config/logging.js";
|
||||||
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
import { stylePromptTitle } from "../terminal/prompt-style.js";
|
||||||
|
|
||||||
|
const CLI_LOGIN_START_URL = "https://nano-gpt.com/api/cli-login/start";
|
||||||
|
const CLI_LOGIN_POLL_URL = "https://nano-gpt.com/api/cli-login/poll";
|
||||||
|
const DEFAULT_POLL_INTERVAL_MS = 2000;
|
||||||
|
const CLIENT_NAME = "moltbot";
|
||||||
|
|
||||||
|
type StartResponse = {
|
||||||
|
device_code: string;
|
||||||
|
user_code: string;
|
||||||
|
verification_uri: string;
|
||||||
|
verification_uri_complete: string;
|
||||||
|
expires_in: number;
|
||||||
|
interval?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PollResponse =
|
||||||
|
| { status: "pending" }
|
||||||
|
| { status: "approved"; key: string }
|
||||||
|
| { status: "expired" }
|
||||||
|
| { status: "consumed" };
|
||||||
|
|
||||||
|
async function requestDeviceCode(): Promise<StartResponse> {
|
||||||
|
const res = await fetch(CLI_LOGIN_START_URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ client_name: CLIENT_NAME }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`NanoGPT device code request failed: HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = (await res.json()) as StartResponse;
|
||||||
|
if (!json.device_code || !json.user_code || !json.verification_uri) {
|
||||||
|
throw new Error("NanoGPT device code response missing required fields");
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollForApiKey(params: {
|
||||||
|
deviceCode: string;
|
||||||
|
intervalMs: number;
|
||||||
|
expiresAt: number;
|
||||||
|
}): Promise<string> {
|
||||||
|
while (Date.now() < params.expiresAt) {
|
||||||
|
const res = await fetch(CLI_LOGIN_POLL_URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ device_code: params.deviceCode }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 202: continue polling
|
||||||
|
if (res.status === 202) {
|
||||||
|
await new Promise((r) => setTimeout(r, params.intervalMs));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 200: approved
|
||||||
|
if (res.status === 200) {
|
||||||
|
const json = (await res.json()) as PollResponse;
|
||||||
|
if (json.status === "approved" && "key" in json) {
|
||||||
|
return json.key;
|
||||||
|
}
|
||||||
|
throw new Error("NanoGPT returned 200 but no API key");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 410: expired
|
||||||
|
if (res.status === 410) {
|
||||||
|
throw new Error("NanoGPT device code expired; run login again");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 409: already consumed
|
||||||
|
if (res.status === 409) {
|
||||||
|
throw new Error("NanoGPT device code already used; run login again");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 404: invalid code
|
||||||
|
if (res.status === 404) {
|
||||||
|
throw new Error("NanoGPT device code invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`NanoGPT poll failed: HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("NanoGPT device code expired; run login again");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function nanogptLoginCommand(
|
||||||
|
opts: { profileId?: string; yes?: boolean; setDefault?: boolean },
|
||||||
|
runtime: RuntimeEnv,
|
||||||
|
) {
|
||||||
|
if (!process.stdin.isTTY) {
|
||||||
|
throw new Error("nanogpt login requires an interactive TTY.");
|
||||||
|
}
|
||||||
|
|
||||||
|
intro(stylePromptTitle("NanoGPT login"));
|
||||||
|
|
||||||
|
const profileId = opts.profileId?.trim() || "nanogpt:default";
|
||||||
|
const store = ensureAuthProfileStore(undefined, {
|
||||||
|
allowKeychainPrompt: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (store.profiles[profileId] && !opts.yes) {
|
||||||
|
note(
|
||||||
|
`Auth profile already exists: ${profileId}\nRe-running will overwrite it.`,
|
||||||
|
stylePromptTitle("Existing credentials"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const spin = spinner();
|
||||||
|
spin.start("Requesting device code from NanoGPT...");
|
||||||
|
const device = await requestDeviceCode();
|
||||||
|
spin.stop("Device code ready");
|
||||||
|
|
||||||
|
note(
|
||||||
|
[
|
||||||
|
`Visit: ${device.verification_uri_complete || device.verification_uri}`,
|
||||||
|
`Code: ${device.user_code}`,
|
||||||
|
].join("\n"),
|
||||||
|
stylePromptTitle("Authorize"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const expiresAt = Date.now() + device.expires_in * 1000;
|
||||||
|
const intervalMs = device.interval ? device.interval * 1000 : DEFAULT_POLL_INTERVAL_MS;
|
||||||
|
|
||||||
|
const polling = spinner();
|
||||||
|
polling.start("Waiting for NanoGPT authorization...");
|
||||||
|
const apiKey = await pollForApiKey({
|
||||||
|
deviceCode: device.device_code,
|
||||||
|
intervalMs,
|
||||||
|
expiresAt,
|
||||||
|
});
|
||||||
|
polling.stop("NanoGPT API key acquired");
|
||||||
|
|
||||||
|
// Store the API key
|
||||||
|
await setNanoGptApiKey(apiKey);
|
||||||
|
|
||||||
|
upsertAuthProfile({
|
||||||
|
profileId,
|
||||||
|
credential: {
|
||||||
|
type: "api_key",
|
||||||
|
provider: "nanogpt",
|
||||||
|
key: apiKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateConfig((cfg) => {
|
||||||
|
let next = applyAuthProfileConfig(cfg, {
|
||||||
|
provider: "nanogpt",
|
||||||
|
profileId,
|
||||||
|
mode: "api_key",
|
||||||
|
});
|
||||||
|
if (opts.setDefault) {
|
||||||
|
next = applyNanoGptConfig(next);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
|
logConfigUpdated(runtime);
|
||||||
|
runtime.log(`Auth profile: ${profileId} (nanogpt/api_key)`);
|
||||||
|
|
||||||
|
if (opts.setDefault) {
|
||||||
|
runtime.log("Default model set to nanogpt/zai-org/glm-4.7");
|
||||||
|
}
|
||||||
|
|
||||||
|
outro("Done");
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user