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

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