feat(volcengine): switch Agent Plan discovery to ListAgentPlanLatestModel

This commit is contained in:
yangsiyuan.rengar
2026-08-19 12:02:24 +08:00
committed by Markus Hartung
parent 34150506f2
commit 07a378c86c
2 changed files with 63 additions and 99 deletions

View File

@@ -7,13 +7,16 @@
* top-level Ark actions, authenticated by the same console cookie + csrf
* token already captured during plan binding (see volcenginePlanBinding.ts).
*
* - Agent Plan: `GetAgentPlanModelMappingMeta` → Result.Data[]
* filter: platform-enabled LLMs plus API-verified canonical registry IDs
* id : RespModelID (already version-suffixed, matches chat endpoint)
* - Agent Plan: `ListAgentPlanLatestModel` → Result.Data[]
* id : ModelId (version-suffixed, matches chat endpoint)
* - Coding Plan: `ListArkCodeLatestModel` → Result.Data[]
* id : ModelId (version-suffixed)
*
* The console API returns only id/name/version/description — NOT capabilities
* Both APIs return the same response shape (ModelId / OutputName / Enabled /
* Description / EnabledThinking). We keep ALL entries — the chat endpoint
* accepts every listed ModelId, and `Enabled` only reflects console visibility.
*
* The console API returns only id/name/description — NOT capabilities
* (contextLength, toolCalling, vision, reasoning). We enrich each discovered
* model from a static family→capability map keyed by the OutputName/ModelName
* prefix, falling back to conservative defaults so new families stay usable
@@ -23,8 +26,6 @@
* persist it via replaceSyncedAvailableModelsForConnection.
*/
import { VOLCENGINE_AGENT_PLAN_MODELS } from "@omniroute/open-sse/config/providers/registry/volcengine/agent-plan/index.ts";
import type { SyncedAvailableModelInput } from "@/lib/db/models/synced";
type JsonRecord = Record<string, unknown>;
@@ -128,8 +129,8 @@ const PLAN_DISCOVERY_CONFIG: Record<
}
> = {
agent: {
action: "GetAgentPlanModelMappingMeta",
payload: { Edition: "agent_plan_personal" },
action: "ListAgentPlanLatestModel",
payload: {},
referer: AGENT_PLAN_REFERER,
requiresAccountId: false,
},
@@ -301,48 +302,20 @@ export function enrichModel(model: DiscoveredVolcModel): SyncedAvailableModelInp
};
}
const AGENT_PLAN_API_VERIFIED_IDS = new Set(VOLCENGINE_AGENT_PLAN_MODELS.map((model) => model.id));
/**
* Parse Agent Plan `GetAgentPlanModelMappingMeta` Result.Data[].
* Parse `ListAgentPlanLatestModel` / `ListArkCodeLatestModel` Result.Data[].
*
* `PlatformAllowStatus` controls console platform visibility/mapping; it is
* NOT an API availability flag. Some canonical Agent Plan chat models (for
* example kimi-k3) are returned with `PlatformAllowStatus=false`, `Type` and
* display metadata unset, while `/api/plan/v3/chat/completions` accepts them.
* Keep those only when they are in the API-verified canonical registry. This
* avoids admitting the same response's media models, auto-router, and legacy
* aliases while still allowing newly platform-enabled LLMs to be discovered.
* Both console APIs return the same response shape: each entry has
* `ModelId` (the version-suffixed ID accepted by the chat endpoint),
* `OutputName` / `ModelName` (the canonical family name used for capability
* enrichment), `Enabled` (console visibility — not API availability), and
* optional `Description` / `EnabledThinking`.
*
* We keep ALL entries with a non-empty `ModelId`. The chat endpoint accepts
* every listed model; `Enabled` only controls whether the model appears in
* the console's model picker, so filtering on it would hide callable models.
*/
export function parseAgentPlanModels(json: JsonRecord): DiscoveredVolcModel[] {
const data = record(json.Result).Data;
const arr = Array.isArray(data) ? data : [];
const out: DiscoveredVolcModel[] = [];
for (const raw of arr) {
const item = record(raw);
const platformEnabledLlm =
item.PlatformAllowStatus === true && stringField(item.Type) === "llm";
const id = stringField(item.RespModelID);
if (!id || (!platformEnabledLlm && !AGENT_PLAN_API_VERIFIED_IDS.has(id))) continue;
const name = stringField(item.RespModelName) || id;
out.push({
id,
name,
...(stringField(item.RespModelVersion)
? { description: `v${stringField(item.RespModelVersion)}` }
: {}),
});
}
return out;
}
/**
* Parse Coding Plan `ListArkCodeLatestModel` Result.Data[].
* The API exposes every catalog entry regardless of subscription state; we
* keep ALL of them (the chat endpoint accepts them, Enabled only reflects
* console visibility) but surface Enabled through the description.
*/
function parseCodingPlanModels(json: JsonRecord): DiscoveredVolcModel[] {
export function parseLatestModelList(json: JsonRecord): DiscoveredVolcModel[] {
const data = record(json.Result).Data;
const arr = Array.isArray(data) ? data : [];
const out: DiscoveredVolcModel[] = [];
@@ -412,8 +385,7 @@ export async function fetchVolcPlanModels(
);
}
const discovered =
kind === "agent" ? parseAgentPlanModels(result.json) : parseCodingPlanModels(result.json);
const discovered = parseLatestModelList(result.json);
if (discovered.length === 0) {
throw new Error(`Volcano ${kind} plan returned no usable models`);
}

View File

@@ -1,27 +1,39 @@
import assert from "node:assert/strict";
import test from "node:test";
import { enrichModel, parseAgentPlanModels } from "@/lib/providers/volcenginePlanModelDiscovery";
import { enrichModel, parseLatestModelList } from "@/lib/providers/volcenginePlanModelDiscovery";
test("Agent Plan discovery keeps canonical API-callable models hidden by the console", () => {
const models = parseAgentPlanModels({
test("Agent Plan discovery keeps all ListAgentPlanLatestModel entries", () => {
// `ListAgentPlanLatestModel` returns the same shape as Coding Plan's
// `ListArkCodeLatestModel`: ModelId / OutputName / Enabled / Description.
// We keep ALL entries — `Enabled` only reflects console visibility, not
// API availability. Previously disabled-but-callable models like
// kimi-k3 must be retained.
const models = parseLatestModelList({
Result: {
Data: [
{
RespModelID: "doubao-seed-evolving",
RespModelName: "doubao-seed-evolving",
PlatformAllowStatus: true,
Type: "llm",
ModelId: "doubao-seed-evolving-latest-version",
OutputName: "doubao-seed-evolving",
Enabled: false,
EnabledThinking: true,
},
{
RespModelID: "kimi-k3",
PlatformAllowStatus: false,
Type: null,
ModelId: "kimi-k3-260701",
OutputName: "kimi-k3",
Enabled: false,
EnabledThinking: true,
},
{
RespModelID: "minimax-m3",
PlatformAllowStatus: false,
Type: null,
ModelId: "auto",
OutputName: "auto",
Enabled: true,
},
{
ModelId: "minimax-m3-modelhub",
OutputName: "minimax-m3",
Enabled: false,
EnabledThinking: true,
},
],
},
@@ -29,49 +41,29 @@ test("Agent Plan discovery keeps canonical API-callable models hidden by the con
assert.deepEqual(
models.map((model) => model.id),
["doubao-seed-evolving", "kimi-k3", "minimax-m3"]
["doubao-seed-evolving-latest-version", "kimi-k3-260701", "auto", "minimax-m3-modelhub"]
);
// OutputName is used as the canonical family name for enrichment.
assert.equal(models[1].name, "kimi-k3");
assert.equal(models[1].enabledThinking, true);
});
test("Agent Plan discovery excludes disabled aliases, auto-routing, and non-chat models", () => {
const models = parseAgentPlanModels({
Result: {
Data: [
{
RespModelID: "future-platform-model",
PlatformAllowStatus: true,
Type: "llm",
},
{
RespModelID: "glm-5.2",
PlatformAllowStatus: false,
Type: null,
},
{
RespModelID: "auto",
PlatformAllowStatus: false,
Type: null,
},
{
RespModelID: "doubao-seed-tts-2.0",
PlatformAllowStatus: true,
Type: "audio",
},
],
},
});
assert.deepEqual(
models.map((model) => model.id),
["future-platform-model"]
);
});
test("Agent Plan canonical Kimi K3 receives registry-equivalent capabilities", () => {
const kimi = enrichModel({ id: "kimi-k3", name: "kimi-k3" });
test("enrichment maps context/vision/tools from the OutputName family", () => {
const kimi = enrichModel({ id: "kimi-k3-260701", name: "kimi-k3" });
assert.equal(kimi.inputTokenLimit, 1048576);
assert.equal(kimi.supportsVision, true);
assert.equal(kimi.supportsTools, true);
assert.equal(kimi.supportsThinking, true);
const glm = enrichModel({ id: "glm-5-3-260801", name: "glm-5.3" });
assert.equal(glm.inputTokenLimit, 1048576);
assert.equal(glm.supportsVision, false);
const doubao = enrichModel({
id: "doubao-seed-evolving-latest-version",
name: "doubao-seed-evolving",
});
assert.equal(doubao.inputTokenLimit, 1048576);
assert.equal(doubao.supportsVision, true);
});