feat(volcengine): integrate Volcengine & Byteplus Provider

This commit is contained in:
fanziqing 2026-02-03 19:57:37 +08:00 committed by Peter Steinberger
parent 95c14d9b5f
commit 559736a5a0
21 changed files with 700 additions and 11 deletions

View File

@ -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:

View File

@ -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;

132
src/agents/doubao-models.ts Normal file
View File

@ -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;

View File

@ -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");
}

View File

@ -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;
}

View File

@ -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 });

View File

@ -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();
}
});
});

View File

@ -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"];

View File

@ -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,

View File

@ -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();

View File

@ -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<AuthChoiceOption> = [
},
{ 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",

View File

@ -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<ApplyAuthChoiceResult | null> {
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,
};
}

View File

@ -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<OnboardOptions>;
};
export type ApplyAuthChoiceResult = {
@ -54,6 +49,8 @@ export async function applyAuthChoice(
applyAuthChoiceCopilotProxy,
applyAuthChoiceQwenPortal,
applyAuthChoiceXAI,
applyAuthChoiceVolcengine,
applyAuthChoiceBytePlus,
];
for (const handler of handlers) {

View File

@ -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<ApplyAuthChoiceResult | null> {
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,
};
}

View File

@ -41,6 +41,8 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
"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",

View File

@ -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));
}

View File

@ -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, {

View File

@ -28,6 +28,8 @@ type AuthChoiceFlagOptions = Pick<
| "xaiApiKey"
| "litellmApiKey"
| "qianfanApiKey"
| "volcengineApiKey"
| "byteplusApiKey"
| "customBaseUrl"
| "customModelId"
| "customApiKey"

View File

@ -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",

View File

@ -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<OnboardProviderAuthFlag>
cliOption: "--qianfan-api-key <key>",
description: "QIANFAN API key",
},
{
optionKey: "volcengineApiKey",
authChoice: "volcengine-api-key",
cliFlag: "--volcengine-api-key",
cliOption: "--volcengine-api-key <key>",
description: "Volcano Engine API key",
},
{
optionKey: "byteplusApiKey",
authChoice: "byteplus-api-key",
cliFlag: "--byteplus-api-key",
cliOption: "--byteplus-api-key <key>",
description: "BytePlus API key",
},
];

View File

@ -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;