feat(llm-pi-ai): modelOverrides reshapes catalog models without replacing the catalog
A route's modelOverrides dict customizes individual installed-catalog models — key = catalog model id, value = the same fields a models entry takes — while the rest of the catalog keeps serving, which a models list cannot express because declaring one replaces the served set. An override becomes the catalog entry's configuration and resolves through the existing entry path, so capacities, reasoningEfforts, compat, and request-default semantics are identical to a models entry's. Unlike Pi's config layer, which ignores unknown ids, every override that lands nowhere is refused at the write that produced it: beside a models list, on a hand-declared route, naming a model the catalog does not describe, or smuggling an id through the schema's unknown-key tolerance.
This commit is contained in:
@@ -184,6 +184,15 @@ export interface PiAiModelProfile {
|
||||
compat?: PiAiCompatProfile
|
||||
}
|
||||
|
||||
/**
|
||||
* Customization of one installed catalog model, keyed by its id in the
|
||||
* route's `modelOverrides` dict — the same fields a `models` entry may set,
|
||||
* with the id living in the key. Unlike a `models` list, overrides leave the
|
||||
* rest of the catalog serving untouched, which is what makes "correct one
|
||||
* model, keep the other thirty-seven" a three-line edit.
|
||||
*/
|
||||
export type PiAiModelOverride = Omit<PiAiModelProfile, 'id'>
|
||||
|
||||
/** The route-level facts model materialization reads. */
|
||||
export interface RouteCatalogRequest {
|
||||
/** Provider route key, stamped onto every materialized model. */
|
||||
@@ -194,6 +203,8 @@ export interface RouteCatalogRequest {
|
||||
baseURL?: string
|
||||
/** Configured catalog; absent means the whole installed catalog for this route. */
|
||||
models?: readonly PiAiModelProfile[]
|
||||
/** Installed-catalog customizations by model id; only meaningful while `models` is absent. */
|
||||
modelOverrides?: Readonly<Record<string, PiAiModelOverride>>
|
||||
/** Reasoning-dispatch switches for every `openai-completions` model on the route; entries override per field. */
|
||||
compat?: PiAiCompatProfile
|
||||
/** Context capacity for a model neither the entry nor the catalog sizes. */
|
||||
@@ -381,9 +392,35 @@ export function resolveRouteModels(request: RouteCatalogRequest): RouteCatalog {
|
||||
// schema materializes `[]` for the absent case, and an empty catalog could
|
||||
// serve no request anyway, so both mean "serve the installed catalog".
|
||||
const configured = request.models ?? []
|
||||
const overrides = request.modelOverrides ?? {}
|
||||
// Every miss is refused, never skipped: an override that lands nowhere is a
|
||||
// typo someone would otherwise hunt for in a silently unchanged model.
|
||||
for (const [id, override] of Object.entries(overrides)) {
|
||||
if (id.length === 0) invalid(provider, 'has a modelOverrides entry with an empty model id')
|
||||
if (defaults.size === 0) {
|
||||
invalid(provider, `sets modelOverrides for "${id}", but the installed catalog does not describe this route;`
|
||||
+ ' a declared route spells every model out in its models list')
|
||||
}
|
||||
if (configured.length > 0) {
|
||||
invalid(provider, `sets modelOverrides for "${id}" beside a models list; models already replaces the served`
|
||||
+ ' catalog, so declare the fields on its entries')
|
||||
}
|
||||
if (!defaults.has(id)) {
|
||||
invalid(provider, `modelOverrides names "${id}", which the installed catalog does not describe`)
|
||||
}
|
||||
// The id lives in the dict key; a value carrying its own would quietly
|
||||
// rename the model it meant to customize. The static shape already omits
|
||||
// it — this guards the schema boundary, which passes unknown keys through.
|
||||
if ('id' in override) {
|
||||
invalid(provider, `modelOverrides entry "${id}" sets "id", which is the dict key`)
|
||||
}
|
||||
}
|
||||
// An override becomes the catalog entry's configuration, so everything a
|
||||
// models entry may declare — capacities, efforts, compat — resolves through
|
||||
// the same path with the same diagnostics and request-default semantics.
|
||||
const entries: readonly PiAiModelProfile[] = configured.length > 0
|
||||
? configured
|
||||
: [...defaults.values()].map(model => ({ id: model.id }))
|
||||
: [...defaults.values()].map(model => ({ id: model.id, ...overrides[model.id] }))
|
||||
if (entries.length === 0) {
|
||||
invalid(provider, 'resolves no models; the installed catalog does not describe this route, so its models'
|
||||
+ ' must be listed in configuration')
|
||||
|
||||
@@ -22,7 +22,7 @@ import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
|
||||
import { normalizeApiKey, resolveRetryPolicy, RetryPolicySchema } from '@deepseek-ai/dsh-llm'
|
||||
import type { ResolvedRetryPolicy, RetryPolicyConfig } from '@deepseek-ai/dsh-llm'
|
||||
import { resolveRouteModels, SUPPORTED_THINKING_FORMATS, THINKING_LEVELS } from './catalog.ts'
|
||||
import type { PiAiCompatProfile, PiAiModelProfile, PiAiReasoningEfforts } from './catalog.ts'
|
||||
import type { PiAiCompatProfile, PiAiModelOverride, PiAiModelProfile, PiAiReasoningEfforts } from './catalog.ts'
|
||||
import { buildProvider, supportedProtocols } from './provider.ts'
|
||||
|
||||
/** Default maximum idle interval while an adapter stream read is outstanding. */
|
||||
@@ -34,7 +34,13 @@ export const DEFAULT_CONTEXT_WINDOW = 262_144
|
||||
/** Output capability assumed for a model neither configuration nor the catalog sizes. */
|
||||
export const DEFAULT_MAX_TOKENS = 32_768
|
||||
|
||||
export type { PiAiCompatProfile, PiAiModelProfile, PiAiReasoningEfforts, PiAiThinkingFormat } from './catalog.ts'
|
||||
export type {
|
||||
PiAiCompatProfile,
|
||||
PiAiModelOverride,
|
||||
PiAiModelProfile,
|
||||
PiAiReasoningEfforts,
|
||||
PiAiThinkingFormat,
|
||||
} from './catalog.ts'
|
||||
|
||||
/** Configuration for one pi-ai provider route; the `providers` dict key IS the route. */
|
||||
export interface PiAiProviderProfile {
|
||||
@@ -62,6 +68,15 @@ export interface PiAiProviderProfile {
|
||||
* unset fields from the installed model of the same id.
|
||||
*/
|
||||
models?: PiAiModelProfile[]
|
||||
/**
|
||||
* Installed-catalog customizations by model id: each entry reshapes that
|
||||
* one model with the same fields a {@link models} entry takes, while the
|
||||
* rest of the catalog keeps serving untouched. Only meaningful on a catalog
|
||||
* route with no `models` list — `models` already replaces the catalog, so
|
||||
* an override beside it, on a route the catalog does not ship, or naming a
|
||||
* model the catalog does not describe is refused rather than skipped.
|
||||
*/
|
||||
modelOverrides?: Record<string, PiAiModelOverride>
|
||||
/**
|
||||
* Reasoning-dispatch switches for every `openai-completions` model on this
|
||||
* route; each model's own `compat` overrides per field. What neither sets
|
||||
@@ -176,6 +191,15 @@ const modelProfile: z<PiAiModelProfile> = z.object({
|
||||
compat: compatProfile,
|
||||
})
|
||||
|
||||
/** A {@link modelProfile} whose id lives in the `modelOverrides` dict key. */
|
||||
const modelOverride: z<PiAiModelOverride> = z.object({
|
||||
name: z.string(),
|
||||
contextWindow: z.number().step(1).min(1),
|
||||
maxTokens: z.number().step(1).min(1),
|
||||
reasoningEfforts: z.union([z.const(false), reasoningEfforts]),
|
||||
compat: compatProfile,
|
||||
})
|
||||
|
||||
const profile = z.object({
|
||||
apiKey: z.string().role('secret'),
|
||||
apiKeyEnv: z.string().role('credential-ref'),
|
||||
@@ -183,6 +207,7 @@ const profile = z.object({
|
||||
api: z.union(supportedProtocols()),
|
||||
baseURL: z.string(),
|
||||
models: z.array(modelProfile),
|
||||
modelOverrides: z.dict(modelOverride),
|
||||
compat: compatProfile,
|
||||
defaultContextWindow: z.number().step(1).min(1).default(DEFAULT_CONTEXT_WINDOW),
|
||||
defaultMaxTokens: z.number().step(1).min(1).default(DEFAULT_MAX_TOKENS),
|
||||
@@ -291,6 +316,7 @@ export function resolveProfiles(
|
||||
...source.api === undefined ? {} : { api: source.api },
|
||||
...source.baseURL === undefined ? {} : { baseURL: source.baseURL },
|
||||
...source.models === undefined ? {} : { models: source.models },
|
||||
...source.modelOverrides === undefined ? {} : { modelOverrides: source.modelOverrides },
|
||||
...source.compat === undefined ? {} : { compat: source.compat },
|
||||
defaultContextWindow: source.defaultContextWindow ?? DEFAULT_CONTEXT_WINDOW,
|
||||
defaultMaxTokens: source.defaultMaxTokens ?? DEFAULT_MAX_TOKENS,
|
||||
|
||||
@@ -70,6 +70,7 @@ export type { PiAiAdapterOptions } from './adapter.ts'
|
||||
export { Config } from './config.ts'
|
||||
export type {
|
||||
PiAiCompatProfile,
|
||||
PiAiModelOverride,
|
||||
PiAiModelProfile,
|
||||
PiAiProviderProfile,
|
||||
PiAiReasoningEfforts,
|
||||
|
||||
Reference in New Issue
Block a user