Merge remote-tracking branch 'origin/master' into worktree/web-multimodal-image-input

# Conflicts:
#	.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.i18n.yaml
#	docs/core-data-structures/core.i18n.yaml
#	docs/module-graph.md
#	packages/client/ui-conversation/src/client/chat/AssistantMarkdown.tsx
#	packages/client/ui-conversation/src/client/index.ts
#	packages/compact/compact-basic/README.i18n.yaml
This commit is contained in:
Yichen Jiang
2026-08-08 17:56:53 +08:00
841 changed files with 13085 additions and 5978 deletions

View File

@@ -14,7 +14,15 @@
import { builtinProviders, getBuiltinModels, getBuiltinProviders } from '@earendil-works/pi-ai/providers/all'
import type { BuiltinProvider } from '@earendil-works/pi-ai/providers/all'
import type { Api, Model, ModelCost, Provider } from '@earendil-works/pi-ai'
import type {
Api,
Model,
ModelCost,
ModelThinkingLevel,
OpenAICompletionsCompat,
Provider,
ThinkingLevelMap,
} from '@earendil-works/pi-ai'
/**
* Pricing for a model the installed catalog does not describe. The harness
@@ -30,6 +38,58 @@ const NO_COST: ModelCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
*/
const TEXT_ONLY: Model<Api>['input'] = ['text']
/**
* Every pi-ai thinking level, in pi-ai's canonical escalation order. The
* `Record` key type is a drift gate: a pi-ai upgrade that adds or removes a
* level fails compilation here naming the drifted key, instead of silently
* narrowing what a profile may declare.
*/
const THINKING_LEVEL_GATE: Record<ModelThinkingLevel, true> = {
off: true,
minimal: true,
low: true,
medium: true,
high: true,
xhigh: true,
max: true,
}
/** Every pi-ai thinking level a profile may declare, in escalation order. */
export const THINKING_LEVELS = Object.keys(THINKING_LEVEL_GATE) as readonly ModelThinkingLevel[]
/** The `compat.thinkingFormat` spellings pi-ai accepts on an `openai-completions` model. */
type PiThinkingFormat = NonNullable<OpenAICompletionsCompat['thinkingFormat']>
/**
* pi-ai thinking formats a profile cannot name: both drive the request through
* `chatTemplateKwargs`, which this configuration does not expose, so offering
* them would hand back a format with nothing to say.
*/
type WithheldThinkingFormat = 'chat-template' | 'qwen-chat-template'
/** One reasoning-dispatch wire format a profile may name. */
export type PiAiThinkingFormat = Exclude<PiThinkingFormat, WithheldThinkingFormat>
/**
* The nameable reasoning-dispatch formats, most-reached first. The `Record`
* key type is a drift gate: a pi-ai upgrade that adds a format (0.84 added
* `baseten`) fails compilation here until the format is classified as offered
* here or withheld above, so the offer never silently lags the upstream set.
*/
const THINKING_FORMAT_GATE: Record<PiAiThinkingFormat, true> = {
'openai': true,
'deepseek': true,
'openrouter': true,
'together': true,
'zai': true,
'qwen': true,
'string-thinking': true,
'ant-ling': true,
}
/** Reasoning-dispatch wire formats a profile may name, most-reached first. */
export const SUPPORTED_THINKING_FORMATS = Object.keys(THINKING_FORMAT_GATE) as readonly PiAiThinkingFormat[]
let providerIndex: Map<string, Provider> | undefined
/**
@@ -71,6 +131,32 @@ export function catalogModels(provider: string): Map<string, Model<Api>> {
return new Map(models.map(model => [model.id, model]))
}
/**
* Selectable reasoning efforts for one model: each key is a level the model
* offers (and selectors show), and its value is the wire spelling dispatch
* sends for it. `off` alone may leave its value empty — "supported, send
* nothing" — because for most providers not thinking is the parameter's
* absence; every other declared level must name a wire value. A level absent
* from the dict is not offered.
*/
export type PiAiReasoningEfforts = Partial<Record<ModelThinkingLevel, string | null>>
/**
* Reasoning-dispatch compatibility switches, set on the route (its models'
* default) or per model (winning over the route). Only the switches pi-ai's
* reasoning dispatch reads are offered; the rest of pi-ai's compat surface
* keeps its baseURL-derived auto-detection. pi-ai types both fields only on
* `OpenAICompletionsCompat` — the other wire protocols carry their reasoning
* shape in the protocol itself — so resolution rejects a model-level switch
* anywhere else, while a route-level default skips past models it cannot fit.
*/
export interface PiAiCompatProfile {
/** Reasoning parameter shape the endpoint expects; absent keeps the catalog entry's, then pi-ai's baseURL-derived guess. */
thinkingFormat?: PiAiThinkingFormat
/** Whether the endpoint accepts `reasoning_effort`; absent keeps the catalog entry's, then pi-ai's baseURL-derived guess. */
supportsReasoningEffort?: boolean
}
/** One configured model entry: an id plus the catalog fields it overrides. */
export interface PiAiModelProfile {
/** Model id sent to the provider and accepted by {@link GenerateOptions.model}. */
@@ -86,8 +172,27 @@ export interface PiAiModelProfile {
* default on its own.
*/
maxTokens?: number
/**
* Selectable reasoning efforts. Absent inherits the installed catalog
* entry's capability (a hand-declared model has none and does not reason);
* `false` declares a non-reasoning model, which is how a profile strips
* reasoning from a catalog model its gateway cannot serve; a non-empty dict
* declares the offered levels and their wire spellings.
*/
reasoningEfforts?: false | PiAiReasoningEfforts
/** Reasoning-dispatch switches for this model, winning over the route's. */
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. */
@@ -98,6 +203,10 @@ 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. */
defaultContextWindow: number
/** Output capability for a model neither the entry nor the catalog sizes. */
@@ -123,6 +232,137 @@ function sharedCatalogApi(defaults: ReadonlyMap<string, Model<Api>>): string | u
return apis.size === 1 ? [...apis][0] : undefined
}
/** The reasoning fields one materialized model carries. */
interface ModelReasoning {
/** Whether the model reasons at all; `false` makes pi-ai ignore the map. */
reasoning: boolean
/** The map dispatch reads; absent only when the installed entry's (or none) applies. */
thinkingLevelMap?: ThinkingLevelMap
}
/**
* Resolve one model's reasoning capability from its declared efforts.
*
* A declared dict translates to pi-ai's `thinkingLevelMap` with every level
* decided explicitly: declared levels carry their wire spelling, undeclared
* levels are pinned to `null` (unsupported). Pinning matters because pi-ai's
* own defaulting is asymmetric — an absent key means "supported" for the five
* base levels but "unsupported" for `xhigh`/`max` — and a profile author
* should not need to know that. A declared `off` with no value is the one
* exception: it stays absent from the map, which pi-ai reads as "supported,
* send nothing" — the correct dispatch where not thinking is the parameter's
* absence — while `off` with a value sends that value.
* @param provider - provider route key, for diagnostics.
* @param entry - the configured model entry.
* @param base - the installed catalog entry of the same id, when one exists.
* @returns the reasoning fields the materialized model carries.
*/
function resolveModelReasoning(
provider: string,
entry: PiAiModelProfile,
base: Model<Api> | undefined,
): ModelReasoning {
const efforts = entry.reasoningEfforts
if (efforts === undefined) {
// Reasoning rides the installed entry or is absent: a bare capability flag
// would make pi-ai advertise effort levels with no `thinkingLevelMap` to
// spell them, and no listing endpoint reports a model's reasoning
// protocol. The entry's map (when any) arrives through the `...base`
// spread in the model literal.
return { reasoning: base?.reasoning ?? false }
}
// The installed entry's map may ride along through `...base`; pi-ai never
// reads it on a non-reasoning model, so stripping it is not worth a field
// enumeration here.
if (efforts === false) return { reasoning: false }
// A YAML `reasoningEfforts:` left valueless arrives as null through the
// schema union — outside the field's declared type, hence the widening —
// while an explicit `{}` arrives as an empty dict. Both declare nothing,
// and neither is a spelling of "inherit" or "disable".
if ((efforts as unknown) === null || Object.keys(efforts).length === 0) {
invalid(provider, `model "${entry.id}" has an empty reasoningEfforts; declare the offered levels, set`
+ ' false for a non-reasoning model, or omit the field to keep the installed catalog\'s capability')
}
const declared = THINKING_LEVELS.flatMap((level) => {
const wire = efforts[level]
return wire === undefined ? [] : [[level, wire] as const]
})
for (const [level, wire] of declared) {
if (wire === null) {
if (level !== 'off') {
invalid(provider, `model "${entry.id}" reasoningEfforts.${level} needs the wire value dispatch`
+ ' should send; only "off" may leave it empty')
}
} else if (wire.length === 0) {
invalid(provider, `model "${entry.id}" reasoningEfforts.${level} must not be an empty string`)
}
}
if (!declared.some(([level]) => level !== 'off')) {
invalid(provider, `model "${entry.id}" reasoningEfforts offers no level beyond "off"; declare a thinking`
+ ' level, or set reasoningEfforts to false for a non-reasoning model')
}
const map: ThinkingLevelMap = {}
for (const level of THINKING_LEVELS) {
const wire = efforts[level]
if (wire === undefined) {
map[level] = null
} else if (wire !== null) {
map[level] = wire
}
}
return { reasoning: true, thinkingLevelMap: map }
}
/**
* Resolve one model's compat block from the profile's reasoning switches.
*
* A model switch wins over the route switch; whatever neither sets keeps the
* installed entry's value, and a field no layer decides falls through to
* pi-ai's baseURL-derived detection. Only an `openai-completions` model takes
* the switches at all: a model-level switch on any other protocol fails
* resolution, while a route-level default skips past such models — the same
* posture as the route-level `reasoning` default, which also must not fail
* models it does not fit.
* @param provider - provider route key, for diagnostics.
* @param entry - the configured model entry.
* @param route - the route-level switches, when any.
* @param base - the installed catalog entry of the same id, when one exists.
* @param api - the model's resolved wire protocol.
* @returns a `compat` field to spread into the model, or nothing.
*/
function resolveModelCompat(
provider: string,
entry: PiAiModelProfile,
route: PiAiCompatProfile | undefined,
base: Model<Api> | undefined,
api: string,
): { compat: OpenAICompletionsCompat } | Record<string, never> {
const thinkingFormat = entry.compat?.thinkingFormat ?? route?.thinkingFormat
const supportsReasoningEffort = entry.compat?.supportsReasoningEffort ?? route?.supportsReasoningEffort
if (thinkingFormat === undefined && supportsReasoningEffort === undefined) return {}
if (api !== 'openai-completions') {
if (entry.compat?.thinkingFormat !== undefined || entry.compat?.supportsReasoningEffort !== undefined) {
invalid(provider, `model "${entry.id}" sets compat reasoning switches, but its api is "${api}";`
+ ' thinkingFormat and supportsReasoningEffort exist only on openai-completions')
}
return {}
}
// The installed entry's compat matches the entry's OWN api — a route-level
// `api` repoint (an anthropic catalog served through an OpenAI-compatible
// gateway) leaves `base.compat` in the other protocol's shape, so it is
// inherited only while the resolved api still is the entry's. A repointed
// model starts from pi-ai's baseURL-derived detection instead, which is
// what a protocol change means for every other compat field too.
const inherited: OpenAICompletionsCompat | undefined = base?.api === api ? base.compat : undefined
return {
compat: {
...inherited,
...thinkingFormat === undefined ? {} : { thinkingFormat },
...supportsReasoningEffort === undefined ? {} : { supportsReasoningEffort },
},
}
}
/** One route's materialized catalog, plus the request caps its profile chose. */
export interface RouteCatalog {
/** The materialized models in configuration order. */
@@ -156,14 +396,42 @@ 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')
}
const routeApi = sharedCatalogApi(defaults)
const routeCompatDefined = request.compat?.thinkingFormat !== undefined
|| request.compat?.supportsReasoningEffort !== undefined
const seen = new Set<string>()
const configuredMaxTokens = new Map<string, number>()
const models = entries.map((entry) => {
@@ -209,15 +477,17 @@ export function resolveRouteModels(request: RouteCatalogRequest): RouteCatalog {
api,
provider,
baseUrl,
// Reasoning rides the installed entry or is absent: a bare boolean would
// make pi-ai advertise effort levels with no `thinkingLevelMap` to spell
// them, and no listing endpoint reports a model's reasoning protocol.
reasoning: base?.reasoning ?? false,
input: base?.input ?? TEXT_ONLY,
cost: base?.cost ?? NO_COST,
contextWindow,
maxTokens,
...resolveModelReasoning(provider, entry, base),
...resolveModelCompat(provider, entry, request.compat, base, api),
}
})
if (routeCompatDefined && !models.some(model => model.api === 'openai-completions')) {
invalid(provider, 'sets compat reasoning switches, but no model on the route speaks openai-completions;'
+ ' thinkingFormat and supportsReasoningEffort exist only on that protocol')
}
return { models, configuredMaxTokens }
}

