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:
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user