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.
- package/CHANGELOG.md +72 -0
- package/LICENSE +21 -0
- package/README.md +824 -0
- package/dist/index.cjs +1373 -0
- package/dist/index.d.cts +469 -0
- package/dist/index.d.ts +469 -0
- package/dist/index.js +1373 -0
- package/dist/styles.css +592 -0
- package/dist/styles.d.cts +2 -0
- package/dist/styles.d.ts +2 -0
- package/dist/utils.cjs +371 -0
- package/dist/utils.d.cts +304 -0
- package/dist/utils.d.ts +304 -0
- package/dist/utils.js +371 -0
- package/package.json +125 -0
package/README.md
ADDED
|
@@ -0,0 +1,824 @@
|
|
|
1
|
+
# open-model-selector
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/open-model-selector) [](https://bundlephobia.com/package/open-model-selector) [](https://opensource.org/licenses/MIT) [](https://www.typescriptlang.org/)
|
|
4
|
+
|
|
5
|
+
### An accessible, themeable React model-selector combobox for any OpenAI-compatible API — with first-class [Venice.ai](https://venice.ai) support.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
<p align="center">
|
|
9
|
+
<img src="https://raw.githubusercontent.com/sethbang/open-model-selector/main/screenshots/main.png" alt="open-model-selector screenshot showing auto-discovery, code snippet, and the model selector popover" width="50%" />
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
> Drop it into your app to let users search, filter, and pick from Venice's frontier model catalog (GPT-5.2, Claude Opus 4.6, Gemini 3 Pro, GLM 4.7, Qwen 3 Coder 480B, and more) or any other `/v1/models` endpoint.
|
|
13
|
+
<br />
|
|
14
|
+
|
|
15
|
+
## Table of Contents
|
|
16
|
+
|
|
17
|
+
- [Features](#features)
|
|
18
|
+
- [Installation](#installation)
|
|
19
|
+
- [Quick Start](#quick-start)
|
|
20
|
+
- [API Reference](#api-reference)
|
|
21
|
+
- [TypeScript](#typescript)
|
|
22
|
+
- [Customization](#customization)
|
|
23
|
+
- [Framework Integration](#framework-integration)
|
|
24
|
+
- [Development](#development)
|
|
25
|
+
- [License](#license)
|
|
26
|
+
- [Links](#links)
|
|
27
|
+
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
- **First-class [Venice.ai](https://venice.ai) support** — full normalizer for Venice's rich `model_spec` format including capabilities, privacy levels, traits, and per-type pricing across 60+ models
|
|
31
|
+
- **Any OpenAI-compatible endpoint** — also works out of the box with OpenAI, OpenRouter, and more
|
|
32
|
+
- **Accessible combobox** built on [cmdk](https://cmdk.paco.me/) + [Radix Popover](https://www.radix-ui.com/primitives/docs/components/popover)
|
|
33
|
+
- **Managed or controlled mode** — auto-fetch models from an API, or pass them in directly
|
|
34
|
+
- **Fuzzy search** by name, provider, ID, and description
|
|
35
|
+
- **Favorites** with `localStorage` persistence or external state control
|
|
36
|
+
- **Sorting** by name (A–Z) or newest
|
|
37
|
+
- **"Use System Default"** sentinel option for fallback behavior
|
|
38
|
+
- **8 model types**: text, image, video, inpaint, embedding, TTS, ASR, upscale
|
|
39
|
+
- **Specialized selectors**: `<TextModelSelector>`, `<ImageModelSelector>`, `<VideoModelSelector>`
|
|
40
|
+
- **Built-in normalizers** for Venice.ai, OpenAI, and OpenRouter response shapes
|
|
41
|
+
- **Scoped CSS** with `--oms-` custom property prefix — never pollutes host styles
|
|
42
|
+
- **Dark mode** via `prefers-color-scheme` and `.dark` class
|
|
43
|
+
- **Full TypeScript types** exported
|
|
44
|
+
- **Dual CJS/ESM output** with sourcemaps
|
|
45
|
+
- **React 18 and 19** support
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
### Install the package
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install open-model-selector
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
yarn add open-model-selector
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pnpm add open-model-selector
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Peer dependencies
|
|
64
|
+
|
|
65
|
+
The following peer dependencies are required and must be installed separately:
|
|
66
|
+
|
|
67
|
+
| Package | Version |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| `react` | `^18.0.0 \|\| ^19.0.0` |
|
|
70
|
+
| `react-dom` | `^18.0.0 \|\| ^19.0.0` |
|
|
71
|
+
| `@radix-ui/react-popover` | `^1.0.0` |
|
|
72
|
+
| `cmdk` | `^1.0.0` |
|
|
73
|
+
|
|
74
|
+
Install the non-React peer dependencies (React and React DOM are typically already in your project):
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# npm
|
|
78
|
+
npm install @radix-ui/react-popover cmdk
|
|
79
|
+
# yarn
|
|
80
|
+
yarn add @radix-ui/react-popover cmdk
|
|
81
|
+
# pnpm
|
|
82
|
+
pnpm add @radix-ui/react-popover cmdk
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
> **React 18 users:** React 18 does not bundle its own TypeScript types. If you're using TypeScript with React 18, ensure `@types/react` and `@types/react-dom` are installed in your project.
|
|
86
|
+
|
|
87
|
+
### CSS import
|
|
88
|
+
|
|
89
|
+
You must import the stylesheet for the component to render correctly:
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import "open-model-selector/styles.css";
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
> Import this once in your app's entry point or layout component.
|
|
96
|
+
|
|
97
|
+
### Node.js requirement
|
|
98
|
+
|
|
99
|
+
Node.js **>=18.0.0** is required.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Quick Start
|
|
104
|
+
|
|
105
|
+
### Managed Mode (API Fetch)
|
|
106
|
+
|
|
107
|
+
The simplest way to use the component — point it at [Venice.ai](https://venice.ai) (or any OpenAI-compatible endpoint) and it handles the rest:
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
import { useState } from "react"
|
|
111
|
+
import { ModelSelector } from "open-model-selector"
|
|
112
|
+
import type { AnyModel } from "open-model-selector"
|
|
113
|
+
import "open-model-selector/styles.css"
|
|
114
|
+
|
|
115
|
+
function App() {
|
|
116
|
+
const [modelId, setModelId] = useState<string>("")
|
|
117
|
+
const [selectedModel, setSelectedModel] = useState<AnyModel | null>(null)
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<ModelSelector
|
|
121
|
+
baseUrl="https://api.venice.ai/api/v1"
|
|
122
|
+
apiKey="your-api-key"
|
|
123
|
+
value={modelId}
|
|
124
|
+
onChange={(id, model) => {
|
|
125
|
+
setModelId(id)
|
|
126
|
+
setSelectedModel(model) // full model object, or null for system default
|
|
127
|
+
}}
|
|
128
|
+
/>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
> The `onChange` callback receives the model ID **and** the full model object (or `null` when "Use System Default" is selected). This eliminates the need for a secondary lookup. The component fetches from `{baseUrl}/models` and normalizes responses automatically. Venice.ai's rich `model_spec` format is fully supported — capabilities, privacy levels, traits, pricing, and deprecation info are all extracted out of the box.
|
|
134
|
+
|
|
135
|
+
### Controlled Mode (Static Models)
|
|
136
|
+
|
|
137
|
+
Pass models directly when you already have them or need full control over the list. Here's an example using Venice.ai's frontier models:
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
import { useState } from "react"
|
|
141
|
+
import { ModelSelector } from "open-model-selector"
|
|
142
|
+
import type { TextModel } from "open-model-selector"
|
|
143
|
+
import "open-model-selector/styles.css"
|
|
144
|
+
|
|
145
|
+
const models: TextModel[] = [
|
|
146
|
+
{
|
|
147
|
+
id: "zai-org-glm-4.7",
|
|
148
|
+
name: "GLM 4.7",
|
|
149
|
+
provider: "venice.ai",
|
|
150
|
+
type: "text",
|
|
151
|
+
created: 1766534400,
|
|
152
|
+
is_favorite: false,
|
|
153
|
+
context_length: 198000,
|
|
154
|
+
capabilities: {
|
|
155
|
+
supportsFunctionCalling: true,
|
|
156
|
+
supportsReasoning: true,
|
|
157
|
+
supportsWebSearch: true,
|
|
158
|
+
},
|
|
159
|
+
pricing: { prompt: 0.00000055, completion: 0.00000265 },
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: "qwen3-coder-480b-a35b-instruct",
|
|
163
|
+
name: "Qwen 3 Coder 480B",
|
|
164
|
+
provider: "venice.ai",
|
|
165
|
+
type: "text",
|
|
166
|
+
created: 1745903059,
|
|
167
|
+
is_favorite: false,
|
|
168
|
+
context_length: 256000,
|
|
169
|
+
capabilities: {
|
|
170
|
+
optimizedForCode: true,
|
|
171
|
+
supportsFunctionCalling: true,
|
|
172
|
+
supportsWebSearch: true,
|
|
173
|
+
},
|
|
174
|
+
pricing: { prompt: 0.00000075, completion: 0.000003 },
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
id: "claude-opus-4-6",
|
|
178
|
+
name: "Claude Opus 4.6",
|
|
179
|
+
provider: "venice.ai",
|
|
180
|
+
type: "text",
|
|
181
|
+
created: 1770249600,
|
|
182
|
+
is_favorite: false,
|
|
183
|
+
context_length: 1000000,
|
|
184
|
+
capabilities: {
|
|
185
|
+
optimizedForCode: true,
|
|
186
|
+
supportsFunctionCalling: true,
|
|
187
|
+
supportsReasoning: true,
|
|
188
|
+
supportsVision: true,
|
|
189
|
+
},
|
|
190
|
+
pricing: { prompt: 0.000006, completion: 0.00003 },
|
|
191
|
+
},
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
function App() {
|
|
195
|
+
const [modelId, setModelId] = useState<string>("zai-org-glm-4.7")
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<ModelSelector
|
|
199
|
+
models={models}
|
|
200
|
+
value={modelId}
|
|
201
|
+
onChange={(id) => setModelId(id)}
|
|
202
|
+
placeholder="Choose a model..."
|
|
203
|
+
/>
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
> When a non-empty `models` array is provided, the internal API fetch is disabled. Venice.ai offers 60+ models across text, image, video, embedding, TTS, ASR, and more — all accessible via a single API key at [venice.ai](https://venice.ai).
|
|
209
|
+
|
|
210
|
+
### Specialized Selectors
|
|
211
|
+
|
|
212
|
+
Type-filtered convenience components for common model categories:
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
import { useState } from "react"
|
|
216
|
+
import { TextModelSelector, ImageModelSelector, VideoModelSelector } from "open-model-selector"
|
|
217
|
+
import "open-model-selector/styles.css" // Required — same stylesheet as <ModelSelector>
|
|
218
|
+
|
|
219
|
+
function App() {
|
|
220
|
+
const [model, setModel] = useState("")
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<>
|
|
224
|
+
{/* These are pre-filtered wrappers around <ModelSelector type="..."> */}
|
|
225
|
+
<TextModelSelector baseUrl="https://api.venice.ai/api/v1" value={model} onChange={(id) => setModel(id)} />
|
|
226
|
+
<ImageModelSelector baseUrl="https://api.venice.ai/api/v1" value={model} onChange={(id) => setModel(id)} />
|
|
227
|
+
<VideoModelSelector baseUrl="https://api.venice.ai/api/v1" value={model} onChange={(id) => setModel(id)} />
|
|
228
|
+
</>
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
> These are convenience wrappers that pass `type="text"`, `type="image"`, or `type="video"` respectively. All three forward `ref` to the root `<div>`, just like `<ModelSelector>`.
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## API Reference
|
|
238
|
+
|
|
239
|
+
### `<ModelSelector>`
|
|
240
|
+
|
|
241
|
+
The primary component. Renders an accessible combobox popover for searching and selecting models.
|
|
242
|
+
|
|
243
|
+
The component forwards `ref` to the root `<div>`.
|
|
244
|
+
|
|
245
|
+
| Prop | Type | Default | Description |
|
|
246
|
+
|------|------|---------|-------------|
|
|
247
|
+
| `models` | `AnyModel[]` | `[]` | Static list of models. When non-empty, disables internal API fetch. |
|
|
248
|
+
| `baseUrl` | `string` | — | Base URL for the OpenAI-compatible API (e.g., `"https://api.venice.ai/api/v1"`). |
|
|
249
|
+
| `apiKey` | `string` | — | API key for authentication. ⚠️ Visible in browser DevTools — use a backend proxy in production. |
|
|
250
|
+
| `type` | `ModelType` | — | Filter to a specific model type (`"text"`, `"image"`, `"video"`, etc.). |
|
|
251
|
+
| `queryParams` | `Record<string, string>` | `{}` | Query parameters appended to the `/models` URL as a query string. See [Query Parameters](#query-parameters) below. |
|
|
252
|
+
| `fetcher` | `(url: string, init?: RequestInit) => Promise<Response>` | `fetch` | Custom fetch function for SSR, proxies, or testing. |
|
|
253
|
+
| `responseExtractor` | `ResponseExtractor` | `defaultResponseExtractor` | Custom function to extract the model array from the API response. |
|
|
254
|
+
| `normalizer` | `ModelNormalizer` | `defaultModelNormalizer` | Custom function to normalize each raw model object into an `AnyModel`. |
|
|
255
|
+
| `value` | `string` | — | Currently selected model ID (controlled). |
|
|
256
|
+
| `onChange` | `(modelId: string, model: AnyModel \| null) => void` | — | Callback when a model is selected. Receives the model ID and the full model object (or `null` for the system-default sentinel). If omitted, a dev-mode warning is logged. |
|
|
257
|
+
| `onToggleFavorite` | `(modelId: string) => void` | — | Callback for favorite toggle. If omitted, favorites use localStorage. |
|
|
258
|
+
| `placeholder` | `string` | `"Select model..."` | Placeholder text when no model is selected. |
|
|
259
|
+
| `sortOrder` | `"name" \| "created"` | — | Controlled sort order. If omitted, internal state is used. |
|
|
260
|
+
| `onSortChange` | `(order: "name" \| "created") => void` | — | Callback when sort changes. |
|
|
261
|
+
| `side` | `"top" \| "bottom" \| "left" \| "right"` | `"bottom"` | Popover placement relative to the trigger. |
|
|
262
|
+
| `className` | `string` | — | Additional CSS class(es) for the root element. |
|
|
263
|
+
| `storageKey` | `string` | `"open-model-selector-favorites"` | localStorage key for persisting favorites (uncontrolled mode). |
|
|
264
|
+
| `showSystemDefault` | `boolean` | `true` | Whether to show the "Use System Default" option. |
|
|
265
|
+
| `showDeprecated` | `boolean` | `true` | Whether to show deprecated models. When `false`, past-date deprecated models are hidden. |
|
|
266
|
+
| `disabled` | `boolean` | `false` | When true, prevents opening the selector and dims the trigger button. |
|
|
267
|
+
|
|
268
|
+
The library exports a sentinel constant for the system default option:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
import { SYSTEM_DEFAULT_VALUE } from "open-model-selector"
|
|
272
|
+
// SYSTEM_DEFAULT_VALUE === "system_default"
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### `useModels` Hook
|
|
276
|
+
|
|
277
|
+
Fetches and normalizes models from an OpenAI-compatible API endpoint. Used internally by `<ModelSelector>` but available for building custom UIs on top of Venice.ai or any other provider.
|
|
278
|
+
|
|
279
|
+
```tsx
|
|
280
|
+
import { useModels } from "open-model-selector"
|
|
281
|
+
|
|
282
|
+
const { models, loading, error } = useModels({
|
|
283
|
+
baseUrl: "https://api.venice.ai/api/v1",
|
|
284
|
+
apiKey: "your-key",
|
|
285
|
+
type: "text",
|
|
286
|
+
})
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
#### Props (`UseModelsProps`)
|
|
290
|
+
|
|
291
|
+
| Prop | Type | Default | Description |
|
|
292
|
+
|------|------|---------|-------------|
|
|
293
|
+
| `baseUrl` | `string` | — | Base URL for the API. If omitted, no fetch occurs. |
|
|
294
|
+
| `apiKey` | `string` | — | Bearer token for authentication. |
|
|
295
|
+
| `type` | `ModelType` | — | Client-side filter by model type. |
|
|
296
|
+
| `queryParams` | `Record<string, string>` | `{}` | Query parameters appended to the `/models` URL. See [Query Parameters](#query-parameters) below. |
|
|
297
|
+
| `fetcher` | `(url: string, init?: RequestInit) => Promise<Response>` | `fetch` | Custom fetch function. |
|
|
298
|
+
| `responseExtractor` | `ResponseExtractor` | `defaultResponseExtractor` | Extracts model array from response JSON. |
|
|
299
|
+
| `normalizer` | `ModelNormalizer` | `defaultModelNormalizer` | Normalizes each raw model into `AnyModel`. |
|
|
300
|
+
|
|
301
|
+
#### Return Value (`UseModelsResult`)
|
|
302
|
+
|
|
303
|
+
| Field | Type | Description |
|
|
304
|
+
|-------|------|-------------|
|
|
305
|
+
| `models` | `AnyModel[]` | Normalized model array. |
|
|
306
|
+
| `loading` | `boolean` | `true` while fetching. |
|
|
307
|
+
| `error` | `Error \| null` | Fetch or normalization error, if any. |
|
|
308
|
+
|
|
309
|
+
> **Notes:**
|
|
310
|
+
> - Automatically cleans up with `AbortController` on unmount
|
|
311
|
+
> - Re-fetches when `baseUrl`, `apiKey`, or `queryParams` change
|
|
312
|
+
> - `fetcher`, `responseExtractor`, and `normalizer` are stored in refs (no memoization needed)
|
|
313
|
+
|
|
314
|
+
> **⚠️ Stability:** `apiKey` and `baseUrl` are used as React effect dependencies. Ensure these are stable values (string constants, state variables, or `useMemo`'d values). Passing an unstable reference (e.g., `apiKey={computeKey()}`) will cause infinite re-fetching.
|
|
315
|
+
|
|
316
|
+
### Query Parameters
|
|
317
|
+
|
|
318
|
+
The `queryParams` prop is appended to the `/models` endpoint URL as query string parameters (e.g., `queryParams={{ type: 'text' }}` fetches from `/models?type=text`).
|
|
319
|
+
|
|
320
|
+
**Venice.ai users:** Venice's `/models` endpoint supports a `type` parameter to filter models server-side. Pass the model type you need:
|
|
321
|
+
|
|
322
|
+
```tsx
|
|
323
|
+
// Fetch only text models from Venice
|
|
324
|
+
<ModelSelector
|
|
325
|
+
baseUrl="https://api.venice.ai/api/v1"
|
|
326
|
+
queryParams={{ type: 'text' }}
|
|
327
|
+
value={model}
|
|
328
|
+
onChange={(id) => setModel(id)}
|
|
329
|
+
/>
|
|
330
|
+
|
|
331
|
+
// Fetch all model types from Venice
|
|
332
|
+
<ModelSelector
|
|
333
|
+
baseUrl="https://api.venice.ai/api/v1"
|
|
334
|
+
queryParams={{ type: 'all' }}
|
|
335
|
+
value={model}
|
|
336
|
+
onChange={(id) => setModel(id)}
|
|
337
|
+
/>
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
> Venice supports `type` values: `"text"`, `"image"`, `"video"`, `"embedding"`, `"tts"`, `"asr"`, `"upscale"`, `"inpaint"`, and `"all"`.
|
|
341
|
+
|
|
342
|
+
**Other APIs:** Pass whatever query parameters your endpoint expects, or omit `queryParams` entirely if none are needed.
|
|
343
|
+
|
|
344
|
+
> **Performance tip:** The `type` prop (e.g., `type="text"`) filters **client-side** — all models are fetched first, then filtered in the browser. For large catalogs, use `queryParams` to filter **server-side** at the API level, reducing payload size and parse time. You can combine both: `queryParams` for server-side pre-filtering and `type` as a client-side safety net.
|
|
345
|
+
|
|
346
|
+
### Model Types
|
|
347
|
+
|
|
348
|
+
The library supports 8 model types, each with its own TypeScript interface extending `BaseModel`:
|
|
349
|
+
|
|
350
|
+
| Type | Interface | Key Fields |
|
|
351
|
+
|------|-----------|------------|
|
|
352
|
+
| `"text"` | `TextModel` | `pricing.prompt`, `pricing.completion`, `pricing.cache_input`, `pricing.cache_write`, `context_length`, `capabilities`, `constraints.temperature`, `constraints.top_p` |
|
|
353
|
+
| `"image"` | `ImageModel` | `pricing.generation`, `pricing.resolutions`, `constraints.aspectRatios`, `constraints.resolutions`, `supportsWebSearch` |
|
|
354
|
+
| `"video"` | `VideoModel` | `constraints.resolutions`, `constraints.durations`, `constraints.aspect_ratios`, `model_sets` |
|
|
355
|
+
| `"inpaint"` | `InpaintModel` | `pricing.generation`, `constraints.aspectRatios`, `constraints.combineImages` |
|
|
356
|
+
| `"embedding"` | `EmbeddingModel` | `pricing.input`, `pricing.output` |
|
|
357
|
+
| `"tts"` | `TtsModel` | `pricing.input`, `voices` |
|
|
358
|
+
| `"asr"` | `AsrModel` | `pricing.per_audio_second` |
|
|
359
|
+
| `"upscale"` | `UpscaleModel` | `pricing.generation` |
|
|
360
|
+
|
|
361
|
+
The union type `AnyModel` represents any of the above.
|
|
362
|
+
|
|
363
|
+
The `BaseModel` interface includes these fields shared by all types:
|
|
364
|
+
|
|
365
|
+
- `id: string`
|
|
366
|
+
- `name: string`
|
|
367
|
+
- `provider: string`
|
|
368
|
+
- `created: number` (Unix timestamp)
|
|
369
|
+
- `type: ModelType`
|
|
370
|
+
- `description?: string`
|
|
371
|
+
- `privacy?: "private" | "anonymized"`
|
|
372
|
+
- `is_favorite?: boolean`
|
|
373
|
+
- `offline?: boolean`
|
|
374
|
+
- `betaModel?: boolean`
|
|
375
|
+
- `modelSource?: string`
|
|
376
|
+
- `traits?: string[]`
|
|
377
|
+
- `deprecation?: { date: string }`
|
|
378
|
+
|
|
379
|
+
```tsx
|
|
380
|
+
import type { TextModel, ImageModel, AnyModel, ModelType } from "open-model-selector"
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
The library also exports prop types for each specialized selector:
|
|
384
|
+
|
|
385
|
+
```tsx
|
|
386
|
+
import type {
|
|
387
|
+
ModelSelectorProps,
|
|
388
|
+
TextModelSelectorProps,
|
|
389
|
+
ImageModelSelectorProps,
|
|
390
|
+
VideoModelSelectorProps,
|
|
391
|
+
} from "open-model-selector"
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
> `TextModelSelectorProps`, `ImageModelSelectorProps`, and `VideoModelSelectorProps` are each `Omit<ModelSelectorProps, 'type'>` — identical to `ModelSelectorProps` with the `type` prop removed.
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## TypeScript
|
|
399
|
+
|
|
400
|
+
Full `.d.ts` type declarations are included in the package — no separate `@types/` install is needed.
|
|
401
|
+
|
|
402
|
+
### All Exported Types
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
// Component props
|
|
406
|
+
import type {
|
|
407
|
+
ModelSelectorProps,
|
|
408
|
+
TextModelSelectorProps,
|
|
409
|
+
ImageModelSelectorProps,
|
|
410
|
+
VideoModelSelectorProps,
|
|
411
|
+
} from "open-model-selector"
|
|
412
|
+
|
|
413
|
+
// Hook types
|
|
414
|
+
import type { UseModelsProps, UseModelsResult, FetchFn } from "open-model-selector"
|
|
415
|
+
|
|
416
|
+
// Base model types
|
|
417
|
+
import type { ModelType, BaseModel, Deprecation, AnyModel } from "open-model-selector"
|
|
418
|
+
|
|
419
|
+
// Text model types
|
|
420
|
+
import type { TextModel, TextPricing, TextCapabilities, TextConstraints } from "open-model-selector"
|
|
421
|
+
|
|
422
|
+
// Image model types
|
|
423
|
+
import type { ImageModel, ImagePricing, ImageConstraints } from "open-model-selector"
|
|
424
|
+
|
|
425
|
+
// Video model types
|
|
426
|
+
import type { VideoModel, VideoConstraints } from "open-model-selector"
|
|
427
|
+
|
|
428
|
+
// Other model types
|
|
429
|
+
import type { InpaintModel, InpaintPricing, InpaintConstraints } from "open-model-selector"
|
|
430
|
+
import type { EmbeddingModel, EmbeddingPricing } from "open-model-selector"
|
|
431
|
+
import type { TtsModel, TtsPricing } from "open-model-selector"
|
|
432
|
+
import type { AsrModel, AsrPricing } from "open-model-selector"
|
|
433
|
+
import type { UpscaleModel, UpscalePricing } from "open-model-selector"
|
|
434
|
+
|
|
435
|
+
// Normalizer types (also available from "open-model-selector/utils")
|
|
436
|
+
import type { ModelNormalizer, ResponseExtractor } from "open-model-selector"
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
> **Tip:** `import type` statements are erased at compile time and are always safe in React Server Components, regardless of `"use client"` directives.
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## Customization
|
|
444
|
+
|
|
445
|
+
### Custom Normalizer
|
|
446
|
+
|
|
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:
|
|
448
|
+
|
|
449
|
+
```tsx
|
|
450
|
+
import { ModelSelector } from "open-model-selector"
|
|
451
|
+
import type { ModelNormalizer, AnyModel } from "open-model-selector"
|
|
452
|
+
|
|
453
|
+
const myNormalizer: ModelNormalizer = (raw): AnyModel => ({
|
|
454
|
+
id: raw.model_id as string,
|
|
455
|
+
name: raw.display_name as string,
|
|
456
|
+
provider: raw.org as string,
|
|
457
|
+
type: "text",
|
|
458
|
+
created: Date.now() / 1000,
|
|
459
|
+
is_favorite: false,
|
|
460
|
+
context_length: Number(raw.max_tokens) || 128000,
|
|
461
|
+
pricing: {
|
|
462
|
+
prompt: Number(raw.cost_per_input_token),
|
|
463
|
+
completion: Number(raw.cost_per_output_token),
|
|
464
|
+
},
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
<ModelSelector
|
|
468
|
+
baseUrl="https://my-api.com/v1"
|
|
469
|
+
normalizer={myNormalizer}
|
|
470
|
+
value={model}
|
|
471
|
+
onChange={(id) => setModel(id)}
|
|
472
|
+
/>
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
> You can also compose with the built-in `extractBaseFields` helper for shared fields. The normalizer is stored in a ref — no `useCallback` wrapper needed.
|
|
476
|
+
|
|
477
|
+
### Custom Response Extractor
|
|
478
|
+
|
|
479
|
+
The built-in `defaultResponseExtractor` handles `{ data: [...] }`, `{ models: [...] }`, and top-level arrays. For non-standard response shapes, provide a custom extractor:
|
|
480
|
+
|
|
481
|
+
```tsx
|
|
482
|
+
import type { ResponseExtractor } from "open-model-selector"
|
|
483
|
+
|
|
484
|
+
const myExtractor: ResponseExtractor = (body) => {
|
|
485
|
+
// Your API returns { results: { items: [...] } }
|
|
486
|
+
const results = (body as any).results
|
|
487
|
+
return results?.items ?? []
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
<ModelSelector
|
|
491
|
+
baseUrl="https://my-api.com/v1"
|
|
492
|
+
responseExtractor={myExtractor}
|
|
493
|
+
value={model}
|
|
494
|
+
onChange={(id) => setModel(id)}
|
|
495
|
+
/>
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### Styling and Theming
|
|
499
|
+
|
|
500
|
+
All CSS variables are scoped under `.oms-reset` and use the `--oms-` prefix — they never pollute `:root` or the host app.
|
|
501
|
+
|
|
502
|
+
**Overriding variables:**
|
|
503
|
+
|
|
504
|
+
```css
|
|
505
|
+
.oms-reset {
|
|
506
|
+
--oms-primary: 210 100% 50%;
|
|
507
|
+
--oms-radius: 0.75rem;
|
|
508
|
+
--oms-popover-width: 400px;
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
**Key CSS variables:**
|
|
513
|
+
|
|
514
|
+
| Variable | Default (Light) | Purpose |
|
|
515
|
+
|----------|----------------|---------|
|
|
516
|
+
| `--oms-background` | `0 0% 100%` | Background color |
|
|
517
|
+
| `--oms-foreground` | `222.2 84% 4.9%` | Text color |
|
|
518
|
+
| `--oms-primary` | `222.2 47.4% 11.2%` | Primary/action color |
|
|
519
|
+
| `--oms-accent` | `210 40% 96.1%` | Hover/selection highlight |
|
|
520
|
+
| `--oms-muted-foreground` | `215.4 16.3% 46.9%` | Subdued text |
|
|
521
|
+
| `--oms-border` | `214.3 31.8% 91.4%` | Border color |
|
|
522
|
+
| `--oms-destructive` | `0 84.2% 60.2%` | Error/destructive color |
|
|
523
|
+
| `--oms-radius` | `0.5rem` | Border radius |
|
|
524
|
+
| `--oms-popover-width` | `300px` | Popover width |
|
|
525
|
+
|
|
526
|
+
> **Important:** CSS variable values must be space-separated HSL triplets (e.g., `220 14% 96%`), **not** hex, rgb, or named colors. The component uses these values inside `hsl()` wrappers internally, following the Shadcn/ui convention. This allows alpha composition: `hsl(var(--oms-accent) / 0.5)`.
|
|
527
|
+
>
|
|
528
|
+
> ```css
|
|
529
|
+
> /* ✅ Correct */
|
|
530
|
+
> .my-theme .oms-popover-content {
|
|
531
|
+
> --oms-background: 220 14% 96%;
|
|
532
|
+
> }
|
|
533
|
+
>
|
|
534
|
+
> /* ❌ Incorrect — will break styling */
|
|
535
|
+
> .my-theme .oms-popover-content {
|
|
536
|
+
> --oms-background: #f0f0f0;
|
|
537
|
+
> --oms-background: rgb(240, 240, 240);
|
|
538
|
+
> --oms-background: white;
|
|
539
|
+
> }
|
|
540
|
+
> ```
|
|
541
|
+
|
|
542
|
+
**Dark mode:**
|
|
543
|
+
|
|
544
|
+
The component supports two dark mode strategies:
|
|
545
|
+
|
|
546
|
+
1. **Automatic** — `@media (prefers-color-scheme: dark)` works out of the box with no configuration.
|
|
547
|
+
2. **Class-based** — Add a `.dark` class to any ancestor element. Compatible with Tailwind's `darkMode: "class"`.
|
|
548
|
+
|
|
549
|
+
```html
|
|
550
|
+
<!-- Automatic: just works with OS dark mode -->
|
|
551
|
+
<ModelSelector ... />
|
|
552
|
+
|
|
553
|
+
<!-- Manual: add .dark class to any ancestor -->
|
|
554
|
+
<div class="dark">
|
|
555
|
+
<ModelSelector ... />
|
|
556
|
+
</div>
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
> Portal-safe — the dark theme is also applied to popover and tooltip content that renders outside the component tree via Radix portals.
|
|
560
|
+
|
|
561
|
+
### Format Utilities
|
|
562
|
+
|
|
563
|
+
The `open-model-selector/utils` entry point exports format utilities without the `"use client"` directive, making them safe for React Server Components:
|
|
564
|
+
|
|
565
|
+
```tsx
|
|
566
|
+
import {
|
|
567
|
+
formatPrice,
|
|
568
|
+
formatContextLength,
|
|
569
|
+
formatFlatPrice,
|
|
570
|
+
formatAudioPrice,
|
|
571
|
+
formatDuration,
|
|
572
|
+
formatResolutions,
|
|
573
|
+
formatAspectRatios,
|
|
574
|
+
} from "open-model-selector/utils"
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
Usage examples:
|
|
578
|
+
|
|
579
|
+
```ts
|
|
580
|
+
formatPrice("0.000003") // "$3.00" — per-token price × 1,000,000
|
|
581
|
+
formatPrice(0.00003) // "$30.00" — works with numbers too
|
|
582
|
+
formatPrice(0) // "Free"
|
|
583
|
+
formatPrice(undefined) // "—"
|
|
584
|
+
formatContextLength(128000) // "128k"
|
|
585
|
+
formatFlatPrice(0.04) // "$0.04"
|
|
586
|
+
formatAudioPrice(0.006) // "$0.0060 / sec"
|
|
587
|
+
formatDuration(["5", "10"]) // "5s – 10s"
|
|
588
|
+
formatResolutions(["720p", "1080p", "4K"]) // "720p, 1080p, 4K"
|
|
589
|
+
formatAspectRatios(["1:1", "16:9", "4:3"]) // "1:1, 16:9, 4:3"
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
> **Note:** `formatPrice` expects the raw **per-token** price (as returned by OpenAI, Venice.ai, OpenRouter, etc.) and converts it to a **per-million-token** display value by multiplying by 1,000,000. For example, a per-token cost of `0.000003` becomes `$3.00` per million tokens. Very small values (< $0.01/M) use 6 decimal places to preserve precision.
|
|
593
|
+
|
|
594
|
+
### Normalizer Utilities
|
|
595
|
+
|
|
596
|
+
In addition to `defaultModelNormalizer` and `defaultResponseExtractor`, the library exports the individual per-type normalizers, type inference helpers, and low-level building blocks. These are available from both `"open-model-selector"` and `"open-model-selector/utils"`.
|
|
597
|
+
|
|
598
|
+
#### Per-Type Normalizers
|
|
599
|
+
|
|
600
|
+
Each model type has a dedicated normalizer that converts a raw API response object into the corresponding typed model:
|
|
601
|
+
|
|
602
|
+
```ts
|
|
603
|
+
import {
|
|
604
|
+
normalizeTextModel,
|
|
605
|
+
normalizeImageModel,
|
|
606
|
+
normalizeVideoModel,
|
|
607
|
+
normalizeInpaintModel,
|
|
608
|
+
normalizeEmbeddingModel,
|
|
609
|
+
normalizeTtsModel,
|
|
610
|
+
normalizeAsrModel,
|
|
611
|
+
normalizeUpscaleModel,
|
|
612
|
+
} from "open-model-selector/utils"
|
|
613
|
+
|
|
614
|
+
// Each accepts a raw object and returns the typed model
|
|
615
|
+
const textModel = normalizeTextModel(rawApiObject) // → TextModel
|
|
616
|
+
const imageModel = normalizeImageModel(rawApiObject) // → ImageModel
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
> These are the same functions used internally by `defaultModelNormalizer`. Use them directly when you need to normalize models of a known type without the dispatching logic.
|
|
620
|
+
|
|
621
|
+
| Function | Returns |
|
|
622
|
+
|----------|---------|
|
|
623
|
+
| `normalizeTextModel(raw)` | `TextModel` |
|
|
624
|
+
| `normalizeImageModel(raw)` | `ImageModel` |
|
|
625
|
+
| `normalizeVideoModel(raw)` | `VideoModel` |
|
|
626
|
+
| `normalizeInpaintModel(raw)` | `InpaintModel` |
|
|
627
|
+
| `normalizeEmbeddingModel(raw)` | `EmbeddingModel` |
|
|
628
|
+
| `normalizeTtsModel(raw)` | `TtsModel` |
|
|
629
|
+
| `normalizeAsrModel(raw)` | `AsrModel` |
|
|
630
|
+
| `normalizeUpscaleModel(raw)` | `UpscaleModel` |
|
|
631
|
+
|
|
632
|
+
#### Type Inference
|
|
633
|
+
|
|
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):
|
|
635
|
+
|
|
636
|
+
```ts
|
|
637
|
+
import { inferTypeFromId, MODEL_ID_TYPE_PATTERNS } from "open-model-selector/utils"
|
|
638
|
+
|
|
639
|
+
inferTypeFromId("dall-e-3") // "image"
|
|
640
|
+
inferTypeFromId("whisper-large-v3") // "asr"
|
|
641
|
+
inferTypeFromId("gpt-4o") // undefined (no match → caller defaults to "text")
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
`MODEL_ID_TYPE_PATTERNS` is the `Array<[RegExp, ModelType]>` used internally. It's exported so you can inspect the built-in rules or extend them for custom providers.
|
|
645
|
+
|
|
646
|
+
#### Low-Level Helpers
|
|
647
|
+
|
|
648
|
+
| Function | Signature | Description |
|
|
649
|
+
|----------|-----------|-------------|
|
|
650
|
+
| `extractBaseFields` | `(raw, type) → Omit<BaseModel, 'type'>` | Extracts shared `BaseModel` fields from a raw API object. Handles both Venice's nested `model_spec` format and flat top-level fields. Useful when writing custom per-type normalizers. |
|
|
651
|
+
| `toNum` | `(v: unknown) → number \| undefined` | Safely coerces an unknown value to a number. Returns `undefined` for `undefined`, `null`, empty strings, and `NaN`. Used internally by all normalizers. |
|
|
652
|
+
|
|
653
|
+
### Helper Utilities
|
|
654
|
+
|
|
655
|
+
These are exported from `"open-model-selector/utils"` (and also re-exported from the main entry):
|
|
656
|
+
|
|
657
|
+
```ts
|
|
658
|
+
import { isDeprecated } from "open-model-selector/utils"
|
|
659
|
+
// or equivalently:
|
|
660
|
+
import { isDeprecated } from "open-model-selector"
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
| Function | Signature | Description |
|
|
664
|
+
|----------|-----------|-------------|
|
|
665
|
+
| `isDeprecated` | `(dateStr: string) → boolean` | Returns `true` if the given ISO 8601 date string is in the past. Handles date-only strings (`"2025-01-15"`) by normalizing to UTC. Returns `false` for invalid dates. |
|
|
666
|
+
|
|
667
|
+
---
|
|
668
|
+
|
|
669
|
+
## Framework Integration
|
|
670
|
+
|
|
671
|
+
### `"use client"` & React Server Components
|
|
672
|
+
|
|
673
|
+
All component and hook modules include a `"use client"` directive — they are safe to import in Next.js App Router, Remix, and other RSC-aware frameworks without extra wrappers.
|
|
674
|
+
|
|
675
|
+
The **`open-model-selector/utils`** sub-path does **not** include `"use client"` and is safe to import directly in Server Components:
|
|
676
|
+
|
|
677
|
+
```tsx
|
|
678
|
+
// ✅ Server Component — works fine
|
|
679
|
+
import { formatPrice, formatContextLength } from "open-model-selector/utils"
|
|
680
|
+
|
|
681
|
+
// ✅ Server Component — type-only imports are always safe
|
|
682
|
+
import type { TextModel, AnyModel } from "open-model-selector"
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
If you need the components or `useModels` hook, import them in a Client Component (any file with `"use client"` at the top, or rendered inside one).
|
|
686
|
+
|
|
687
|
+
### SSR Compatibility
|
|
688
|
+
|
|
689
|
+
The library is SSR-safe:
|
|
690
|
+
|
|
691
|
+
- An isomorphic `useLayoutEffect` pattern is used internally, so there are no React warnings during server-side rendering.
|
|
692
|
+
- Works with **Next.js** (App Router and Pages Router), **Remix**, **Gatsby**, and any other SSR framework.
|
|
693
|
+
- Tooltips render via a `createPortal` call — they require a DOM environment, but this is handled automatically since the portal only mounts after hydration.
|
|
694
|
+
|
|
695
|
+
### Security
|
|
696
|
+
|
|
697
|
+
- **API keys** are sent as an `Authorization: Bearer` header, never as URL query parameters.
|
|
698
|
+
- **Error messages** from failed requests are constructed from `response.status` and `response.statusText` only — user-supplied content is not interpolated into the DOM.
|
|
699
|
+
- **Client-side key exposure:** Any `apiKey` passed as a prop is visible in browser DevTools. For production apps, use one of:
|
|
700
|
+
- A server-side proxy that injects the key (pass a relative `baseUrl` and a custom `fetcher`)
|
|
701
|
+
- Environment variables scoped to the server (e.g., `VENICE_API_KEY`) with a thin API route
|
|
702
|
+
- A scoped/read-only API key with minimal permissions
|
|
703
|
+
|
|
704
|
+
#### Recommended: Backend Proxy Pattern
|
|
705
|
+
|
|
706
|
+
Instead of passing `apiKey` directly, use a server-side proxy and the `fetcher` prop:
|
|
707
|
+
|
|
708
|
+
```tsx
|
|
709
|
+
// Next.js API Route: app/api/models/route.ts
|
|
710
|
+
import { NextResponse } from 'next/server'
|
|
711
|
+
|
|
712
|
+
export async function GET() {
|
|
713
|
+
const res = await fetch('https://api.venice.ai/api/v1/models', {
|
|
714
|
+
headers: { Authorization: `Bearer ${process.env.VENICE_API_KEY}` },
|
|
715
|
+
})
|
|
716
|
+
return NextResponse.json(await res.json())
|
|
717
|
+
}
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
```tsx
|
|
721
|
+
// Client component
|
|
722
|
+
<ModelSelector
|
|
723
|
+
baseUrl="/api"
|
|
724
|
+
fetcher={(url, init) => fetch(url, init)}
|
|
725
|
+
onChange={(id) => console.log(id)}
|
|
726
|
+
/>
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
This keeps your API key server-side and never exposes it to the browser.
|
|
730
|
+
|
|
731
|
+
### Accessibility
|
|
732
|
+
|
|
733
|
+
The component implements the [ARIA combobox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) and is designed to work well with screen readers and keyboard-only navigation.
|
|
734
|
+
|
|
735
|
+
**Combobox & Popover:**
|
|
736
|
+
- Trigger button has `role="combobox"` with `aria-expanded`, `aria-haspopup="listbox"`, and `aria-controls` linking to the listbox
|
|
737
|
+
- Dynamic `aria-label` reflects the current selection state
|
|
738
|
+
- Search input is labeled (`aria-label="Search models"`)
|
|
739
|
+
- Loading state uses `role="status"` with `aria-live="polite"`; errors use `role="alert"`
|
|
740
|
+
|
|
741
|
+
**Keyboard Navigation** (provided by [cmdk](https://cmdk.paco.me/) and [Radix Popover](https://www.radix-ui.com/primitives/docs/components/popover)):
|
|
742
|
+
- `↑` / `↓` — navigate the model list
|
|
743
|
+
- `Enter` — select the highlighted model
|
|
744
|
+
- `Escape` — close the popover and return focus to the trigger
|
|
745
|
+
- Type-ahead filtering via the search input (auto-focused on open)
|
|
746
|
+
|
|
747
|
+
**Tooltips:**
|
|
748
|
+
- Model detail tooltips use `role="tooltip"` with a unique `id` linked via `aria-describedby` on the trigger
|
|
749
|
+
- Tooltips appear on both hover and keyboard focus (`onFocus` / `onBlur`), and dismiss with `Escape`
|
|
750
|
+
|
|
751
|
+
---
|
|
752
|
+
|
|
753
|
+
## Development
|
|
754
|
+
|
|
755
|
+
### Scripts
|
|
756
|
+
|
|
757
|
+
| Command | Description |
|
|
758
|
+
|---------|-------------|
|
|
759
|
+
| `npm run build` | Build with tsup (CJS + ESM + types + sourcemaps). Cleans `dist/` first via `prebuild`. |
|
|
760
|
+
| `npm run dev` | Build in watch mode for development. |
|
|
761
|
+
| `npm run typecheck` | Run TypeScript type checking (`tsc`). |
|
|
762
|
+
| `npm test` | Run tests with Vitest. |
|
|
763
|
+
| `npm run test:watch` | Run tests in watch mode. |
|
|
764
|
+
| `npm run storybook` | Start Storybook dev server on port 6006. |
|
|
765
|
+
| `npm run build-storybook` | Build static Storybook site. |
|
|
766
|
+
|
|
767
|
+
> `npm run prepublishOnly` runs typecheck → test → build automatically before publishing.
|
|
768
|
+
|
|
769
|
+
### Testing
|
|
770
|
+
|
|
771
|
+
The project uses a layered testing strategy:
|
|
772
|
+
|
|
773
|
+
- **Unit tests**: Vitest with jsdom environment
|
|
774
|
+
- **Component tests**: `@testing-library/react` + `@testing-library/user-event`
|
|
775
|
+
- **Browser tests**: Storybook + Playwright via `@storybook/addon-vitest` and `@vitest/browser-playwright`
|
|
776
|
+
|
|
777
|
+
Test files:
|
|
778
|
+
|
|
779
|
+
- [`src/components/model-selector.test.tsx`](src/components/model-selector.test.tsx) — Component tests
|
|
780
|
+
- [`src/hooks/use-models.test.tsx`](src/hooks/use-models.test.tsx) — Hook tests
|
|
781
|
+
- [`src/utils/format.test.ts`](src/utils/format.test.ts) — Format utility tests
|
|
782
|
+
- [`src/utils/normalizers/normalizers.test.ts`](src/utils/normalizers/normalizers.test.ts) — Normalizer tests
|
|
783
|
+
|
|
784
|
+
```bash
|
|
785
|
+
# Run all tests
|
|
786
|
+
npm test
|
|
787
|
+
|
|
788
|
+
# Run tests in watch mode
|
|
789
|
+
npm run test:watch
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
### Storybook
|
|
793
|
+
|
|
794
|
+
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
|
+
|
|
796
|
+
```bash
|
|
797
|
+
npm run storybook
|
|
798
|
+
# Opens at http://localhost:6006
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
### Contributing
|
|
802
|
+
|
|
803
|
+
1. Fork the repository
|
|
804
|
+
2. Create a feature branch (`git checkout -b feature/my-feature`)
|
|
805
|
+
3. Make your changes
|
|
806
|
+
4. Run `npm run typecheck && npm test` to verify
|
|
807
|
+
5. Commit and push
|
|
808
|
+
6. Open a Pull Request
|
|
809
|
+
|
|
810
|
+
---
|
|
811
|
+
|
|
812
|
+
## License
|
|
813
|
+
|
|
814
|
+
MIT — see [LICENSE](LICENSE) for details.
|
|
815
|
+
|
|
816
|
+
---
|
|
817
|
+
|
|
818
|
+
## Links
|
|
819
|
+
|
|
820
|
+
- [npm](https://www.npmjs.com/package/open-model-selector)
|
|
821
|
+
- [GitHub](https://github.com/sethbang/open-model-selector)
|
|
822
|
+
- [Changelog](CHANGELOG.md)
|
|
823
|
+
- [Issues](https://github.com/sethbang/open-model-selector/issues)
|
|
824
|
+
- [Venice.ai](https://venice.ai) — recommended OpenAI-compatible API provider, offering both frontier models and open source models.
|