open-model-selector 0.1.0 → 0.2.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,26 @@ This project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] — 2026-02-20
11
+
12
+ ### Added
13
+
14
+ - **Multi-provider type resolution** — `defaultModelNormalizer` now uses a 6-tier classification strategy to correctly identify model types across providers that use non-standard vocabulary:
15
+ 1. Direct match against canonical types (Venice)
16
+ 2. Non-text alias mapping (Together AI: `"audio"`→`"tts"`, `"transcribe"`→`"asr"`)
17
+ 3. Architecture-based inference from `output_modalities` (OpenRouter)
18
+ 4. Heuristic inference from model ID patterns
19
+ 5. Text-aliased type (Together AI: `"chat"`→`"text"`, Mistral: `"base"`→`"text"`, Vercel: `"language"`→`"text"`)
20
+ 6. Default to `"text"`
21
+ - **`TYPE_ALIASES` export** — maps provider-specific type strings (`chat`, `language`, `base`, `moderation`, `rerank`, `audio`, `transcribe`) to canonical `ModelType` values; exported from both `"open-model-selector"` and `"open-model-selector/utils"` so consumers can inspect or extend
22
+ - **Extended `MODEL_ID_TYPE_PATTERNS`** — new heuristic patterns: `embedqa`, `imagen`, `gpt-image`, `image-preview`, `kling`
23
+ - **Provider compatibility test suite** (`provider-compat.test.ts`) — 418 lines covering type resolution for Together AI, Vercel AI Gateway, Mistral AI, OpenRouter, OpenAI, Nvidia NIM, Helicone, and Venice AI response shapes, plus `defaultResponseExtractor` wrapper shapes
24
+ - **Live provider Storybook stories** (`provider-live.stories.tsx`) — interactive stories for testing against 12 real provider APIs: OpenAI, OpenRouter, Together, Groq, Cerebras, Nvidia, Mistral, DeepSeek, SambaNova, Venice, Helicone, and Vercel
25
+ - **`.env.example`** — environment variable template for 13 API provider keys used by Storybook live stories and the snapshot capture script
26
+ - **Provider snapshot capture script** (`scripts/capture-provider-snapshots.cjs`) — fetches and stores real provider API responses for test fixture generation
27
+ - **`.env` parser utility** (`scripts/parse-env.cjs`) — shared `.env` file parser used by both Storybook config and the snapshot capture script
28
+ - **Storybook provider proxy support** — `.storybook/main.ts` now loads `.env` keys and configures Vite proxies for cross-origin provider API access during development
29
+
10
30
  ## [0.1.0] — 2026-02-06
11
31
 
12
32
  Initial release of `open-model-selector`.
@@ -68,5 +88,6 @@ Initial release of `open-model-selector`.
68
88
  - **Normalizer**: `ModelNormalizer`, `ResponseExtractor`
69
89
  - **Dual CJS/ESM output** — built with tsup, sourcemaps and `.d.ts` included
70
90
 
