open-model-selector 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.
@@ -0,0 +1,469 @@
1
+ import * as react from 'react';
2
+
3
+ /** Union of all supported model type identifiers. */
4
+ type ModelType = 'text' | 'image' | 'video' | 'inpaint' | 'embedding' | 'tts' | 'asr' | 'upscale';
5
+ interface Deprecation {
6
+ /** ISO 8601 date string indicating when the model is/was deprecated */
7
+ date: string;
8
+ }
9
+ interface BaseModel {
10
+ id: string;
11
+ name: string;
12
+ provider: string;
13
+ created: number;
14
+ type: ModelType;
15
+ description?: string;
16
+ privacy?: 'private' | 'anonymized';
17
+ offline?: boolean;
18
+ betaModel?: boolean;
19
+ /** Whether the user has marked this model as a favorite.
20
+ * CLIENT-SIDE OVERLAY — this field is NEVER returned by any API.
21
+ * It is a UI-layer concern managed via localStorage in the ModelSelector component.
22
+ * Normalizers MUST default this to `false`; the component hydrates the real value
23
+ * from localStorage at render time. */
24
+ is_favorite?: boolean;
25
+ modelSource?: string;
26
+ traits?: string[];
27
+ /** Deprecation info if this model is scheduled for or has been deprecated. */
28
+ deprecation?: Deprecation;
29
+ }
30
+
31
+ interface TextPricing {
32
+ prompt?: number;
33
+ completion?: number;
34
+ cache_input?: number;
35
+ cache_write?: number;
36
+ }
37
+ /** Feature flags indicating what a text model supports (vision, reasoning, function calling, etc.). */
38
+ interface TextCapabilities {
39
+ optimizedForCode?: boolean;
40
+ supportsVision?: boolean;
41
+ supportsReasoning?: boolean;
42
+ supportsFunctionCalling?: boolean;
43
+ supportsResponseSchema?: boolean;
44
+ supportsLogProbs?: boolean;
45
+ supportsAudioInput?: boolean;
46
+ supportsVideoInput?: boolean;
47
+ supportsWebSearch?: boolean;
48
+ quantization?: string;
49
+ }
50
+ /** Default sampling parameter values for a text model. */
51
+ interface TextConstraints {
52
+ temperature?: {
53
+ default: number;
54
+ };
55
+ top_p?: {
56
+ default: number;
57
+ };
58
+ }
59
+ /** A text/chat completion model with context length, pricing, and optional capabilities. */
60
+ interface TextModel extends BaseModel {
61
+ type: 'text';
62
+ context_length: number;
63
+ pricing: TextPricing;
64
+ capabilities?: TextCapabilities;
65
+ constraints?: TextConstraints;
66
+ }
67
+
68
+ interface ImagePricing {
69
+ generation?: number;
70
+ upscale_2x?: number;
71
+ upscale_4x?: number;
72
+ resolutions?: Record<string, number>;
73
+ }
74
+ /** Generation constraints for an image model (prompt limits, aspect ratios, resolutions, etc.). */
75
+ interface ImageConstraints {
76
+ promptCharacterLimit?: number;
77
+ steps?: {
78
+ default: number;
79
+ max: number;
80
+ };
81
+ widthHeightDivisor?: number;
82
+ aspectRatios?: string[];
83
+ defaultAspectRatio?: string;
84
+ resolutions?: string[];
85
+ defaultResolution?: string;
86
+ }
87
+ interface ImageModel extends BaseModel {
88
+ type: 'image';
89
+ pricing: ImagePricing;
90
+ constraints?: ImageConstraints;
91
+ /** Whether web search is supported for this image model.
92
+ * In the Venice API, this field lives at `model_spec.supportsWebSearch`
93
+ * (a sibling of `model_spec.constraints`), NOT inside `model_spec.constraints`. */
94
+ supportsWebSearch?: boolean;
95
+ }
96
+
97
+ /** Generation constraints for a video model (aspect ratios, durations, audio support, etc.). */
98
+ interface VideoConstraints {
99
+ model_type?: 'text-to-video' | 'image-to-video' | 'video';
100
+ aspect_ratios?: string[];
101
+ resolutions?: string[];
102
+ durations?: string[];
103
+ audio?: boolean;
104
+ audio_configurable?: boolean;
105
+ audio_input?: boolean;
106
+ video_input?: boolean;
107
+ }
108
+ /** A video generation model. Note: no pricing data — the Venice API omits it for video models. */
109
+ interface VideoModel extends BaseModel {
110
+ type: 'video';
111
+ constraints?: VideoConstraints;
112
+ model_sets?: string[];
113
+ }
114
+
115
+ interface InpaintPricing {
116
+ generation?: number;
117
+ }
118
+ /** Generation constraints for an inpainting model (aspect ratios, image combining). */
119
+ interface InpaintConstraints {
120
+ aspectRatios?: string[];
121
+ combineImages?: boolean;
122
+ }
123
+ /** An image inpainting/editing model with pricing and generation constraints. */
124
+ interface InpaintModel extends BaseModel {
125
+ type: 'inpaint';
126
+ pricing: InpaintPricing;
127
+ constraints?: InpaintConstraints;
128
+ }
129
+
130
+ interface EmbeddingPricing {
131
+ input?: number;
132
+ output?: number;
133
+ }
134
+ /** A text embedding model with per-token pricing. */
135
+ interface EmbeddingModel extends BaseModel {
136
+ type: 'embedding';
137
+ pricing: EmbeddingPricing;
138
+ }
139
+
140
+ interface TtsPricing {
141
+ input?: number;
142
+ }
143
+ /** A text-to-speech model with pricing and available voice options. */
144
+ interface TtsModel extends BaseModel {
145
+ type: 'tts';
146
+ pricing: TtsPricing;
147
+ voices?: string[];
148
+ }
149
+
150
+ interface AsrPricing {
151
+ per_audio_second?: number;
152
+ }
153
+ /** An automatic speech recognition (speech-to-text) model with per-second pricing. */
154
+ interface AsrModel extends BaseModel {
155
+ type: 'asr';
156
+ pricing: AsrPricing;
157
+ }
158
+
159
+ interface UpscalePricing {
160
+ generation?: number;
161
+ }
162
+ /** An image upscaling model with flat per-generation pricing. */
163
+ interface UpscaleModel extends BaseModel {
164
+ type: 'upscale';
165
+ pricing: UpscalePricing;
166
+ }
167
+
168
+ /** Discriminated union of all supported model types. Use `model.type` to narrow. */
169
+ type AnyModel = TextModel | ImageModel | VideoModel | InpaintModel | EmbeddingModel | TtsModel | AsrModel | UpscaleModel;
170
+
171
+ /** Safely coerce an unknown value to a number. Returns undefined for non-numeric/NaN. */
172
+ declare function toNum(v: unknown): number | undefined;
173
+ /** Extract shared BaseModel fields from a raw API response object.
174
+ * Venice nests most metadata under model_spec; other providers use top-level fields.
175
+ * This helper checks both locations with model_spec taking priority for Venice-specific fields. */
176
+ declare function extractBaseFields(raw: Record<string, unknown>): Omit<BaseModel, 'type'>;
177
+
178
+ /** Known model ID patterns for heuristic type inference.
179
+ * Used when the API response lacks an explicit `type` field (non-Venice providers).
180
+ * Exported so consumers can inspect or extend. */
181
+ declare const MODEL_ID_TYPE_PATTERNS: Array<[RegExp, ModelType]>;
182
+ /** Infer model type from its ID using naming conventions.
183
+ * Returns undefined if no pattern matches (caller should fall back to 'text'). */
184
+ declare function inferTypeFromId(id: string): ModelType | undefined;
185
+
186
+ /** Normalize a raw API response object into a TextModel. */
187
+ declare function normalizeTextModel(raw: Record<string, unknown>): TextModel;
188
+
189
+ /** Normalize a raw API response object into an ImageModel. */
190
+ declare function normalizeImageModel(raw: Record<string, unknown>): ImageModel;
191
+
192
+ /** Normalize a raw API response object into a VideoModel.
193
+ * Note: Video models have NO pricing data from the Venice API. */
194
+ declare function normalizeVideoModel(raw: Record<string, unknown>): VideoModel;
195
+
196
+ /** Normalize a raw API response object into an InpaintModel. */
197
+ declare function normalizeInpaintModel(raw: Record<string, unknown>): InpaintModel;
198
+
199
+ /** Normalize a raw API response object into an EmbeddingModel.
200
+ * Note: Venice embedding models have BOTH input and output pricing. */
201
+ declare function normalizeEmbeddingModel(raw: Record<string, unknown>): EmbeddingModel;
202
+
203
+ /** Normalize a raw API response object into a TtsModel. */
204
+ declare function normalizeTtsModel(raw: Record<string, unknown>): TtsModel;
205
+
206
+ /** Normalize a raw API response object into an AsrModel. */
207
+ declare function normalizeAsrModel(raw: Record<string, unknown>): AsrModel;
208
+
209
+ /** Normalize a raw API response object into an UpscaleModel. */
210
+ declare function normalizeUpscaleModel(raw: Record<string, unknown>): UpscaleModel;
211
+
212
+ /** Function that extracts the array of raw model objects from an API response body. */
213
+ type ResponseExtractor = (body: Record<string, unknown> | unknown[]) => Record<string, unknown>[];
214
+ /** Function that normalizes a single raw model object into an AnyModel. */
215
+ type ModelNormalizer = (raw: Record<string, unknown>) => AnyModel;
216
+ /** Default response extractor handling common API response shapes:
217
+ * - Top-level array → return as-is
218
+ * - `{ data: [...] }` (OpenAI standard) → return data
219
+ * - `{ models: [...] }` → return models
220
+ * - Fallback → empty array */
221
+ declare function defaultResponseExtractor(body: Record<string, unknown> | unknown[]): Record<string, unknown>[];
222
+ /** Default dispatching model normalizer.
223
+ * Uses three-tier type resolution:
224
+ * 1. Explicit `raw.type` field (Venice, custom providers)
225
+ * 2. Heuristic inference from model ID patterns (OpenAI, OpenRouter, etc.)
226
+ * 3. Fallback to 'text' — safe default for most providers */
227
+ declare function defaultModelNormalizer(raw: Record<string, unknown>): AnyModel;
228
+
229
+ /** A fetch-compatible function signature. Used for SSR, testing, or proxy scenarios. */
230
+ type FetchFn = (url: string, init?: RequestInit) => Promise<Response>;
231
+ interface UseModelsProps {
232
+ baseUrl?: string;
233
+ apiKey?: string;
234
+ /** Filter results to a specific model type. If omitted, returns all types. */
235
+ type?: ModelType;
236
+ /**
237
+ * Query parameters to append to the /models endpoint URL.
238
+ *
239
+ * Venice.ai requires `{ type: 'all' }` to return non-text models.
240
+ * Other providers typically need no query params.
241
+ *
242
+ * @default {}
243
+ */
244
+ queryParams?: Record<string, string>;
245
+ fetcher?: FetchFn;
246
+ responseExtractor?: ResponseExtractor;
247
+ normalizer?: ModelNormalizer;
248
+ }
249
+ interface UseModelsResult {
250
+ models: AnyModel[];
251
+ loading: boolean;
252
+ error: Error | null;
253
+ }
254
+ /**
255
+ * Fetches and normalizes AI models from an OpenAI-compatible `/models` endpoint.
256
+ * Handles loading state, errors, abort/timeout, and optional client-side type filtering.
257
+ *
258
+ * @example
259
+ * ```ts
260
+ * const { models, loading, error } = useModels({ baseUrl: "/api/v1", type: "text" })
261
+ * ```
262
+ */
263
+ declare function useModels(props: UseModelsProps): UseModelsResult;
264
+
265
+ /** Sentinel value representing system default model selection */
266
+ declare const SYSTEM_DEFAULT_VALUE: "system_default";
267
+ /**
268
+ * Props for the ModelSelector component.
269
+ *
270
+ * The component can operate in two modes:
271
+ * - **Managed Mode**: Provide `baseUrl` (and optionally `apiKey`) to fetch models from an API
272
+ * - **Controlled Mode**: Provide a static `models` array directly
273
+ */
274
+ interface ModelSelectorProps {
275
+ /**
276
+ * Static list of models to display. When a non-empty array is provided,
277
+ * the internal API fetch is disabled and only these models are shown.
278
+ */
279
+ models?: AnyModel[];
280
+ /** Base URL for the OpenAI-compatible API endpoint (e.g., "https://api.venice.ai/api/v1") */
281
+ baseUrl?: string;
282
+ /** API key for authentication. Warning: This is visible in browser DevTools. Consider using a backend proxy. */
283
+ apiKey?: string;
284
+ /** Filter results to a specific model type. If omitted, returns all types. */
285
+ type?: ModelType;
286
+ /**
287
+ * Query parameters appended to the /models endpoint URL as a query string.
288
+ * For Venice.ai, pass { type: 'text' } or { type: 'all' } to filter models server-side.
289
+ * @default {}
290
+ */
291
+ queryParams?: Record<string, string>;
292
+ /**
293
+ * Custom fetch function for SSR, testing, or proxy scenarios.
294
+ * If not provided, uses global fetch.
295
+ */
296
+ fetcher?: UseModelsProps['fetcher'];
297
+ /**
298
+ * Custom function to extract the raw model array from the API response body.
299
+ * @see UseModelsProps.responseExtractor
300
+ */
301
+ responseExtractor?: UseModelsProps['responseExtractor'];
302
+ /**
303
+ * Custom function to normalize each raw model object into an AnyModel.
304
+ * @see UseModelsProps.normalizer
305
+ */
306
+ normalizer?: UseModelsProps['normalizer'];
307
+ /** Currently selected model ID (controlled component pattern) */
308
+ value?: string;
309
+ /**
310
+ * Callback fired when a model is selected.
311
+ * Receives the model ID and the full model object (or `null` for the system-default sentinel).
312
+ * If omitted, selections are silently ignored (read-only / display-only usage).
313
+ */
314
+ onChange?: (modelId: string, model: AnyModel | null) => void;
315
+ /** Callback fired when a model is favorited/unfavorited. Only relevant if favorites are controlled. */
316
+ onToggleFavorite?: (modelId: string) => void;
317
+ /** Placeholder text shown when no model is selected. @default "Select model..." */
318
+ placeholder?: string;
319
+ /** Current sort order for models. If provided, component operates in controlled mode for sorting. */
320
+ sortOrder?: "name" | "created";
321
+ /** Callback fired when sort order changes. Only relevant if `sortOrder` is controlled. */
322
+ onSortChange?: (order: "name" | "created") => void;
323
+ /** Popover placement relative to the trigger. @default "bottom" */
324
+ side?: "top" | "bottom" | "left" | "right";
325
+ /** Additional CSS class name(s) to apply to the root element */
326
+ className?: string;
327
+ /**
328
+ * Custom localStorage key for persisting favorites in uncontrolled mode.
329
+ * @default "open-model-selector-favorites"
330
+ */
331
+ storageKey?: string;
332
+ /** Whether to show the "Use System Default" option. @default true */
333
+ showSystemDefault?: boolean;
334
+ /**
335
+ * Whether to show deprecated models. Default: true.
336
+ * When false, models with deprecation.date in the past are hidden.
337
+ * Models with future deprecation dates are always shown with a warning.
338
+ */
339
+ showDeprecated?: boolean;
340
+ /** When true, prevents opening the selector and dims the trigger button. */
341
+ disabled?: boolean;
342
+ }
343
+ /**
344
+ * A searchable, accessible model selector combobox for AI models.
345
+ * Supports managed mode (fetches from API) and controlled mode (static model list).
346
+ * Features include favorites, sorting, deprecation warnings, and screen-reader announcements.
347
+ *
348
+ * @example
349
+ * ```tsx
350
+ * <ModelSelector type="text" baseUrl="/api/v1" onChange={(id) => setModel(id)} />
351
+ * ```
352
+ */
353
+ declare const ModelSelector: react.ForwardRefExoticComponent<ModelSelectorProps & react.RefAttributes<HTMLDivElement>>;
354
+
355
+ /**
356
+ * Pre-configured ModelSelector that only displays text/chat models.
357
+ *
358
+ * @example
359
+ * ```tsx
360
+ * <TextModelSelector baseUrl="/api/v1" onChange={(id) => setModel(id)} />
361
+ * ```
362
+ */
363
+ declare const TextModelSelector: react.ForwardRefExoticComponent<Omit<ModelSelectorProps, "type"> & react.RefAttributes<HTMLDivElement>>;
364
+
365
+ /**
366
+ * Pre-configured ModelSelector that only displays image generation models.
367
+ *
368
+ * @example
369
+ * ```tsx
370
+ * <ImageModelSelector baseUrl="/api/v1" onChange={(id) => setModel(id)} />
371
+ * ```
372
+ */
373
+ declare const ImageModelSelector: react.ForwardRefExoticComponent<Omit<ModelSelectorProps, "type"> & react.RefAttributes<HTMLDivElement>>;
374
+
375
+ /**
376
+ * Pre-configured ModelSelector that only displays video generation models.
377
+ *
378
+ * @example
379
+ * ```tsx
380
+ * <VideoModelSelector baseUrl="/api/v1" onChange={(id) => setModel(id)} />
381
+ * ```
382
+ */
383
+ declare const VideoModelSelector: react.ForwardRefExoticComponent<Omit<ModelSelectorProps, "type"> & react.RefAttributes<HTMLDivElement>>;
384
+
385
+ /** Check whether a deprecation date is in the past.
386
+ * Date-only ISO 8601 strings ("2025-01-15") are parsed as UTC midnight per the
387
+ * ES spec, but we normalize explicitly to avoid any cross-engine ambiguity.
388
+ * Returns false for invalid / unparseable date strings. */
389
+ declare function isDeprecated(dateStr: string): boolean;
390
+
391
+ /**
392
+ * Formats a per-token price into a human-readable per-million-tokens string.
393
+ *
394
+ * Accepts the price as a per-token value (matching the OpenAI / OpenRouter
395
+ * convention) and multiplies by 1,000,000 to display the per-million rate.
396
+ *
397
+ * @param value - Price per token as a number or numeric string.
398
+ * Accepts `undefined`, `null`, empty string, or `NaN` — all return `"—"`.
399
+ * @returns A formatted dollar string (e.g. `"$30.00"`).
400
+ * - Returns `"—"` for missing, empty, or non-numeric input.
401
+ * - Uses 2 decimal places when the per-million value is ≥ $0.01.
402
+ * - Uses 6 decimal places when the per-million value is < $0.01
403
+ * (to preserve precision for very cheap models).
404
+ *
405
+ * @example
406
+ * ```ts
407
+ * formatPrice(0.00003) // "$30.00" (per-token → per-million)
408
+ * formatPrice("0.000015") // "$15.00"
409
+ * formatPrice(1e-12) // "$0.000001"
410
+ * formatPrice(undefined) // "—"
411
+ * ```
412
+ *
413
+ */
414
+ declare function formatPrice(value: string | number | undefined | null): string;
415
+ /**
416
+ * Formats a token count into a compact human-readable string.
417
+ *
418
+ * @param tokens - Raw token count (e.g. `128000`, `1_000_000`).
419
+ * @returns A compact string like `"128k"` or `"1M"`.
420
+ * - Values ≥ 1,000,000 are shown in millions (e.g. `"1M"`, `"1.5M"`).
421
+ * - Values < 1,000 are shown as exact numbers (e.g. `500` → `"500"`).
422
+ * - Values ≥ 1,000 and < 1,000,000 are divided by 1,000 and rounded to the nearest
423
+ * integer (e.g. `8192` → `"8k"`, `1000` → `"1k"`).
424
+ *
425
+ * @example
426
+ * ```ts
427
+ * formatContextLength(128000) // "128k"
428
+ * formatContextLength(1_000_000) // "1M"
429
+ * formatContextLength(1_500_000) // "1.5M"
430
+ * ```
431
+ *
432
+ */
433
+ declare function formatContextLength(tokens: number): string;
434
+ /**
435
+ * Format a flat USD price (per-generation, per-inpaint, per-upscale).
436
+ * Unlike formatPrice which handles per-token values, this formats absolute USD amounts.
437
+ * Examples: 0.04 → "$0.04", 0.18 → "$0.18", 0 → "$0.00"
438
+ */
439
+ declare function formatFlatPrice(usd: number | undefined | null): string;
440
+ /**
441
+ * Format a per-audio-second price.
442
+ * Examples: 0.01 → "$0.01 / sec", 0.006 → "$0.006 / sec"
443
+ */
444
+ declare function formatAudioPrice(perSecondUsd: number | undefined | null): string;
445
+ /**
446
+ * Format an array of duration strings into a range.
447
+ * Examples: ["5", "10", "30"] → "5s – 30s", ["10"] → "10s", [] → "—"
448
+ * Handles both numeric strings and strings that may already have units.
449
+ */
450
+ declare function formatDuration(durations: string[] | undefined | null): string;
451
+ /**
452
+ * Format an array of resolution strings into a comma-separated list.
453
+ * Examples: ["720p", "1080p", "4K"] → "720p, 1080p, 4K", [] → "—"
454
+ */
455
+ declare function formatResolutions(resolutions: string[] | undefined | null): string;
456
+ /**
457
+ * Format an array of aspect ratio strings into a comma-separated list.
458
+ * Examples: ["16:9", "9:16", "1:1"] → "16:9, 9:16, 1:1", [] → "—"
459
+ */
460
+ declare function formatAspectRatios(ratios: string[] | undefined | null): string;
461
+
462
+ /** Props for TextModelSelector — same as ModelSelectorProps but without `type`. */
463
+ type TextModelSelectorProps = Omit<ModelSelectorProps, 'type'>;
464
+ /** Props for ImageModelSelector — same as ModelSelectorProps but without `type`. */
465
+ type ImageModelSelectorProps = Omit<ModelSelectorProps, 'type'>;
466
+ /** Props for VideoModelSelector — same as ModelSelectorProps but without `type`. */
467
+ type VideoModelSelectorProps = Omit<ModelSelectorProps, 'type'>;
468
+
469
+ export { type AnyModel, type AsrModel, type AsrPricing, type BaseModel, type Deprecation, type EmbeddingModel, type EmbeddingPricing, type FetchFn, type ImageConstraints, type ImageModel, ImageModelSelector, type ImageModelSelectorProps, type ImagePricing, type InpaintConstraints, type InpaintModel, type InpaintPricing, MODEL_ID_TYPE_PATTERNS, type ModelNormalizer, ModelSelector, type ModelSelectorProps, type ModelType, type ResponseExtractor, SYSTEM_DEFAULT_VALUE, type TextCapabilities, type TextConstraints, type TextModel, TextModelSelector, type TextModelSelectorProps, type TextPricing, type TtsModel, type TtsPricing, type UpscaleModel, type UpscalePricing, type UseModelsProps, type UseModelsResult, type VideoConstraints, type VideoModel, VideoModelSelector, type VideoModelSelectorProps, defaultModelNormalizer, defaultResponseExtractor, extractBaseFields, formatAspectRatios, formatAudioPrice, formatContextLength, formatDuration, formatFlatPrice, formatPrice, formatResolutions, inferTypeFromId, isDeprecated, normalizeAsrModel, normalizeEmbeddingModel, normalizeImageModel, normalizeInpaintModel, normalizeTextModel, normalizeTtsModel, normalizeUpscaleModel, normalizeVideoModel, toNum, useModels };