View File

@@ -21,8 +21,8 @@ import type { CredentialRef } from '@deepseek-ai/dsh-credentials'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import { resolveRetryPolicy, RetryPolicySchema } from '@deepseek-ai/dsh-llm'
import type { ResolvedRetryPolicy, RetryPolicyConfig } from '@deepseek-ai/dsh-llm'
import { resolveRouteModels } from './catalog.ts'
import type { PiAiModelProfile } from './catalog.ts'
import { resolveRouteModels, SUPPORTED_THINKING_FORMATS, THINKING_LEVELS } 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 { PiAiModelProfile } 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 {
@@ -56,6 +62,22 @@ 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
* keeps the installed catalog entry's value, then pi-ai's baseURL-derived
* detection.
*/
compat?: PiAiCompatProfile
/**
* Context capacity for a model this route lists that neither the entry nor
* the installed catalog sizes (default 262,144). A guess by construction, so
@@ -133,23 +155,58 @@ const thinkingBudgets = z.object({
high: z.number(),
})
const modelProfile: z<PiAiModelProfile> = z.object({
id: z.string().required(),
const compatProfile: z<PiAiCompatProfile> = z.object({
thinkingFormat: z.union(SUPPORTED_THINKING_FORMATS),
supportsReasoningEffort: z.boolean(),
})
/**
* Keys are the offered levels, values their wire spellings. A valueless key
* (`off:`) survives validation because schemastery passes nullable data
* through before any member schema runs — `z.const(null)` only shapes the
* error for non-null wrong values and what a configuration surface renders.
* Only resolution decides which levels may leave the value empty, so the
* diagnostic can name the route and model. The assertion narrows
* schemastery's `Dict`, which types every literal key as required; dict
* validation is per-present-key, so the runtime shape is the partial record.
*/
const reasoningEfforts = z.dict(
z.union([z.string(), z.const(null)]),
z.union(THINKING_LEVELS),
) as unknown as z<PiAiReasoningEfforts>
/** The fields a `models` entry and a `modelOverrides` value share; only the id's home differs. */
const modelFields = {
name: z.string(),
contextWindow: z.number().step(1).min(1),
maxTokens: z.number().step(1).min(1),
// The union, not a bare dict: schemastery materializes an absent dict as
// `{}`, and absent must stay distinguishable — it means "inherit the
// installed catalog's capability", while `false` disables reasoning.
reasoningEfforts: z.union([z.const(false), reasoningEfforts]),
compat: compatProfile,
}
const modelProfile: z<PiAiModelProfile> = z.object({
id: z.string().required(),
...modelFields,
})
/** A {@link modelProfile} whose id lives in the `modelOverrides` dict key. */
const modelOverride: z<PiAiModelOverride> = z.object(modelFields)
const profile = z.object({
apiKeyEnv: z.string().role('credential-ref'),
displayName: z.string(),
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),
headers: z.dict(z.string()),
reasoning: z.union(['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']),
reasoning: z.union(THINKING_LEVELS),
thinkingBudgets,
cacheRetention: z.union(['none', 'short', 'long']),
transport: z.union(['sse', 'websocket', 'websocket-cached', 'auto']),
@@ -240,6 +297,8 @@ 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,
})

