Merge PR #6745: perf: lazily construct models config schema (@usr-bin-roygbiv)

This commit is contained in:
can1357
2026-07-27 04:58:28 +02:00
6 changed files with 508 additions and 335 deletions
@@ -53,6 +53,10 @@ function migrateJsonToYml(jsonPath: string, ymlPath: string) {
}
}
export type ConfigSchemaSource =
| { readonly kind: "eager"; readonly schema: Type }
| { readonly kind: "deferred"; readonly resolve: () => Type };
export interface IConfigFile<T> {
readonly id: string;
readonly schema: Type;
@@ -125,14 +129,17 @@ export class ConfigFile<T> implements IConfigFile<T> {
readonly #basePath: string;
readonly #yamlFallbackPath: string | null;
readonly #jsonMigrationPath: string | null;
readonly #schemaSource: ConfigSchemaSource;
#resolvedSchema?: Type;
#cache?: LoadResult<T>;
#auxValidate?: (value: T) => void;
constructor(
readonly id: string,
readonly schema: Type,
schema: Type | ConfigSchemaSource,
configPath: string = path.join(getAgentDir(), `${id}.yml`),
) {
this.#schemaSource = typeof schema === "function" ? { kind: "eager", schema } : schema;
this.#basePath = configPath;
if (configPath.endsWith(".yml")) {
this.#yamlFallbackPath = `${configPath.slice(0, -4)}.yaml`;
@@ -150,6 +157,12 @@ export class ConfigFile<T> implements IConfigFile<T> {
}
}
get schema(): Type {
if (this.#schemaSource.kind === "eager") return this.#schemaSource.schema;
if (!this.#resolvedSchema) this.#resolvedSchema = this.#schemaSource.resolve();
return this.#resolvedSchema;
}
/**
* Run the JSON → YAML migration synchronously, if applicable. Idempotent.
* Sync callers (tests, settings init) hit this implicitly via {@link tryLoad}.
@@ -164,8 +177,9 @@ export class ConfigFile<T> implements IConfigFile<T> {
relocate(configPath?: string): ConfigFile<T> {
if (!configPath || configPath === this.#basePath) return this;
const result = new ConfigFile<T>(this.id, this.schema, configPath);
const result = new ConfigFile<T>(this.id, this.#schemaSource, configPath);
result.#auxValidate = this.#auxValidate;
result.#resolvedSchema = this.#resolvedSchema;
result.#ensureMigrated();
return result;
}
@@ -0,0 +1,313 @@
import { once } from "@oh-my-pi/pi-utils";
import { scope } from "arktype";
export const getModelsConfigSchemaBundle = once(() => {
// Config schemas validate at most a handful of times per process (on config
// load), so the eager JIT codegen ArkType runs at definition time is pure
// startup tax. A local jitless scope skips that codegen and falls back to
// interpreted traversal — ~65% cheaper to construct, validation correctness
// unchanged. (No `name`: duplicate module instances would collide.)
const { type } = scope({}, { jitless: true });
const OpenRouterRoutingSchema = type({
"only?": "string[]",
"order?": "string[]",
});
const VercelGatewayRoutingSchema = type({
"only?": "string[]",
"order?": "string[]",
});
const ReasoningEffortMapSchema = type({
"minimal?": "string",
"low?": "string",
"medium?": "string",
"high?": "string",
"xhigh?": "string",
"max?": "string",
});
const OpenAICompatFields = {
"supportsStore?": "boolean",
"supportsDeveloperRole?": "boolean",
"supportsMultipleSystemMessages?": "boolean",
"supportsReasoningEffort?": "boolean",
"reasoningEffortMap?": ReasoningEffortMapSchema,
"maxTokensField?": '"max_completion_tokens" | "max_tokens"',
"supportsUsageInStreaming?": "boolean",
"requiresToolResultName?": "boolean",
"requiresMistralToolIds?": "boolean",
"requiresAssistantAfterToolResult?": "boolean",
"requiresThinkingAsText?": "boolean",
"reasoningContentField?": '"reasoning_content" | "reasoning" | "reasoning_text"',
"requiresReasoningContentForToolCalls?": "boolean",
"allowsSyntheticReasoningContentForToolCalls?": "boolean",
"requiresAssistantContentForToolCalls?": "boolean",
"supportsToolChoice?": "boolean",
"supportsForcedToolChoice?": "boolean",
"disableReasoningOnForcedToolChoice?": "boolean",
"disableReasoningOnToolChoice?": "boolean",
"thinkingFormat?": '"openai" | "openrouter" | "zai" | "qwen" | "qwen-chat-template"',
"openRouterRouting?": OpenRouterRoutingSchema,
"vercelGatewayRouting?": VercelGatewayRoutingSchema,
"extraBody?": { "[string]": "unknown" },
"cacheControlFormat?": '"anthropic"',
"supportsStrictMode?": "boolean",
"toolStrictMode?": '"all_strict" | "none"',
"streamIdleTimeoutMs?": "number >= 0",
"supportsLongPromptCacheRetention?": "boolean",
"supportsReasoningParams?": "boolean",
"alwaysSendMaxTokens?": "boolean",
"strictResponsesPairing?": "boolean",
"supportsImageDetailOriginal?": "boolean",
// anthropic-messages compat flags (same `compat` slot, per-api interpretation)
"supportsEagerToolInputStreaming?": "boolean",
"allowAnthropicHeaderOverrides?": "boolean",
"requiresToolResultId?": "boolean",
"replayUnsignedThinking?": "boolean",
} as const;
const OpenAICompatFieldsSchema = type(OpenAICompatFields);
const OpenAICompatSchema = type({
...OpenAICompatFields,
"whenThinking?": OpenAICompatFieldsSchema,
});
const BedrockCompatSchema = type({
"promptCacheMode?": '"none" | "automatic" | "explicit"',
"supportsLongPromptCacheRetention?": "boolean",
"promptCacheMinimumTokens?": "number >= 0",
"promptCacheMaximumCheckpoints?": "number >= 0",
});
// Provider-level overrides can target bundled models whose API is not repeated
// in models.yml, so preserve the sparse compat shape for each supported API.
const ApiCompatSchema = OpenAICompatSchema.and(BedrockCompatSchema);
const ApiSchema = type(
'"openai-completions" | "openai-responses" | "openai-codex-responses" | "azure-openai-responses" | "anthropic-messages" | "bedrock-converse-stream" | "google-generative-ai" | "google-gemini-cli" | "google-vertex"',
);
const EffortSchema = type('"minimal" | "low" | "medium" | "high" | "xhigh" | "max"');
const ThinkingControlModeSchema = type(
'"effort" | "budget" | "google-level" | "anthropic-adaptive" | "anthropic-budget-effort"',
);
const EFFORT_ORDER = ["minimal", "low", "medium", "high", "xhigh", "max"] as const;
/**
* Accepts the canonical `efforts` vocabulary plus the legacy
* `minLevel`/`maxLevel`/`levels` range shape, normalizing both to
* `ThinkingConfig` (ordered `efforts`, never empty). Precedence mirrors the
* old runtime: explicit `levels` beat the min..max range; `efforts` beats both.
*/
const ModelThinkingSchema = type({
mode: ThinkingControlModeSchema,
"efforts?": EffortSchema.array(),
"defaultLevel?": EffortSchema,
"effortMap?": ReasoningEffortMapSchema,
"supportsDisplay?": "boolean",
// Legacy range vocabulary (pre-efforts configs).
"minLevel?": EffortSchema,
"maxLevel?": EffortSchema,
"levels?": EffortSchema.array(),
})
.narrow(
(value, ctx) =>
value.efforts !== undefined ||
value.levels !== undefined ||
(value.minLevel !== undefined && value.maxLevel !== undefined) ||
ctx.mustBe("thinking with `efforts` (or legacy `levels`/`minLevel`+`maxLevel`)"),
)
.pipe((value: any) => {
let resolved = value.efforts ?? value.levels;
if (!resolved) {
const minIndex = EFFORT_ORDER.indexOf(value.minLevel!);
const maxIndex = EFFORT_ORDER.indexOf(value.maxLevel!);
resolved = EFFORT_ORDER.slice(minIndex, Math.max(minIndex, maxIndex) + 1);
}
return {
mode: value.mode,
efforts: resolved,
...(value.defaultLevel !== undefined && { defaultLevel: value.defaultLevel }),
...(value.effortMap !== undefined && { effortMap: value.effortMap }),
...(value.supportsDisplay !== undefined && { supportsDisplay: value.supportsDisplay }),
};
});
const RemoteCompactionSchema = type({
"enabled?": "boolean",
"api?": ApiSchema,
"endpoint?": "string",
"model?": "string",
"v2StreamingEnabled?": "boolean",
"v2Endpoint?": "string",
"streamingEndpoint?": "string",
}).narrow((value, ctx) => {
if (value.endpoint !== undefined && typeof value.endpoint === "string" && value.endpoint.length === 0) {
return ctx.mustBe("remoteCompaction.endpoint a non-empty string");
}
if (value.model !== undefined && typeof value.model === "string" && value.model.length === 0) {
return ctx.mustBe("remoteCompaction.model a non-empty string");
}
if (value.v2Endpoint !== undefined && typeof value.v2Endpoint === "string" && value.v2Endpoint.length === 0) {
return ctx.mustBe("remoteCompaction.v2Endpoint a non-empty string");
}
if (
value.streamingEndpoint !== undefined &&
typeof value.streamingEndpoint === "string" &&
value.streamingEndpoint.length === 0
) {
return ctx.mustBe("remoteCompaction.streamingEndpoint a non-empty string");
}
return true;
});
const ModelDefinitionSchema = type({
id: "string",
"name?": "string",
"api?": ApiSchema,
"baseUrl?": "string",
"reasoning?": "boolean",
"thinking?": ModelThinkingSchema,
"input?": '("text" | "image")[]',
"supportsTools?": "boolean",
"cost?": {
input: "number",
output: "number",
cacheRead: "number",
cacheWrite: "number",
},
"premiumMultiplier?": "number",
"contextWindow?": "number",
"maxTokens?": "number",
"omitMaxOutputTokens?": "boolean",
"headers?": { "[string]": "string" },
"compat?": ApiCompatSchema,
"contextPromotionTarget?": "string",
"compactionModel?": "string",
"remoteCompaction?": RemoteCompactionSchema,
}).narrow((value, ctx) => {
// Enforce id non-empty
if (typeof value.id === "string" && value.id.length === 0) {
return ctx.mustBe("id a non-empty string");
}
if (value.name !== undefined && typeof value.name === "string" && value.name.length === 0) {
return ctx.mustBe("name a non-empty string");
}
if (value.baseUrl !== undefined && typeof value.baseUrl === "string" && value.baseUrl.length === 0) {
return ctx.mustBe("baseUrl a non-empty string");
}
if (
value.contextPromotionTarget !== undefined &&
typeof value.contextPromotionTarget === "string" &&
value.contextPromotionTarget.length === 0
) {
return ctx.mustBe("contextPromotionTarget a non-empty string");
}
if (
value.compactionModel !== undefined &&
typeof value.compactionModel === "string" &&
value.compactionModel.length === 0
) {
return ctx.mustBe("compactionModel a non-empty string");
}
return true;
});
const ModelOverrideSchema = type({
"name?": "string",
"reasoning?": "boolean",
"thinking?": ModelThinkingSchema,
"input?": '("text" | "image")[]',
"supportsTools?": "boolean",
"cost?": {
"input?": "number",
"output?": "number",
"cacheRead?": "number",
"cacheWrite?": "number",
},
"premiumMultiplier?": "number",
"contextWindow?": "number",
"maxTokens?": "number",
"omitMaxOutputTokens?": "boolean",
"headers?": { "[string]": "string" },
"compat?": ApiCompatSchema,
"contextPromotionTarget?": "string",
"compactionModel?": "string",
"remoteCompaction?": RemoteCompactionSchema,
}).narrow((value, ctx) => {
if (value.name !== undefined && typeof value.name === "string" && value.name.length === 0) {
return ctx.mustBe("name a non-empty string");
}
if (
value.contextPromotionTarget !== undefined &&
typeof value.contextPromotionTarget === "string" &&
value.contextPromotionTarget.length === 0
) {
return ctx.mustBe("contextPromotionTarget a non-empty string");
}
if (
value.compactionModel !== undefined &&
typeof value.compactionModel === "string" &&
value.compactionModel.length === 0
) {
return ctx.mustBe("compactionModel a non-empty string");
}
return true;
});
const ProviderDiscoverySchema = type({
type: '"ollama" | "llama.cpp" | "lm-studio" | "openai-models-list" | "proxy" | "litellm"',
});
const ProviderAuthSchema = type('"apiKey" | "none" | "oauth"');
const ProviderConfigSchema = type({
"baseUrl?": "string",
"apiKey?": "string",
"api?": ApiSchema,
"headers?": { "[string]": "string" },
"compat?": ApiCompatSchema,
"remoteCompaction?": RemoteCompactionSchema,
"authHeader?": "boolean",
"auth?": ProviderAuthSchema,
"discovery?": ProviderDiscoverySchema,
"models?": ModelDefinitionSchema.array(),
"modelOverrides?": { "[string]": ModelOverrideSchema },
"disableStrictTools?": "boolean",
/**
* Streaming transport override. When set to `"pi-native"`, omp dispatches
* every model under this provider via the auth-gateway's
* `POST /v1/pi/stream` endpoint instead of the per-provider SDK. The
* provider's `baseUrl` must point at a compatible `omp auth-gateway`
* and `apiKey` must carry the gateway bearer.
*/
"transport?": '"pi-native"',
}).narrow((value, ctx) => {
if (value.baseUrl !== undefined && typeof value.baseUrl === "string" && value.baseUrl.length === 0) {
return ctx.mustBe("baseUrl a non-empty string");
}
if (value.apiKey !== undefined && typeof value.apiKey === "string" && value.apiKey.length === 0) {
return ctx.mustBe("apiKey a non-empty string");
}
return true;
});
const ModelsConfigSchema = type({
"providers?": { "[string]": ProviderConfigSchema },
});
return {
OpenAICompatSchema,
ModelOverrideSchema,
ProviderDiscoverySchema,
ProviderAuthSchema,
ModelsConfigSchema,
};
});
export const getModelsConfigSchema = () => getModelsConfigSchemaBundle().ModelsConfigSchema;
@@ -1,307 +1,14 @@
import { scope } from "arktype";
import { getModelsConfigSchemaBundle } from "./models-config-schema-bundle";
// Config schemas validate at most a handful of times per process (on config
// load), so the eager JIT codegen ArkType runs at definition time is pure
// startup tax. A local jitless scope skips that codegen and falls back to
// interpreted traversal — ~65% cheaper to construct, validation correctness
// unchanged. (No `name`: duplicate module instances would collide.)
const { type } = scope({}, { jitless: true });
const OpenRouterRoutingSchema = type({
"only?": "string[]",
"order?": "string[]",
});
const VercelGatewayRoutingSchema = type({
"only?": "string[]",
"order?": "string[]",
});
const ReasoningEffortMapSchema = type({
"minimal?": "string",
"low?": "string",
"medium?": "string",
"high?": "string",
"xhigh?": "string",
"max?": "string",
});
const OpenAICompatFields = {
"supportsStore?": "boolean",
"supportsDeveloperRole?": "boolean",
"supportsMultipleSystemMessages?": "boolean",
"supportsReasoningEffort?": "boolean",
"reasoningEffortMap?": ReasoningEffortMapSchema,
"maxTokensField?": '"max_completion_tokens" | "max_tokens"',
"supportsUsageInStreaming?": "boolean",
"requiresToolResultName?": "boolean",
"requiresMistralToolIds?": "boolean",
"requiresAssistantAfterToolResult?": "boolean",
"requiresThinkingAsText?": "boolean",
"reasoningContentField?": '"reasoning_content" | "reasoning" | "reasoning_text"',
"requiresReasoningContentForToolCalls?": "boolean",
"allowsSyntheticReasoningContentForToolCalls?": "boolean",
"requiresAssistantContentForToolCalls?": "boolean",
"supportsToolChoice?": "boolean",
"supportsForcedToolChoice?": "boolean",
"disableReasoningOnForcedToolChoice?": "boolean",
"disableReasoningOnToolChoice?": "boolean",
"thinkingFormat?": '"openai" | "openrouter" | "zai" | "qwen" | "qwen-chat-template"',
"openRouterRouting?": OpenRouterRoutingSchema,
"vercelGatewayRouting?": VercelGatewayRoutingSchema,
"extraBody?": { "[string]": "unknown" },
"cacheControlFormat?": '"anthropic"',
"supportsStrictMode?": "boolean",
"toolStrictMode?": '"all_strict" | "none"',
"streamIdleTimeoutMs?": "number >= 0",
"supportsLongPromptCacheRetention?": "boolean",
"supportsReasoningParams?": "boolean",
"alwaysSendMaxTokens?": "boolean",
"strictResponsesPairing?": "boolean",
"supportsImageDetailOriginal?": "boolean",
// anthropic-messages compat flags (same `compat` slot, per-api interpretation)
"supportsEagerToolInputStreaming?": "boolean",
"allowAnthropicHeaderOverrides?": "boolean",
"requiresToolResultId?": "boolean",
"replayUnsignedThinking?": "boolean",
} as const;
const OpenAICompatFieldsSchema = type(OpenAICompatFields);
export const OpenAICompatSchema = type({
...OpenAICompatFields,
"whenThinking?": OpenAICompatFieldsSchema,
});
const BedrockCompatSchema = type({
"promptCacheMode?": '"none" | "automatic" | "explicit"',
"supportsLongPromptCacheRetention?": "boolean",
"promptCacheMinimumTokens?": "number >= 0",
"promptCacheMaximumCheckpoints?": "number >= 0",
});
// Provider-level overrides can target bundled models whose API is not repeated
// in models.yml, so preserve the sparse compat shape for each supported API.
const ApiCompatSchema = OpenAICompatSchema.and(BedrockCompatSchema);
const ApiSchema = type(
'"openai-completions" | "openai-responses" | "openai-codex-responses" | "azure-openai-responses" | "anthropic-messages" | "bedrock-converse-stream" | "google-generative-ai" | "google-gemini-cli" | "google-vertex"',
);
const EffortSchema = type('"minimal" | "low" | "medium" | "high" | "xhigh" | "max"');
const ThinkingControlModeSchema = type(
'"effort" | "budget" | "google-level" | "anthropic-adaptive" | "anthropic-budget-effort"',
);
const EFFORT_ORDER = ["minimal", "low", "medium", "high", "xhigh", "max"] as const;
/**
* Accepts the canonical `efforts` vocabulary plus the legacy
* `minLevel`/`maxLevel`/`levels` range shape, normalizing both to
* `ThinkingConfig` (ordered `efforts`, never empty). Precedence mirrors the
* old runtime: explicit `levels` beat the min..max range; `efforts` beats both.
*/
const ModelThinkingSchema = type({
mode: ThinkingControlModeSchema,
"efforts?": EffortSchema.array(),
"defaultLevel?": EffortSchema,
"effortMap?": ReasoningEffortMapSchema,
"supportsDisplay?": "boolean",
// Legacy range vocabulary (pre-efforts configs).
"minLevel?": EffortSchema,
"maxLevel?": EffortSchema,
"levels?": EffortSchema.array(),
})
.narrow(
(value, ctx) =>
value.efforts !== undefined ||
value.levels !== undefined ||
(value.minLevel !== undefined && value.maxLevel !== undefined) ||
ctx.mustBe("thinking with `efforts` (or legacy `levels`/`minLevel`+`maxLevel`)"),
)
.pipe((value: any) => {
let resolved = value.efforts ?? value.levels;
if (!resolved) {
const minIndex = EFFORT_ORDER.indexOf(value.minLevel!);
const maxIndex = EFFORT_ORDER.indexOf(value.maxLevel!);
resolved = EFFORT_ORDER.slice(minIndex, Math.max(minIndex, maxIndex) + 1);
}
return {
mode: value.mode,
efforts: resolved,
...(value.defaultLevel !== undefined && { defaultLevel: value.defaultLevel }),
...(value.effortMap !== undefined && { effortMap: value.effortMap }),
...(value.supportsDisplay !== undefined && { supportsDisplay: value.supportsDisplay }),
};
});
const RemoteCompactionSchema = type({
"enabled?": "boolean",
"api?": ApiSchema,
"endpoint?": "string",
"model?": "string",
"v2StreamingEnabled?": "boolean",
"v2Endpoint?": "string",
"streamingEndpoint?": "string",
}).narrow((value, ctx) => {
if (value.endpoint !== undefined && typeof value.endpoint === "string" && value.endpoint.length === 0) {
return ctx.mustBe("remoteCompaction.endpoint a non-empty string");
}
if (value.model !== undefined && typeof value.model === "string" && value.model.length === 0) {
return ctx.mustBe("remoteCompaction.model a non-empty string");
}
if (value.v2Endpoint !== undefined && typeof value.v2Endpoint === "string" && value.v2Endpoint.length === 0) {
return ctx.mustBe("remoteCompaction.v2Endpoint a non-empty string");
}
if (
value.streamingEndpoint !== undefined &&
typeof value.streamingEndpoint === "string" &&
value.streamingEndpoint.length === 0
) {
return ctx.mustBe("remoteCompaction.streamingEndpoint a non-empty string");
}
return true;
});
const ModelDefinitionSchema = type({
id: "string",
"name?": "string",
"api?": ApiSchema,
"baseUrl?": "string",
"reasoning?": "boolean",
"thinking?": ModelThinkingSchema,
"input?": '("text" | "image")[]',
"supportsTools?": "boolean",
"cost?": {
input: "number",
output: "number",
cacheRead: "number",
cacheWrite: "number",
},
"premiumMultiplier?": "number",
"contextWindow?": "number",
"maxTokens?": "number",
"omitMaxOutputTokens?": "boolean",
"headers?": { "[string]": "string" },
"compat?": ApiCompatSchema,
"contextPromotionTarget?": "string",
"compactionModel?": "string",
"remoteCompaction?": RemoteCompactionSchema,
}).narrow((value, ctx) => {
// Enforce id non-empty
if (typeof value.id === "string" && value.id.length === 0) {
return ctx.mustBe("id a non-empty string");
}
if (value.name !== undefined && typeof value.name === "string" && value.name.length === 0) {
return ctx.mustBe("name a non-empty string");
}
if (value.baseUrl !== undefined && typeof value.baseUrl === "string" && value.baseUrl.length === 0) {
return ctx.mustBe("baseUrl a non-empty string");
}
if (
value.contextPromotionTarget !== undefined &&
typeof value.contextPromotionTarget === "string" &&
value.contextPromotionTarget.length === 0
) {
return ctx.mustBe("contextPromotionTarget a non-empty string");
}
if (
value.compactionModel !== undefined &&
typeof value.compactionModel === "string" &&
value.compactionModel.length === 0
) {
return ctx.mustBe("compactionModel a non-empty string");
}
return true;
});
export const ModelOverrideSchema = type({
"name?": "string",
"reasoning?": "boolean",
"thinking?": ModelThinkingSchema,
"input?": '("text" | "image")[]',
"supportsTools?": "boolean",
"cost?": {
"input?": "number",
"output?": "number",
"cacheRead?": "number",
"cacheWrite?": "number",
},
"premiumMultiplier?": "number",
"contextWindow?": "number",
"maxTokens?": "number",
"omitMaxOutputTokens?": "boolean",
"headers?": { "[string]": "string" },
"compat?": ApiCompatSchema,
"contextPromotionTarget?": "string",
"compactionModel?": "string",
"remoteCompaction?": RemoteCompactionSchema,
}).narrow((value, ctx) => {
if (value.name !== undefined && typeof value.name === "string" && value.name.length === 0) {
return ctx.mustBe("name a non-empty string");
}
if (
value.contextPromotionTarget !== undefined &&
typeof value.contextPromotionTarget === "string" &&
value.contextPromotionTarget.length === 0
) {
return ctx.mustBe("contextPromotionTarget a non-empty string");
}
if (
value.compactionModel !== undefined &&
typeof value.compactionModel === "string" &&
value.compactionModel.length === 0
) {
return ctx.mustBe("compactionModel a non-empty string");
}
return true;
});
export const {
OpenAICompatSchema,
ModelOverrideSchema,
ProviderDiscoverySchema,
ProviderAuthSchema,
ModelsConfigSchema,
} = getModelsConfigSchemaBundle();
export type ModelOverride = typeof ModelOverrideSchema.infer;
export const ProviderDiscoverySchema = type({
type: '"ollama" | "llama.cpp" | "lm-studio" | "openai-models-list" | "proxy" | "litellm"',
});
export const ProviderAuthSchema = type('"apiKey" | "none" | "oauth"');
export type ProviderAuthMode = typeof ProviderAuthSchema.infer;
export type ProviderDiscovery = typeof ProviderDiscoverySchema.infer;
const ProviderConfigSchema = type({
"baseUrl?": "string",
"apiKey?": "string",
"api?": ApiSchema,
"headers?": { "[string]": "string" },
"compat?": ApiCompatSchema,
"remoteCompaction?": RemoteCompactionSchema,
"authHeader?": "boolean",
"auth?": ProviderAuthSchema,
"discovery?": ProviderDiscoverySchema,
"models?": ModelDefinitionSchema.array(),
"modelOverrides?": { "[string]": ModelOverrideSchema },
"disableStrictTools?": "boolean",
/**
* Streaming transport override. When set to `"pi-native"`, omp dispatches
* every model under this provider via the auth-gateway's
* `POST /v1/pi/stream` endpoint instead of the per-provider SDK. The
* provider's `baseUrl` must point at a compatible `omp auth-gateway`
* and `apiKey` must carry the gateway bearer.
*/
"transport?": '"pi-native"',
}).narrow((value, ctx) => {
if (value.baseUrl !== undefined && typeof value.baseUrl === "string" && value.baseUrl.length === 0) {
return ctx.mustBe("baseUrl a non-empty string");
}
if (value.apiKey !== undefined && typeof value.apiKey === "string" && value.apiKey.length === 0) {
return ctx.mustBe("apiKey a non-empty string");
}
return true;
});
export const ModelsConfigSchema = type({
"providers?": { "[string]": ProviderConfigSchema },
});
export type ModelsConfig = typeof ModelsConfigSchema.infer;
@@ -4,12 +4,8 @@
import type { Api, ModelSpec } from "@oh-my-pi/pi-ai/types";
import { ConfigFile } from "./config-file";
import {
type ModelsConfig,
ModelsConfigSchema,
type ProviderAuthMode,
type ProviderDiscovery,
} from "./models-config-schema";
import type { ModelsConfig, ProviderAuthMode, ProviderDiscovery } from "./models-config-schema";
import { getModelsConfigSchema } from "./models-config-schema-bundle";
export type ProviderValidationMode = "models-config" | "runtime-register";
@@ -106,29 +102,29 @@ export function validateProviderConfiguration(
}
}
export const ModelsConfigFile = new ConfigFile<ModelsConfig>("models", ModelsConfigSchema).withValidation(
"models",
config => {
const providers = config.providers ?? {};
for (const providerName in providers) {
const providerConfig = providers[providerName];
validateProviderConfiguration(
providerName,
{
baseUrl: providerConfig.baseUrl,
headers: providerConfig.headers,
apiKey: providerConfig.apiKey,
api: providerConfig.api as Api | undefined,
auth: (providerConfig.auth ?? "apiKey") as ProviderAuthMode,
discovery: providerConfig.discovery as ProviderDiscovery | undefined,
compat: providerConfig.compat,
remoteCompaction: providerConfig.remoteCompaction,
disableStrictTools: providerConfig.disableStrictTools,
modelOverrides: providerConfig.modelOverrides,
models: (providerConfig.models ?? []) as ProviderValidationModel[],
},
"models-config",
);
}
},
);
export const ModelsConfigFile = new ConfigFile<ModelsConfig>("models", {
kind: "deferred",
resolve: getModelsConfigSchema,
}).withValidation("models", config => {
const providers = config.providers ?? {};
for (const providerName in providers) {
const providerConfig = providers[providerName];
validateProviderConfiguration(
providerName,
{
baseUrl: providerConfig.baseUrl,
headers: providerConfig.headers,
apiKey: providerConfig.apiKey,
api: providerConfig.api as Api | undefined,
auth: (providerConfig.auth ?? "apiKey") as ProviderAuthMode,
discovery: providerConfig.discovery as ProviderDiscovery | undefined,
compat: providerConfig.compat,
remoteCompaction: providerConfig.remoteCompaction,
disableStrictTools: providerConfig.disableStrictTools,
modelOverrides: providerConfig.modelOverrides,
models: (providerConfig.models ?? []) as ProviderValidationModel[],
},
"models-config",
);
}
});
@@ -0,0 +1,78 @@
import * as path from "node:path";
import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
import { ModelsConfigFile } from "@oh-my-pi/pi-coding-agent/config/models-config";
import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
import { YAML } from "bun";
interface HeapSnapshot {
nodes: number[];
snapshot: { meta: { node_fields: string[] } };
}
const root = process.argv[2];
const mode = process.argv[3];
if (!root || (mode !== "missing" && mode !== "custom")) {
throw new Error("Expected an isolated config root and missing|custom mode");
}
const configPath = path.join(root, mode, "models.yml");
if (mode === "custom") {
await Bun.write(
configPath,
YAML.stringify(
{
providers: {
"lazy-models": {
baseUrl: "https://lazy.example/v1",
api: "openai-responses",
auth: "none",
models: [
{
id: "lazy-model",
reasoning: true,
thinking: {
mode: "effort",
minLevel: "low",
maxLevel: "high",
defaultLevel: "medium",
},
},
],
},
},
},
null,
2,
),
);
}
const authStorage = await AuthStorage.create(":memory:");
try {
const registry = new ModelRegistry(authStorage, configPath);
const model =
mode === "custom" ? registry.find("lazy-models", "lazy-model") : registry.find("anthropic", "claude-sonnet-4-5");
const firstSchema = mode === "custom" ? ModelsConfigFile.relocate(configPath).schema : undefined;
const secondSchema =
mode === "custom" ? ModelsConfigFile.relocate(path.join(root, "second", "models.yml")).schema : undefined;
Bun.gc(true);
const snapshot = JSON.parse(Bun.generateHeapSnapshot("v8")) as HeapSnapshot;
const nodeWidth = snapshot.snapshot.meta.node_fields.length;
process.stdout.write(
JSON.stringify({
retainedHeapNodes: snapshot.nodes.length / nodeWidth,
schemaIdentityStable: mode === "custom" ? firstSchema === secondSchema : undefined,
model: model && {
provider: model.provider,
id: model.id,
baseUrl: model.baseUrl,
api: model.api,
thinking: model.thinking,
},
}),
);
} finally {
authStorage.close();
}
@@ -0,0 +1,65 @@
import { expect, test } from "bun:test";
import * as path from "node:path";
import { TempDir } from "@oh-my-pi/pi-utils";
interface ProbeResult {
retainedHeapNodes: number;
schemaIdentityStable?: boolean;
model?: {
provider: string;
id: string;
baseUrl: string;
api: string;
thinking?: unknown;
};
}
const probePath = path.join(import.meta.dir, "fixtures", "models-config-validator-construction-probe.ts");
async function runProbe(root: string, mode: "missing" | "custom"): Promise<ProbeResult> {
const proc = Bun.spawn([process.execPath, probePath, root, mode], {
cwd: path.join(import.meta.dir, "../../.."),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode, stderr).toBe(0);
return JSON.parse(stdout) as ProbeResult;
}
test("models config validation resources are retained only for a custom config", async () => {
const tempDir = TempDir.createSync("@models-config-validator-");
try {
const missing = await runProbe(tempDir.path(), "missing");
const custom = await runProbe(tempDir.path(), "custom");
expect(missing.model).toMatchObject({
provider: "anthropic",
id: "claude-sonnet-4-5",
baseUrl: "https://api.anthropic.com",
api: "anthropic-messages",
});
expect(custom.model).toEqual({
provider: "lazy-models",
id: "lazy-model",
baseUrl: "https://lazy.example/v1",
api: "openai-responses",
thinking: {
mode: "effort",
efforts: ["low", "medium", "high"],
defaultLevel: "medium",
},
});
expect(custom.schemaIdentityStable).toBe(true);
expect(
custom.retainedHeapNodes - missing.retainedHeapNodes,
"custom config validation should retain its schema bundle",
).toBeGreaterThan(15_000);
} finally {
await tempDir.remove().catch(() => {});
}
});