This commit is contained in:
sirouk 2026-01-30 20:45:14 +05:00 committed by GitHub
commit 83e4028d66
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 754 additions and 11 deletions

View File

@ -297,7 +297,7 @@ Options:
- `--non-interactive`
- `--mode <local|remote>`
- `--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|chutes-api-key|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>`
- `--token-provider <id>` (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`)
@ -307,6 +307,7 @@ Options:
- `--openrouter-api-key <key>`
- `--ai-gateway-api-key <key>`
- `--moonshot-api-key <key>`
- `--chutes-api-key <key>`
- `--kimi-code-api-key <key>`
- `--gemini-api-key <key>`
- `--zai-api-key <key>`

182
docs/providers/chutes.md Normal file
View File

@ -0,0 +1,182 @@
---
summary: "Use Chutes AI with OpenClaw"
read_when:
- You want to use Chutes AI models in OpenClaw
- You need to configure Chutes via OAuth or API key
---
# Chutes AI
Chutes provides high-performance inference for open-weight models, including GLM 4.7 Flash. OpenClaw supports Chutes via both OAuth and API key authentication.
Models are fetched dynamically from the Chutes API, ensuring you always have access to the latest models, accurate pricing, and context window limits.
## Why Chutes in OpenClaw
- **High Performance**: Optimized inference for top-tier open-weight models.
- **Trusted Execution Environment (TEE)**: Run models in a secure, verifiable enclave for maximum privacy.
- **Dynamic Discovery**: Automatic access to new models as they are released on Chutes.
- **OpenAI-compatible**: Standard `/v1` endpoints for seamless integration.
## Features
- **OAuth + API Key**: Multiple ways to authenticate based on your needs.
- **TEE Filtering**: Easily filter for models running in a Trusted Execution Environment.
- **Tool Calling**: Support for function calling on major models like Qwen 3 and DeepSeek V3.
- **Streaming**: ✅ Full streaming support for all models.
## CLI setup
To configure Chutes with an API key:
```bash
openclaw onboard --auth-choice chutes-api-key
```
To configure Chutes with OAuth (browser-based):
```bash
openclaw onboard --auth-choice chutes
```
**Non-interactive setup:**
```bash
openclaw onboard --non-interactive \
--accept-risk \
--auth-choice chutes-api-key \
--chutes-api-key "$CHUTES_API_KEY"
```
## Which Model Should I Use?
| Use Case | Recommended Model | Why |
|----------|-------------------|-----|
| **General chat** | `chutes/zai-org/GLM-4.7-Flash` | Fast, reliable, and the default choice |
| **Best Overall** | `chutes/moonshotai/Kimi-K2.5-TEE` | 1T parameter MoE model; perfect scores in reasoning/ethics benchmarks |
| **TEE Privacy** | `chutes/deepseek-ai/DeepSeek-V3.2-TEE` | Top-tier reasoning in a secure enclave |
| **Complex reasoning** | `chutes/Qwen/Qwen3-235B-A22B-Instruct-2507-TEE` | Massive 235B model with TEE support |
| **Tool calling** | `chutes/chutesai/Mistral-Small-3.1-24B-Instruct-2503` | Excellent tool support and performance |
OAuth allows you to use your Chutes account without manually managing API keys. OpenClaw uses the standard [Sign in with Chutes](https://github.com/chutesai/Sign-in-with-Chutes) flow.
### OAuth Scopes
OpenClaw requests the following scopes by default:
- `openid` (Required for authentication)
- `profile` (Access to username, email, name)
- `chutes:invoke` (Required to make AI API calls on your behalf)
### Custom OAuth App (Advanced)
If you wish to use your own OAuth application instead of the default, set these environment variables before running onboarding:
- `CHUTES_CLIENT_ID`: Your OAuth client ID
- `CHUTES_CLIENT_SECRET`: Your OAuth client secret (if applicable)
- `CHUTES_OAUTH_REDIRECT_URI`: Your redirect URI (default: `http://127.0.0.1:1456/oauth-callback`)
## Config snippet
```json5
{
env: { CHUTES_API_KEY: "sk-..." },
agents: { defaults: { model: { primary: "chutes/zai-org/GLM-4.7-Flash" } } },
models: {
providers: {
chutes: {
baseUrl: "https://llm.chutes.ai/v1",
api: "openai-completions",
apiKey: "${CHUTES_API_KEY}",
teeOnly: false // Set to true to filter models by Trusted Execution Environment
}
}
}
}
```
## Model Discovery
OpenClaw automatically discovers models from the Chutes API when credentials are configured. If the API is unreachable, it falls back to a curated catalog of popular models.
The discovery process:
1. Fetches available models from `https://llm.chutes.ai/v1/models`
2. Merges with local catalog metadata (context windows, capabilities)
3. Applies `teeOnly` filtering if configured
## Available Models
### TEE Models (Trusted Execution Environment)
| Model ID | Name | Context | Features |
|----------|------|---------|----------|
| `moonshotai/Kimi-K2.5-TEE` | Kimi K2.5 | 256k | Vision, tools |
| `deepseek-ai/DeepSeek-V3.2-TEE` | DeepSeek V3.2 | 203k | Reasoning, tools |
| `Qwen/Qwen3-235B-A22B-Instruct-2507-TEE` | Qwen 3 235B | 262k | Tools |
| `mistralai/Mistral-Small-24B-Instruct-2501-TEE` | Mistral Small 24B | 131k | Tools |
### Standard Models
| Model ID | Name | Context | Features |
|----------|------|---------|----------|
| `zai-org/GLM-4.7-Flash` | GLM 4.7 Flash | 128k | Fast, general |
| `chutesai/Mistral-Small-3.1-24B-Instruct-2503` | Mistral Small 3.1 | 131k | Tools |
| `NousResearch/Hermes-4-14B` | Hermes 4 14B | 41k | Tools |
For a full list, see the [Chutes Models API](https://llm.chutes.ai/v1/models).
## Streaming and Tool Support
| Feature | Support |
|---------|---------|
| **Streaming** | ✅ All models |
| **Function calling** | ✅ Most models (Qwen 3, DeepSeek, Mistral, Kimi) |
| **Vision/Images** | ✅ Kimi K2.5 |
| **JSON mode** | ✅ Supported via `response_format` |
## Usage Examples
```bash
# Use default model (GLM 4.7 Flash)
openclaw chat --model chutes/zai-org/GLM-4.7-Flash "Hello!"
# Use Kimi K2.5 TEE (best overall)
openclaw chat --model chutes/moonshotai/Kimi-K2.5-TEE "Explain quantum computing"
# Use DeepSeek V3.2 TEE for reasoning
openclaw chat --model chutes/deepseek-ai/DeepSeek-V3.2-TEE "Solve this logic puzzle..."
# List available Chutes models
openclaw models list | grep chutes
```
## Troubleshooting
### API key not recognized
```bash
echo $CHUTES_API_KEY
openclaw models list | grep chutes
```
Ensure the key is valid and starts with the expected prefix.
### Model not available
The Chutes model catalog updates dynamically. Run `openclaw models list` to see currently available models. Some models may be temporarily offline.
### Connection issues
Chutes API is at `https://llm.chutes.ai/v1`. Ensure your network allows HTTPS connections.
## Notes
- Chutes models use the `chutes/` provider prefix
- Default model: `chutes/zai-org/GLM-4.7-Flash`
- OpenAI-compatible endpoints
- **TEE models** run in a Trusted Execution Environment for maximum privacy; filter with `teeOnly: true`
## Links
- [Chutes AI](https://chutes.ai)
- [Models API](https://llm.chutes.ai/v1/models)
- [Sign in with Chutes](https://github.com/chutesai/Sign-in-with-Chutes)

View File

@ -42,6 +42,7 @@ See [Venice AI](/providers/venice).
- [OpenCode Zen](/providers/opencode)
- [Amazon Bedrock](/bedrock)
- [Z.AI](/providers/zai)
- [Chutes AI](/providers/chutes)
- [Xiaomi](/providers/xiaomi)
- [GLM models](/providers/glm)
- [MiniMax](/providers/minimax)

126
src/agents/chutes-models.ts Normal file
View File

@ -0,0 +1,126 @@
import type { ModelDefinitionConfig } from "../config/types.models.js";
import {
CHUTES_BASE_URL,
CHUTES_DEFAULT_COST,
CHUTES_DEFAULT_MODEL_ID,
CHUTES_DEFAULT_MODEL_REF,
CHUTES_MODEL_CATALOG,
} from "../commands/onboard-auth.models.js";
export {
CHUTES_BASE_URL,
CHUTES_DEFAULT_COST,
CHUTES_DEFAULT_MODEL_ID,
CHUTES_DEFAULT_MODEL_REF,
CHUTES_MODEL_CATALOG,
};
export interface ChutesModelEntry {
id: string;
name?: string;
context_length?: number;
max_output_length?: number;
confidential_compute?: boolean;
pricing?: { prompt: number; completion: number };
supported_features?: string[];
}
export async function fetchChutesModels(): Promise<ChutesModelEntry[]> {
// Skip dynamic fetching in test environments to avoid network issues and timeouts.
if (
process.env.VITEST ||
process.env.NODE_ENV === "test" ||
process.env.OPENCLAW_SKIP_DYNAMIC_MODELS === "1"
) {
return [];
}
try {
const response = await fetch(`${CHUTES_BASE_URL}/models`, {
signal: AbortSignal.timeout(5000),
});
if (!response.ok) {
throw new Error(`Failed to fetch Chutes models: ${response.statusText}`);
}
const data = (await response.json()) as { data: ChutesModelEntry[] };
return data.data || [];
} catch (error) {
console.warn(`[chutes-models] Failed to fetch models: ${String(error)}`);
return [];
}
}
export function mapChutesModelToDefinition(entry: ChutesModelEntry): ModelDefinitionConfig {
return {
id: entry.id,
name: entry.name || entry.id,
reasoning: entry.supported_features?.includes("reasoning") ?? false,
input: ["text"],
cost: {
input: entry.pricing?.prompt ?? 0,
output: entry.pricing?.completion ?? 0,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: entry.context_length || 128000,
maxTokens: entry.max_output_length || 4096,
confidentialCompute: entry.confidential_compute,
};
}
/** Convert a catalog entry to a mutable ModelDefinitionConfig */
function catalogEntryToDefinition(
entry: (typeof CHUTES_MODEL_CATALOG)[number],
): ModelDefinitionConfig {
return {
id: entry.id,
name: entry.name,
reasoning: entry.reasoning,
input: [...entry.input], // spread to make mutable
contextWindow: entry.contextWindow,
maxTokens: entry.maxTokens,
cost: CHUTES_DEFAULT_COST,
confidentialCompute: "confidentialCompute" in entry ? entry.confidentialCompute : undefined,
};
}
export async function discoverChutesModels(opts?: {
teeOnly?: boolean;
}): Promise<ModelDefinitionConfig[]> {
const catalogModels = CHUTES_MODEL_CATALOG.map(catalogEntryToDefinition);
const apiModels = await fetchChutesModels();
if (apiModels.length === 0) {
if (opts?.teeOnly) {
return catalogModels.filter((m) => m.confidentialCompute === true);
}
return catalogModels;
}
// Merge discovered models with catalog metadata
const catalogById = new Map<string, (typeof CHUTES_MODEL_CATALOG)[number]>(
CHUTES_MODEL_CATALOG.map((m) => [m.id, m]),
);
const models: ModelDefinitionConfig[] = [];
for (const apiModel of apiModels) {
const catalogEntry = catalogById.get(apiModel.id);
if (catalogEntry) {
// Use catalog metadata for known models, but respect API's confidential_compute
const def = catalogEntryToDefinition(catalogEntry);
def.confidentialCompute =
apiModel.confidential_compute ??
("confidentialCompute" in catalogEntry ? catalogEntry.confidentialCompute : undefined);
models.push(def);
} else {
// Create definition for newly discovered models not in catalog
models.push(mapChutesModelToDefinition(apiModel));
}
}
let filtered = models;
if (opts?.teeOnly) {
filtered = models.filter((model) => model.confidentialCompute === true);
}
return filtered.length > 0 ? filtered : catalogModels;
}

View File

@ -14,6 +14,8 @@ export type ChutesPkce = { verifier: string; challenge: string };
export type ChutesUserInfo = {
sub?: string;
username?: string;
email?: string;
name?: string;
created_at?: string;
};
@ -130,7 +132,7 @@ export async function exchangeChutesCodeForTokens(params: {
access,
refresh,
expires: coerceExpiresAt(expiresIn, now),
email: info?.username,
email: info?.email || info?.username,
accountId: info?.sub,
clientId: params.app.clientId,
} as unknown as ChutesStoredOAuth;

View File

@ -248,7 +248,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
}
if (normalized === "chutes") {
return pick("CHUTES_OAUTH_TOKEN") ?? pick("CHUTES_API_KEY");
return pick("CHUTES_API_KEY") ?? pick("CHUTES_OAUTH_TOKEN");
}
if (normalized === "zai") {

View File

@ -13,6 +13,7 @@ import {
SYNTHETIC_MODEL_CATALOG,
} from "./synthetic-models.js";
import { discoverVeniceModels, VENICE_BASE_URL } from "./venice-models.js";
import { discoverChutesModels, CHUTES_BASE_URL } from "./chutes-models.js";
type ModelsConfig = NonNullable<OpenClawConfig["models"]>;
export type ProviderConfig = NonNullable<ModelsConfig["providers"]>[string];
@ -379,6 +380,16 @@ async function buildVeniceProvider(): Promise<ProviderConfig> {
};
}
async function buildChutesProvider(opts?: { teeOnly?: boolean }): Promise<ProviderConfig> {
const models = await discoverChutesModels(opts);
return {
baseUrl: CHUTES_BASE_URL,
api: "openai-completions",
models,
teeOnly: opts?.teeOnly,
};
}
async function buildOllamaProvider(): Promise<ProviderConfig> {
const models = await discoverOllamaModels();
return {
@ -390,6 +401,7 @@ async function buildOllamaProvider(): Promise<ProviderConfig> {
export async function resolveImplicitProviders(params: {
agentDir: string;
config?: OpenClawConfig;
}): Promise<ModelsConfig["providers"]> {
const providers: Record<string, ProviderConfig> = {};
const authStore = ensureAuthProfileStore(params.agentDir, {
@ -431,6 +443,15 @@ export async function resolveImplicitProviders(params: {
providers.venice = { ...(await buildVeniceProvider()), apiKey: veniceKey };
}
const chutesKey =
resolveEnvApiKeyVarName("chutes") ??
resolveApiKeyFromProfiles({ provider: "chutes", store: authStore });
if (chutesKey) {
const chutesCfg = params.config?.models?.providers?.chutes;
const teeOnly = chutesCfg?.teeOnly === true;
providers.chutes = { ...(await buildChutesProvider({ teeOnly })), apiKey: chutesKey };
}
const qwenProfiles = listProfilesForProvider(authStore, "qwen-portal");
if (qwenProfiles.length > 0) {
providers["qwen-portal"] = {

View File

@ -80,7 +80,7 @@ export async function ensureOpenClawModelsJson(
const agentDir = agentDirOverride?.trim() ? agentDirOverride.trim() : resolveOpenClawAgentDir();
const explicitProviders = (cfg.models?.providers ?? {}) as Record<string, ProviderConfig>;
const implicitProviders = await resolveImplicitProviders({ agentDir });
const implicitProviders = await resolveImplicitProviders({ agentDir, config: cfg });
const providers: Record<string, ProviderConfig> = mergeProviders({
implicit: implicitProviders,
explicit: explicitProviders,

View File

@ -52,7 +52,7 @@ export function registerOnboardCommand(program: Command) {
.option("--mode <mode>", "Wizard mode: local|remote")
.option(
"--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|chutes-api-key|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",
)
.option(
"--token-provider <id>",
@ -69,6 +69,7 @@ export function registerOnboardCommand(program: Command) {
.option("--openrouter-api-key <key>", "OpenRouter API key")
.option("--ai-gateway-api-key <key>", "Vercel AI Gateway API key")
.option("--moonshot-api-key <key>", "Moonshot API key")
.option("--chutes-api-key <key>", "Chutes API key")
.option("--kimi-code-api-key <key>", "Kimi Code API key")
.option("--gemini-api-key <key>", "Gemini API key")
.option("--zai-api-key <key>", "Z.AI API key")
@ -120,6 +121,7 @@ export function registerOnboardCommand(program: Command) {
openrouterApiKey: opts.openrouterApiKey as string | undefined,
aiGatewayApiKey: opts.aiGatewayApiKey as string | undefined,
moonshotApiKey: opts.moonshotApiKey as string | undefined,
chutesApiKey: opts.chutesApiKey as string | undefined,
kimiCodeApiKey: opts.kimiCodeApiKey as string | undefined,
geminiApiKey: opts.geminiApiKey as string | undefined,
zaiApiKey: opts.zaiApiKey as string | undefined,

View File

@ -85,7 +85,7 @@ describe("buildAuthChoiceOptions", () => {
expect(options.some((opt) => opt.value === "synthetic-api-key")).toBe(true);
});
it("includes Chutes OAuth auth choice", () => {
it("includes Chutes OAuth and API key auth choices", () => {
const store: AuthProfileStore = { version: 1, profiles: {} };
const options = buildAuthChoiceOptions({
store,
@ -93,6 +93,7 @@ describe("buildAuthChoiceOptions", () => {
});
expect(options.some((opt) => opt.value === "chutes")).toBe(true);
expect(options.some((opt) => opt.value === "chutes-api-key")).toBe(true);
});
it("includes Qwen auth choice", () => {

View File

@ -21,6 +21,7 @@ export type AuthChoiceGroupId =
| "minimax"
| "synthetic"
| "venice"
| "chutes"
| "qwen";
export type AuthChoiceGroup = {
@ -54,6 +55,12 @@ const AUTH_CHOICE_GROUP_DEFS: {
hint: "M2.1 (recommended)",
choices: ["minimax-api", "minimax-api-lightning"],
},
{
value: "chutes",
label: "Chutes AI",
hint: "TEE privacy + high-performance open models",
choices: ["chutes", "chutes-api-key"],
},
{
value: "qwen",
label: "Qwen",
@ -139,7 +146,16 @@ export function buildAuthChoiceOptions(params: {
value: "openai-codex",
label: "OpenAI Codex (ChatGPT OAuth)",
});
options.push({ value: "chutes", label: "Chutes (OAuth)" });
options.push({
value: "chutes",
label: "Chutes (OAuth)",
hint: "TEE privacy + high-performance inference",
});
options.push({
value: "chutes-api-key",
label: "Chutes API key",
hint: "TEE privacy + high-performance inference",
});
options.push({ value: "openai-api-key", label: "OpenAI API key" });
options.push({ value: "openrouter-api-key", label: "OpenRouter API key" });
options.push({

View File

@ -13,6 +13,8 @@ import {
} from "./google-gemini-model-default.js";
import {
applyAuthProfileConfig,
applyChutesConfig,
applyChutesProviderConfig,
applyKimiCodeConfig,
applyKimiCodeProviderConfig,
applyMoonshotConfig,
@ -30,12 +32,14 @@ import {
applyXiaomiConfig,
applyXiaomiProviderConfig,
applyZaiConfig,
CHUTES_DEFAULT_MODEL_REF,
KIMI_CODE_MODEL_REF,
MOONSHOT_DEFAULT_MODEL_REF,
OPENROUTER_DEFAULT_MODEL_REF,
SYNTHETIC_DEFAULT_MODEL_REF,
VENICE_DEFAULT_MODEL_REF,
VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
setChutesApiKey,
XIAOMI_DEFAULT_MODEL_REF,
setGeminiApiKey,
setKimiCodeApiKey,
@ -77,6 +81,8 @@ export async function applyAuthChoiceApiProviders(
authChoice = "ai-gateway-api-key";
} else if (params.opts.tokenProvider === "moonshot") {
authChoice = "moonshot-api-key";
} else if (params.opts.tokenProvider === "chutes") {
authChoice = "chutes-api-key";
} else if (params.opts.tokenProvider === "kimi-code") {
authChoice = "kimi-code-api-key";
} else if (params.opts.tokenProvider === "google") {
@ -271,6 +277,53 @@ export async function applyAuthChoiceApiProviders(
return { config: nextConfig, agentModelOverride };
}
if (authChoice === "chutes-api-key") {
let hasCredential = false;
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "chutes") {
await setChutesApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir);
hasCredential = true;
}
const envKey = process.env.CHUTES_API_KEY?.trim();
if (envKey) {
const useExisting = await params.prompter.confirm({
message: `Use existing CHUTES_API_KEY (env, ${formatApiKeyPreview(envKey)})?`,
initialValue: true,
});
if (useExisting) {
await setChutesApiKey(envKey, params.agentDir);
hasCredential = true;
}
}
if (!hasCredential) {
const key = await params.prompter.text({
message: "Enter Chutes API key",
validate: validateApiKeyInput,
});
await setChutesApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "chutes:default",
provider: "chutes",
mode: "api_key",
});
{
const applied = await applyDefaultModelChoice({
config: nextConfig,
setDefaultModel: params.setDefaultModel,
defaultModel: CHUTES_DEFAULT_MODEL_REF,
applyDefaultConfig: applyChutesConfig,
applyProviderConfig: applyChutesProviderConfig,
noteAgentModel,
prompter: params.prompter,
});
nextConfig = applied.config;
agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
}
return { config: nextConfig, agentModelOverride };
}
if (authChoice === "kimi-code-api-key") {
let hasCredential = false;
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "kimi-code") {

View File

@ -13,6 +13,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
"openrouter-api-key": "openrouter",
"ai-gateway-api-key": "vercel-ai-gateway",
"moonshot-api-key": "moonshot",
"chutes-api-key": "chutes",
"kimi-code-api-key": "kimi-code",
"gemini-api-key": "google",
"google-antigravity": "google-antigravity",

View File

@ -6,6 +6,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import type { RuntimeEnv } from "../runtime.js";
import type { WizardPrompter } from "../wizard/prompts.js";
import type { ProviderPlugin } from "../plugins/types.js";
import { applyAuthChoice, resolvePreferredProviderForAuthChoice } from "./auth-choice.js";
import type { AuthChoice } from "./onboard-types.js";
@ -13,7 +14,7 @@ vi.mock("../providers/github-copilot-auth.js", () => ({
githubCopilotLoginCommand: vi.fn(async () => {}),
}));
const resolvePluginProviders = vi.hoisted(() => vi.fn(() => []));
const resolvePluginProviders = vi.hoisted(() => vi.fn(() => [] as ProviderPlugin[]));
vi.mock("../plugins/providers.js", () => ({
resolvePluginProviders,
}));
@ -492,6 +493,117 @@ describe("applyAuthChoice", () => {
});
});
it("prompts and writes Chutes API key when selecting chutes-api-key", async () => {
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-"));
process.env.OPENCLAW_STATE_DIR = tempStateDir;
process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent");
process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR;
const text = vi.fn().mockResolvedValue("sk-chutes-test");
const select: WizardPrompter["select"] = vi.fn(
async (params) => params.options[0]?.value as never,
);
const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []);
const prompter: WizardPrompter = {
intro: vi.fn(noopAsync),
outro: vi.fn(noopAsync),
note: vi.fn(noopAsync),
select,
multiselect,
text,
confirm: vi.fn(async () => false),
progress: vi.fn(() => ({ update: noop, stop: noop })),
};
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn((code: number) => {
throw new Error(`exit:${code}`);
}),
};
const result = await applyAuthChoice({
authChoice: "chutes-api-key",
config: {},
prompter,
runtime,
setDefaultModel: true,
});
expect(text).toHaveBeenCalledWith(expect.objectContaining({ message: "Enter Chutes API key" }));
expect(result.config.auth?.profiles?.["chutes:default"]).toMatchObject({
provider: "chutes",
mode: "api_key",
});
const authProfilePath = authProfilePathFor(requireAgentDir());
const raw = await fs.readFile(authProfilePath, "utf8");
const parsed = JSON.parse(raw) as {
profiles?: Record<string, { key?: string }>;
};
expect(parsed.profiles?.["chutes:default"]?.key).toBe("sk-chutes-test");
});
it("uses existing CHUTES_API_KEY when selecting chutes-api-key", async () => {
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-"));
process.env.OPENCLAW_STATE_DIR = tempStateDir;
process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent");
process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR;
process.env.CHUTES_API_KEY = "sk-chutes-env-test";
const text = vi.fn();
const select: WizardPrompter["select"] = vi.fn(
async (params) => params.options[0]?.value as never,
);
const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []);
const confirm = vi.fn(async () => true);
const prompter: WizardPrompter = {
intro: vi.fn(noopAsync),
outro: vi.fn(noopAsync),
note: vi.fn(noopAsync),
select,
multiselect,
text,
confirm,
progress: vi.fn(() => ({ update: noop, stop: noop })),
};
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn((code: number) => {
throw new Error(`exit:${code}`);
}),
};
const result = await applyAuthChoice({
authChoice: "chutes-api-key",
config: {},
prompter,
runtime,
setDefaultModel: true,
});
expect(confirm).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining("CHUTES_API_KEY"),
}),
);
expect(text).not.toHaveBeenCalled();
expect(result.config.auth?.profiles?.["chutes:default"]).toMatchObject({
provider: "chutes",
mode: "api_key",
});
const authProfilePath = authProfilePathFor(requireAgentDir());
const raw = await fs.readFile(authProfilePath, "utf8");
const parsed = JSON.parse(raw) as {
profiles?: Record<string, { key?: string }>;
};
expect(parsed.profiles?.["chutes:default"]?.key).toBe("sk-chutes-env-test");
delete process.env.CHUTES_API_KEY;
});
it("writes Qwen credentials when selecting qwen-portal", async () => {
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-"));
process.env.OPENCLAW_STATE_DIR = tempStateDir;
@ -512,7 +624,7 @@ describe("applyAuthChoice", () => {
{
profileId: "qwen-portal:default",
credential: {
type: "oauth",
type: "oauth" as const,
provider: "qwen-portal",
access: "access",
refresh: "refresh",
@ -526,7 +638,7 @@ describe("applyAuthChoice", () => {
"qwen-portal": {
baseUrl: "https://portal.qwen.ai/v1",
apiKey: "qwen-oauth",
api: "openai-completions",
api: "openai-completions" as const,
models: [],
},
},

View File

@ -19,8 +19,11 @@ import {
ZAI_DEFAULT_MODEL_REF,
} from "./onboard-auth.credentials.js";
import {
buildChutesModelDefinition,
buildKimiCodeModelDefinition,
buildMoonshotModelDefinition,
CHUTES_BASE_URL,
CHUTES_DEFAULT_MODEL_REF,
KIMI_CODE_BASE_URL,
KIMI_CODE_MODEL_ID,
KIMI_CODE_MODEL_REF,
@ -204,6 +207,67 @@ export function applyMoonshotConfig(cfg: OpenClawConfig): OpenClawConfig {
};
}
export function applyChutesProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
const models = { ...cfg.agents?.defaults?.models };
models[CHUTES_DEFAULT_MODEL_REF] = {
...models[CHUTES_DEFAULT_MODEL_REF],
alias: models[CHUTES_DEFAULT_MODEL_REF]?.alias ?? "GLM 4.7 Flash",
};
const providers = { ...cfg.models?.providers };
const existingProvider = providers.chutes;
const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record<
string,
unknown
> as { apiKey?: string; teeOnly?: boolean };
const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined;
const normalizedApiKey = resolvedApiKey?.trim();
providers.chutes = {
...existingProviderRest,
baseUrl: CHUTES_BASE_URL,
api: "openai-completions",
...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}),
models: existingProvider?.models || [buildChutesModelDefinition()],
};
return {
...cfg,
agents: {
...cfg.agents,
defaults: {
...cfg.agents?.defaults,
models,
},
},
models: {
mode: cfg.models?.mode ?? "merge",
providers,
},
};
}
export function applyChutesConfig(cfg: OpenClawConfig): OpenClawConfig {
const next = applyChutesProviderConfig(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: CHUTES_DEFAULT_MODEL_REF,
},
},
},
};
}
export function applyKimiCodeProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
const models = { ...cfg.agents?.defaults?.models };
models[KIMI_CODE_MODEL_REF] = {

View File

@ -73,6 +73,19 @@ export async function setMoonshotApiKey(key: string, agentDir?: string) {
});
}
export async function setChutesApiKey(key: string, agentDir?: string) {
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({
profileId: "chutes:default",
credential: {
type: "api_key",
provider: "chutes",
key,
},
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setKimiCodeApiKey(key: string, agentDir?: string) {
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({

View File

@ -12,6 +12,11 @@ export const MOONSHOT_DEFAULT_MODEL_ID = "kimi-k2-0905-preview";
export const MOONSHOT_DEFAULT_MODEL_REF = `moonshot/${MOONSHOT_DEFAULT_MODEL_ID}`;
export const MOONSHOT_DEFAULT_CONTEXT_WINDOW = 256000;
export const MOONSHOT_DEFAULT_MAX_TOKENS = 8192;
export const CHUTES_BASE_URL = "https://llm.chutes.ai/v1";
export const CHUTES_DEFAULT_MODEL_ID = "zai-org/GLM-4.7-Flash";
export const CHUTES_DEFAULT_MODEL_REF = `chutes/${CHUTES_DEFAULT_MODEL_ID}`;
export const CHUTES_DEFAULT_CONTEXT_WINDOW = 128000;
export const CHUTES_DEFAULT_MAX_TOKENS = 4096;
export const KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1";
export const KIMI_CODE_MODEL_ID = "kimi-for-coding";
export const KIMI_CODE_MODEL_REF = `kimi-code/${KIMI_CODE_MODEL_ID}`;
@ -45,6 +50,12 @@ export const MOONSHOT_DEFAULT_COST = {
cacheRead: 0,
cacheWrite: 0,
};
export const CHUTES_DEFAULT_COST = {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
};
export const KIMI_CODE_DEFAULT_COST = {
input: 0,
output: 0,
@ -91,6 +102,64 @@ export function buildMinimaxApiModelDefinition(modelId: string): ModelDefinition
});
}
/**
* Complete catalog of popular Chutes AI models.
* This catalog serves as a fallback when the Chutes API is unreachable.
*/
export const CHUTES_MODEL_CATALOG = [
{
id: "zai-org/GLM-4.7-Flash",
name: "GLM 4.7 Flash",
reasoning: false,
input: ["text"],
contextWindow: 128000,
maxTokens: 4096,
},
{
id: "moonshotai/Kimi-K2.5-TEE",
name: "Kimi K2.5 (TEE)",
reasoning: false,
input: ["text", "image"],
contextWindow: 256000,
maxTokens: 8192,
confidentialCompute: true,
},
{
id: "Qwen/Qwen3-235B-A22B-Instruct-2507-TEE",
name: "Qwen 3 235B (Tools, TEE)",
reasoning: false,
input: ["text"],
contextWindow: 262144,
maxTokens: 4096,
confidentialCompute: true,
},
{
id: "deepseek-ai/DeepSeek-V3.2-TEE",
name: "DeepSeek V3.2 (Tools, TEE)",
reasoning: false,
input: ["text"],
contextWindow: 202752,
maxTokens: 4096,
confidentialCompute: true,
},
{
id: "chutesai/Mistral-Small-3.1-24B-Instruct-2503",
name: "Mistral Small 3.1 (Tools)",
reasoning: false,
input: ["text"],
contextWindow: 131072,
maxTokens: 4096,
},
{
id: "NousResearch/Hermes-4-14B",
name: "Hermes 4 14B (Tools)",
reasoning: false,
input: ["text"],
contextWindow: 40960,
maxTokens: 4096,
},
] as const;
export function buildMoonshotModelDefinition(): ModelDefinitionConfig {
return {
id: MOONSHOT_DEFAULT_MODEL_ID,
@ -103,6 +172,29 @@ export function buildMoonshotModelDefinition(): ModelDefinitionConfig {
};
}
export function buildChutesModelDefinition(
modelId: string = CHUTES_DEFAULT_MODEL_ID,
): ModelDefinitionConfig {
const catalogEntry = CHUTES_MODEL_CATALOG.find((m) => m.id === modelId);
if (catalogEntry) {
return {
...catalogEntry,
input: [...catalogEntry.input],
cost: CHUTES_DEFAULT_COST,
};
}
return {
id: modelId,
name: modelId === CHUTES_DEFAULT_MODEL_ID ? "GLM 4.7 Flash" : modelId,
reasoning: false,
input: ["text"],
cost: CHUTES_DEFAULT_COST,
contextWindow: CHUTES_DEFAULT_CONTEXT_WINDOW,
maxTokens: CHUTES_DEFAULT_MAX_TOKENS,
};
}
export function buildKimiCodeModelDefinition(): ModelDefinitionConfig {
return {
id: KIMI_CODE_MODEL_ID,

View File

@ -5,6 +5,8 @@ export {
export { VENICE_DEFAULT_MODEL_ID, VENICE_DEFAULT_MODEL_REF } from "../agents/venice-models.js";
export {
applyAuthProfileConfig,
applyChutesConfig,
applyChutesProviderConfig,
applyKimiCodeConfig,
applyKimiCodeProviderConfig,
applyMoonshotConfig,
@ -37,6 +39,7 @@ export {
export {
OPENROUTER_DEFAULT_MODEL_REF,
setAnthropicApiKey,
setChutesApiKey,
setGeminiApiKey,
setKimiCodeApiKey,
setMinimaxApiKey,
@ -54,10 +57,14 @@ export {
ZAI_DEFAULT_MODEL_REF,
} from "./onboard-auth.credentials.js";
export {
buildChutesModelDefinition,
buildKimiCodeModelDefinition,
buildMinimaxApiModelDefinition,
buildMinimaxModelDefinition,
buildMoonshotModelDefinition,
CHUTES_BASE_URL,
CHUTES_DEFAULT_MODEL_ID,
CHUTES_DEFAULT_MODEL_REF,
DEFAULT_MINIMAX_BASE_URL,
KIMI_CODE_BASE_URL,
KIMI_CODE_MODEL_ID,

View File

@ -8,6 +8,7 @@ import { buildTokenProfileId, validateAnthropicSetupToken } from "../../auth-tok
import { applyGoogleGeminiModelDefault } from "../../google-gemini-model-default.js";
import {
applyAuthProfileConfig,
applyChutesConfig,
applyKimiCodeConfig,
applyMinimaxApiConfig,
applyMinimaxConfig,
@ -20,6 +21,7 @@ import {
applyXiaomiConfig,
applyZaiConfig,
setAnthropicApiKey,
setChutesApiKey,
setGeminiApiKey,
setKimiCodeApiKey,
setMinimaxApiKey,
@ -33,7 +35,7 @@ import {
setZaiApiKey,
} from "../../onboard-auth.js";
import type { AuthChoice, OnboardOptions } from "../../onboard-types.js";
import { resolveNonInteractiveApiKey } from "../api-keys.js";
import { NonInteractiveApiKeySource, resolveNonInteractiveApiKey } from "../api-keys.js";
import { shortenHomePath } from "../../../utils.js";
export async function applyNonInteractiveAuthChoice(params: {
@ -273,6 +275,45 @@ export async function applyNonInteractiveAuthChoice(params: {
return applyMoonshotConfig(nextConfig);
}
if (authChoice === "chutes-api-key") {
let resolvedKey = opts.chutesApiKey?.trim();
let source: NonInteractiveApiKeySource = "flag";
if (!resolvedKey) {
resolvedKey = process.env.CHUTES_API_KEY?.trim();
source = "env";
}
if (!resolvedKey) {
const resolved = await resolveNonInteractiveApiKey({
provider: "chutes",
cfg: baseConfig,
flagValue: opts.chutesApiKey,
flagName: "--chutes-api-key",
envVar: "CHUTES_API_KEY",
runtime,
});
if (!resolved) return null;
if (resolved.source === "env" && !process.env.CHUTES_API_KEY) {
// resolveNonInteractiveApiKey found CHUTES_OAUTH_TOKEN via resolveEnvApiKey
// Skip it for api-key onboarding.
runtime.error("Missing --chutes-api-key (or CHUTES_API_KEY in env).");
runtime.exit(1);
return null;
}
resolvedKey = resolved.key;
source = resolved.source;
}
if (source !== "profile") await setChutesApiKey(resolvedKey);
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "chutes:default",
provider: "chutes",
mode: "api_key",
});
return applyChutesConfig(nextConfig);
}
if (authChoice === "kimi-code-api-key") {
const resolved = await resolveNonInteractiveApiKey({
provider: "kimi-code",

View File

@ -9,6 +9,7 @@ export type AuthChoice =
| "claude-cli"
| "token"
| "chutes"
| "chutes-api-key"
| "openai-codex"
| "openai-api-key"
| "openrouter-api-key"
@ -65,6 +66,7 @@ export type OnboardOptions = {
openrouterApiKey?: string;
aiGatewayApiKey?: string;
moonshotApiKey?: string;
chutesApiKey?: string;
kimiCodeApiKey?: string;
geminiApiKey?: string;
zaiApiKey?: string;

View File

@ -31,6 +31,8 @@ export type ModelDefinitionConfig = {
maxTokens: number;
headers?: Record<string, string>;
compat?: ModelCompatConfig;
/** Chutes-only: indicates the model runs in a Trusted Execution Environment */
confidentialCompute?: boolean;
};
export type ModelProviderConfig = {
@ -41,6 +43,8 @@ export type ModelProviderConfig = {
headers?: Record<string, string>;
authHeader?: boolean;
models: ModelDefinitionConfig[];
/** Chutes-only: filter models by confidential_compute: true */
teeOnly?: boolean;
};
export type BedrockDiscoveryConfig = {

View File

@ -43,6 +43,7 @@ export const ModelDefinitionSchema = z
maxTokens: z.number().positive().optional(),
headers: z.record(z.string(), z.string()).optional(),
compat: ModelCompatSchema,
confidentialCompute: z.boolean().optional(),
})
.strict();
@ -57,6 +58,7 @@ export const ModelProviderSchema = z
headers: z.record(z.string(), z.string()).optional(),
authHeader: z.boolean().optional(),
models: z.array(ModelDefinitionSchema),
teeOnly: z.boolean().optional(),
})
.strict();