feat(volcengine): add Ark plan providers

This commit is contained in:
yangsiyuan.rengar
2026-08-13 20:26:12 +08:00
committed by Markus Hartung
parent f58e8bef6f
commit d732cf615d
16 changed files with 948 additions and 1 deletions

View File

@@ -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<string, RegistryEntry> = {
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,

View File

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

View File

@@ -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",
};

View File

@@ -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",

View File

@@ -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":

View File

@@ -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<string, unknown>;
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<string, string> = {
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<string, UsageQuota> {
const quotas: Record<string, UsageQuota> = {};
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<string, UsageQuota> {
const quotas: Record<string, UsageQuota> = {};
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}`,
};
}
}

View File

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

View File

@@ -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({
<Button size="sm" icon="add" onClick={() => gateConnectionFlow(openPrimaryAddFlow)}>
{providerSupportsPat ? providerText(t, "addPat", "Add PAT") : t("add")}
</Button>
{(providerId === "volcengine-agent-plan" ||
providerId === "volcengine-coding-plan") &&
connectVolcengineAccount && (
<Button
size="sm"
variant="secondary"
icon="login"
loading={connectingVolcengineAccount}
onClick={() => gateConnectionFlow(connectVolcengineAccount)}
>
{providerText(t, "connectVolcengineAccount", "Connect Volcano Account")}
</Button>
)}
{providerId === "qoder" && (
<Button
size="sm"

View File

@@ -0,0 +1,32 @@
import { NextResponse } from "next/server";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { bindVolcenginePlansFromConsoleCredentials } from "@/lib/providers/volcenginePlanBinding";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
export async function POST(request: Request): Promise<NextResponse> {
const auth = await requireManagementAuth(request);
if (auth) return auth;
const body = await request.json().catch(() => ({}));
const timeout = typeof body.timeout === "number" ? body.timeout : undefined;
try {
const { inAppLoginService } = await import("@omniroute/open-sse/services/inAppLoginService.ts");
const login = await inAppLoginService.startLogin("volcengine-console", { timeout });
if (!login.success || !login.credentials) {
return NextResponse.json(
{ success: false, error: login.error || "Volcano console login failed" },
{ status: 400 }
);
}
const binding = await bindVolcenginePlansFromConsoleCredentials(login.credentials);
return NextResponse.json({ success: true, binding });
} catch (error) {
const message = sanitizeErrorMessage(error instanceof Error ? error.message : error);
return NextResponse.json(
{ success: false, error: `Volcano account binding failed: ${message}` },
{ status: 500 }
);
}
}

View File

@@ -11,7 +11,13 @@
const TOOL_ONLY_SERVICE_KINDS = new Set<string>(["webSearch", "webFetch"]);
/** Providers whose registry catalog is the complete, intentional model list. */
const CURATED_MODEL_ONLY_PROVIDERS = new Set<string>(["chatgpt-web", "kimi-web", "zai-web"]);
const CURATED_MODEL_ONLY_PROVIDERS = new Set<string>([
"chatgpt-web",
"kimi-web",
"zai-web",
"volcengine-agent-plan",
"volcengine-coding-plan",
]);
export function providerUsesCuratedModelsOnly(providerId: string): boolean {
return CURATED_MODEL_ONLY_PROVIDERS.has(providerId.trim().toLowerCase());

View File

@@ -0,0 +1,273 @@
import {
createProviderConnection,
getProviderConnections,
updateProviderConnection,
} from "@/models";
type JsonRecord = Record<string, unknown>;
const CONSOLE_TOP_BASE = "https://console.volcengine.com/api/top/ark/cn-beijing/2024-01-01";
const CODING_PLAN_PROVIDER = "volcengine-coding-plan";
const AGENT_PLAN_PROVIDER = "volcengine-agent-plan";
const PLAN_CONFIG = {
coding: {
provider: CODING_PLAN_PROVIDER,
name: "Volcano Ark Coding Plan",
usageAction: "GetCodingPlanUsage",
referer: "https://console.volcengine.com/ark/region:cn-beijing/subscription/coding-plan",
listApiKeysPayload: { ProjectName: "default" },
},
agent: {
provider: AGENT_PLAN_PROVIDER,
name: "Volcano Ark Agent Plan",
usageAction: "GetAgentPlanAFPUsage",
referer: "https://console.volcengine.com/ark/region:cn-beijing/subscription/agent-plan",
listApiKeysPayload: {
ProjectName: "default",
Filter: { Scene: "RealAgentPlanPersonal" },
},
},
} as const;
type PlanKind = keyof typeof PLAN_CONFIG;
interface ConsoleApiResult {
ok: boolean;
status: number;
json: JsonRecord;
error: string | null;
}
function stringField(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
}
function record(value: unknown): JsonRecord {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
}
function buildCookieHeader(credentials: JsonRecord): string {
const rawCookie = stringField(credentials.volcConsoleCookie);
if (rawCookie) return rawCookie;
const names = ["digest", "AccountID", "csrfToken", "userInfo"];
return names
.map((name) => {
const value = stringField(credentials[name]);
return value ? `${name}=${value}` : "";
})
.filter(Boolean)
.join("; ");
}
function extractCsrf(credentials: JsonRecord, cookieHeader: string): string {
const explicit = stringField(credentials.volcCsrfToken) || stringField(credentials.csrfToken);
if (explicit) return explicit;
return cookieHeader.match(/(?:^|;\s*)csrfToken=([^;]+)/)?.[1]?.trim() || "";
}
async function callConsoleApi(
action: string,
payload: JsonRecord,
cookieHeader: string,
csrfToken: string,
referer: string
): Promise<ConsoleApiResult> {
const response = await fetch(`${CONSOLE_TOP_BASE}/${action}?`, {
method: "POST",
headers: {
accept: "application/json, text/plain, */*",
"content-type": "application/json",
cookie: cookieHeader,
origin: "https://console.volcengine.com",
referer,
"x-csrf-token": csrfToken,
},
body: JSON.stringify(payload),
});
const text = await response.text();
let json: JsonRecord = {};
try {
json = record(JSON.parse(text));
} catch {
// Non-JSON console failures are reported through `error` below.
}
const meta = record(json.ResponseMetadata);
const err = record(meta.Error);
const message = stringField(err.Message);
return {
ok: response.ok && !message,
status: response.status,
json,
error: message || (response.ok ? null : text.slice(0, 200)),
};
}
async function detectPlan(
kind: PlanKind,
cookieHeader: string,
csrfToken: string
): Promise<{ available: boolean; usage: JsonRecord; error: string | null }> {
const cfg = PLAN_CONFIG[kind];
const result = await callConsoleApi(cfg.usageAction, {}, cookieHeader, csrfToken, cfg.referer);
if (!result.ok) {
return { available: false, usage: {}, error: result.error };
}
return { available: true, usage: record(result.json.Result), error: null };
}
function firstApiKeyItem(result: JsonRecord): JsonRecord | null {
const items = record(result.Result).Items;
if (!Array.isArray(items)) return null;
return record(items[0]);
}
async function fetchRawApiKey(
kind: PlanKind,
cookieHeader: string,
csrfToken: string
): Promise<{ apiKey: string; id: number | null; maskedKey: string | null; error: string | null }> {
const cfg = PLAN_CONFIG[kind];
const list = await callConsoleApi(
"ListApiKeys",
cfg.listApiKeysPayload,
cookieHeader,
csrfToken,
cfg.referer
);
if (!list.ok) {
return { apiKey: "", id: null, maskedKey: null, error: list.error || "ListApiKeys failed" };
}
const item = firstApiKeyItem(list.json);
const id = Number(item?.Id);
if (!Number.isFinite(id) || id <= 0) {
return { apiKey: "", id: null, maskedKey: null, error: "No API key found for this plan" };
}
const raw = await callConsoleApi(
"GetRawApiKey",
{ Id: id },
cookieHeader,
csrfToken,
cfg.referer
);
if (!raw.ok) {
return { apiKey: "", id, maskedKey: stringField(item?.Key) || null, error: raw.error };
}
const apiKey = stringField(record(raw.json.Result).ApiKey);
if (!apiKey) {
return {
apiKey: "",
id,
maskedKey: stringField(item?.Key) || null,
error: "Raw API key missing",
};
}
return { apiKey, id, maskedKey: stringField(item?.Key) || null, error: null };
}
async function upsertConnection(
kind: PlanKind,
apiKey: string,
cookieHeader: string,
csrfToken: string,
apiKeyId: number | null,
usage: JsonRecord
) {
const cfg = PLAN_CONFIG[kind];
const providerSpecificData = {
volcConsoleCookie: cookieHeader,
volcCsrfToken: csrfToken,
volcApiKeyId: apiKeyId,
volcPlanKind: kind,
volcLastUsage: usage,
};
const existing = (await getProviderConnections({ provider: cfg.provider })).find(
(conn: JsonRecord) => stringField(conn.name) === cfg.name
);
if (existing?.id) {
return await updateProviderConnection(stringField(existing.id), {
apiKey,
name: cfg.name,
providerSpecificData,
isActive: true,
testStatus: "active",
});
}
return await createProviderConnection({
provider: cfg.provider,
authType: "apikey",
name: cfg.name,
apiKey,
providerSpecificData,
isActive: true,
testStatus: "active",
});
}
export async function bindVolcenginePlansFromConsoleCredentials(credentials: JsonRecord) {
const cookieHeader = buildCookieHeader(credentials);
const csrfToken = extractCsrf(credentials, cookieHeader);
if (!cookieHeader || !csrfToken) {
throw new Error("Volcano console cookie or csrfToken is missing");
}
const results: Array<{
plan: PlanKind;
available: boolean;
ok: boolean;
connectionId?: string;
apiKeyId?: number | null;
maskedKey?: string | null;
error?: string | null;
}> = [];
for (const kind of ["coding", "agent"] as PlanKind[]) {
const detected = await detectPlan(kind, cookieHeader, csrfToken);
if (!detected.available) {
results.push({ plan: kind, available: false, ok: false, error: detected.error });
continue;
}
const key = await fetchRawApiKey(kind, cookieHeader, csrfToken);
if (!key.apiKey) {
results.push({
plan: kind,
available: true,
ok: false,
apiKeyId: key.id,
maskedKey: key.maskedKey,
error: key.error,
});
continue;
}
const connection = await upsertConnection(
kind,
key.apiKey,
cookieHeader,
csrfToken,
key.id,
detected.usage
);
results.push({
plan: kind,
available: true,
ok: Boolean(connection?.id),
connectionId: stringField(connection?.id),
apiKeyId: key.id,
maskedKey: key.maskedKey,
});
}
return {
cookieCaptured: true,
results,
};
}

View File

@@ -100,6 +100,9 @@ const PROVIDER_LIMITS_APIKEY_PROVIDERS = new Set([
"hyperagent",
"ha",
"firecrawl",
// Volcano Ark Plan subscriptions (agent-plan / coding-plan)
"volcengine-agent-plan",
"volcengine-coding-plan",
// Command Code API key → /alpha/billing/credits + windowLimits
"command-code",
"conol-web",

View File

@@ -95,6 +95,7 @@ export const LOCAL_ONLY_API_PREFIXES: ReadonlyArray<string> = [
*/
export const LOCAL_ONLY_API_PATTERNS: ReadonlyArray<RegExp> = [
/^\/api\/providers\/[^/]+\/login\/?$/,
/^\/api\/providers\/volcengine-plan\/connect\/?$/,
/^\/api\/providers\/[^/]+\/refresh-cursor\/?$/,
/^\/api\/providers\/[^/]+\/chatgpt-web-codex-doctor\/?$/,
];

View File

@@ -74,6 +74,7 @@ export function getProviderConnectionFamilyIds(providerId: unknown): readonly st
// Web / Cookie Providers
// API Key Providers
// Sub-categories within APIKEY_PROVIDERS (used by dashboard and catalog views).
@@ -142,6 +143,7 @@ export const AGGREGATOR_PROVIDER_IDS = new Set([
"helixmind",
"tabitoken",
"logfare",
]);
export const ENTERPRISE_CLOUD_PROVIDER_IDS = new Set([
@@ -505,6 +507,9 @@ export const USAGE_SUPPORTED_PROVIDERS = [
"grok-cli",
// 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 rolling windows
"command-code",
"conol-web",

View File

@@ -199,6 +199,26 @@ export const APIKEY_PROVIDERS_REGIONAL = {
textIcon: "VE",
website: "https://www.volcengine.com",
},
"volcengine-agent-plan": {
id: "volcengine-agent-plan",
alias: "veap",
name: "Volcengine Ark Agent Plan",
icon: "local_fire_department",
color: "#DC2626",
textIcon: "VA",
website: "https://console.volcengine.com/ark/region:cn-beijing/subscription/agent-plan",
authHint: "Connect your Volcano Engine account or use an Ark Agent Plan subscription API key.",
},
"volcengine-coding-plan": {
id: "volcengine-coding-plan",
alias: "vecp",
name: "Volcengine Ark Coding Plan",
icon: "code",
color: "#FF6A00",
textIcon: "VC",
website: "https://console.volcengine.com/ark/region:cn-beijing/subscription/coding-plan",
authHint: "Connect your Volcano Engine account or use an Ark Coding Plan subscription API key.",
},
gigachat: {
id: "gigachat",
alias: "gigachat",

View File

@@ -52,6 +52,7 @@ export const SPAWN_CAPABLE_PREFIXES: ReadonlyArray<string> = [
*/
export const SPAWN_CAPABLE_PATTERNS: ReadonlyArray<RegExp> = [
/^\/api\/providers\/[^/]+\/login\/?$/, // pre-existing gap: in LOCAL_ONLY_API_PATTERNS today but never in a spawn-capable deny-list
/^\/api\/providers\/volcengine-plan\/connect\/?$/, // launches Playwright to bind a Volcano Engine console session
/^\/api\/providers\/[^/]+\/refresh-cursor\/?$/, // spawns cursor-agent via renewal.ts (Hard Rules #15 + #17)
/^\/api\/providers\/cursor\/agent-availability\/?$/, // static path (no dynamic segment), but kept in this array alongside its /api/providers/ siblings rather than the flat SPAWN_CAPABLE_PREFIXES array — spawns cursor-agent status via checkCursorAgentAvailability()/getCachedCursorAgentAvailability() (Hard Rules #15 + #17)
/^\/api\/providers\/[^/]+\/chatgpt-web-codex-doctor\/?$/, // spawns via getTunnelRuntimeStatus() → spawnSync("...","runtimes status") (open-sse/executors/chatgpt-web-codex/tunnelClient.ts). Mirrors LOCAL_ONLY_API_PATTERNS in routeGuard.ts; keep the two in sync (GHSA-9q3h-mjm5-f4gj).