@cherrystudio/provider-registry 0.0.0-stage → 0.1.1

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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +166 -2
  3. package/data/models.json +22694 -0
  4. package/data/provider-models.json +41090 -0
  5. package/data/providers.json +2722 -0
  6. package/dist/bundled-catalog.cjs +12 -0
  7. package/dist/bundled-catalog.d.cts +5 -0
  8. package/dist/bundled-catalog.d.mts +5 -0
  9. package/dist/bundled-catalog.mjs +11 -0
  10. package/dist/bundled-models--P3b6A6o.mjs +25220 -0
  11. package/dist/bundled-models-uS9C7ldF.cjs +25224 -0
  12. package/dist/bundled-models.cjs +3 -0
  13. package/dist/bundled-models.d.cts +5 -0
  14. package/dist/bundled-models.d.mts +5 -0
  15. package/dist/bundled-models.mjs +2 -0
  16. package/dist/bundled-provider-models-CT2Ha5w2.cjs +37586 -0
  17. package/dist/bundled-provider-models-y-RYjBUH.mjs +37582 -0
  18. package/dist/bundled-provider-models.cjs +3 -0
  19. package/dist/bundled-provider-models.d.cts +5 -0
  20. package/dist/bundled-provider-models.d.mts +5 -0
  21. package/dist/bundled-provider-models.mjs +2 -0
  22. package/dist/bundled-providers-BcszhEy2.mjs +2172 -0
  23. package/dist/bundled-providers-Bv_iM9AO.cjs +2176 -0
  24. package/dist/bundled-providers.cjs +3 -0
  25. package/dist/bundled-providers.d.cts +5 -0
  26. package/dist/bundled-providers.d.mts +5 -0
  27. package/dist/bundled-providers.mjs +2 -0
  28. package/dist/catalog-DElG_l-8.mjs +369 -0
  29. package/dist/catalog-DurdOhD0.cjs +442 -0
  30. package/dist/catalog-XJyrtwI9.d.cts +53 -0
  31. package/dist/catalog-XJyrtwI9.d.mts +53 -0
  32. package/dist/index.cjs +2620 -0
  33. package/dist/index.d.cts +3932 -0
  34. package/dist/index.d.mts +3932 -0
  35. package/dist/index.mjs +2549 -0
  36. package/dist/model-NFEbqWMs.mjs +665 -0
  37. package/dist/model-kwW2vWDI.cjs +855 -0
  38. package/dist/provider-D14qMZsE.mjs +431 -0
  39. package/dist/provider-DkSIaann.cjs +527 -0
  40. package/dist/provider-models-BjKC53mr.cjs +85 -0
  41. package/dist/provider-models-BkxD9T17.mjs +67 -0
  42. package/dist/provider-models-DuZBXGmP.d.cts +28692 -0
  43. package/dist/provider-models-DuZBXGmP.d.mts +28692 -0
  44. package/dist/registry-BjETdicx.d.mts +35 -0
  45. package/dist/registry-Di5ULW6Z.d.cts +35 -0
  46. package/dist/registry-loader.cjs +170 -0
  47. package/dist/registry-loader.d.cts +64 -0
  48. package/dist/registry-loader.d.mts +64 -0
  49. package/dist/registry-loader.mjs +156 -0
  50. package/package.json +146 -3
