mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
feat: model recommend beta version
This commit is contained in:
@@ -43,7 +43,9 @@
|
||||
"check": "vp check"
|
||||
},
|
||||
"dependencies": {
|
||||
"bailian-cli-core": "workspace:*"
|
||||
"bailian-cli-core": "workspace:*",
|
||||
"boxen": "catalog:",
|
||||
"chalk": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@clack/prompts": "^0.7.0",
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
import {
|
||||
analyzeIntent,
|
||||
buildDocLink,
|
||||
type Config,
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
type GetModelsOptions,
|
||||
type GlobalFlags,
|
||||
getModels,
|
||||
type IntentProfile,
|
||||
isInteractive,
|
||||
type PipelineStep,
|
||||
type RecommendedModel,
|
||||
type RecommendResult,
|
||||
rankModels,
|
||||
recallSemantic,
|
||||
} from "bailian-cli-core";
|
||||
import boxen from "boxen";
|
||||
import chalk, { Chalk, type ChalkInstance } from "chalk";
|
||||
import { emitBare, emitResult } from "../../output/output.ts";
|
||||
import { createSpinner } from "../../output/progress.ts";
|
||||
import { failIfMissing, promptText } from "../../output/prompt.ts";
|
||||
|
||||
function formatContextWindow(tokens: number): string {
|
||||
if (tokens >= 1_000_000)
|
||||
return `${(tokens / 1_000_000).toFixed(tokens % 1_000_000 === 0 ? 0 : 1)}M`;
|
||||
if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(tokens % 1_000 === 0 ? 0 : 1)}K`;
|
||||
return String(tokens);
|
||||
}
|
||||
|
||||
const MODALITY_LABELS: Record<string, string> = {
|
||||
Text: "文本",
|
||||
Image: "图片",
|
||||
Video: "视频",
|
||||
Audio: "音频",
|
||||
};
|
||||
const CAPABILITY_LABELS: Record<string, string> = {
|
||||
TG: "文本生成",
|
||||
VU: "视觉理解",
|
||||
IG: "图像生成",
|
||||
VG: "视频生成",
|
||||
TTS: "语音合成",
|
||||
ASR: "语音识别",
|
||||
Reasoning: "推理",
|
||||
};
|
||||
const BUDGET_LABELS: Record<string, string> = {
|
||||
low: "低成本优先",
|
||||
medium: "适中",
|
||||
high: "高投入",
|
||||
};
|
||||
const QUALITY_LABELS: Record<string, string> = {
|
||||
flagship: "旗舰优先",
|
||||
balanced: "均衡",
|
||||
"cost-optimized": "性价比优先",
|
||||
};
|
||||
|
||||
function formatIntentSummary(intent: IntentProfile, noColor: boolean): string {
|
||||
const colorize = noColor ? new Chalk({ level: 0 }) : chalk;
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(colorize.cyan.bold("需求理解"));
|
||||
|
||||
if (intent.taskSummary) {
|
||||
lines.push("");
|
||||
lines.push(intent.taskSummary);
|
||||
}
|
||||
|
||||
if (intent.scenarioHints.length) {
|
||||
lines.push("");
|
||||
lines.push(`${colorize.dim("场景特征")} ${intent.scenarioHints.join(" · ")}`);
|
||||
}
|
||||
|
||||
const inputLabels = intent.inputModality.map((mod) => MODALITY_LABELS[mod] ?? mod);
|
||||
const outputLabels = intent.outputModality.map((mod) => MODALITY_LABELS[mod] ?? mod);
|
||||
if (inputLabels.length || outputLabels.length) {
|
||||
lines.push("");
|
||||
const parts: string[] = [];
|
||||
if (inputLabels.length) parts.push(`${colorize.dim("输入")} ${inputLabels.join(", ")}`);
|
||||
if (outputLabels.length) parts.push(`${colorize.dim("输出")} ${outputLabels.join(", ")}`);
|
||||
lines.push(parts.join(" "));
|
||||
}
|
||||
|
||||
const capLabels = intent.requiredCapabilities.map((cap) => CAPABILITY_LABELS[cap] ?? cap);
|
||||
if (capLabels.length) {
|
||||
lines.push(`${colorize.dim("所需能力")} ${capLabels.join(", ")}`);
|
||||
}
|
||||
|
||||
const budgetLabel = BUDGET_LABELS[intent.budget] ?? intent.budget;
|
||||
const qualityLabel = QUALITY_LABELS[intent.qualityPreference] ?? intent.qualityPreference;
|
||||
lines.push("");
|
||||
lines.push(
|
||||
`${colorize.dim("预算倾向")} ${budgetLabel} ${colorize.dim("质量偏好")} ${qualityLabel}`,
|
||||
);
|
||||
|
||||
if (intent.segments?.length) {
|
||||
lines.push("");
|
||||
lines.push(colorize.dim("任务拆解"));
|
||||
for (const [idx, segment] of intent.segments.entries()) {
|
||||
const outMods = segment.outputModality.map((mod) => MODALITY_LABELS[mod] ?? mod).join(", ");
|
||||
lines.push(
|
||||
` ${colorize.dim(`${idx + 1}.`)} ${segment.step}${outMods ? colorize.dim(` → ${outMods}`) : ""}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return boxen(lines.join("\n"), {
|
||||
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
||||
margin: { top: 0, bottom: 0, left: 1, right: 0 },
|
||||
borderColor: "cyan",
|
||||
borderStyle: "round",
|
||||
dimBorder: true,
|
||||
});
|
||||
}
|
||||
|
||||
const RECOMMEND_LABELS = ["最佳推荐", "次优选择", "备选参考"];
|
||||
|
||||
function renderCard(rec: RecommendedModel, index: number, colorize: ChalkInstance): string {
|
||||
const labelColors = [colorize.green.bold, colorize.blue.bold, colorize.magenta.bold];
|
||||
const colorFn = labelColors[index] ?? colorize.white.bold;
|
||||
const label = RECOMMEND_LABELS[index] ?? `推荐 #${index + 1}`;
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(colorFn(`⬢ 推荐 #${index + 1} — ${label}`));
|
||||
lines.push("");
|
||||
lines.push(`${colorize.bold(rec.name)} ${colorize.dim(`(${rec.model})`)}`);
|
||||
lines.push("");
|
||||
lines.push(`${colorize.cyan("推荐理由")} ${rec.reason}`);
|
||||
|
||||
if (rec.highlights.length) {
|
||||
lines.push("");
|
||||
lines.push(
|
||||
rec.highlights.map((highlight) => colorize.bgGray.white(` ${highlight} `)).join(" "),
|
||||
);
|
||||
}
|
||||
|
||||
const meta: string[] = [];
|
||||
if (rec.contextWindow) meta.push(`上下文 ${formatContextWindow(rec.contextWindow)}`);
|
||||
if (rec.maxOutputTokens) meta.push(`最大输出 ${formatContextWindow(rec.maxOutputTokens)}`);
|
||||
if (meta.length) {
|
||||
lines.push("");
|
||||
lines.push(colorize.dim(meta.join(" · ")));
|
||||
}
|
||||
|
||||
const docLink = buildDocLink(rec.docUrl);
|
||||
if (docLink) {
|
||||
lines.push("");
|
||||
lines.push(colorize.dim(`文档 ${docLink}`));
|
||||
}
|
||||
|
||||
return boxen(lines.join("\n"), {
|
||||
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
||||
margin: { top: 0, bottom: 0, left: 1, right: 0 },
|
||||
borderColor: "gray",
|
||||
borderStyle: "round",
|
||||
dimBorder: true,
|
||||
});
|
||||
}
|
||||
|
||||
function formatSingleResult(results: RecommendedModel[], noColor: boolean): string {
|
||||
const colorize = noColor ? new Chalk({ level: 0 }) : chalk;
|
||||
return results.map((rec, idx) => renderCard(rec, idx, colorize)).join("\n");
|
||||
}
|
||||
|
||||
function formatPipelineResult(summary: string, steps: PipelineStep[], noColor: boolean): string {
|
||||
const colorize = noColor ? new Chalk({ level: 0 }) : chalk;
|
||||
const lines: string[] = [];
|
||||
lines.push(` ${colorize.yellow.bold("⚡ 组合方案")} ${summary}`);
|
||||
|
||||
for (const [stepIdx, { step, recommendations, warnings }] of steps.entries()) {
|
||||
lines.push("");
|
||||
lines.push(colorize.bold(` ━━━ Step ${stepIdx + 1}: ${step} ━━━`));
|
||||
|
||||
if (warnings?.length) {
|
||||
for (const warning of warnings) {
|
||||
lines.push(` ${colorize.yellow("⚠")} ${colorize.yellow(warning)}`);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
lines.push(recommendations.map((rec, idx) => renderCard(rec, idx, colorize)).join("\n"));
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function formatResult(result: RecommendResult, noColor: boolean): string {
|
||||
if (result.type === "pipeline") {
|
||||
return formatPipelineResult(result.summary, result.steps, noColor);
|
||||
}
|
||||
return formatSingleResult(result.recommendations, noColor);
|
||||
}
|
||||
|
||||
function isEmptyResult(result: RecommendResult): boolean {
|
||||
if (result.type === "pipeline") return result.steps.length === 0;
|
||||
return result.recommendations.length === 0;
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
name: "advisor recommend",
|
||||
description:
|
||||
"Recommend the best models for your use case (intent analysis → candidate recall → LLM ranking)",
|
||||
usage: "bl advisor recommend <prompt> [flags]",
|
||||
options: [
|
||||
{
|
||||
flag: "--message <text>",
|
||||
description: "Describe your requirements (alternative to positional prompt)",
|
||||
},
|
||||
{
|
||||
flag: "--dry-run",
|
||||
description: "Show intent analysis and candidate list without LLM ranking",
|
||||
},
|
||||
{
|
||||
flag: "--output <format>",
|
||||
description: "Output format: text (default in TTY), json, yaml",
|
||||
},
|
||||
],
|
||||
examples: [
|
||||
'bl advisor recommend --message "我要做一个能理解图片的客服机器人"',
|
||||
'bl advisor recommend --message "做一个Agent自动根据用户意图生成动画片"',
|
||||
'bl advisor recommend --message "法律合同审查,要求高精准度"',
|
||||
'bl advisor recommend --message "做一个低成本高并发的在线客服" --output json',
|
||||
'bl advisor recommend --message "长文本摘要" --dry-run',
|
||||
"bl advisor recommend # 交互式输入需求",
|
||||
],
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
const positional = ((flags as Record<string, unknown>)._positional as string[]) ?? [];
|
||||
let userInput = (flags.message as string) || positional.join(" ");
|
||||
|
||||
if (!userInput.trim()) {
|
||||
if (isInteractive({ nonInteractive: config.nonInteractive })) {
|
||||
const hint = await promptText({ message: "描述你的需求:" });
|
||||
if (!hint) {
|
||||
process.stderr.write("已取消。\n");
|
||||
process.exit(1);
|
||||
}
|
||||
userInput = hint;
|
||||
} else {
|
||||
failIfMissing("message", 'bl advisor recommend "你的需求"');
|
||||
}
|
||||
}
|
||||
|
||||
const top = 3;
|
||||
const format = detectOutputFormat(config.output);
|
||||
|
||||
const modelsOptions: GetModelsOptions = {
|
||||
onPrepareStart: () => process.stderr.write("初始化中...\n"),
|
||||
};
|
||||
process.stderr.write("正在分析需求...\n");
|
||||
const [allModels, intent] = await Promise.all([
|
||||
getModels(config, modelsOptions),
|
||||
analyzeIntent(config, userInput),
|
||||
]);
|
||||
|
||||
if (intent.confidence === 0) {
|
||||
process.stderr.write("需求分析超时,使用默认参数继续...\n");
|
||||
} else {
|
||||
process.stderr.write("\n");
|
||||
}
|
||||
|
||||
// Stage 2: Candidate Recall (semantic recall, auto-builds embeddings on first run)
|
||||
const candidates = await recallSemantic(config, allModels, userInput, 50, intent);
|
||||
|
||||
if (config.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
userInput,
|
||||
intent,
|
||||
candidateCount: candidates.length,
|
||||
candidates: candidates.map(({ model, score }) => ({
|
||||
model: model.model,
|
||||
score,
|
||||
})),
|
||||
top,
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Stage 3: LLM Ranking
|
||||
const spinner = createSpinner("正在推荐最佳模型...");
|
||||
spinner.start();
|
||||
|
||||
const result = await rankModels(config, candidates, intent, userInput, top);
|
||||
|
||||
spinner.stop();
|
||||
|
||||
if (isEmptyResult(result)) {
|
||||
emitBare("暂无满足该需求的模型。");
|
||||
return;
|
||||
}
|
||||
|
||||
if (format !== "text") {
|
||||
emitResult(result, format);
|
||||
return;
|
||||
}
|
||||
|
||||
emitBare(formatIntentSummary(intent, config.noColor));
|
||||
emitBare("");
|
||||
emitBare(formatResult(result, config.noColor));
|
||||
},
|
||||
});
|
||||
@@ -35,6 +35,7 @@ import consoleCall from "./console/call.ts";
|
||||
import usageFree from "./usage/free.ts";
|
||||
import pipelineRun from "./pipeline/run.ts";
|
||||
import pipelineValidate from "./pipeline/validate.ts";
|
||||
import advisorRecommend from "./advisor/recommend.ts";
|
||||
|
||||
/** Command registry map (no dependency on registry.ts — safe for build-time import). */
|
||||
export const commands: Record<string, Command> = {
|
||||
@@ -72,5 +73,6 @@ export const commands: Record<string, Command> = {
|
||||
"config show": configShow,
|
||||
"config set": configSet,
|
||||
"config export-schema": configExportSchema,
|
||||
"advisor recommend": advisorRecommend,
|
||||
update: update,
|
||||
};
|
||||
|
||||
@@ -63,7 +63,8 @@ const NO_AUTH_SETUP = [
|
||||
];
|
||||
|
||||
async function main() {
|
||||
const argv = process.argv.slice(2);
|
||||
let argv = process.argv.slice(2);
|
||||
if (argv[0] === "--") argv = argv.slice(1);
|
||||
|
||||
if (argv.includes("--version") || argv.includes("-v")) {
|
||||
process.stdout.write(`bl ${CLI_VERSION}\n`);
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCli } from "./helpers.ts";
|
||||
|
||||
describe("e2e: advisor recommend", () => {
|
||||
test("advisor 分组展示子命令帮助且成功退出", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli(["advisor"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(`${stdout}\n${stderr}`).toMatch(/advisor|recommend/i);
|
||||
});
|
||||
|
||||
test("advisor recommend --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["advisor", "recommend", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/recommend|--message|dry-run/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!isDashScopeE2EReady())("e2e: advisor recommend(DashScope)", () => {
|
||||
test("advisor recommend 缺少 --message 时打印帮助并退出 (0)", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"advisor",
|
||||
"recommend",
|
||||
"--non-interactive",
|
||||
]);
|
||||
expect(exitCode).toBe(0);
|
||||
expect(`${stdout}\n${stderr}`).toMatch(/--message|Usage:/i);
|
||||
});
|
||||
|
||||
test("advisor recommend --dry-run 输出意图分析和候选列表", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"advisor",
|
||||
"recommend",
|
||||
"--dry-run",
|
||||
"--message",
|
||||
"我想做一个能理解图片的客服机器人",
|
||||
"--non-interactive",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
userInput?: string;
|
||||
intent?: { requiredCapabilities?: string[]; inputModality?: string[] };
|
||||
candidateCount?: number;
|
||||
candidates?: Array<{ model?: string; score?: number }>;
|
||||
}>(stdout);
|
||||
expect(data.userInput).toBe("我想做一个能理解图片的客服机器人");
|
||||
expect(data.intent?.requiredCapabilities).toContain("VU");
|
||||
expect(data.intent?.inputModality).toContain("Image");
|
||||
expect(data.candidateCount).toBeGreaterThan(0);
|
||||
expect(data.candidates?.[0]?.model).toBeDefined();
|
||||
expect(data.candidates?.[0]?.score).toBeGreaterThan(0);
|
||||
}, 60_000);
|
||||
|
||||
test("advisor recommend 完整推荐流程返回结果", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"advisor",
|
||||
"recommend",
|
||||
"--message",
|
||||
"低成本高并发的在线客服",
|
||||
"--non-interactive",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
type?: string;
|
||||
recommendations?: Array<{
|
||||
model?: string;
|
||||
name?: string;
|
||||
reason?: string;
|
||||
}>;
|
||||
}>(stdout);
|
||||
expect(data.type).toBe("single");
|
||||
expect(data.recommendations?.length).toBeGreaterThan(0);
|
||||
expect(data.recommendations?.[0]?.model).toBeDefined();
|
||||
expect(data.recommendations?.[0]?.reason).toBeDefined();
|
||||
}, 120_000);
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { Config } from "../config/schema.ts";
|
||||
import { BailianError } from "../errors/base.ts";
|
||||
import { ExitCode } from "../errors/codes.ts";
|
||||
import { ApiSource } from "./sources/api.ts";
|
||||
import { CatalogSource } from "./sources/catalog.ts";
|
||||
import type { ModelSource } from "./sources/types.ts";
|
||||
import type { ModelProfile } from "./types.ts";
|
||||
|
||||
export interface GetModelsOptions {
|
||||
onPrepareStart?: () => void;
|
||||
}
|
||||
|
||||
export async function getModels(
|
||||
config: Config,
|
||||
options?: GetModelsOptions,
|
||||
): Promise<ModelProfile[]> {
|
||||
const sources: ModelSource[] = [
|
||||
new CatalogSource({ onPrepareStart: options?.onPrepareStart }),
|
||||
new ApiSource(config),
|
||||
];
|
||||
|
||||
for (const source of sources) {
|
||||
if (source.available()) {
|
||||
const models = await source.load();
|
||||
if (models.length > 0) return models;
|
||||
}
|
||||
}
|
||||
|
||||
// CatalogSource not available → trigger install + load
|
||||
const catalog = sources[0] as CatalogSource;
|
||||
const models = await catalog.load();
|
||||
if (models.length > 0) return models;
|
||||
|
||||
throw new BailianError("No model data available.", ExitCode.GENERAL);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { IntentProfile } from "../types.ts";
|
||||
import { Budgets, Capabilities, Complexities, ContextNeeds, QualityPreferences } from "../types.ts";
|
||||
|
||||
export const DEFAULT_INTENT: IntentProfile = {
|
||||
complexity: Complexities.Single,
|
||||
taskSummary: "",
|
||||
scenarioHints: [],
|
||||
inputModality: [],
|
||||
outputModality: [],
|
||||
requiredCapabilities: [Capabilities.TG],
|
||||
requiredFeatures: [],
|
||||
budget: Budgets.Medium,
|
||||
contextNeed: ContextNeeds.Standard,
|
||||
qualityPreference: QualityPreferences.Balanced,
|
||||
confidence: 0,
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
export { DEFAULT_INTENT } from "./defaults.ts";
|
||||
export {
|
||||
INTENT_MODEL,
|
||||
INTENT_SYSTEM_PROMPT,
|
||||
PIPELINE_SYSTEM_PROMPT,
|
||||
RANKING_MODEL,
|
||||
RANKING_MODEL_FAST,
|
||||
SINGLE_SYSTEM_PROMPT,
|
||||
} from "./prompts.ts";
|
||||
export {
|
||||
CONTEXT_THRESHOLDS,
|
||||
FALLBACK_THRESHOLD,
|
||||
GENERATION_CAPS,
|
||||
MAX_CANDIDATES,
|
||||
MIN_CANDIDATES,
|
||||
SNAPSHOT_DATE_RE,
|
||||
TEXT_CAPS,
|
||||
} from "./scoring.ts";
|
||||
@@ -0,0 +1,143 @@
|
||||
export const INTENT_MODEL = "qwen-turbo";
|
||||
export const RANKING_MODEL = "qwen3.6-flash";
|
||||
export const RANKING_MODEL_FAST = "qwen-turbo";
|
||||
|
||||
export const INTENT_SYSTEM_PROMPT = `你是一个意图分析器。根据用户的需求描述,先理解用户场景,再提取结构化信息。
|
||||
|
||||
## 分析步骤
|
||||
1. 用一句话总结用户的核心需求(taskSummary),要体现具体场景而非泛泛描述
|
||||
2. 推断场景特征(scenarioHints),例如:["需要低延迟","面向C端用户","高并发","对话式交互","离线批处理","需要精准度"]
|
||||
3. 基于场景特征推断 budget 和 qualityPreference
|
||||
- 只在用户明确表达或场景强烈暗示时偏离默认值
|
||||
- 用户明确说"低成本"、"便宜"、"省钱" → budget:"low"
|
||||
- 用户明确说"最好的"、"高精度"、"不计成本" → qualityPreference:"flagship"
|
||||
- 场景本身有强约束时才推断:如"日均百万请求的客服" → budget:"low"(高并发=成本敏感)
|
||||
- 其他情况保持 budget:"medium", qualityPreference:"balanced"
|
||||
4. 提取模态、能力、特性等结构化字段
|
||||
|
||||
## 示例
|
||||
|
||||
用户: "做一个低成本高并发的在线客服"
|
||||
→ budget:"low", qualityPreference:"cost-optimized"(用户明确说了低成本)
|
||||
|
||||
用户: "法律合同审查,要求高精准度"
|
||||
→ budget:"medium", qualityPreference:"flagship"(用户明确要求高精准度,但没提预算)
|
||||
|
||||
用户: "我要做一个能理解图片的客服机器人"
|
||||
→ budget:"medium", qualityPreference:"balanced"(用户没提成本和质量要求,不过度推断)
|
||||
|
||||
用户: "帮我选一个写代码的模型"
|
||||
→ budget:"medium", qualityPreference:"balanced"(通用需求,无明确倾向)
|
||||
|
||||
用户: "预算有限,做个简单的文本摘要功能"
|
||||
→ budget:"low", qualityPreference:"cost-optimized"(用户说了预算有限)
|
||||
|
||||
用户: "企业级知识库问答,准确率是第一优先级"
|
||||
→ budget:"high", qualityPreference:"flagship"(企业级+准确率第一=愿投入高成本)
|
||||
|
||||
用户: "个人学习项目,试试AI生成图片"
|
||||
→ budget:"low", qualityPreference:"cost-optimized"(个人学习=成本敏感)
|
||||
|
||||
用户: "做一个Agent自动根据用户意图生成动画片"
|
||||
→ budget:"medium", qualityPreference:"balanced"(复杂pipeline,但没明确成本/质量约束)
|
||||
|
||||
## 输出字段
|
||||
- taskSummary: 一句话场景理解(必须具体,禁止"用户想用AI做某事"这种废话)
|
||||
- scenarioHints: 推断的场景特征数组
|
||||
- complexity: "single"(单一模型可完成)或 "pipeline"(需要多个模型协同)
|
||||
- segments: 仅 pipeline 时填写,每步包含 step/inputModality/outputModality/requiredCapabilities。
|
||||
- step 必须是一句话描述该步骤在用户任务中解决的具体问题,例如"解析天气预报数据,生成适合视频制作的场景描述文本",禁止用编号或泛化的模态标签
|
||||
- segments 必须形成模态链路:每步的 inputModality 应包含上一步的 outputModality,确保上下游数据可以衔接
|
||||
- inputModality: 用户输入涉及的模态 ["Text","Image","Video","Audio"]
|
||||
- outputModality: 期望输出的模态
|
||||
- requiredCapabilities: 需要的能力。可选代码(必须严格使用,不要自创):
|
||||
TG=文本生成, Reasoning=推理, VU=视觉理解, IG=图像生成, VG=视频生成,
|
||||
TTS=语音合成, ASR=语音识别, Realtime-ASR=实时语音识别,
|
||||
Realtime-Text-to-Speech=实时语音合成, Realtime-Audio-Translate=实时音频翻译,
|
||||
Realtime-Omni=实时全模态, Multimodal-Omni=全模态, ME=多模态嵌入,
|
||||
TR=翻译, 3D-generation=3D生成
|
||||
- requiredFeatures: 需要的特性 (function-calling, web-search, structured-outputs, prefix-completion)
|
||||
- budget: "low"/"medium"/"high"(基于场景推断,不要默认 medium)
|
||||
- contextNeed: "standard"/"large"/"extra-large"
|
||||
- qualityPreference: "flagship"/"balanced"/"cost-optimized"(基于场景推断,不要默认 balanced)
|
||||
|
||||
只输出 JSON,不要有其他文字。`;
|
||||
|
||||
export const SINGLE_SYSTEM_PROMPT = `你是阿里云百炼平台的模型推荐顾问。从以下候选模型中选出最佳推荐。
|
||||
|
||||
## 背景
|
||||
系统已根据用户意图预筛选了候选模型,你只需从中精选并排序。
|
||||
意图分析中包含 budget 和 qualityPreference 字段,这代表了用户的实际需求层次。
|
||||
|
||||
## 推荐策略
|
||||
|
||||
推荐 3 个不同档次的模型,但排序必须反映用户的真实需求:
|
||||
|
||||
- 推荐 #1(最佳推荐):根据 budget 和 qualityPreference 判断哪个档次最适合用户,把那个档次的最佳模型放在第一位
|
||||
- 推荐 #2(次优选择):另一个档次中值得考虑的模型,说明与 #1 相比的 tradeoff
|
||||
- 推荐 #3(备选参考):第三个视角的选择,说明适用场景差异
|
||||
|
||||
关键原则:
|
||||
- budget:"low" / qualityPreference:"cost-optimized" → 推荐 #1 应该是性价比最高的模型,而非旗舰模型
|
||||
- budget:"high" / qualityPreference:"flagship" → 推荐 #1 应该是能力最强的旗舰模型
|
||||
- budget:"medium" / qualityPreference:"balanced" → 推荐 #1 应该是综合匹配度最高的模型,不预设档次偏好
|
||||
|
||||
每个推荐都必须说明该模型为什么适合(或作为备选为什么值得考虑),理由必须关联用户的具体需求。
|
||||
|
||||
## 规则
|
||||
- 只能推荐候选列表中的模型,严禁推荐列表外的模型
|
||||
- 严禁使用泛泛的推荐理由(如"性能强大"、"综合能力好"、"效果不错"),每条 reason 必须说明该模型解决用户任务中的什么具体问题
|
||||
- 三个推荐的理由不允许雷同,每个必须从不同维度论证
|
||||
- 有定价信息时:结合 budget 字段权衡,把最符合用户预算的放在最前面
|
||||
- 有家族信息时:避免推荐同一家族的多个模型,优先推荐稳定版本
|
||||
- 有版本标签时:优先推荐 stable/latest 版本,除非用户明确需要特定版本
|
||||
- 没有增强字段的模型:按能力和描述排序即可,不因缺少信息而降权
|
||||
- 如果没有合适的模型,返回空数组
|
||||
- 如果你认为该需求实际需要多模型协同完成(pipeline),可以输出 type:"pipeline" 格式
|
||||
- 输出严格 JSON,不要输出其他内容
|
||||
|
||||
## 输出格式
|
||||
|
||||
单一任务:
|
||||
{"type":"single","recommendations":[{"model":"模型ID","reason":"推荐理由","highlights":["亮点"]}]}
|
||||
|
||||
复合任务(仅当你确信需要多模型协同时):
|
||||
{"type":"pipeline","summary":"一句话方案描述","steps":[{"step":"步骤描述","recommendations":[{"model":"模型ID","reason":"选择理由","highlights":["亮点"]}]}]}`;
|
||||
|
||||
export const PIPELINE_SYSTEM_PROMPT = `你是阿里云百炼平台的模型推荐顾问。用户需求已被拆解为多步骤流水线,请为每步选出最佳模型。
|
||||
|
||||
## 背景
|
||||
系统已根据各步骤需求预筛选了候选模型。
|
||||
意图分析中包含 budget 和 qualityPreference 字段,这代表了用户的实际需求层次。
|
||||
|
||||
## 推荐策略
|
||||
|
||||
每步推荐 3 个不同档次的模型,但排序必须反映用户的真实需求:
|
||||
|
||||
- 推荐 #1(最佳推荐):根据 budget 和 qualityPreference 判断哪个档次最适合用户,把那个档次的最佳模型放在第一位
|
||||
- 推荐 #2(次优选择):另一个档次中值得考虑的模型,说明 tradeoff
|
||||
- 推荐 #3(备选参考):第三个视角的选择,说明适用场景差异
|
||||
|
||||
关键原则:
|
||||
- budget:"low" / qualityPreference:"cost-optimized" → 推荐 #1 应该是性价比最高的模型
|
||||
- budget:"high" / qualityPreference:"flagship" → 推荐 #1 应该是能力最强的旗舰模型
|
||||
- budget:"medium" / qualityPreference:"balanced" → 推荐 #1 应该是综合匹配度最高的模型
|
||||
|
||||
## 规则
|
||||
- 只能推荐候选列表中的模型
|
||||
- 每步推荐多个模型,按优先级排序,每个推荐给出简短理由和关键亮点
|
||||
- step 字段必须用一句话描述该步骤在用户任务中解决的具体问题,禁止用编号或泛化的模态标签(如"输出: Text")
|
||||
- 严禁使用泛泛的推荐理由,每条 reason 必须说明该模型在这一步解决用户任务中的什么具体问题
|
||||
- 有定价信息时:结合 budget 字段权衡,把最符合用户预算的放在最前面
|
||||
- 有家族信息时:避免在相邻步骤使用同一家族的不同规格模型,除非确实需要
|
||||
- 没有增强字段的模型:按能力和描述排序即可,不因缺少信息而降权
|
||||
- 相邻步骤的模型必须模态兼容:上一步模型的输出模态必须被下一步模型的输入模态支持
|
||||
- 如果你认为该需求其实单模型可以完成,可以输出 type:"single" 格式
|
||||
- 输出严格 JSON
|
||||
|
||||
## 输出格式
|
||||
|
||||
{"type":"pipeline","summary":"一句话方案描述","steps":[{"step":"该步骤在用户任务中解决的具体问题","recommendations":[{"model":"模型ID","reason":"该模型如何解决这一步的具体问题","highlights":["亮点"]}]}]}
|
||||
|
||||
或者(如果你认为单模型即可):
|
||||
{"type":"single","recommendations":[{"model":"模型ID","reason":"推荐理由","highlights":["亮点"]}]}`;
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Capabilities } from "../types.ts";
|
||||
import type { Capability, ContextNeed } from "../types.ts";
|
||||
|
||||
export const MAX_CANDIDATES = 50;
|
||||
export const MIN_CANDIDATES = 10;
|
||||
export const FALLBACK_THRESHOLD = 5;
|
||||
export const FAMILY_CANDIDATE_CAP = 3;
|
||||
export const SNAPSHOT_DATE_RE = /-\d{4}-\d{2}-\d{2}$/;
|
||||
|
||||
export const GENERATION_CAPS: ReadonlySet<Capability> = new Set<Capability>([
|
||||
Capabilities.IG,
|
||||
Capabilities.VG,
|
||||
Capabilities.TTS,
|
||||
Capabilities.RealtimeTTS,
|
||||
Capabilities.ThreeDGeneration,
|
||||
]);
|
||||
|
||||
export const TEXT_CAPS: ReadonlySet<Capability> = new Set<Capability>([
|
||||
Capabilities.TG,
|
||||
Capabilities.Reasoning,
|
||||
Capabilities.ASR,
|
||||
Capabilities.RealtimeASR,
|
||||
Capabilities.RealtimeAudioTranslate,
|
||||
Capabilities.TR,
|
||||
Capabilities.ME,
|
||||
]);
|
||||
|
||||
export const CONTEXT_THRESHOLDS: Record<ContextNeed, number> = {
|
||||
standard: 0,
|
||||
large: 32000,
|
||||
"extra-large": 128000,
|
||||
};
|
||||
@@ -0,0 +1,195 @@
|
||||
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { getConfigDir } from "../config/paths.ts";
|
||||
import type { Config } from "../config/schema.ts";
|
||||
import { requestJson } from "../client/http.ts";
|
||||
import type { ModelProfile } from "./types.ts";
|
||||
|
||||
const EMBEDDING_MODEL = "text-embedding-v4";
|
||||
const DIMENSIONS = 512;
|
||||
const EMBEDDINGS_FILE = "models-embeddings.json";
|
||||
const BATCH_SIZE = 10;
|
||||
|
||||
export interface ModelEmbedding {
|
||||
id: string;
|
||||
vector: number[];
|
||||
}
|
||||
|
||||
export interface EmbeddingsData {
|
||||
model: string;
|
||||
dimensions: number;
|
||||
count: number;
|
||||
items: ModelEmbedding[];
|
||||
}
|
||||
|
||||
function skillDataDir(): string {
|
||||
return join(getConfigDir(), "skills/doc-llm-wiki");
|
||||
}
|
||||
|
||||
function embeddingsPath(): string {
|
||||
return join(skillDataDir(), EMBEDDINGS_FILE);
|
||||
}
|
||||
|
||||
export function loadModelEmbeddings(): ModelEmbedding[] | null {
|
||||
const path = embeddingsPath();
|
||||
if (!existsSync(path)) return null;
|
||||
try {
|
||||
const raw = JSON.parse(readFileSync(path, "utf-8")) as EmbeddingsData;
|
||||
return raw.items;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function embedQuery(config: Config, text: string): Promise<number[]> {
|
||||
const url = `${config.baseUrl}/compatible-mode/v1/embeddings`;
|
||||
const body = {
|
||||
model: EMBEDDING_MODEL,
|
||||
input: [text],
|
||||
dimensions: DIMENSIONS,
|
||||
encoding_format: "float",
|
||||
};
|
||||
|
||||
const response = await requestJson<{
|
||||
data: { index: number; embedding: number[] }[];
|
||||
}>(config, { url, method: "POST", body, timeout: 10000 });
|
||||
|
||||
return response.data[0].embedding;
|
||||
}
|
||||
|
||||
async function embedBatch(config: Config, texts: string[]): Promise<number[][]> {
|
||||
const url = `${config.baseUrl}/compatible-mode/v1/embeddings`;
|
||||
const body = {
|
||||
model: EMBEDDING_MODEL,
|
||||
input: texts,
|
||||
dimensions: DIMENSIONS,
|
||||
encoding_format: "float",
|
||||
};
|
||||
|
||||
const response = await requestJson<{
|
||||
data: { index: number; embedding: number[] }[];
|
||||
}>(config, { url, method: "POST", body, timeout: 30000 });
|
||||
|
||||
return response.data
|
||||
.sort((left, right) => left.index - right.index)
|
||||
.map((item) => item.embedding);
|
||||
}
|
||||
|
||||
const CAPABILITY_LABELS: Record<string, string> = {
|
||||
TG: "文本生成",
|
||||
Reasoning: "推理",
|
||||
VU: "视觉理解",
|
||||
IG: "图像生成",
|
||||
VG: "视频生成",
|
||||
TTS: "语音合成",
|
||||
ASR: "语音识别",
|
||||
};
|
||||
|
||||
const MODALITY_LABELS: Record<string, string> = {
|
||||
Text: "文本",
|
||||
Image: "图片/图像",
|
||||
Video: "视频",
|
||||
Audio: "音频/语音",
|
||||
};
|
||||
|
||||
interface GroupData {
|
||||
description?: string;
|
||||
items?: { model: string; description?: string }[];
|
||||
}
|
||||
|
||||
function loadGroupDescriptions(): Map<string, string> {
|
||||
const groupsDir = join(skillDataDir(), "groups");
|
||||
const map = new Map<string, string>();
|
||||
if (!existsSync(groupsDir)) return map;
|
||||
|
||||
for (const file of readdirSync(groupsDir).filter((name) => name.endsWith(".json"))) {
|
||||
try {
|
||||
const data = JSON.parse(readFileSync(join(groupsDir, file), "utf-8")) as GroupData;
|
||||
const groupDesc = data.description ?? "";
|
||||
if (data.items) {
|
||||
for (const item of data.items) {
|
||||
map.set(item.model, item.description || groupDesc);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// skip
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function buildModelText(model: ModelProfile, descriptions: Map<string, string>): string {
|
||||
const caps = (model.capabilities ?? []).map((cap) => CAPABILITY_LABELS[cap] ?? cap).join(", ");
|
||||
|
||||
const description =
|
||||
descriptions.get(model.model) || model.shortDescription || model.description || "";
|
||||
|
||||
const inputMods = (model.inferenceMetadata?.request_modality ?? [])
|
||||
.map((mod) => MODALITY_LABELS[mod] ?? mod)
|
||||
.join(", ");
|
||||
const outputMods = (model.inferenceMetadata?.response_modality ?? [])
|
||||
.map((mod) => MODALITY_LABELS[mod] ?? mod)
|
||||
.join(", ");
|
||||
|
||||
const parts = [
|
||||
model.name,
|
||||
model.model,
|
||||
description,
|
||||
caps ? `能力: ${caps}` : "",
|
||||
inputMods ? `输入: ${inputMods}` : "",
|
||||
outputMods ? `输出: ${outputMods}` : "",
|
||||
model.features?.length ? `特性: ${model.features.join(", ")}` : "",
|
||||
model.familyName || "",
|
||||
model.category ? `定位: ${model.category}` : "",
|
||||
].filter(Boolean);
|
||||
|
||||
return parts.join(" | ");
|
||||
}
|
||||
|
||||
export async function buildAndCacheEmbeddings(
|
||||
config: Config,
|
||||
models: ModelProfile[],
|
||||
): Promise<ModelEmbedding[]> {
|
||||
const descriptions = loadGroupDescriptions();
|
||||
const texts = models.map((profile) => buildModelText(profile, descriptions));
|
||||
|
||||
const allVectors: number[][] = [];
|
||||
for (let batchStart = 0; batchStart < texts.length; batchStart += BATCH_SIZE) {
|
||||
const batch = texts.slice(batchStart, batchStart + BATCH_SIZE);
|
||||
const vectors = await embedBatch(config, batch);
|
||||
allVectors.push(...vectors);
|
||||
}
|
||||
|
||||
const items: ModelEmbedding[] = models.map((profile, idx) => ({
|
||||
id: profile.model,
|
||||
vector: allVectors[idx],
|
||||
}));
|
||||
|
||||
const output: EmbeddingsData = {
|
||||
model: EMBEDDING_MODEL,
|
||||
dimensions: DIMENSIONS,
|
||||
count: items.length,
|
||||
items,
|
||||
};
|
||||
|
||||
const outPath = embeddingsPath();
|
||||
mkdirSync(dirname(outPath), { recursive: true });
|
||||
writeFileSync(outPath, JSON.stringify(output));
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
export function cosineSimilarity(vecA: number[], vecB: number[]): number {
|
||||
let dot = 0;
|
||||
let normA = 0;
|
||||
let normB = 0;
|
||||
for (let idx = 0; idx < vecA.length; idx++) {
|
||||
dot += vecA[idx] * vecB[idx];
|
||||
normA += vecA[idx] * vecA[idx];
|
||||
normB += vecB[idx] * vecB[idx];
|
||||
}
|
||||
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
||||
return denom === 0 ? 0 : dot / denom;
|
||||
}
|
||||
|
||||
export { DIMENSIONS, EMBEDDING_MODEL };
|
||||
@@ -0,0 +1,39 @@
|
||||
export type { GetModelsOptions } from "./cache.ts";
|
||||
export { getModels } from "./cache.ts";
|
||||
export { analyzeIntent } from "./intent.ts";
|
||||
export type { ScoredCandidate } from "./recall.ts";
|
||||
export { recallCandidates } from "./recall.ts";
|
||||
export { recallSemantic, isSemanticAvailable } from "./recall-semantic.ts";
|
||||
export type { RecommendOptions } from "./recommend.ts";
|
||||
export { buildDocLink, rankModels } from "./recommend.ts";
|
||||
export type { ModelSource } from "./sources/types.ts";
|
||||
export type {
|
||||
Budget,
|
||||
Capability,
|
||||
Complexity,
|
||||
ContextNeed,
|
||||
Feature,
|
||||
IntentProfile,
|
||||
IntentSegment,
|
||||
Modality,
|
||||
ModelCategory,
|
||||
ModelPrice,
|
||||
ModelProfile,
|
||||
PipelineResult,
|
||||
PipelineStep,
|
||||
QpmLimit,
|
||||
QualityPreference,
|
||||
RecommendedModel,
|
||||
RecommendResult,
|
||||
SingleResult,
|
||||
} from "./types.ts";
|
||||
export {
|
||||
Budgets,
|
||||
Capabilities,
|
||||
Complexities,
|
||||
ContextNeeds,
|
||||
Features,
|
||||
Modalities,
|
||||
ModelCategories,
|
||||
QualityPreferences,
|
||||
} from "./types.ts";
|
||||
@@ -0,0 +1,65 @@
|
||||
import { requestJson } from "../client/http.ts";
|
||||
import { chatEndpoint } from "../client/endpoints.ts";
|
||||
import type { Config } from "../config/schema.ts";
|
||||
import type { ChatResponse } from "../types/api.ts";
|
||||
import { Complexities } from "./types.ts";
|
||||
import type { IntentProfile } from "./types.ts";
|
||||
import { INTENT_MODEL, INTENT_SYSTEM_PROMPT } from "./constants/prompts.ts";
|
||||
import { DEFAULT_INTENT } from "./constants/defaults.ts";
|
||||
|
||||
export async function analyzeIntent(config: Config, input: string): Promise<IntentProfile> {
|
||||
const url = chatEndpoint(config.baseUrl);
|
||||
|
||||
const body = {
|
||||
model: INTENT_MODEL,
|
||||
messages: [
|
||||
{ role: "system", content: INTENT_SYSTEM_PROMPT },
|
||||
{ role: "user", content: input },
|
||||
],
|
||||
max_tokens: 1024,
|
||||
temperature: 0,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await requestJson<ChatResponse>(config, {
|
||||
url,
|
||||
method: "POST",
|
||||
body,
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
const content = response.choices?.[0]?.message?.content ?? "";
|
||||
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
||||
if (!jsonMatch) return DEFAULT_INTENT;
|
||||
|
||||
const parsed = JSON.parse(jsonMatch[0]);
|
||||
return {
|
||||
complexity:
|
||||
parsed.complexity === Complexities.Pipeline ? Complexities.Pipeline : Complexities.Single,
|
||||
taskSummary: typeof parsed.taskSummary === "string" ? parsed.taskSummary : "",
|
||||
scenarioHints: Array.isArray(parsed.scenarioHints) ? parsed.scenarioHints : [],
|
||||
segments: Array.isArray(parsed.segments)
|
||||
? parsed.segments.map((seg: Record<string, unknown>) => ({
|
||||
step: (seg.step as string) ?? "",
|
||||
inputModality: Array.isArray(seg.inputModality) ? seg.inputModality : [],
|
||||
outputModality: Array.isArray(seg.outputModality) ? seg.outputModality : [],
|
||||
requiredCapabilities: Array.isArray(seg.requiredCapabilities)
|
||||
? seg.requiredCapabilities
|
||||
: [],
|
||||
}))
|
||||
: undefined,
|
||||
inputModality: Array.isArray(parsed.inputModality) ? parsed.inputModality : [],
|
||||
outputModality: Array.isArray(parsed.outputModality) ? parsed.outputModality : [],
|
||||
requiredCapabilities: Array.isArray(parsed.requiredCapabilities)
|
||||
? parsed.requiredCapabilities
|
||||
: [],
|
||||
requiredFeatures: Array.isArray(parsed.requiredFeatures) ? parsed.requiredFeatures : [],
|
||||
budget: parsed.budget ?? DEFAULT_INTENT.budget,
|
||||
contextNeed: parsed.contextNeed ?? DEFAULT_INTENT.contextNeed,
|
||||
qualityPreference: parsed.qualityPreference ?? DEFAULT_INTENT.qualityPreference,
|
||||
confidence: 1,
|
||||
};
|
||||
} catch {
|
||||
return DEFAULT_INTENT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import type { Config } from "../config/schema.ts";
|
||||
import type { IntentProfile, IntentSegment, ModelProfile } from "./types.ts";
|
||||
import { Complexities } from "./types.ts";
|
||||
import {
|
||||
buildAndCacheEmbeddings,
|
||||
cosineSimilarity,
|
||||
embedQuery,
|
||||
loadModelEmbeddings,
|
||||
type ModelEmbedding,
|
||||
} from "./embedding.ts";
|
||||
import type { ScoredCandidate } from "./recall.ts";
|
||||
|
||||
let cachedEmbeddings: ModelEmbedding[] | null = null;
|
||||
|
||||
function getEmbeddings(): ModelEmbedding[] | null {
|
||||
if (cachedEmbeddings === null) {
|
||||
cachedEmbeddings = loadModelEmbeddings();
|
||||
}
|
||||
return cachedEmbeddings;
|
||||
}
|
||||
|
||||
export function isSemanticAvailable(): boolean {
|
||||
return getEmbeddings() !== null;
|
||||
}
|
||||
|
||||
function matchesSegment(model: ModelProfile, segment: IntentSegment): boolean {
|
||||
const modelIn = model.inferenceMetadata?.request_modality ?? [];
|
||||
const modelOut = model.inferenceMetadata?.response_modality ?? [];
|
||||
const inOk =
|
||||
segment.inputModality.length === 0 ||
|
||||
segment.inputModality.some((mod) => modelIn.includes(mod));
|
||||
const outOk =
|
||||
segment.outputModality.length === 0 ||
|
||||
segment.outputModality.some((mod) => modelOut.includes(mod));
|
||||
if (!inOk || !outOk) return false;
|
||||
if (segment.requiredCapabilities.length === 0) return true;
|
||||
return segment.requiredCapabilities.some((cap) => model.capabilities.includes(cap));
|
||||
}
|
||||
|
||||
function rankByEmbedding(
|
||||
embeddings: ModelEmbedding[],
|
||||
queryVector: number[],
|
||||
allowedIds: Set<string>,
|
||||
topK: number,
|
||||
): { id: string; similarity: number }[] {
|
||||
return embeddings
|
||||
.filter((item) => allowedIds.has(item.id))
|
||||
.map((item) => ({ id: item.id, similarity: cosineSimilarity(queryVector, item.vector) }))
|
||||
.sort((left, right) => right.similarity - left.similarity)
|
||||
.slice(0, topK);
|
||||
}
|
||||
|
||||
export async function recallSemantic(
|
||||
config: Config,
|
||||
models: ModelProfile[],
|
||||
query: string,
|
||||
topK: number,
|
||||
intent?: IntentProfile,
|
||||
): Promise<ScoredCandidate[]> {
|
||||
let embeddings = getEmbeddings();
|
||||
|
||||
if (!embeddings) {
|
||||
embeddings = await buildAndCacheEmbeddings(config, models);
|
||||
cachedEmbeddings = embeddings;
|
||||
}
|
||||
|
||||
const queryVector = await embedQuery(config, query);
|
||||
const modelMap = new Map(models.map((profile) => [profile.model, profile]));
|
||||
|
||||
if (intent?.complexity === Complexities.Pipeline && intent.segments?.length) {
|
||||
const seen = new Set<string>();
|
||||
const results: ScoredCandidate[] = [];
|
||||
const perSegment = Math.max(5, Math.ceil(topK / intent.segments.length));
|
||||
|
||||
for (const segment of intent.segments) {
|
||||
const matched = models.filter((profile) => matchesSegment(profile, segment));
|
||||
const allowedIds = new Set(
|
||||
matched.filter((profile) => !seen.has(profile.model)).map((profile) => profile.model),
|
||||
);
|
||||
if (allowedIds.size === 0) continue;
|
||||
|
||||
const scored = rankByEmbedding(embeddings, queryVector, allowedIds, perSegment);
|
||||
for (const { id, similarity } of scored) {
|
||||
const model = modelMap.get(id);
|
||||
if (model && !seen.has(id)) {
|
||||
results.push({ model, score: similarity });
|
||||
seen.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
const allIds = new Set(models.map((profile) => profile.model));
|
||||
const scored = rankByEmbedding(embeddings, queryVector, allIds, topK);
|
||||
|
||||
const results: ScoredCandidate[] = [];
|
||||
for (const { id, similarity } of scored) {
|
||||
const model = modelMap.get(id);
|
||||
if (model) {
|
||||
results.push({ model, score: similarity });
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
import { Complexities, ContextNeeds, QualityPreferences, ModelCategories } from "./types.ts";
|
||||
import type { ModelProfile, IntentProfile, IntentSegment, Capability, Modality } from "./types.ts";
|
||||
import {
|
||||
MAX_CANDIDATES,
|
||||
MIN_CANDIDATES,
|
||||
FALLBACK_THRESHOLD,
|
||||
FAMILY_CANDIDATE_CAP,
|
||||
SNAPSHOT_DATE_RE,
|
||||
GENERATION_CAPS,
|
||||
TEXT_CAPS,
|
||||
CONTEXT_THRESHOLDS,
|
||||
} from "./constants/scoring.ts";
|
||||
|
||||
export interface ScoredCandidate {
|
||||
model: ModelProfile;
|
||||
score: number;
|
||||
}
|
||||
|
||||
function hasMultiDomainCapabilities(caps: Capability[]): boolean {
|
||||
let hasGen = false;
|
||||
let hasText = false;
|
||||
for (const cap of caps) {
|
||||
if (GENERATION_CAPS.has(cap)) hasGen = true;
|
||||
if (TEXT_CAPS.has(cap)) hasText = true;
|
||||
}
|
||||
return hasGen && hasText;
|
||||
}
|
||||
|
||||
function deduplicateSnapshots(models: ModelProfile[]): ModelProfile[] {
|
||||
const mainModels = new Set(models.map(({ model }) => model));
|
||||
return models.filter(({ model }) => {
|
||||
const base = model.replace(SNAPSHOT_DATE_RE, "");
|
||||
if (base === model) return true;
|
||||
return !mainModels.has(base);
|
||||
});
|
||||
}
|
||||
|
||||
function matchesModality(
|
||||
model: ModelProfile,
|
||||
inputModality: Modality[],
|
||||
outputModality: Modality[],
|
||||
): boolean {
|
||||
const modelInput = model.inferenceMetadata?.request_modality ?? [];
|
||||
const modelOutput = model.inferenceMetadata?.response_modality ?? [];
|
||||
|
||||
if (inputModality.length > 0) {
|
||||
if (!inputModality.some((mod) => modelInput.includes(mod))) return false;
|
||||
}
|
||||
if (outputModality.length > 0) {
|
||||
if (!outputModality.some((mod) => modelOutput.includes(mod))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function matchesUpstream(model: ModelProfile, upstreamOutput: Modality[]): boolean {
|
||||
if (upstreamOutput.length === 0) return true;
|
||||
const accepts = model.inferenceMetadata?.request_modality ?? [];
|
||||
return upstreamOutput.some((mod) => accepts.includes(mod));
|
||||
}
|
||||
|
||||
function scoreModel(model: ModelProfile, intent: IntentProfile): number {
|
||||
const { requiredCapabilities, requiredFeatures, contextNeed, qualityPreference } = intent;
|
||||
const { capabilities, features, contextWindow, category } = model;
|
||||
let score = 0;
|
||||
|
||||
for (const cap of requiredCapabilities) {
|
||||
if (capabilities.includes(cap)) score += 10;
|
||||
}
|
||||
|
||||
for (const feat of requiredFeatures) {
|
||||
if (features.includes(feat)) score += 5;
|
||||
}
|
||||
|
||||
const ctxThreshold = CONTEXT_THRESHOLDS[contextNeed];
|
||||
if (ctxThreshold > 0 && (contextWindow ?? 0) >= ctxThreshold) {
|
||||
score += 8;
|
||||
}
|
||||
|
||||
if (qualityPreference === QualityPreferences.Flagship && category === ModelCategories.Flagship) {
|
||||
score += 15;
|
||||
} else if (
|
||||
qualityPreference === QualityPreferences.CostOptimized &&
|
||||
category === ModelCategories.CostOptimized
|
||||
) {
|
||||
score += 15;
|
||||
} else if (qualityPreference === QualityPreferences.Balanced) {
|
||||
if (category === ModelCategories.Flagship) score += 5;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
function scoreAndRank(
|
||||
models: ModelProfile[],
|
||||
intent: IntentProfile,
|
||||
limit: number,
|
||||
): ScoredCandidate[] {
|
||||
return models
|
||||
.map((model) => ({ model, score: scoreModel(model, intent) }))
|
||||
.sort((left, right) => right.score - left.score)
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
function candidateIds(candidates: ScoredCandidate[]): Set<string> {
|
||||
return new Set(candidates.map(({ model }) => model.model));
|
||||
}
|
||||
|
||||
function capByFamily(candidates: ScoredCandidate[], cap: number): ScoredCandidate[] {
|
||||
const counts = new Map<string, number>();
|
||||
const kept: ScoredCandidate[] = [];
|
||||
const overflow: ScoredCandidate[] = [];
|
||||
for (const candidate of candidates) {
|
||||
const family = candidate.model.family;
|
||||
if (!family) {
|
||||
kept.push(candidate);
|
||||
continue;
|
||||
}
|
||||
const cur = counts.get(family) ?? 0;
|
||||
if (cur < cap) {
|
||||
kept.push(candidate);
|
||||
counts.set(family, cur + 1);
|
||||
} else {
|
||||
overflow.push(candidate);
|
||||
}
|
||||
}
|
||||
if (kept.length >= MIN_CANDIDATES) return kept;
|
||||
return [...kept, ...overflow.slice(0, MIN_CANDIDATES - kept.length)];
|
||||
}
|
||||
|
||||
function deduplicateCandidates(
|
||||
candidates: ScoredCandidate[],
|
||||
excludeIds: ReadonlySet<string>,
|
||||
): ScoredCandidate[] {
|
||||
const seen = new Set(excludeIds);
|
||||
return candidates.filter((candidate) => {
|
||||
if (seen.has(candidate.model.model)) return false;
|
||||
seen.add(candidate.model.model);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function computeRemaining(
|
||||
models: ModelProfile[],
|
||||
intent: IntentProfile,
|
||||
excludeIds: ReadonlySet<string>,
|
||||
): ScoredCandidate[] {
|
||||
if (excludeIds.size >= MIN_CANDIDATES) return [];
|
||||
return scoreAndRank(
|
||||
models.filter(({ model }) => !excludeIds.has(model)),
|
||||
intent,
|
||||
MIN_CANDIDATES - excludeIds.size,
|
||||
);
|
||||
}
|
||||
|
||||
function recallForSegment(
|
||||
models: ModelProfile[],
|
||||
segment: IntentSegment,
|
||||
upstreamOutput: Modality[],
|
||||
budget: IntentProfile["budget"],
|
||||
qualityPreference: IntentProfile["qualityPreference"],
|
||||
): ScoredCandidate[] {
|
||||
const { inputModality, outputModality, requiredCapabilities } = segment;
|
||||
const segmentIntent: IntentProfile = {
|
||||
complexity: Complexities.Single,
|
||||
taskSummary: "",
|
||||
scenarioHints: [],
|
||||
inputModality,
|
||||
outputModality,
|
||||
requiredCapabilities,
|
||||
requiredFeatures: [],
|
||||
budget,
|
||||
contextNeed: ContextNeeds.Standard,
|
||||
qualityPreference,
|
||||
confidence: 1,
|
||||
};
|
||||
|
||||
let candidates = models.filter(
|
||||
(profile) =>
|
||||
matchesModality(profile, inputModality, outputModality) &&
|
||||
matchesUpstream(profile, upstreamOutput),
|
||||
);
|
||||
|
||||
if (candidates.length < FALLBACK_THRESHOLD) {
|
||||
candidates = models.filter((profile) =>
|
||||
matchesModality(profile, inputModality, outputModality),
|
||||
);
|
||||
}
|
||||
|
||||
if (candidates.length < FALLBACK_THRESHOLD) {
|
||||
candidates = models;
|
||||
}
|
||||
|
||||
return scoreAndRank(candidates, segmentIntent, 5);
|
||||
}
|
||||
|
||||
export function recallCandidates(models: ModelProfile[], intent: IntentProfile): ScoredCandidate[] {
|
||||
models = deduplicateSnapshots(models);
|
||||
|
||||
let result: ScoredCandidate[];
|
||||
|
||||
if (intent.complexity === Complexities.Pipeline && intent.segments?.length) {
|
||||
let results: ScoredCandidate[] = [];
|
||||
|
||||
for (const [segIdx, segment] of intent.segments.entries()) {
|
||||
const upstreamOutput = segIdx === 0 ? [] : intent.segments[segIdx - 1].outputModality;
|
||||
const segCandidates = recallForSegment(
|
||||
models,
|
||||
segment,
|
||||
upstreamOutput,
|
||||
intent.budget,
|
||||
intent.qualityPreference,
|
||||
);
|
||||
const unique = deduplicateCandidates(segCandidates, candidateIds(results));
|
||||
results = [...results, ...unique];
|
||||
}
|
||||
|
||||
const remaining = computeRemaining(models, intent, candidateIds(results));
|
||||
result = [...results, ...remaining];
|
||||
} else if (hasMultiDomainCapabilities(intent.requiredCapabilities)) {
|
||||
result = recallCrossDomain(models, intent);
|
||||
} else {
|
||||
let hardFiltered = models.filter((profile) =>
|
||||
matchesModality(profile, intent.inputModality, intent.outputModality),
|
||||
);
|
||||
|
||||
if (hardFiltered.length < FALLBACK_THRESHOLD) {
|
||||
hardFiltered = models;
|
||||
}
|
||||
|
||||
result = scoreAndRank(hardFiltered, intent, MAX_CANDIDATES);
|
||||
}
|
||||
|
||||
return capByFamily(result, FAMILY_CANDIDATE_CAP);
|
||||
}
|
||||
|
||||
function recallCrossDomain(models: ModelProfile[], intent: IntentProfile): ScoredCandidate[] {
|
||||
const perDomain = Math.ceil(MAX_CANDIDATES / 2);
|
||||
|
||||
const genCaps = intent.requiredCapabilities.filter((cap) => GENERATION_CAPS.has(cap));
|
||||
const textCaps = intent.requiredCapabilities.filter((cap) => TEXT_CAPS.has(cap));
|
||||
|
||||
let results: ScoredCandidate[] = [];
|
||||
|
||||
if (genCaps.length > 0) {
|
||||
const genModels = models.filter((profile) =>
|
||||
genCaps.some((cap) => profile.capabilities.includes(cap)),
|
||||
);
|
||||
results = scoreAndRank(genModels, intent, perDomain);
|
||||
}
|
||||
|
||||
if (textCaps.length > 0) {
|
||||
const excludeIds = candidateIds(results);
|
||||
const textIntent: IntentProfile = { ...intent, requiredCapabilities: textCaps };
|
||||
const textModels = models.filter(
|
||||
(profile) =>
|
||||
!excludeIds.has(profile.model) &&
|
||||
textCaps.some((cap) => profile.capabilities.includes(cap)),
|
||||
);
|
||||
results = [...results, ...scoreAndRank(textModels, textIntent, perDomain)];
|
||||
}
|
||||
|
||||
const remaining = computeRemaining(models, intent, candidateIds(results));
|
||||
return [...results, ...remaining];
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
import { chatEndpoint } from "../client/endpoints.ts";
|
||||
import { request, requestJson } from "../client/http.ts";
|
||||
import { parseSSE } from "../client/stream.ts";
|
||||
import type { Config } from "../config/schema.ts";
|
||||
import type { ChatResponse, StreamChunk } from "../types/api.ts";
|
||||
import {
|
||||
PIPELINE_SYSTEM_PROMPT,
|
||||
RANKING_MODEL,
|
||||
RANKING_MODEL_FAST,
|
||||
SINGLE_SYSTEM_PROMPT,
|
||||
} from "./constants/prompts.ts";
|
||||
import type { ScoredCandidate } from "./recall.ts";
|
||||
import type {
|
||||
IntentProfile,
|
||||
ModelProfile,
|
||||
PipelineStep,
|
||||
RecommendedModel,
|
||||
RecommendResult,
|
||||
} from "./types.ts";
|
||||
import { Complexities, ContextNeeds } from "./types.ts";
|
||||
|
||||
export interface RecommendOptions {
|
||||
onThinking?: (text: string) => void;
|
||||
onContentStart?: () => void;
|
||||
enableThinking?: boolean;
|
||||
}
|
||||
|
||||
function formatPrices(profile: ModelProfile): string | undefined {
|
||||
if (!profile.prices?.length) return undefined;
|
||||
return profile.prices.map((price) => `${price.type}:${price.price}/${price.unit}`).join(", ");
|
||||
}
|
||||
|
||||
function formatQpm(profile: ModelProfile): string | undefined {
|
||||
if (!profile.qpmInfo) return undefined;
|
||||
const entries = Object.entries(profile.qpmInfo);
|
||||
if (entries.length === 0) return undefined;
|
||||
return entries
|
||||
.map(([key, limit]) => `${key}:${limit.count_limit}/${limit.count_limit_period}s`)
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
function buildCandidatesContext(candidates: ScoredCandidate[]): string {
|
||||
return candidates
|
||||
.map(({ model: profile }) => {
|
||||
const parts = [
|
||||
`ID: ${profile.model}`,
|
||||
`名称: ${profile.name}`,
|
||||
`描述: ${profile.shortDescription || profile.description}`,
|
||||
`能力: ${profile.capabilities.join(", ")}`,
|
||||
`特性: ${profile.features.join(", ")}`,
|
||||
];
|
||||
if (profile.contextWindow) parts.push(`上下文窗口: ${profile.contextWindow}`);
|
||||
if (profile.maxOutputTokens) parts.push(`最大输出: ${profile.maxOutputTokens}`);
|
||||
if (profile.category) parts.push(`类别: ${profile.category}`);
|
||||
const modality = profile.inferenceMetadata;
|
||||
if (modality?.request_modality?.length)
|
||||
parts.push(`输入模态: ${modality.request_modality.join(", ")}`);
|
||||
if (modality?.response_modality?.length)
|
||||
parts.push(`输出模态: ${modality.response_modality.join(", ")}`);
|
||||
const prices = formatPrices(profile);
|
||||
if (prices) parts.push(`定价: ${prices}`);
|
||||
const qpm = formatQpm(profile);
|
||||
if (qpm) parts.push(`QPM: ${qpm}`);
|
||||
if (profile.versionTag) parts.push(`版本: ${profile.versionTag}`);
|
||||
if (profile.openSource !== undefined) parts.push(`开源: ${profile.openSource ? "是" : "否"}`);
|
||||
if (profile.family) parts.push(`家族: ${profile.family}`);
|
||||
return parts.join(" | ");
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function buildIntentContext(intent: IntentProfile): string {
|
||||
const {
|
||||
taskSummary,
|
||||
scenarioHints,
|
||||
inputModality,
|
||||
outputModality,
|
||||
requiredCapabilities,
|
||||
requiredFeatures,
|
||||
budget,
|
||||
qualityPreference,
|
||||
contextNeed,
|
||||
segments,
|
||||
} = intent;
|
||||
const parts: string[] = [];
|
||||
if (taskSummary) parts.push(`场景理解: ${taskSummary}`);
|
||||
if (scenarioHints.length) parts.push(`场景特征: ${scenarioHints.join(", ")}`);
|
||||
if (inputModality.length) parts.push(`输入模态: ${inputModality.join(", ")}`);
|
||||
if (outputModality.length) parts.push(`输出模态: ${outputModality.join(", ")}`);
|
||||
if (requiredCapabilities.length) parts.push(`所需能力: ${requiredCapabilities.join(", ")}`);
|
||||
if (requiredFeatures.length) parts.push(`所需特性: ${requiredFeatures.join(", ")}`);
|
||||
parts.push(`预算倾向: ${budget}`);
|
||||
parts.push(`质量偏好: ${qualityPreference}`);
|
||||
if (contextNeed !== ContextNeeds.Standard) parts.push(`上下文需求: ${contextNeed}`);
|
||||
if (segments?.length) {
|
||||
parts.push(`拆解步骤:`);
|
||||
for (const seg of segments) {
|
||||
const inMod = seg.inputModality.join(",") || "无";
|
||||
const outMod = seg.outputModality.join(",") || "无";
|
||||
const caps = seg.requiredCapabilities.join(",") || "无";
|
||||
parts.push(` - ${seg.step} (输入: ${inMod} → 输出: ${outMod}, 能力: ${caps})`);
|
||||
}
|
||||
}
|
||||
return parts.join("\n");
|
||||
}
|
||||
|
||||
export function buildDocLink(docUrl?: string): string | undefined {
|
||||
if (!docUrl) return undefined;
|
||||
const match = docUrl.match(/\/(\d+)\.html/);
|
||||
if (!match) return undefined;
|
||||
return `https://bailian.console.aliyun.com/cn-beijing?tab=doc#/doc/?type=model&url=${match[1]}`;
|
||||
}
|
||||
|
||||
function buildRecommendations(
|
||||
items: any[],
|
||||
modelMap: Map<string, ModelProfile>,
|
||||
limit: number,
|
||||
): RecommendedModel[] {
|
||||
const list = Array.isArray(items) ? items : [];
|
||||
const recommendations: RecommendedModel[] = [];
|
||||
const seenFamilies = new Set<string>();
|
||||
|
||||
for (const item of list) {
|
||||
const profile = modelMap.get(item.model);
|
||||
if (!profile) continue;
|
||||
if (profile.family && seenFamilies.has(profile.family)) continue;
|
||||
if (profile.family) seenFamilies.add(profile.family);
|
||||
const { model, name, category, contextWindow, maxOutputTokens, docUrl } = profile;
|
||||
recommendations.push({
|
||||
model,
|
||||
name,
|
||||
reason: item.reason ?? "",
|
||||
highlights: item.highlights ?? [],
|
||||
category,
|
||||
contextWindow,
|
||||
maxOutputTokens,
|
||||
docUrl,
|
||||
});
|
||||
if (recommendations.length >= limit) break;
|
||||
}
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
function validatePipelineCompatibility(
|
||||
steps: PipelineStep[],
|
||||
modelMap: Map<string, ModelProfile>,
|
||||
): void {
|
||||
for (let stepIdx = 1; stepIdx < steps.length; stepIdx++) {
|
||||
const prevStep = steps[stepIdx - 1];
|
||||
const currStep = steps[stepIdx];
|
||||
const prevOutputs = new Set(
|
||||
prevStep.recommendations.flatMap((rec) => {
|
||||
const profile = modelMap.get(rec.model);
|
||||
return profile?.inferenceMetadata?.response_modality ?? [];
|
||||
}),
|
||||
);
|
||||
|
||||
if (prevOutputs.size === 0) continue;
|
||||
|
||||
const warnings: string[] = [];
|
||||
for (const rec of currStep.recommendations) {
|
||||
const profile = modelMap.get(rec.model);
|
||||
const accepts = profile?.inferenceMetadata?.request_modality ?? [];
|
||||
const compatible = accepts.some((mod) => prevOutputs.has(mod));
|
||||
if (!compatible && accepts.length > 0) {
|
||||
warnings.push(
|
||||
`${rec.name} 的输入模态 [${accepts.join(", ")}] 可能不兼容上一步的输出模态 [${[...prevOutputs].join(", ")}]`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (warnings.length > 0) {
|
||||
currStep.warnings = warnings;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function rankModels(
|
||||
config: Config,
|
||||
candidates: ScoredCandidate[],
|
||||
intent: IntentProfile,
|
||||
userInput: string,
|
||||
top: number,
|
||||
options?: RecommendOptions,
|
||||
): Promise<RecommendResult> {
|
||||
const candidatesContext = buildCandidatesContext(candidates);
|
||||
const intentContext = buildIntentContext(intent);
|
||||
const systemPrompt =
|
||||
intent.complexity === Complexities.Pipeline ? PIPELINE_SYSTEM_PROMPT : SINGLE_SYSTEM_PROMPT;
|
||||
|
||||
const useThinkingModel = options?.enableThinking ?? false;
|
||||
|
||||
const userMessage =
|
||||
intent.complexity === Complexities.Pipeline
|
||||
? `意图分析结果:\n${intentContext}\n\n候选模型列表:\n${candidatesContext}\n\n用户原始需求:${userInput}\n\n请为流水线各步骤各推荐最多 ${top} 个模型。`
|
||||
: `意图分析结果:\n${intentContext}\n\n候选模型列表:\n${candidatesContext}\n\n用户原始需求:${userInput}\n\n请推荐最多 ${top} 个模型。`;
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
model: useThinkingModel ? RANKING_MODEL : RANKING_MODEL_FAST,
|
||||
messages: [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: userMessage },
|
||||
],
|
||||
max_tokens: 4096,
|
||||
temperature: 0,
|
||||
};
|
||||
|
||||
if (useThinkingModel) {
|
||||
body.stream = true;
|
||||
body.enable_thinking = true;
|
||||
}
|
||||
|
||||
const url = chatEndpoint(config.baseUrl);
|
||||
let content: string;
|
||||
|
||||
if (useThinkingModel) {
|
||||
const res = await request(config, {
|
||||
url,
|
||||
method: "POST",
|
||||
body,
|
||||
stream: true,
|
||||
});
|
||||
|
||||
let accumulated = "";
|
||||
let contentStarted = false;
|
||||
for await (const event of parseSSE(res)) {
|
||||
if (event.data === "[DONE]") break;
|
||||
try {
|
||||
const parsed = JSON.parse(event.data) as StreamChunk;
|
||||
for (const choice of parsed.choices) {
|
||||
const delta = choice.delta;
|
||||
if (delta.reasoning_content && options?.onThinking) {
|
||||
options.onThinking(delta.reasoning_content);
|
||||
}
|
||||
if (delta.content) {
|
||||
if (!contentStarted) {
|
||||
contentStarted = true;
|
||||
options?.onContentStart?.();
|
||||
}
|
||||
accumulated += delta.content;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// skip unparseable chunks
|
||||
}
|
||||
}
|
||||
content = accumulated || "{}";
|
||||
} else {
|
||||
const response = await requestJson<ChatResponse>(config, {
|
||||
url,
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
content = response.choices?.[0]?.message?.content ?? "{}";
|
||||
}
|
||||
|
||||
let parsed: any;
|
||||
try {
|
||||
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
||||
parsed = JSON.parse(jsonMatch?.[0] ?? "{}");
|
||||
} catch {
|
||||
return { type: Complexities.Single, recommendations: [] };
|
||||
}
|
||||
|
||||
const modelMap = new Map(candidates.map(({ model: profile }) => [profile.model, profile]));
|
||||
|
||||
if (parsed.type === Complexities.Pipeline && Array.isArray(parsed.steps)) {
|
||||
const steps: PipelineStep[] = [];
|
||||
for (const rawStep of parsed.steps) {
|
||||
const items = rawStep.recommendations ?? (rawStep.model ? [rawStep] : []);
|
||||
const recs = buildRecommendations(items, modelMap, top);
|
||||
if (recs.length > 0) {
|
||||
steps.push({ step: rawStep.step ?? "", recommendations: recs });
|
||||
}
|
||||
}
|
||||
validatePipelineCompatibility(steps, modelMap);
|
||||
return {
|
||||
type: Complexities.Pipeline,
|
||||
summary: parsed.summary ?? "",
|
||||
steps,
|
||||
};
|
||||
}
|
||||
|
||||
const items = parsed.recommendations ?? parsed ?? [];
|
||||
const recommendations = buildRecommendations(items, modelMap, top);
|
||||
|
||||
return { type: Complexities.Single, recommendations };
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { Config } from "../../config/schema.ts";
|
||||
import { fetchModelList } from "../../console/models.ts";
|
||||
import type { ModelProfile } from "../types.ts";
|
||||
import type { ModelSource } from "./types.ts";
|
||||
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
function toModelProfile(item: Record<string, unknown>): ModelProfile | null {
|
||||
if (!item.model) return null;
|
||||
const meta = item.inferenceMetadata as Record<string, unknown> | undefined;
|
||||
return {
|
||||
model: item.model as string,
|
||||
name: (item.name as string) ?? (item.model as string),
|
||||
description: (item.description as string) ?? (item.shortDescription as string) ?? "",
|
||||
shortDescription: item.shortDescription as string | undefined,
|
||||
provider: (item.provider as string) ?? "",
|
||||
capabilities: (item.capabilities as string[]) ?? [],
|
||||
features: (item.features as string[]) ?? [],
|
||||
category: item.category as ModelProfile["category"],
|
||||
contextWindow: (item.contextWindow as number) ?? undefined,
|
||||
maxOutputTokens: (item.maxOutputTokens as number) ?? undefined,
|
||||
maxInputTokens: (item.maxInputTokens as number) ?? undefined,
|
||||
docUrl: item.docUrl as string | undefined,
|
||||
collectionTag: item.collectionTag as string | undefined,
|
||||
inferenceMetadata: meta as ModelProfile["inferenceMetadata"],
|
||||
prices: item.prices as ModelProfile["prices"],
|
||||
qpmInfo: item.qpmInfo as ModelProfile["qpmInfo"],
|
||||
versionTag: item.versionTag as string | undefined,
|
||||
openSource: item.openSource as boolean | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export class ApiSource implements ModelSource {
|
||||
readonly name = "api";
|
||||
constructor(private config: Config) {}
|
||||
|
||||
available(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
async load(): Promise<ModelProfile[]> {
|
||||
const first = await fetchModelList(this.config, "", {
|
||||
pageNo: 1,
|
||||
pageSize: PAGE_SIZE,
|
||||
});
|
||||
const allRaw = [...first.models];
|
||||
|
||||
const totalPages = Math.ceil(first.total / PAGE_SIZE);
|
||||
for (let page = 2; page <= totalPages; page++) {
|
||||
const result = await fetchModelList(this.config, "", {
|
||||
pageNo: page,
|
||||
pageSize: PAGE_SIZE,
|
||||
});
|
||||
allRaw.push(...result.models);
|
||||
}
|
||||
|
||||
return allRaw
|
||||
.map(toModelProfile)
|
||||
.filter((profile): profile is ModelProfile => profile !== null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { cpSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { getConfigDir } from "../../config/paths.ts";
|
||||
import type { ModelPrice, ModelProfile, QpmLimit } from "../types.ts";
|
||||
import type { ModelSource } from "./types.ts";
|
||||
|
||||
const SKILL_DIR_NAME = "skills/doc-llm-wiki";
|
||||
const MODELS_FILE = "models.jsonl";
|
||||
|
||||
function getCatalogDir(): string {
|
||||
return join(getConfigDir(), SKILL_DIR_NAME);
|
||||
}
|
||||
|
||||
function getCatalogPath(): string {
|
||||
return join(getCatalogDir(), MODELS_FILE);
|
||||
}
|
||||
|
||||
function getMonorepoModelsDir(): string {
|
||||
const coreDir = dirname(fileURLToPath(import.meta.url));
|
||||
return join(coreDir, "../../../../../skills/doc-llm-wiki/models");
|
||||
}
|
||||
|
||||
function fromJsonlRecord(raw: Record<string, unknown>): ModelProfile | null {
|
||||
if (!raw.model || typeof raw.model !== "string") return null;
|
||||
return {
|
||||
model: raw.model,
|
||||
name: (raw.name as string) ?? raw.model,
|
||||
description: (raw.description as string) ?? "",
|
||||
provider: (raw.provider as string) ?? "",
|
||||
capabilities: (raw.capabilities as string[]) ?? [],
|
||||
features: (raw.features as string[]) ?? [],
|
||||
contextWindow: raw.contextWindow as number | undefined,
|
||||
maxOutputTokens: raw.maxOutputTokens as number | undefined,
|
||||
docUrl: raw.docUrl as string | undefined,
|
||||
inferenceMetadata: raw.inferenceMetadata as ModelProfile["inferenceMetadata"],
|
||||
shortDescription: raw.shortDescription as string | undefined,
|
||||
category: raw.category as ModelProfile["category"],
|
||||
collectionTag: raw.collectionTag as string | undefined,
|
||||
maxInputTokens: raw.maxInputTokens as number | undefined,
|
||||
prices: raw.prices as ModelPrice[] | undefined,
|
||||
qpmInfo: raw.qpmInfo as Record<string, QpmLimit> | undefined,
|
||||
versionTag: raw.versionTag as string | undefined,
|
||||
openSource: raw.openSource as boolean | undefined,
|
||||
family: raw.family as string | undefined,
|
||||
familyName: raw.familyName as string | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function readJsonlModels(filePath: string): ModelProfile[] {
|
||||
const content = readFileSync(filePath, "utf-8");
|
||||
const lines = content.split("\n").filter(Boolean);
|
||||
const models: ModelProfile[] = [];
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const record = fromJsonlRecord(JSON.parse(line));
|
||||
if (record) models.push(record);
|
||||
} catch {
|
||||
// skip malformed lines
|
||||
}
|
||||
}
|
||||
return models;
|
||||
}
|
||||
|
||||
function installFromMonorepo(): boolean {
|
||||
const src = getMonorepoModelsDir();
|
||||
if (!existsSync(join(src, MODELS_FILE))) return false;
|
||||
const dest = getCatalogDir();
|
||||
try {
|
||||
mkdirSync(dest, { recursive: true });
|
||||
cpSync(src, dest, { recursive: true });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export interface CatalogSourceOptions {
|
||||
onPrepareStart?: () => void;
|
||||
}
|
||||
|
||||
export class CatalogSource implements ModelSource {
|
||||
readonly name = "catalog";
|
||||
private options: CatalogSourceOptions;
|
||||
|
||||
constructor(options?: CatalogSourceOptions) {
|
||||
this.options = options ?? {};
|
||||
}
|
||||
|
||||
available(): boolean {
|
||||
return existsSync(getCatalogPath());
|
||||
}
|
||||
|
||||
async load(): Promise<ModelProfile[]> {
|
||||
if (!this.available()) {
|
||||
this.options.onPrepareStart?.();
|
||||
const installed = installFromMonorepo();
|
||||
if (!installed) return [];
|
||||
}
|
||||
return readJsonlModels(getCatalogPath());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { ModelProfile } from "../types.ts";
|
||||
|
||||
export interface ModelSource {
|
||||
name: string;
|
||||
available(): boolean;
|
||||
load(): Promise<ModelProfile[]>;
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
// ---- Shared Enums ----
|
||||
|
||||
export type Modality = "Text" | "Image" | "Video" | "Audio";
|
||||
export type Complexity = "single" | "pipeline";
|
||||
export type Budget = "low" | "medium" | "high";
|
||||
export type ContextNeed = "standard" | "large" | "extra-large";
|
||||
export type QualityPreference = "flagship" | "balanced" | "cost-optimized";
|
||||
export type Capability =
|
||||
| "TG"
|
||||
| "Reasoning"
|
||||
| "VU"
|
||||
| "IG"
|
||||
| "VG"
|
||||
| "TTS"
|
||||
| "ASR"
|
||||
| "Realtime-ASR"
|
||||
| "Realtime-Text-to-Speech"
|
||||
| "Realtime-Audio-Translate"
|
||||
| "Realtime-Omni"
|
||||
| "Multimodal-Omni"
|
||||
| "ME"
|
||||
| "TR"
|
||||
| "3D-generation";
|
||||
export type Feature =
|
||||
| "function-calling"
|
||||
| "web-search"
|
||||
| "structured-outputs"
|
||||
| "prefix-completion";
|
||||
export type ModelCategory = "Flagship" | "Cost-optimized";
|
||||
|
||||
export const Modalities = {
|
||||
Text: "Text",
|
||||
Image: "Image",
|
||||
Video: "Video",
|
||||
Audio: "Audio",
|
||||
} as const;
|
||||
export const Complexities = { Single: "single", Pipeline: "pipeline" } as const;
|
||||
export const Budgets = { Low: "low", Medium: "medium", High: "high" } as const;
|
||||
export const ContextNeeds = {
|
||||
Standard: "standard",
|
||||
Large: "large",
|
||||
ExtraLarge: "extra-large",
|
||||
} as const;
|
||||
export const QualityPreferences = {
|
||||
Flagship: "flagship",
|
||||
Balanced: "balanced",
|
||||
CostOptimized: "cost-optimized",
|
||||
} as const;
|
||||
export const Capabilities = {
|
||||
TG: "TG",
|
||||
Reasoning: "Reasoning",
|
||||
VU: "VU",
|
||||
IG: "IG",
|
||||
VG: "VG",
|
||||
TTS: "TTS",
|
||||
ASR: "ASR",
|
||||
RealtimeASR: "Realtime-ASR",
|
||||
RealtimeTTS: "Realtime-Text-to-Speech",
|
||||
RealtimeAudioTranslate: "Realtime-Audio-Translate",
|
||||
RealtimeOmni: "Realtime-Omni",
|
||||
MultimodalOmni: "Multimodal-Omni",
|
||||
ME: "ME",
|
||||
TR: "TR",
|
||||
ThreeDGeneration: "3D-generation",
|
||||
} as const;
|
||||
export const Features = {
|
||||
FunctionCalling: "function-calling",
|
||||
WebSearch: "web-search",
|
||||
StructuredOutputs: "structured-outputs",
|
||||
PrefixCompletion: "prefix-completion",
|
||||
} as const;
|
||||
export const ModelCategories = {
|
||||
Flagship: "Flagship",
|
||||
CostOptimized: "Cost-optimized",
|
||||
} as const;
|
||||
|
||||
// ---- Intent Analysis ----
|
||||
|
||||
export interface IntentSegment {
|
||||
step: string;
|
||||
inputModality: Modality[];
|
||||
outputModality: Modality[];
|
||||
requiredCapabilities: Capability[];
|
||||
}
|
||||
|
||||
export interface IntentProfile {
|
||||
complexity: Complexity;
|
||||
segments?: IntentSegment[];
|
||||
|
||||
taskSummary: string;
|
||||
scenarioHints: string[];
|
||||
|
||||
inputModality: Modality[];
|
||||
outputModality: Modality[];
|
||||
requiredCapabilities: Capability[];
|
||||
requiredFeatures: Feature[];
|
||||
|
||||
budget: Budget;
|
||||
contextNeed: ContextNeed;
|
||||
qualityPreference: QualityPreference;
|
||||
confidence: number;
|
||||
}
|
||||
|
||||
// ---- Model Profile ----
|
||||
|
||||
export interface ModelPrice {
|
||||
type: string;
|
||||
unit: string;
|
||||
price: string;
|
||||
}
|
||||
|
||||
export interface QpmLimit {
|
||||
count_limit: number;
|
||||
count_limit_period: number;
|
||||
usage_limit: number;
|
||||
usage_limit_field: string;
|
||||
usage_limit_period: number;
|
||||
}
|
||||
|
||||
export interface ModelProfile {
|
||||
model: string;
|
||||
name: string;
|
||||
description: string;
|
||||
provider: string;
|
||||
capabilities: string[];
|
||||
features: string[];
|
||||
contextWindow?: number;
|
||||
maxOutputTokens?: number;
|
||||
docUrl?: string;
|
||||
inferenceMetadata?: {
|
||||
request_modality?: Modality[];
|
||||
response_modality?: Modality[];
|
||||
};
|
||||
|
||||
shortDescription?: string;
|
||||
category?: ModelCategory;
|
||||
collectionTag?: string;
|
||||
maxInputTokens?: number;
|
||||
prices?: ModelPrice[];
|
||||
qpmInfo?: Record<string, QpmLimit>;
|
||||
versionTag?: string;
|
||||
openSource?: boolean;
|
||||
family?: string;
|
||||
familyName?: string;
|
||||
}
|
||||
|
||||
export interface RecommendedModel {
|
||||
model: string;
|
||||
name: string;
|
||||
reason: string;
|
||||
highlights: string[];
|
||||
category?: ModelCategory;
|
||||
contextWindow?: number;
|
||||
maxOutputTokens?: number;
|
||||
docUrl?: string;
|
||||
}
|
||||
|
||||
export interface PipelineStep {
|
||||
step: string;
|
||||
recommendations: RecommendedModel[];
|
||||
warnings?: string[];
|
||||
}
|
||||
|
||||
export interface SingleResult {
|
||||
type: "single";
|
||||
recommendations: RecommendedModel[];
|
||||
}
|
||||
|
||||
export interface PipelineResult {
|
||||
type: "pipeline";
|
||||
summary: string;
|
||||
steps: PipelineStep[];
|
||||
}
|
||||
|
||||
export type RecommendResult = SingleResult | PipelineResult;
|
||||
@@ -87,7 +87,7 @@ export function loadConfig(flags: GlobalFlags): Config {
|
||||
consoleGatewayUrl:
|
||||
process.env.BAILIAN_CONSOLE_GATEWAY_URL ||
|
||||
file.console_gateway_url ||
|
||||
"https://bailian-cs.console.aliyun.com",
|
||||
"https://pre-bailian-cs.console.aliyun.com",
|
||||
verbose: flags.verbose || process.env.DASHSCOPE_VERBOSE === "1",
|
||||
quiet: flags.quiet || false,
|
||||
noColor: flags.noColor || process.env.NO_COLOR !== undefined || !process.stdout.isTTY,
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
export type { ConsoleGatewayRequest } from "./gateway.ts";
|
||||
export { callConsoleGateway } from "./gateway.ts";
|
||||
export type { ModelListParams, ModelListResult } from "./models.ts";
|
||||
export { fetchModelList } from "./models.ts";
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { callConsoleGateway } from "./gateway.ts";
|
||||
import type { Config } from "../config/schema.ts";
|
||||
|
||||
const MODEL_LIST_API = "zeldaHttp.dashscopeModel./zelda/api/v1/modelCenter/listFoundationModels";
|
||||
|
||||
export interface ModelListParams {
|
||||
pageNo?: number;
|
||||
pageSize?: number;
|
||||
name?: string;
|
||||
providers?: string[];
|
||||
capabilities?: string[];
|
||||
region?: string;
|
||||
}
|
||||
|
||||
export interface ModelListResult {
|
||||
total: number;
|
||||
models: Record<string, unknown>[];
|
||||
}
|
||||
|
||||
export async function fetchModelList(
|
||||
config: Config,
|
||||
token: string,
|
||||
params: ModelListParams = {},
|
||||
): Promise<ModelListResult> {
|
||||
const {
|
||||
pageNo = 1,
|
||||
pageSize = 50,
|
||||
name = "",
|
||||
providers = [],
|
||||
capabilities = [],
|
||||
region = "cn-beijing",
|
||||
} = params;
|
||||
|
||||
const result = (await callConsoleGateway(config, token, {
|
||||
api: MODEL_LIST_API,
|
||||
data: {
|
||||
input: {
|
||||
pageNo,
|
||||
pageSize,
|
||||
name,
|
||||
providers,
|
||||
inferenceProviders: [],
|
||||
features: [],
|
||||
group: true,
|
||||
capabilities,
|
||||
contextWindows: [],
|
||||
},
|
||||
},
|
||||
region,
|
||||
})) as any;
|
||||
|
||||
const responseData = result?.data?.DataV2?.data ?? result?.data ?? {};
|
||||
const total: number = responseData?.data?.total ?? responseData?.total ?? 0;
|
||||
const groups: any[] = responseData?.data?.list ?? responseData?.list ?? [];
|
||||
|
||||
const models: Record<string, unknown>[] = [];
|
||||
for (const group of groups) {
|
||||
if (group.items?.length) {
|
||||
for (const item of group.items) models.push(item);
|
||||
} else {
|
||||
models.push(group);
|
||||
}
|
||||
}
|
||||
|
||||
return { total, models };
|
||||
}
|
||||
@@ -12,3 +12,4 @@ export * from "./files/index.ts";
|
||||
export * from "./types/index.ts";
|
||||
export * from "./utils/index.ts";
|
||||
export * from "./telemetry/index.ts";
|
||||
export * from "./advisor/index.ts";
|
||||
|
||||
Generated
+152
@@ -12,6 +12,12 @@ catalogs:
|
||||
ajv:
|
||||
specifier: ^8.20.0
|
||||
version: 8.20.0
|
||||
boxen:
|
||||
specifier: ^8.0.1
|
||||
version: 8.0.1
|
||||
chalk:
|
||||
specifier: ^5.6.2
|
||||
version: 5.6.2
|
||||
vite-plus:
|
||||
specifier: latest
|
||||
version: 0.1.22
|
||||
@@ -36,6 +42,12 @@ importers:
|
||||
bailian-cli-core:
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
boxen:
|
||||
specifier: 'catalog:'
|
||||
version: 8.0.1
|
||||
chalk:
|
||||
specifier: 'catalog:'
|
||||
version: 5.6.2
|
||||
devDependencies:
|
||||
'@clack/prompts':
|
||||
specifier: ^0.7.0
|
||||
@@ -698,14 +710,51 @@ packages:
|
||||
ajv@8.20.0:
|
||||
resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
|
||||
|
||||
ansi-align@3.0.1:
|
||||
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
|
||||
|
||||
ansi-regex@5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-regex@6.2.2:
|
||||
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ansi-styles@6.2.3:
|
||||
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
assertion-error@2.0.1:
|
||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
boxen@8.0.1:
|
||||
resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
camelcase@8.0.0:
|
||||
resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
chalk@5.6.2:
|
||||
resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
|
||||
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
||||
|
||||
cli-boxes@3.0.0:
|
||||
resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
detect-libc@2.1.2:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
emoji-regex@10.6.0:
|
||||
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
|
||||
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
es-module-lexer@1.7.0:
|
||||
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
|
||||
|
||||
@@ -729,6 +778,14 @@ packages:
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
get-east-asian-width@1.6.0:
|
||||
resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
is-fullwidth-code-point@3.0.0:
|
||||
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
jiti@2.6.1:
|
||||
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
||||
hasBin: true
|
||||
@@ -883,6 +940,22 @@ packages:
|
||||
std-env@4.1.0:
|
||||
resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
|
||||
|
||||
string-width@4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
string-width@7.2.0:
|
||||
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
strip-ansi@7.2.0:
|
||||
resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
tinybench@2.9.0:
|
||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||
|
||||
@@ -905,6 +978,10 @@ packages:
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
type-fest@4.41.0:
|
||||
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
typescript@6.0.3:
|
||||
resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
|
||||
engines: {node: '>=14.17'}
|
||||
@@ -964,6 +1041,14 @@ packages:
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
widest-line@5.0.0:
|
||||
resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
wrap-ansi@9.0.2:
|
||||
resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
ws@8.20.0:
|
||||
resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
@@ -1401,10 +1486,41 @@ snapshots:
|
||||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
|
||||
ansi-align@3.0.1:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
ansi-regex@6.2.2: {}
|
||||
|
||||
ansi-styles@6.2.3: {}
|
||||
|
||||
assertion-error@2.0.1: {}
|
||||
|
||||
boxen@8.0.1:
|
||||
dependencies:
|
||||
ansi-align: 3.0.1
|
||||
camelcase: 8.0.0
|
||||
chalk: 5.6.2
|
||||
cli-boxes: 3.0.0
|
||||
string-width: 7.2.0
|
||||
type-fest: 4.41.0
|
||||
widest-line: 5.0.0
|
||||
wrap-ansi: 9.0.2
|
||||
|
||||
camelcase@8.0.0: {}
|
||||
|
||||
chalk@5.6.2: {}
|
||||
|
||||
cli-boxes@3.0.0: {}
|
||||
|
||||
detect-libc@2.1.2: {}
|
||||
|
||||
emoji-regex@10.6.0: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
es-module-lexer@1.7.0: {}
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
@@ -1418,6 +1534,10 @@ snapshots:
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
get-east-asian-width@1.6.0: {}
|
||||
|
||||
is-fullwidth-code-point@3.0.0: {}
|
||||
|
||||
jiti@2.6.1:
|
||||
optional: true
|
||||
|
||||
@@ -1585,6 +1705,26 @@ snapshots:
|
||||
|
||||
std-env@4.1.0: {}
|
||||
|
||||
string-width@4.2.3:
|
||||
dependencies:
|
||||
emoji-regex: 8.0.0
|
||||
is-fullwidth-code-point: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
string-width@7.2.0:
|
||||
dependencies:
|
||||
emoji-regex: 10.6.0
|
||||
get-east-asian-width: 1.6.0
|
||||
strip-ansi: 7.2.0
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
|
||||
strip-ansi@7.2.0:
|
||||
dependencies:
|
||||
ansi-regex: 6.2.2
|
||||
|
||||
tinybench@2.9.0: {}
|
||||
|
||||
tinyexec@1.1.2: {}
|
||||
@@ -1601,6 +1741,8 @@ snapshots:
|
||||
tslib@2.8.1:
|
||||
optional: true
|
||||
|
||||
type-fest@4.41.0: {}
|
||||
|
||||
typescript@6.0.3: {}
|
||||
|
||||
undici-types@7.16.0: {}
|
||||
@@ -1732,6 +1874,16 @@ snapshots:
|
||||
jiti: 2.6.1
|
||||
yaml: 2.8.3
|
||||
|
||||
widest-line@5.0.0:
|
||||
dependencies:
|
||||
string-width: 7.2.0
|
||||
|
||||
wrap-ansi@9.0.2:
|
||||
dependencies:
|
||||
ansi-styles: 6.2.3
|
||||
string-width: 7.2.0
|
||||
strip-ansi: 7.2.0
|
||||
|
||||
ws@8.20.0: {}
|
||||
|
||||
yaml@2.8.3: {}
|
||||
|
||||
@@ -5,6 +5,8 @@ packages:
|
||||
catalog:
|
||||
"@types/node": ^24
|
||||
ajv: ^8.20.0
|
||||
boxen: ^8.0.1
|
||||
chalk: ^5.6.2
|
||||
typescript: ^5
|
||||
vite: npm:@voidzero-dev/vite-plus-core@latest
|
||||
vite-plus: latest
|
||||
|
||||
Reference in New Issue
Block a user