feat(volcengine): live model discovery for Ark plan providers

Replace the static curated model lists for volcengine-agent-plan and
volcengine-coding-plan with live discovery from the console APIs
(GetAgentPlanModelMappingMeta / ListArkCodeLatestModel), authenticated by
the console cookie+csrf already captured at plan binding time.

- Add volcenginePlanModelDiscovery.ts: fetch + parse + capability enrichment
  (family->contextLength/vision/reasoning map, conservative default fallback).
  Console calls go through a dynamic undici import to bypass OmniRoute's
  global fetch patch (built for LLM provider traffic, reroutes console hits).
  Coding plan's ListArkCodeLatestModel needs {AccountId:<number>} extracted
  from the console cookie; agent plan's GetAgentPlanModelMappingMeta filters
  PlatformAllowStatus===true && Type==='llm'.
- Remove both plan ids from CURATED_MODEL_ONLY_PROVIDERS so synced models
  merge into /v1/models and the dashboard Sync Models button works.
- sync-models route: short-circuit to console discovery for plan providers
  (the chat API has no /models endpoint); persist via
  replaceSyncedAvailableModelsForConnection.
- volcenginePlanBinding: set autoSync:true on new plan connections so the
  24h modelSyncScheduler refreshes them automatically.
- volcPlanAutoSyncBackfill: idempotent boot-time backfill so pre-existing
  plan connections also enter the scheduler.

Verified end-to-end on local OmniRoute build against live Volcano console:
agent plan synced 7 LLMs, coding plan synced 11 models, /v1/models exposes
all of them (incl. new glm-5-3-260801 / deepseek-v4-flash-260801).
This commit is contained in:
yangsiyuan.rengar
2026-08-17 16:52:06 +08:00
committed by Markus Hartung
parent d732cf615d
commit 76ac1c8b7e
6 changed files with 561 additions and 14 deletions

View File

@@ -21,6 +21,11 @@ import {
import { autoSyncCodexProfilesFromLiveCatalog } from "@/lib/cli-helper/codexProfileAutoSync";
import { autoSyncClaudeProfilesFromLiveCatalog } from "@/lib/cli-helper/claudeProfileAutoSync";
import { providerUsesCuratedModelsOnly } from "@/lib/providers/modelListingCapability";
import {
fetchVolcPlanModels,
providerToVolcPlanKind,
} from "@/lib/providers/volcenginePlanModelDiscovery";
import { replaceSyncedAvailableModelsForConnection } from "@/lib/db/models";
import { GET as getProviderModels } from "../models/route";
import { isDegradedDiscovery } from "./degradedLocalCatalog";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
@@ -423,6 +428,84 @@ export async function POST(request: Request, { params }: { params: Promise<{ id:
logProvider = toNonEmptyString(connection.provider) || "unknown";
channelLabel = getModelSyncChannelLabel(connection);
// Volcano Ark plan providers: discover models live from the console API
// (cookie+csrf captured at bind time). The chat API has no /models
// endpoint, so the default discovery path below cannot serve them.
const volcPlanKind = providerToVolcPlanKind(logProvider);
if (volcPlanKind) {
const psd =
connection.providerSpecificData && typeof connection.providerSpecificData === "object"
? (connection.providerSpecificData as JsonRecord)
: {};
const cookie = toNonEmptyString(psd.volcConsoleCookie) || "";
const csrf = toNonEmptyString(psd.volcCsrfToken) || "";
const duration = Date.now() - start;
let discovered;
try {
discovered = await fetchVolcPlanModels(volcPlanKind, cookie, csrf);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
await saveCallLog({
method: "POST",
path: `/api/providers/${id}/sync-models`,
status: 401,
model: "model-sync",
provider: logProvider,
sourceFormat: "-",
connectionId: id,
duration,
error: message,
requestType: "model-sync",
...(channelLabel ? { responseBody: { channel: channelLabel } } : {}),
}).catch(() => undefined);
return NextResponse.json(
{ error: sanitizeErrorMessage(message) || "Volcano plan discovery failed" },
{ status: 401 }
);
}
const previous = await getSyncedAvailableModelsForConnection(logProvider, id);
const synced = await replaceSyncedAvailableModelsForConnection(logProvider, id, discovered);
const prevIds = new Set(previous.map((m) => String(m.id)));
const added = synced.filter((m) => !prevIds.has(String(m.id))).length;
const removed = previous.filter(
(m) => !synced.some((n) => String(n.id) === String(m.id))
).length;
await saveCallLog({
method: "GET",
path: `/api/providers/${id}/models`,
status: 200,
model: "model-sync",
provider: logProvider,
sourceFormat: "console-discovery",
connectionId: id,
duration: Date.now() - start,
requestType: "model-sync",
responseBody: {
source: "volcengine-plan-console-discovery",
plan: volcPlanKind,
syncedModels: synced.length,
added,
removed,
provider: logProvider,
channel: channelLabel,
mode,
},
}).catch(() => undefined);
return NextResponse.json({
ok: true,
provider: logProvider,
connectionId: id,
source: "volcengine-plan-console-discovery",
plan: volcPlanKind,
mode,
syncedModels: synced.length,
availableModelsCount: synced.length,
modelChanges: { added, removed, total: added + removed },
models: synced,
});
}
if (providerUsesCuratedModelsOnly(logProvider)) {
const [removedSyncedLists, removedImportedModelIds] = await Promise.all([
deleteSyncedAvailableModelsForProvider(logProvider),