@chatcode/cco-llm-chatcode-config 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -0
  3. package/README.zh.md +133 -0
  4. package/cordis.patch.yml +4 -0
  5. package/cordis.web.patch.yml +12 -0
  6. package/docs/chatcode-login.md +88 -0
  7. package/docs/chatcode-login.zh.md +179 -0
  8. package/docs/chatcode-models.md +29 -0
  9. package/docs/chatcode-models.zh.md +29 -0
  10. package/docs/chatcode-reporting.md +96 -0
  11. package/docs/chatcode-reporting.zh.md +96 -0
  12. package/docs/decisions/2026-08-31-chatcode-model-source.md +39 -0
  13. package/docs/decisions/2026-08-31-chatcode-model-source.zh.md +39 -0
  14. package/docs/decisions/2026-09-16-actual-model-adapter-routing.md +31 -0
  15. package/docs/decisions/2026-09-16-actual-model-adapter-routing.zh.md +31 -0
  16. package/lib/client.js +469 -0
  17. package/lib/index.d.ts +263 -0
  18. package/lib/index.d.ts.map +1 -0
  19. package/lib/index.js +4873 -0
  20. package/lib/index.js.map +1 -0
  21. package/lib/startup-gate-BaCbWaKH.js +164 -0
  22. package/lib/startup-gate-BaCbWaKH.js.map +1 -0
  23. package/lib/web-startup.d.ts +9 -0
  24. package/lib/web-startup.d.ts.map +1 -0
  25. package/lib/web-startup.js +20 -0
  26. package/lib/web-startup.js.map +1 -0
  27. package/package.json +121 -0
  28. package/vendor/README.md +7 -0
  29. package/vendor/dsh-llm-pi-ai/LICENSE +21 -0
  30. package/vendor/dsh-llm-pi-ai/README.i18n.yaml +6 -0
  31. package/vendor/dsh-llm-pi-ai/README.md +238 -0
  32. package/vendor/dsh-llm-pi-ai/README.zh.md +238 -0
  33. package/vendor/dsh-llm-pi-ai/package.json +65 -0
  34. package/vendor/dsh-llm-pi-ai/src/adapter.ts +434 -0
  35. package/vendor/dsh-llm-pi-ai/src/auth.ts +241 -0
  36. package/vendor/dsh-llm-pi-ai/src/catalog.ts +908 -0
  37. package/vendor/dsh-llm-pi-ai/src/config.ts +478 -0
  38. package/vendor/dsh-llm-pi-ai/src/context.ts +349 -0
  39. package/vendor/dsh-llm-pi-ai/src/discovery.ts +284 -0
  40. package/vendor/dsh-llm-pi-ai/src/index.ts +336 -0
  41. package/vendor/dsh-llm-pi-ai/src/invariant.ts +30 -0
  42. package/vendor/dsh-llm-pi-ai/src/login.ts +161 -0
  43. package/vendor/dsh-llm-pi-ai/src/provider.ts +192 -0
  44. package/vendor/dsh-llm-pi-ai/src/replay.ts +249 -0
  45. package/vendor/dsh-llm-pi-ai/src/stream.ts +232 -0
  46. package/vendor/dsh-llm-pi-ai/tests/adapter.e2e.ts +168 -0
  47. package/vendor/dsh-llm-pi-ai/tests/adapter.spec.ts +1034 -0
  48. package/vendor/dsh-llm-pi-ai/tests/assemble.ts +32 -0
  49. package/vendor/dsh-llm-pi-ai/tests/auth-double.ts +39 -0
  50. package/vendor/dsh-llm-pi-ai/tests/auth.spec.ts +221 -0
  51. package/vendor/dsh-llm-pi-ai/tests/catalog.spec.ts +1220 -0
  52. package/vendor/dsh-llm-pi-ai/tests/config.spec.ts +111 -0
  53. package/vendor/dsh-llm-pi-ai/tests/context.spec.ts +474 -0
  54. package/vendor/dsh-llm-pi-ai/tests/convert.spec.ts +922 -0
  55. package/vendor/dsh-llm-pi-ai/tests/discovery.spec.ts +374 -0
  56. package/vendor/dsh-llm-pi-ai/tests/dynamic-config.spec.ts +241 -0
  57. package/vendor/dsh-llm-pi-ai/tests/fixtures/qr-code.png +0 -0
  58. package/vendor/dsh-llm-pi-ai/tests/loader-composition.spec.ts +244 -0
  59. package/vendor/dsh-llm-pi-ai/tests/login.spec.ts +198 -0
  60. package/vendor/dsh-llm-pi-ai/tests/mock-server.ts +82 -0
  61. package/vendor/dsh-llm-pi-ai/tests/provider-apis.e2e.ts +266 -0
  62. package/vendor/dsh-llm-pi-ai/tests/sdk-options.spec.ts +106 -0
  63. package/vendor/dsh-llm-pi-ai/tsconfig.json +4 -0
  64. package/vendor/dsh-llm-pi-ai/tsconfig.upstream.json +51 -0