View File

@@ -32,11 +32,24 @@
* apiKeyEnv: ACME_GATEWAY_API_KEY
* api: openai-completions
* baseURL: https://gateway.acme.example/v1
* # Reasoning dialect for a URL pi-ai cannot recognize.
* compat:
* thinkingFormat: deepseek
* models:
* - id: acme-large
* name: Acme Large
* contextWindow: 65536
* maxTokens: 4096
* - id: acme-think
* name: Acme Think
* contextWindow: 262144
* maxTokens: 32768
* # key = selectable level, value = wire spelling; only off may
* # leave the value empty (supported, send nothing).
* reasoningEfforts:
* off:
* high: high
* max: ultra
* ```
*
* @module @deepseek-ai/dsh-llm-pi-ai
@@ -56,7 +69,15 @@ import { discoverModels } from './discovery.ts'
export { PiAiAdapter } from './adapter.ts'
export type { PiAiAdapterOptions } from './adapter.ts'
export { Config } from './config.ts'
export type { PiAiModelProfile, PiAiProviderProfile, ResolvedPiAiProviderProfile } from './config.ts'
export type {
PiAiCompatProfile,
PiAiModelOverride,
PiAiModelProfile,
PiAiProviderProfile,
PiAiReasoningEfforts,
PiAiThinkingFormat,
ResolvedPiAiProviderProfile,
} from './config.ts'
export { supportedProtocols } from './provider.ts'
export const name = 'llm-pi-ai'