mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-25 08:32:11 +03:00
feat(volcengine): add Ark plan providers
This commit is contained in:
committed by
Markus Hartung
parent
f58e8bef6f
commit
d732cf615d
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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",
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -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":
|
||||
|
||||
317
open-sse/services/usage/volcenginePlan.ts
Normal file
317
open-sse/services/usage/volcenginePlan.ts
Normal 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}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
32
src/app/api/providers/volcengine-plan/connect/route.ts
Normal file
32
src/app/api/providers/volcengine-plan/connect/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
273
src/lib/providers/volcenginePlanBinding.ts
Normal file
273
src/lib/providers/volcenginePlanBinding.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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\/?$/,
|
||||
];
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user