mirror of
https://github.com/luckyyzh/pi-agent-integrated.git
synced 2026-10-03 11:09:34 +00:00
405 lines
14 KiB
TypeScript
405 lines
14 KiB
TypeScript
export interface ModelCatalogCost {
|
|
input?: number;
|
|
output?: number;
|
|
cacheRead?: number;
|
|
cacheWrite?: number;
|
|
}
|
|
|
|
export interface ModelCatalogEntry {
|
|
key: string;
|
|
providerId: string;
|
|
providerName: string;
|
|
providerBaseUrl?: string;
|
|
id: string;
|
|
name: string;
|
|
reasoning?: boolean;
|
|
input?: string[];
|
|
contextWindow?: number;
|
|
maxTokens?: number;
|
|
cost: ModelCatalogCost;
|
|
}
|
|
|
|
export interface ModelCatalogPreset {
|
|
name?: string;
|
|
reasoning?: boolean;
|
|
input?: string[];
|
|
contextWindow?: number;
|
|
maxTokens?: number;
|
|
cost?: ModelCatalogCost;
|
|
}
|
|
|
|
export type ModelCatalogMatchMethod = "provider" | "base-url" | "consensus" | "none";
|
|
|
|
export type ModelCatalogPriceRecommendation =
|
|
| {
|
|
status: "reliable";
|
|
method: Exclude<ModelCatalogMatchMethod, "none">;
|
|
cost: ModelCatalogCost;
|
|
providerId?: string;
|
|
providerName?: string;
|
|
support: number;
|
|
total: number;
|
|
}
|
|
| {
|
|
status: "unreliable";
|
|
reason: "no-exact-match" | "no-valid-price" | "insufficient-support" | "conflict";
|
|
support: number;
|
|
total: number;
|
|
};
|
|
|
|
export interface ModelCatalogRecommendation {
|
|
exactMatches: number;
|
|
metadataMethod: ModelCatalogMatchMethod;
|
|
matchedProviderId?: string;
|
|
matchedProviderName?: string;
|
|
preset: ModelCatalogPreset;
|
|
price: ModelCatalogPriceRecommendation;
|
|
}
|
|
|
|
const CONSENSUS_MIN_SHARE = 0.6;
|
|
const KNOWN_PROVIDER_HOSTS: Record<string, readonly string[]> = {
|
|
anthropic: ["api.anthropic.com"],
|
|
google: ["generativelanguage.googleapis.com"],
|
|
openai: ["api.openai.com"],
|
|
openrouter: ["openrouter.ai"],
|
|
};
|
|
const SUPPORTED_INPUT_MODALITIES = new Set(["text", "image"]);
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function cleanString(value: unknown): string | undefined {
|
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
}
|
|
|
|
function optionalNonNegativeNumber(value: unknown): number | undefined {
|
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
}
|
|
|
|
function optionalPositiveNumber(value: unknown): number | undefined {
|
|
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
}
|
|
|
|
function readCost(value: unknown): ModelCatalogCost {
|
|
if (!isRecord(value)) return {};
|
|
return {
|
|
input: optionalNonNegativeNumber(value.input),
|
|
output: optionalNonNegativeNumber(value.output),
|
|
cacheRead: optionalNonNegativeNumber(value.cache_read),
|
|
cacheWrite: optionalNonNegativeNumber(value.cache_write),
|
|
};
|
|
}
|
|
|
|
function readInputModalities(value: unknown): string[] | undefined {
|
|
if (!isRecord(value) || !Array.isArray(value.input)) return undefined;
|
|
const input = Array.from(new Set(value.input
|
|
.filter((entry): entry is string => typeof entry === "string")
|
|
.map((entry) => entry.trim().toLocaleLowerCase())
|
|
.filter((entry) => SUPPORTED_INPUT_MODALITIES.has(entry))));
|
|
return input.length ? input : undefined;
|
|
}
|
|
|
|
function normalizeProvider(value: string): string {
|
|
return value.trim().toLocaleLowerCase().replace(/[^a-z0-9]/g, "");
|
|
}
|
|
|
|
function normalizeModelId(value: string): string {
|
|
return value.trim().toLocaleLowerCase().replace(/^models\//, "");
|
|
}
|
|
|
|
function hostname(value: string | undefined): string | undefined {
|
|
if (!value) return undefined;
|
|
try {
|
|
return new URL(value).hostname.toLocaleLowerCase().replace(/\.$/, "");
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function hostMatches(actual: string, expected: string): boolean {
|
|
return actual === expected || actual.endsWith(`.${expected}`);
|
|
}
|
|
|
|
function providerMatches(entry: ModelCatalogEntry, providerHint: string): boolean {
|
|
const normalizedHint = normalizeProvider(providerHint);
|
|
if (!normalizedHint) return false;
|
|
return normalizeProvider(entry.providerId) === normalizedHint
|
|
|| normalizeProvider(entry.providerName) === normalizedHint;
|
|
}
|
|
|
|
function baseUrlMatches(entry: ModelCatalogEntry, baseUrl: string): boolean {
|
|
const actualHost = hostname(baseUrl);
|
|
if (!actualHost) return false;
|
|
const knownHosts = KNOWN_PROVIDER_HOSTS[normalizeProvider(entry.providerId)] ?? [];
|
|
const providerHost = hostname(entry.providerBaseUrl);
|
|
return [...knownHosts, ...(providerHost ? [providerHost] : [])]
|
|
.some((candidate) => hostMatches(actualHost, candidate));
|
|
}
|
|
|
|
function exactModelMatches(entry: ModelCatalogEntry, query: string): boolean {
|
|
const normalizedQuery = normalizeModelId(query);
|
|
if (!normalizedQuery) return false;
|
|
const normalizedId = normalizeModelId(entry.id);
|
|
const normalizedFullId = `${entry.providerId.toLocaleLowerCase()}/${normalizedId}`;
|
|
return normalizedId === normalizedQuery || normalizedFullId === normalizedQuery;
|
|
}
|
|
|
|
function validPrice(entry: ModelCatalogEntry): entry is ModelCatalogEntry & {
|
|
cost: ModelCatalogCost & { input: number; output: number };
|
|
} {
|
|
return entry.cost.input !== undefined && entry.cost.output !== undefined;
|
|
}
|
|
|
|
function modeValue<T>(
|
|
values: readonly T[],
|
|
total: number,
|
|
keyFor: (value: T) => string,
|
|
): T | undefined {
|
|
if (values.length === 0 || total <= 0) return undefined;
|
|
const groups = new Map<string, { value: T; count: number }>();
|
|
for (const value of values) {
|
|
const key = keyFor(value);
|
|
const current = groups.get(key);
|
|
if (current) current.count += 1;
|
|
else groups.set(key, { value, count: 1 });
|
|
}
|
|
const ranked = [...groups.values()].sort((a, b) => b.count - a.count);
|
|
const winner = ranked[0];
|
|
if (!winner || winner.count / total < CONSENSUS_MIN_SHARE) return undefined;
|
|
if (ranked[1]?.count === winner.count) return undefined;
|
|
return winner.value;
|
|
}
|
|
|
|
function modeNumber(values: readonly number[]): number | undefined {
|
|
if (values.length === 0) return undefined;
|
|
const groups = new Map<number, number>();
|
|
for (const value of values) groups.set(value, (groups.get(value) ?? 0) + 1);
|
|
const ranked = [...groups.entries()].sort((a, b) => b[1] - a[1]);
|
|
if (!ranked[0] || ranked[1]?.[1] === ranked[0][1]) return undefined;
|
|
return ranked[0][0];
|
|
}
|
|
|
|
function metadataFromEntry(entry: ModelCatalogEntry): ModelCatalogPreset {
|
|
return {
|
|
name: entry.name,
|
|
reasoning: entry.reasoning,
|
|
input: entry.input,
|
|
contextWindow: entry.contextWindow,
|
|
maxTokens: entry.maxTokens,
|
|
};
|
|
}
|
|
|
|
function consensusMetadata(entries: readonly ModelCatalogEntry[]): ModelCatalogPreset {
|
|
const total = entries.length;
|
|
return {
|
|
name: modeValue(entries.map((entry) => entry.name), total, (value) => value.toLocaleLowerCase()),
|
|
reasoning: modeValue(
|
|
entries.flatMap((entry) => entry.reasoning === undefined ? [] : [entry.reasoning]),
|
|
total,
|
|
String,
|
|
),
|
|
input: modeValue(
|
|
entries.flatMap((entry) => entry.input ? [entry.input] : []),
|
|
total,
|
|
(value) => [...value].sort().join(","),
|
|
),
|
|
contextWindow: modeValue(
|
|
entries.flatMap((entry) => entry.contextWindow === undefined ? [] : [entry.contextWindow]),
|
|
total,
|
|
String,
|
|
),
|
|
maxTokens: modeValue(
|
|
entries.flatMap((entry) => entry.maxTokens === undefined ? [] : [entry.maxTokens]),
|
|
total,
|
|
String,
|
|
),
|
|
};
|
|
}
|
|
|
|
function priceFromEntry(
|
|
entry: ModelCatalogEntry & { cost: ModelCatalogCost & { input: number; output: number } },
|
|
method: "provider" | "base-url",
|
|
): ModelCatalogPriceRecommendation {
|
|
return {
|
|
status: "reliable",
|
|
method,
|
|
cost: entry.cost,
|
|
providerId: entry.providerId,
|
|
providerName: entry.providerName,
|
|
support: 1,
|
|
total: 1,
|
|
};
|
|
}
|
|
|
|
function consensusPrice(entries: readonly ModelCatalogEntry[]): ModelCatalogPriceRecommendation {
|
|
const priced = entries.filter(validPrice);
|
|
if (priced.length === 0) {
|
|
return { status: "unreliable", reason: "no-valid-price", support: 0, total: 0 };
|
|
}
|
|
if (priced.length === 1) {
|
|
return { status: "unreliable", reason: "insufficient-support", support: 1, total: 1 };
|
|
}
|
|
|
|
const groups = new Map<string, typeof priced>();
|
|
for (const entry of priced) {
|
|
const key = JSON.stringify([entry.cost.input, entry.cost.output]);
|
|
const group = groups.get(key);
|
|
if (group) group.push(entry);
|
|
else groups.set(key, [entry]);
|
|
}
|
|
const ranked = [...groups.values()].sort((a, b) => b.length - a.length);
|
|
const winner = ranked[0];
|
|
if (!winner) {
|
|
return { status: "unreliable", reason: "no-valid-price", support: 0, total: priced.length };
|
|
}
|
|
if (ranked[1]?.length === winner.length || winner.length / priced.length < CONSENSUS_MIN_SHARE) {
|
|
return {
|
|
status: "unreliable",
|
|
reason: "conflict",
|
|
support: winner.length,
|
|
total: priced.length,
|
|
};
|
|
}
|
|
|
|
const cacheRead = modeNumber(winner.flatMap((entry) => entry.cost.cacheRead === undefined ? [] : [entry.cost.cacheRead]));
|
|
const cacheWrite = modeNumber(winner.flatMap((entry) => entry.cost.cacheWrite === undefined ? [] : [entry.cost.cacheWrite]));
|
|
return {
|
|
status: "reliable",
|
|
method: "consensus",
|
|
cost: {
|
|
input: winner[0].cost.input,
|
|
output: winner[0].cost.output,
|
|
cacheRead,
|
|
cacheWrite,
|
|
},
|
|
support: winner.length,
|
|
total: priced.length,
|
|
};
|
|
}
|
|
|
|
export function flattenModelsDevCatalog(value: unknown): ModelCatalogEntry[] {
|
|
if (!isRecord(value)) return [];
|
|
|
|
const entries: ModelCatalogEntry[] = [];
|
|
for (const [providerId, rawProvider] of Object.entries(value)) {
|
|
if (!isRecord(rawProvider) || !isRecord(rawProvider.models)) continue;
|
|
const providerName = cleanString(rawProvider.name) ?? providerId;
|
|
const providerBaseUrl = cleanString(rawProvider.api);
|
|
|
|
for (const [fallbackId, rawModel] of Object.entries(rawProvider.models)) {
|
|
if (!isRecord(rawModel)) continue;
|
|
const id = cleanString(rawModel.id) ?? fallbackId;
|
|
if (!id) continue;
|
|
const name = cleanString(rawModel.name) ?? id;
|
|
const entry: ModelCatalogEntry = {
|
|
key: `${providerId}/${id}`,
|
|
providerId,
|
|
providerName,
|
|
id,
|
|
name,
|
|
cost: readCost(rawModel.cost),
|
|
};
|
|
if (providerBaseUrl) entry.providerBaseUrl = providerBaseUrl;
|
|
if (typeof rawModel.reasoning === "boolean") entry.reasoning = rawModel.reasoning;
|
|
const input = readInputModalities(rawModel.modalities);
|
|
if (input) entry.input = input;
|
|
if (isRecord(rawModel.limit)) {
|
|
const contextWindow = optionalPositiveNumber(rawModel.limit.context);
|
|
const maxTokens = optionalPositiveNumber(rawModel.limit.output);
|
|
if (contextWindow !== undefined) entry.contextWindow = contextWindow;
|
|
if (maxTokens !== undefined) entry.maxTokens = maxTokens;
|
|
}
|
|
entries.push(entry);
|
|
}
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
export function recommendModelCatalogPreset(
|
|
entries: readonly ModelCatalogEntry[],
|
|
query: string,
|
|
providerHint = "",
|
|
baseUrl = "",
|
|
): ModelCatalogRecommendation {
|
|
const exactEntries = entries.filter((entry) => exactModelMatches(entry, query));
|
|
if (exactEntries.length === 0) {
|
|
return {
|
|
exactMatches: 0,
|
|
metadataMethod: "none",
|
|
preset: {},
|
|
price: { status: "unreliable", reason: "no-exact-match", support: 0, total: 0 },
|
|
};
|
|
}
|
|
|
|
const providerEntries = exactEntries.filter((entry) => providerMatches(entry, providerHint));
|
|
const baseUrlEntries = exactEntries.filter((entry) => baseUrlMatches(entry, baseUrl));
|
|
const metadataEntry = providerEntries[0] ?? baseUrlEntries[0];
|
|
const metadataMethod: ModelCatalogMatchMethod = providerEntries.length
|
|
? "provider"
|
|
: baseUrlEntries.length
|
|
? "base-url"
|
|
: "consensus";
|
|
const preset = metadataEntry ? metadataFromEntry(metadataEntry) : consensusMetadata(exactEntries);
|
|
|
|
const providerPrice = providerEntries.find(validPrice);
|
|
const baseUrlPrice = baseUrlEntries.find(validPrice);
|
|
const price = providerPrice
|
|
? priceFromEntry(providerPrice, "provider")
|
|
: baseUrlPrice
|
|
? priceFromEntry(baseUrlPrice, "base-url")
|
|
: consensusPrice(exactEntries);
|
|
if (price.status === "reliable") preset.cost = price.cost;
|
|
|
|
return {
|
|
exactMatches: exactEntries.length,
|
|
metadataMethod,
|
|
matchedProviderId: metadataEntry?.providerId,
|
|
matchedProviderName: metadataEntry?.providerName,
|
|
preset,
|
|
price,
|
|
};
|
|
}
|
|
|
|
function matchRank(entry: ModelCatalogEntry, query: string, providerHint: string): number {
|
|
const id = entry.id.toLocaleLowerCase();
|
|
const name = entry.name.toLocaleLowerCase();
|
|
const providerId = entry.providerId.toLocaleLowerCase();
|
|
const providerName = entry.providerName.toLocaleLowerCase();
|
|
const fullId = `${providerId}/${id}`;
|
|
|
|
let rank = 20;
|
|
if (!query) rank = 10;
|
|
else if (id === query || fullId === query) rank = 0;
|
|
else if (name === query) rank = 1;
|
|
else if (id.startsWith(query) || name.startsWith(query)) rank = 2;
|
|
else if (fullId.startsWith(query) || providerId === query || providerName === query) rank = 3;
|
|
else if (id.includes(query) || name.includes(query)) rank = 4;
|
|
else if (fullId.includes(query) || providerName.includes(query)) rank = 5;
|
|
|
|
if (rank < 20 && providerHint && (providerId === providerHint || providerName === providerHint)) rank -= 0.5;
|
|
return rank;
|
|
}
|
|
|
|
export function searchModelCatalog(
|
|
entries: readonly ModelCatalogEntry[],
|
|
query: string,
|
|
providerHint = "",
|
|
limit = 50,
|
|
): ModelCatalogEntry[] {
|
|
const normalizedQuery = query.trim().toLocaleLowerCase();
|
|
const normalizedProvider = providerHint.trim().toLocaleLowerCase();
|
|
const cappedLimit = Math.max(1, Math.min(100, Math.floor(limit) || 50));
|
|
|
|
return entries
|
|
.map((entry) => ({ entry, rank: matchRank(entry, normalizedQuery, normalizedProvider) }))
|
|
.filter(({ rank }) => !normalizedQuery || rank < 20)
|
|
.sort((a, b) => a.rank - b.rank
|
|
|| a.entry.providerName.localeCompare(b.entry.providerName, undefined, { sensitivity: "base" })
|
|
|| a.entry.name.localeCompare(b.entry.name, undefined, { numeric: true, sensitivity: "base" })
|
|
|| a.entry.id.localeCompare(b.entry.id, undefined, { numeric: true, sensitivity: "base" }))
|
|
.slice(0, cappedLimit)
|
|
.map(({ entry }) => entry);
|
|
}
|