fix: harden model catalog projections

This commit is contained in:
Codex
2026-09-02 19:25:01 +02:00
parent ce4c31a6fb
commit a6a5bf2036
38 changed files with 573 additions and 83 deletions
+1
View File
@@ -268,6 +268,7 @@ export function buildApp(config: AppConfig, deps?: BuildAppDeps): FastifyInstanc
);
const listModels = deps?.listModels ?? createPiModelLister(config, {
modelCatalog: runtimeModelCatalog,
warn: (detail) => app.log.warn(
{ component: "pi-model-list", detail },
"Pi enabled-model configuration warning",
@@ -3,16 +3,17 @@ import type { CatalogDatabase } from "../repository.js";
const canonicalModelPattern = "^[a-z][a-z0-9._-]{0,63}/[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$";
const legacyModelPattern = "^[a-z][a-z0-9._-]{0,63}$";
const historicalOrCanonicalModelPattern = `(${legacyModelPattern})|(${canonicalModelPattern})`;
export async function up(db: Kysely<CatalogDatabase>): Promise<void> {
await sql.raw(`alter table description_generation_runs
drop constraint description_generation_runs_model_id_check,
add constraint description_generation_runs_model_id_check
check (model_id ~ '${canonicalModelPattern}')`).execute(db);
check (model_id ~ '${historicalOrCanonicalModelPattern}')`).execute(db);
await sql.raw(`alter table sensitive_data_suggestion_runs
drop constraint sensitive_data_suggestion_runs_model_id_check,
add constraint sensitive_data_suggestion_runs_model_id_check
check (model_id ~ '${canonicalModelPattern}')`).execute(db);
check (model_id ~ '${historicalOrCanonicalModelPattern}')`).execute(db);
}
export async function down(db: Kysely<CatalogDatabase>): Promise<void> {
+17 -1
View File
@@ -10,6 +10,7 @@ import {
readConfiguredPiAgentFile,
validateDeclarativePiConfig,
} from "./managed-config.js";
import type { RuntimeModelCatalog } from "../models/runtime-model-catalog.js";
export interface PiModel {
provider: string;
@@ -30,6 +31,7 @@ interface Opts {
nowMs?: () => number;
loadEnabledModels?: () => PiEnabledModelsResult;
readModelsStore?: () => string | undefined;
modelCatalog?: RuntimeModelCatalog;
warn?: (message: string) => void;
}
@@ -82,9 +84,23 @@ export function createPiModelLister(cfg: AppConfig, opts: Opts = {}): ListModels
const byCompositeId = new Map(
available.map((model) => [`${model.provider}/${model.id}`, model]),
);
const catalogByPiId = new Map(
(opts.modelCatalog?.sessionModels() ?? []).map((model) => [
`${model.provider}/${model.upstreamModel}`,
model,
]),
);
const models = enabled.ids.flatMap((id) => {
const model = byCompositeId.get(id);
return model ? [model] : [];
if (!model) return [];
const catalogModel = catalogByPiId.get(id);
return [catalogModel ? {
...model,
provider: catalogModel.provider,
id: catalogModel.model,
name: catalogModel.label,
reasoning: catalogModel.session?.reasoning ?? model.reasoning,
} : model];
});
if (models.length === 0) opts.warn?.("No Pi-enabled models are currently available");
cache = { at: now(), models };
+4 -1
View File
@@ -260,8 +260,11 @@ export class PiProcessManager {
const thinking = o.thinking ?? this.cfg.defaults.thinking;
if (provider && model) {
const upstreamModel = this.modelCatalog.sessionModels()
.find((entry) => entry.provider === provider && entry.model === model)
?.upstreamModel ?? model;
const response = await rt.rpc.request(
{ type: "set_model", provider, modelId: model } as object & { type: string },
{ type: "set_model", provider, modelId: upstreamModel } as object & { type: string },
);
rt.bridge.setContextWindow(response?.data?.contextWindow);
}
+2 -1
View File
@@ -79,6 +79,7 @@ export function createPiProviderSmoke(
|| catalog.defaultSession !== null;
const catalogModel = catalog.sessionModels()
.find((entry) => entry.provider === canonicalProvider && entry.model === model);
const upstreamModel = catalogModel?.upstreamModel ?? model;
const credentialName = catalogModel?.authentication.mode === "secret_env"
? catalogModel.authentication.apiKeyEnv
: undefined;
@@ -130,7 +131,7 @@ export function createPiProviderSmoke(
const capabilityGuard = failOnUnexpectedCapabilities(rpc);
const turn = async (): Promise<void> => {
requireSuccessfulResponse(await rpc.request({
type: "set_model", provider: canonicalProvider, modelId: model,
type: "set_model", provider: canonicalProvider, modelId: upstreamModel,
} as object & { type: string }));
requireSuccessfulResponse(await rpc.request({
type: "set_thinking_level", level: reasoning,
+1 -1
View File
@@ -140,7 +140,7 @@ function buildEmbeddingConfig(rendered: Record<string, unknown>): CanonicalEmbed
}
const model = requireString(embeddings, "model");
return {
id: `ollama/${model}`,
id: optionalString(embeddings, "id") ?? `ollama/${model}`,
model,
dimensions: requireNumber(embeddings, "dimensions"),
};
@@ -274,6 +274,8 @@ export function renderRuntimeConfig(
},
embedding: {
provider: "ollama_internal",
id: semanticRuntime.internalEmbeddingId
?? `ollama/${semanticRuntime.internalEmbeddingModel}`,
model: semanticRuntime.internalEmbeddingModel,
dimensions: semanticRuntime.internalEmbeddingDimensions,
},
@@ -287,6 +289,8 @@ export function renderRuntimeConfig(
embeddings: {
provider: "ollama_internal",
base_url: semanticRuntime.internalEmbeddingUrl,
id: semanticRuntime.internalEmbeddingId
?? `ollama/${semanticRuntime.internalEmbeddingModel}`,
model: semanticRuntime.internalEmbeddingModel,
dimensions: semanticRuntime.internalEmbeddingDimensions,
},
@@ -1,4 +1,5 @@
import { spawnSync } from "node:child_process";
import { randomUUID } from "node:crypto";
import { PostgreSqlContainer } from "@testcontainers/postgresql";
import { CamelCasePlugin, Kysely, PostgresDialect, sql } from "kysely";
import { Pool } from "pg";
@@ -33,7 +34,30 @@ test.skipIf(!dockerAvailable)("PostgreSQL migration enforces one database per wo
await upDescriptionGeneration(db);
await upSensitiveSuggestionRuns(db);
await upAiTokenUsage(db);
const historicalDatabaseId = randomUUID();
await db.insertInto("workspaceDatabases").values({
id: historicalDatabaseId,
workspaceId: "migration-history",
engine: "postgres",
databaseName: "warehouse",
schemaName: "public",
}).execute();
await db.insertInto("descriptionGenerationRuns").values({
id: randomUUID(), databaseId: historicalDatabaseId, scope: "all",
modelId: "openai-mini", language: "en", status: "completed", total: 1,
processed: 1, generated: 1,
}).execute();
await db.insertInto("sensitiveDataSuggestionRuns").values({
id: randomUUID(), databaseId: historicalDatabaseId, scope: "all",
modelId: "openai-mini", status: "completed", total: 1,
suggestedSensitive: 1,
}).execute();
await upCanonicalModelIds(db);
await expect(db.insertInto("descriptionGenerationRuns").values({
id: randomUUID(), databaseId: historicalDatabaseId, scope: "all",
modelId: "openai/gpt-5-mini", language: "en", status: "completed", total: 1,
processed: 1, generated: 1,
}).execute()).resolves.toBeDefined();
await sql`CREATE ROLE thothii_catalog_runtime`.execute(db);
await upRuntimeSequencePrivileges(db);
const sequencePrivilege = await sql<{ allowed: boolean }>`
+31
View File
@@ -6,6 +6,7 @@ import { join } from "node:path";
import path from "node:path";
import { createPiModelLister } from "../src/pi/list-models.js";
import { loadConfig } from "../src/config.js";
import type { RuntimeModel, RuntimeModelCatalog } from "../src/models/runtime-model-catalog.js";
const FAKE = path.resolve("../harness/tests/fake_pi/fake_pi_rpc.mjs");
@@ -44,6 +45,36 @@ test("createPiModelLister returns mapped PiModel[] from get_available_models", a
}
});
test("catalog listing translates upstream Pi IDs back to canonical model keys", async () => {
const script = scriptWith([
{ provider: "local", id: "qwen2.5:7b", name: "Upstream label", reasoning: false },
]);
const model: RuntimeModel = {
id: "local/qwen", provider: "local", model: "qwen", label: "Catalog Qwen",
upstreamModel: "qwen2.5:7b", endpoint: { baseUrl: "http://ollama:11434/v1" },
authentication: { mode: "none" }, sessionAdapter: { mode: "openai_compatible" },
session: { reasoning: true, contextWindow: 32768, maxTokens: 8192 },
};
const modelCatalog: RuntimeModelCatalog = {
defaultSession: model.id, defaultMetadataGeneration: null, embedding: null,
sessionModels: () => [model], metadataModels: () => [], hasSession: (id) => id === model.id,
};
try {
const lister = createPiModelLister(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
...noManagedModels,
modelCatalog,
loadEnabledModels: enabled("local/qwen2.5:7b"),
spawnFn: () => spawn("node", [FAKE, script]) as any,
});
await expect(lister()).resolves.toEqual([{
provider: "local", id: "qwen", name: "Catalog Qwen", reasoning: true,
}]);
} finally {
rmSync(path.dirname(script), { recursive: true, force: true });
}
});
test("createPiModelLister caches within ttl (spawns once for two calls)", async () => {
const script = scriptWith([{ provider: "zai", id: "glm-5.2", name: "GLM 5.2", reasoning: true }]);
try {
+48
View File
@@ -718,6 +718,54 @@ test.each([["OpenAI", "openai"], ["gemini", "google"]])(
},
);
test("set_model translates a canonical catalog key to its upstream Pi model ID", async () => {
const root = mkdtempSync(path.join(tmpdir(), "thothii-upstream-model-"));
const agentDir = path.join(root, "agent");
mkdirSync(agentDir, { mode: 0o700 });
writeFileSync(path.join(agentDir, "models.json"), JSON.stringify({
providers: {
local: {
baseUrl: "http://ollama:11434/v1", apiKey: "local",
models: [{ id: "qwen2.5:7b" }],
},
},
}), { mode: 0o600 });
vi.stubEnv("PI_CODING_AGENT_DIR", agentDir);
const child = recordingChild();
child.stderr.resume = () => {};
child.stdin.write = (data: unknown) => {
const request = JSON.parse(String(data));
child._writes.push(String(data));
if (request.id) {
queueMicrotask(() => child.stdout.emit("data", `${JSON.stringify({
type: "response", id: request.id, success: true,
})}\n`));
}
return true;
};
const model: RuntimeModel = {
id: "local/qwen", provider: "local", model: "qwen", label: "Qwen",
upstreamModel: "qwen2.5:7b", endpoint: { baseUrl: "http://ollama:11434/v1" },
authentication: { mode: "none" }, sessionAdapter: { mode: "openai_compatible" },
session: { reasoning: false, contextWindow: 32768, maxTokens: 8192 },
};
const modelCatalog: RuntimeModelCatalog = {
defaultSession: model.id, defaultMetadataGeneration: null, embedding: null,
sessionModels: () => [model], metadataModels: () => [], hasSession: (id) => id === model.id,
};
const mgr = new PiProcessManager(loadConfig({ PI_BIN: "/usr/local/bin/pi" }), {
modelCatalog, authProviders: () => new Set(), spawnFn: () => child as any,
});
try {
await mgr.spawnFor("upstream-model", { provider: "local", model: "qwen" });
expect(child._writes.join("")).toContain('"modelId":"qwen2.5:7b"');
} finally {
mgr.teardown("upstream-model");
vi.unstubAllEnvs();
rmSync(root, { recursive: true, force: true });
}
});
test.each(["installation-local", "private-compatible"])(
"provider %s configured with a literal apiKey spawns without a managed key",
async (provider) => {
+12 -1
View File
@@ -300,11 +300,22 @@ test("provider smoke makes one configured request from an isolated no-capability
});
}
});
const smokeModel: RuntimeModel = {
id: "zai/catalog-glm", provider: "zai", model: "catalog-glm", label: "GLM",
upstreamModel: "glm-5.2", authentication: { mode: "pi_auth" },
sessionAdapter: { mode: "pi_builtin" }, session: { reasoning: true },
};
const smokeCatalog: RuntimeModelCatalog = {
defaultSession: smokeModel.id, defaultMetadataGeneration: null, embedding: null,
sessionModels: () => [smokeModel], metadataModels: () => [],
hasSession: (id) => id === smokeModel.id,
};
const smoke = createPiProviderSmoke(loadConfig({
THT_HARNESS_DIR: "/app/harness",
PI_BIN: "/usr/local/bin/pi",
THT_DATA_ROOT: "/mounted-workflow-state",
}), {
modelCatalog: smokeCatalog,
spawnFn: (...args) => {
spawns.push(args);
expect(args[2].cwd).not.toBe("/app/harness");
@@ -324,7 +335,7 @@ test("provider smoke makes one configured request from an isolated no-capability
});
await expect(smoke({
provider: "zai", model: "glm-5.2", reasoning: "medium", timeoutMs: 750,
provider: "zai", model: "catalog-glm", reasoning: "medium", timeoutMs: 750,
})).resolves.toBeUndefined();
expect(spawns).toHaveLength(1);
expect(spawns[0][0]).toBe("/usr/local/bin/pi");
@@ -73,6 +73,7 @@ test("derives the internal Qdrant and Ollama runtime shape from workspace v4 plu
vector: { engine: "qdrant", base_url: "http://qdrant:6333", collection: "psd-clinical" },
embeddings: {
provider: "ollama_internal", base_url: "http://embedding:11434",
id: "ollama/qwen3-embedding:0.6b",
model: "qwen3-embedding:0.6b", dimensions: 1024,
},
},