added volcano engine model provider

This commit is contained in:
michael.zhang 2026-01-28 18:45:13 +08:00
parent 9688454a30
commit 8f7425fc3c
8 changed files with 281 additions and 3 deletions

View File

@ -285,6 +285,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
venice: "VENICE_API_KEY",
mistral: "MISTRAL_API_KEY",
opencode: "OPENCODE_API_KEY",
volcengine: "VOLCENGINE_API_KEY",
};
const envVar = envMap[normalized];
if (!envVar) return null;

View File

@ -359,6 +359,16 @@ async function buildOllamaProvider(): Promise<ProviderConfig> {
};
}
const ARK_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3";
function buildVolcengineProvider(): ProviderConfig {
return {
baseUrl: ARK_BASE_URL,
api: "openai-completions",
models: [],
};
}
export async function resolveImplicitProviders(params: {
agentDir: string;
}): Promise<ModelsConfig["providers"]> {
@ -402,6 +412,13 @@ export async function resolveImplicitProviders(params: {
providers.venice = { ...(await buildVeniceProvider()), apiKey: veniceKey };
}
const volcengineKey =
resolveEnvApiKeyVarName("volcengine") ??
resolveApiKeyFromProfiles({ provider: "volcengine", store: authStore });
if (volcengineKey) {
providers.volcengine = { ...buildVolcengineProvider(), apiKey: volcengineKey };
}
const qwenProfiles = listProfilesForProvider(authStore, "qwen-portal");
if (qwenProfiles.length > 0) {
providers["qwen-portal"] = {

View File

@ -52,7 +52,7 @@ export function registerOnboardCommand(program: Command) {
.option("--mode <mode>", "Wizard mode: local|remote")
.option(
"--auth-choice <choice>",
"Auth: setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip",
"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|apiKey|minimax-api|minimax-api-lightning|opencode-zen|volcengine-api-key|skip",
)
.option(
"--token-provider <id>",
@ -76,6 +76,7 @@ export function registerOnboardCommand(program: Command) {
.option("--synthetic-api-key <key>", "Synthetic API key")
.option("--venice-api-key <key>", "Venice API key")
.option("--opencode-zen-api-key <key>", "OpenCode Zen API key")
.option("--volcengine-api-key <key>", "Volcano Engine API key")
.option("--gateway-port <port>", "Gateway port")
.option("--gateway-bind <mode>", "Gateway bind: loopback|tailnet|lan|auto|custom")
.option("--gateway-auth <mode>", "Gateway auth: token|password")
@ -126,6 +127,7 @@ export function registerOnboardCommand(program: Command) {
syntheticApiKey: opts.syntheticApiKey as string | undefined,
veniceApiKey: opts.veniceApiKey as string | undefined,
opencodeZenApiKey: opts.opencodeZenApiKey as string | undefined,
volcengineApiKey: opts.volcengineApiKey as string | undefined,
gatewayPort:
typeof gatewayPort === "number" && Number.isFinite(gatewayPort)
? gatewayPort

View File

@ -20,7 +20,8 @@ export type AuthChoiceGroupId =
| "minimax"
| "synthetic"
| "venice"
| "qwen";
| "qwen"
| "volcengine";
export type AuthChoiceGroup = {
value: AuthChoiceGroupId;
@ -113,6 +114,12 @@ const AUTH_CHOICE_GROUP_DEFS: {
hint: "API key",
choices: ["opencode-zen"],
},
{
value: "volcengine",
label: "Volcano Engine",
hint: "ARK API key",
choices: ["volcengine-api-key"],
},
];
export function buildAuthChoiceOptions(params: {
@ -164,6 +171,7 @@ export function buildAuthChoiceOptions(params: {
hint: "Uses the bundled Gemini CLI auth plugin",
});
options.push({ value: "zai-api-key", label: "Z.AI (GLM 4.7) API key" });
options.push({ value: "volcengine-api-key", label: "Volcano Engine (ARK) API key" });
options.push({ value: "qwen-portal", label: "Qwen OAuth" });
options.push({
value: "copilot-proxy",

View File

@ -11,6 +11,7 @@ import { applyAuthChoiceMiniMax } from "./auth-choice.apply.minimax.js";
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 { applyAuthChoiceVolcengine } from "./auth-choice.apply.volcengine.js";
import type { AuthChoice } from "./onboard-types.js";
export type ApplyAuthChoiceParams = {
@ -24,6 +25,7 @@ export type ApplyAuthChoiceParams = {
opts?: {
tokenProvider?: string;
token?: string;
volcengineApiKey?: string;
};
};
@ -46,6 +48,7 @@ export async function applyAuthChoice(
applyAuthChoiceGoogleGeminiCli,
applyAuthChoiceCopilotProxy,
applyAuthChoiceQwenPortal,
applyAuthChoiceVolcengine,
];
for (const handler of handlers) {

View File

@ -0,0 +1,245 @@
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 type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
import { applyAuthProfileConfig } from "./onboard-auth.js";
const ARK_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3";
type ArkModel = {
id: string;
owned_by: string;
};
type ArkModelsResponse = {
data: ArkModel[];
};
export async function applyAuthChoiceVolcengine(
params: ApplyAuthChoiceParams,
): Promise<ApplyAuthChoiceResult | null> {
const authChoice = params.authChoice;
if (authChoice !== "volcengine-api-key") {
return null;
}
// 1. Get API Key
let apiKey = resolveEnvApiKey("volcengine")?.apiKey;
if (process.env.VOLCENGINE_API_KEY) {
apiKey = process.env.VOLCENGINE_API_KEY;
}
if (params.opts?.tokenProvider === "volcengine" && params.opts?.token) {
apiKey = params.opts.token;
}
if (params.opts?.volcengineApiKey) {
apiKey = params.opts.volcengineApiKey;
}
if (apiKey) {
const useExisting = await params.prompter.confirm({
message: `Use existing VOLCENGINE_API_KEY (${formatApiKeyPreview(apiKey)})?`,
initialValue: true,
});
if (!useExisting) {
apiKey = undefined;
}
}
if (!apiKey) {
const input = await params.prompter.text({
message: "Enter Volcano Engine (ARK) API key",
validate: validateApiKeyInput,
});
if (typeof input === "symbol") return null; // Aborted
apiKey = normalizeApiKeyInput(String(input));
}
// Save API Key
const result = upsertSharedEnvVar({
key: "VOLCENGINE_API_KEY",
value: apiKey,
});
if (!process.env.VOLCENGINE_API_KEY) {
process.env.VOLCENGINE_API_KEY = apiKey;
}
await params.prompter.note(
`Saved VOLCENGINE_API_KEY to ${result.path}`,
"Volcano Engine API key",
);
// 2. Models (Used for config generation later)
const models: ArkModel[] = [];
// 3. Select Model
let modelId: string | null = null;
let selectionMessage = "Select a model (Auto-verified)";
// Helper to verify model access
const verifyModelAccess = async (id: string): Promise<boolean> => {
const verifySpin = params.prompter.progress(`Verifying access to ${id} (10s timeout)...`);
try {
const res = await fetch(`${ARK_BASE_URL}/chat/completions`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: id,
messages: [{ role: "user", content: "hi" }],
max_tokens: 1,
stream: false,
}),
signal: AbortSignal.timeout(10000),
});
if (!res.ok) {
const errData = await res.json().catch(() => ({}));
const errMsg = errData?.error?.message || res.statusText;
throw new Error(errMsg);
}
verifySpin.stop(`Access verified: ${id}`);
return true;
} catch (err: any) {
verifySpin.stop("Access denied or potential timeout");
await params.prompter.note(
`Model "${id}" verification failed:\n${err.message}\n\nTip: You may need to create an Endpoint in ARK console or enable Pay-as-you-go.`,
"Validation Error",
);
return false;
}
};
while (!modelId) {
const PREDEFINED_MODELS = [
"glm-4-7-251222",
"doubao-seed-1-8-251228",
"deepseek-v3-2-251201",
"kimi-k2-thinking-251104",
];
const choices = [
// 1. Predefined Models
...PREDEFINED_MODELS.map((id) => ({
value: id,
label: id,
hint: "Predefined",
})),
// 2. Manual Entry (Always available as fallback)
{
value: "__manual__",
label: "Enter Manually (e.g. Endpoint ID ep-2025...)",
hint: "Use this if your Endpoint is not listed",
},
];
const selection = await params.prompter.select({
message: selectionMessage,
options: choices,
});
if (typeof selection === "symbol") return null;
let candidateId: string;
if (selection === "__manual__") {
const input = await params.prompter.text({
message: "Enter Endpoint ID (e.g. ep-20250604...)",
validate: (val) => (val.length > 0 ? undefined : "Endpoint ID is required"),
});
if (typeof input === "symbol") return null;
candidateId = String(input);
} else {
candidateId = String(selection);
}
// Verify validity
const isValid = await verifyModelAccess(candidateId);
if (isValid) {
modelId = candidateId;
} else {
selectionMessage =
"Access Denied - Please ensure you have activated this model/endpoint in ARK Console";
}
}
// 4. Update Config
let nextConfig = applyAuthProfileConfig(params.config, {
profileId: "volcengine:default",
provider: "volcengine", // We might need to map this provider if 'volcengine' isn't valid in core
mode: "api_key",
});
// Configure default model
// Configure default model (Always set as primary when explicitly configuring via this wizard)
if (params.agentId) {
nextConfig = {
...nextConfig,
agents: {
...nextConfig.agents,
defaults: {
...nextConfig.agents?.defaults,
model: {
...nextConfig.agents?.defaults?.model,
primary: `volcengine/${modelId}`, // referencing the provider/model
},
},
},
};
} else {
// Always set default model for the workspace globally
nextConfig = {
...nextConfig,
agents: {
...nextConfig.agents,
defaults: {
...nextConfig.agents?.defaults,
model: {
...nextConfig.agents?.defaults?.model,
primary: `volcengine/${modelId}`,
},
},
},
};
}
// Configure Provider
const existingProviderOrder = nextConfig.models?.providers;
// We need to cast this because TypeScript might complain if we add new keys to providers if it's strictly typed
const providers = { ...(existingProviderOrder || {}) } as any;
providers.volcengine = {
baseUrl: ARK_BASE_URL,
api: "openai-completions",
apiKey: apiKey,
models: [
{
id: modelId,
name: modelId,
},
...models
.filter((m) => m.id !== modelId)
.map((m) => ({
id: m.id,
name: m.id,
})),
],
};
nextConfig = {
...nextConfig,
models: {
...nextConfig.models,
providers,
},
};
return { config: nextConfig, agentModelOverride: modelId };
}

View File

@ -31,6 +31,7 @@ export type AuthChoice =
| "github-copilot"
| "copilot-proxy"
| "qwen-portal"
| "volcengine-api-key"
| "skip";
export type GatewayAuthChoice = "token" | "password";
export type ResetScope = "config" | "config+creds+sessions" | "full";
@ -71,6 +72,7 @@ export type OnboardOptions = {
syntheticApiKey?: string;
veniceApiKey?: string;
opencodeZenApiKey?: string;
volcengineApiKey?: string;
gatewayPort?: number;
gatewayBind?: GatewayBind;
gatewayAuth?: GatewayAuthChoice;

View File

@ -375,7 +375,7 @@ export async function runOnboardingWizard(
});
nextConfig = authResult.config;
if (authChoiceFromPrompt) {
if (authChoiceFromPrompt && !authResult.agentModelOverride) {
const modelSelection = await promptDefaultModel({
config: nextConfig,
prompter,