react-openrouter-model-picker 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/LICENSE +21 -0
- package/README.md +126 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +211 -0
- package/dist/index.js +473 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +230 -0
- package/package.json +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bartlomiej Zimny
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# react-openrouter-model-picker
|
|
2
|
+
|
|
3
|
+
A searchable React picker over **every** model available on [OpenRouter](https://openrouter.ai), with live prices per 1M tokens on each row. Text search across id, name and vendor, vendor filter, sorting (name / cheapest / newest / context), keyboard navigation, dark mode, English and Polish labels.
|
|
4
|
+
|
|
5
|
+
No Tailwind, no state library, no OpenRouter API key required for the list. React 18+.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm i react-openrouter-model-picker
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { OpenRouterModelPicker, useOpenRouterModels } from 'react-openrouter-model-picker';
|
|
19
|
+
import 'react-openrouter-model-picker/styles.css';
|
|
20
|
+
|
|
21
|
+
function ModelSelect() {
|
|
22
|
+
const [model, setModel] = useState('');
|
|
23
|
+
const { models, loading, refreshing, error, refresh, fetchedAt } = useOpenRouterModels();
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<OpenRouterModelPicker
|
|
27
|
+
label="Model"
|
|
28
|
+
models={models}
|
|
29
|
+
loading={loading}
|
|
30
|
+
refreshing={refreshing}
|
|
31
|
+
error={error}
|
|
32
|
+
onRefresh={refresh}
|
|
33
|
+
fetchedAt={fetchedAt}
|
|
34
|
+
value={model}
|
|
35
|
+
onChange={setModel}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`useOpenRouterModels` calls `https://openrouter.ai/api/v1/models` directly (public, no key), keeps the list in a module-level cache for 10 minutes and shares one request between all pickers on the page.
|
|
42
|
+
|
|
43
|
+
## Loading models your own way
|
|
44
|
+
|
|
45
|
+
The component is *headless on the data side*: it only needs a `models` array. Any of these work:
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
// 1. With an API key (returns only models the key can call)
|
|
49
|
+
useOpenRouterModels({ apiKey });
|
|
50
|
+
|
|
51
|
+
// 2. Through your backend proxy (auth, caching, rate limits stay server-side)
|
|
52
|
+
useOpenRouterModels({
|
|
53
|
+
fetcher: () => fetch('/api/models').then((r) => r.json()).then(normalizeModelsResponse),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// 3. With TanStack Query / SWR — skip the hook entirely
|
|
57
|
+
const { data } = useQuery({ queryKey: ['models'], queryFn: () => fetchOpenRouterModels() });
|
|
58
|
+
<OpenRouterModelPicker models={data ?? []} … />
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`normalizeModel`, `normalizeModelsResponse` and `fetchOpenRouterModels` are plain TypeScript and run on a server too (Node 18+), so an API can share the exact same `OpenRouterModel` shape and price normalization as the UI.
|
|
62
|
+
|
|
63
|
+
## Props
|
|
64
|
+
|
|
65
|
+
| Prop | Type | Description |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `models` | `OpenRouterModel[]` | Required. Normalized list. |
|
|
68
|
+
| `value` / `onChange` | `string` / `(id: string) => void` | Selected model id. `""` means none / automatic. |
|
|
69
|
+
| `loading`, `refreshing`, `error` | | Passed through from your loader. |
|
|
70
|
+
| `onRefresh` | `() => void` | Renders a "Refresh prices" button. |
|
|
71
|
+
| `fetchedAt` | `string \| Date` | Shown as "prices as of 10:14" next to the label. |
|
|
72
|
+
| `defaultModelId` | `string` | Marked in the list; shown under the "automatic" entry. |
|
|
73
|
+
| `allowAuto` | `boolean` | Adds an "automatic" entry mapping to `""`. |
|
|
74
|
+
| `label` | `ReactNode` | Label above the control. |
|
|
75
|
+
| `compact` | `boolean` | Tighter paddings, no detail chips. |
|
|
76
|
+
| `labels` | `Partial<PickerLabels>` | Text overrides. `plLabels` ships Polish. |
|
|
77
|
+
| `vendorLabels` | `Record<string,string>` | Vendor id → display name. |
|
|
78
|
+
| `filter` | `(m) => boolean` | e.g. `m => m.supportsJsonMode`. |
|
|
79
|
+
| `initialSort` | `'name' \| 'cheapest' \| 'newest' \| 'context'` | |
|
|
80
|
+
| `theme` | `'light' \| 'dark'` | Force a theme (default follows the OS). |
|
|
81
|
+
| `disabled`, `className`, `id` | | |
|
|
82
|
+
|
|
83
|
+
### `OpenRouterModel`
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
{
|
|
87
|
+
id: 'openai/gpt-5', name: 'OpenAI: GPT-5', vendor: 'openai', created, description,
|
|
88
|
+
contextLength: 400000, maxCompletionTokens, inputModalities, outputModalities,
|
|
89
|
+
supportedParameters, builtInWebSearch, supportsJsonMode, supportsReasoning,
|
|
90
|
+
pricing: { promptPerM: 1.25, completionPerM: 10, perRequest: 0, perWebSearch: 0.01,
|
|
91
|
+
reasoningPerM, cacheReadPerM, perImage, isFree: false }
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Prices are USD per **1,000,000 tokens** (OpenRouter reports per-token strings; the library converts).
|
|
96
|
+
|
|
97
|
+
## Theming
|
|
98
|
+
|
|
99
|
+
All colours, radii and fonts are CSS custom properties on `.orp`. Override them on any ancestor:
|
|
100
|
+
|
|
101
|
+
```css
|
|
102
|
+
.orp {
|
|
103
|
+
--orp-accent: #0ea5e9;
|
|
104
|
+
--orp-bg: var(--card-bg);
|
|
105
|
+
--orp-surface: var(--input-bg);
|
|
106
|
+
--orp-border: var(--input-border);
|
|
107
|
+
--orp-fg: var(--foreground);
|
|
108
|
+
--orp-muted: var(--muted);
|
|
109
|
+
--orp-radius: 12px;
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Dark mode follows `prefers-color-scheme`; pass `theme="light" | "dark"` to force one. The full variable list is at the top of `styles.css`.
|
|
114
|
+
|
|
115
|
+
## Development
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npm i
|
|
119
|
+
npm run dev # demo at http://localhost:5173 against the live OpenRouter list
|
|
120
|
+
npm test
|
|
121
|
+
npm run build # dist/index.js, dist/index.cjs, dist/index.d.ts, dist/styles.css
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("react/jsx-runtime"),i=require("react");function U(e){return e==null?"—":e===0?"$0":e<.01?`$${e.toFixed(4)}`:`$${e.toFixed(2)}`}function W(e){if(!e)return"—";if(e>=1e6){const r=(e/1e6).toFixed(1);return`${r.endsWith(".0")?r.slice(0,-2):r}M`}return e>=1e3?`${Math.round(e/1e3)}k`:String(e)}const re={openai:"OpenAI",anthropic:"Anthropic",google:"Google","meta-llama":"Meta",mistralai:"Mistral",perplexity:"Perplexity","x-ai":"xAI",deepseek:"DeepSeek",qwen:"Qwen",cohere:"Cohere",amazon:"Amazon",microsoft:"Microsoft",nvidia:"NVIDIA",moonshotai:"Moonshot","z-ai":"Z.ai",openrouter:"OpenRouter"};function V(e,r){return r?.[e]??re[e]??e.replace(/-/g," ").replace(/\b\w/g,n=>n.toUpperCase())}function J(e){return e.pricing.promptPerM*.75+e.pricing.completionPerM*.25+e.pricing.perRequest*1e3}const te={placeholder:"Select a model…",searchPlaceholder:"Search: gpt-5, claude, sonar, gemini, llama…",allVendors:"All vendors",sortName:"Name",sortCheapest:"Cheapest",sortNewest:"Newest",sortContext:"Context",refresh:"Refresh prices",refreshing:"refreshing…",loading:"Loading models from OpenRouter…",loadError:"Could not load the model list",noMatches:e=>`No models match “${e}”.`,countOf:(e,r)=>`${e} of ${r}`,modelsCount:e=>`${e} OpenRouter models`,pricesFrom:e=>`prices as of ${e}`,auto:"Automatic (server default)",now:"now:",isDefault:"default",free:"free",web:"web",json:"json",reasoning:"reasoning",context:"context",maxOutput:"max output",perRequest:"per request",priceTitle:"USD per 1M tokens: input / output",webTitle:"Built-in web search",jsonTitle:"Supports response_format JSON"},he={placeholder:"Wybierz model…",searchPlaceholder:"Szukaj: gpt-5, claude, sonar, gemini, llama…",allVendors:"Wszyscy producenci",sortName:"Nazwa",sortCheapest:"Najtańsze",sortNewest:"Najnowsze",sortContext:"Kontekst",refresh:"Odśwież ceny",refreshing:"odświeżam…",loading:"Pobieram listę modeli z OpenRoutera…",loadError:"Nie udało się pobrać listy modeli",noMatches:e=>`Brak modeli pasujących do „${e}”.`,countOf:(e,r)=>`${e} z ${r}`,modelsCount:e=>`${e} modeli OpenRouter`,pricesFrom:e=>`ceny z ${e}`,auto:"Automatycznie (domyślny z serwera)",now:"teraz:",isDefault:"domyślny",free:"darmowy",web:"web",json:"json",reasoning:"reasoning",context:"kontekst",maxOutput:"max wyjście",perRequest:"za żądanie",priceTitle:"USD za 1M tokenów: wejście / wyjście",webTitle:"Wbudowane wyszukiwanie w sieci",jsonTitle:"Obsługuje response_format JSON"};function _e(e,r,n){if(!r)return!0;const l=`${e.id} ${e.name} ${V(e.vendor,n)}`.toLowerCase();return r.split(/\s+/).every(c=>l.includes(c))}function fe(e){if(!e)return null;const r=typeof e=="string"?new Date(e):e;return Number.isNaN(r.getTime())?null:r.toLocaleTimeString(void 0,{hour:"2-digit",minute:"2-digit"})}function xe(e){const{models:r,value:n,onChange:l,loading:c,refreshing:p,error:x,onRefresh:d,fetchedAt:R,defaultModelId:g,allowAuto:A,label:N,compact:P,disabled:T,vendorLabels:w,filter:y,initialSort:j="name",theme:v,className:C,id:S}=e,o=i.useMemo(()=>({...te,...e.labels??{}}),[e.labels]),[m,_]=i.useState(!1),[k,G]=i.useState(""),[O,le]=i.useState("all"),[L,ce]=i.useState(j),[z,E]=i.useState(0),B=i.useRef(null),H=i.useRef(null),Z=i.useRef(null),b=i.useMemo(()=>y?r.filter(y):r,[r,y]),u=n?b.find(s=>s.id===n)??r.find(s=>s.id===n):void 0,pe=i.useMemo(()=>{const s=new Map;for(const a of b)s.set(a.vendor,(s.get(a.vendor)??0)+1);return[...s.entries()].sort((a,q)=>q[1]-a[1]||a[0].localeCompare(q[0]))},[b]),M=i.useMemo(()=>{const s=k.trim().toLowerCase(),a=b.filter(h=>(O==="all"||h.vendor===O)&&_e(h,s,w)),q={name:(h,f)=>h.name.localeCompare(f.name),cheapest:(h,f)=>J(h)-J(f)||h.name.localeCompare(f.name),newest:(h,f)=>f.created-h.created||h.name.localeCompare(f.name),context:(h,f)=>(f.contextLength??0)-(h.contextLength??0)||h.name.localeCompare(f.name)};return[...a].sort(q[L])},[b,k,O,L,w]);i.useEffect(()=>{if(!m)return;const s=a=>{B.current&&!B.current.contains(a.target)&&_(!1)};return document.addEventListener("mousedown",s),()=>document.removeEventListener("mousedown",s)},[m]),i.useEffect(()=>{E(0)},[k,O,L]),i.useEffect(()=>{if(!m)return;Z.current?.focus();const s=M.findIndex(a=>a.id===n);s>=0&&E(s)},[m]),i.useEffect(()=>{H.current?.querySelector(`[data-index="${z}"]`)?.scrollIntoView?.({block:"nearest"})},[z,m]);const X=s=>{l(s),_(!1),G("")},ue=s=>{if(s.key==="ArrowDown")s.preventDefault(),E(a=>Math.min(M.length-1,a+1));else if(s.key==="ArrowUp")s.preventDefault(),E(a=>Math.max(0,a-1));else if(s.key==="Enter"){s.preventDefault();const a=M[z];a&&X(a.id)}else s.key==="Escape"&&_(!1)},Y=!!A&&!n,de=u?u.name:n||(Y?o.auto:o.placeholder),ee=fe(R),D=x?x instanceof Error?x.message:String(x):null,me=["orp",m&&"orp--open",P&&"orp--compact",N&&"orp--has-label",C].filter(Boolean).join(" ");return t.jsxs("div",{ref:B,className:me,"data-theme":v,id:S,children:[N&&t.jsxs("div",{className:"orp__label",children:[t.jsx("span",{children:N}),t.jsxs("span",{className:"orp__label-meta",children:[r.length?o.modelsCount(r.length):"",ee&&` · ${o.pricesFrom(ee)}`]})]}),t.jsxs("div",{className:"orp__anchor",children:[t.jsx("button",{type:"button",className:"orp__summary",disabled:T,"aria-haspopup":"listbox","aria-expanded":m,onClick:()=>_(s=>!s),children:t.jsxs("div",{className:"orp__summary-inner",children:[t.jsxs("div",{className:"orp__summary-text",children:[t.jsx("p",{className:"orp__summary-title",children:de}),u?t.jsx("p",{className:"orp__summary-sub orp__mono",children:u.id}):Y&&g?t.jsxs("p",{className:"orp__summary-sub",children:[o.now," ",t.jsx("span",{className:"orp__mono",children:g})]}):c?t.jsx("p",{className:"orp__summary-sub",children:o.loading}):D?t.jsx("p",{className:"orp__summary-sub orp__summary-sub--error",children:o.loadError}):null]}),t.jsxs("div",{className:"orp__summary-right",children:[u&&t.jsx(se,{model:u,t:o}),t.jsx("svg",{className:"orp__chevron",width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",children:t.jsx("polyline",{points:"6,9 12,15 18,9"})})]})]})}),m&&t.jsxs("div",{className:"orp__popover",children:[t.jsxs("div",{className:"orp__toolbar",children:[t.jsxs("div",{className:"orp__search",children:[t.jsxs("svg",{className:"orp__search-icon",width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",children:[t.jsx("circle",{cx:"11",cy:"11",r:"8"}),t.jsx("line",{x1:"21",y1:"21",x2:"16.65",y2:"16.65"})]}),t.jsx("input",{ref:Z,className:"orp__input",value:k,onChange:s=>G(s.target.value),onKeyDown:ue,placeholder:o.searchPlaceholder,"aria-label":o.searchPlaceholder,role:"combobox","aria-expanded":"true","aria-controls":`${S??"orp"}-listbox`})]}),t.jsxs("div",{className:"orp__filters",children:[t.jsxs("select",{className:"orp__select",value:O,onChange:s=>le(s.target.value),"aria-label":o.allVendors,children:[t.jsxs("option",{value:"all",children:[o.allVendors," (",b.length,")"]}),pe.map(([s,a])=>t.jsxs("option",{value:s,children:[V(s,w)," (",a,")"]},s))]}),t.jsx("div",{className:"orp__sorts",role:"group",children:[["name",o.sortName],["cheapest",o.sortCheapest],["newest",o.sortNewest],["context",o.sortContext]].map(([s,a])=>t.jsx("button",{type:"button",onClick:()=>ce(s),className:`orp__sort${L===s?" orp__sort--active":""}`,"aria-pressed":L===s,children:a},s))}),t.jsx("span",{className:"orp__count",children:o.countOf(M.length,b.length)}),d&&t.jsx("button",{type:"button",className:"orp__refresh",onClick:d,disabled:!!p,title:o.refresh,children:p?o.refreshing:o.refresh})]})]}),t.jsxs("div",{ref:H,className:"orp__list",role:"listbox",id:`${S??"orp"}-listbox`,children:[A&&t.jsxs("button",{type:"button",role:"option","aria-selected":!n,className:`orp__auto${n?"":" orp__auto--selected"}`,onClick:()=>{l(""),_(!1)},children:[o.auto,g&&t.jsx("span",{className:"orp__auto-id orp__mono",children:g})]}),c&&!r.length&&t.jsx("p",{className:"orp__message",children:o.loading}),D&&!r.length&&t.jsxs("p",{className:"orp__message orp__message--error",children:[o.loadError,": ",D]}),!c&&!D&&!M.length&&t.jsx("p",{className:"orp__message",children:o.noMatches(k)}),M.map((s,a)=>t.jsx(ge,{model:s,index:a,active:a===z,selected:s.id===n,isDefault:s.id===g,t:o,onHover:()=>E(a),onPick:()=>X(s.id)},s.id))]})]})]}),u&&!P&&t.jsxs("div",{className:"orp__details",children:[t.jsx("span",{className:"orp__chip",children:V(u.vendor,w)}),t.jsxs("span",{className:"orp__chip",children:[o.context," ",W(u.contextLength)]}),u.maxCompletionTokens&&t.jsxs("span",{className:"orp__chip",children:[o.maxOutput," ",W(u.maxCompletionTokens)]}),u.builtInWebSearch&&t.jsx("span",{className:"orp__chip orp__chip--info",children:o.webTitle}),u.supportsJsonMode&&t.jsx("span",{className:"orp__chip orp__chip--ok",children:o.json}),u.supportsReasoning&&t.jsx("span",{className:"orp__chip orp__chip--violet",children:o.reasoning}),u.pricing.perRequest>0&&t.jsxs("span",{className:"orp__chip orp__chip--warn",children:["+",U(u.pricing.perRequest)," ",o.perRequest]}),u.pricing.isFree&&t.jsx("span",{className:"orp__chip orp__chip--ok",children:o.free})]})]})}function ge({model:e,index:r,active:n,selected:l,isDefault:c,t:p,onHover:x,onPick:d}){return t.jsxs("button",{type:"button",role:"option","aria-selected":l,"data-index":r,"data-model-id":e.id,onMouseEnter:x,onClick:d,className:`orp__row${n?" orp__row--active":""}${l?" orp__row--selected":""}`,children:[t.jsxs("div",{className:"orp__row-main",children:[t.jsxs("p",{className:"orp__row-name",children:[e.name,c&&t.jsx("span",{className:"orp__row-default",children:p.isDefault})]}),t.jsx("p",{className:"orp__row-id",children:e.id})]}),t.jsxs("div",{className:"orp__row-side",children:[e.builtInWebSearch&&t.jsx("span",{className:"orp__badge orp__badge--info",title:p.webTitle,children:p.web}),e.supportsJsonMode&&t.jsx("span",{className:"orp__badge orp__badge--ok",title:p.jsonTitle,children:p.json}),t.jsx("span",{className:"orp__ctx",children:W(e.contextLength)}),t.jsx(se,{model:e,t:p})]})]})}function se({model:e,t:r}){return e.pricing.isFree?t.jsx("span",{className:"orp__badge orp__badge--ok",children:r.free}):t.jsxs("span",{className:"orp__price",title:r.priceTitle,children:[U(e.pricing.promptPerM)," ",t.jsx("span",{className:"orp__price-sep",children:"/"})," ",U(e.pricing.completionPerM)]})}const Q="https://openrouter.ai/api/v1/models",je=1e6;function I(e){if(e==null||e==="")return null;const r=typeof e=="number"?e:Number(String(e));return Number.isFinite(r)?r:null}function F(e){const r=I(e);return r==null?null:r*je}function oe(e){const r=e??{},n=F(r.prompt)??0,l=F(r.completion)??0,c=I(r.request)??0;return{promptPerM:n,completionPerM:l,reasoningPerM:F(r.internal_reasoning),perRequest:c,perWebSearch:I(r.web_search)??0,cacheReadPerM:F(r.input_cache_read),perImage:I(r.image)??0,isFree:n===0&&l===0&&c===0}}function ne(e){const r=e;if(!r||typeof r.id!="string")return null;const n=Array.isArray(r.architecture?.output_modalities)?r.architecture.output_modalities:["text"];if(!n.includes("text"))return null;const l=Array.isArray(r.supported_parameters)?r.supported_parameters:[],c=r.id.indexOf("/"),p=c>0?r.id.slice(0,c):"other";return{id:r.id,name:typeof r.name=="string"&&r.name?r.name:r.id,description:typeof r.description=="string"?r.description:"",vendor:p,created:typeof r.created=="number"?r.created:0,contextLength:typeof r.context_length=="number"?r.context_length:r.top_provider?.context_length??null,maxCompletionTokens:typeof r.top_provider?.max_completion_tokens=="number"?r.top_provider.max_completion_tokens:null,inputModalities:Array.isArray(r.architecture?.input_modalities)?r.architecture.input_modalities:["text"],outputModalities:n,supportedParameters:l,builtInWebSearch:p==="perplexity",supportsJsonMode:l.includes("response_format")||l.includes("structured_outputs"),supportsReasoning:l.includes("reasoning")||l.includes("include_reasoning"),pricing:oe(r.pricing)}}function ae(e){const r=e?.data;return Array.isArray(r)?r.map(ne).filter(n=>!!n):[]}async function ie(e={}){const r={Accept:"application/json",...e.headers??{}};e.apiKey&&(r.Authorization=`Bearer ${e.apiKey}`);const n=await fetch(e.url??Q,{headers:r,signal:e.signal});if(!n.ok)throw new Error(`OpenRouter /models returned ${n.status}`);return ae(await n.json())}const be=600*1e3,$=new Map,K=new Map;function Ne(){$.clear()}function we(e={}){const{fetcher:r,ttlMs:n=be,enabled:l=!0,apiKey:c,url:p=Q,headers:x}=e,d=r?`fetcher:${p}`:`${p}|${c??""}`,[R,g]=i.useState(()=>$.get(d)??null),[A,N]=i.useState(!1),[P,T]=i.useState(!1),[w,y]=i.useState(null),j=i.useRef(!0),v=i.useRef(r);v.current=r,i.useEffect(()=>(j.current=!0,()=>{j.current=!1}),[]);const C=i.useCallback(async S=>{const o=$.get(d);if(!S&&o&&Date.now()-o.fetchedAt<n){g(o);return}let m=K.get(d);m||(m=(v.current?v.current():ie({apiKey:c,url:p,headers:x})).then(_=>($.set(d,{models:_,fetchedAt:Date.now()}),_)).finally(()=>K.delete(d)),K.set(d,m)),o?T(!0):N(!0),y(null);try{await m,j.current&&g($.get(d)??null)}catch(_){j.current&&y(_ instanceof Error?_:new Error(String(_)))}finally{j.current&&(N(!1),T(!1))}},[d,n,c,p,x]);return i.useEffect(()=>{l&&C(!1)},[l,C]),{models:R?.models??[],loading:A,refreshing:P,error:w,fetchedAt:R?new Date(R.fetchedAt).toISOString():null,refresh:()=>C(!0)}}exports.DEFAULT_VENDOR_LABELS=re;exports.OPENROUTER_MODELS_URL=Q;exports.OpenRouterModelPicker=xe;exports.blendedPricePerM=J;exports.clearOpenRouterModelsCache=Ne;exports.enLabels=te;exports.fetchOpenRouterModels=ie;exports.formatContext=W;exports.formatPerM=U;exports.normalizeModel=ne;exports.normalizeModelsResponse=ae;exports.normalizePricing=oe;exports.plLabels=he;exports.useOpenRouterModels=we;exports.vendorLabel=V;
|
|
2
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/format.ts","../src/labels.ts","../src/OpenRouterModelPicker.tsx","../src/models.ts","../src/useOpenRouterModels.ts"],"sourcesContent":["import type { OpenRouterModel } from './models';\n\n/** \"$1.25\" / \"$0.0004\" / \"$0\" — USD per 1M tokens. */\nexport function formatPerM(value: number | null | undefined): string {\n if (value == null) return '—';\n if (value === 0) return '$0';\n if (value < 0.01) return `$${value.toFixed(4)}`;\n return `$${value.toFixed(2)}`;\n}\n\n/** \"200k\" / \"1M\" / \"1.5M\" context window. */\nexport function formatContext(tokens: number | null | undefined): string {\n if (!tokens) return '—';\n if (tokens >= 1_000_000) {\n const m = (tokens / 1_000_000).toFixed(1);\n return `${m.endsWith('.0') ? m.slice(0, -2) : m}M`;\n }\n if (tokens >= 1000) return `${Math.round(tokens / 1000)}k`;\n return String(tokens);\n}\n\nexport const DEFAULT_VENDOR_LABELS: Record<string, string> = {\n openai: 'OpenAI', anthropic: 'Anthropic', google: 'Google', 'meta-llama': 'Meta', mistralai: 'Mistral',\n perplexity: 'Perplexity', 'x-ai': 'xAI', deepseek: 'DeepSeek', qwen: 'Qwen', cohere: 'Cohere',\n amazon: 'Amazon', microsoft: 'Microsoft', nvidia: 'NVIDIA', moonshotai: 'Moonshot', 'z-ai': 'Z.ai',\n openrouter: 'OpenRouter',\n};\n\nexport function vendorLabel(vendor: string, overrides?: Record<string, string>): string {\n return overrides?.[vendor] ?? DEFAULT_VENDOR_LABELS[vendor]\n ?? vendor.replace(/-/g, ' ').replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\n/**\n * Blended USD per 1M tokens at a typical 3:1 input:output ratio — used by the\n * \"cheapest\" sort. A per-request fee is counted as ~1000 requests per 1M tokens.\n */\nexport function blendedPricePerM(m: OpenRouterModel): number {\n return m.pricing.promptPerM * 0.75 + m.pricing.completionPerM * 0.25 + m.pricing.perRequest * 1000;\n}\n","export interface PickerLabels {\n placeholder: string;\n searchPlaceholder: string;\n allVendors: string;\n sortName: string;\n sortCheapest: string;\n sortNewest: string;\n sortContext: string;\n refresh: string;\n refreshing: string;\n loading: string;\n loadError: string;\n noMatches: (query: string) => string;\n countOf: (shown: number, total: number) => string;\n modelsCount: (n: number) => string;\n pricesFrom: (time: string) => string;\n auto: string;\n now: string;\n isDefault: string;\n free: string;\n web: string;\n json: string;\n reasoning: string;\n context: string;\n maxOutput: string;\n perRequest: string;\n priceTitle: string;\n webTitle: string;\n jsonTitle: string;\n}\n\nexport const enLabels: PickerLabels = {\n placeholder: 'Select a model…',\n searchPlaceholder: 'Search: gpt-5, claude, sonar, gemini, llama…',\n allVendors: 'All vendors',\n sortName: 'Name',\n sortCheapest: 'Cheapest',\n sortNewest: 'Newest',\n sortContext: 'Context',\n refresh: 'Refresh prices',\n refreshing: 'refreshing…',\n loading: 'Loading models from OpenRouter…',\n loadError: 'Could not load the model list',\n noMatches: (q) => `No models match “${q}”.`,\n countOf: (shown, total) => `${shown} of ${total}`,\n modelsCount: (n) => `${n} OpenRouter models`,\n pricesFrom: (t) => `prices as of ${t}`,\n auto: 'Automatic (server default)',\n now: 'now:',\n isDefault: 'default',\n free: 'free',\n web: 'web',\n json: 'json',\n reasoning: 'reasoning',\n context: 'context',\n maxOutput: 'max output',\n perRequest: 'per request',\n priceTitle: 'USD per 1M tokens: input / output',\n webTitle: 'Built-in web search',\n jsonTitle: 'Supports response_format JSON',\n};\n\nexport const plLabels: PickerLabels = {\n placeholder: 'Wybierz model…',\n searchPlaceholder: 'Szukaj: gpt-5, claude, sonar, gemini, llama…',\n allVendors: 'Wszyscy producenci',\n sortName: 'Nazwa',\n sortCheapest: 'Najtańsze',\n sortNewest: 'Najnowsze',\n sortContext: 'Kontekst',\n refresh: 'Odśwież ceny',\n refreshing: 'odświeżam…',\n loading: 'Pobieram listę modeli z OpenRoutera…',\n loadError: 'Nie udało się pobrać listy modeli',\n noMatches: (q) => `Brak modeli pasujących do „${q}”.`,\n countOf: (shown, total) => `${shown} z ${total}`,\n modelsCount: (n) => `${n} modeli OpenRouter`,\n pricesFrom: (t) => `ceny z ${t}`,\n auto: 'Automatycznie (domyślny z serwera)',\n now: 'teraz:',\n isDefault: 'domyślny',\n free: 'darmowy',\n web: 'web',\n json: 'json',\n reasoning: 'reasoning',\n context: 'kontekst',\n maxOutput: 'max wyjście',\n perRequest: 'za żądanie',\n priceTitle: 'USD za 1M tokenów: wejście / wyjście',\n webTitle: 'Wbudowane wyszukiwanie w sieci',\n jsonTitle: 'Obsługuje response_format JSON',\n};\n","import { useEffect, useMemo, useRef, useState, type KeyboardEvent, type ReactNode } from 'react';\nimport type { OpenRouterModel } from './models';\nimport { blendedPricePerM, formatContext, formatPerM, vendorLabel } from './format';\nimport { enLabels, type PickerLabels } from './labels';\n\nexport type PickerSortKey = 'name' | 'cheapest' | 'newest' | 'context';\n\nexport interface OpenRouterModelPickerProps {\n /** Normalized model list — from `useOpenRouterModels`, your own query, or a server. */\n models: OpenRouterModel[];\n /** Selected model id. Empty/undefined = nothing selected (or \"automatic\" when `allowAuto`). */\n value?: string;\n /** Called with the model id, or \"\" when the automatic option is chosen. */\n onChange: (id: string) => void;\n loading?: boolean;\n refreshing?: boolean;\n error?: unknown;\n /** Shown as a \"Refresh prices\" button inside the dropdown. */\n onRefresh?: () => void;\n /** ISO string or Date of the list currently shown — rendered next to the label. */\n fetchedAt?: string | Date | null;\n /** Model the server would use when none is chosen; marked in the list and shown under \"automatic\". */\n defaultModelId?: string;\n /** Adds an \"automatic\" entry at the top of the list that maps to value \"\". */\n allowAuto?: boolean;\n /** Label above the control. Omit for a bare control. */\n label?: ReactNode;\n /** Tighter paddings, no detail chips under the control. */\n compact?: boolean;\n disabled?: boolean;\n /** Partial overrides merged over the English defaults; import `plLabels` for Polish. */\n labels?: Partial<PickerLabels>;\n /** Vendor id → display name overrides (e.g. { 'x-ai': 'xAI' }). */\n vendorLabels?: Record<string, string>;\n /** Restrict the list (e.g. only models with JSON mode). */\n filter?: (model: OpenRouterModel) => boolean;\n initialSort?: PickerSortKey;\n /** Force light/dark regardless of the OS setting. */\n theme?: 'light' | 'dark';\n className?: string;\n id?: string;\n}\n\nfunction matches(m: OpenRouterModel, q: string, vendors?: Record<string, string>): boolean {\n if (!q) return true;\n const hay = `${m.id} ${m.name} ${vendorLabel(m.vendor, vendors)}`.toLowerCase();\n return q.split(/\\s+/).every((term) => hay.includes(term));\n}\n\nfunction timeLabel(value: string | Date | null | undefined): string | null {\n if (!value) return null;\n const d = typeof value === 'string' ? new Date(value) : value;\n return Number.isNaN(d.getTime()) ? null : d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });\n}\n\n/**\n * Searchable picker over the full OpenRouter model list: text search across\n * id / name / vendor, vendor filter, sorting, keyboard navigation, and a price\n * per 1M tokens on every row — choosing a model is choosing its price list.\n */\nexport function OpenRouterModelPicker(props: OpenRouterModelPickerProps) {\n const {\n models, value, onChange, loading, refreshing, error, onRefresh, fetchedAt, defaultModelId,\n allowAuto, label, compact, disabled, vendorLabels, filter, initialSort = 'name', theme, className, id,\n } = props;\n const t: PickerLabels = useMemo(() => ({ ...enLabels, ...(props.labels ?? {}) }), [props.labels]);\n\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState('');\n const [vendor, setVendor] = useState('all');\n const [sort, setSort] = useState<PickerSortKey>(initialSort);\n const [cursor, setCursor] = useState(0);\n const rootRef = useRef<HTMLDivElement>(null);\n const listRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n\n const visible = useMemo(() => (filter ? models.filter(filter) : models), [models, filter]);\n const selected = value ? visible.find((m) => m.id === value) ?? models.find((m) => m.id === value) : undefined;\n\n const vendors = useMemo(() => {\n const counts = new Map<string, number>();\n for (const m of visible) counts.set(m.vendor, (counts.get(m.vendor) ?? 0) + 1);\n return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));\n }, [visible]);\n\n const filtered = useMemo(() => {\n const q = query.trim().toLowerCase();\n const list = visible.filter((m) => (vendor === 'all' || m.vendor === vendor) && matches(m, q, vendorLabels));\n const sorters: Record<PickerSortKey, (a: OpenRouterModel, b: OpenRouterModel) => number> = {\n name: (a, b) => a.name.localeCompare(b.name),\n cheapest: (a, b) => blendedPricePerM(a) - blendedPricePerM(b) || a.name.localeCompare(b.name),\n newest: (a, b) => b.created - a.created || a.name.localeCompare(b.name),\n context: (a, b) => (b.contextLength ?? 0) - (a.contextLength ?? 0) || a.name.localeCompare(b.name),\n };\n return [...list].sort(sorters[sort]);\n }, [visible, query, vendor, sort, vendorLabels]);\n\n // Close on outside click.\n useEffect(() => {\n if (!open) return;\n const onDown = (e: MouseEvent) => {\n if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpen(false);\n };\n document.addEventListener('mousedown', onDown);\n return () => document.removeEventListener('mousedown', onDown);\n }, [open]);\n\n useEffect(() => { setCursor(0); }, [query, vendor, sort]);\n\n useEffect(() => {\n if (!open) return;\n inputRef.current?.focus();\n const idx = filtered.findIndex((m) => m.id === value);\n if (idx >= 0) setCursor(idx);\n // Only when opening: re-running on every filter change would fight the cursor reset above.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open]);\n\n useEffect(() => {\n const el = listRef.current?.querySelector<HTMLElement>(`[data-index=\"${cursor}\"]`);\n el?.scrollIntoView?.({ block: 'nearest' });\n }, [cursor, open]);\n\n const pick = (modelId: string) => { onChange(modelId); setOpen(false); setQuery(''); };\n\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'ArrowDown') { e.preventDefault(); setCursor((c) => Math.min(filtered.length - 1, c + 1)); }\n else if (e.key === 'ArrowUp') { e.preventDefault(); setCursor((c) => Math.max(0, c - 1)); }\n else if (e.key === 'Enter') { e.preventDefault(); const m = filtered[cursor]; if (m) pick(m.id); }\n else if (e.key === 'Escape') { setOpen(false); }\n };\n\n const isAuto = !!allowAuto && !value;\n const summaryTitle = selected ? selected.name : value ? value : isAuto ? t.auto : t.placeholder;\n const priceTime = timeLabel(fetchedAt);\n const errorMessage = error ? (error instanceof Error ? error.message : String(error)) : null;\n\n const rootClass = ['orp', open && 'orp--open', compact && 'orp--compact', label && 'orp--has-label', className]\n .filter(Boolean).join(' ');\n\n return (\n <div ref={rootRef} className={rootClass} data-theme={theme} id={id}>\n {label && (\n <div className=\"orp__label\">\n <span>{label}</span>\n <span className=\"orp__label-meta\">\n {models.length ? t.modelsCount(models.length) : ''}\n {priceTime && ` · ${t.pricesFrom(priceTime)}`}\n </span>\n </div>\n )}\n\n <div className=\"orp__anchor\">\n <button\n type=\"button\"\n className=\"orp__summary\"\n disabled={disabled}\n aria-haspopup=\"listbox\"\n aria-expanded={open}\n onClick={() => setOpen((o) => !o)}\n >\n <div className=\"orp__summary-inner\">\n <div className=\"orp__summary-text\">\n <p className=\"orp__summary-title\">{summaryTitle}</p>\n {selected ? (\n <p className=\"orp__summary-sub orp__mono\">{selected.id}</p>\n ) : isAuto && defaultModelId ? (\n <p className=\"orp__summary-sub\">{t.now} <span className=\"orp__mono\">{defaultModelId}</span></p>\n ) : loading ? (\n <p className=\"orp__summary-sub\">{t.loading}</p>\n ) : errorMessage ? (\n <p className=\"orp__summary-sub orp__summary-sub--error\">{t.loadError}</p>\n ) : null}\n </div>\n <div className=\"orp__summary-right\">\n {selected && <PriceTag model={selected} t={t} />}\n <svg className=\"orp__chevron\" width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <polyline points=\"6,9 12,15 18,9\" />\n </svg>\n </div>\n </div>\n </button>\n\n {open && (\n <div className=\"orp__popover\">\n <div className=\"orp__toolbar\">\n <div className=\"orp__search\">\n <svg className=\"orp__search-icon\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <circle cx=\"11\" cy=\"11\" r=\"8\" /><line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\" />\n </svg>\n <input\n ref={inputRef}\n className=\"orp__input\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n onKeyDown={onKeyDown}\n placeholder={t.searchPlaceholder}\n aria-label={t.searchPlaceholder}\n role=\"combobox\"\n aria-expanded=\"true\"\n aria-controls={`${id ?? 'orp'}-listbox`}\n />\n </div>\n <div className=\"orp__filters\">\n <select className=\"orp__select\" value={vendor} onChange={(e) => setVendor(e.target.value)} aria-label={t.allVendors}>\n <option value=\"all\">{t.allVendors} ({visible.length})</option>\n {vendors.map(([v, n]) => <option key={v} value={v}>{vendorLabel(v, vendorLabels)} ({n})</option>)}\n </select>\n <div className=\"orp__sorts\" role=\"group\">\n {([['name', t.sortName], ['cheapest', t.sortCheapest], ['newest', t.sortNewest], ['context', t.sortContext]] as const).map(([key, text]) => (\n <button key={key} type=\"button\" onClick={() => setSort(key)}\n className={`orp__sort${sort === key ? ' orp__sort--active' : ''}`} aria-pressed={sort === key}>\n {text}\n </button>\n ))}\n </div>\n <span className=\"orp__count\">{t.countOf(filtered.length, visible.length)}</span>\n {onRefresh && (\n <button type=\"button\" className=\"orp__refresh\" onClick={onRefresh} disabled={!!refreshing} title={t.refresh}>\n {refreshing ? t.refreshing : t.refresh}\n </button>\n )}\n </div>\n </div>\n\n <div ref={listRef} className=\"orp__list\" role=\"listbox\" id={`${id ?? 'orp'}-listbox`}>\n {allowAuto && (\n <button type=\"button\" role=\"option\" aria-selected={!value}\n className={`orp__auto${!value ? ' orp__auto--selected' : ''}`}\n onClick={() => { onChange(''); setOpen(false); }}>\n {t.auto}\n {defaultModelId && <span className=\"orp__auto-id orp__mono\">{defaultModelId}</span>}\n </button>\n )}\n {loading && !models.length && <p className=\"orp__message\">{t.loading}</p>}\n {errorMessage && !models.length && <p className=\"orp__message orp__message--error\">{t.loadError}: {errorMessage}</p>}\n {!loading && !errorMessage && !filtered.length && <p className=\"orp__message\">{t.noMatches(query)}</p>}\n {filtered.map((m, i) => (\n <ModelRow key={m.id} model={m} index={i} active={i === cursor} selected={m.id === value}\n isDefault={m.id === defaultModelId} t={t} onHover={() => setCursor(i)} onPick={() => pick(m.id)} />\n ))}\n </div>\n </div>\n )}\n </div>\n\n {selected && !compact && (\n <div className=\"orp__details\">\n <span className=\"orp__chip\">{vendorLabel(selected.vendor, vendorLabels)}</span>\n <span className=\"orp__chip\">{t.context} {formatContext(selected.contextLength)}</span>\n {selected.maxCompletionTokens && <span className=\"orp__chip\">{t.maxOutput} {formatContext(selected.maxCompletionTokens)}</span>}\n {selected.builtInWebSearch && <span className=\"orp__chip orp__chip--info\">{t.webTitle}</span>}\n {selected.supportsJsonMode && <span className=\"orp__chip orp__chip--ok\">{t.json}</span>}\n {selected.supportsReasoning && <span className=\"orp__chip orp__chip--violet\">{t.reasoning}</span>}\n {selected.pricing.perRequest > 0 && <span className=\"orp__chip orp__chip--warn\">+{formatPerM(selected.pricing.perRequest)} {t.perRequest}</span>}\n {selected.pricing.isFree && <span className=\"orp__chip orp__chip--ok\">{t.free}</span>}\n </div>\n )}\n </div>\n );\n}\n\nfunction ModelRow({ model, index, active, selected, isDefault, t, onHover, onPick }: {\n model: OpenRouterModel; index: number; active: boolean; selected: boolean; isDefault: boolean;\n t: PickerLabels; onHover: () => void; onPick: () => void;\n}) {\n return (\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={selected}\n data-index={index}\n data-model-id={model.id}\n onMouseEnter={onHover}\n onClick={onPick}\n className={`orp__row${active ? ' orp__row--active' : ''}${selected ? ' orp__row--selected' : ''}`}\n >\n <div className=\"orp__row-main\">\n <p className=\"orp__row-name\">\n {model.name}\n {isDefault && <span className=\"orp__row-default\">{t.isDefault}</span>}\n </p>\n <p className=\"orp__row-id\">{model.id}</p>\n </div>\n <div className=\"orp__row-side\">\n {model.builtInWebSearch && <span className=\"orp__badge orp__badge--info\" title={t.webTitle}>{t.web}</span>}\n {model.supportsJsonMode && <span className=\"orp__badge orp__badge--ok\" title={t.jsonTitle}>{t.json}</span>}\n <span className=\"orp__ctx\">{formatContext(model.contextLength)}</span>\n <PriceTag model={model} t={t} />\n </div>\n </button>\n );\n}\n\n/** \"$1.25 / $10.00\" — USD per 1M tokens, input / output. */\nfunction PriceTag({ model, t }: { model: OpenRouterModel; t: PickerLabels }) {\n if (model.pricing.isFree) return <span className=\"orp__badge orp__badge--ok\">{t.free}</span>;\n return (\n <span className=\"orp__price\" title={t.priceTitle}>\n {formatPerM(model.pricing.promptPerM)} <span className=\"orp__price-sep\">/</span> {formatPerM(model.pricing.completionPerM)}\n </span>\n );\n}\n","/**\n * Normalization of the OpenRouter `GET /api/v1/models` response.\n *\n * Pure TypeScript, no React — safe to reuse on a server. OpenRouter reports\n * prices as STRINGS per single token (e.g. \"0.000008\"); here they become\n * numbers per 1,000,000 tokens, which is how price lists are usually read.\n */\n\nexport const OPENROUTER_MODELS_URL = 'https://openrouter.ai/api/v1/models';\n\nexport interface ModelPricing {\n /** USD per 1M input (prompt) tokens. */\n promptPerM: number;\n /** USD per 1M output (completion) tokens. */\n completionPerM: number;\n /** USD per 1M reasoning tokens, when the provider bills them separately. */\n reasoningPerM: number | null;\n /** Flat USD fee per request (some providers charge it on top of tokens). */\n perRequest: number;\n /** USD per web search performed by a model with built-in search. */\n perWebSearch: number;\n /** USD per 1M tokens read from prompt cache. */\n cacheReadPerM: number | null;\n /** USD per input image. */\n perImage: number;\n /** True when prompt, completion and per-request prices are all zero. */\n isFree: boolean;\n}\n\nexport interface OpenRouterModel {\n /** OpenRouter slug, e.g. \"openai/gpt-5\". */\n id: string;\n /** Display name, e.g. \"OpenAI: GPT-5\". */\n name: string;\n description: string;\n /** Vendor taken from the id prefix (\"openai/gpt-5\" → \"openai\"). */\n vendor: string;\n /** Unix timestamp (seconds) when the model was added. */\n created: number;\n contextLength: number | null;\n maxCompletionTokens: number | null;\n inputModalities: string[];\n outputModalities: string[];\n /** OpenAI-compatible parameters the model accepts (union over providers). */\n supportedParameters: string[];\n /** The model searches the web on its own (Perplexity) — no plugin needed. */\n builtInWebSearch: boolean;\n /** Accepts `response_format` / `structured_outputs`. */\n supportsJsonMode: boolean;\n supportsReasoning: boolean;\n pricing: ModelPricing;\n}\n\nconst PER_M = 1_000_000;\n\nfunction perToken(value: unknown): number | null {\n if (value == null || value === '') return null;\n const n = typeof value === 'number' ? value : Number(String(value));\n return Number.isFinite(n) ? n : null;\n}\n\nfunction perMillion(value: unknown): number | null {\n const n = perToken(value);\n return n == null ? null : n * PER_M;\n}\n\n/** Raw `pricing` object of one model → per-1M prices. */\nexport function normalizePricing(raw: unknown): ModelPricing {\n const r = (raw ?? {}) as Record<string, unknown>;\n const promptPerM = perMillion(r.prompt) ?? 0;\n const completionPerM = perMillion(r.completion) ?? 0;\n const perRequest = perToken(r.request) ?? 0;\n return {\n promptPerM,\n completionPerM,\n reasoningPerM: perMillion(r.internal_reasoning),\n perRequest,\n perWebSearch: perToken(r.web_search) ?? 0,\n cacheReadPerM: perMillion(r.input_cache_read),\n perImage: perToken(r.image) ?? 0,\n isFree: promptPerM === 0 && completionPerM === 0 && perRequest === 0,\n };\n}\n\n/**\n * One raw entry from `data[]` → `OpenRouterModel`.\n * Returns null for entries that cannot produce text (image-only models).\n */\nexport function normalizeModel(raw: unknown): OpenRouterModel | null {\n const r = raw as Record<string, any> | null;\n if (!r || typeof r.id !== 'string') return null;\n\n const outputModalities: string[] = Array.isArray(r.architecture?.output_modalities)\n ? r.architecture.output_modalities\n : ['text'];\n if (!outputModalities.includes('text')) return null;\n\n const supportedParameters: string[] = Array.isArray(r.supported_parameters) ? r.supported_parameters : [];\n const slash = r.id.indexOf('/');\n const vendor = slash > 0 ? r.id.slice(0, slash) : 'other';\n\n return {\n id: r.id,\n name: typeof r.name === 'string' && r.name ? r.name : r.id,\n description: typeof r.description === 'string' ? r.description : '',\n vendor,\n created: typeof r.created === 'number' ? r.created : 0,\n contextLength: typeof r.context_length === 'number' ? r.context_length : (r.top_provider?.context_length ?? null),\n maxCompletionTokens: typeof r.top_provider?.max_completion_tokens === 'number' ? r.top_provider.max_completion_tokens : null,\n inputModalities: Array.isArray(r.architecture?.input_modalities) ? r.architecture.input_modalities : ['text'],\n outputModalities,\n supportedParameters,\n // Only Perplexity always searches without extra parameters; everything else needs the OpenRouter web plugin.\n builtInWebSearch: vendor === 'perplexity',\n supportsJsonMode: supportedParameters.includes('response_format') || supportedParameters.includes('structured_outputs'),\n supportsReasoning: supportedParameters.includes('reasoning') || supportedParameters.includes('include_reasoning'),\n pricing: normalizePricing(r.pricing),\n };\n}\n\n/** Whole `/models` response body → normalized list (text-capable models only). */\nexport function normalizeModelsResponse(body: unknown): OpenRouterModel[] {\n const data = (body as { data?: unknown[] } | null)?.data;\n if (!Array.isArray(data)) return [];\n return data.map(normalizeModel).filter((m): m is OpenRouterModel => !!m);\n}\n\nexport interface FetchModelsOptions {\n /** Optional. Without a key the public list is returned; with one, only models the key can call. */\n apiKey?: string;\n url?: string;\n signal?: AbortSignal;\n /** Extra headers, e.g. `HTTP-Referer` / `X-Title` for OpenRouter app attribution. */\n headers?: Record<string, string>;\n}\n\n/** Fetch and normalize the OpenRouter model list. Works in browsers and Node 18+. */\nexport async function fetchOpenRouterModels(options: FetchModelsOptions = {}): Promise<OpenRouterModel[]> {\n const headers: Record<string, string> = { Accept: 'application/json', ...(options.headers ?? {}) };\n if (options.apiKey) headers.Authorization = `Bearer ${options.apiKey}`;\n const res = await fetch(options.url ?? OPENROUTER_MODELS_URL, { headers, signal: options.signal });\n if (!res.ok) throw new Error(`OpenRouter /models returned ${res.status}`);\n return normalizeModelsResponse(await res.json());\n}\n","import { useCallback, useEffect, useRef, useState } from 'react';\nimport { fetchOpenRouterModels, OPENROUTER_MODELS_URL, type FetchModelsOptions, type OpenRouterModel } from './models';\n\nexport interface UseOpenRouterModelsOptions extends Omit<FetchModelsOptions, 'signal'> {\n /**\n * Replace the default fetch entirely — e.g. call your own backend that proxies\n * OpenRouter and adds auth. Must resolve to already-normalized models\n * (use `normalizeModelsResponse` if your backend forwards the raw body).\n */\n fetcher?: () => Promise<OpenRouterModel[]>;\n /** How long a fetched list stays fresh across all hook instances. Default 10 minutes. */\n ttlMs?: number;\n /** Set to false to skip fetching (e.g. until the user opens the picker). */\n enabled?: boolean;\n}\n\nexport interface UseOpenRouterModelsResult {\n models: OpenRouterModel[];\n loading: boolean;\n refreshing: boolean;\n error: Error | null;\n /** ISO time of the list currently held (null before the first successful fetch). */\n fetchedAt: string | null;\n /** Bypass the cache and fetch again. */\n refresh: () => Promise<void>;\n}\n\ninterface CacheEntry {\n models: OpenRouterModel[];\n fetchedAt: number;\n}\n\nconst DEFAULT_TTL_MS = 10 * 60 * 1000;\nconst cache = new Map<string, CacheEntry>();\nconst inflight = new Map<string, Promise<OpenRouterModel[]>>();\n\n/** Clear the module-level cache (tests, or after changing the API key). */\nexport function clearOpenRouterModelsCache(): void {\n cache.clear();\n}\n\n/**\n * Loads the OpenRouter model list with a small module-level cache, so several\n * pickers on one page share a single request. No external state library needed.\n * If you already use TanStack Query or SWR, skip this hook and pass `models`\n * from your own query into the picker.\n */\nexport function useOpenRouterModels(options: UseOpenRouterModelsOptions = {}): UseOpenRouterModelsResult {\n const { fetcher, ttlMs = DEFAULT_TTL_MS, enabled = true, apiKey, url = OPENROUTER_MODELS_URL, headers } = options;\n const cacheKey = fetcher ? `fetcher:${url}` : `${url}|${apiKey ?? ''}`;\n\n const [entry, setEntry] = useState<CacheEntry | null>(() => cache.get(cacheKey) ?? null);\n const [loading, setLoading] = useState(false);\n const [refreshing, setRefreshing] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n const mounted = useRef(true);\n const fetcherRef = useRef(fetcher);\n fetcherRef.current = fetcher;\n\n useEffect(() => {\n mounted.current = true;\n return () => { mounted.current = false; };\n }, []);\n\n const load = useCallback(async (force: boolean) => {\n const cached = cache.get(cacheKey);\n if (!force && cached && Date.now() - cached.fetchedAt < ttlMs) {\n setEntry(cached);\n return;\n }\n let promise = inflight.get(cacheKey);\n if (!promise) {\n promise = (fetcherRef.current ? fetcherRef.current() : fetchOpenRouterModels({ apiKey, url, headers }))\n .then((models) => {\n cache.set(cacheKey, { models, fetchedAt: Date.now() });\n return models;\n })\n .finally(() => inflight.delete(cacheKey));\n inflight.set(cacheKey, promise);\n }\n cached ? setRefreshing(true) : setLoading(true);\n setError(null);\n try {\n await promise;\n if (mounted.current) setEntry(cache.get(cacheKey) ?? null);\n } catch (e) {\n if (mounted.current) setError(e instanceof Error ? e : new Error(String(e)));\n } finally {\n if (mounted.current) { setLoading(false); setRefreshing(false); }\n }\n }, [cacheKey, ttlMs, apiKey, url, headers]);\n\n useEffect(() => {\n if (enabled) void load(false);\n }, [enabled, load]);\n\n return {\n models: entry?.models ?? [],\n loading,\n refreshing,\n error,\n fetchedAt: entry ? new Date(entry.fetchedAt).toISOString() : null,\n refresh: () => load(true),\n };\n}\n"],"names":["formatPerM","value","formatContext","tokens","m","DEFAULT_VENDOR_LABELS","vendorLabel","vendor","overrides","c","blendedPricePerM","enLabels","q","shown","total","n","t","plLabels","matches","vendors","hay","term","timeLabel","d","OpenRouterModelPicker","props","models","onChange","loading","refreshing","error","onRefresh","fetchedAt","defaultModelId","allowAuto","label","compact","disabled","vendorLabels","filter","initialSort","theme","className","id","useMemo","open","setOpen","useState","query","setQuery","setVendor","sort","setSort","cursor","setCursor","rootRef","useRef","listRef","inputRef","visible","selected","counts","b","filtered","list","sorters","a","useEffect","onDown","e","idx","pick","modelId","onKeyDown","isAuto","summaryTitle","priceTime","errorMessage","rootClass","jsxs","jsx","o","PriceTag","v","key","text","i","ModelRow","model","index","active","isDefault","onHover","onPick","OPENROUTER_MODELS_URL","PER_M","perToken","perMillion","normalizePricing","raw","promptPerM","completionPerM","perRequest","normalizeModel","outputModalities","supportedParameters","slash","normalizeModelsResponse","body","data","fetchOpenRouterModels","options","headers","res","DEFAULT_TTL_MS","cache","inflight","clearOpenRouterModelsCache","useOpenRouterModels","fetcher","ttlMs","enabled","apiKey","url","cacheKey","entry","setEntry","setLoading","setRefreshing","setError","mounted","fetcherRef","load","useCallback","force","cached","promise"],"mappings":"wIAGO,SAASA,EAAWC,EAA0C,CACnE,OAAIA,GAAS,KAAa,IACtBA,IAAU,EAAU,KACpBA,EAAQ,IAAa,IAAIA,EAAM,QAAQ,CAAC,CAAC,GACtC,IAAIA,EAAM,QAAQ,CAAC,CAAC,EAC7B,CAGO,SAASC,EAAcC,EAA2C,CACvE,GAAI,CAACA,EAAQ,MAAO,IACpB,GAAIA,GAAU,IAAW,CACvB,MAAMC,GAAKD,EAAS,KAAW,QAAQ,CAAC,EACxC,MAAO,GAAGC,EAAE,SAAS,IAAI,EAAIA,EAAE,MAAM,EAAG,EAAE,EAAIA,CAAC,GACjD,CACA,OAAID,GAAU,IAAa,GAAG,KAAK,MAAMA,EAAS,GAAI,CAAC,IAChD,OAAOA,CAAM,CACtB,CAEO,MAAME,GAAgD,CAC3D,OAAQ,SAAU,UAAW,YAAa,OAAQ,SAAU,aAAc,OAAQ,UAAW,UAC7F,WAAY,aAAc,OAAQ,MAAO,SAAU,WAAY,KAAM,OAAQ,OAAQ,SACrF,OAAQ,SAAU,UAAW,YAAa,OAAQ,SAAU,WAAY,WAAY,OAAQ,OAC5F,WAAY,YACd,EAEO,SAASC,EAAYC,EAAgBC,EAA4C,CACtF,OAAOA,IAAYD,CAAM,GAAKF,GAAsBE,CAAM,GACrDA,EAAO,QAAQ,KAAM,GAAG,EAAE,QAAQ,QAAUE,GAAMA,EAAE,aAAa,CACxE,CAMO,SAASC,EAAiBN,EAA4B,CAC3D,OAAOA,EAAE,QAAQ,WAAa,IAAOA,EAAE,QAAQ,eAAiB,IAAOA,EAAE,QAAQ,WAAa,GAChG,CCRO,MAAMO,GAAyB,CACpC,YAAa,kBACb,kBAAmB,+CACnB,WAAY,cACZ,SAAU,OACV,aAAc,WACd,WAAY,SACZ,YAAa,UACb,QAAS,iBACT,WAAY,cACZ,QAAS,kCACT,UAAW,gCACX,UAAYC,GAAM,oBAAoBA,CAAC,KACvC,QAAS,CAACC,EAAOC,IAAU,GAAGD,CAAK,OAAOC,CAAK,GAC/C,YAAcC,GAAM,GAAGA,CAAC,qBACxB,WAAaC,GAAM,gBAAgBA,CAAC,GACpC,KAAM,6BACN,IAAK,OACL,UAAW,UACX,KAAM,OACN,IAAK,MACL,KAAM,OACN,UAAW,YACX,QAAS,UACT,UAAW,aACX,WAAY,cACZ,WAAY,oCACZ,SAAU,sBACV,UAAW,+BACb,EAEaC,GAAyB,CACpC,YAAa,iBACb,kBAAmB,+CACnB,WAAY,qBACZ,SAAU,QACV,aAAc,YACd,WAAY,YACZ,YAAa,WACb,QAAS,eACT,WAAY,aACZ,QAAS,uCACT,UAAW,oCACX,UAAYL,GAAM,8BAA8BA,CAAC,KACjD,QAAS,CAACC,EAAOC,IAAU,GAAGD,CAAK,MAAMC,CAAK,GAC9C,YAAcC,GAAM,GAAGA,CAAC,qBACxB,WAAaC,GAAM,UAAUA,CAAC,GAC9B,KAAM,qCACN,IAAK,SACL,UAAW,WACX,KAAM,UACN,IAAK,MACL,KAAM,OACN,UAAW,YACX,QAAS,WACT,UAAW,cACX,WAAY,aACZ,WAAY,uCACZ,SAAU,iCACV,UAAW,gCACb,EChDA,SAASE,GAAQd,EAAoBQ,EAAWO,EAA2C,CACzF,GAAI,CAACP,EAAG,MAAO,GACf,MAAMQ,EAAM,GAAGhB,EAAE,EAAE,IAAIA,EAAE,IAAI,IAAIE,EAAYF,EAAE,OAAQe,CAAO,CAAC,GAAG,YAAA,EAClE,OAAOP,EAAE,MAAM,KAAK,EAAE,MAAOS,GAASD,EAAI,SAASC,CAAI,CAAC,CAC1D,CAEA,SAASC,GAAUrB,EAAwD,CACzE,GAAI,CAACA,EAAO,OAAO,KACnB,MAAMsB,EAAI,OAAOtB,GAAU,SAAW,IAAI,KAAKA,CAAK,EAAIA,EACxD,OAAO,OAAO,MAAMsB,EAAE,QAAA,CAAS,EAAI,KAAOA,EAAE,mBAAmB,OAAW,CAAE,KAAM,UAAW,OAAQ,UAAW,CAClH,CAOO,SAASC,GAAsBC,EAAmC,CACvE,KAAM,CACJ,OAAAC,EAAQ,MAAAzB,EAAO,SAAA0B,EAAU,QAAAC,EAAS,WAAAC,EAAY,MAAAC,EAAO,UAAAC,EAAW,UAAAC,EAAW,eAAAC,EAC3E,UAAAC,EAAW,MAAAC,EAAO,QAAAC,EAAS,SAAAC,EAAU,aAAAC,EAAc,OAAAC,EAAQ,YAAAC,EAAc,OAAQ,MAAAC,EAAO,UAAAC,EAAW,GAAAC,CAAA,EACjGlB,EACET,EAAkB4B,EAAAA,QAAQ,KAAO,CAAE,GAAGjC,GAAU,GAAIc,EAAM,QAAU,CAAA,CAAC,GAAO,CAACA,EAAM,MAAM,CAAC,EAE1F,CAACoB,EAAMC,CAAO,EAAIC,EAAAA,SAAS,EAAK,EAChC,CAACC,EAAOC,CAAQ,EAAIF,EAAAA,SAAS,EAAE,EAC/B,CAACxC,EAAQ2C,EAAS,EAAIH,EAAAA,SAAS,KAAK,EACpC,CAACI,EAAMC,EAAO,EAAIL,EAAAA,SAAwBP,CAAW,EACrD,CAACa,EAAQC,CAAS,EAAIP,EAAAA,SAAS,CAAC,EAChCQ,EAAUC,EAAAA,OAAuB,IAAI,EACrCC,EAAUD,EAAAA,OAAuB,IAAI,EACrCE,EAAWF,EAAAA,OAAyB,IAAI,EAExCG,EAAUf,EAAAA,QAAQ,IAAOL,EAASb,EAAO,OAAOa,CAAM,EAAIb,EAAS,CAACA,EAAQa,CAAM,CAAC,EACnFqB,EAAW3D,EAAQ0D,EAAQ,KAAMvD,GAAMA,EAAE,KAAOH,CAAK,GAAKyB,EAAO,KAAMtB,GAAMA,EAAE,KAAOH,CAAK,EAAI,OAE/FkB,GAAUyB,EAAAA,QAAQ,IAAM,CAC5B,MAAMiB,MAAa,IACnB,UAAWzD,KAAKuD,EAASE,EAAO,IAAIzD,EAAE,QAASyD,EAAO,IAAIzD,EAAE,MAAM,GAAK,GAAK,CAAC,EAC7E,MAAO,CAAC,GAAGyD,EAAO,QAAA,CAAS,EAAE,KAAK,CAAC,EAAGC,IAAMA,EAAE,CAAC,EAAI,EAAE,CAAC,GAAK,EAAE,CAAC,EAAE,cAAcA,EAAE,CAAC,CAAC,CAAC,CACrF,EAAG,CAACH,CAAO,CAAC,EAENI,EAAWnB,EAAAA,QAAQ,IAAM,CAC7B,MAAMhC,EAAIoC,EAAM,KAAA,EAAO,YAAA,EACjBgB,EAAOL,EAAQ,OAAQvD,IAAOG,IAAW,OAASH,EAAE,SAAWG,IAAWW,GAAQd,EAAGQ,EAAG0B,CAAY,CAAC,EACrG2B,EAAqF,CACzF,KAAM,CAACC,EAAGJ,IAAMI,EAAE,KAAK,cAAcJ,EAAE,IAAI,EAC3C,SAAU,CAACI,EAAGJ,IAAMpD,EAAiBwD,CAAC,EAAIxD,EAAiBoD,CAAC,GAAKI,EAAE,KAAK,cAAcJ,EAAE,IAAI,EAC5F,OAAQ,CAACI,EAAGJ,IAAMA,EAAE,QAAUI,EAAE,SAAWA,EAAE,KAAK,cAAcJ,EAAE,IAAI,EACtE,QAAS,CAACI,EAAGJ,KAAOA,EAAE,eAAiB,IAAMI,EAAE,eAAiB,IAAMA,EAAE,KAAK,cAAcJ,EAAE,IAAI,CAAA,EAEnG,MAAO,CAAC,GAAGE,CAAI,EAAE,KAAKC,EAAQd,CAAI,CAAC,CACrC,EAAG,CAACQ,EAASX,EAAOzC,EAAQ4C,EAAMb,CAAY,CAAC,EAG/C6B,EAAAA,UAAU,IAAM,CACd,GAAI,CAACtB,EAAM,OACX,MAAMuB,EAAUC,GAAkB,CAC5Bd,EAAQ,SAAW,CAACA,EAAQ,QAAQ,SAASc,EAAE,MAAc,GAAGvB,EAAQ,EAAK,CACnF,EACA,gBAAS,iBAAiB,YAAasB,CAAM,EACtC,IAAM,SAAS,oBAAoB,YAAaA,CAAM,CAC/D,EAAG,CAACvB,CAAI,CAAC,EAETsB,EAAAA,UAAU,IAAM,CAAEb,EAAU,CAAC,CAAG,EAAG,CAACN,EAAOzC,EAAQ4C,CAAI,CAAC,EAExDgB,EAAAA,UAAU,IAAM,CACd,GAAI,CAACtB,EAAM,OACXa,EAAS,SAAS,MAAA,EAClB,MAAMY,EAAMP,EAAS,UAAW3D,GAAMA,EAAE,KAAOH,CAAK,EAChDqE,GAAO,GAAGhB,EAAUgB,CAAG,CAG7B,EAAG,CAACzB,CAAI,CAAC,EAETsB,EAAAA,UAAU,IAAM,CACHV,EAAQ,SAAS,cAA2B,gBAAgBJ,CAAM,IAAI,GAC7E,iBAAiB,CAAE,MAAO,SAAA,CAAW,CAC3C,EAAG,CAACA,EAAQR,CAAI,CAAC,EAEjB,MAAM0B,EAAQC,GAAoB,CAAE7C,EAAS6C,CAAO,EAAG1B,EAAQ,EAAK,EAAGG,EAAS,EAAE,CAAG,EAE/EwB,GAAaJ,GAAqB,CACtC,GAAIA,EAAE,MAAQ,YAAeA,EAAE,eAAA,EAAkBf,EAAW7C,GAAM,KAAK,IAAIsD,EAAS,OAAS,EAAGtD,EAAI,CAAC,CAAC,UAC7F4D,EAAE,MAAQ,UAAaA,EAAE,eAAA,EAAkBf,EAAW7C,GAAM,KAAK,IAAI,EAAGA,EAAI,CAAC,CAAC,UAC9E4D,EAAE,MAAQ,QAAS,CAAEA,EAAE,eAAA,EAAkB,MAAMjE,EAAI2D,EAASV,CAAM,EAAOjD,GAAGmE,EAAKnE,EAAE,EAAE,CAAG,MACxFiE,EAAE,MAAQ,UAAYvB,EAAQ,EAAK,CAC9C,EAEM4B,EAAS,CAAC,CAACxC,GAAa,CAACjC,EACzB0E,GAAef,EAAWA,EAAS,KAAO3D,IAAgByE,EAAS1D,EAAE,KAAOA,EAAE,aAC9E4D,GAAYtD,GAAUU,CAAS,EAC/B6C,EAAe/C,EAASA,aAAiB,MAAQA,EAAM,QAAU,OAAOA,CAAK,EAAK,KAElFgD,GAAY,CAAC,MAAOjC,GAAQ,YAAaT,GAAW,eAAgBD,GAAS,iBAAkBO,CAAS,EAC3G,OAAO,OAAO,EAAE,KAAK,GAAG,EAE3B,OACEqC,OAAC,OAAI,IAAKxB,EAAS,UAAWuB,GAAW,aAAYrC,EAAO,GAAAE,EACzD,SAAA,CAAAR,GACC4C,EAAAA,KAAC,MAAA,CAAI,UAAU,aACb,SAAA,CAAAC,EAAAA,IAAC,QAAM,SAAA7C,CAAA,CAAM,EACb4C,EAAAA,KAAC,OAAA,CAAK,UAAU,kBACb,SAAA,CAAArD,EAAO,OAASV,EAAE,YAAYU,EAAO,MAAM,EAAI,GAC/CkD,IAAa,MAAM5D,EAAE,WAAW4D,EAAS,CAAC,EAAA,CAAA,CAC7C,CAAA,EACF,EAGFG,EAAAA,KAAC,MAAA,CAAI,UAAU,cACf,SAAA,CAAAC,EAAAA,IAAC,SAAA,CACC,KAAK,SACL,UAAU,eACV,SAAA3C,EACA,gBAAc,UACd,gBAAeQ,EACf,QAAS,IAAMC,EAASmC,GAAM,CAACA,CAAC,EAEhC,SAAAF,EAAAA,KAAC,MAAA,CAAI,UAAU,qBACb,SAAA,CAAAA,EAAAA,KAAC,MAAA,CAAI,UAAU,oBACb,SAAA,CAAAC,EAAAA,IAAC,IAAA,CAAE,UAAU,qBAAsB,SAAAL,GAAa,EAC/Cf,EACCoB,EAAAA,IAAC,IAAA,CAAE,UAAU,6BAA8B,SAAApB,EAAS,EAAA,CAAG,EACrDc,GAAUzC,EACZ8C,EAAAA,KAAC,IAAA,CAAE,UAAU,mBAAoB,SAAA,CAAA/D,EAAE,IAAI,IAACgE,EAAAA,IAAC,OAAA,CAAK,UAAU,YAAa,SAAA/C,CAAA,CAAe,CAAA,EAAO,EACzFL,EACFoD,EAAAA,IAAC,IAAA,CAAE,UAAU,mBAAoB,SAAAhE,EAAE,OAAA,CAAQ,EACzC6D,QACD,IAAA,CAAE,UAAU,2CAA4C,SAAA7D,EAAE,UAAU,EACnE,IAAA,EACN,EACA+D,EAAAA,KAAC,MAAA,CAAI,UAAU,qBACZ,SAAA,CAAAnB,GAAYoB,EAAAA,IAACE,GAAA,CAAS,MAAOtB,EAAU,EAAA5C,EAAM,EAC9CgE,EAAAA,IAAC,MAAA,CAAI,UAAU,eAAe,MAAM,KAAK,OAAO,KAAK,QAAQ,YAAY,KAAK,OAAO,OAAO,eAAe,YAAY,IAAI,cAAc,QAAQ,eAAe,QAAQ,cAAY,OAClL,SAAAA,EAAAA,IAAC,WAAA,CAAS,OAAO,gBAAA,CAAiB,CAAA,CACpC,CAAA,CAAA,CACF,CAAA,CAAA,CACF,CAAA,CAAA,EAGDnC,GACCkC,EAAAA,KAAC,MAAA,CAAI,UAAU,eACb,SAAA,CAAAA,EAAAA,KAAC,MAAA,CAAI,UAAU,eACb,SAAA,CAAAA,EAAAA,KAAC,MAAA,CAAI,UAAU,cACb,SAAA,CAAAA,EAAAA,KAAC,MAAA,CAAI,UAAU,mBAAmB,MAAM,KAAK,OAAO,KAAK,QAAQ,YAAY,KAAK,OAAO,OAAO,eAAe,YAAY,IAAI,cAAc,QAAQ,eAAe,QAAQ,cAAY,OACtL,SAAA,CAAAC,MAAC,UAAO,GAAG,KAAK,GAAG,KAAK,EAAE,IAAI,EAAEA,EAAAA,IAAC,QAAK,GAAG,KAAK,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAA,CAAQ,CAAA,EAC9E,EACAA,EAAAA,IAAC,QAAA,CACC,IAAKtB,EACL,UAAU,aACV,MAAOV,EACP,SAAWqB,GAAMpB,EAASoB,EAAE,OAAO,KAAK,EACxC,UAAAI,GACA,YAAazD,EAAE,kBACf,aAAYA,EAAE,kBACd,KAAK,WACL,gBAAc,OACd,gBAAe,GAAG2B,GAAM,KAAK,UAAA,CAAA,CAC/B,EACF,EACAoC,EAAAA,KAAC,MAAA,CAAI,UAAU,eACb,SAAA,CAAAA,EAAAA,KAAC,SAAA,CAAO,UAAU,cAAc,MAAOxE,EAAQ,SAAW8D,GAAMnB,GAAUmB,EAAE,OAAO,KAAK,EAAG,aAAYrD,EAAE,WACvG,SAAA,CAAA+D,EAAAA,KAAC,SAAA,CAAO,MAAM,MAAO,SAAA,CAAA/D,EAAE,WAAW,KAAG2C,EAAQ,OAAO,GAAA,EAAC,EACpDxC,GAAQ,IAAI,CAAC,CAACgE,EAAGpE,CAAC,IAAMgE,EAAAA,KAAC,SAAA,CAAe,MAAOI,EAAI,SAAA,CAAA7E,EAAY6E,EAAG7C,CAAY,EAAE,KAAGvB,EAAE,GAAA,CAAA,EAAhDoE,CAAiD,CAAS,CAAA,EAClG,EACAH,EAAAA,IAAC,MAAA,CAAI,UAAU,aAAa,KAAK,QAC7B,SAAA,CAAC,CAAC,OAAQhE,EAAE,QAAQ,EAAG,CAAC,WAAYA,EAAE,YAAY,EAAG,CAAC,SAAUA,EAAE,UAAU,EAAG,CAAC,UAAWA,EAAE,WAAW,CAAC,EAAY,IAAI,CAAC,CAACoE,EAAKC,CAAI,IACpIL,EAAAA,IAAC,SAAA,CAAiB,KAAK,SAAS,QAAS,IAAM5B,GAAQgC,CAAG,EACxD,UAAW,YAAYjC,IAASiC,EAAM,qBAAuB,EAAE,GAAI,eAAcjC,IAASiC,EACzF,SAAAC,CAAA,EAFUD,CAAA,CAId,EACH,EACAJ,EAAAA,IAAC,OAAA,CAAK,UAAU,aAAc,SAAAhE,EAAE,QAAQ+C,EAAS,OAAQJ,EAAQ,MAAM,CAAA,CAAE,EACxE5B,SACE,SAAA,CAAO,KAAK,SAAS,UAAU,eAAe,QAASA,EAAW,SAAU,CAAC,CAACF,EAAY,MAAOb,EAAE,QACjG,WAAaA,EAAE,WAAaA,EAAE,OAAA,CACjC,CAAA,CAAA,CAEJ,CAAA,EACF,EAEA+D,EAAAA,KAAC,MAAA,CAAI,IAAKtB,EAAS,UAAU,YAAY,KAAK,UAAU,GAAI,GAAGd,GAAM,KAAK,WACvE,SAAA,CAAAT,GACC6C,EAAAA,KAAC,SAAA,CAAO,KAAK,SAAS,KAAK,SAAS,gBAAe,CAAC9E,EAClD,UAAW,YAAaA,EAAiC,GAAzB,sBAA2B,GAC3D,QAAS,IAAM,CAAE0B,EAAS,EAAE,EAAGmB,EAAQ,EAAK,CAAG,EAC9C,SAAA,CAAA9B,EAAE,KACFiB,GAAkB+C,EAAAA,IAAC,OAAA,CAAK,UAAU,yBAA0B,SAAA/C,CAAA,CAAe,CAAA,CAAA,CAAA,EAG/EL,GAAW,CAACF,EAAO,cAAW,IAAA,CAAE,UAAU,eAAgB,SAAAV,EAAE,OAAA,CAAQ,EACpE6D,GAAgB,CAACnD,EAAO,QAAUqD,EAAAA,KAAC,IAAA,CAAE,UAAU,mCAAoC,SAAA,CAAA/D,EAAE,UAAU,KAAG6D,CAAA,EAAa,EAC/G,CAACjD,GAAW,CAACiD,GAAgB,CAACd,EAAS,QAAUiB,EAAAA,IAAC,IAAA,CAAE,UAAU,eAAgB,SAAAhE,EAAE,UAAUgC,CAAK,EAAE,EACjGe,EAAS,IAAI,CAAC3D,EAAGkF,IAChBN,EAAAA,IAACO,GAAA,CAAoB,MAAOnF,EAAG,MAAOkF,EAAG,OAAQA,IAAMjC,EAAQ,SAAUjD,EAAE,KAAOH,EAChF,UAAWG,EAAE,KAAO6B,EAAgB,EAAAjB,EAAM,QAAS,IAAMsC,EAAUgC,CAAC,EAAG,OAAQ,IAAMf,EAAKnE,EAAE,EAAE,CAAA,EADjFA,EAAE,EAAA,CAElB,CAAA,CAAA,CACH,CAAA,CAAA,CACF,CAAA,EAEF,EAECwD,GAAY,CAACxB,GACZ2C,EAAAA,KAAC,MAAA,CAAI,UAAU,eACb,SAAA,CAAAC,EAAAA,IAAC,QAAK,UAAU,YAAa,WAAYpB,EAAS,OAAQtB,CAAY,EAAE,EACxEyC,EAAAA,KAAC,OAAA,CAAK,UAAU,YAAa,SAAA,CAAA/D,EAAE,QAAQ,IAAEd,EAAc0D,EAAS,aAAa,CAAA,EAAE,EAC9EA,EAAS,qBAAuBmB,OAAC,OAAA,CAAK,UAAU,YAAa,SAAA,CAAA/D,EAAE,UAAU,IAAEd,EAAc0D,EAAS,mBAAmB,CAAA,EAAE,EACvHA,EAAS,kBAAoBoB,EAAAA,IAAC,QAAK,UAAU,4BAA6B,WAAE,SAAS,EACrFpB,EAAS,kBAAoBoB,EAAAA,IAAC,QAAK,UAAU,0BAA2B,WAAE,KAAK,EAC/EpB,EAAS,mBAAqBoB,EAAAA,IAAC,QAAK,UAAU,8BAA+B,WAAE,UAAU,EACzFpB,EAAS,QAAQ,WAAa,GAAKmB,EAAAA,KAAC,OAAA,CAAK,UAAU,4BAA4B,SAAA,CAAA,IAAE/E,EAAW4D,EAAS,QAAQ,UAAU,EAAE,IAAE5C,EAAE,UAAA,EAAW,EACxI4C,EAAS,QAAQ,QAAUoB,EAAAA,IAAC,QAAK,UAAU,0BAA2B,WAAE,IAAA,CAAK,CAAA,CAAA,CAChF,CAAA,EAEJ,CAEJ,CAEA,SAASO,GAAS,CAAE,MAAAC,EAAO,MAAAC,EAAO,OAAAC,EAAQ,SAAA9B,EAAU,UAAA+B,EAAW,EAAA3E,EAAG,QAAA4E,EAAS,OAAAC,GAGxE,CACD,OACEd,EAAAA,KAAC,SAAA,CACC,KAAK,SACL,KAAK,SACL,gBAAenB,EACf,aAAY6B,EACZ,gBAAeD,EAAM,GACrB,aAAcI,EACd,QAASC,EACT,UAAW,WAAWH,EAAS,oBAAsB,EAAE,GAAG9B,EAAW,sBAAwB,EAAE,GAE/F,SAAA,CAAAmB,EAAAA,KAAC,MAAA,CAAI,UAAU,gBACb,SAAA,CAAAA,EAAAA,KAAC,IAAA,CAAE,UAAU,gBACV,SAAA,CAAAS,EAAM,KACNG,GAAaX,EAAAA,IAAC,OAAA,CAAK,UAAU,mBAAoB,WAAE,SAAA,CAAU,CAAA,EAChE,EACAA,EAAAA,IAAC,IAAA,CAAE,UAAU,cAAe,WAAM,EAAA,CAAG,CAAA,EACvC,EACAD,EAAAA,KAAC,MAAA,CAAI,UAAU,gBACZ,SAAA,CAAAS,EAAM,wBAAqB,OAAA,CAAK,UAAU,8BAA8B,MAAOxE,EAAE,SAAW,SAAAA,EAAE,GAAA,CAAI,EAClGwE,EAAM,kBAAoBR,EAAAA,IAAC,OAAA,CAAK,UAAU,4BAA4B,MAAOhE,EAAE,UAAY,SAAAA,EAAE,IAAA,CAAK,QAClG,OAAA,CAAK,UAAU,WAAY,SAAAd,EAAcsF,EAAM,aAAa,EAAE,EAC/DR,EAAAA,IAACE,GAAA,CAAS,MAAAM,EAAc,EAAAxE,CAAA,CAAM,CAAA,CAAA,CAChC,CAAA,CAAA,CAAA,CAGN,CAGA,SAASkE,GAAS,CAAE,MAAAM,EAAO,EAAAxE,GAAkD,CAC3E,OAAIwE,EAAM,QAAQ,aAAgB,OAAA,CAAK,UAAU,4BAA6B,SAAAxE,EAAE,IAAA,CAAK,SAElF,OAAA,CAAK,UAAU,aAAa,MAAOA,EAAE,WACnC,SAAA,CAAAhB,EAAWwF,EAAM,QAAQ,UAAU,EAAE,IAACR,EAAAA,IAAC,OAAA,CAAK,UAAU,iBAAiB,SAAA,IAAC,EAAO,IAAEhF,EAAWwF,EAAM,QAAQ,cAAc,CAAA,EAC3H,CAEJ,CCtSO,MAAMM,EAAwB,sCA6C/BC,GAAQ,IAEd,SAASC,EAAS/F,EAA+B,CAC/C,GAAIA,GAAS,MAAQA,IAAU,GAAI,OAAO,KAC1C,MAAMc,EAAI,OAAOd,GAAU,SAAWA,EAAQ,OAAO,OAAOA,CAAK,CAAC,EAClE,OAAO,OAAO,SAASc,CAAC,EAAIA,EAAI,IAClC,CAEA,SAASkF,EAAWhG,EAA+B,CACjD,MAAMc,EAAIiF,EAAS/F,CAAK,EACxB,OAAOc,GAAK,KAAO,KAAOA,EAAIgF,EAChC,CAGO,SAASG,GAAiBC,EAA4B,CAC3D,MAAM,EAAKA,GAAO,CAAA,EACZC,EAAaH,EAAW,EAAE,MAAM,GAAK,EACrCI,EAAiBJ,EAAW,EAAE,UAAU,GAAK,EAC7CK,EAAaN,EAAS,EAAE,OAAO,GAAK,EAC1C,MAAO,CACL,WAAAI,EACA,eAAAC,EACA,cAAeJ,EAAW,EAAE,kBAAkB,EAC9C,WAAAK,EACA,aAAcN,EAAS,EAAE,UAAU,GAAK,EACxC,cAAeC,EAAW,EAAE,gBAAgB,EAC5C,SAAUD,EAAS,EAAE,KAAK,GAAK,EAC/B,OAAQI,IAAe,GAAKC,IAAmB,GAAKC,IAAe,CAAA,CAEvE,CAMO,SAASC,GAAeJ,EAAsC,CACnE,MAAM,EAAIA,EACV,GAAI,CAAC,GAAK,OAAO,EAAE,IAAO,SAAU,OAAO,KAE3C,MAAMK,EAA6B,MAAM,QAAQ,EAAE,cAAc,iBAAiB,EAC9E,EAAE,aAAa,kBACf,CAAC,MAAM,EACX,GAAI,CAACA,EAAiB,SAAS,MAAM,EAAG,OAAO,KAE/C,MAAMC,EAAgC,MAAM,QAAQ,EAAE,oBAAoB,EAAI,EAAE,qBAAuB,CAAA,EACjGC,EAAQ,EAAE,GAAG,QAAQ,GAAG,EACxBnG,EAASmG,EAAQ,EAAI,EAAE,GAAG,MAAM,EAAGA,CAAK,EAAI,QAElD,MAAO,CACL,GAAI,EAAE,GACN,KAAM,OAAO,EAAE,MAAS,UAAY,EAAE,KAAO,EAAE,KAAO,EAAE,GACxD,YAAa,OAAO,EAAE,aAAgB,SAAW,EAAE,YAAc,GACjE,OAAAnG,EACA,QAAS,OAAO,EAAE,SAAY,SAAW,EAAE,QAAU,EACrD,cAAe,OAAO,EAAE,gBAAmB,SAAW,EAAE,eAAkB,EAAE,cAAc,gBAAkB,KAC5G,oBAAqB,OAAO,EAAE,cAAc,uBAA0B,SAAW,EAAE,aAAa,sBAAwB,KACxH,gBAAiB,MAAM,QAAQ,EAAE,cAAc,gBAAgB,EAAI,EAAE,aAAa,iBAAmB,CAAC,MAAM,EAC5G,iBAAAiG,EACA,oBAAAC,EAEA,iBAAkBlG,IAAW,aAC7B,iBAAkBkG,EAAoB,SAAS,iBAAiB,GAAKA,EAAoB,SAAS,oBAAoB,EACtH,kBAAmBA,EAAoB,SAAS,WAAW,GAAKA,EAAoB,SAAS,mBAAmB,EAChH,QAASP,GAAiB,EAAE,OAAO,CAAA,CAEvC,CAGO,SAASS,GAAwBC,EAAkC,CACxE,MAAMC,EAAQD,GAAsC,KACpD,OAAK,MAAM,QAAQC,CAAI,EAChBA,EAAK,IAAIN,EAAc,EAAE,OAAQnG,GAA4B,CAAC,CAACA,CAAC,EADtC,CAAA,CAEnC,CAYA,eAAsB0G,GAAsBC,EAA8B,GAAgC,CACxG,MAAMC,EAAkC,CAAE,OAAQ,mBAAoB,GAAID,EAAQ,SAAW,EAAC,EAC1FA,EAAQ,SAAQC,EAAQ,cAAgB,UAAUD,EAAQ,MAAM,IACpE,MAAME,EAAM,MAAM,MAAMF,EAAQ,KAAOjB,EAAuB,CAAE,QAAAkB,EAAS,OAAQD,EAAQ,MAAA,CAAQ,EACjG,GAAI,CAACE,EAAI,GAAI,MAAM,IAAI,MAAM,+BAA+BA,EAAI,MAAM,EAAE,EACxE,OAAON,GAAwB,MAAMM,EAAI,MAAM,CACjD,CC/GA,MAAMC,GAAiB,IAAU,IAC3BC,MAAY,IACZC,MAAe,IAGd,SAASC,IAAmC,CACjDF,EAAM,MAAA,CACR,CAQO,SAASG,GAAoBP,EAAsC,GAA+B,CACvG,KAAM,CAAE,QAAAQ,EAAS,MAAAC,EAAQN,GAAgB,QAAAO,EAAU,GAAM,OAAAC,EAAQ,IAAAC,EAAM7B,EAAuB,QAAAkB,CAAA,EAAYD,EACpGa,EAAWL,EAAU,WAAWI,CAAG,GAAK,GAAGA,CAAG,IAAID,GAAU,EAAE,GAE9D,CAACG,EAAOC,CAAQ,EAAI/E,EAAAA,SAA4B,IAAMoE,EAAM,IAAIS,CAAQ,GAAK,IAAI,EACjF,CAAChG,EAASmG,CAAU,EAAIhF,EAAAA,SAAS,EAAK,EACtC,CAAClB,EAAYmG,CAAa,EAAIjF,EAAAA,SAAS,EAAK,EAC5C,CAACjB,EAAOmG,CAAQ,EAAIlF,EAAAA,SAAuB,IAAI,EAC/CmF,EAAU1E,EAAAA,OAAO,EAAI,EACrB2E,EAAa3E,EAAAA,OAAO+D,CAAO,EACjCY,EAAW,QAAUZ,EAErBpD,EAAAA,UAAU,KACR+D,EAAQ,QAAU,GACX,IAAM,CAAEA,EAAQ,QAAU,EAAO,GACvC,CAAA,CAAE,EAEL,MAAME,EAAOC,cAAY,MAAOC,GAAmB,CACjD,MAAMC,EAASpB,EAAM,IAAIS,CAAQ,EACjC,GAAI,CAACU,GAASC,GAAU,KAAK,MAAQA,EAAO,UAAYf,EAAO,CAC7DM,EAASS,CAAM,EACf,MACF,CACA,IAAIC,EAAUpB,EAAS,IAAIQ,CAAQ,EAC9BY,IACHA,GAAWL,EAAW,QAAUA,EAAW,QAAA,EAAYrB,GAAsB,CAAE,OAAAY,EAAQ,IAAAC,EAAK,QAAAX,CAAA,CAAS,GAClG,KAAMtF,IACLyF,EAAM,IAAIS,EAAU,CAAE,OAAAlG,EAAQ,UAAW,KAAK,IAAA,EAAO,EAC9CA,EACR,EACA,QAAQ,IAAM0F,EAAS,OAAOQ,CAAQ,CAAC,EAC1CR,EAAS,IAAIQ,EAAUY,CAAO,GAEhCD,EAASP,EAAc,EAAI,EAAID,EAAW,EAAI,EAC9CE,EAAS,IAAI,EACb,GAAI,CACF,MAAMO,EACFN,EAAQ,SAASJ,EAASX,EAAM,IAAIS,CAAQ,GAAK,IAAI,CAC3D,OAASvD,EAAG,CACN6D,EAAQ,SAASD,EAAS5D,aAAa,MAAQA,EAAI,IAAI,MAAM,OAAOA,CAAC,CAAC,CAAC,CAC7E,QAAA,CACM6D,EAAQ,UAAWH,EAAW,EAAK,EAAGC,EAAc,EAAK,EAC/D,CACF,EAAG,CAACJ,EAAUJ,EAAOE,EAAQC,EAAKX,CAAO,CAAC,EAE1C7C,OAAAA,EAAAA,UAAU,IAAM,CACVsD,GAAcW,EAAK,EAAK,CAC9B,EAAG,CAACX,EAASW,CAAI,CAAC,EAEX,CACL,OAAQP,GAAO,QAAU,CAAA,EACzB,QAAAjG,EACA,WAAAC,EACA,MAAAC,EACA,UAAW+F,EAAQ,IAAI,KAAKA,EAAM,SAAS,EAAE,cAAgB,KAC7D,QAAS,IAAMO,EAAK,EAAI,CAAA,CAE5B"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { JSX } from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Blended USD per 1M tokens at a typical 3:1 input:output ratio — used by the
|
|
6
|
+
* "cheapest" sort. A per-request fee is counted as ~1000 requests per 1M tokens.
|
|
7
|
+
*/
|
|
8
|
+
export declare function blendedPricePerM(m: OpenRouterModel): number;
|
|
9
|
+
|
|
10
|
+
/** Clear the module-level cache (tests, or after changing the API key). */
|
|
11
|
+
export declare function clearOpenRouterModelsCache(): void;
|
|
12
|
+
|
|
13
|
+
export declare const DEFAULT_VENDOR_LABELS: Record<string, string>;
|
|
14
|
+
|
|
15
|
+
export declare const enLabels: PickerLabels;
|
|
16
|
+
|
|
17
|
+
export declare interface FetchModelsOptions {
|
|
18
|
+
/** Optional. Without a key the public list is returned; with one, only models the key can call. */
|
|
19
|
+
apiKey?: string;
|
|
20
|
+
url?: string;
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
/** Extra headers, e.g. `HTTP-Referer` / `X-Title` for OpenRouter app attribution. */
|
|
23
|
+
headers?: Record<string, string>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Fetch and normalize the OpenRouter model list. Works in browsers and Node 18+. */
|
|
27
|
+
export declare function fetchOpenRouterModels(options?: FetchModelsOptions): Promise<OpenRouterModel[]>;
|
|
28
|
+
|
|
29
|
+
/** "200k" / "1M" / "1.5M" context window. */
|
|
30
|
+
export declare function formatContext(tokens: number | null | undefined): string;
|
|
31
|
+
|
|
32
|
+
/** "$1.25" / "$0.0004" / "$0" — USD per 1M tokens. */
|
|
33
|
+
export declare function formatPerM(value: number | null | undefined): string;
|
|
34
|
+
|
|
35
|
+
export declare interface ModelPricing {
|
|
36
|
+
/** USD per 1M input (prompt) tokens. */
|
|
37
|
+
promptPerM: number;
|
|
38
|
+
/** USD per 1M output (completion) tokens. */
|
|
39
|
+
completionPerM: number;
|
|
40
|
+
/** USD per 1M reasoning tokens, when the provider bills them separately. */
|
|
41
|
+
reasoningPerM: number | null;
|
|
42
|
+
/** Flat USD fee per request (some providers charge it on top of tokens). */
|
|
43
|
+
perRequest: number;
|
|
44
|
+
/** USD per web search performed by a model with built-in search. */
|
|
45
|
+
perWebSearch: number;
|
|
46
|
+
/** USD per 1M tokens read from prompt cache. */
|
|
47
|
+
cacheReadPerM: number | null;
|
|
48
|
+
/** USD per input image. */
|
|
49
|
+
perImage: number;
|
|
50
|
+
/** True when prompt, completion and per-request prices are all zero. */
|
|
51
|
+
isFree: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* One raw entry from `data[]` → `OpenRouterModel`.
|
|
56
|
+
* Returns null for entries that cannot produce text (image-only models).
|
|
57
|
+
*/
|
|
58
|
+
export declare function normalizeModel(raw: unknown): OpenRouterModel | null;
|
|
59
|
+
|
|
60
|
+
/** Whole `/models` response body → normalized list (text-capable models only). */
|
|
61
|
+
export declare function normalizeModelsResponse(body: unknown): OpenRouterModel[];
|
|
62
|
+
|
|
63
|
+
/** Raw `pricing` object of one model → per-1M prices. */
|
|
64
|
+
export declare function normalizePricing(raw: unknown): ModelPricing;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Normalization of the OpenRouter `GET /api/v1/models` response.
|
|
68
|
+
*
|
|
69
|
+
* Pure TypeScript, no React — safe to reuse on a server. OpenRouter reports
|
|
70
|
+
* prices as STRINGS per single token (e.g. "0.000008"); here they become
|
|
71
|
+
* numbers per 1,000,000 tokens, which is how price lists are usually read.
|
|
72
|
+
*/
|
|
73
|
+
export declare const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
|
74
|
+
|
|
75
|
+
export declare interface OpenRouterModel {
|
|
76
|
+
/** OpenRouter slug, e.g. "openai/gpt-5". */
|
|
77
|
+
id: string;
|
|
78
|
+
/** Display name, e.g. "OpenAI: GPT-5". */
|
|
79
|
+
name: string;
|
|
80
|
+
description: string;
|
|
81
|
+
/** Vendor taken from the id prefix ("openai/gpt-5" → "openai"). */
|
|
82
|
+
vendor: string;
|
|
83
|
+
/** Unix timestamp (seconds) when the model was added. */
|
|
84
|
+
created: number;
|
|
85
|
+
contextLength: number | null;
|
|
86
|
+
maxCompletionTokens: number | null;
|
|
87
|
+
inputModalities: string[];
|
|
88
|
+
outputModalities: string[];
|
|
89
|
+
/** OpenAI-compatible parameters the model accepts (union over providers). */
|
|
90
|
+
supportedParameters: string[];
|
|
91
|
+
/** The model searches the web on its own (Perplexity) — no plugin needed. */
|
|
92
|
+
builtInWebSearch: boolean;
|
|
93
|
+
/** Accepts `response_format` / `structured_outputs`. */
|
|
94
|
+
supportsJsonMode: boolean;
|
|
95
|
+
supportsReasoning: boolean;
|
|
96
|
+
pricing: ModelPricing;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Searchable picker over the full OpenRouter model list: text search across
|
|
101
|
+
* id / name / vendor, vendor filter, sorting, keyboard navigation, and a price
|
|
102
|
+
* per 1M tokens on every row — choosing a model is choosing its price list.
|
|
103
|
+
*/
|
|
104
|
+
export declare function OpenRouterModelPicker(props: OpenRouterModelPickerProps): JSX.Element;
|
|
105
|
+
|
|
106
|
+
export declare interface OpenRouterModelPickerProps {
|
|
107
|
+
/** Normalized model list — from `useOpenRouterModels`, your own query, or a server. */
|
|
108
|
+
models: OpenRouterModel[];
|
|
109
|
+
/** Selected model id. Empty/undefined = nothing selected (or "automatic" when `allowAuto`). */
|
|
110
|
+
value?: string;
|
|
111
|
+
/** Called with the model id, or "" when the automatic option is chosen. */
|
|
112
|
+
onChange: (id: string) => void;
|
|
113
|
+
loading?: boolean;
|
|
114
|
+
refreshing?: boolean;
|
|
115
|
+
error?: unknown;
|
|
116
|
+
/** Shown as a "Refresh prices" button inside the dropdown. */
|
|
117
|
+
onRefresh?: () => void;
|
|
118
|
+
/** ISO string or Date of the list currently shown — rendered next to the label. */
|
|
119
|
+
fetchedAt?: string | Date | null;
|
|
120
|
+
/** Model the server would use when none is chosen; marked in the list and shown under "automatic". */
|
|
121
|
+
defaultModelId?: string;
|
|
122
|
+
/** Adds an "automatic" entry at the top of the list that maps to value "". */
|
|
123
|
+
allowAuto?: boolean;
|
|
124
|
+
/** Label above the control. Omit for a bare control. */
|
|
125
|
+
label?: ReactNode;
|
|
126
|
+
/** Tighter paddings, no detail chips under the control. */
|
|
127
|
+
compact?: boolean;
|
|
128
|
+
disabled?: boolean;
|
|
129
|
+
/** Partial overrides merged over the English defaults; import `plLabels` for Polish. */
|
|
130
|
+
labels?: Partial<PickerLabels>;
|
|
131
|
+
/** Vendor id → display name overrides (e.g. { 'x-ai': 'xAI' }). */
|
|
132
|
+
vendorLabels?: Record<string, string>;
|
|
133
|
+
/** Restrict the list (e.g. only models with JSON mode). */
|
|
134
|
+
filter?: (model: OpenRouterModel) => boolean;
|
|
135
|
+
initialSort?: PickerSortKey;
|
|
136
|
+
/** Force light/dark regardless of the OS setting. */
|
|
137
|
+
theme?: 'light' | 'dark';
|
|
138
|
+
className?: string;
|
|
139
|
+
id?: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export declare interface PickerLabels {
|
|
143
|
+
placeholder: string;
|
|
144
|
+
searchPlaceholder: string;
|
|
145
|
+
allVendors: string;
|
|
146
|
+
sortName: string;
|
|
147
|
+
sortCheapest: string;
|
|
148
|
+
sortNewest: string;
|
|
149
|
+
sortContext: string;
|
|
150
|
+
refresh: string;
|
|
151
|
+
refreshing: string;
|
|
152
|
+
loading: string;
|
|
153
|
+
loadError: string;
|
|
154
|
+
noMatches: (query: string) => string;
|
|
155
|
+
countOf: (shown: number, total: number) => string;
|
|
156
|
+
modelsCount: (n: number) => string;
|
|
157
|
+
pricesFrom: (time: string) => string;
|
|
158
|
+
auto: string;
|
|
159
|
+
now: string;
|
|
160
|
+
isDefault: string;
|
|
161
|
+
free: string;
|
|
162
|
+
web: string;
|
|
163
|
+
json: string;
|
|
164
|
+
reasoning: string;
|
|
165
|
+
context: string;
|
|
166
|
+
maxOutput: string;
|
|
167
|
+
perRequest: string;
|
|
168
|
+
priceTitle: string;
|
|
169
|
+
webTitle: string;
|
|
170
|
+
jsonTitle: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export declare type PickerSortKey = 'name' | 'cheapest' | 'newest' | 'context';
|
|
174
|
+
|
|
175
|
+
export declare const plLabels: PickerLabels;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Loads the OpenRouter model list with a small module-level cache, so several
|
|
179
|
+
* pickers on one page share a single request. No external state library needed.
|
|
180
|
+
* If you already use TanStack Query or SWR, skip this hook and pass `models`
|
|
181
|
+
* from your own query into the picker.
|
|
182
|
+
*/
|
|
183
|
+
export declare function useOpenRouterModels(options?: UseOpenRouterModelsOptions): UseOpenRouterModelsResult;
|
|
184
|
+
|
|
185
|
+
export declare interface UseOpenRouterModelsOptions extends Omit<FetchModelsOptions, 'signal'> {
|
|
186
|
+
/**
|
|
187
|
+
* Replace the default fetch entirely — e.g. call your own backend that proxies
|
|
188
|
+
* OpenRouter and adds auth. Must resolve to already-normalized models
|
|
189
|
+
* (use `normalizeModelsResponse` if your backend forwards the raw body).
|
|
190
|
+
*/
|
|
191
|
+
fetcher?: () => Promise<OpenRouterModel[]>;
|
|
192
|
+
/** How long a fetched list stays fresh across all hook instances. Default 10 minutes. */
|
|
193
|
+
ttlMs?: number;
|
|
194
|
+
/** Set to false to skip fetching (e.g. until the user opens the picker). */
|
|
195
|
+
enabled?: boolean;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export declare interface UseOpenRouterModelsResult {
|
|
199
|
+
models: OpenRouterModel[];
|
|
200
|
+
loading: boolean;
|
|
201
|
+
refreshing: boolean;
|
|
202
|
+
error: Error | null;
|
|
203
|
+
/** ISO time of the list currently held (null before the first successful fetch). */
|
|
204
|
+
fetchedAt: string | null;
|
|
205
|
+
/** Bypass the cache and fetch again. */
|
|
206
|
+
refresh: () => Promise<void>;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export declare function vendorLabel(vendor: string, overrides?: Record<string, string>): string;
|
|
210
|
+
|
|
211
|
+
export { }
|