From 559736a5a0d36f7b8a8f90c18a04709558cb0e08 Mon Sep 17 00:00:00 2001 From: fanziqing Date: Tue, 3 Feb 2026 19:57:37 +0800 Subject: [PATCH] feat(volcengine): integrate Volcengine & Byteplus Provider --- docs/concepts/model-providers.md | 64 +++++++++ src/agents/byteplus-models.ts | 108 ++++++++++++++ src/agents/doubao-models.ts | 132 ++++++++++++++++++ src/agents/model-auth.ts | 7 + src/agents/model-selection.ts | 4 + src/agents/models-config.providers.ts | 68 +++++++++ ....providers.volcengine-byteplus.e2e.test.ts | 40 ++++++ src/agents/pi-embedded-runner/model.ts | 5 +- src/cli/program/register.onboard.ts | 2 + src/commands/auth-choice-options.e2e.test.ts | 2 + src/commands/auth-choice-options.ts | 14 ++ src/commands/auth-choice.apply.byteplus.ts | 73 ++++++++++ src/commands/auth-choice.apply.ts | 15 +- src/commands/auth-choice.apply.volcengine.ts | 73 ++++++++++ .../auth-choice.preferred-provider.ts | 2 + src/commands/model-picker.ts | 10 +- ...-non-interactive.provider-auth.e2e.test.ts | 21 +++ .../local/auth-choice-inference.ts | 2 + .../local/auth-choice.ts | 47 +++++++ src/commands/onboard-provider-auth-flags.ts | 16 +++ src/commands/onboard-types.ts | 6 + 21 files changed, 700 insertions(+), 11 deletions(-) create mode 100644 src/agents/byteplus-models.ts create mode 100644 src/agents/doubao-models.ts create mode 100644 src/agents/models-config.providers.volcengine-byteplus.e2e.test.ts create mode 100644 src/commands/auth-choice.apply.byteplus.ts create mode 100644 src/commands/auth-choice.apply.volcengine.ts diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index f59c34b49..01d1029e0 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -216,6 +216,70 @@ Model refs: See [/providers/qwen](/providers/qwen) for setup details and notes. +### Volcano Engine (Doubao) + +Volcano Engine (火山引擎) provides access to Doubao and other models in China. + +- Provider: `volcengine` (coding: `volcengine-plan`) +- Auth: `VOLCANO_ENGINE_API_KEY` +- Example model: `volcengine/doubao-seed-1-8-251228` +- CLI: `openclaw onboard --auth-choice volcengine-api-key` + +```json5 +{ + agents: { + defaults: { model: { primary: "volcengine/doubao-seed-1-8-251228" } } + } +} +``` + +Available models: + +- `volcengine/doubao-seed-1-8-251228` (Doubao Seed 1.8) +- `volcengine/doubao-seed-code-preview-251028` +- `volcengine/kimi-k2-5-260127` (Kimi K2.5) +- `volcengine/glm-4-7-251222` (GLM 4.7) +- `volcengine/deepseek-v3-2-251201` (DeepSeek V3.2 128K) + +Coding models (`volcengine-plan`): + +- `volcengine-plan/ark-code-latest` +- `volcengine-plan/doubao-seed-code` +- `volcengine-plan/kimi-k2.5` +- `volcengine-plan/kimi-k2-thinking` +- `volcengine-plan/glm-4.7` + +### BytePlus (International) + +BytePlus ARK provides access to the same models as Volcano Engine for international users. + +- Provider: `byteplus` (coding: `byteplus-plan`) +- Auth: `BYTEPLUS_API_KEY` +- Example model: `byteplus/seed-1-8-251228` +- CLI: `openclaw onboard --auth-choice byteplus-api-key` + +```json5 +{ + agents: { + defaults: { model: { primary: "byteplus/seed-1-8-251228" } } + } +} +``` + +Available models: + +- `byteplus/seed-1-8-251228` (Seed 1.8) +- `byteplus/kimi-k2-5-260127` (Kimi K2.5) +- `byteplus/glm-4-7-251222` (GLM 4.7) + +Coding models (`byteplus-plan`): + +- `byteplus-plan/ark-code-latest` +- `byteplus-plan/doubao-seed-code` +- `byteplus-plan/kimi-k2.5` +- `byteplus-plan/kimi-k2-thinking` +- `byteplus-plan/glm-4.7` + ### Synthetic Synthetic provides Anthropic-compatible models behind the `synthetic` provider: diff --git a/src/agents/byteplus-models.ts b/src/agents/byteplus-models.ts new file mode 100644 index 000000000..f60be606e --- /dev/null +++ b/src/agents/byteplus-models.ts @@ -0,0 +1,108 @@ +import type { ModelDefinitionConfig } from "../config/types.js"; + +export const BYTEPLUS_BASE_URL = "https://ark.ap-southeast.bytepluses.com/api/v3"; +export const BYTEPLUS_CODING_BASE_URL = "https://ark.ap-southeast.bytepluses.com/api/coding/v3"; +export const BYTEPLUS_DEFAULT_MODEL_ID = "seed-1-8-251228"; +export const BYTEPLUS_CODING_DEFAULT_MODEL_ID = "ark-code-latest"; +export const BYTEPLUS_DEFAULT_MODEL_REF = `byteplus/${BYTEPLUS_DEFAULT_MODEL_ID}`; + +// BytePlus pricing (approximate, adjust based on actual pricing) +export const BYTEPLUS_DEFAULT_COST = { + input: 0.0001, // $0.0001 per 1K tokens + output: 0.0002, // $0.0002 per 1K tokens + cacheRead: 0, + cacheWrite: 0, +}; + +/** + * Complete catalog of BytePlus ARK models. + * + * BytePlus ARK provides access to various models + * through the ARK API. Authentication requires a BYTEPLUS_API_KEY. + */ +export const BYTEPLUS_MODEL_CATALOG = [ + { + id: "seed-1-8-251228", + name: "Seed 1.8", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 256000, + maxTokens: 4096, + }, + { + id: "kimi-k2-5-260127", + name: "Kimi K2.5", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 256000, + maxTokens: 4096, + }, + { + id: "glm-4-7-251222", + name: "GLM 4.7", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 200000, + maxTokens: 4096, + }, +] as const; + +export type BytePlusCatalogEntry = (typeof BYTEPLUS_MODEL_CATALOG)[number]; +export type BytePlusCodingCatalogEntry = (typeof BYTEPLUS_CODING_MODEL_CATALOG)[number]; + +export function buildBytePlusModelDefinition( + entry: BytePlusCatalogEntry | BytePlusCodingCatalogEntry, +): ModelDefinitionConfig { + return { + id: entry.id, + name: entry.name, + reasoning: entry.reasoning, + input: [...entry.input], + cost: BYTEPLUS_DEFAULT_COST, + contextWindow: entry.contextWindow, + maxTokens: entry.maxTokens, + }; +} + +export const BYTEPLUS_CODING_MODEL_CATALOG = [ + { + id: "ark-code-latest", + name: "Ark Coding Plan", + reasoning: false, + input: ["text"] as const, + contextWindow: 256000, + maxTokens: 4096, + }, + { + id: "doubao-seed-code", + name: "Doubao Seed Code", + reasoning: false, + input: ["text"] as const, + contextWindow: 256000, + maxTokens: 4096, + }, + { + id: "glm-4.7", + name: "GLM 4.7 Coding", + reasoning: false, + input: ["text"] as const, + contextWindow: 200000, + maxTokens: 4096, + }, + { + id: "kimi-k2-thinking", + name: "Kimi K2 Thinking", + reasoning: false, + input: ["text"] as const, + contextWindow: 256000, + maxTokens: 4096, + }, + { + id: "kimi-k2.5", + name: "Kimi K2.5 Coding", + reasoning: false, + input: ["text"] as const, + contextWindow: 256000, + maxTokens: 4096, + }, +] as const; diff --git a/src/agents/doubao-models.ts b/src/agents/doubao-models.ts new file mode 100644 index 000000000..a1f3f4e5b --- /dev/null +++ b/src/agents/doubao-models.ts @@ -0,0 +1,132 @@ +import type { ModelDefinitionConfig } from "../config/types.js"; + +export const DOUBAO_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3"; +export const DOUBAO_CODING_BASE_URL = "https://ark.cn-beijing.volces.com/api/coding/v3"; +export const DOUBAO_DEFAULT_MODEL_ID = "doubao-seed-1-8-251228"; +export const DOUBAO_CODING_DEFAULT_MODEL_ID = "ark-code-latest"; +export const DOUBAO_DEFAULT_MODEL_REF = `volcengine/${DOUBAO_DEFAULT_MODEL_ID}`; + +// Volcano Engine Doubao pricing (approximate, adjust based on actual pricing) +export const DOUBAO_DEFAULT_COST = { + input: 0.0001, // ¥0.0001 per 1K tokens + output: 0.0002, // ¥0.0002 per 1K tokens + cacheRead: 0, + cacheWrite: 0, +}; + +/** + * Complete catalog of Volcano Engine models. + * + * Volcano Engine provides access to models + * through the API. Authentication requires a Volcano Engine API Key. + */ +export const DOUBAO_MODEL_CATALOG = [ + { + id: "doubao-seed-code-preview-251028", + name: "doubao-seed-code-preview-251028", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 256000, + maxTokens: 4096, + }, + { + id: "doubao-seed-1-8-251228", + name: "Doubao Seed 1.8", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 256000, + maxTokens: 4096, + }, + { + id: "kimi-k2-5-260127", + name: "Kimi K2.5", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 256000, + maxTokens: 4096, + }, + { + id: "glm-4-7-251222", + name: "GLM 4.7", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 200000, + maxTokens: 4096, + }, + { + id: "deepseek-v3-2-251201", + name: "DeepSeek V3.2", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 128000, + maxTokens: 4096, + }, +] as const; + +export type DoubaoCatalogEntry = (typeof DOUBAO_MODEL_CATALOG)[number]; +export type DoubaoCodingCatalogEntry = (typeof DOUBAO_CODING_MODEL_CATALOG)[number]; + +export function buildDoubaoModelDefinition( + entry: DoubaoCatalogEntry | DoubaoCodingCatalogEntry, +): ModelDefinitionConfig { + return { + id: entry.id, + name: entry.name, + reasoning: entry.reasoning, + input: [...entry.input], + cost: DOUBAO_DEFAULT_COST, + contextWindow: entry.contextWindow, + maxTokens: entry.maxTokens, + }; +} + +export const DOUBAO_CODING_MODEL_CATALOG = [ + { + id: "ark-code-latest", + name: "Ark Coding Plan", + reasoning: false, + input: ["text"] as const, + contextWindow: 256000, + maxTokens: 4096, + }, + { + id: "doubao-seed-code", + name: "Doubao Seed Code", + reasoning: false, + input: ["text"] as const, + contextWindow: 256000, + maxTokens: 4096, + }, + { + id: "glm-4.7", + name: "GLM 4.7 Coding", + reasoning: false, + input: ["text"] as const, + contextWindow: 200000, + maxTokens: 4096, + }, + { + id: "kimi-k2-thinking", + name: "Kimi K2 Thinking", + reasoning: false, + input: ["text"] as const, + contextWindow: 256000, + maxTokens: 4096, + }, + { + id: "kimi-k2.5", + name: "Kimi K2.5 Coding", + reasoning: false, + input: ["text"] as const, + contextWindow: 256000, + maxTokens: 4096, + }, + { + id: "doubao-seed-code-preview-251028", + name: "Doubao Seed Code Preview", + reasoning: false, + input: ["text"] as const, + contextWindow: 256000, + maxTokens: 4096, + }, +] as const; diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index b8ef41530..e3a2b8142 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -279,6 +279,13 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null { return pick("QWEN_OAUTH_TOKEN") ?? pick("QWEN_PORTAL_API_KEY"); } + if (normalized === "volcengine" || normalized === "volcengine-plan") { + return pick("VOLCANO_ENGINE_API_KEY"); + } + + if (normalized === "byteplus" || normalized === "byteplus-plan") { + return pick("BYTEPLUS_API_KEY"); + } if (normalized === "minimax-portal") { return pick("MINIMAX_OAUTH_TOKEN") ?? pick("MINIMAX_API_KEY"); } diff --git a/src/agents/model-selection.ts b/src/agents/model-selection.ts index 73286ad4f..eedb4d78d 100644 --- a/src/agents/model-selection.ts +++ b/src/agents/model-selection.ts @@ -46,6 +46,10 @@ export function normalizeProviderId(provider: string): string { if (normalized === "kimi-code") { return "kimi-coding"; } + // Backward compatibility for older provider naming. + if (normalized === "bytedance" || normalized === "doubao") { + return "volcengine"; + } return normalized; } diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index e622e70ec..f999d153c 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -10,6 +10,20 @@ import { buildCloudflareAiGatewayModelDefinition, resolveCloudflareAiGatewayBaseUrl, } from "./cloudflare-ai-gateway.js"; +import { + buildBytePlusModelDefinition, + BYTEPLUS_BASE_URL, + BYTEPLUS_MODEL_CATALOG, + BYTEPLUS_CODING_BASE_URL, + BYTEPLUS_CODING_MODEL_CATALOG, +} from "./byteplus-models.js"; +import { + buildDoubaoModelDefinition, + DOUBAO_BASE_URL, + DOUBAO_MODEL_CATALOG, + DOUBAO_CODING_BASE_URL, + DOUBAO_CODING_MODEL_CATALOG, +} from "./doubao-models.js"; import { discoverHuggingfaceModels, HUGGINGFACE_BASE_URL, @@ -547,6 +561,38 @@ function buildSyntheticProvider(): ProviderConfig { }; } +function buildDoubaoProvider(): ProviderConfig { + return { + baseUrl: DOUBAO_BASE_URL, + api: "openai-completions", + models: DOUBAO_MODEL_CATALOG.map(buildDoubaoModelDefinition), + }; +} + +function buildDoubaoCodingProvider(): ProviderConfig { + return { + baseUrl: DOUBAO_CODING_BASE_URL, + api: "openai-completions", + models: DOUBAO_CODING_MODEL_CATALOG.map(buildDoubaoModelDefinition), + }; +} + +function buildBytePlusProvider(): ProviderConfig { + return { + baseUrl: BYTEPLUS_BASE_URL, + api: "openai-completions", + models: BYTEPLUS_MODEL_CATALOG.map(buildBytePlusModelDefinition), + }; +} + +function buildBytePlusCodingProvider(): ProviderConfig { + return { + baseUrl: BYTEPLUS_CODING_BASE_URL, + api: "openai-completions", + models: BYTEPLUS_CODING_MODEL_CATALOG.map(buildBytePlusModelDefinition), + }; +} + export function buildXiaomiProvider(): ProviderConfig { return { baseUrl: XIAOMI_BASE_URL, @@ -745,6 +791,28 @@ export async function resolveImplicitProviders(params: { }; } + const volcengineKey = + resolveEnvApiKeyVarName("volcengine") ?? + resolveApiKeyFromProfiles({ provider: "volcengine", store: authStore }); + if (volcengineKey) { + providers.volcengine = { ...buildDoubaoProvider(), apiKey: volcengineKey }; + providers["volcengine-plan"] = { + ...buildDoubaoCodingProvider(), + apiKey: volcengineKey, + }; + } + + const byteplusKey = + resolveEnvApiKeyVarName("byteplus") ?? + resolveApiKeyFromProfiles({ provider: "byteplus", store: authStore }); + if (byteplusKey) { + providers.byteplus = { ...buildBytePlusProvider(), apiKey: byteplusKey }; + providers["byteplus-plan"] = { + ...buildBytePlusCodingProvider(), + apiKey: byteplusKey, + }; + } + const xiaomiKey = resolveEnvApiKeyVarName("xiaomi") ?? resolveApiKeyFromProfiles({ provider: "xiaomi", store: authStore }); diff --git a/src/agents/models-config.providers.volcengine-byteplus.e2e.test.ts b/src/agents/models-config.providers.volcengine-byteplus.e2e.test.ts new file mode 100644 index 000000000..9ce3ad892 --- /dev/null +++ b/src/agents/models-config.providers.volcengine-byteplus.e2e.test.ts @@ -0,0 +1,40 @@ +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { captureEnv } from "../test-utils/env.js"; +import { resolveImplicitProviders } from "./models-config.providers.js"; + +describe("Volcengine and BytePlus providers", () => { + it("includes volcengine and volcengine-plan when VOLCANO_ENGINE_API_KEY is configured", async () => { + const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-")); + const envSnapshot = captureEnv(["VOLCANO_ENGINE_API_KEY"]); + process.env.VOLCANO_ENGINE_API_KEY = "test-key"; + + try { + const providers = await resolveImplicitProviders({ agentDir }); + expect(providers?.volcengine).toBeDefined(); + expect(providers?.["volcengine-plan"]).toBeDefined(); + expect(providers?.volcengine?.apiKey).toBe("VOLCANO_ENGINE_API_KEY"); + expect(providers?.["volcengine-plan"]?.apiKey).toBe("VOLCANO_ENGINE_API_KEY"); + } finally { + envSnapshot.restore(); + } + }); + + it("includes byteplus and byteplus-plan when BYTEPLUS_API_KEY is configured", async () => { + const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-")); + const envSnapshot = captureEnv(["BYTEPLUS_API_KEY"]); + process.env.BYTEPLUS_API_KEY = "test-key"; + + try { + const providers = await resolveImplicitProviders({ agentDir }); + expect(providers?.byteplus).toBeDefined(); + expect(providers?.["byteplus-plan"]).toBeDefined(); + expect(providers?.byteplus?.apiKey).toBe("BYTEPLUS_API_KEY"); + expect(providers?.["byteplus-plan"]?.apiKey).toBe("BYTEPLUS_API_KEY"); + } finally { + envSnapshot.restore(); + } + }); +}); diff --git a/src/agents/pi-embedded-runner/model.ts b/src/agents/pi-embedded-runner/model.ts index 3eb819174..276938503 100644 --- a/src/agents/pi-embedded-runner/model.ts +++ b/src/agents/pi-embedded-runner/model.ts @@ -14,7 +14,10 @@ import { type ModelRegistry, } from "../pi-model-discovery.js"; -type InlineModelEntry = ModelDefinitionConfig & { provider: string; baseUrl?: string }; +type InlineModelEntry = ModelDefinitionConfig & { + provider: string; + baseUrl?: string; +}; type InlineProviderConfig = { baseUrl?: string; api?: ModelDefinitionConfig["api"]; diff --git a/src/cli/program/register.onboard.ts b/src/cli/program/register.onboard.ts index 2d6ec567a..cd344a8d2 100644 --- a/src/cli/program/register.onboard.ts +++ b/src/cli/program/register.onboard.ts @@ -150,6 +150,8 @@ export function registerOnboardCommand(program: Command) { opencodeZenApiKey: opts.opencodeZenApiKey as string | undefined, xaiApiKey: opts.xaiApiKey as string | undefined, litellmApiKey: opts.litellmApiKey as string | undefined, + volcengineApiKey: opts.volcengineApiKey as string | undefined, + byteplusApiKey: opts.byteplusApiKey as string | undefined, customBaseUrl: opts.customBaseUrl as string | undefined, customApiKey: opts.customApiKey as string | undefined, customModelId: opts.customModelId as string | undefined, diff --git a/src/commands/auth-choice-options.e2e.test.ts b/src/commands/auth-choice-options.e2e.test.ts index 53c9c3c05..aed522a36 100644 --- a/src/commands/auth-choice-options.e2e.test.ts +++ b/src/commands/auth-choice-options.e2e.test.ts @@ -43,6 +43,8 @@ describe("buildAuthChoiceOptions", () => { ["Chutes OAuth auth choice", ["chutes"]], ["Qwen auth choice", ["qwen-portal"]], ["xAI auth choice", ["xai-api-key"]], + ["Volcano Engine auth choice", ["volcengine-api-key"]], + ["BytePlus auth choice", ["byteplus-api-key"]], ["vLLM auth choice", ["vllm"]], ])("includes %s", (_label, expectedValues) => { const options = getOptions(); diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index e5269ab36..4a1fbc3f1 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -70,6 +70,18 @@ const AUTH_CHOICE_GROUP_DEFS: { hint: "API key", choices: ["xai-api-key"], }, + { + value: "volcengine", + label: "Volcano Engine", + hint: "API key", + choices: ["volcengine-api-key"], + }, + { + value: "byteplus", + label: "BytePlus", + hint: "API key", + choices: ["byteplus-api-key"], + }, { value: "openrouter", label: "OpenRouter", @@ -180,6 +192,8 @@ const BASE_AUTH_CHOICE_OPTIONS: ReadonlyArray = [ }, { value: "openai-api-key", label: "OpenAI API key" }, { value: "xai-api-key", label: "xAI (Grok) API key" }, + { value: "volcengine-api-key", label: "Volcano Engine API key" }, + { value: "byteplus-api-key", label: "BytePlus API key" }, { value: "qianfan-api-key", label: "Qianfan API key", diff --git a/src/commands/auth-choice.apply.byteplus.ts b/src/commands/auth-choice.apply.byteplus.ts new file mode 100644 index 000000000..edbbd728e --- /dev/null +++ b/src/commands/auth-choice.apply.byteplus.ts @@ -0,0 +1,73 @@ +import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js"; +import { resolveEnvApiKey } from "../agents/model-auth.js"; +import { upsertSharedEnvVar } from "../infra/env-file.js"; +import { + formatApiKeyPreview, + normalizeApiKeyInput, + validateApiKeyInput, +} from "./auth-choice.api-key.js"; +import { applyPrimaryModel } from "./model-picker.js"; + +/** Default model for BytePlus auth onboarding. */ +export const BYTEPLUS_DEFAULT_MODEL = "byteplus-plan/ark-code-latest"; + +export async function applyAuthChoiceBytePlus( + params: ApplyAuthChoiceParams, +): Promise { + if (params.authChoice !== "byteplus-api-key") { + return null; + } + + const envKey = resolveEnvApiKey("byteplus"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing BYTEPLUS_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + const result = upsertSharedEnvVar({ + key: "BYTEPLUS_API_KEY", + value: envKey.apiKey, + }); + if (!process.env.BYTEPLUS_API_KEY) { + process.env.BYTEPLUS_API_KEY = envKey.apiKey; + } + await params.prompter.note( + `Copied BYTEPLUS_API_KEY to ${result.path} for launchd compatibility.`, + "BytePlus API key", + ); + const configWithModel = applyPrimaryModel(params.config, BYTEPLUS_DEFAULT_MODEL); + return { + config: configWithModel, + agentModelOverride: BYTEPLUS_DEFAULT_MODEL, + }; + } + } + + let key: string | undefined; + if (params.opts?.byteplusApiKey) { + key = params.opts.byteplusApiKey; + } else { + key = await params.prompter.text({ + message: "Enter BytePlus API key", + validate: validateApiKeyInput, + }); + } + + const trimmed = normalizeApiKeyInput(String(key)); + const result = upsertSharedEnvVar({ + key: "BYTEPLUS_API_KEY", + value: trimmed, + }); + process.env.BYTEPLUS_API_KEY = trimmed; + await params.prompter.note( + `Saved BYTEPLUS_API_KEY to ${result.path} for launchd compatibility.`, + "BytePlus API key", + ); + + const configWithModel = applyPrimaryModel(params.config, BYTEPLUS_DEFAULT_MODEL); + return { + config: configWithModel, + agentModelOverride: BYTEPLUS_DEFAULT_MODEL, + }; +} diff --git a/src/commands/auth-choice.apply.ts b/src/commands/auth-choice.apply.ts index be07d07c5..54e516f94 100644 --- a/src/commands/auth-choice.apply.ts +++ b/src/commands/auth-choice.apply.ts @@ -1,8 +1,10 @@ import type { OpenClawConfig } from "../config/config.js"; import type { RuntimeEnv } from "../runtime.js"; import type { WizardPrompter } from "../wizard/prompts.js"; +import type { AuthChoice, OnboardOptions } from "./onboard-types.js"; import { applyAuthChoiceAnthropic } from "./auth-choice.apply.anthropic.js"; import { applyAuthChoiceApiProviders } from "./auth-choice.apply.api-providers.js"; +import { applyAuthChoiceBytePlus } from "./auth-choice.apply.byteplus.js"; import { applyAuthChoiceCopilotProxy } from "./auth-choice.apply.copilot-proxy.js"; import { applyAuthChoiceGitHubCopilot } from "./auth-choice.apply.github-copilot.js"; import { applyAuthChoiceGoogleAntigravity } from "./auth-choice.apply.google-antigravity.js"; @@ -12,8 +14,8 @@ import { applyAuthChoiceOAuth } from "./auth-choice.apply.oauth.js"; import { applyAuthChoiceOpenAI } from "./auth-choice.apply.openai.js"; import { applyAuthChoiceQwenPortal } from "./auth-choice.apply.qwen-portal.js"; import { applyAuthChoiceVllm } from "./auth-choice.apply.vllm.js"; +import { applyAuthChoiceVolcengine } from "./auth-choice.apply.volcengine.js"; import { applyAuthChoiceXAI } from "./auth-choice.apply.xai.js"; -import type { AuthChoice } from "./onboard-types.js"; export type ApplyAuthChoiceParams = { authChoice: AuthChoice; @@ -23,14 +25,7 @@ export type ApplyAuthChoiceParams = { agentDir?: string; setDefaultModel: boolean; agentId?: string; - opts?: { - tokenProvider?: string; - token?: string; - cloudflareAiGatewayAccountId?: string; - cloudflareAiGatewayGatewayId?: string; - cloudflareAiGatewayApiKey?: string; - xaiApiKey?: string; - }; + opts?: Partial; }; export type ApplyAuthChoiceResult = { @@ -54,6 +49,8 @@ export async function applyAuthChoice( applyAuthChoiceCopilotProxy, applyAuthChoiceQwenPortal, applyAuthChoiceXAI, + applyAuthChoiceVolcengine, + applyAuthChoiceBytePlus, ]; for (const handler of handlers) { diff --git a/src/commands/auth-choice.apply.volcengine.ts b/src/commands/auth-choice.apply.volcengine.ts new file mode 100644 index 000000000..7b22b9649 --- /dev/null +++ b/src/commands/auth-choice.apply.volcengine.ts @@ -0,0 +1,73 @@ +import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js"; +import { resolveEnvApiKey } from "../agents/model-auth.js"; +import { upsertSharedEnvVar } from "../infra/env-file.js"; +import { + formatApiKeyPreview, + normalizeApiKeyInput, + validateApiKeyInput, +} from "./auth-choice.api-key.js"; +import { applyPrimaryModel } from "./model-picker.js"; + +/** Default model for Volcano Engine auth onboarding. */ +export const VOLCENGINE_DEFAULT_MODEL = "volcengine-plan/ark-code-latest"; + +export async function applyAuthChoiceVolcengine( + params: ApplyAuthChoiceParams, +): Promise { + if (params.authChoice !== "volcengine-api-key") { + return null; + } + + const envKey = resolveEnvApiKey("volcengine"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing VOLCANO_ENGINE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + const result = upsertSharedEnvVar({ + key: "VOLCANO_ENGINE_API_KEY", + value: envKey.apiKey, + }); + if (!process.env.VOLCANO_ENGINE_API_KEY) { + process.env.VOLCANO_ENGINE_API_KEY = envKey.apiKey; + } + await params.prompter.note( + `Copied VOLCANO_ENGINE_API_KEY to ${result.path} for launchd compatibility.`, + "Volcano Engine API Key", + ); + const configWithModel = applyPrimaryModel(params.config, VOLCENGINE_DEFAULT_MODEL); + return { + config: configWithModel, + agentModelOverride: VOLCENGINE_DEFAULT_MODEL, + }; + } + } + + let key: string | undefined; + if (params.opts?.volcengineApiKey) { + key = params.opts.volcengineApiKey; + } else { + key = await params.prompter.text({ + message: "Enter Volcano Engine API Key", + validate: validateApiKeyInput, + }); + } + + const trimmed = normalizeApiKeyInput(String(key)); + const result = upsertSharedEnvVar({ + key: "VOLCANO_ENGINE_API_KEY", + value: trimmed, + }); + process.env.VOLCANO_ENGINE_API_KEY = trimmed; + await params.prompter.note( + `Saved VOLCANO_ENGINE_API_KEY to ${result.path} for launchd compatibility.`, + "Volcano Engine API Key", + ); + + const configWithModel = applyPrimaryModel(params.config, VOLCENGINE_DEFAULT_MODEL); + return { + config: configWithModel, + agentModelOverride: VOLCENGINE_DEFAULT_MODEL, + }; +} diff --git a/src/commands/auth-choice.preferred-provider.ts b/src/commands/auth-choice.preferred-provider.ts index 05eaa83ea..c8479b982 100644 --- a/src/commands/auth-choice.preferred-provider.ts +++ b/src/commands/auth-choice.preferred-provider.ts @@ -41,6 +41,8 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial> = { "xai-api-key": "xai", "litellm-api-key": "litellm", "qwen-portal": "qwen-portal", + "volcengine-api-key": "volcengine", + "byteplus-api-key": "byteplus", "minimax-portal": "minimax-portal", "qianfan-api-key": "qianfan", "custom-api-key": "custom", diff --git a/src/commands/model-picker.ts b/src/commands/model-picker.ts index ebf662369..6b1c8691e 100644 --- a/src/commands/model-picker.ts +++ b/src/commands/model-picker.ts @@ -258,7 +258,15 @@ export async function promptDefaultModel( } if (hasPreferredProvider && preferredProvider) { - models = models.filter((entry) => entry.provider === preferredProvider); + models = models.filter((entry) => { + if (preferredProvider === "volcengine") { + return entry.provider === "volcengine" || entry.provider === "volcengine-plan"; + } + if (preferredProvider === "byteplus") { + return entry.provider === "byteplus" || entry.provider === "byteplus-plan"; + } + return entry.provider === preferredProvider; + }); if (preferredProvider === "anthropic") { models = models.filter((entry) => !isAnthropicLegacyModel(entry)); } diff --git a/src/commands/onboard-non-interactive.provider-auth.e2e.test.ts b/src/commands/onboard-non-interactive.provider-auth.e2e.test.ts index e59c5b193..bb0a3d14c 100644 --- a/src/commands/onboard-non-interactive.provider-auth.e2e.test.ts +++ b/src/commands/onboard-non-interactive.provider-auth.e2e.test.ts @@ -227,6 +227,27 @@ describe("onboard (non-interactive): provider auth", () => { }); }, 60_000); + it("stores Volcano Engine API key and sets default model", async () => { + await withOnboardEnv("openclaw-onboard-volcengine-", async (env) => { + const cfg = await runOnboardingAndReadConfig(env, { + authChoice: "volcengine-api-key", + volcengineApiKey: "volcengine-test-key", + }); + + expect(cfg.agents?.defaults?.model?.primary).toBe("volcengine-plan/ark-code-latest"); + }); + }, 60_000); + + it("infers BytePlus auth choice from --byteplus-api-key and sets default model", async () => { + await withOnboardEnv("openclaw-onboard-byteplus-infer-", async (env) => { + const cfg = await runOnboardingAndReadConfig(env, { + byteplusApiKey: "byteplus-test-key", + }); + + expect(cfg.agents?.defaults?.model?.primary).toBe("byteplus-plan/ark-code-latest"); + }); + }, 60_000); + it("stores Vercel AI Gateway API key and sets default model", async () => { await withOnboardEnv("openclaw-onboard-ai-gateway-", async (env) => { const cfg = await runOnboardingAndReadConfig(env, { diff --git a/src/commands/onboard-non-interactive/local/auth-choice-inference.ts b/src/commands/onboard-non-interactive/local/auth-choice-inference.ts index 1064961bd..b5c5c44b5 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice-inference.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice-inference.ts @@ -28,6 +28,8 @@ type AuthChoiceFlagOptions = Pick< | "xaiApiKey" | "litellmApiKey" | "qianfanApiKey" + | "volcengineApiKey" + | "byteplusApiKey" | "customBaseUrl" | "customModelId" | "customApiKey" diff --git a/src/commands/onboard-non-interactive/local/auth-choice.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts index 61f437adf..ea97d12e5 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.ts @@ -55,6 +55,7 @@ import { resolveCustomProviderId, } from "../../onboard-custom.js"; import type { AuthChoice, OnboardOptions } from "../../onboard-types.js"; +import { applyPrimaryModel } from "../../model-picker.js"; import { applyOpenAIConfig } from "../../openai-model-default.js"; import { detectZaiEndpoint } from "../../zai-endpoint-detect.js"; import { resolveNonInteractiveApiKey } from "../api-keys.js"; @@ -303,6 +304,52 @@ export async function applyNonInteractiveAuthChoice(params: { return applyXaiConfig(nextConfig); } + if (authChoice === "volcengine-api-key") { + const resolved = await resolveNonInteractiveApiKey({ + provider: "volcengine", + cfg: baseConfig, + flagValue: opts.volcengineApiKey, + flagName: "--volcengine-api-key", + envVar: "VOLCANO_ENGINE_API_KEY", + runtime, + }); + if (!resolved) { + return null; + } + if (resolved.source !== "profile") { + const result = upsertSharedEnvVar({ + key: "VOLCANO_ENGINE_API_KEY", + value: resolved.key, + }); + process.env.VOLCANO_ENGINE_API_KEY = resolved.key; + runtime.log(`Saved VOLCANO_ENGINE_API_KEY to ${shortenHomePath(result.path)}`); + } + return applyPrimaryModel(nextConfig, "volcengine-plan/ark-code-latest"); + } + + if (authChoice === "byteplus-api-key") { + const resolved = await resolveNonInteractiveApiKey({ + provider: "byteplus", + cfg: baseConfig, + flagValue: opts.byteplusApiKey, + flagName: "--byteplus-api-key", + envVar: "BYTEPLUS_API_KEY", + runtime, + }); + if (!resolved) { + return null; + } + if (resolved.source !== "profile") { + const result = upsertSharedEnvVar({ + key: "BYTEPLUS_API_KEY", + value: resolved.key, + }); + process.env.BYTEPLUS_API_KEY = resolved.key; + runtime.log(`Saved BYTEPLUS_API_KEY to ${shortenHomePath(result.path)}`); + } + return applyPrimaryModel(nextConfig, "byteplus-plan/ark-code-latest"); + } + if (authChoice === "qianfan-api-key") { const resolved = await resolveNonInteractiveApiKey({ provider: "qianfan", diff --git a/src/commands/onboard-provider-auth-flags.ts b/src/commands/onboard-provider-auth-flags.ts index 2ca75c2ae..f55ea438e 100644 --- a/src/commands/onboard-provider-auth-flags.ts +++ b/src/commands/onboard-provider-auth-flags.ts @@ -21,6 +21,8 @@ type OnboardProviderAuthOptionKey = keyof Pick< | "xaiApiKey" | "litellmApiKey" | "qianfanApiKey" + | "volcengineApiKey" + | "byteplusApiKey" >; export type OnboardProviderAuthFlag = { @@ -166,4 +168,18 @@ export const ONBOARD_PROVIDER_AUTH_FLAGS: ReadonlyArray cliOption: "--qianfan-api-key ", description: "QIANFAN API key", }, + { + optionKey: "volcengineApiKey", + authChoice: "volcengine-api-key", + cliFlag: "--volcengine-api-key", + cliOption: "--volcengine-api-key ", + description: "Volcano Engine API key", + }, + { + optionKey: "byteplusApiKey", + authChoice: "byteplus-api-key", + cliFlag: "--byteplus-api-key", + cliOption: "--byteplus-api-key ", + description: "BytePlus API key", + }, ]; diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index 43a9cde76..c3ec88b7b 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -45,6 +45,8 @@ export type AuthChoice = | "copilot-proxy" | "qwen-portal" | "xai-api-key" + | "volcengine-api-key" + | "byteplus-api-key" | "qianfan-api-key" | "custom-api-key" | "skip"; @@ -71,6 +73,8 @@ export type AuthChoiceGroupId = | "huggingface" | "qianfan" | "xai" + | "volcengine" + | "byteplus" | "custom"; export type GatewayAuthChoice = "token" | "password"; export type ResetScope = "config" | "config+creds+sessions" | "full"; @@ -119,6 +123,8 @@ export type OnboardOptions = { huggingfaceApiKey?: string; opencodeZenApiKey?: string; xaiApiKey?: string; + volcengineApiKey?: string; + byteplusApiKey?: string; qianfanApiKey?: string; customBaseUrl?: string; customApiKey?: string;