chore(vendor): import pi 1.3.0
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
98
third_party/pi/packages/ai/scripts/model-data.ts
vendored
98
third_party/pi/packages/ai/scripts/model-data.ts
vendored
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
||||
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
export const MODEL_DATA_SCHEMA_VERSION = 3;
|
||||
export const MODEL_DATA_SCHEMA_VERSION = 6;
|
||||
export const MODEL_DATA_MANIFEST_FILE = ".manifest.json";
|
||||
|
||||
export type ModelDataStructure = Record<string, Record<string, string>>;
|
||||
@@ -15,7 +15,7 @@ export interface ModelDataManifest {
|
||||
}
|
||||
|
||||
const MODEL_DATA_IMPORT_PATTERN =
|
||||
/^import \{ [A-Z][A-Z0-9_]*_MODELS \} from "\.\/providers\/([^"/]+)\.models\.ts";$/gm;
|
||||
/^import \{ [A-Z][A-Z0-9_]*_CLASSIFIER_MODELS, [A-Z][A-Z0-9_]*_IMAGE_MODELS, [A-Z][A-Z0-9_]*_MODELS \} from "\.\/providers\/([^"/]+)\.models\.ts";$/gm;
|
||||
|
||||
function sha256(value: string): string {
|
||||
return createHash("sha256").update(value).digest("hex");
|
||||
@@ -50,6 +50,10 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isModalityList(value: unknown): value is ("text" | "image")[] {
|
||||
return Array.isArray(value) && value.length > 0 && value.every((entry) => entry === "text" || entry === "image");
|
||||
}
|
||||
|
||||
function readJsonObject(path: string, description: string, errors: string[]): Record<string, unknown> | undefined {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
@@ -73,9 +77,9 @@ function readProviderStructure(path: string, providerId: string): Record<string,
|
||||
const models = new Map<string, string>();
|
||||
for (const [api, value] of Object.entries(groups)) {
|
||||
if (!isRecord(value)) throw new Error(`${path} API group ${JSON.stringify(api)} must be an object`);
|
||||
for (const modelId of Object.keys(value)) {
|
||||
if (models.has(modelId)) throw new Error(`${path} contains model ${modelId} in more than one API group`);
|
||||
models.set(modelId, api);
|
||||
for (const modelKey of Object.keys(value)) {
|
||||
if (models.has(modelKey)) throw new Error(`${path} contains ${modelKey} in more than one API group`);
|
||||
models.set(modelKey, api);
|
||||
}
|
||||
}
|
||||
if (models.size === 0) throw new Error(`${path} contains no generated model data`);
|
||||
@@ -158,19 +162,36 @@ function validateModelValue(
|
||||
}
|
||||
if (typeof value.name !== "string" || value.name.length === 0) errors.push(`${label} has no model name`);
|
||||
if (typeof value.baseUrl !== "string") errors.push(`${label} has no baseUrl string`);
|
||||
if (typeof value.reasoning !== "boolean") errors.push(`${label} has no reasoning boolean`);
|
||||
if (
|
||||
!Array.isArray(value.input) ||
|
||||
value.input.length === 0 ||
|
||||
value.input.some((entry) => entry !== "text" && entry !== "image")
|
||||
) {
|
||||
errors.push(`${label} has invalid input modalities`);
|
||||
if (!isModalityList(value.input)) errors.push(`${label} has invalid input modalities`);
|
||||
if (value.type === "image") {
|
||||
if (!isModalityList(value.output) || !value.output.includes("image")) {
|
||||
errors.push(`${label} has invalid output modalities`);
|
||||
}
|
||||
} else if (value.output !== undefined) {
|
||||
errors.push(`${label} has unsupported output modalities`);
|
||||
}
|
||||
if (typeof value.contextWindow !== "number" || !Number.isFinite(value.contextWindow) || value.contextWindow <= 0) {
|
||||
errors.push(`${label} has invalid contextWindow`);
|
||||
}
|
||||
if (typeof value.maxTokens !== "number" || !Number.isFinite(value.maxTokens) || value.maxTokens <= 0) {
|
||||
errors.push(`${label} has invalid maxTokens`);
|
||||
if (value.type === "chat") {
|
||||
if (typeof value.reasoning !== "boolean") errors.push(`${label} has no reasoning boolean`);
|
||||
if (
|
||||
typeof value.contextWindow !== "number" ||
|
||||
!Number.isFinite(value.contextWindow) ||
|
||||
value.contextWindow <= 0
|
||||
) {
|
||||
errors.push(`${label} has invalid contextWindow`);
|
||||
}
|
||||
if (typeof value.maxTokens !== "number" || !Number.isFinite(value.maxTokens) || value.maxTokens <= 0) {
|
||||
errors.push(`${label} has invalid maxTokens`);
|
||||
}
|
||||
} else if (value.type === "classifier") {
|
||||
if (
|
||||
typeof value.contextWindow !== "number" ||
|
||||
!Number.isFinite(value.contextWindow) ||
|
||||
value.contextWindow <= 0
|
||||
) {
|
||||
errors.push(`${label} has invalid contextWindow`);
|
||||
}
|
||||
} else if (value.type !== "image") {
|
||||
errors.push(`${label} has type ${JSON.stringify(value.type)}, expected "chat", "image", or "classifier"`);
|
||||
}
|
||||
if (!isRecord(value.cost)) {
|
||||
errors.push(`${label} has invalid cost metadata`);
|
||||
@@ -246,13 +267,18 @@ export function validateModelDataDirectory(structure: ModelDataStructure, dataDi
|
||||
errors.push(`${filename} API group ${JSON.stringify(api)} must be an object`);
|
||||
continue;
|
||||
}
|
||||
for (const [modelId, model] of Object.entries(value)) {
|
||||
if (actualModels.has(modelId)) {
|
||||
errors.push(`${providerId}/${modelId} appears in more than one API group`);
|
||||
for (const [modelKey, model] of Object.entries(value)) {
|
||||
if (actualModels.has(modelKey)) {
|
||||
errors.push(`${providerId}/${modelKey} appears in more than one API group`);
|
||||
continue;
|
||||
}
|
||||
actualModels.set(modelId, api);
|
||||
actualModels.set(modelKey, api);
|
||||
const separator = modelKey.indexOf(":");
|
||||
const modelId = separator >= 0 ? modelKey.slice(separator + 1) : modelKey;
|
||||
validateModelValue(model, providerId, modelId, api, errors);
|
||||
if (isRecord(model) && modelKey !== `${String(model.type)}:${String(model.id)}`) {
|
||||
errors.push(`${providerId}/${modelKey} has mismatched type/id identity`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,3 +304,33 @@ export function validateGeneratedModelData(packageRoot: string): void {
|
||||
const structure = readModelDataStructure(packageRoot);
|
||||
validateModelDataDirectory(structure, join(packageRoot, "src", "providers", "data"));
|
||||
}
|
||||
|
||||
export interface ModelCatalogEntry {
|
||||
type: string;
|
||||
id: string;
|
||||
api: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group one provider's typed catalog entries by API and key them by `type:id`,
|
||||
* the layout of `src/providers/data/<provider>.json`.
|
||||
*/
|
||||
export function groupProviderModelData<T extends ModelCatalogEntry>(
|
||||
providerId: string,
|
||||
models: readonly T[],
|
||||
): { groups: Record<string, Record<string, T>>; structure: Record<string, string> } {
|
||||
const groups: Record<string, Record<string, T>> = {};
|
||||
const structure: Record<string, string> = {};
|
||||
for (const api of Array.from(new Set(models.map((model) => model.api))).sort()) {
|
||||
const group: Record<string, T> = {};
|
||||
for (const model of models) {
|
||||
if (model.api !== api) continue;
|
||||
const identity = `${model.type}:${model.id}`;
|
||||
if (group[identity]) throw new Error(`${providerId}/${identity} has duplicate ${api} catalog entries`);
|
||||
group[identity] = model;
|
||||
structure[identity] = api;
|
||||
}
|
||||
groups[api] = group;
|
||||
}
|
||||
return { groups, structure };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user