@@ -0,0 +1,665 @@
1
+ import * as z from "zod";
2
+ //#region src/schemas/enums.ts
3
+ /**
4
+ * Canonical enum definitions for the registry system.
5
+ *
6
+ * These are the SINGLE SOURCE OF TRUTH for all enum types.
7
+ * Uses `as const` objects with kebab-case string values for debuggability.
8
+ *
9
+ * - registry/schemas/ uses these via z.enum()
10
+ * - shared/data/types/ re-exports these directly
11
+ */
12
+ const ENDPOINT_TYPE = {
13
+ ANTHROPIC_MESSAGES: "anthropic-messages",
14
+ GOOGLE_GENERATE_CONTENT: "google-generate-content",
15
+ JINA_RERANK: "jina-rerank",
16
+ OLLAMA_CHAT: "ollama-chat",
17
+ OLLAMA_GENERATE: "ollama-generate",
18
+ OPENAI_AUDIO_TRANSCRIPTION: "openai-audio-transcription",
19
+ OPENAI_AUDIO_TRANSLATION: "openai-audio-translation",
20
+ OPENAI_CHAT_COMPLETIONS: "openai-chat-completions",
21
+ OPENAI_EMBEDDINGS: "openai-embeddings",
22
+ OPENAI_IMAGE_EDIT: "openai-image-edit",
23
+ OPENAI_IMAGE_GENERATION: "openai-image-generation",
24
+ OPENAI_RESPONSES: "openai-responses",
25
+ OPENAI_TEXT_COMPLETIONS: "openai-text-completions",
26
+ OPENAI_TEXT_TO_SPEECH: "openai-text-to-speech",
27
+ OPENAI_VIDEO_GENERATION: "openai-video-generation"
28
+ };
29
+ const MODEL_CAPABILITY = {
30
+ FUNCTION_CALL: "function-call",
31
+ REASONING: "reasoning",
32
+ IMAGE_RECOGNITION: "image-recognition",
33
+ IMAGE_GENERATION: "image-generation",
34
+ AUDIO_RECOGNITION: "audio-recognition",
35
+ AUDIO_GENERATION: "audio-generation",
36
+ EMBEDDING: "embedding",
37
+ RERANK: "rerank",
38
+ AUDIO_TRANSCRIPT: "audio-transcript",
39
+ VIDEO_RECOGNITION: "video-recognition",
40
+ VIDEO_GENERATION: "video-generation",
41
+ STRUCTURED_OUTPUT: "structured-output",
42
+ FILE_INPUT: "file-input",
43
+ CODE_EXECUTION: "code-execution",
44
+ FILE_SEARCH: "file-search",
45
+ COMPUTER_USE: "computer-use"
46
+ };
47
+ const SERVER_TOOL = {
48
+ WEB_SEARCH: "web-search",
49
+ URL_CONTEXT: "url-context"
50
+ };
51
+ const SERVER_TOOL_MODEL_SCOPE = {
52
+ /** The provider supplies the tool for every chat model it serves. */
53
+ ALL_CHAT_MODELS: "all-chat-models",
54
+ /** Tool-specific model eligibility must also pass. */
55
+ MODEL_DEPENDENT: "model-dependent"
56
+ };
57
+ /**
58
+ * The closed vocabulary of image-generation param keys. A model declares a
59
+ * subset of these under `imageGeneration.modes[mode].supports` (see
60
+ * `ImageModeDefSchema`), and they flow end-to-end as `painting.params` keys.
61
+ *
62
+ * Unlike the other enums, the VALUES are camelCase on purpose: they ARE the
63
+ * runtime param-bag keys and must match `painting.params`/registry data
64
+ * verbatim — do NOT kebab-case them (it would break the wire contract).
65
+ *
66
+ * This is the single source of truth that ties together the registry schema,
67
+ * the form's `KEY_LABELS`/`OPTION_LABELS`, and `canonicalGenerate`'s
68
+ * `POSITIONAL_RENAME`. Adding a new canonical param is a deliberate change:
69
+ * add the member here, give it a label in `KEY_LABELS`, and declare it on the
70
+ * relevant models in registry data.
71
+ */
72
+ const CANONICAL_PARAM_KEY = {
73
+ ADD_WATERMARK: "addWatermark",
74
+ ASPECT_RATIO: "aspectRatio",
75
+ BACKGROUND: "background",
76
+ BOTTOM_SCALE: "bottomScale",
77
+ CFG: "cfg",
78
+ CUSTOM_SIZE: "customSize",
79
+ DETAIL: "detail",
80
+ ENABLE_INTERLEAVE: "enableInterleave",
81
+ FUNCTION: "function",
82
+ GUIDANCE_SCALE: "guidanceScale",
83
+ IMAGE_RESOLUTION: "imageResolution",
84
+ IMAGE_WEIGHT: "imageWeight",
85
+ IS_SKETCH: "isSketch",
86
+ LEFT_SCALE: "leftScale",
87
+ MAGIC_PROMPT_OPTION: "magicPromptOption",
88
+ MAX_IMAGES: "maxImages",
89
+ MODERATION: "moderation",
90
+ NEGATIVE_PROMPT: "negativePrompt",
91
+ NUM_IMAGES: "numImages",
92
+ NUM_INFERENCE_STEPS: "numInferenceSteps",
93
+ OUTPUT_FORMAT: "outputFormat",
94
+ OUTPUT_COMPRESSION: "outputCompression",
95
+ PERSON_GENERATION: "personGeneration",
96
+ PROMPT_ENHANCEMENT: "promptEnhancement",
97
+ PROMPT_EXTEND: "promptExtend",
98
+ QUALITY: "quality",
99
+ RESOLUTION: "resolution",
100
+ REF_MODE: "refMode",
101
+ REF_STRENGTH: "refStrength",
102
+ RENDERING_SPEED: "renderingSpeed",
103
+ RESEMBLANCE: "resemblance",
104
+ RIGHT_SCALE: "rightScale",
105
+ SAFETY_TOLERANCE: "safetyTolerance",
106
+ SEED: "seed",
107
+ SEQUENTIAL_IMAGE_GENERATION: "sequentialImageGeneration",
108
+ SIZE: "size",
109
+ SOURCE_LANG: "sourceLang",
110
+ STRENGTH: "strength",
111
+ STYLE: "style",
112
+ STYLE_TYPE: "styleType",
113
+ TARGET_LANG: "targetLang",
114
+ THINKING_MODE: "thinkingMode",
115
+ TOP_SCALE: "topScale",
116
+ UPSCALE_FACTOR: "upscaleFactor"
117
+ };
118
+ const MODALITY = {
119
+ TEXT: "text",
120
+ IMAGE: "image",
121
+ AUDIO: "audio",
122
+ VIDEO: "video",
123
+ VECTOR: "vector"
124
+ };
125
+ const CURRENCY = {
126
+ USD: "USD",
127
+ CNY: "CNY"
128
+ };
129
+ const REASONING_EFFORT = {
130
+ NONE: "none",
131
+ MINIMAL: "minimal",
132
+ LOW: "low",
133
+ MEDIUM: "medium",
134
+ HIGH: "high",
135
+ XHIGH: "xhigh",
136
+ MAX: "max",
137
+ ULTRA: "ultra",
138
+ AUTO: "auto"
139
+ };
140
+ /**
141
+ * Intensity ladder for nearest-match fallback when a persisted effort is not
142
+ * in a model's vocabulary (model switch / vocabulary shrink). `auto` is
143
+ * deliberately last — it is a mode, not an intensity.
144
+ */
145
+ const REASONING_EFFORT_ORDER = [
146
+ "none",
147
+ "minimal",
148
+ "low",
149
+ "medium",
150
+ "high",
151
+ "xhigh",
152
+ "max",
153
+ "ultra",
154
+ "auto"
155
+ ];
156
+ /** Extract the value tuple from a const object for use with z.enum(). */
157
+ function objectValues(obj) {
158
+ return Object.values(obj);
159
+ }
160
+ //#endregion
161
+ //#region src/schemas/common.ts
162
+ /**
163
+ * Common type definitions for the registry system
164
+ * Shared across model, provider, and override schemas
165
+ */
166
+ const EndpointTypeSchema = z.enum(objectValues(ENDPOINT_TYPE));
167
+ const ModelIdSchema = z.string().min(1);
168
+ const ProviderIdSchema = z.string().min(1);
169
+ /**
170
+ * Opaque change-detection token for a generated artifact. The generator stamps it with a hash of the
171
+ * artifact's content, so equal content ⇒ equal version and ANY content change ⇒ new version — that's
172
+ * the contract seeders rely on (`SeedRunner` skips a seeder whose journal version matches). Older
173
+ * artifacts carried a generation date; treat the value as opaque, never parse it.
174
+ */
175
+ const VersionSchema = z.string().min(1);
176
+ z.iso.datetime();
177
+ const NumericRangeSchema = z.object({
178
+ min: z.number(),
179
+ max: z.number()
180
+ }).refine((r) => r.min <= r.max, { message: "min must be less than or equal to max" });
181
+ z.object({
182
+ min: z.string(),
183
+ max: z.string()
184
+ });
185
+ const ZodCurrencySchema = z.enum(objectValues(CURRENCY)).optional();
186
+ const PricePerTokenSchema = z.object({
187
+ perMillionTokens: z.number().nonnegative().nullable(),
188
+ currency: ZodCurrencySchema
189
+ });
190
+ const MetadataSchema = z.record(z.string(), z.unknown()).optional();
191
+ //#endregion
192
+ //#region src/schemas/forwardCompat.ts
193
+ /**
194
+ * Forward compatibility for the wire contract — a catalog emitted by a NEWER
195
+ * generator must stay readable by THIS client.
196
+ *
197
+ * `z.object` already strips unknown keys, so additive fields were always safe.
198
+ * The remaining break was vocabulary growth: one unknown member of a closed
199
+ * `z.enum` failed its whole document, which is why every new effort/capability
200
+ * used to force a {@link REGISTRY_SCHEMA_VERSION} bump. {@link looseArray}
201
+ * degrades that failure to the smallest unit that can be dropped:
202
+ *
203
+ * - inside a list of enum values → drop the unknown member, keep the entry
204
+ * - inside a list of entries (models/providers/overrides) → drop the entry
205
+ * the client cannot represent, keep the rest of the catalog
206
+ *
207
+ * Values this client cannot *execute* (a new adapter family, endpoint type,
208
+ * wire behavior) are still gated — by `REGISTRY_MIN_APP_VERSION`, not by
209
+ * parse failure.
210
+ */
211
+ /**
212
+ * Like `z.array(item)`, but silently drops members that fail to parse.
213
+ *
214
+ * @param min Minimum members required AFTER dropping — the entry is rejected
215
+ * (and dropped by its own enclosing `looseArray`) when too few survive.
216
+ */
217
+ function looseArray(item, { min } = {}) {
218
+ const array = z.array(z.unknown()).transform((values) => values.reduce((kept, value) => {
219
+ const parsed = item.safeParse(value);
220
+ if (parsed.success) kept.push(parsed.data);
221
+ return kept;
222
+ }, []));
223
+ return min === void 0 ? array : array.refine((values) => values.length >= min, { message: `expected at least ${min} recognized value(s)` });
224
+ }
225
+ //#endregion
226
+ //#region src/schemas/model.ts
227
+ /**
228
+ * Model configuration schema definitions
229
+ * Defines the structure for model metadata, capabilities, and configurations
230
+ */
231
+ const ModalitySchema = z.enum(objectValues(MODALITY));
232
+ const ModelCapabilityTypeSchema = z.enum(objectValues(MODEL_CAPABILITY));
233
+ const CanonicalParamKeySchema = z.enum(objectValues(CANONICAL_PARAM_KEY));
234
+ const ThinkingTokenLimitsSchema = z.object({
235
+ min: z.number().nonnegative().optional(),
236
+ max: z.number().positive().optional(),
237
+ default: z.number().nonnegative().optional()
238
+ }).refine((d) => d.min == null === (d.max == null), { message: "min and max must be both present or both absent" }).refine((d) => d.min == null || d.max == null || d.min <= d.max, { message: "min must be less than or equal to max" });
239
+ /** Reasoning effort levels shared across providers */
240
+ const ReasoningEffortSchema = z.enum(objectValues(REASONING_EFFORT));
241
+ /**
242
+ * Per-model reasoning control declaration — the SOURCE from which the legacy
243
+ * pair (`supportedEfforts` / `thinkingTokenLimits`) is DERIVED at generation
244
+ * time (`deriveLegacyReasoningFields`). Kinds align 1:1 with models.dev
245
+ * `reasoning_options` so upstream ingestion is lossless.
246
+ */
247
+ const ReasoningControlSchema = z.discriminatedUnion("kind", [
248
+ z.object({
249
+ /** Discrete effort knob. `values` is the model's intrinsic vocabulary, in
250
+ * UI display order. The active endpoint profile may map those values to a
251
+ * narrower wire vocabulary (`'none'` present ⇔ reasoning can be disabled). */
252
+ kind: z.literal("effort"),
253
+ values: looseArray(ReasoningEffortSchema, { min: 1 }),
254
+ default: ReasoningEffortSchema.optional()
255
+ }),
256
+ z.object({
257
+ /** Numeric thinking-token budget knob. */
258
+ kind: z.literal("budget"),
259
+ min: z.number().nonnegative(),
260
+ max: z.number().positive(),
261
+ default: z.number().nonnegative().optional()
262
+ }),
263
+ z.object({
264
+ /** On/off only — no effort levels, no budget. */
265
+ kind: z.literal("toggle"),
266
+ default: z.boolean().optional()
267
+ })
268
+ ]);
269
+ /**
270
+ * Which dialect variant of its endpoint's native protocol a model generation
271
+ * speaks. NOT a wire format — the format still follows the serving endpoint.
272
+ * This only disambiguates when one protocol carries two mutually exclusive
273
+ * parameter shapes across model generations, and the older generation
274
+ * hard-rejects the newer field:
275
+ * - `google-generate-content`: Gemini 3 `thinkingLevel` vs 2.x `thinkingBudget`
276
+ * - `anthropic-messages`: Claude 4.6+ `thinking.type=adaptive` vs <=4.5
277
+ * `thinking.type=enabled` + `budget_tokens`
278
+ *
279
+ * Opus 5.5 always uses adaptive thinking; Sonnet 5.5 also supports `between_tools`.
280
+ * It has effect only where the format profile declares the matching alternative, so open-weight models on openai-compatible endpoints are
281
+ * unaffected (their dialect really does follow the provider — see the rule
282
+ * on {@link ReasoningFamilyRuleSchema}).
283
+ */
284
+ const ReasoningWireDialectSchema = z.enum([
285
+ "effort",
286
+ "budget",
287
+ "adaptive-always",
288
+ "adaptive-between-tools"
289
+ ]);
290
+ /**
291
+ * A creator-declared reasoning FAMILY rule — ID-pattern knowledge as DATA
292
+ * (#16598). Creators declare these next to their models (`Creator.
293
+ * reasoningFamilies`); generation compiles them into per-model `controls`
294
+ * and the shipped `patterns/reasoning-families.gen.ts` artifact consumed by
295
+ * the zero-knowledge matchers.
296
+ *
297
+ * Every rule is one of two semantic kinds:
298
+ * - PROFILE (default): "this pattern IS a reasoning SKU (with knobs K)".
299
+ * Membership is implied — the ingest gate (`inferReasoningMembership`)
300
+ * accepts any id a profile rule matches. A profile may carry no knobs at
301
+ * all (a fixed reasoner: reasons, nothing to tune).
302
+ * - TEMPLATE (`template: true`): "models of this family that DO reason use
303
+ * knob shape K". Deliberately broader than membership (e.g. the `^qwen`
304
+ * toggle) — contributes knobs only, never membership; SKUs are admitted
305
+ * by profile rules, the generic id shapes, or a declared capability.
306
+ *
307
+ * A rule carries MODEL KNOBS ONLY — never a reasoning format/wire field:
308
+ * open-weight models are served by many providers and the serialization
309
+ * dialect follows the serving endpoint, not a runtime model-id match. The one
310
+ * narrow exception is `wireDialect`, which does NOT name a format: it picks
311
+ * between the generation-dialects a single first-party protocol defines
312
+ * for itself (see {@link ReasoningWireDialectSchema}). That fact is the
313
+ * vendor's own API contract and holds across every provider proxying it, so
314
+ * it belongs to the model, not the endpoint.
315
+ *
316
+ * Matching: `pattern` is a case-insensitive regex SOURCE tested against the
317
+ * lowercased, namespace-stripped id (vocabulary part) and the raw id string
318
+ * (budget part — token-limit callers pass `provider::model` unique ids, so
319
+ * budget-only rules should stay unanchored). Patterns must be
320
+ * vendor-specific (same discipline as `idPrefixes`). Within a creator,
321
+ * declaration order is match priority — first rule wins per part.
322
+ */
323
+ const compilableRegexSource = z.string().refine((source) => {
324
+ try {
325
+ new RegExp(source, "i");
326
+ return true;
327
+ } catch {
328
+ return false;
329
+ }
330
+ }, { message: "pattern must be a valid regular expression" });
331
+ z.object({
332
+ /** Case-insensitive regex source. Must compile. */
333
+ pattern: compilableRegexSource,
334
+ /** Intrinsic effort vocabulary, in UI display order. */
335
+ effort: looseArray(ReasoningEffortSchema, { min: 1 }).optional(),
336
+ /**
337
+ * Thinking on/off switch. `false` is an EXPLICIT "always-on, no switch"
338
+ * declaration that stops broader family rules below from applying
339
+ * (e.g. qwen3 `*-thinking` SKUs vs the generic qwen toggle).
340
+ */
341
+ toggle: z.boolean().optional(),
342
+ /** Thinking-token budget range. */
343
+ budget: z.object({
344
+ min: z.number().nonnegative(),
345
+ max: z.number().positive()
346
+ }).refine((b) => b.min <= b.max, { message: "budget min must be <= max" }).optional(),
347
+ /** Knob-shape template for a broad family — contributes NO membership. */
348
+ template: z.literal(true).optional(),
349
+ /** Native-protocol dialect for this model generation. */
350
+ wireDialect: ReasoningWireDialectSchema.optional()
351
+ }).refine((rule) => rule.template !== true || rule.effort !== void 0 || rule.toggle !== void 0 || rule.budget !== void 0 || rule.wireDialect !== void 0, { message: "a template rule with no knobs declares nothing — drop it or make it a profile" });
352
+ const CommonReasoningFieldsSchema = {
353
+ /** Source of truth for the model's reasoning knobs (at most one per kind).
354
+ * The legacy fields below are DERIVED from it when present. */
355
+ controls: looseArray(ReasoningControlSchema).optional(),
356
+ thinkingTokenLimits: ThinkingTokenLimitsSchema.optional(),
357
+ supportedEfforts: looseArray(ReasoningEffortSchema).optional(),
358
+ /** What the API does when no reasoning param is sent. */
359
+ defaultEffort: ReasoningEffortSchema.optional(),
360
+ /** Native-protocol dialect this model generation speaks, when its protocol
361
+ * defines more than one. Selects the endpoint profile's wire variant. */
362
+ wireDialect: ReasoningWireDialectSchema.optional()
363
+ };
364
+ /**
365
+ * Reasoning support schema — describes model-level reasoning capabilities.
366
+ *
367
+ * This only captures WHAT the model supports (effort levels, token limits).
368
+ * HOW to invoke reasoning is defined by the provider's reasoning format
369
+ * (see provider.ts ProviderReasoningFormatSchema).
370
+ */
371
+ const ReasoningSupportSchema = z.object({ ...CommonReasoningFieldsSchema }).superRefine((r, ctx) => {
372
+ const kinds = (r.controls ?? []).map((c) => c.kind);
373
+ if (new Set(kinds).size !== kinds.length) ctx.addIssue({
374
+ code: "custom",
375
+ message: "at most one reasoning control per kind"
376
+ });
377
+ for (const c of r.controls ?? []) {
378
+ if (c.kind === "effort" && c.default != null && !c.values.includes(c.default)) ctx.addIssue({
379
+ code: "custom",
380
+ message: "effort default must be a member of values"
381
+ });
382
+ if (c.kind === "budget" && (c.min > c.max || c.default != null && (c.default < c.min || c.default > c.max))) ctx.addIssue({
383
+ code: "custom",
384
+ message: "budget range must satisfy min <= default <= max"
385
+ });
386
+ }
387
+ });
388
+ /**
389
+ * Image-generation support describes what controls a model accepts, in a
390
+ * shape uniform across all models so the painting page can render the
391
+ * right controls without per-vendor branching.
392
+ *
393
+ * `supports` is a flat map of canonical param keys to widget specs — the
394
+ * renderer dispatches by `spec.type`. `size` / `numImages` / `customSize`
395
+ * are no longer top-level fields; they're entries inside `supports` like
396
+ * everything else. `modes` is `Record<Mode, ModeDef>` (always an object,
397
+ * never an array) so single-mode models declare `{ generate: { ... } }`
398
+ * uniformly; multi-mode models with different params per mode (Ideogram
399
+ * V_*) declare each mode's complete `ModeDef` explicitly.
400
+ *
401
+ * Vendor wire transforms (snake_case keys, `'ASPECT_X_Y' → 'X:Y'` strings,
402
+ * `Uint8Array → base64`) live in the AI SDK image-model adapters under
403
+ * `aiCore/provider/custom/`; this schema carries canonical names only.
404
+ * Per-mode transport routing (PPIO endpoint URL + sync/async flag) lives
405
+ * on `ModeDef.vendorTransport` so it travels with the registry data.
406
+ */
407
+ const ImageGenerationModeSchema = z.enum([
408
+ "generate",
409
+ "edit",
410
+ "remix",
411
+ "upscale",
412
+ "merge"
413
+ ]);
414
+ const SwitchSpecSchema = z.object({
415
+ type: z.literal("switch"),
416
+ default: z.boolean().optional()
417
+ });
418
+ const EnumSpecSchema = z.object({
419
+ type: z.literal("enum"),
420
+ options: z.array(z.string()).min(1),
421
+ default: z.string().optional(),
422
+ /** `'chips'` for compact button rows (size / aspectRatio / imageResolution);
423
+ * defaults to `'select'` (dropdown) when omitted. */
424
+ render: z.enum(["select", "chips"]).optional(),
425
+ columns: z.number().int().positive().optional()
426
+ });
427
+ const RangeSpecSchema = z.object({
428
+ type: z.literal("range"),
429
+ min: z.number(),
430
+ max: z.number(),
431
+ default: z.number().optional(),
432
+ /** Omitted means the numeric input accepts any precision; renderers may
433
+ * still choose an interaction step for controls such as sliders. */
434
+ step: z.number().optional()
435
+ }).refine((r) => r.min <= r.max, { message: "min must be ≤ max" });
436
+ const RangeIntSpecSchema = z.object({
437
+ type: z.literal("range"),
438
+ min: z.number().int(),
439
+ max: z.number().int(),
440
+ default: z.number().int().optional(),
441
+ step: z.number().int().positive().default(1)
442
+ }).refine((r) => r.min <= r.max, { message: "min must be ≤ max" });
443
+ const SizeSpecSchema = z.object({
444
+ type: z.literal("size"),
445
+ /** Both width and height share this bound. */
446
+ minSide: z.number(),
447
+ maxSide: z.number(),
448
+ /** When set, the size widget only renders when the named enum is at
449
+ * `'custom'` (CogView pattern: pick the `'custom'` chip on the size
450
+ * enum to reveal width/height inputs). */
451
+ pairedEnumKey: z.string().optional()
452
+ });
453
+ const TextSpecSchema = z.object({
454
+ type: z.literal("text"),
455
+ multiline: z.boolean().optional()
456
+ });
457
+ const SupportSpecSchema = z.discriminatedUnion("type", [
458
+ SwitchSpecSchema,
459
+ EnumSpecSchema,
460
+ RangeSpecSchema,
461
+ SizeSpecSchema,
462
+ TextSpecSchema
463
+ ]);
464
+ const INTEGER_RANGE_PARAM_KEYS = [
465
+ CANONICAL_PARAM_KEY.NUM_IMAGES,
466
+ CANONICAL_PARAM_KEY.MAX_IMAGES,
467
+ CANONICAL_PARAM_KEY.NUM_INFERENCE_STEPS,
468
+ CANONICAL_PARAM_KEY.SAFETY_TOLERANCE,
469
+ CANONICAL_PARAM_KEY.OUTPUT_COMPRESSION
470
+ ];
471
+ const ImageSupportsSchema = z.partialRecord(CanonicalParamKeySchema, SupportSpecSchema).transform((supports, ctx) => {
472
+ const normalized = { ...supports };
473
+ for (const key of INTEGER_RANGE_PARAM_KEYS) {
474
+ const spec = supports[key];
475
+ if (spec === void 0) continue;
476
+ const result = RangeIntSpecSchema.safeParse(spec);
477
+ if (result.success) normalized[key] = result.data;
478
+ else for (const issue of result.error.issues) ctx.addIssue({
479
+ ...issue,
480
+ path: [key, ...issue.path]
481
+ });
482
+ }
483
+ return normalized;
484
+ });
485
+ /**
486
+ * Per-mode model capability declaration. The renderer iterates `supports`
487
+ * and dispatches `specToField` by `spec.type`; no per-vendor logic. `supports`
488
+ * keys are drawn from the closed `CanonicalParamKey` vocabulary (see
489
+ * `CANONICAL_PARAM_KEY` in `enums.ts`) — an unknown key fails to parse, and
490
+ * the same vocabulary types the form's `KEY_LABELS`/`OPTION_LABELS` and
491
+ * `canonicalGenerate`'s `POSITIONAL_RENAME`, so a typo/rename is a compile or
492
+ * parse error rather than a silent raw-key render. Adding a new canonical
493
+ * param: (1) add the member to `CANONICAL_PARAM_KEY`, (2) add a label to
494
+ * `KEY_LABELS` in `imageGenerationToFields`, (3) declare it on models'
495
+ * `supports`.
496
+ *
497
+ * `vendorTransport` carries PPIO-style per-model endpoint routing — the
498
+ * AI SDK adapter for that vendor reads endpoint + isSync off the registry
499
+ * instead of a hand-maintained routing table.
500
+ */
501
+ const ImageModeDefSchema = z.object({
502
+ supports: ImageSupportsSchema,
503
+ maxInputImages: z.number().int().positive().optional(),
504
+ vendorTransport: z.object({
505
+ endpoint: z.string().regex(/^\/(?!\/)/, "vendor transport endpoint must be a root-relative path, not a URL"),
506
+ isSync: z.boolean().optional()
507
+ }).optional(),
508
+ /**
509
+ * When `false`, the generic painting pipeline does NOT enforce a non-empty
510
+ * `painting.prompt` before submitting. Set on models like DashScope's
511
+ * `qwen-mt-image` (image-text translation: no prompt, just source/target
512
+ * languages) or PPIO's image-upscaler / image-eraser / image-remove-bg
513
+ * variants. Default is `true` (prompt required).
514
+ */
515
+ requirePrompt: z.boolean().optional()
516
+ });
517
+ const ImageGenerationSupportSchema = z.object({ modes: z.partialRecord(ImageGenerationModeSchema, ImageModeDefSchema) });
518
+ const ParameterSupportSchema = z.object({
519
+ temperature: z.object({
520
+ supported: z.boolean(),
521
+ range: NumericRangeSchema.optional()
522
+ }).default({ supported: true }),
523
+ topP: z.object({
524
+ supported: z.boolean(),
525
+ range: NumericRangeSchema.optional()
526
+ }).default({ supported: true }),
527
+ topK: z.object({
528
+ supported: z.boolean(),
529
+ range: NumericRangeSchema.optional()
530
+ }).default({ supported: false }),
531
+ frequencyPenalty: z.boolean().default(true),
532
+ presencePenalty: z.boolean().default(true),
533
+ maxTokens: z.boolean().default(true),
534
+ stopSequences: z.boolean().default(true),
535
+ systemMessage: z.boolean().default(true)
536
+ });
537
+ /**
538
+ * Model pricing configuration.
539
+ *
540
+ * Pricing tiers based on actual provider billing models:
541
+ * - input/output per-token: OpenAI, Anthropic, Google, all major LLM providers
542
+ * - cacheRead/cacheWrite: Anthropic prompt caching, OpenAI cached tokens
543
+ * - perImage: DALL-E (per-image), Midjourney (per-image)
544
+ * - perMinute: Whisper, ElevenLabs (per-minute audio billing)
545
+ */
546
+ const ModelPricingObjectSchema = z.object({
547
+ input: PricePerTokenSchema,
548
+ output: PricePerTokenSchema,
549
+ cacheRead: PricePerTokenSchema.optional(),
550
+ cacheWrite: PricePerTokenSchema.optional(),
551
+ inputTokenTiers: z.array(z.object({
552
+ minInputTokens: z.number().int().positive().refine(Number.isSafeInteger),
553
+ input: PricePerTokenSchema,
554
+ output: PricePerTokenSchema,
555
+ cacheRead: PricePerTokenSchema.optional(),
556
+ cacheWrite: PricePerTokenSchema.optional()
557
+ })).optional(),
558
+ perImage: z.object({
559
+ price: z.number(),
560
+ currency: ZodCurrencySchema,
561
+ unit: z.enum(["image", "pixel"]).optional()
562
+ }).optional(),
563
+ perMinute: z.object({
564
+ price: z.number(),
565
+ currency: ZodCurrencySchema
566
+ }).optional()
567
+ });
568
+ function validateInputTokenPricingTiers(pricing, ctx) {
569
+ for (let index = 1; index < (pricing.inputTokenTiers?.length ?? 0); index++) if (pricing.inputTokenTiers[index].minInputTokens <= pricing.inputTokenTiers[index - 1].minInputTokens) ctx.addIssue({
570
+ code: "custom",
571
+ path: [
572
+ "inputTokenTiers",
573
+ index,
574
+ "minInputTokens"
575
+ ],
576
+ message: "minInputTokens must be strictly increasing"
577
+ });
578
+ if (!pricing.inputTokenTiers?.length) return;
579
+ const rates = [
580
+ ...pricing.input ? [{
581
+ rate: pricing.input,
582
+ path: ["input"]
583
+ }] : [],
584
+ ...pricing.output ? [{
585
+ rate: pricing.output,
586
+ path: ["output"]
587
+ }] : [],
588
+ ...pricing.cacheRead ? [{
589
+ rate: pricing.cacheRead,
590
+ path: ["cacheRead"]
591
+ }] : [],
592
+ ...pricing.cacheWrite ? [{
593
+ rate: pricing.cacheWrite,
594
+ path: ["cacheWrite"]
595
+ }] : [],
596
+ ...pricing.inputTokenTiers.flatMap((tier, index) => [
597
+ {
598
+ rate: tier.input,
599
+ path: [
600
+ "inputTokenTiers",
601
+ index,
602
+ "input"
603
+ ]
604
+ },
605
+ {
606
+ rate: tier.output,
607
+ path: [
608
+ "inputTokenTiers",
609
+ index,
610
+ "output"
611
+ ]
612
+ },
613
+ ...tier.cacheRead ? [{
614
+ rate: tier.cacheRead,
615
+ path: [
616
+ "inputTokenTiers",
617
+ index,
618
+ "cacheRead"
619
+ ]
620
+ }] : [],
621
+ ...tier.cacheWrite ? [{
622
+ rate: tier.cacheWrite,
623
+ path: [
624
+ "inputTokenTiers",
625
+ index,
626
+ "cacheWrite"
627
+ ]
628
+ }] : []
629
+ ])
630
+ ];
631
+ const currency = rates[0]?.rate.currency ?? CURRENCY.USD;
632
+ for (const { rate, path } of rates) if ((rate.currency ?? CURRENCY.USD) !== currency) ctx.addIssue({
633
+ code: "custom",
634
+ path: [...path, "currency"],
635
+ message: "pricing currencies must match"
636
+ });
637
+ }
638
+ const ModelPricingSchema = ModelPricingObjectSchema.superRefine(validateInputTokenPricingTiers);
639
+ const PartialModelPricingSchema = ModelPricingObjectSchema.partial().superRefine(validateInputTokenPricingTiers);
640
+ const ModelConfigSchema = z.object({
641
+ id: ModelIdSchema,
642
+ name: z.string(),
643
+ description: z.string().optional(),
644
+ capabilities: looseArray(ModelCapabilityTypeSchema).refine((arr) => new Set(arr).size === arr.length, { message: "Capabilities must be unique" }).optional(),
645
+ inputModalities: looseArray(ModalitySchema).refine((arr) => new Set(arr).size === arr.length, { message: "Input modalities must be unique" }).optional(),
646
+ outputModalities: looseArray(ModalitySchema).refine((arr) => new Set(arr).size === arr.length, { message: "Output modalities must be unique" }).optional(),
647
+ endpointTypes: looseArray(EndpointTypeSchema).optional(),
648
+ contextWindow: z.number().optional(),
649
+ maxOutputTokens: z.number().optional(),
650
+ maxInputTokens: z.number().optional(),
651
+ pricing: ModelPricingSchema.optional(),
652
+ reasoning: ReasoningSupportSchema.optional(),
653
+ parameterSupport: ParameterSupportSchema.optional(),
654
+ imageGeneration: ImageGenerationSupportSchema.optional(),
655
+ family: z.string().optional(),
656
+ ownedBy: z.string().optional(),
657
+ openWeights: z.boolean().optional(),
658
+ metadata: MetadataSchema
659
+ });
660
+ const ModelListSchema = z.object({
661
+ version: VersionSchema,
662
+ models: looseArray(ModelConfigSchema)
663
+ });
664
+ //#endregion
665
+ export { REASONING_EFFORT as C, objectValues as D, SERVER_TOOL_MODEL_SCOPE as E, MODEL_CAPABILITY as S, SERVER_TOOL as T, ZodCurrencySchema as _, ModelConfigSchema as a, ENDPOINT_TYPE as b, PartialModelPricingSchema as c, looseArray as d, EndpointTypeSchema as f, VersionSchema as g, ProviderIdSchema as h, ModelCapabilityTypeSchema as i, ReasoningControlSchema as l, ModelIdSchema as m, ImageGenerationSupportSchema as n, ModelListSchema as o, MetadataSchema as p, ModalitySchema as r, ParameterSupportSchema as s, ImageGenerationModeSchema as t, ReasoningSupportSchema as u, CANONICAL_PARAM_KEY as v, REASONING_EFFORT_ORDER as w, MODALITY as x, CURRENCY as y };