diff --git a/open-sse/config/providers/index.ts b/open-sse/config/providers/index.ts index 9c557003be..638df8d492 100644 --- a/open-sse/config/providers/index.ts +++ b/open-sse/config/providers/index.ts @@ -70,6 +70,8 @@ import { togetherProvider } from "./registry/together/index.ts"; import { cohereProvider } from "./registry/cohere/index.ts"; import { cursorProvider, cursor_apiProvider } from "./registry/cursor/index.ts"; import { volcengineProvider } from "./registry/volcengine/index.ts"; +import { volcengine_agent_planProvider } from "./registry/volcengine/agent-plan/index.ts"; +import { volcengine_coding_planProvider } from "./registry/volcengine/coding-plan/index.ts"; import { freetheaiProvider } from "./registry/freetheai/index.ts"; import { g4f_groqProvider } from "./registry/g4f-groq/index.ts"; import { g4f_geminiProvider } from "./registry/g4f-gemini/index.ts"; @@ -337,6 +339,8 @@ export const REGISTRY: Record = { cursor: cursorProvider, "cursor-api": cursor_apiProvider, volcengine: volcengineProvider, + "volcengine-agent-plan": volcengine_agent_planProvider, + "volcengine-coding-plan": volcengine_coding_planProvider, freetheai: freetheaiProvider, "g4f-groq": g4f_groqProvider, "g4f-gemini": g4f_geminiProvider, diff --git a/open-sse/config/providers/registry/volcengine/agent-plan/index.ts b/open-sse/config/providers/registry/volcengine/agent-plan/index.ts new file mode 100644 index 0000000000..f34fc84586 --- /dev/null +++ b/open-sse/config/providers/registry/volcengine/agent-plan/index.ts @@ -0,0 +1,115 @@ +import type { RegistryEntry, RegistryModel } from "../../../shared.ts"; + +/** + * Volcano Ark Agent Plan models. + * + * The Agent Plan subscription (console.volcengine.com/ark/subscription/agent-plan) + * is served by the Plan API endpoint — `/api/plan/v3` — which differs from both the + * standard pay-per-use API (`/api/v3`) and the Coding Plan API (`/api/coding/v3`). + * The Plan API has NO `/models` listing endpoint (returns 404); key validation falls + * back to a chat probe against the first model. Model IDs below verified live against + * /api/plan/v3/chat/completions (all return 200). + */ +export const VOLCENGINE_AGENT_PLAN_MODELS: RegistryModel[] = [ + { + id: "doubao-seed-evolving", + name: "Doubao Seed Evolving (Agent Plan)", + contextLength: 1048576, + toolCalling: true, + supportsVision: true, + supportsReasoning: true, + }, + { + id: "doubao-seed-2-1-turbo-260628", + name: "Doubao Seed 2.1 Turbo (Agent Plan)", + contextLength: 262144, + toolCalling: true, + supportsVision: true, + supportsReasoning: true, + }, + { + id: "doubao-seed-2-0-lite-260215", + name: "Doubao Seed 2.0 Lite (Agent Plan)", + contextLength: 262144, + toolCalling: true, + supportsVision: true, + supportsReasoning: true, + }, + { + id: "doubao-seed-2-0-mini-260215", + name: "Doubao Seed 2.0 Mini (Agent Plan)", + contextLength: 262144, + toolCalling: true, + supportsVision: true, + supportsReasoning: true, + }, + { + id: "deepseek-v4-flash-ga-260731", + name: "DeepSeek V4 Flash GA (Agent Plan)", + contextLength: 1048576, + toolCalling: true, + supportsReasoning: true, + }, + { + id: "kimi-k3", + name: "Kimi K3 (Agent Plan)", + contextLength: 1048576, + toolCalling: true, + supportsVision: true, + supportsReasoning: true, + }, + { + id: "glm-5-2-260617", + name: "GLM 5.2 (Agent Plan)", + contextLength: 1048576, + toolCalling: true, + supportsReasoning: true, + }, + { + id: "kimi-k2.7-code", + name: "Kimi K2.7 Code (Agent Plan)", + contextLength: 1048576, + toolCalling: true, + supportsVision: true, + supportsReasoning: true, + }, + { + id: "minimax-m3", + name: "MiniMax M3 (Agent Plan)", + contextLength: 1048576, + toolCalling: true, + supportsReasoning: true, + }, + { + id: "deepseek-v4-pro-260425", + name: "DeepSeek V4 Pro (Agent Plan)", + contextLength: 1048576, + toolCalling: true, + supportsReasoning: true, + }, + { + id: "minimax-m2.7", + name: "MiniMax M2.7 (Agent Plan)", + contextLength: 1048576, + toolCalling: true, + supportsReasoning: true, + }, + { + id: "kimi-k2.6", + name: "Kimi K2.6 (Agent Plan)", + contextLength: 1048576, + toolCalling: true, + supportsReasoning: true, + }, +]; + +export const volcengine_agent_planProvider: RegistryEntry = { + id: "volcengine-agent-plan", + alias: "veap", + format: "openai", + executor: "default", + baseUrl: "https://ark.cn-beijing.volces.com/api/plan/v3/chat/completions", + authType: "apikey", + authHeader: "bearer", + models: VOLCENGINE_AGENT_PLAN_MODELS, +}; diff --git a/open-sse/config/providers/registry/volcengine/coding-plan/index.ts b/open-sse/config/providers/registry/volcengine/coding-plan/index.ts new file mode 100644 index 0000000000..c93bb15a5f --- /dev/null +++ b/open-sse/config/providers/registry/volcengine/coding-plan/index.ts @@ -0,0 +1,92 @@ +import type { RegistryEntry, RegistryModel } from "../../../shared.ts"; + +/** + * Volcano Ark Coding Plan models. + * + * The Coding Plan subscription (console.volcengine.com/ark/subscription/coding-plan) + * is served by a DEDICATED endpoint — `/api/coding/v3` — which differs from both the + * standard pay-per-use API (`/api/v3`) and the Agent Plan API (`/api/plan/v3`). Using + * the wrong base URL returns HTTP 401 "The API key or AK/SK ... is missing or invalid" + * even with a valid Coding Plan key. Model IDs below verified live against + * /api/coding/v3/chat/completions (all return 200). + */ +export const VOLCENGINE_CODING_PLAN_MODELS: RegistryModel[] = [ + { + id: "doubao-seed-2-1-turbo", + name: "Doubao Seed 2.1 Turbo (Coding Plan)", + contextLength: 262144, + toolCalling: true, + supportsVision: true, + supportsReasoning: true, + }, + { + id: "doubao-seed-2.0-lite", + name: "Doubao Seed 2.0 Lite (Coding Plan)", + contextLength: 262144, + toolCalling: true, + supportsVision: true, + supportsReasoning: true, + }, + { + id: "deepseek-v4-flash", + name: "DeepSeek V4 Flash (Coding Plan)", + contextLength: 1048576, + toolCalling: true, + supportsReasoning: true, + }, + { + id: "glm-5.2", + name: "GLM 5.2 (Coding Plan)", + contextLength: 1048576, + toolCalling: true, + supportsReasoning: true, + }, + { + id: "kimi-k2.7-code", + name: "Kimi K2.7 Code (Coding Plan)", + contextLength: 1048576, + toolCalling: true, + supportsVision: true, + supportsReasoning: true, + }, + { + id: "minimax-m3", + name: "MiniMax M3 (Coding Plan)", + contextLength: 1048576, + toolCalling: true, + supportsReasoning: true, + }, + { + id: "deepseek-v4-pro", + name: "DeepSeek V4 Pro (Coding Plan)", + contextLength: 1048576, + toolCalling: true, + supportsReasoning: true, + }, + { + id: "minimax-m2.7", + name: "MiniMax M2.7 (Coding Plan)", + contextLength: 1048576, + toolCalling: true, + supportsReasoning: true, + }, + { + id: "kimi-k2.6", + name: "Kimi K2.6 (Coding Plan)", + contextLength: 1048576, + toolCalling: true, + supportsReasoning: true, + }, +]; + +export const volcengine_coding_planProvider: RegistryEntry = { + id: "volcengine-coding-plan", + alias: "vecp", + format: "openai", + executor: "default", + baseUrl: "https://ark.cn-beijing.volces.com/api/coding/v3/chat/completions", + authType: "apikey", + authHeader: "bearer", + models: VOLCENGINE_CODING_PLAN_MODELS, + modelsUrl: "/models", +}; diff --git a/open-sse/services/tokenExtractionConfig.ts b/open-sse/services/tokenExtractionConfig.ts index 8ffc7af921..9aefedf706 100644 --- a/open-sse/services/tokenExtractionConfig.ts +++ b/open-sse/services/tokenExtractionConfig.ts @@ -185,6 +185,26 @@ const RAW_CONFIGS: TokenExtractionConfig[] = [ { cookieDomain: ".chat.qwen.ai" } ), + // ── Volcano Engine Ark Console ─────────────────────────── + config( + "volcengine-console", + "Volcano Engine Ark Console", + "https://console.volcengine.com/ark/region:cn-beijing/subscription/coding-plan", + "https://console.volcengine.com", + [ + { type: "cookie", name: "digest", domain: ".volcengine.com" }, + { type: "cookie", name: "AccountID", domain: ".volcengine.com" }, + { type: "cookie", name: "csrfToken", domain: ".volcengine.com" }, + { type: "cookie", name: "userInfo", domain: ".volcengine.com" }, + ], + "Log in to the Volcano Engine Ark console. The console session is used to discover Agent/Coding Plan API keys and live quota usage.", + { + cookieDomain: ".volcengine.com", + successUrlPattern: /console\.volcengine\.com\/ark/i, + pollingConfig: { timeout: 300_000, minLoginTime: 3000 }, + } + ), + // ── Kimi Web ────────────────────────────────────────────── config( "kimi-web", diff --git a/open-sse/services/usage.ts b/open-sse/services/usage.ts index 7f4a97486a..45b2e8c4e0 100644 --- a/open-sse/services/usage.ts +++ b/open-sse/services/usage.ts @@ -68,6 +68,7 @@ import { getXaiUsage } from "./usage/xai.ts"; import { getXaiOauthUsage } from "./usage/xaiOauth.ts"; import { getGrokCliUsage } from "./usage/grokCli.ts"; import { getFirecrawlUsage } from "./usage/firecrawl.ts"; +import { getVolcenginePlanUsage } from "./usage/volcenginePlan.ts"; import { getCommandCodeUsage } from "./usage/command-code.ts"; import { getQwenTokenPlanUsage } from "./usage/qwen-token-plan.ts"; import { getConolUsage } from "./conolUsage.ts"; @@ -135,6 +136,9 @@ export const USAGE_FETCHER_PROVIDERS = [ "ha", // Firecrawl team credits (GET /v2/team/credit-usage) "firecrawl", + // Volcano Ark Plan subscriptions (agent-plan / coding-plan) + "volcengine-agent-plan", + "volcengine-coding-plan", // Command Code credits + 5h/weekly windows (GET /alpha/billing/credits) "command-code", "conol-web", @@ -242,6 +246,9 @@ export async function getUsageForProvider( return await getHyperAgentUsage(apiKey || accessToken, providerSpecificData); case "firecrawl": return await getFirecrawlUsage(id || "", apiKey, connection); + case "volcengine-agent-plan": + case "volcengine-coding-plan": + return await getVolcenginePlanUsage(apiKey || "", provider, providerSpecificData); case "command-code": return await getCommandCodeUsage(apiKey || accessToken || ""); case "conol-web": diff --git a/open-sse/services/usage/volcenginePlan.ts b/open-sse/services/usage/volcenginePlan.ts new file mode 100644 index 0000000000..64bc4a4b99 --- /dev/null +++ b/open-sse/services/usage/volcenginePlan.ts @@ -0,0 +1,317 @@ +/** + * usage/volcenginePlan.ts — Volcano Ark Plan usage fetcher. + * + * Volcano Engine Ark serves the two subscription plans on DISTINCT chat base URLs: + * - Agent Plan → https://ark.cn-beijing.volces.com/api/plan/v3 + * - Coding Plan → https://ark.cn-beijing.volces.com/api/coding/v3 + * (both differ from the standard pay-per-use API at /api/v3). + * + * The data-plane API exposes NO quota/usage endpoint. Real subscription usage + * lives behind the Ark console's authenticated "top" API, which is keyed by the + * browser session cookie (+ CSRF token), NOT the ark- API key: + * - Coding Plan → POST /api/top/ark/cn-beijing/2024-01-01/GetCodingPlanUsage + * - Agent Plan → POST /api/top/ark/cn-beijing/2024-01-01/GetAgentPlanAFPUsage + * + * When the connection carries a console cookie in providerSpecificData + * (`volcConsoleCookie` + `volcCsrfToken`), we fetch the real quota windows and + * map them into OmniRoute's UsageQuota shape. Without a cookie we fall back to a + * data-plane connectivity probe (validates the key, no quota numbers). + */ + +import { toRecord, toNumber } from "./scalars.ts"; +import { type UsageQuota } from "./quota.ts"; + +type JsonRecord = Record; + +const AGENT_PLAN_BASE_URL = "https://ark.cn-beijing.volces.com/api/plan/v3"; +const CODING_PLAN_BASE_URL = "https://ark.cn-beijing.volces.com/api/coding/v3"; + +const CONSOLE_TOP_BASE = "https://console.volcengine.com/api/top/ark/cn-beijing/2024-01-01"; + +// First model probed for the Agent Plan chat-based validation (no /models endpoint). +const AGENT_PLAN_PROBE_MODEL = "doubao-seed-2-0-pro-260215"; + +const CONSOLE_HINT_AGENT = "console.volcengine.com/ark → 订阅 Agent Plan"; +const CONSOLE_HINT_CODING = "console.volcengine.com/ark → 订阅 Coding Plan"; + +function getPlanName(provider: string): string { + if (provider === "volcengine-agent-plan") return "Volcano Ark Agent Plan"; + if (provider === "volcengine-coding-plan") return "Volcano Ark Coding Plan"; + return "Volcano Ark Plan"; +} + +function getBaseUrl(provider: string, providerSpecificData?: JsonRecord): string { + const override = providerSpecificData?.arkPlanBaseUrl; + if (typeof override === "string" && override.trim()) return override.trim().replace(/\/+$/, ""); + if (provider === "volcengine-coding-plan") return CODING_PLAN_BASE_URL; + return AGENT_PLAN_BASE_URL; +} + +// ── Console cookie helpers ────────────────────────────────────────────────── + +function getConsoleCookie(providerSpecificData?: JsonRecord): string { + const cookie = providerSpecificData?.volcConsoleCookie; + return typeof cookie === "string" ? cookie.trim() : ""; +} + +function getConsoleCsrf(providerSpecificData?: JsonRecord, cookie = ""): string { + const explicit = providerSpecificData?.volcCsrfToken; + if (typeof explicit === "string" && explicit.trim()) return explicit.trim(); + // Fall back to the csrfToken embedded in the cookie string. + const match = cookie.match(/csrfToken=([^;]+)/); + return match ? match[1].trim() : ""; +} + +async function callConsoleApi( + action: string, + cookie: string, + csrf: string, + referer: string +): Promise<{ ok: boolean; status: number; json: JsonRecord; error?: string }> { + const response = await fetch(`${CONSOLE_TOP_BASE}/${action}?`, { + method: "POST", + headers: { + accept: "application/json, text/plain, */*", + "content-type": "application/json", + cookie, + origin: "https://console.volcengine.com", + referer, + "x-csrf-token": csrf, + }, + body: "{}", + }); + const text = await response.text(); + let json: JsonRecord = {}; + try { + json = toRecord(JSON.parse(text)); + } catch { + /* non-JSON */ + } + const err = toRecord(toRecord(json.ResponseMetadata).Error); + const errMsg = typeof err.Message === "string" ? err.Message : ""; + return { ok: response.ok && !errMsg, status: response.status, json, error: errMsg }; +} + +// ── Console usage → UsageQuota mapping ─────────────────────────────────────── + +function tsToIso(seconds: number): string | null { + if (!seconds || seconds <= 0) return null; + const ms = seconds < 1e12 ? seconds * 1000 : seconds; + const d = new Date(ms); + return Number.isNaN(d.getTime()) ? null : d.toISOString(); +} + +const CODING_WINDOW_LABEL: Record = { + session: "Session (5h)", + weekly: "Weekly", + monthly: "Monthly", + daily: "Daily", +}; + +/** + * Map GetCodingPlanUsage → quotas. Coding Plan reports each window as a used + * `Percent` (0-100) against `Cap` (100), so remaining = Cap - Percent. + */ +function mapCodingPlanUsage(result: JsonRecord): Record { + const quotas: Record = {}; + const windows = Array.isArray(result.QuotaUsage) ? result.QuotaUsage : []; + for (const raw of windows) { + const w = toRecord(raw); + const level = String(w.Level || "").toLowerCase(); + if (!level) continue; + const cap = toNumber(w.Cap, 100) || 100; + const usedPercent = toNumber(w.Percent, 0); + const remainingPercentage = Math.max(0, Math.min(100, cap - usedPercent)); + quotas[level] = { + used: usedPercent, + total: cap, + remaining: Math.max(0, cap - usedPercent), + remainingPercentage, + resetAt: tsToIso(toNumber(w.ResetTimestamp, 0)), + unlimited: false, + displayName: CODING_WINDOW_LABEL[level] || level, + }; + } + return quotas; +} + +const AGENT_WINDOW_LABEL: Array<[string, string]> = [ + ["AFPFiveHour", "Session (5h)"], + ["AFPDaily", "Daily"], + ["AFPWeekly", "Weekly"], + ["AFPMonthly", "Monthly"], +]; + +/** + * Map GetAgentPlanAFPUsage → quotas. Agent Plan reports absolute `Quota`/`Used` + * (AFP credits) per window with a millisecond `ResetTime`. + */ +function mapAgentPlanUsage(result: JsonRecord): Record { + const quotas: Record = {}; + for (const [key, label] of AGENT_WINDOW_LABEL) { + const w = toRecord(result[key]); + if (Object.keys(w).length === 0) continue; + const total = toNumber(w.Quota, 0); + const used = toNumber(w.Used, 0); + const remaining = Math.max(0, total - used); + const remainingPercentage = + total > 0 ? Math.max(0, Math.min(100, (remaining / total) * 100)) : 100; + const resetMs = toNumber(w.ResetTime, 0); + quotas[key] = { + used, + total, + remaining, + remainingPercentage, + // Agent Plan ResetTime is in milliseconds already. + resetAt: tsToIso(resetMs >= 1e12 ? resetMs / 1000 : resetMs), + unlimited: false, + displayName: label, + }; + } + return quotas; +} + +// ── Data-plane connectivity probes (fallback, no cookie) ───────────────────── + +function parseArkError(json: unknown): { code: string; message: string } | null { + const data = toRecord(json); + const error = toRecord(data.error); + if (!error.code && !error.message && !data.message) return null; + return { + code: String(error.code || ""), + message: String(error.message || data.message || ""), + }; +} + +function authErrorMessage(planName: string, status: number, errorMsg: string): string { + if (status === 401) { + const isFormatError = /format.*incorrect|incorrect.*format/i.test(errorMsg); + return isFormatError + ? `Invalid API key format. ${planName} keys start with 'ark-'. Check your subscription key.` + : `Invalid API key or the key does not belong to a ${planName} subscription.`; + } + if (status === 403) { + return `Access denied. Ensure your key has an active ${planName} subscription.`; + } + return `${planName} API error (${status}): ${errorMsg}`; +} + +async function reportError(response: Response, responseText: string, planName: string) { + let data: unknown = null; + try { + data = JSON.parse(responseText); + } catch { + /* non-JSON error body */ + } + const arkError = parseArkError(data); + return { + plan: planName, + message: authErrorMessage( + planName, + response.status, + arkError?.message || responseText.slice(0, 200) + ), + }; +} + +/** Coding Plan: validate via the working /models listing endpoint. */ +async function probeCodingPlan(baseUrl: string, apiKey: string, planName: string) { + const response = await fetch(`${baseUrl}/models`, { + method: "GET", + headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, + }); + const responseText = await response.text(); + if (!response.ok) return reportError(response, responseText, planName); + return { + plan: planName, + message: `${planName} connected. Add your console cookie (volcConsoleCookie) to view live quota, or check ${CONSOLE_HINT_CODING}.`, + }; +} + +/** Agent Plan: no /models endpoint — validate via a minimal chat probe. */ +async function probeAgentPlan(baseUrl: string, apiKey: string, planName: string) { + const response = await fetch(`${baseUrl}/chat/completions`, { + method: "POST", + headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, + body: JSON.stringify({ + model: AGENT_PLAN_PROBE_MODEL, + messages: [{ role: "user", content: "hi" }], + max_tokens: 1, + stream: false, + }), + }); + const responseText = await response.text(); + if (!response.ok) return reportError(response, responseText, planName); + return { + plan: planName, + message: `${planName} connected. Add your console cookie (volcConsoleCookie) to view live quota, or check ${CONSOLE_HINT_AGENT}.`, + }; +} + +// ── Entry point ────────────────────────────────────────────────────────────── + +export async function getVolcenginePlanUsage( + apiKey: string, + provider: string, + providerSpecificData?: JsonRecord +) { + const planName = getPlanName(provider); + const isCoding = provider === "volcengine-coding-plan"; + + // Preferred path: real usage via the authenticated console "top" API. + const cookie = getConsoleCookie(providerSpecificData); + if (cookie) { + const csrf = getConsoleCsrf(providerSpecificData, cookie); + const action = isCoding ? "GetCodingPlanUsage" : "GetAgentPlanAFPUsage"; + const referer = isCoding + ? "https://console.volcengine.com/ark/region:cn-beijing/subscription/coding-plan" + : "https://console.volcengine.com/ark/region:cn-beijing/subscription/agent-plan"; + try { + const { ok, status, json, error } = await callConsoleApi(action, cookie, csrf, referer); + if (ok) { + const result = toRecord(json.Result); + const quotas = isCoding ? mapCodingPlanUsage(result) : mapAgentPlanUsage(result); + if (Object.keys(quotas).length > 0) { + const planType = typeof result.PlanType === "string" ? ` (${result.PlanType})` : ""; + return { plan: `${planName}${planType}`, quotas }; + } + return { + plan: planName, + message: `${planName} connected. No active quota windows reported.`, + }; + } + // Cookie present but console call failed (expired session / no subscription). + if (status === 401 || status === 403 || /login|unauthor|登录|鉴权/i.test(error || "")) { + return { + plan: planName, + message: `Console session expired. Refresh volcConsoleCookie to view live quota.`, + }; + } + return { + plan: planName, + message: `${planName}: console usage unavailable${error ? ` (${error})` : ""}.`, + }; + } catch (err) { + return { + plan: planName, + message: `${planName} — unable to reach the Ark console: ${(err as Error).message}`, + }; + } + } + + // Fallback: data-plane connectivity probe (needs the ark- API key). + if (!apiKey) { + return { message: "API key not available. Add an Ark Plan API key to view usage." }; + } + const baseUrl = getBaseUrl(provider, providerSpecificData); + try { + return isCoding + ? await probeCodingPlan(baseUrl, apiKey, planName) + : await probeAgentPlan(baseUrl, apiKey, planName); + } catch (error) { + return { + plan: planName, + message: `${planName} — unable to reach the Ark API: ${(error as Error).message}`, + }; + } +} diff --git a/src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx b/src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx index 3ceaf1d46f..9ada0a2df7 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx @@ -92,6 +92,7 @@ export default function ProviderDetailPageClient() { const [importClaudeModalOpen, setImportClaudeModalOpen] = useState(false); const [importGeminiModalOpen, setImportGeminiModalOpen] = useState(false); const [importGrokCliModalOpen, setImportGrokCliModalOpen] = useState(false); + const [connectingVolcengineAccount, setConnectingVolcengineAccount] = useState(false); const isOpenAICompatible = isOpenAICompatibleProvider(providerId); const isCcCompatible = isClaudeCodeCompatibleProvider(providerId); const isCommandCode = providerId === "command-code"; @@ -381,6 +382,37 @@ export default function ProviderDetailPageClient() { openApiKeyAddFlow(); }, [providerId, isOAuth, openApiKeyAddFlow]); + const connectVolcengineAccount = useCallback(async () => { + setConnectingVolcengineAccount(true); + try { + const response = await fetch("/api/providers/volcengine-plan/connect", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ timeout: 300_000 }), + }); + const data = await response.json().catch(() => ({})); + if (!response.ok || !data?.success) { + throw new Error(data?.error || "Failed to connect Volcano account"); + } + const results = Array.isArray(data?.binding?.results) ? data.binding.results : []; + const connected = results.filter((item: any) => item?.ok).length; + const failed = results.filter((item: any) => item && item.ok === false && item.available); + if (connected > 0) { + notify.success(`Connected ${connected} Volcano plan${connected > 1 ? "s" : ""}`); + } + if (failed.length > 0) { + notify.error( + failed.map((item: any) => `${item.plan}: ${item.error || "failed"}`).join("; ") + ); + } + await fetchConnections(); + } catch (error) { + notify.error(error instanceof Error ? error.message : "Failed to connect Volcano account"); + } finally { + setConnectingVolcengineAccount(false); + } + }, [fetchConnections, notify]); + const { commandCodeAuthState, handleCloseAddApiKeyModal, @@ -595,6 +627,8 @@ export default function ProviderDetailPageClient() { gateConnectionFlow={gateConnectionFlow} openApiKeyAddFlow={openApiKeyAddFlow} openPrimaryAddFlow={openPrimaryAddFlow} + connectVolcengineAccount={connectVolcengineAccount} + connectingVolcengineAccount={connectingVolcengineAccount} openExternalLinkFlow={openExternalLinkFlow} handleOpenCommandCodeConnect={handleOpenCommandCodeConnect} commandCodeAuthState={commandCodeAuthState} diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionsHeaderToolbar.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionsHeaderToolbar.tsx index 5f736ef2a2..fec3de0877 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionsHeaderToolbar.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionsHeaderToolbar.tsx @@ -40,6 +40,8 @@ type ConnectionsHeaderToolbarProps = { gateConnectionFlow: (callback: () => void) => void; openApiKeyAddFlow: () => void; openPrimaryAddFlow: () => void; + connectVolcengineAccount?: () => void; + connectingVolcengineAccount?: boolean; openExternalLinkFlow: () => void; handleOpenCommandCodeConnect: () => void; commandCodeAuthState: { phase: string }; @@ -86,6 +88,8 @@ export default function ConnectionsHeaderToolbar({ gateConnectionFlow, openApiKeyAddFlow, openPrimaryAddFlow, + connectVolcengineAccount, + connectingVolcengineAccount, openExternalLinkFlow, handleOpenCommandCodeConnect, commandCodeAuthState, @@ -303,6 +307,19 @@ export default function ConnectionsHeaderToolbar({ + {(providerId === "volcengine-agent-plan" || + providerId === "volcengine-coding-plan") && + connectVolcengineAccount && ( + + )} {providerId === "qoder" && (