feat: add Chutes AI provider integration
- Add OAuth + API key authentication flows - Dynamic model discovery with TEE filtering and fallback catalog - CLI onboarding: --auth-choice chutes/chutes-api-key, --chutes-api-key flag - Provider config with teeOnly and confidentialCompute support - Comprehensive docs at docs/providers/chutes.md - Tests for auth-choice options and onboarding flows Models include GLM 4.7 Flash, Kimi K2.5-TEE, DeepSeek V3.2-TEE, Qwen 3 235B-TEE, and more via OpenAI-compatible endpoints.
This commit is contained in:
parent
9025da2296
commit
8cfe61dda2
@ -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
182
docs/providers/chutes.md
Normal 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)
|
||||
@ -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
126
src/agents/chutes-models.ts
Normal 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;
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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") {
|
||||
|
||||
@ -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"] = {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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") {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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: [],
|
||||
},
|
||||
},
|
||||
|
||||
@ -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] = {
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user