打通多模型并行对比接口与采纳结果。

冻结消息上下文后并行重放多个配置,并顺带挂上定价接口路由。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-14 01:14:33 +08:00
co-authored by Cursor
parent a6be5f7f62
commit 6bdca7ccf7
3 changed files with 342 additions and 1 deletions
+238
View File
@@ -0,0 +1,238 @@
import { randomUUID } from "node:crypto";
import {
getApiProfile,
listApiProfiles,
profileToLlmConfig,
} from "../config/api-profiles.js";
import {
OpenAiCompatibleProvider,
type ChatMessage as LlmChatMessage,
type ToolCallPayload,
} from "../llm/client.js";
import type { LlmContextTrace } from "../types/context-trace.js";
import type { GenerationParameters } from "../types/preset.js";
export type CompareCandidateStatus = "pending" | "running" | "done" | "error";
export type CompareCandidate = {
profileId: string;
profileName: string;
model: string;
status: CompareCandidateStatus;
content?: string;
thinking?: string;
error?: string;
usage?: {
totalTokens?: number;
cachedTokens?: number;
cacheMissTokens?: number;
};
finishedAt?: string;
};
export type ModelCompareJob = {
id: string;
sessionId: string;
messageId: string;
createdAt: string;
status: "running" | "done" | "error";
/** 原消息正文,便于预览对照 */
original: {
text: string;
thinking?: string;
model?: string;
};
candidates: CompareCandidate[];
};
const jobs = new Map<string, ModelCompareJob>();
const JOB_TTL_MS = 30 * 60 * 1000;
function pruneJobs(): void {
const cutoff = Date.now() - JOB_TTL_MS;
for (const [id, job] of jobs) {
if (Date.parse(job.createdAt) < cutoff) jobs.delete(id);
}
}
/** 把 contextTrace 里冻住的条目还原成可发给 LLM 的 messages */
export function reconstructLlmMessages(
trace: LlmContextTrace,
): LlmChatMessage[] {
const out: LlmChatMessage[] = [];
for (const item of trace.messages) {
const role = String(item.role ?? "user");
const raw = typeof item.content === "string" ? item.content : "";
const restored = tryParseExtras(raw);
if (restored) {
out.push(restored);
continue;
}
if (role === "assistant") {
out.push({ role: "assistant", content: raw || null });
} else if (role === "tool") {
out.push({ role: "tool", content: raw, tool_call_id: "unknown" });
} else if (role === "system") {
out.push({ role: "system", content: raw });
} else {
out.push({ role: "user", content: raw });
}
}
return out;
}
function tryParseExtras(raw: string): LlmChatMessage | null {
const trimmed = raw.trim();
if (!trimmed.startsWith("{")) return null;
try {
const parsed = JSON.parse(trimmed) as {
role?: string;
content?: string | null;
tool_calls?: ToolCallPayload[];
tool_call_id?: string;
};
if (!parsed || typeof parsed !== "object" || !parsed.role) return null;
if (parsed.role === "assistant") {
return {
role: "assistant",
content: parsed.content ?? null,
...(Array.isArray(parsed.tool_calls)
? { tool_calls: parsed.tool_calls }
: {}),
};
}
if (parsed.role === "tool" && parsed.tool_call_id) {
return {
role: "tool",
content: typeof parsed.content === "string" ? parsed.content : "",
tool_call_id: parsed.tool_call_id,
};
}
if (parsed.role === "system" || parsed.role === "user") {
return {
role: parsed.role,
content: typeof parsed.content === "string" ? parsed.content : "",
};
}
} catch {
/* plain text */
}
return null;
}
export function createCompareJob(input: {
sessionId: string;
messageId: string;
profileIds: string[];
original: ModelCompareJob["original"];
}): ModelCompareJob {
pruneJobs();
const uniqueIds = [...new Set(input.profileIds.map((id) => id.trim()).filter(Boolean))];
if (uniqueIds.length === 0) {
throw new Error("请至少选择一个 API 配置");
}
const candidates: CompareCandidate[] = uniqueIds.map((profileId) => {
const profile = getApiProfile(profileId);
if (!profile?.apiKey?.trim()) {
throw new Error(`API 配置不可用:${profileId}`);
}
return {
profileId: profile.id,
profileName: profile.name,
model: profile.model,
status: "pending" as const,
};
});
const job: ModelCompareJob = {
id: randomUUID(),
sessionId: input.sessionId,
messageId: input.messageId,
createdAt: new Date().toISOString(),
status: "running",
original: input.original,
candidates,
};
jobs.set(job.id, job);
return job;
}
export function getCompareJob(jobId: string): ModelCompareJob | null {
pruneJobs();
return jobs.get(jobId) ?? null;
}
export function listProfilesForCompare(): Array<{
id: string;
name: string;
model: string;
hasKey: boolean;
}> {
return listApiProfiles().map((p) => ({
id: p.id,
name: p.name,
model: p.model,
hasKey: Boolean(p.apiKey?.trim()),
}));
}
/** 并行用各 profile 重放同一 contextTrace(不改会话) */
export async function runCompareJob(
job: ModelCompareJob,
trace: LlmContextTrace,
): Promise<ModelCompareJob> {
const messages = reconstructLlmMessages(trace);
if (messages.length === 0) {
job.status = "error";
for (const c of job.candidates) {
c.status = "error";
c.error = "上下文为空";
c.finishedAt = new Date().toISOString();
}
return job;
}
const generation = trace.generation as GenerationParameters | undefined;
await Promise.all(
job.candidates.map(async (candidate) => {
candidate.status = "running";
try {
const profile = getApiProfile(candidate.profileId);
if (!profile?.apiKey?.trim()) {
throw new Error("配置不存在或缺少 API Key");
}
const llm = new OpenAiCompatibleProvider(profileToLlmConfig(profile));
const result = await llm.complete(messages, {
generation,
caller: `model-compare:${candidate.profileId}`,
});
candidate.content = result.content ?? "";
candidate.thinking = result.reasoning;
candidate.usage = result.usage
? {
totalTokens: result.usage.totalTokens,
cachedTokens: result.usage.cachedTokens,
cacheMissTokens: result.usage.cacheMissTokens,
}
: undefined;
if (result.model) candidate.model = result.model;
candidate.status = "done";
} catch (err) {
candidate.status = "error";
candidate.error = err instanceof Error ? err.message : String(err);
} finally {
candidate.finishedAt = new Date().toISOString();
}
}),
);
const anyDone = job.candidates.some((c) => c.status === "done");
job.status = anyDone ? "done" : "error";
return job;
}
export function findCandidate(
job: ModelCompareJob,
profileId: string,
): CompareCandidate | undefined {
return job.candidates.find((c) => c.profileId === profileId);
}
+48 -1
View File
@@ -70,6 +70,8 @@ const server = createServer(async (req, res) => {
"GET /api/directors",
"GET /api/modules",
"GET /api/stats/tokens",
"GET /api/stats/pricing",
"POST /api/stats/pricing/sources",
],
});
return;
@@ -240,7 +242,7 @@ const server = createServer(async (req, res) => {
}
const messageActionMatch = sub.match(
/^\/messages\/([^/]+)\/(edit|refresh|variant|delete|restart)$/,
/^\/messages\/([^/]+)\/(edit|refresh|variant|delete|restart|compare)$/,
);
if (req.method === "POST" && messageActionMatch) {
const messageId = decodeURIComponent(messageActionMatch[1]);
@@ -248,6 +250,7 @@ const server = createServer(async (req, res) => {
const body = JSON.parse(await readBody(req).catch(() => "{}")) as {
text?: string;
direction?: string;
profileIds?: string[];
};
let view;
try {
@@ -277,6 +280,17 @@ const server = createServer(async (req, res) => {
view = await sessionManager.deleteMessage(sessionId, messageId);
} else if (action === "restart") {
view = await sessionManager.restartFromMessage(sessionId, messageId);
} else if (action === "compare") {
const profileIds = Array.isArray(body.profileIds)
? body.profileIds.filter((x): x is string => typeof x === "string")
: [];
const job = await sessionManager.startModelCompare(
sessionId,
messageId,
profileIds,
);
json(res, 200, job);
return;
} else {
json(res, 400, { error: "未知 action" });
return;
@@ -291,6 +305,39 @@ const server = createServer(async (req, res) => {
return;
}
const compareJobMatch = sub.match(/^\/compare\/([^/]+)(?:\/(adopt))?$/);
if (compareJobMatch) {
const jobId = decodeURIComponent(compareJobMatch[1]);
const adopt = compareJobMatch[2] === "adopt";
try {
if (req.method === "GET" && !adopt) {
json(res, 200, sessionManager.getModelCompare(sessionId, jobId));
return;
}
if (req.method === "POST" && adopt) {
const body = JSON.parse(await readBody(req).catch(() => "{}")) as {
profileId?: string;
};
if (!body.profileId?.trim()) {
json(res, 400, { error: "缺少 profileId" });
return;
}
const view = await sessionManager.adoptModelCompare(
sessionId,
jobId,
body.profileId.trim(),
);
json(res, 200, view);
return;
}
} catch (err) {
json(res, 400, {
error: err instanceof Error ? err.message : "对比操作失败",
});
return;
}
}
if (req.method === "POST" && sub === "/lifecycle") {
const body = JSON.parse(await readBody(req)) as { stage?: string };
if (body.stage !== "design" && body.stage !== "play") {
+56
View File
@@ -0,0 +1,56 @@
import { describe, expect, it } from "vitest";
import { reconstructLlmMessages } from "../src/server/model-compare.js";
import { buildContextTrace } from "../src/types/context-trace.js";
describe("model-compare reconstructLlmMessages", () => {
it("restores plain roles", () => {
const trace = buildContextTrace({
caller: "test",
messages: [
{ role: "system", content: "sys" },
{ role: "user", content: "hi" },
],
model: "m1",
});
expect(reconstructLlmMessages(trace)).toEqual([
{ role: "system", content: "sys" },
{ role: "user", content: "hi" },
]);
});
it("restores tool_calls extras frozen as JSON content", () => {
const trace = buildContextTrace({
caller: "test",
messages: [
{
role: "assistant",
content: null,
tool_calls: [
{
id: "c1",
type: "function",
function: { name: "foo", arguments: "{}" },
},
],
},
{ role: "tool", content: "ok", tool_call_id: "c1" },
],
});
const msgs = reconstructLlmMessages(trace);
expect(msgs[0]).toMatchObject({
role: "assistant",
tool_calls: [
{
id: "c1",
type: "function",
function: { name: "foo", arguments: "{}" },
},
],
});
expect(msgs[1]).toEqual({
role: "tool",
content: "ok",
tool_call_id: "c1",
});
});
});