71
- [Unreleased]: https://github.com/sethbang/open-model-selector/compare/v0.1.0...HEAD
91
+ [Unreleased]: https://github.com/sethbang/open-model-selector/compare/v0.2.0...HEAD
92
+ [0.2.0]: https://github.com/sethbang/open-model-selector/compare/v0.1.0...v0.2.0
72
93
  [0.1.0]: https://github.com/sethbang/open-model-selector/releases/tag/v0.1.0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # open-model-selector
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/open-model-selector)](https://www.npmjs.com/package/open-model-selector) [![npm bundle size](https://img.shields.io/bundlephobia/minzip/open-model-selector)](https://bundlephobia.com/package/open-model-selector) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue.svg)](https://www.typescriptlang.org/)
3
+ [![npm version](https://img.shields.io/npm/v/open-model-selector)](https://www.npmjs.com/package/open-model-selector) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue.svg)](https://www.typescriptlang.org/)
4
4
 
5
5
  ### An accessible, themeable React model-selector combobox for any OpenAI-compatible API — with first-class [Venice.ai](https://venice.ai) support.
6
6
 
@@ -37,13 +37,16 @@
37
37
  - **"Use System Default"** sentinel option for fallback behavior
38
38
  - **8 model types**: text, image, video, inpaint, embedding, TTS, ASR, upscale
39
39
  - **Specialized selectors**: `<TextModelSelector>`, `<ImageModelSelector>`, `<VideoModelSelector>`
40
- - **Built-in normalizers** for Venice.ai, OpenAI, and OpenRouter response shapes
40
+ - **Built-in normalizers** for Venice.ai, OpenAI, OpenRouter, Together AI, Vercel AI Gateway, Mistral, Groq, Cerebras, Nvidia NIM, DeepSeek, SambaNova, and Helicone response shapes
41
41
  - **Scoped CSS** with `--oms-` custom property prefix — never pollutes host styles
42
42
  - **Dark mode** via `prefers-color-scheme` and `.dark` class
43
43
  - **Full TypeScript types** exported
44
44
  - **Dual CJS/ESM output** with sourcemaps
45
45
  - **React 18 and 19** support
46
46
 
47
+ ### Check out the [demo here!](https://sethbang.github.io/open-model-selector-demo/)
48
+
49
+
47
50
  ## Installation
48
51
 
49
52
  ### Install the package
@@ -444,7 +447,7 @@ import type { ModelNormalizer, ResponseExtractor } from "open-model-selector"
444
447
 
445
448
  ### Custom Normalizer
446
449
 
447
- The built-in `defaultModelNormalizer` handles Venice.ai, OpenAI, and OpenRouter response shapes automatically — including Venice's nested `model_spec` format with capabilities, privacy, traits, and deprecation info. If your API returns a different shape, provide a custom normalizer:
450
+ The built-in `defaultModelNormalizer` handles response shapes from Venice.ai, OpenAI, OpenRouter, Together AI, Vercel AI Gateway, Mistral, Groq, Cerebras, Nvidia NIM, DeepSeek, SambaNova, and Helicone automatically — using a [6-tier type classification strategy](#type-inference) that resolves provider-specific vocabulary, architecture metadata, and model ID heuristics. Venice's nested `model_spec` format with capabilities, privacy, traits, and deprecation info is fully supported. If your API returns a different shape, provide a custom normalizer:
448
451
 
449
452
  ```tsx
450
453
  import { ModelSelector } from "open-model-selector"
@@ -631,12 +634,42 @@ const imageModel = normalizeImageModel(rawApiObject) // → ImageModel
631
634
 
632
635
  #### Type Inference
633
636
 
634
- `inferTypeFromId` uses heuristic pattern matching on a model's ID string to determine its type. This is how `defaultModelNormalizer` classifies models from providers that don't include an explicit `type` field (e.g., OpenAI, OpenRouter):
637
+ `defaultModelNormalizer` uses a **6-tier type resolution strategy** to correctly classify models across providers with different metadata formats:
638
+
639
+ | Tier | Strategy | Providers |
640
+ |------|----------|-----------|
641
+ | 1 | Direct match against canonical types (`text`, `image`, etc.) | Venice |
642
+ | 2 | Non-text alias mapping (e.g. `"audio"`→`"tts"`, `"transcribe"`→`"asr"`) | Together AI |
643
+ | 3 | Architecture-based inference from `output_modalities` | OpenRouter |
644
+ | 4 | Heuristic pattern matching on model ID | OpenAI, Nvidia, Helicone |
645
+ | 5 | Text-aliased type (e.g. `"chat"`→`"text"`, `"base"`→`"text"`) | Together AI, Mistral, Vercel |
646
+ | 6 | Default to `"text"` | All |
647
+
648
+ > Text aliases (Tier 5) are checked **after** the ID heuristic (Tier 4) so that e.g. Mistral's `mistral-embed` (type: `"base"`) still gets classified as `"embedding"` via its model ID rather than being swallowed by the `"base"`→`"text"` alias.
649
+
650
+ **`TYPE_ALIASES`** maps provider-specific type vocabulary to canonical `ModelType` values:
651
+
652
+ ```ts
653
+ import { TYPE_ALIASES } from "open-model-selector/utils"
654
+
655
+ // TYPE_ALIASES = {
656
+ // chat: 'text', // Together AI
657
+ // language: 'text', // Vercel AI Gateway, Together AI
658
+ // base: 'text', // Mistral AI
659
+ // moderation: 'text', // Together AI
660
+ // rerank: 'text', // Together AI
661
+ // audio: 'tts', // Together AI (Cartesia Sonic etc.)
662
+ // transcribe: 'asr', // Together AI (Whisper etc.)
663
+ // }
664
+ ```
665
+
666
+ **`inferTypeFromId`** uses heuristic pattern matching on a model's ID string (Tier 4). This is how `defaultModelNormalizer` classifies models from providers that don't include an explicit `type` field (e.g., OpenAI, Nvidia):
635
667
 
636
668
  ```ts
637
669
  import { inferTypeFromId, MODEL_ID_TYPE_PATTERNS } from "open-model-selector/utils"
638
670
 
639
671
  inferTypeFromId("dall-e-3") // "image"
672
+ inferTypeFromId("gpt-image-1") // "image"
640
673
  inferTypeFromId("whisper-large-v3") // "asr"
641
674
  inferTypeFromId("gpt-4o") // undefined (no match → caller defaults to "text")
642
675
  ```
@@ -763,6 +796,7 @@ The component implements the [ARIA combobox pattern](https://www.w3.org/WAI/ARIA
763
796
  | `npm run test:watch` | Run tests in watch mode. |
764
797
  | `npm run storybook` | Start Storybook dev server on port 6006. |
765
798
  | `npm run build-storybook` | Build static Storybook site. |
799
+ | `node scripts/capture-provider-snapshots.cjs` | Capture real provider API responses into `test-fixtures/providers/` for test fixture generation. Requires `.env` with API keys. |
766
800
 
767
801
  > `npm run prepublishOnly` runs typecheck → test → build automatically before publishing.
768
802
 
@@ -780,6 +814,7 @@ Test files:
780
814
  - [`src/hooks/use-models.test.tsx`](src/hooks/use-models.test.tsx) — Hook tests
781
815
  - [`src/utils/format.test.ts`](src/utils/format.test.ts) — Format utility tests
782
816
  - [`src/utils/normalizers/normalizers.test.ts`](src/utils/normalizers/normalizers.test.ts) — Normalizer tests
817
+ - [`src/utils/normalizers/provider-compat.test.ts`](src/utils/normalizers/provider-compat.test.ts) — Provider compatibility tests (Together AI, Vercel, Mistral, OpenRouter, OpenAI, Nvidia, Helicone, Venice)
783
818
 
784
819
  ```bash
785
820
  # Run all tests
@@ -793,7 +828,14 @@ npm run test:watch
793
828
 
794
829
  The project includes comprehensive Storybook stories covering multiple scenarios: Default, PreselectedModel, SystemDefault, CustomPlaceholder, SortByNewest, ControlledFavorites, EmptyState, LoadingState, ErrorState, PopoverTop, WideContainer, DarkMode, MinimalModels, and VeniceLive.
795
830
 
831
+ **Live provider stories** are also available for testing against real APIs (OpenAI, OpenRouter, Together, Groq, Cerebras, Nvidia, Mistral, DeepSeek, SambaNova, Venice, Helicone, Vercel). To use them:
832
+
833
+ 1. Copy `.env.example` to `.env` and fill in the API keys you want to test
834
+ 2. Run `npm run storybook` — the Storybook config auto-loads your `.env` keys and configures CORS proxies
835
+
796
836
  ```bash
837
+ cp .env.example .env
838
+ # Edit .env with your API keys
797
839
  npm run storybook
798
840
  # Opens at http://localhost:6006
799
841
  ```
package/dist/index.cjs CHANGED
@@ -10,11 +10,12 @@ var _reactpopover = require('@radix-ui/react-popover'); var PopoverPrimitive = _
10
10
 
11
11
  // src/utils/normalizers/type-inference.ts
12
12
  var MODEL_ID_TYPE_PATTERNS = [
13
- [/\b(embed|embedding)\b/i, "embedding"],
14
- [/\b(dall-e|stable-diffusion|sdxl|midjourney|flux)\b/i, "image"],
13
+ [/\b(embed|embedding|embedqa)\b/i, "embedding"],
14
+ [/\b(dall-e|stable-diffusion|sdxl|midjourney|flux|imagen)\b/i, "image"],
15
+ [/\b(gpt-image|image-preview)\b/i, "image"],
15
16
  [/\b(tts)\b/i, "tts"],
16
17
  [/\b(whisper|asr)\b/i, "asr"],
17
- [/\b(sora|video|wan)\b/i, "video"],
18
+ [/\b(sora|video|wan|kling)\b/i, "video"],
18
19
  [/\b(inpaint)\b/i, "inpaint"],
19
20
  [/\b(upscale|esrgan)\b/i, "upscale"]
20
21
  ];
@@ -277,10 +278,45 @@ function defaultResponseExtractor(body) {
277
278
  return [];
278
279
  }
279
280
  var VALID_TYPES = /* @__PURE__ */ new Set(["text", "image", "video", "inpaint", "embedding", "tts", "asr", "upscale"]);
281
+ var TYPE_ALIASES = {
282
+ // Together AI
283
+ chat: "text",
284
+ language: "text",
285
+ // Vercel AI Gateway, Together AI
286
+ base: "text",
287
+ // Mistral AI
288
+ moderation: "text",
289
+ // Together AI
290
+ rerank: "text",
291
+ // Together AI
292
+ audio: "tts",
293
+ // Together AI (Cartesia Sonic etc.)
294
+ transcribe: "asr"
295
+ // Together AI (Whisper etc.)
296
+ // image, video, embedding already match VALID_TYPES directly
297
+ };
298
+ function inferTypeFromArchitecture(raw) {
299
+ const arch = raw.architecture;
300
+ if (!arch) return void 0;
301
+ const outputMods = arch.output_modalities;
302
+ if (!Array.isArray(outputMods) || outputMods.length === 0) return void 0;
303
+ if (outputMods.includes("image") && !outputMods.includes("text")) return "image";
304
+ if (outputMods.length === 1 && outputMods[0] === "audio") return "tts";
305
+ return void 0;
306
+ }
280
307
  function defaultModelNormalizer(raw) {
281
308
  const id = _nullishCoalesce(_nullishCoalesce(raw.id, () => ( raw.model_id)), () => ( ""));
282
309
  const rawType = raw.type;
283
- const type = _nullishCoalesce(_nullishCoalesce((rawType && VALID_TYPES.has(rawType) ? rawType : void 0), () => ( inferTypeFromId(id))), () => ( "text"));
310
+ const aliasedType = rawType && rawType in TYPE_ALIASES ? TYPE_ALIASES[rawType] : void 0;
311
+ const type = (
312
+ // 1. Direct match against canonical types (Venice)
313
+ _nullishCoalesce(_nullishCoalesce(_nullishCoalesce(_nullishCoalesce(_nullishCoalesce((rawType && VALID_TYPES.has(rawType) ? rawType : void 0), () => ( // 2. Non-text alias (e.g. "audio"→"tts", "transcribe"→"asr") — these are specific enough to trust
314
+ (aliasedType && aliasedType !== "text" ? aliasedType : void 0))), () => ( // 3. Architecture-based inference (OpenRouter output_modalities)
315
+ inferTypeFromArchitecture(raw))), () => ( // 4. Heuristic from model ID patterns
316
+ inferTypeFromId(id))), () => ( // 5. Text-aliased type (e.g. "chat"→"text", "base"→"text", "language"→"text")
317
+ aliasedType)), () => ( // 6. Default to 'text'
318
+ "text"))
319
+ );
284
320
  switch (type) {
285
321
  case "text":
286
322
  return normalizeTextModel(raw);
@@ -325,10 +361,10 @@ function useModels(props) {
325
361
  setError(null);
326
362
  return;
327
363
  }
328
- if (!/^https?:\/\//i.test(baseUrl)) {
364
+ if (!/^(https?:\/\/|\/(?!\/))/i.test(baseUrl)) {
329
365
  setAllModels([]);
330
366
  setLoading(false);
331
- setError(new Error(`Invalid baseUrl scheme: URL must start with http:// or https://`));
367
+ setError(new Error(`Invalid baseUrl scheme: URL must start with http://, https://, or /`));
332
368
  return;
333
369
  }
334
370
  const controller = new AbortController();
@@ -1370,4 +1406,5 @@ VideoModelSelector.displayName = "VideoModelSelector";
1370
1406
 
1371
1407
 
1372
1408
 
1373
- exports.ImageModelSelector = ImageModelSelector; exports.MODEL_ID_TYPE_PATTERNS = MODEL_ID_TYPE_PATTERNS; exports.ModelSelector = ModelSelector; exports.SYSTEM_DEFAULT_VALUE = SYSTEM_DEFAULT_VALUE; exports.TextModelSelector = TextModelSelector; exports.VideoModelSelector = VideoModelSelector; exports.defaultModelNormalizer = defaultModelNormalizer; exports.defaultResponseExtractor = defaultResponseExtractor; exports.extractBaseFields = extractBaseFields; exports.formatAspectRatios = formatAspectRatios; exports.formatAudioPrice = formatAudioPrice; exports.formatContextLength = formatContextLength; exports.formatDuration = formatDuration; exports.formatFlatPrice = formatFlatPrice; exports.formatPrice = formatPrice; exports.formatResolutions = formatResolutions; exports.inferTypeFromId = inferTypeFromId; exports.isDeprecated = isDeprecated; exports.normalizeAsrModel = normalizeAsrModel; exports.normalizeEmbeddingModel = normalizeEmbeddingModel; exports.normalizeImageModel = normalizeImageModel; exports.normalizeInpaintModel = normalizeInpaintModel; exports.normalizeTextModel = normalizeTextModel; exports.normalizeTtsModel = normalizeTtsModel; exports.normalizeUpscaleModel = normalizeUpscaleModel; exports.normalizeVideoModel = normalizeVideoModel; exports.toNum = toNum; exports.useModels = useModels;
1409
+
1410
+ exports.ImageModelSelector = ImageModelSelector; exports.MODEL_ID_TYPE_PATTERNS = MODEL_ID_TYPE_PATTERNS; exports.ModelSelector = ModelSelector; exports.SYSTEM_DEFAULT_VALUE = SYSTEM_DEFAULT_VALUE; exports.TYPE_ALIASES = TYPE_ALIASES; exports.TextModelSelector = TextModelSelector; exports.VideoModelSelector = VideoModelSelector; exports.defaultModelNormalizer = defaultModelNormalizer; exports.defaultResponseExtractor = defaultResponseExtractor; exports.extractBaseFields = extractBaseFields; exports.formatAspectRatios = formatAspectRatios; exports.formatAudioPrice = formatAudioPrice; exports.formatContextLength = formatContextLength; exports.formatDuration = formatDuration; exports.formatFlatPrice = formatFlatPrice; exports.formatPrice = formatPrice; exports.formatResolutions = formatResolutions; exports.inferTypeFromId = inferTypeFromId; exports.isDeprecated = isDeprecated; exports.normalizeAsrModel = normalizeAsrModel; exports.normalizeEmbeddingModel = normalizeEmbeddingModel; exports.normalizeImageModel = normalizeImageModel; exports.normalizeInpaintModel = normalizeInpaintModel; exports.normalizeTextModel = normalizeTextModel; exports.normalizeTtsModel = normalizeTtsModel; exports.normalizeUpscaleModel = normalizeUpscaleModel; exports.normalizeVideoModel = normalizeVideoModel; exports.toNum = toNum; exports.useModels = useModels;
package/dist/index.d.cts CHANGED
@@ -177,7 +177,10 @@ declare function extractBaseFields(raw: Record<string, unknown>): Omit<BaseModel
177
177
 
178
178
  /** Known model ID patterns for heuristic type inference.
179
179
  * Used when the API response lacks an explicit `type` field (non-Venice providers).
180
- * Exported so consumers can inspect or extend. */
180
+ * Exported so consumers can inspect or extend.
181
+ *
182
+ * Patterns are tested in order — first match wins. More specific patterns should
183
+ * come before broader ones (e.g. "gpt-image" before generic "image"). */
181
184
  declare const MODEL_ID_TYPE_PATTERNS: Array<[RegExp, ModelType]>;
182
185
  /** Infer model type from its ID using naming conventions.
183
186
  * Returns undefined if no pattern matches (caller should fall back to 'text'). */
@@ -219,11 +222,22 @@ type ModelNormalizer = (raw: Record<string, unknown>) => AnyModel;
219
222
  * - `{ models: [...] }` → return models
220
223
  * - Fallback → empty array */
221
224
  declare function defaultResponseExtractor(body: Record<string, unknown> | unknown[]): Record<string, unknown>[];
225
+ /** Maps provider-specific type strings to our canonical ModelType values.
226
+ * Covers vocabulary differences across Together AI, Vercel AI Gateway, Mistral, etc.
227
+ * Exported so consumers can inspect or extend. */
228
+ declare const TYPE_ALIASES: Record<string, ModelType>;
222
229
  /** 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 */
230
+ * Uses multi-tier type resolution:
231
+ * 1. Explicit `raw.type` field matching our canonical types (Venice, custom providers)
232
+ * 2. Non-text alias mapping for provider-specific vocabulary (Together AI: "audio"→"tts")
233
+ * 3. Architecture-based inference from `output_modalities` (OpenRouter)
234
+ * 4. Heuristic inference from model ID patterns (OpenAI, Nvidia, etc.)
235
+ * 5. Text-aliased type (Together AI: "chat"→"text", Mistral: "base"→"text")
236
+ * 6. Fallback to 'text' — safe default for most providers
237
+ *
238
+ * Note: aliases that resolve to 'text' are checked AFTER the ID heuristic (Tier 4)
239
+ * so that e.g. Mistral's `mistral-embed` (type: "base") still gets classified as
240
+ * 'embedding' via its model ID rather than being swallowed by the "base"→"text" alias. */
227
241
  declare function defaultModelNormalizer(raw: Record<string, unknown>): AnyModel;
228
242
 
229
243
  /** A fetch-compatible function signature. Used for SSR, testing, or proxy scenarios. */
@@ -466,4 +480,4 @@ type ImageModelSelectorProps = Omit<ModelSelectorProps, 'type'>;
466
480
  /** Props for VideoModelSelector — same as ModelSelectorProps but without `type`. */
467
481
  type VideoModelSelectorProps = Omit<ModelSelectorProps, 'type'>;
468
482
 
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 };
483
+ 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_ALIASES, 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 };
package/dist/index.d.ts CHANGED
@@ -177,7 +177,10 @@ declare function extractBaseFields(raw: Record<string, unknown>): Omit<BaseModel
177
177
 
178
178
  /** Known model ID patterns for heuristic type inference.
179
179
  * Used when the API response lacks an explicit `type` field (non-Venice providers).
180
- * Exported so consumers can inspect or extend. */
180
+ * Exported so consumers can inspect or extend.
181
+ *
182
+ * Patterns are tested in order — first match wins. More specific patterns should
183
+ * come before broader ones (e.g. "gpt-image" before generic "image"). */
181
184
  declare const MODEL_ID_TYPE_PATTERNS: Array<[RegExp, ModelType]>;
182
185
  /** Infer model type from its ID using naming conventions.
183
186
  * Returns undefined if no pattern matches (caller should fall back to 'text'). */
@@ -219,11 +222,22 @@ type ModelNormalizer = (raw: Record<string, unknown>) => AnyModel;
219
222
  * - `{ models: [...] }` → return models
220
223
  * - Fallback → empty array */
221
224
  declare function defaultResponseExtractor(body: Record<string, unknown> | unknown[]): Record<string, unknown>[];
225
+ /** Maps provider-specific type strings to our canonical ModelType values.
226
+ * Covers vocabulary differences across Together AI, Vercel AI Gateway, Mistral, etc.
227
+ * Exported so consumers can inspect or extend. */
228
+ declare const TYPE_ALIASES: Record<string, ModelType>;
222
229
  /** 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 */
230
+ * Uses multi-tier type resolution:
231
+ * 1. Explicit `raw.type` field matching our canonical types (Venice, custom providers)
232
+ * 2. Non-text alias mapping for provider-specific vocabulary (Together AI: "audio"→"tts")
233
+ * 3. Architecture-based inference from `output_modalities` (OpenRouter)
234
+ * 4. Heuristic inference from model ID patterns (OpenAI, Nvidia, etc.)
235
+ * 5. Text-aliased type (Together AI: "chat"→"text", Mistral: "base"→"text")
236
+ * 6. Fallback to 'text' — safe default for most providers
237
+ *
238
+ * Note: aliases that resolve to 'text' are checked AFTER the ID heuristic (Tier 4)
239
+ * so that e.g. Mistral's `mistral-embed` (type: "base") still gets classified as
240
+ * 'embedding' via its model ID rather than being swallowed by the "base"→"text" alias. */
227
241
  declare function defaultModelNormalizer(raw: Record<string, unknown>): AnyModel;
228
242
 
229
243
  /** A fetch-compatible function signature. Used for SSR, testing, or proxy scenarios. */
@@ -466,4 +480,4 @@ type ImageModelSelectorProps = Omit<ModelSelectorProps, 'type'>;
466
480
  /** Props for VideoModelSelector — same as ModelSelectorProps but without `type`. */
467
481
  type VideoModelSelectorProps = Omit<ModelSelectorProps, 'type'>;
468
482
 
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 };
483
+ 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_ALIASES, 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 };
package/dist/index.js CHANGED
@@ -10,11 +10,12 @@ import { useState, useEffect, useRef, useMemo } from "react";
10
10
 
11
11
  // src/utils/normalizers/type-inference.ts
12
12
  var MODEL_ID_TYPE_PATTERNS = [
13
- [/\b(embed|embedding)\b/i, "embedding"],
14
- [/\b(dall-e|stable-diffusion|sdxl|midjourney|flux)\b/i, "image"],
13
+ [/\b(embed|embedding|embedqa)\b/i, "embedding"],
14
+ [/\b(dall-e|stable-diffusion|sdxl|midjourney|flux|imagen)\b/i, "image"],
15
+ [/\b(gpt-image|image-preview)\b/i, "image"],
15
16
  [/\b(tts)\b/i, "tts"],
16
17
  [/\b(whisper|asr)\b/i, "asr"],
17
- [/\b(sora|video|wan)\b/i, "video"],
18
+ [/\b(sora|video|wan|kling)\b/i, "video"],
18
19
  [/\b(inpaint)\b/i, "inpaint"],
19
20
  [/\b(upscale|esrgan)\b/i, "upscale"]
20
21
  ];
@@ -277,10 +278,45 @@ function defaultResponseExtractor(body) {
277
278
  return [];
278
279
  }
279
280
  var VALID_TYPES = /* @__PURE__ */ new Set(["text", "image", "video", "inpaint", "embedding", "tts", "asr", "upscale"]);
281
+ var TYPE_ALIASES = {
282
+ // Together AI
283
+ chat: "text",
284
+ language: "text",
285
+ // Vercel AI Gateway, Together AI
286
+ base: "text",
287
+ // Mistral AI
288
+ moderation: "text",
289
+ // Together AI
290
+ rerank: "text",
291
+ // Together AI
292
+ audio: "tts",
293
+ // Together AI (Cartesia Sonic etc.)
294
+ transcribe: "asr"
295
+ // Together AI (Whisper etc.)
296
+ // image, video, embedding already match VALID_TYPES directly
297
+ };
298
+ function inferTypeFromArchitecture(raw) {
299
+ const arch = raw.architecture;
300
+ if (!arch) return void 0;
301
+ const outputMods = arch.output_modalities;
302
+ if (!Array.isArray(outputMods) || outputMods.length === 0) return void 0;
303
+ if (outputMods.includes("image") && !outputMods.includes("text")) return "image";
304
+ if (outputMods.length === 1 && outputMods[0] === "audio") return "tts";
305
+ return void 0;
306
+ }
280
307
  function defaultModelNormalizer(raw) {
281
308
  const id = raw.id ?? raw.model_id ?? "";
282
309
  const rawType = raw.type;
283
- const type = (rawType && VALID_TYPES.has(rawType) ? rawType : void 0) ?? inferTypeFromId(id) ?? "text";
310
+ const aliasedType = rawType && rawType in TYPE_ALIASES ? TYPE_ALIASES[rawType] : void 0;
311
+ const type = (
312
+ // 1. Direct match against canonical types (Venice)
313
+ (rawType && VALID_TYPES.has(rawType) ? rawType : void 0) ?? // 2. Non-text alias (e.g. "audio"→"tts", "transcribe"→"asr") — these are specific enough to trust
314
+ (aliasedType && aliasedType !== "text" ? aliasedType : void 0) ?? // 3. Architecture-based inference (OpenRouter output_modalities)
315
+ inferTypeFromArchitecture(raw) ?? // 4. Heuristic from model ID patterns
316
+ inferTypeFromId(id) ?? // 5. Text-aliased type (e.g. "chat"→"text", "base"→"text", "language"→"text")
317
+ aliasedType ?? // 6. Default to 'text'
318
+ "text"
319
+ );
284
320
  switch (type) {
285
321
  case "text":
286
322
  return normalizeTextModel(raw);
@@ -325,10 +361,10 @@ function useModels(props) {
325
361
  setError(null);
326
362
  return;
327
363
  }
328
- if (!/^https?:\/\//i.test(baseUrl)) {
364
+ if (!/^(https?:\/\/|\/(?!\/))/i.test(baseUrl)) {
329
365
  setAllModels([]);
330
366
  setLoading(false);
331
- setError(new Error(`Invalid baseUrl scheme: URL must start with http:// or https://`));
367
+ setError(new Error(`Invalid baseUrl scheme: URL must start with http://, https://, or /`));
332
368
  return;
333
369
  }
334
370
  const controller = new AbortController();
@@ -1346,6 +1382,7 @@ export {
1346
1382
  MODEL_ID_TYPE_PATTERNS,
1347
1383
  ModelSelector,
1348
1384
  SYSTEM_DEFAULT_VALUE,
1385
+ TYPE_ALIASES,
1349
1386
  TextModelSelector,
1350
1387
  VideoModelSelector,
1351
1388
  defaultModelNormalizer,
package/dist/utils.cjs CHANGED
@@ -1,10 +1,11 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/utils/normalizers/type-inference.ts
2
2
  var MODEL_ID_TYPE_PATTERNS = [
3
- [/\b(embed|embedding)\b/i, "embedding"],
4
- [/\b(dall-e|stable-diffusion|sdxl|midjourney|flux)\b/i, "image"],
3
+ [/\b(embed|embedding|embedqa)\b/i, "embedding"],
4
+ [/\b(dall-e|stable-diffusion|sdxl|midjourney|flux|imagen)\b/i, "image"],
5
+ [/\b(gpt-image|image-preview)\b/i, "image"],
5
6
  [/\b(tts)\b/i, "tts"],
6
7
  [/\b(whisper|asr)\b/i, "asr"],
7
- [/\b(sora|video|wan)\b/i, "video"],
8
+ [/\b(sora|video|wan|kling)\b/i, "video"],
8
9
  [/\b(inpaint)\b/i, "inpaint"],
9
10
  [/\b(upscale|esrgan)\b/i, "upscale"]
10
11
  ];
@@ -267,10 +268,45 @@ function defaultResponseExtractor(body) {
267
268
  return [];
268
269
  }
269
270
  var VALID_TYPES = /* @__PURE__ */ new Set(["text", "image", "video", "inpaint", "embedding", "tts", "asr", "upscale"]);
271
+ var TYPE_ALIASES = {
272
+ // Together AI
273
+ chat: "text",
274
+ language: "text",
275
+ // Vercel AI Gateway, Together AI
276
+ base: "text",
277
+ // Mistral AI
278
+ moderation: "text",
279
+ // Together AI
280
+ rerank: "text",
281
+ // Together AI
282
+ audio: "tts",
283
+ // Together AI (Cartesia Sonic etc.)
284
+ transcribe: "asr"
285
+ // Together AI (Whisper etc.)
286
+ // image, video, embedding already match VALID_TYPES directly
287
+ };
288
+ function inferTypeFromArchitecture(raw) {
289
+ const arch = raw.architecture;
290
+ if (!arch) return void 0;
291
+ const outputMods = arch.output_modalities;
292
+ if (!Array.isArray(outputMods) || outputMods.length === 0) return void 0;
293
+ if (outputMods.includes("image") && !outputMods.includes("text")) return "image";
294
+ if (outputMods.length === 1 && outputMods[0] === "audio") return "tts";
295
+ return void 0;
296
+ }
270
297
  function defaultModelNormalizer(raw) {
271
298
  const id = _nullishCoalesce(_nullishCoalesce(raw.id, () => ( raw.model_id)), () => ( ""));
272
299
  const rawType = raw.type;
273
- const type = _nullishCoalesce(_nullishCoalesce((rawType && VALID_TYPES.has(rawType) ? rawType : void 0), () => ( inferTypeFromId(id))), () => ( "text"));
300
+ const aliasedType = rawType && rawType in TYPE_ALIASES ? TYPE_ALIASES[rawType] : void 0;
301
+ const type = (
302
+ // 1. Direct match against canonical types (Venice)
303
+ _nullishCoalesce(_nullishCoalesce(_nullishCoalesce(_nullishCoalesce(_nullishCoalesce((rawType && VALID_TYPES.has(rawType) ? rawType : void 0), () => ( // 2. Non-text alias (e.g. "audio"→"tts", "transcribe"→"asr") — these are specific enough to trust
304
+ (aliasedType && aliasedType !== "text" ? aliasedType : void 0))), () => ( // 3. Architecture-based inference (OpenRouter output_modalities)
305
+ inferTypeFromArchitecture(raw))), () => ( // 4. Heuristic from model ID patterns
306
+ inferTypeFromId(id))), () => ( // 5. Text-aliased type (e.g. "chat"→"text", "base"→"text", "language"→"text")
307
+ aliasedType)), () => ( // 6. Default to 'text'
308
+ "text"))
309
+ );
274
310
  switch (type) {
275
311
  case "text":
276
312
  return normalizeTextModel(raw);
@@ -368,4 +404,5 @@ function formatAspectRatios(ratios) {
368
404
 
369
405
 
370
406
 
371
- exports.MODEL_ID_TYPE_PATTERNS = MODEL_ID_TYPE_PATTERNS; exports.defaultModelNormalizer = defaultModelNormalizer; exports.defaultResponseExtractor = defaultResponseExtractor; exports.extractBaseFields = extractBaseFields; exports.formatAspectRatios = formatAspectRatios; exports.formatAudioPrice = formatAudioPrice; exports.formatContextLength = formatContextLength; exports.formatDuration = formatDuration; exports.formatFlatPrice = formatFlatPrice; exports.formatPrice = formatPrice; exports.formatResolutions = formatResolutions; exports.inferTypeFromId = inferTypeFromId; exports.isDeprecated = isDeprecated; exports.normalizeAsrModel = normalizeAsrModel; exports.normalizeEmbeddingModel = normalizeEmbeddingModel; exports.normalizeImageModel = normalizeImageModel; exports.normalizeInpaintModel = normalizeInpaintModel; exports.normalizeTextModel = normalizeTextModel; exports.normalizeTtsModel = normalizeTtsModel; exports.normalizeUpscaleModel = normalizeUpscaleModel; exports.normalizeVideoModel = normalizeVideoModel; exports.toNum = toNum;
407
+
408
+ exports.MODEL_ID_TYPE_PATTERNS = MODEL_ID_TYPE_PATTERNS; exports.TYPE_ALIASES = TYPE_ALIASES; exports.defaultModelNormalizer = defaultModelNormalizer; exports.defaultResponseExtractor = defaultResponseExtractor; exports.extractBaseFields = extractBaseFields; exports.formatAspectRatios = formatAspectRatios; exports.formatAudioPrice = formatAudioPrice; exports.formatContextLength = formatContextLength; exports.formatDuration = formatDuration; exports.formatFlatPrice = formatFlatPrice; exports.formatPrice = formatPrice; exports.formatResolutions = formatResolutions; exports.inferTypeFromId = inferTypeFromId; exports.isDeprecated = isDeprecated; exports.normalizeAsrModel = normalizeAsrModel; exports.normalizeEmbeddingModel = normalizeEmbeddingModel; exports.normalizeImageModel = normalizeImageModel; exports.normalizeInpaintModel = normalizeInpaintModel; exports.normalizeTextModel = normalizeTextModel; exports.normalizeTtsModel = normalizeTtsModel; exports.normalizeUpscaleModel = normalizeUpscaleModel; exports.normalizeVideoModel = normalizeVideoModel; exports.toNum = toNum;
package/dist/utils.d.cts CHANGED
@@ -175,7 +175,10 @@ declare function extractBaseFields(raw: Record<string, unknown>): Omit<BaseModel
175
175
 
176
176
  /** Known model ID patterns for heuristic type inference.
177
177
  * Used when the API response lacks an explicit `type` field (non-Venice providers).
178
- * Exported so consumers can inspect or extend. */
178
+ * Exported so consumers can inspect or extend.
179
+ *
180
+ * Patterns are tested in order — first match wins. More specific patterns should
181
+ * come before broader ones (e.g. "gpt-image" before generic "image"). */
179
182
  declare const MODEL_ID_TYPE_PATTERNS: Array<[RegExp, ModelType]>;
180
183
  /** Infer model type from its ID using naming conventions.
181
184
  * Returns undefined if no pattern matches (caller should fall back to 'text'). */
@@ -217,11 +220,22 @@ type ModelNormalizer = (raw: Record<string, unknown>) => AnyModel;
217
220
  * - `{ models: [...] }` → return models
218
221
  * - Fallback → empty array */
219
222
  declare function defaultResponseExtractor(body: Record<string, unknown> | unknown[]): Record<string, unknown>[];
223
+ /** Maps provider-specific type strings to our canonical ModelType values.
224
+ * Covers vocabulary differences across Together AI, Vercel AI Gateway, Mistral, etc.
225
+ * Exported so consumers can inspect or extend. */
226
+ declare const TYPE_ALIASES: Record<string, ModelType>;
220
227
  /** Default dispatching model normalizer.
221
- * Uses three-tier type resolution:
222
- * 1. Explicit `raw.type` field (Venice, custom providers)
223
- * 2. Heuristic inference from model ID patterns (OpenAI, OpenRouter, etc.)
224
- * 3. Fallback to 'text' safe default for most providers */
228
+ * Uses multi-tier type resolution:
229
+ * 1. Explicit `raw.type` field matching our canonical types (Venice, custom providers)
230
+ * 2. Non-text alias mapping for provider-specific vocabulary (Together AI: "audio"→"tts")
231
+ * 3. Architecture-based inference from `output_modalities` (OpenRouter)
232
+ * 4. Heuristic inference from model ID patterns (OpenAI, Nvidia, etc.)
233
+ * 5. Text-aliased type (Together AI: "chat"→"text", Mistral: "base"→"text")
234
+ * 6. Fallback to 'text' — safe default for most providers
235
+ *
236
+ * Note: aliases that resolve to 'text' are checked AFTER the ID heuristic (Tier 4)
237
+ * so that e.g. Mistral's `mistral-embed` (type: "base") still gets classified as
238
+ * 'embedding' via its model ID rather than being swallowed by the "base"→"text" alias. */
225
239
  declare function defaultModelNormalizer(raw: Record<string, unknown>): AnyModel;
226
240
 
227
241
  /** Check whether a deprecation date is in the past.
@@ -301,4 +315,4 @@ declare function formatResolutions(resolutions: string[] | undefined | null): st
301
315
  */
302
316
  declare function formatAspectRatios(ratios: string[] | undefined | null): string;
303
317
 
304
- export { type AnyModel, type AsrModel, type AsrPricing, type BaseModel, type Deprecation, type EmbeddingModel, type EmbeddingPricing, type ImageConstraints, type ImageModel, type ImagePricing, type InpaintConstraints, type InpaintModel, type InpaintPricing, MODEL_ID_TYPE_PATTERNS, type ModelNormalizer, type ModelType, type ResponseExtractor, type TextCapabilities, type TextConstraints, type TextModel, type TextPricing, type TtsModel, type TtsPricing, type UpscaleModel, type UpscalePricing, type VideoConstraints, type VideoModel, defaultModelNormalizer, defaultResponseExtractor, extractBaseFields, formatAspectRatios, formatAudioPrice, formatContextLength, formatDuration, formatFlatPrice, formatPrice, formatResolutions, inferTypeFromId, isDeprecated, normalizeAsrModel, normalizeEmbeddingModel, normalizeImageModel, normalizeInpaintModel, normalizeTextModel, normalizeTtsModel, normalizeUpscaleModel, normalizeVideoModel, toNum };
318
+ export { type AnyModel, type AsrModel, type AsrPricing, type BaseModel, type Deprecation, type EmbeddingModel, type EmbeddingPricing, type ImageConstraints, type ImageModel, type ImagePricing, type InpaintConstraints, type InpaintModel, type InpaintPricing, MODEL_ID_TYPE_PATTERNS, type ModelNormalizer, type ModelType, type ResponseExtractor, TYPE_ALIASES, type TextCapabilities, type TextConstraints, type TextModel, type TextPricing, type TtsModel, type TtsPricing, type UpscaleModel, type UpscalePricing, type VideoConstraints, type VideoModel, defaultModelNormalizer, defaultResponseExtractor, extractBaseFields, formatAspectRatios, formatAudioPrice, formatContextLength, formatDuration, formatFlatPrice, formatPrice, formatResolutions, inferTypeFromId, isDeprecated, normalizeAsrModel, normalizeEmbeddingModel, normalizeImageModel, normalizeInpaintModel, normalizeTextModel, normalizeTtsModel, normalizeUpscaleModel, normalizeVideoModel, toNum };
package/dist/utils.d.ts CHANGED
@@ -175,7 +175,10 @@ declare function extractBaseFields(raw: Record<string, unknown>): Omit<BaseModel
175
175
 
176
176
  /** Known model ID patterns for heuristic type inference.
177
177
  * Used when the API response lacks an explicit `type` field (non-Venice providers).
178
- * Exported so consumers can inspect or extend. */
178
+ * Exported so consumers can inspect or extend.
179
+ *
180
+ * Patterns are tested in order — first match wins. More specific patterns should
181
+ * come before broader ones (e.g. "gpt-image" before generic "image"). */
179
182
  declare const MODEL_ID_TYPE_PATTERNS: Array<[RegExp, ModelType]>;
180
183
  /** Infer model type from its ID using naming conventions.
181
184
  * Returns undefined if no pattern matches (caller should fall back to 'text'). */
@@ -217,11 +220,22 @@ type ModelNormalizer = (raw: Record<string, unknown>) => AnyModel;
217
220
  * - `{ models: [...] }` → return models
218
221
  * - Fallback → empty array */
219
222
  declare function defaultResponseExtractor(body: Record<string, unknown> | unknown[]): Record<string, unknown>[];
223
+ /** Maps provider-specific type strings to our canonical ModelType values.
224
+ * Covers vocabulary differences across Together AI, Vercel AI Gateway, Mistral, etc.
225
+ * Exported so consumers can inspect or extend. */
226
+ declare const TYPE_ALIASES: Record<string, ModelType>;
220
227
  /** Default dispatching model normalizer.
221
- * Uses three-tier type resolution:
222
- * 1. Explicit `raw.type` field (Venice, custom providers)
223
- * 2. Heuristic inference from model ID patterns (OpenAI, OpenRouter, etc.)
224
- * 3. Fallback to 'text' safe default for most providers */
228
+ * Uses multi-tier type resolution:
229
+ * 1. Explicit `raw.type` field matching our canonical types (Venice, custom providers)
230
+ * 2. Non-text alias mapping for provider-specific vocabulary (Together AI: "audio"→"tts")
231
+ * 3. Architecture-based inference from `output_modalities` (OpenRouter)
232
+ * 4. Heuristic inference from model ID patterns (OpenAI, Nvidia, etc.)
233
+ * 5. Text-aliased type (Together AI: "chat"→"text", Mistral: "base"→"text")
234
+ * 6. Fallback to 'text' — safe default for most providers
235
+ *
236
+ * Note: aliases that resolve to 'text' are checked AFTER the ID heuristic (Tier 4)
237
+ * so that e.g. Mistral's `mistral-embed` (type: "base") still gets classified as
238
+ * 'embedding' via its model ID rather than being swallowed by the "base"→"text" alias. */
225
239
  declare function defaultModelNormalizer(raw: Record<string, unknown>): AnyModel;
226
240
 
227
241
  /** Check whether a deprecation date is in the past.
@@ -301,4 +315,4 @@ declare function formatResolutions(resolutions: string[] | undefined | null): st
301
315
  */
302
316
  declare function formatAspectRatios(ratios: string[] | undefined | null): string;
303
317
 
304
- export { type AnyModel, type AsrModel, type AsrPricing, type BaseModel, type Deprecation, type EmbeddingModel, type EmbeddingPricing, type ImageConstraints, type ImageModel, type ImagePricing, type InpaintConstraints, type InpaintModel, type InpaintPricing, MODEL_ID_TYPE_PATTERNS, type ModelNormalizer, type ModelType, type ResponseExtractor, type TextCapabilities, type TextConstraints, type TextModel, type TextPricing, type TtsModel, type TtsPricing, type UpscaleModel, type UpscalePricing, type VideoConstraints, type VideoModel, defaultModelNormalizer, defaultResponseExtractor, extractBaseFields, formatAspectRatios, formatAudioPrice, formatContextLength, formatDuration, formatFlatPrice, formatPrice, formatResolutions, inferTypeFromId, isDeprecated, normalizeAsrModel, normalizeEmbeddingModel, normalizeImageModel, normalizeInpaintModel, normalizeTextModel, normalizeTtsModel, normalizeUpscaleModel, normalizeVideoModel, toNum };
318
+ export { type AnyModel, type AsrModel, type AsrPricing, type BaseModel, type Deprecation, type EmbeddingModel, type EmbeddingPricing, type ImageConstraints, type ImageModel, type ImagePricing, type InpaintConstraints, type InpaintModel, type InpaintPricing, MODEL_ID_TYPE_PATTERNS, type ModelNormalizer, type ModelType, type ResponseExtractor, TYPE_ALIASES, type TextCapabilities, type TextConstraints, type TextModel, type TextPricing, type TtsModel, type TtsPricing, type UpscaleModel, type UpscalePricing, type VideoConstraints, type VideoModel, defaultModelNormalizer, defaultResponseExtractor, extractBaseFields, formatAspectRatios, formatAudioPrice, formatContextLength, formatDuration, formatFlatPrice, formatPrice, formatResolutions, inferTypeFromId, isDeprecated, normalizeAsrModel, normalizeEmbeddingModel, normalizeImageModel, normalizeInpaintModel, normalizeTextModel, normalizeTtsModel, normalizeUpscaleModel, normalizeVideoModel, toNum };
package/dist/utils.js CHANGED
@@ -1,10 +1,11 @@
1
1
  // src/utils/normalizers/type-inference.ts
2
2
  var MODEL_ID_TYPE_PATTERNS = [
3
- [/\b(embed|embedding)\b/i, "embedding"],
4
- [/\b(dall-e|stable-diffusion|sdxl|midjourney|flux)\b/i, "image"],
3
+ [/\b(embed|embedding|embedqa)\b/i, "embedding"],
4
+ [/\b(dall-e|stable-diffusion|sdxl|midjourney|flux|imagen)\b/i, "image"],
5
+ [/\b(gpt-image|image-preview)\b/i, "image"],
5
6
  [/\b(tts)\b/i, "tts"],
6
7
  [/\b(whisper|asr)\b/i, "asr"],
7
- [/\b(sora|video|wan)\b/i, "video"],
8
+ [/\b(sora|video|wan|kling)\b/i, "video"],
8
9
  [/\b(inpaint)\b/i, "inpaint"],
9
10
  [/\b(upscale|esrgan)\b/i, "upscale"]
10
11
  ];
@@ -267,10 +268,45 @@ function defaultResponseExtractor(body) {
267
268
  return [];
268
269
  }
269
270
  var VALID_TYPES = /* @__PURE__ */ new Set(["text", "image", "video", "inpaint", "embedding", "tts", "asr", "upscale"]);
271
+ var TYPE_ALIASES = {
272
+ // Together AI
273
+ chat: "text",
274
+ language: "text",
275
+ // Vercel AI Gateway, Together AI
276
+ base: "text",
277
+ // Mistral AI
278
+ moderation: "text",
279
+ // Together AI
280
+ rerank: "text",
281
+ // Together AI
282
+ audio: "tts",
283
+ // Together AI (Cartesia Sonic etc.)
284
+ transcribe: "asr"
285
+ // Together AI (Whisper etc.)
286
+ // image, video, embedding already match VALID_TYPES directly
287
+ };
288
+ function inferTypeFromArchitecture(raw) {
289
+ const arch = raw.architecture;
290
+ if (!arch) return void 0;
291
+ const outputMods = arch.output_modalities;
292
+ if (!Array.isArray(outputMods) || outputMods.length === 0) return void 0;
293
+ if (outputMods.includes("image") && !outputMods.includes("text")) return "image";
294
+ if (outputMods.length === 1 && outputMods[0] === "audio") return "tts";
295
+ return void 0;
296
+ }
270
297
  function defaultModelNormalizer(raw) {
271
298
  const id = raw.id ?? raw.model_id ?? "";
272
299
  const rawType = raw.type;
273
- const type = (rawType && VALID_TYPES.has(rawType) ? rawType : void 0) ?? inferTypeFromId(id) ?? "text";
300
+ const aliasedType = rawType && rawType in TYPE_ALIASES ? TYPE_ALIASES[rawType] : void 0;
301
+ const type = (
302
+ // 1. Direct match against canonical types (Venice)
303
+ (rawType && VALID_TYPES.has(rawType) ? rawType : void 0) ?? // 2. Non-text alias (e.g. "audio"→"tts", "transcribe"→"asr") — these are specific enough to trust
304
+ (aliasedType && aliasedType !== "text" ? aliasedType : void 0) ?? // 3. Architecture-based inference (OpenRouter output_modalities)
305
+ inferTypeFromArchitecture(raw) ?? // 4. Heuristic from model ID patterns
306
+ inferTypeFromId(id) ?? // 5. Text-aliased type (e.g. "chat"→"text", "base"→"text", "language"→"text")
307
+ aliasedType ?? // 6. Default to 'text'
308
+ "text"
309
+ );
274
310
  switch (type) {
275
311
  case "text":
276
312
  return normalizeTextModel(raw);
@@ -347,6 +383,7 @@ function formatAspectRatios(ratios) {
347
383
  }
348
384
  export {
349
385
  MODEL_ID_TYPE_PATTERNS,
386
+ TYPE_ALIASES,
350
387
  defaultModelNormalizer,
351
388
  defaultResponseExtractor,
352
389
  extractBaseFields,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-model-selector",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A generic, OpenAI-compatible model selector component for React.",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",