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",
|
venice: "VENICE_API_KEY",
|
||||||
mistral: "MISTRAL_API_KEY",
|
mistral: "MISTRAL_API_KEY",
|
||||||
opencode: "OPENCODE_API_KEY",
|
opencode: "OPENCODE_API_KEY",
|
||||||
|
volcengine: "VOLCENGINE_API_KEY",
|
||||||
};
|
};
|
||||||
const envVar = envMap[normalized];
|
const envVar = envMap[normalized];
|
||||||
if (!envVar) return null;
|
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: {
|
export async function resolveImplicitProviders(params: {
|
||||||
agentDir: string;
|
agentDir: string;
|
||||||
}): Promise<ModelsConfig["providers"]> {
|
}): Promise<ModelsConfig["providers"]> {
|
||||||
@ -402,6 +412,13 @@ export async function resolveImplicitProviders(params: {
|
|||||||
providers.venice = { ...(await buildVeniceProvider()), apiKey: veniceKey };
|
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");
|
const qwenProfiles = listProfilesForProvider(authStore, "qwen-portal");
|
||||||
if (qwenProfiles.length > 0) {
|
if (qwenProfiles.length > 0) {
|
||||||
providers["qwen-portal"] = {
|
providers["qwen-portal"] = {
|
||||||
|
|||||||
@ -52,7 +52,7 @@ export function registerOnboardCommand(program: Command) {
|
|||||||
.option("--mode <mode>", "Wizard mode: local|remote")
|
.option("--mode <mode>", "Wizard mode: local|remote")
|
||||||
.option(
|
.option(
|
||||||
"--auth-choice <choice>",
|
"--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(
|
.option(
|
||||||
"--token-provider <id>",
|
"--token-provider <id>",
|
||||||
@ -76,6 +76,7 @@ export function registerOnboardCommand(program: Command) {
|
|||||||
.option("--synthetic-api-key <key>", "Synthetic API key")
|
.option("--synthetic-api-key <key>", "Synthetic API key")
|
||||||
.option("--venice-api-key <key>", "Venice API key")
|
.option("--venice-api-key <key>", "Venice API key")
|
||||||
.option("--opencode-zen-api-key <key>", "OpenCode Zen 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-port <port>", "Gateway port")
|
||||||
.option("--gateway-bind <mode>", "Gateway bind: loopback|tailnet|lan|auto|custom")
|
.option("--gateway-bind <mode>", "Gateway bind: loopback|tailnet|lan|auto|custom")
|
||||||
.option("--gateway-auth <mode>", "Gateway auth: token|password")
|
.option("--gateway-auth <mode>", "Gateway auth: token|password")
|
||||||
@ -126,6 +127,7 @@ export function registerOnboardCommand(program: Command) {
|
|||||||
syntheticApiKey: opts.syntheticApiKey as string | undefined,
|
syntheticApiKey: opts.syntheticApiKey as string | undefined,
|
||||||
veniceApiKey: opts.veniceApiKey as string | undefined,
|
veniceApiKey: opts.veniceApiKey as string | undefined,
|
||||||
opencodeZenApiKey: opts.opencodeZenApiKey as string | undefined,
|
opencodeZenApiKey: opts.opencodeZenApiKey as string | undefined,
|
||||||
|
volcengineApiKey: opts.volcengineApiKey as string | undefined,
|
||||||
gatewayPort:
|
gatewayPort:
|
||||||
typeof gatewayPort === "number" && Number.isFinite(gatewayPort)
|
typeof gatewayPort === "number" && Number.isFinite(gatewayPort)
|
||||||
? gatewayPort
|
? gatewayPort
|
||||||
|
|||||||
@ -20,7 +20,8 @@ export type AuthChoiceGroupId =
|
|||||||
| "minimax"
|
| "minimax"
|
||||||
| "synthetic"
|
| "synthetic"
|
||||||
| "venice"
|
| "venice"
|
||||||
| "qwen";
|
| "qwen"
|
||||||
|
| "volcengine";
|
||||||
|
|
||||||
export type AuthChoiceGroup = {
|
export type AuthChoiceGroup = {
|
||||||
value: AuthChoiceGroupId;
|
value: AuthChoiceGroupId;
|
||||||
@ -113,6 +114,12 @@ const AUTH_CHOICE_GROUP_DEFS: {
|
|||||||
hint: "API key",
|
hint: "API key",
|
||||||
choices: ["opencode-zen"],
|
choices: ["opencode-zen"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: "volcengine",
|
||||||
|
label: "Volcano Engine",
|
||||||
|
hint: "ARK API key",
|
||||||
|
choices: ["volcengine-api-key"],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function buildAuthChoiceOptions(params: {
|
export function buildAuthChoiceOptions(params: {
|
||||||
@ -164,6 +171,7 @@ export function buildAuthChoiceOptions(params: {
|
|||||||
hint: "Uses the bundled Gemini CLI auth plugin",
|
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: "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: "qwen-portal", label: "Qwen OAuth" });
|
||||||
options.push({
|
options.push({
|
||||||
value: "copilot-proxy",
|
value: "copilot-proxy",
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { applyAuthChoiceMiniMax } from "./auth-choice.apply.minimax.js";
|
|||||||
import { applyAuthChoiceOAuth } from "./auth-choice.apply.oauth.js";
|
import { applyAuthChoiceOAuth } from "./auth-choice.apply.oauth.js";
|
||||||
import { applyAuthChoiceOpenAI } from "./auth-choice.apply.openai.js";
|
import { applyAuthChoiceOpenAI } from "./auth-choice.apply.openai.js";
|
||||||
import { applyAuthChoiceQwenPortal } from "./auth-choice.apply.qwen-portal.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";
|
import type { AuthChoice } from "./onboard-types.js";
|
||||||
|
|
||||||
export type ApplyAuthChoiceParams = {
|
export type ApplyAuthChoiceParams = {
|
||||||
@ -24,6 +25,7 @@ export type ApplyAuthChoiceParams = {
|
|||||||
opts?: {
|
opts?: {
|
||||||
tokenProvider?: string;
|
tokenProvider?: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
|
volcengineApiKey?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -46,6 +48,7 @@ export async function applyAuthChoice(
|
|||||||
applyAuthChoiceGoogleGeminiCli,
|
applyAuthChoiceGoogleGeminiCli,
|
||||||
applyAuthChoiceCopilotProxy,
|
applyAuthChoiceCopilotProxy,
|
||||||
applyAuthChoiceQwenPortal,
|
applyAuthChoiceQwenPortal,
|
||||||
|
applyAuthChoiceVolcengine,
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const handler of handlers) {
|
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"
|
| "github-copilot"
|
||||||
| "copilot-proxy"
|
| "copilot-proxy"
|
||||||
| "qwen-portal"
|
| "qwen-portal"
|
||||||
|
| "volcengine-api-key"
|
||||||
| "skip";
|
| "skip";
|
||||||
export type GatewayAuthChoice = "token" | "password";
|
export type GatewayAuthChoice = "token" | "password";
|
||||||
export type ResetScope = "config" | "config+creds+sessions" | "full";
|
export type ResetScope = "config" | "config+creds+sessions" | "full";
|
||||||
@ -71,6 +72,7 @@ export type OnboardOptions = {
|
|||||||
syntheticApiKey?: string;
|
syntheticApiKey?: string;
|
||||||
veniceApiKey?: string;
|
veniceApiKey?: string;
|
||||||
opencodeZenApiKey?: string;
|
opencodeZenApiKey?: string;
|
||||||
|
volcengineApiKey?: string;
|
||||||
gatewayPort?: number;
|
gatewayPort?: number;
|
||||||
gatewayBind?: GatewayBind;
|
gatewayBind?: GatewayBind;
|
||||||
gatewayAuth?: GatewayAuthChoice;
|
gatewayAuth?: GatewayAuthChoice;
|
||||||
|
|||||||
@ -375,7 +375,7 @@ export async function runOnboardingWizard(
|
|||||||
});
|
});
|
||||||
nextConfig = authResult.config;
|
nextConfig = authResult.config;
|
||||||
|
|
||||||
if (authChoiceFromPrompt) {
|
if (authChoiceFromPrompt && !authResult.agentModelOverride) {
|
||||||
const modelSelection = await promptDefaultModel({
|
const modelSelection = await promptDefaultModel({
|
||||||
config: nextConfig,
|
config: nextConfig,
|
||||||
prompter,
|
prompter,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user