added volcano engine model provider
This commit is contained in:
parent
9688454a30
commit
8f7425fc3c
@ -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;
|
||||
|
||||
@ -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"] = {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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) {
|
||||
|
||||
245
src/commands/auth-choice.apply.volcengine.ts
Normal file
245
src/commands/auth-choice.apply.volcengine.ts
Normal 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 };
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -375,7 +375,7 @@ export async function runOnboardingWizard(
|
||||
});
|
||||
nextConfig = authResult.config;
|
||||
|
||||
if (authChoiceFromPrompt) {
|
||||
if (authChoiceFromPrompt && !authResult.agentModelOverride) {
|
||||
const modelSelection = await promptDefaultModel({
|
||||
config: nextConfig,
|
||||
prompter,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user