@@ -0,0 +1,908 @@
1
+ /**
2
+ * Materialization of one provider route's model catalog. The installed pi-ai
3
+ * catalog supplies defaults keyed by model id, and a profile's own model
4
+ * entries override them field by field, so a route naming a catalog provider
5
+ * stays configuration-free while a route pi-ai has never heard of is fully
6
+ * describable from `settings.yaml`.
7
+ *
8
+ * Every pi-ai `Model` field the harness cannot default is required here rather
9
+ * than at request time: an unserviceable route fails while its configuration is
10
+ * being resolved, which is the earliest point that can name the offending key.
11
+ *
12
+ * @module dsh-llm-pi-ai/catalog
13
+ */
14
+
15
+ import { builtinProviders, getBuiltinModels, getBuiltinProviders } from '@earendil-works/pi-ai/providers/all'
16
+ import type { BuiltinProvider } from '@earendil-works/pi-ai/providers/all'
17
+ import type {
18
+ AnthropicMessagesCompat,
19
+ Api,
20
+ BedrockCompat,
21
+ ChatTemplateKwargValue,
22
+ KnownApi,
23
+ Model,
24
+ ModelCost,
25
+ ModelThinkingLevel,
26
+ OpenAICompletionsCompat,
27
+ OpenAIResponsesCompat,
28
+ Provider,
29
+ ThinkingLevelMap,
30
+ } from '@earendil-works/pi-ai'
31
+
32
+ /**
33
+ * Pricing for a model the installed catalog does not describe. The harness
34
+ * never reads pi-ai's cost metadata — `replay.ts` zeroes it and no consumer
35
+ * reports spend — so this is the absence of a fact, not a configurable rate.
36
+ */
37
+ const NO_COST: ModelCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
38
+
39
+ /** One request modality a pi-ai model may accept. */
40
+ export type PiAiModality = Model<Api>['input'][number]
41
+
42
+ /**
43
+ * Every pi-ai request modality. The `Record` key type is a drift gate: a pi-ai
44
+ * upgrade that adds or removes a modality fails compilation here naming the
45
+ * drifted key, instead of silently narrowing what a profile may declare.
46
+ */
47
+ const MODALITY_GATE: Record<PiAiModality, true> = {
48
+ text: true,
49
+ image: true,
50
+ }
51
+
52
+ /** Every request modality a profile may declare. */
53
+ export const MODALITIES = Object.keys(MODALITY_GATE) as readonly PiAiModality[]
54
+
55
+ /**
56
+ * One entry's modality list, or `undefined` when it states no answer. Absent
57
+ * and empty mean the same thing — `[]` describes a model that accepts nothing
58
+ * and could serve no request — which is what makes an entry naming a catalog
59
+ * model without declaring modalities keep the catalog's, since the config
60
+ * schema materializes `[]` for an absent array.
61
+ * @param configured - the list a `models` or `modelOverrides` entry supplied.
62
+ * @returns the declared modalities, or `undefined` to ask the next level.
63
+ */
64
+ function declaredInput(configured: readonly PiAiModality[] | undefined): Model<Api>['input'] | undefined {
65
+ return configured === undefined || configured.length === 0 ? undefined : [...configured]
66
+ }
67
+
68
+ /**
69
+ * Every pi-ai thinking level, in pi-ai's canonical escalation order. The
70
+ * `Record` key type is a drift gate: a pi-ai upgrade that adds or removes a
71
+ * level fails compilation here naming the drifted key, instead of silently
72
+ * narrowing what a profile may declare.
73
+ */
74
+ const THINKING_LEVEL_GATE: Record<ModelThinkingLevel, true> = {
75
+ off: true,
76
+ minimal: true,
77
+ low: true,
78
+ medium: true,
79
+ high: true,
80
+ xhigh: true,
81
+ max: true,
82
+ }
83
+
84
+ /** Every pi-ai thinking level a profile may declare, in escalation order. */
85
+ export const THINKING_LEVELS = Object.keys(THINKING_LEVEL_GATE) as readonly ModelThinkingLevel[]
86
+
87
+ /** One reasoning-dispatch wire format a profile may name. */
88
+ export type PiAiThinkingFormat = NonNullable<OpenAICompletionsCompat['thinkingFormat']>
89
+
90
+ /**
91
+ * The nameable reasoning-dispatch formats, most-reached first. The `Record`
92
+ * key type is a drift gate: an upstream format addition fails compilation
93
+ * here until it is named, so the offer never silently lags the upstream set.
94
+ * The two `chat-template` variants are nameable because
95
+ * {@link PiAiCompatProfile.chatTemplateKwargs} carries their kwargs;
96
+ * `baseten` is nameable because {@link PiAiCompatProfile.chatTemplateArgs}
97
+ * carries its arguments.
98
+ */
99
+ const THINKING_FORMAT_GATE: Record<PiAiThinkingFormat, true> = {
100
+ 'openai': true,
101
+ 'deepseek': true,
102
+ 'openrouter': true,
103
+ 'together': true,
104
+ 'baseten': true,
105
+ 'zai': true,
106
+ 'qwen': true,
107
+ 'chat-template': true,
108
+ 'qwen-chat-template': true,
109
+ 'string-thinking': true,
110
+ 'ant-ling': true,
111
+ }
112
+
113
+ /** Reasoning-dispatch wire formats a profile may name, most-reached first. */
114
+ export const SUPPORTED_THINKING_FORMATS = Object.keys(THINKING_FORMAT_GATE) as readonly PiAiThinkingFormat[]
115
+
116
+ /** The output-cap field spellings pi-ai accepts. */
117
+ export type PiAiMaxTokensField = NonNullable<OpenAICompletionsCompat['maxTokensField']>
118
+
119
+ /** Drift gate over {@link PiAiMaxTokensField}; an upstream spelling added here fails compilation until named. */
120
+ const MAX_TOKENS_FIELD_GATE: Record<PiAiMaxTokensField, true> = {
121
+ max_completion_tokens: true,
122
+ max_tokens: true,
123
+ }
124
+
125
+ /** The output-cap field spellings a profile may name. */
126
+ export const MAX_TOKENS_FIELDS = Object.keys(MAX_TOKENS_FIELD_GATE) as readonly PiAiMaxTokensField[]
127
+
128
+ /** The prompt-cache marker conventions pi-ai accepts. */
129
+ export type PiAiCacheControlFormat = NonNullable<OpenAICompletionsCompat['cacheControlFormat']>
130
+
131
+ /** Drift gate over {@link PiAiCacheControlFormat}; a new upstream convention fails compilation until named. */
132
+ const CACHE_CONTROL_FORMAT_GATE: Record<PiAiCacheControlFormat, true> = {
133
+ anthropic: true,
134
+ }
135
+
136
+ /** The prompt-cache marker conventions a profile may name. */
137
+ export const CACHE_CONTROL_FORMATS = Object.keys(CACHE_CONTROL_FORMAT_GATE) as readonly PiAiCacheControlFormat[]
138
+
139
+ /** The request-state placeholders a `chat_template_kwargs` value may name. */
140
+ export type PiAiChatTemplateVar = Extract<ChatTemplateKwargValue, { $var: string }>['$var']
141
+
142
+ /** Drift gate over {@link PiAiChatTemplateVar}; a new upstream placeholder fails compilation until named. */
143
+ const CHAT_TEMPLATE_VAR_GATE: Record<PiAiChatTemplateVar, true> = {
144
+ 'thinking.enabled': true,
145
+ 'thinking.effort': true,
146
+ }
147
+
148
+ /** The request-state placeholders a profile may name. */
149
+ export const CHAT_TEMPLATE_VARS = Object.keys(CHAT_TEMPLATE_VAR_GATE) as readonly PiAiChatTemplateVar[]
150
+
151
+ let providerIndex: Map<string, Provider> | undefined
152
+
153
+ /**
154
+ * Installed catalog providers by id, constructed once. Each entry owns the API
155
+ * implementations for its own models, which is why a catalog route reuses this
156
+ * provider instead of being rebuilt from parts.
157
+ * @returns the catalog provider index.
158
+ */
159
+ function catalogProviders(): Map<string, Provider> {
160
+ providerIndex ??= new Map(builtinProviders().map(provider => [provider.id, provider]))
161
+ return providerIndex
162
+ }
163
+
164
+ /**
165
+ * The installed catalog provider for one route, when pi-ai ships one.
166
+ * @param provider - provider route key.
167
+ * @returns the catalog provider, or `undefined` for a route pi-ai does not ship.
168
+ */
169
+ export function catalogProvider(provider: string): Provider | undefined {
170
+ return catalogProviders().get(provider)
171
+ }
172
+
173
+ /**
174
+ * Every provider route the installed pi-ai catalog ships.
175
+ * @returns the catalog provider ids.
176
+ */
177
+ export function catalogProviderIds(): readonly string[] {
178
+ return getBuiltinProviders()
179
+ }
180
+
181
+ /**
182
+ * The installed catalog models for one route, indexed by model id.
183
+ * @param provider - provider route key.
184
+ * @returns catalog models by id; empty for a route pi-ai does not ship.
185
+ */
186
+ export function catalogModels(provider: string): Map<string, Model<Api>> {
187
+ if (!catalogProviders().has(provider)) return new Map()
188
+ const models = getBuiltinModels(provider as BuiltinProvider) as Model<Api>[]
189
+ return new Map(models.map(model => [model.id, model]))
190
+ }
191
+
192
+ /**
193
+ * Selectable reasoning efforts for one model: each key is a level the model
194
+ * offers (and selectors show), and its value is the wire spelling dispatch
195
+ * sends for it. `off` alone may leave its value empty — "supported, send
196
+ * nothing" — because for most providers not thinking is the parameter's
197
+ * absence; every other declared level must name a wire value. A level absent
198
+ * from the dict is not offered.
199
+ */
200
+ export type PiAiReasoningEfforts = Partial<Record<ModelThinkingLevel, string | null>>
201
+
202
+ /**
203
+ * Whether one pi-ai compat field is configurable on a profile.
204
+ *
205
+ * `withhold` is the disposition for a field pi-ai's installed catalog already
206
+ * sets for a named vendor. Reaching for one of those on a hand-declared route
207
+ * means configuring a provider that should have been named as a catalog route
208
+ * instead, where the installed entry carries the right value already.
209
+ */
210
+ type CompatDisposition = 'offer' | 'withhold'
211
+
212
+ /**
213
+ * Disposition of every `OpenAICompletionsCompat` field. The `Record` key type
214
+ * is a drift gate: a pi-ai upgrade that adds a field fails compilation here
215
+ * until it is classified, so the offer never silently lags the upstream set.
216
+ */
217
+ const COMPLETIONS_COMPAT_GATE = {
218
+ supportsStore: 'offer',
219
+ supportsDeveloperRole: 'offer',
220
+ supportsReasoningEffort: 'offer',
221
+ supportsUsageInStreaming: 'offer',
222
+ supportsFinishReason: 'offer',
223
+ maxTokensField: 'offer',
224
+ requiresToolResultName: 'offer',
225
+ requiresAssistantAfterToolResult: 'offer',
226
+ requiresThinkingAsText: 'offer',
227
+ requiresReasoningContentOnAssistantMessages: 'offer',
228
+ thinkingFormat: 'offer',
229
+ chatTemplateKwargs: 'offer',
230
+ chatTemplateArgs: 'offer',
231
+ supportsThinkingTokenBudget: 'offer',
232
+ supportsStrictMode: 'offer',
233
+ cacheControlFormat: 'offer',
234
+ supportsLongCacheRetention: 'offer',
235
+ openRouterRouting: 'withhold',
236
+ vercelGatewayRouting: 'withhold',
237
+ zaiToolStream: 'withhold',
238
+ supportsOpenAIGrammarTools: 'withhold',
239
+ sendSessionAffinityHeaders: 'withhold',
240
+ deferredToolsMode: 'withhold',
241
+ sessionAffinityFormat: 'withhold',
242
+ } as const satisfies Record<keyof OpenAICompletionsCompat, CompatDisposition>
243
+
244
+ /** Disposition of every `OpenAIResponsesCompat` field; a drift gate like the one above. */
245
+ const RESPONSES_COMPAT_GATE = {
246
+ supportsDeveloperRole: 'offer',
247
+ supportsStrictMode: 'offer',
248
+ supportsLongCacheRetention: 'offer',
249
+ sessionAffinityFormat: 'withhold',
250
+ supportsOpenAIGrammarTools: 'withhold',
251
+ supportsAdditionalTools: 'withhold',
252
+ supportsToolSearch: 'withhold',
253
+ supportsExplicitPromptCacheMode: 'withhold',
254
+ } as const satisfies Record<keyof OpenAIResponsesCompat, CompatDisposition>
255
+
256
+ /** Disposition of every `AnthropicMessagesCompat` field; a drift gate like the one above. */
257
+ const ANTHROPIC_COMPAT_GATE = {
258
+ supportsEagerToolInputStreaming: 'offer',
259
+ supportsLongCacheRetention: 'offer',
260
+ supportsCacheControlOnTools: 'offer',
261
+ supportsTemperature: 'offer',
262
+ forceAdaptiveThinking: 'offer',
263
+ allowEmptySignature: 'offer',
264
+ supportsStrictTools: 'offer',
265
+ sendSessionAffinityHeaders: 'withhold',
266
+ supportsToolReferences: 'withhold',
267
+ } as const satisfies Record<keyof AnthropicMessagesCompat, CompatDisposition>
268
+
269
+ /** Disposition of every `BedrockCompat` field; a drift gate like the one above. */
270
+ const BEDROCK_COMPAT_GATE = {
271
+ supportsStrictMode: 'offer',
272
+ } as const satisfies Record<keyof BedrockCompat, CompatDisposition>
273
+
274
+ /**
275
+ * Every wire protocol pi-ai gives a compat type. Derived from `Model.compat`'s
276
+ * own conditional rather than listed by hand, so a pi-ai release that gives a
277
+ * further protocol a compat type fails the {@link COMPAT_GATES} entry list
278
+ * until someone classifies its fields. A protocol pi-ai gives no compat type
279
+ * resolves away here and takes no configured compat at all.
280
+ */
281
+ type ApiWithCompat = { [K in KnownApi]: NonNullable<Model<K>['compat']> extends never ? never : K }[KnownApi]
282
+
283
+ /**
284
+ * The compat gate of every wire protocol a profile may configure.
285
+ *
286
+ * Keyed by protocol, but grouped by pi-ai's compat *type*: the three Responses
287
+ * protocols share `OpenAIResponsesCompat`, so a switch settable on one is
288
+ * settable on all three. Keying by protocol alone would refuse
289
+ * `azure-openai-responses` and `openai-codex-responses` the fields their own
290
+ * models declare.
291
+ */
292
+ const COMPAT_GATES: Readonly<Record<ApiWithCompat, Readonly<Record<string, CompatDisposition>>>> = {
293
+ 'openai-completions': COMPLETIONS_COMPAT_GATE,
294
+ 'openai-responses': RESPONSES_COMPAT_GATE,
295
+ 'azure-openai-responses': RESPONSES_COMPAT_GATE,
296
+ 'openai-codex-responses': RESPONSES_COMPAT_GATE,
297
+ 'anthropic-messages': ANTHROPIC_COMPAT_GATE,
298
+ 'bedrock-converse-stream': BEDROCK_COMPAT_GATE,
299
+ }
300
+
301
+ /**
302
+ * The compat gate of one resolved protocol. A `string` lookup rather than a
303
+ * keyed read: a route's `api` is configuration, so it may name a protocol
304
+ * pi-ai gives no compat type — or none at all.
305
+ * @param api - resolved wire protocol.
306
+ * @returns that protocol's field gate, or `undefined` when it takes no compat.
307
+ */
308
+ function compatGate(api: string): Readonly<Record<string, CompatDisposition>> | undefined {
309
+ return (COMPAT_GATES as Readonly<Record<string, Readonly<Record<string, CompatDisposition>>>>)[api]
310
+ }
311
+
312
+ /** The field names one gate offers. */
313
+ type OfferedIn<G> = { [K in keyof G]: G[K] extends 'offer' ? K : never }[keyof G]
314
+
315
+ /** Every compat field name a profile may set, on whichever protocol takes it. */
316
+ type OfferedCompatField =
317
+ | OfferedIn<typeof COMPLETIONS_COMPAT_GATE>
318
+ | OfferedIn<typeof RESPONSES_COMPAT_GATE>
319
+ | OfferedIn<typeof ANTHROPIC_COMPAT_GATE>
320
+ | OfferedIn<typeof BEDROCK_COMPAT_GATE>
321
+
322
+ /**
323
+ * pi-ai wire-compatibility switches, set on the route (its models' default) or
324
+ * per model (winning over the route, field by field).
325
+ *
326
+ * pi-ai decides each of these from the provider id and baseURL when no layer
327
+ * sets it, and a private gateway's URL says nothing: for an endpoint it does
328
+ * not recognize the detection answers as though it were OpenAI itself, which
329
+ * is wrong for most OpenAI-compatible gateways. So every field here is one a
330
+ * deployment must be able to state because nothing can infer it, while the
331
+ * fields pi-ai's catalog sets for a named vendor stay withheld.
332
+ *
333
+ * A field belongs to the protocols whose upstream compat type declares it: a
334
+ * model-level switch its protocol does not take fails resolution, and a
335
+ * route-level one skips past models it cannot fit. "The three Responses
336
+ * protocols" below means `openai-responses`, `azure-openai-responses`, and
337
+ * `openai-codex-responses`, which pi-ai gives one shared compat type, so a
338
+ * switch settable on one is settable on all three.
339
+ */
340
+ export interface PiAiCompatProfile {
341
+ /** Whether the endpoint accepts `store`; `openai-completions`. */
342
+ supportsStore?: boolean
343
+ /**
344
+ * Whether the endpoint accepts the `developer` role for the system prompt,
345
+ * which pi-ai sends only to a reasoning model; `false` keeps `system`.
346
+ * `openai-completions` and the three Responses protocols.
347
+ */
348
+ supportsDeveloperRole?: boolean
349
+ /** Whether the endpoint accepts `reasoning_effort`; `openai-completions`. */
350
+ supportsReasoningEffort?: boolean
351
+ /** Whether the endpoint accepts `stream_options: {include_usage: true}`; `openai-completions`. */
352
+ supportsUsageInStreaming?: boolean
353
+ /**
354
+ * Whether streams include `finish_reason`; `false` lets pi-ai infer the
355
+ * terminal reason when the stream ends; `openai-completions`.
356
+ */
357
+ supportsFinishReason?: boolean
358
+ /** Which output-cap field the endpoint reads; `openai-completions`. */
359
+ maxTokensField?: NonNullable<OpenAICompletionsCompat['maxTokensField']>
360
+ /** Whether tool results must carry `name`; `openai-completions`. */
361
+ requiresToolResultName?: boolean
362
+ /** Whether a user message after tool results needs an assistant message between; `openai-completions`. */
363
+ requiresAssistantAfterToolResult?: boolean
364
+ /** Whether thinking blocks must travel as text in `<thinking>` delimiters; `openai-completions`. */
365
+ requiresThinkingAsText?: boolean
366
+ /** Whether replayed assistant messages need an empty `reasoning_content` while reasoning is on; `openai-completions`. */
367
+ requiresReasoningContentOnAssistantMessages?: boolean
368
+ /** Reasoning parameter format the endpoint expects; `openai-completions`. */
369
+ thinkingFormat?: PiAiThinkingFormat
370
+ /**
371
+ * Kwargs sent as `chat_template_kwargs`, which pi-ai reads only under the
372
+ * two `chat-template` thinking formats; `openai-completions`. Nothing checks
373
+ * that pairing: the format in force may come from the installed catalog
374
+ * entry or from pi-ai's own baseURL detection, neither of which resolution
375
+ * can read, so kwargs set beside another format are sent nowhere.
376
+ */
377
+ chatTemplateKwargs?: NonNullable<OpenAICompletionsCompat['chatTemplateKwargs']>
378
+ /** Arguments sent as `chat_template_args` under the `baseten` thinking format; `openai-completions`. */
379
+ chatTemplateArgs?: NonNullable<OpenAICompletionsCompat['chatTemplateArgs']>
380
+ /** Whether the endpoint accepts `thinking_token_budget` to cap vLLM reasoning; `openai-completions`. */
381
+ supportsThinkingTokenBudget?: boolean
382
+ /**
383
+ * Whether the endpoint accepts `strict` in tool definitions;
384
+ * `openai-completions`, the three Responses protocols, `bedrock-converse-stream`.
385
+ */
386
+ supportsStrictMode?: boolean
387
+ /** Prompt-cache marker convention; `openai-completions`. */
388
+ cacheControlFormat?: NonNullable<OpenAICompletionsCompat['cacheControlFormat']>
389
+ /**
390
+ * Whether the endpoint accepts long prompt-cache retention;
391
+ * `openai-completions`, the three Responses protocols, `anthropic-messages`.
392
+ */
393
+ supportsLongCacheRetention?: boolean
394
+ /** Whether the endpoint accepts per-tool `eager_input_streaming`; `anthropic-messages`. */
395
+ supportsEagerToolInputStreaming?: boolean
396
+ /** Whether the endpoint accepts `cache_control` on tool definitions; `anthropic-messages`. */
397
+ supportsCacheControlOnTools?: boolean
398
+ /** Whether the endpoint accepts the `temperature` request field; `anthropic-messages`. */
399
+ supportsTemperature?: boolean
400
+ /** Whether to force adaptive thinking regardless of model id; `anthropic-messages`. */
401
+ forceAdaptiveThinking?: boolean
402
+ /** Whether to replay an empty thinking signature instead of converting thinking to text; `anthropic-messages`. */
403
+ allowEmptySignature?: boolean
404
+ /** Whether the endpoint accepts Anthropic strict tool schemas; `anthropic-messages`. */
405
+ supportsStrictTools?: boolean
406
+ }
407
+
408
+ /** Compile-time constraint that `T` is `never`. */
409
+ type AssertNever<T extends never> = T
410
+
411
+ /**
412
+ * Proof that every documented field is one a gate offers. A field the profile
413
+ * declares past the gates fails compilation with its own name in the error.
414
+ */
415
+ export type EveryProfileFieldIsOffered = AssertNever<Exclude<keyof PiAiCompatProfile, OfferedCompatField>>
416
+
417
+ /**
418
+ * Proof that every offered field is documented. A gate entry flipped to
419
+ * `offer` without a profile field fails compilation with its own name in the
420
+ * error, which is the half a schema alone cannot catch.
421
+ */
422
+ export type EveryOfferedFieldIsDocumented = AssertNever<Exclude<OfferedCompatField, keyof PiAiCompatProfile>>
423
+
424
+ /** Compile-time constraint that `T` is `true`. */
425
+ type AssertTrue<T extends true> = T
426
+
427
+ /** Every compat type a gate classifies, merged so one `Pick` reaches all offered fields. */
428
+ type UpstreamCompat = OpenAICompletionsCompat & OpenAIResponsesCompat & AnthropicMessagesCompat & BedrockCompat
429
+
430
+ /**
431
+ * Proof that each documented field carries its upstream type, not a hand-copied
432
+ * restatement of it. The name gates above pin *which* fields exist; this pins
433
+ * their types, in both directions because each catches a different drift. A
434
+ * profile field wider than upstream accepts a value the provider rejects, and
435
+ * `resolveModelCompat`'s cast to `ModelCompat` would hide it; a narrower one
436
+ * refuses a value the provider accepts, which is how an upgrade that widens a
437
+ * union would otherwise leave configuration silently behind.
438
+ */
439
+ export type EveryProfileFieldMatchesUpstream = AssertTrue<
440
+ PiAiCompatProfile extends Partial<Pick<UpstreamCompat, OfferedCompatField>>
441
+ ? Partial<Pick<UpstreamCompat, OfferedCompatField>> extends PiAiCompatProfile ? true : false
442
+ : false
443
+ >
444
+
445
+ /**
446
+ * The compat entries a profile actually set.
447
+ *
448
+ * schemastery materializes an absent dict as `{}` — the behavior
449
+ * `reasoningEfforts` works around with a union — so every parsed profile
450
+ * carries both template-argument keys whether or not anyone wrote them. An
451
+ * empty one states nothing here: it would send no arguments, which is exactly
452
+ * what leaving the field out does, so absent and empty are the same request
453
+ * and neither may make a route look like it configured a switch. A valueless
454
+ * scalar is the other thing schemastery lets through, and it is refused by
455
+ * {@link assertOfferedCompatFields} before this runs rather than filtered.
456
+ * @param compat - the configured switches, when any.
457
+ * @returns the entries carrying a value, in declaration order.
458
+ */
459
+ function configuredCompatEntries(compat: PiAiCompatProfile | undefined): readonly (readonly [string, unknown])[] {
460
+ return Object.entries(compat ?? {}).flatMap(([field, value]) => {
461
+ const empty = typeof value === 'object' && value !== null && !Array.isArray(value)
462
+ && Object.keys(value as object).length === 0
463
+ return empty ? [] : [[field, value] as const]
464
+ })
465
+ }
466
+
467
+ /**
468
+ * The protocols offering one compat field, in {@link COMPAT_GATES} order.
469
+ * @param field - configured compat field name.
470
+ * @returns the protocols whose compat takes it; empty when none does, which
471
+ * is either a withheld field or a name no upstream compat type declares.
472
+ */
473
+ function compatProtocols(field: string): readonly string[] {
474
+ return Object.entries(COMPAT_GATES).flatMap(([api, gate]) => gate[field] === 'offer' ? [api] : [])
475
+ }
476
+
477
+ /**
478
+ * The compat fields one protocol offers, for a diagnostic that has to show
479
+ * what was available instead of the name that missed.
480
+ * @param api - wire protocol.
481
+ * @returns the offered field names, or an empty list for a protocol taking no compat.
482
+ */
483
+ function offeredCompatFields(api: string): readonly string[] {
484
+ return Object.entries(compatGate(api) ?? {}).flatMap(([field, disposition]) => disposition === 'offer' ? [field] : [])
485
+ }
486
+
487
+ /**
488
+ * Every offered field name, deduplicated, for the one diagnostic that cannot
489
+ * narrow by protocol: the vocabulary check runs before any protocol resolves,
490
+ * which is what lets it refuse a misspelling on a route whose models would
491
+ * never have reached the protocol that declares the intended field.
492
+ * @returns the offered field names across every protocol, in gate order.
493
+ */
494
+ function allOfferedCompatFields(): readonly string[] {
495
+ const fields = new Set<string>()
496
+ for (const api of Object.keys(COMPAT_GATES)) {
497
+ for (const field of offeredCompatFields(api)) fields.add(field)
498
+ }
499
+ return [...fields]
500
+ }
501
+
502
+ /**
503
+ * Reject a compat key no protocol offers. Runs before any protocol is
504
+ * resolved, so a withheld field or a misspelling fails even on a route whose
505
+ * models never reach the protocol that would have taken it — the alternative
506
+ * being the silent drop that let an unreadable switch look applied.
507
+ * @param provider - provider route key, for diagnostics.
508
+ * @param site - the configuration site, for diagnostics.
509
+ * @param compat - the configured switches, when any.
510
+ * @throws Error naming the offending key.
511
+ */
512
+ function assertOfferedCompatFields(
513
+ provider: string,
514
+ site: string,
515
+ compat: PiAiCompatProfile | undefined,
516
+ ): void {
517
+ // Every key, not only the ones carrying a value: a withheld or undeclared
518
+ // name is never in the schema, so schemastery cannot have materialized it —
519
+ // whatever its value, a person wrote it and expects it to do something.
520
+ for (const [field, value] of Object.entries(compat ?? {})) {
521
+ // The name is judged before the value, so a withheld or misspelled key
522
+ // written bare is refused for being that name rather than for being empty:
523
+ // the other order sends someone to supply a value the key would be refused
524
+ // with anyway.
525
+ if (compatProtocols(field).length === 0) {
526
+ const declared = Object.values(COMPAT_GATES).some(gate => gate[field] !== undefined)
527
+ if (declared) {
528
+ invalid(provider, `${site} sets compat "${field}", which is not configurable here: pi-ai's installed`
529
+ + ' catalog sets it for the vendors that need it, so name that provider as the route instead')
530
+ }
531
+ invalid(provider, `${site} sets compat "${field}", which no wire protocol declares; the configurable`
532
+ + ` switches are ${allOfferedCompatFields().join(', ')}`)
533
+ }
534
+ // A valueless key (`supportsDeveloperRole:`) survives schemastery, which
535
+ // passes nullable data through before any member schema runs — the same
536
+ // behavior `reasoningEfforts` documents — and a `cordis.yml` entry may
537
+ // reach the same state through `!!js undefined`. Either way the key is
538
+ // kept, so carrying it forward writes nothing over whatever the next layer
539
+ // resolved, leaving pi-ai's `??` at its baseURL detection: the "written but
540
+ // not applied" outcome this surface exists to refuse.
541
+ if (value == null) {
542
+ invalid(provider, `${site} sets compat "${field}" with no value; give it one, or remove the key to`
543
+ + ' leave the field to the next layer — the installed catalog entry, then pi-ai\'s own detection')
544
+ }
545
+ }
546
+ }
547
+
548
+ /** One configured model entry: an id plus the catalog fields it overrides. */
549
+ export interface PiAiModelProfile {
550
+ /** Model id sent to the provider and accepted by {@link GenerateOptions.model}. */
551
+ id: string
552
+ /** Display name for selectors; defaults to the catalog name, then the id. */
553
+ name?: string
554
+ /** Maximum combined request and response context in tokens. */
555
+ contextWindow?: number
556
+ /**
557
+ * Maximum output tokens. Configuring one also makes it this model's
558
+ * per-request default; a value inherited from the installed catalog, or the
559
+ * route's fallback, is the model's capability and never becomes a request
560
+ * default on its own.
561
+ */
562
+ maxTokens?: number
563
+ /**
564
+ * Request modalities this model accepts. Absent — or empty, which describes
565
+ * a model that accepts nothing and so states no answer either — keeps the
566
+ * installed catalog entry's modalities, then the route's `defaultInput`.
567
+ * Declaring images is what makes a hand-declared vision model usable, and
568
+ * declaring text alone corrects a catalog model whose gateway does not serve
569
+ * what the catalog records. This is a claim about the endpoint, not a check
570
+ * of it: nothing interrogates a gateway for what it accepts, so a model
571
+ * claiming images its endpoint refuses is refused by the provider instead,
572
+ * mid-turn.
573
+ */
574
+ input?: PiAiModality[]
575
+ /**
576
+ * Selectable reasoning efforts. Absent inherits the installed catalog
577
+ * entry's capability (a hand-declared model has none and does not reason);
578
+ * `false` declares a non-reasoning model, which is how a profile strips
579
+ * reasoning from a catalog model its gateway cannot serve; a non-empty dict
580
+ * declares the offered levels and their wire spellings.
581
+ */
582
+ reasoningEfforts?: false | PiAiReasoningEfforts
583
+ /** pi-ai wire-compatibility switches for this model, winning over the route's per field; one its protocol does not declare is refused. */
584
+ compat?: PiAiCompatProfile
585
+ }
586
+
587
+ /**
588
+ * Customization of one installed catalog model, keyed by its id in the
589
+ * route's `modelOverrides` dict — the same fields a `models` entry may set,
590
+ * with the id living in the key. Unlike a `models` list, overrides leave the
591
+ * rest of the catalog serving untouched, which is what makes "correct one
592
+ * model, keep the other thirty-seven" a three-line edit.
593
+ */
594
+ export type PiAiModelOverride = Omit<PiAiModelProfile, 'id'>
595
+
596
+ /** The route-level facts model materialization reads. */
597
+ export interface RouteCatalogRequest {
598
+ /** Provider route key, stamped onto every materialized model. */
599
+ provider: string
600
+ /** Wire protocol override; absent defers to each catalog model's own API. */
601
+ api?: string
602
+ /** Endpoint override; absent defers to the catalog model, then the catalog provider. */
603
+ baseURL?: string
604
+ /** Configured catalog; absent means the whole installed catalog for this route. */
605
+ models?: readonly PiAiModelProfile[]
606
+ /** Installed-catalog customizations by model id; only meaningful while `models` is absent. */
607
+ modelOverrides?: Readonly<Record<string, PiAiModelOverride>>
608
+ /** Route-level wire-compatibility switches, landing on each model whose protocol declares them; entries override per field. */
609
+ compat?: PiAiCompatProfile
610
+ /** Context capacity for a model neither the entry nor the catalog sizes. */
611
+ defaultContextWindow: number
612
+ /** Output capability for a model neither the entry nor the catalog sizes. */
613
+ defaultMaxTokens: number
614
+ /** Modalities for a model neither the entry nor the catalog declares. */
615
+ defaultInput: Model<Api>['input']
616
+ }
617
+
618
+ /** Report a route the deployment cannot serve, naming the settings key at fault. */
619
+ function invalid(provider: string, detail: string): never {
620
+ throw new Error(`llm-pi-ai: provider "${provider}" ${detail}`)
621
+ }
622
+
623
+ /**
624
+ * The one wire protocol a catalog route's shipped models agree on. This is what
625
+ * lets a deployment add a model the installed catalog has not caught up with —
626
+ * a provider's newest release — without restating the protocol its siblings
627
+ * already use. A route whose shipped models disagree (an OpenAI-style catalog
628
+ * spanning Responses and Chat Completions) has no such answer, so a model it
629
+ * does not describe must name its protocol at the route.
630
+ */
631
+ function sharedCatalogApi(defaults: ReadonlyMap<string, Model<Api>>): string | undefined {
632
+ const apis = new Set<string>()
633
+ for (const model of defaults.values()) apis.add(model.api)
634
+ return apis.size === 1 ? [...apis][0] : undefined
635
+ }
636
+
637
+ /** The reasoning fields one materialized model carries. */
638
+ interface ModelReasoning {
639
+ /** Whether the model reasons at all; `false` makes pi-ai ignore the map. */
640
+ reasoning: boolean
641
+ /** The map dispatch reads; absent only when the installed entry's (or none) applies. */
642
+ thinkingLevelMap?: ThinkingLevelMap
643
+ }
644
+
645
+ /**
646
+ * Resolve one model's reasoning capability from its declared efforts.
647
+ *
648
+ * A declared dict translates to pi-ai's `thinkingLevelMap` with every level
649
+ * decided explicitly: declared levels carry their wire spelling, undeclared
650
+ * levels are pinned to `null` (unsupported). Pinning matters because pi-ai's
651
+ * own defaulting is asymmetric — an absent key means "supported" for the five
652
+ * base levels but "unsupported" for `xhigh`/`max` — and a profile author
653
+ * should not need to know that. A declared `off` with no value is the one
654
+ * exception: it stays absent from the map, which pi-ai reads as "supported,
655
+ * send nothing" — the correct dispatch where not thinking is the parameter's
656
+ * absence — while `off` with a value sends that value.
657
+ * @param provider - provider route key, for diagnostics.
658
+ * @param entry - the configured model entry.
659
+ * @param base - the installed catalog entry of the same id, when one exists.
660
+ * @returns the reasoning fields the materialized model carries.
661
+ */
662
+ function resolveModelReasoning(
663
+ provider: string,
664
+ entry: PiAiModelProfile,
665
+ base: Model<Api> | undefined,
666
+ ): ModelReasoning {
667
+ const efforts = entry.reasoningEfforts
668
+ if (efforts === undefined) {
669
+ // Reasoning rides the installed entry or is absent: a bare capability flag
670
+ // would make pi-ai advertise effort levels with no `thinkingLevelMap` to
671
+ // spell them, and no listing endpoint reports a model's reasoning
672
+ // protocol. The entry's map (when any) arrives through the `...base`
673
+ // spread in the model literal.
674
+ return { reasoning: base?.reasoning ?? false }
675
+ }
676
+ // The installed entry's map may ride along through `...base`; pi-ai never
677
+ // reads it on a non-reasoning model, so stripping it is not worth a field
678
+ // enumeration here.
679
+ if (efforts === false) return { reasoning: false }
680
+ // A YAML `reasoningEfforts:` left valueless arrives as null through the
681
+ // schema union — outside the field's declared type, hence the widening —
682
+ // while an explicit `{}` arrives as an empty dict. Both declare nothing,
683
+ // and neither is a spelling of "inherit" or "disable".
684
+ if ((efforts as unknown) === null || Object.keys(efforts).length === 0) {
685
+ invalid(provider, `model "${entry.id}" has an empty reasoningEfforts; declare the offered levels, set`
686
+ + ' false for a non-reasoning model, or omit the field to keep the installed catalog\'s capability')
687
+ }
688
+ const declared = THINKING_LEVELS.flatMap((level) => {
689
+ const wire = efforts[level]
690
+ return wire === undefined ? [] : [[level, wire] as const]
691
+ })
692
+ for (const [level, wire] of declared) {
693
+ if (wire === null) {
694
+ if (level !== 'off') {
695
+ invalid(provider, `model "${entry.id}" reasoningEfforts.${level} needs the wire value dispatch`
696
+ + ' should send; only "off" may leave it empty')
697
+ }
698
+ } else if (wire.length === 0) {
699
+ invalid(provider, `model "${entry.id}" reasoningEfforts.${level} must not be an empty string`)
700
+ }
701
+ }
702
+ if (!declared.some(([level]) => level !== 'off')) {
703
+ invalid(provider, `model "${entry.id}" reasoningEfforts offers no level beyond "off"; declare a thinking`
704
+ + ' level, or set reasoningEfforts to false for a non-reasoning model')
705
+ }
706
+ const map: ThinkingLevelMap = {}
707
+ for (const level of THINKING_LEVELS) {
708
+ const wire = efforts[level]
709
+ if (wire === undefined) {
710
+ map[level] = null
711
+ } else if (wire !== null) {
712
+ map[level] = wire
713
+ }
714
+ }
715
+ return { reasoning: true, thinkingLevelMap: map }
716
+ }
717
+
718
+ /** The compat block a materialized model carries, whichever protocol it speaks. */
719
+ type ModelCompat = OpenAICompletionsCompat | OpenAIResponsesCompat | AnthropicMessagesCompat | BedrockCompat
720
+
721
+ /**
722
+ * Resolve one model's compat block from the profile's switches.
723
+ *
724
+ * A model switch wins over the route switch field by field; whatever neither
725
+ * sets keeps the installed entry's value, and a field no layer decides falls
726
+ * through to pi-ai's own detection. A model-level switch its protocol does not
727
+ * take fails resolution — about one named model it can only be a mistake —
728
+ * while a route-level one skips past such models, since a route default must
729
+ * stay settable on a route whose models do not all speak one protocol. Every
730
+ * field reaching here is offered by some protocol; {@link
731
+ * assertOfferedCompatFields} has already refused the rest.
732
+ * @param provider - provider route key, for diagnostics.
733
+ * @param entry - the configured model entry.
734
+ * @param route - the route-level switches, when any.
735
+ * @param base - the installed catalog entry of the same id, when one exists.
736
+ * @param api - the model's resolved wire protocol.
737
+ * @returns a `compat` field to spread into the model, or nothing.
738
+ */
739
+ function resolveModelCompat(
740
+ provider: string,
741
+ entry: PiAiModelProfile,
742
+ route: PiAiCompatProfile | undefined,
743
+ base: Model<Api> | undefined,
744
+ api: string,
745
+ ): { compat: ModelCompat } | Record<string, never> {
746
+ const gate = compatGate(api)
747
+ const configured: Record<string, unknown> = {}
748
+ for (const [field, value] of configuredCompatEntries(route)) {
749
+ if (gate?.[field] !== 'offer') continue
750
+ configured[field] = value
751
+ }
752
+ for (const [field, value] of configuredCompatEntries(entry.compat)) {
753
+ if (gate?.[field] !== 'offer') {
754
+ const offered = offeredCompatFields(api)
755
+ invalid(provider, `model "${entry.id}" sets compat "${field}", but its api is "${api}", which does not`
756
+ + ` take it; that switch exists on ${compatProtocols(field).join(', ')}, and "${api}" offers`
757
+ + ` ${offered.length === 0 ? 'no configurable compat' : offered.join(', ')}`)
758
+ }
759
+ configured[field] = value
760
+ }
761
+ if (Object.keys(configured).length === 0) return {}
762
+ // The installed entry's compat matches the entry's OWN api — a route-level
763
+ // `api` repoint (an anthropic catalog served through an OpenAI-compatible
764
+ // gateway) leaves `base.compat` in the other protocol's shape, so it is
765
+ // inherited only while the resolved api still is the entry's. A repointed
766
+ // model starts from pi-ai's baseURL-derived detection instead, which is
767
+ // what a protocol change means for every other compat field too.
768
+ const inherited = base?.api === api ? base.compat : undefined
769
+ return { compat: { ...inherited, ...configured } as ModelCompat }
770
+ }
771
+
772
+ /** One route's materialized catalog, plus the request caps its profile chose. */
773
+ export interface RouteCatalog {
774
+ /** The materialized models in configuration order. */
775
+ models: readonly Model<Api>[]
776
+ /**
777
+ * Per-request output caps this profile explicitly configured, by model id.
778
+ *
779
+ * Separate from `Model.maxTokens` because the two answer different
780
+ * questions: pi-ai requires `maxTokens` as the model's output *capability*,
781
+ * while the harness seam's `defaultMaxTokens` is a cap the deployment chose
782
+ * to send on requests that name none. Materializing a catalog capability as
783
+ * a request default would start capping every request at a number nobody
784
+ * picked, so only an explicit configuration lands here.
785
+ */
786
+ configuredMaxTokens: ReadonlyMap<string, number>
787
+ }
788
+
789
+ /**
790
+ * Materialize one route's catalog by merging the installed catalog defaults
791
+ * under the configured entries. A route with no configured `models` serves the
792
+ * installed catalog unchanged, which is what keeps an existing
793
+ * `providers: { deepseek: { apiKeyEnv: … } }` profile working untouched.
794
+ * @param request - the route-level catalog facts.
795
+ * @returns the materialized models and the explicitly configured request caps.
796
+ */
797
+ export function resolveRouteModels(request: RouteCatalogRequest): RouteCatalog {
798
+ const { provider } = request
799
+ const defaults = catalogModels(provider)
800
+ const providerBaseUrl = catalogProvider(provider)?.baseUrl
801
+ // An absent `models` key and an empty one are the same request: the config
802
+ // schema materializes `[]` for the absent case, and an empty catalog could
803
+ // serve no request anyway, so both mean "serve the installed catalog".
804
+ const configured = request.models ?? []
805
+ const overrides = request.modelOverrides ?? {}
806
+ // Every miss is refused, never skipped: an override that lands nowhere is a
807
+ // typo someone would otherwise hunt for in a silently unchanged model.
808
+ for (const [id, override] of Object.entries(overrides)) {
809
+ if (id.length === 0) invalid(provider, 'has a modelOverrides entry with an empty model id')
810
+ if (defaults.size === 0) {
811
+ invalid(provider, `sets modelOverrides for "${id}", but the installed catalog does not describe this route;`
812
+ + ' a declared route spells every model out in its models list')
813
+ }
814
+ if (configured.length > 0) {
815
+ invalid(provider, `sets modelOverrides for "${id}" beside a models list; models already replaces the served`
816
+ + ' catalog, so declare the fields on its entries')
817
+ }
818
+ if (!defaults.has(id)) {
819
+ invalid(provider, `modelOverrides names "${id}", which the installed catalog does not describe`)
820
+ }
821
+ // The id lives in the dict key; a value carrying its own would quietly
822
+ // rename the model it meant to customize. The static shape already omits
823
+ // it — this guards the schema boundary, which passes unknown keys through.
824
+ if ('id' in override) {
825
+ invalid(provider, `modelOverrides entry "${id}" sets "id", which is the dict key`)
826
+ }
827
+ }
828
+ // An override becomes the catalog entry's configuration, so everything a
829
+ // models entry may declare — capacities, efforts, compat — resolves through
830
+ // the same path with the same diagnostics and request-default semantics.
831
+ const entries: readonly PiAiModelProfile[] = configured.length > 0
832
+ ? configured
833
+ : [...defaults.values()].map(model => ({ id: model.id, ...overrides[model.id] }))
834
+ if (entries.length === 0) {
835
+ invalid(provider, 'resolves no models; the installed catalog does not describe this route, so its models'
836
+ + ' must be listed in configuration')
837
+ }
838
+ const routeApi = sharedCatalogApi(defaults)
839
+ // Vocabulary before protocols: a withheld or undeclared switch is refused
840
+ // wherever it is written, so it cannot look applied on a route whose models
841
+ // never reach the protocol that would have taken it.
842
+ assertOfferedCompatFields(provider, 'route', request.compat)
843
+ for (const entry of entries) {
844
+ assertOfferedCompatFields(provider, `model "${entry.id}"`, entry.compat)
845
+ }
846
+ const seen = new Set<string>()
847
+ const configuredMaxTokens = new Map<string, number>()
848
+ const models = entries.map((entry) => {
849
+ if (entry.id.length === 0) invalid(provider, 'has a model with an empty id')
850
+ if (seen.has(entry.id)) invalid(provider, `lists model "${entry.id}" more than once`)
851
+ seen.add(entry.id)
852
+ const base = defaults.get(entry.id)
853
+ const api = request.api ?? base?.api ?? routeApi
854
+ if (api === undefined) {
855
+ invalid(provider, `model "${entry.id}" needs an api; the installed catalog does not describe it, so set the`
856
+ + ' route\'s api to the wire protocol its endpoint speaks')
857
+ }
858
+ const baseUrl = request.baseURL ?? base?.baseUrl ?? providerBaseUrl
859
+ if (baseUrl === undefined) {
860
+ invalid(provider, `model "${entry.id}" needs a baseURL; the installed catalog does not describe this route`)
861
+ }
862
+ // Capacities fall back to the route's own defaults, so a model listing that
863
+ // discloses nothing but ids still yields a serviceable route. The fallback
864
+ // is a guess by construction, which is why it is a configurable route field
865
+ // rather than a constant buried here.
866
+ const contextWindow = entry.contextWindow ?? base?.contextWindow ?? request.defaultContextWindow
867
+ if (!Number.isInteger(contextWindow) || contextWindow <= 0) {
868
+ invalid(provider, `model "${entry.id}" contextWindow must be a positive integer`)
869
+ }
870
+ const maxTokens = entry.maxTokens ?? base?.maxTokens ?? request.defaultMaxTokens
871
+ if (!Number.isInteger(maxTokens) || maxTokens <= 0) {
872
+ invalid(provider, `model "${entry.id}" maxTokens must be a positive integer`)
873
+ }
874
+ // Only a value the profile named is a deployment choice; the catalog's is
875
+ // the model's capability and stays out of request defaults.
876
+ if (entry.maxTokens !== undefined) configuredMaxTokens.set(entry.id, entry.maxTokens)
877
+ return {
878
+ // The installed entry lays the floor, and the fields below override it.
879
+ // Enumerating instead would silently drop every `Model` field this
880
+ // package does not model — reasoning-level spellings, compatibility
881
+ // quirks, model headers, and whatever a pi-ai upgrade adds next. Spread,
882
+ // never enumerate.
883
+ ...base,
884
+ id: entry.id,
885
+ name: entry.name ?? base?.name ?? entry.id,
886
+ api,
887
+ provider,
888
+ baseUrl,
889
+ input: declaredInput(entry.input) ?? base?.input ?? [...request.defaultInput],
890
+ cost: base?.cost ?? NO_COST,
891
+ contextWindow,
892
+ maxTokens,
893
+ ...resolveModelReasoning(provider, entry, base),
894
+ ...resolveModelCompat(provider, entry, request.compat, base, api),
895
+ }
896
+ })
897
+ // Per field, not per block: a route may default a switch its completions
898
+ // models take beside one only its anthropic models do, and neither should
899
+ // fail for the other's sake. What is refused is a route default no model on
900
+ // the route could ever read, which is a route that will not behave as written.
901
+ for (const [field] of configuredCompatEntries(request.compat)) {
902
+ const takers = compatProtocols(field)
903
+ if (models.some(model => takers.includes(model.api))) continue
904
+ invalid(provider, `sets compat "${field}", but no model on the route speaks a protocol that takes it;`
905
+ + ` it exists on ${takers.join(', ')}`)
906
+ }
907
+ return { models, configuredMaxTokens }
908
+ }