@llmnative/react 2.3.0 → 3.0.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.
Files changed (61) hide show
  1. package/README.md +3 -2
  2. package/dist/index.js +20 -21
  3. package/dist/index.js.map +1 -1
  4. package/dist/index.mjs +6768 -6811
  5. package/dist/index.mjs.map +1 -1
  6. package/dist/modelIdentity-CIXNPtTA.mjs +624 -0
  7. package/dist/modelIdentity-CIXNPtTA.mjs.map +1 -0
  8. package/dist/modelIdentity-DX62i7g2.js +3 -0
  9. package/dist/modelIdentity-DX62i7g2.js.map +1 -0
  10. package/dist/server-B430eH0y.mjs +352 -0
  11. package/dist/server-B430eH0y.mjs.map +1 -0
  12. package/dist/server-CDheNgME.js +2 -0
  13. package/dist/server-CDheNgME.js.map +1 -0
  14. package/dist/server.js +1 -1
  15. package/dist/server.mjs +3 -3
  16. package/dist/types/src/App.d.ts +6 -4
  17. package/dist/types/src/I18n.d.ts +2 -0
  18. package/dist/types/src/components/widgets/grid-core/GridCore.d.ts +1 -1
  19. package/dist/types/src/components/widgets/grid-core/types.d.ts +2 -0
  20. package/dist/types/src/components/widgets/grid-core/useGridDBRecords.d.ts +6 -2
  21. package/dist/types/src/index.d.ts +21 -8
  22. package/dist/types/src/libs/index.d.ts +0 -1
  23. package/dist/types/src/providers/ProviderRegistryContext.d.ts +7 -5
  24. package/dist/types/src/providers/ai/server.d.ts +20 -0
  25. package/dist/types/src/providers/api/ApiProvider.d.ts +1 -1
  26. package/dist/types/src/providers/data/DataProvider.d.ts +29 -0
  27. package/dist/types/src/providers/data/firebase.d.ts +2 -2
  28. package/dist/types/src/providers/data/firestore.d.ts +2 -2
  29. package/dist/types/src/providers/data/query.d.ts +14 -0
  30. package/dist/types/src/providers/data/useDataSubscription.d.ts +20 -0
  31. package/dist/types/src/providers/email/EmailError.d.ts +27 -0
  32. package/dist/types/src/providers/email/EmailProviderContext.d.ts +13 -7
  33. package/dist/types/src/providers/email/EmailRead.d.ts +48 -0
  34. package/dist/types/src/providers/email/EmailSend.d.ts +54 -0
  35. package/dist/types/src/providers/email/brevo/BrevoEmailSender.d.ts +21 -0
  36. package/dist/types/src/providers/email/definitions.d.ts +4 -2
  37. package/dist/types/src/providers/email/gmail/GmailEmailReader.d.ts +37 -0
  38. package/dist/types/src/providers/email/gmail/GmailEmailSender.d.ts +21 -0
  39. package/dist/types/src/providers/email/gmail/browser.d.ts +10 -0
  40. package/dist/types/src/providers/email/gmail/gmailApi.d.ts +53 -0
  41. package/dist/types/src/providers/email/gmail/gmailOAuth.d.ts +25 -0
  42. package/dist/types/src/providers/email/imap/ImapEmailReader.d.ts +70 -0
  43. package/dist/types/src/providers/email/resend/ResendEmailSender.d.ts +20 -0
  44. package/dist/types/src/providers/manifest.d.ts +7 -5
  45. package/dist/vite.js +1 -1
  46. package/dist/vite.mjs +1 -1
  47. package/llms-full.txt +1690 -1625
  48. package/llms.txt +83 -82
  49. package/package.json +18 -1
  50. package/dist/modelIdentity-CGWliGDU.mjs +0 -183
  51. package/dist/modelIdentity-CGWliGDU.mjs.map +0 -1
  52. package/dist/modelIdentity-CgRMrC5H.js +0 -2
  53. package/dist/modelIdentity-CgRMrC5H.js.map +0 -1
  54. package/dist/server-CCbjPJEz.js +0 -2
  55. package/dist/server-CCbjPJEz.js.map +0 -1
  56. package/dist/server-CiWwr3Zg.mjs +0 -107
  57. package/dist/server-CiWwr3Zg.mjs.map +0 -1
  58. package/dist/types/src/libs/email.d.ts +0 -16
  59. package/dist/types/src/providers/email/EmailProvider.d.ts +0 -10
  60. package/dist/types/src/providers/email/google/GmailEmailProvider.d.ts +0 -7
  61. package/dist/types/src/providers/email/google/email.d.ts +0 -13
package/llms.txt CHANGED
@@ -1,82 +1,83 @@
1
- # @llmnative/react
2
-
3
- > Framework React AI-first per generazione deterministica di UI da configurazione dichiarativa.
4
- > Data-driven di default, schema-driven opzionale. Ottimizzato per minimo consumo di token AI.
5
- > Apache-2.0 — versione 1.0.0
6
-
7
- ## Install
8
-
9
- Requisito: **Node.js 24 LTS** (`>=24.18.0 <25`, `engines` del pacchetto).
10
-
11
- ```bash
12
- npm install @llmnative/react
13
- npx @llmnative/react create # scaffold progetto Vite completo
14
- npx @llmnative/react create --yes --provider=mock # scaffold senza backend
15
- ```
16
-
17
- ```tsx
18
- import '@llmnative/react/dist/index.css' // necessario una volta sola
19
- ```
20
-
21
- ## Cosa fa
22
-
23
- `Grid` genera tabelle e gallery CRUD complete (fetch real-time, modal add/edit/delete, sorting, pagination, groupBy) da una configurazione colonne. `Form` gestisce stato, validazione, oggetti annidati e lifecycle hooks verso qualsiasi provider. I provider (Firebase, Supabase, mock) si swappano in `<App>` senza toccare i componenti.
24
-
25
- ## 5 pattern core — 90% dei casi d'uso
26
-
27
- ```tsx
28
- // 1 — Lista CRUD con real-time
29
- <Grid path="/users" columns={[{ key: 'name', label: 'Nome', sortable: true }, { key: 'email', label: 'Email' }]} actions={['add','edit','delete']} pagination={{ limit: 20 }} />
30
-
31
- // 2 — Form standalone
32
- <Form path="/products" appearance="card" showBack>
33
- <Input name="title" label="Titolo" required />
34
- <Select name="status" label="Stato" options={['draft','published']} />
35
- </Form>
36
-
37
- // 3 — Oggetti annidati e array dinamici
38
- <Input name="address.city" label="Città" />
39
- <Repeat name="items" defaultLength={1}>{(i) => <Input name={`items.${i}.name`} />}</Repeat>
40
-
41
- // 4 — Colonne con renderer custom e Badge
42
- <Grid path="/orders" columns={[{ key: 'status', render: ({ value }) => <Badge variant="success">{value}</Badge> }, { key: 'amount', render: ({ value }) => `€ ${value.toFixed(2)}` }]} />
43
-
44
- // 5 — Form con lifecycle hooks
45
- <Form path="/posts" onLoad={d => ({...d, price: d.price/100})} onSave={async ({record}) => ({...record, price: record.price*100})} onComplete={async ({action}) => { if (action === 'save') navigate('/posts') }} />
46
- ```
47
-
48
- ## Provider matrix
49
-
50
- | Servizio | Driver disponibili | Obbligatorio |
51
- |---|---|---|
52
- | data | `mock`, `dbRealtime` (Firebase RTDB), `firestoreDb` (Firestore), `supabaseDb` | sì (fallback: mock) |
53
- | storage | `firestorage`, `supabaseStorage` | no |
54
- | auth | `googleAuth`, `firebaseAuth`, `supabaseAuth`, `dropboxAuth` | sì (fallback: googleAuth) |
55
- | email | `gmail` | no |
56
- | ai | `openai`, `gemini`, `anthropic`, `deepseek`, `mistral`, `openrouter`, `opencode`, `cloudflare`, `groq` | no |
57
- | credentials | `googleServiceAccount` | no |
58
-
59
- ```tsx
60
- <App providers={{ firebase: { apiKey, authDomain, databaseURL }, services: { data: 'dbRealtime', storage: 'firestorage', auth: 'googleAuth' } }} />
61
- ```
62
-
63
- ## Struttura componenti
64
-
65
- ```
66
- ui/ → primitivi stateless: Alert, Badge, Button, Card, DescriptionList, Image, Modal, Table, Gallery, Tab, Repeat, Icon, LocaleSwitcher, ThemeSwitcher, ProviderSwitcher, LayoutBuilder, ContextMenu
67
- ui/fields/ → campi form: Input, Select, UploadImage, UploadDocument, UploadCSV, CodeEditor, ImageField, RichText, Prompt, Crop, ContextMenu
68
- blocks/ → composizioni: Menu, Breadcrumbs, Notifications, Search, Carousel, Dropdown, Toolbar, SideNav, ListCard, AuthButton
69
- widgets/ → smart (hanno stato e provider): Grid, Form, MarkdownReader, ImageEditor, TabDynamic, Chatbot, ChatMarkdown, ChatReasoning, ChatQuestions, ChatActivity
70
- ```
71
-
72
- Tutti gli import da `'@llmnative/react'` — mai da sottodirectory.
73
-
74
- ## Non adatto per
75
-
76
- - Marketing sites, e-commerce, landing page con layout custom
77
- - UI pixel-perfect che non mappano a CRUD
78
- - Applicazioni senza dati strutturati
79
-
80
- ## Riferimento completo
81
-
82
- → [llms-full.txt](llms-full.txt) — API di ogni componente, ogni provider, ogni hook, tipi TypeScript
1
+ # @llmnative/react
2
+
3
+ > Framework React AI-first per generazione deterministica di UI da configurazione dichiarativa.
4
+ > Data-driven di default, schema-driven opzionale. Ottimizzato per minimo consumo di token AI.
5
+ > Apache-2.0 — versione 1.0.0
6
+
7
+ ## Install
8
+
9
+ Requisito: **Node.js 24 LTS** (`>=24.18.0 <25`, `engines` del pacchetto).
10
+
11
+ ```bash
12
+ npm install @llmnative/react
13
+ npx @llmnative/react create # scaffold progetto Vite completo
14
+ npx @llmnative/react create --yes --provider=mock # scaffold senza backend
15
+ ```
16
+
17
+ ```tsx
18
+ import '@llmnative/react/dist/index.css' // necessario una volta sola
19
+ ```
20
+
21
+ ## Cosa fa
22
+
23
+ `Grid` genera tabelle e gallery CRUD complete (fetch real-time, modal add/edit/delete, sorting, pagination, groupBy) da una configurazione colonne. `Form` gestisce stato, validazione, oggetti annidati e lifecycle hooks verso qualsiasi provider. I provider (Firebase, Supabase, mock) si swappano in `<App>` senza toccare i componenti.
24
+
25
+ ## 5 pattern core — 90% dei casi d'uso
26
+
27
+ ```tsx
28
+ // 1 — Lista CRUD con real-time
29
+ <Grid path="/users" columns={[{ key: 'name', label: 'Nome', sortable: true }, { key: 'email', label: 'Email' }]} actions={['add','edit','delete']} pagination={{ limit: 20 }} />
30
+
31
+ // 2 — Form standalone
32
+ <Form path="/products" appearance="card" showBack>
33
+ <Input name="title" label="Titolo" required />
34
+ <Select name="status" label="Stato" options={['draft','published']} />
35
+ </Form>
36
+
37
+ // 3 — Oggetti annidati e array dinamici
38
+ <Input name="address.city" label="Città" />
39
+ <Repeat name="items" defaultLength={1}>{(i) => <Input name={`items.${i}.name`} />}</Repeat>
40
+
41
+ // 4 — Colonne con renderer custom e Badge
42
+ <Grid path="/orders" columns={[{ key: 'status', render: ({ value }) => <Badge variant="success">{value}</Badge> }, { key: 'amount', render: ({ value }) => `€ ${value.toFixed(2)}` }]} />
43
+
44
+ // 5 — Form con lifecycle hooks
45
+ <Form path="/posts" onLoad={d => ({...d, price: d.price/100})} onSave={async ({record}) => ({...record, price: record.price*100})} onComplete={async ({action}) => { if (action === 'save') navigate('/posts') }} />
46
+ ```
47
+
48
+ ## Provider matrix
49
+
50
+ | Servizio | Driver disponibili | Obbligatorio |
51
+ |---|---|---|
52
+ | data | `mock`, `dbRealtime` (Firebase RTDB), `firestoreDb` (Firestore), `supabaseDb` | sì (fallback: mock) |
53
+ | storage | `firestorage`, `supabaseStorage` | no |
54
+ | auth | `googleAuth`, `firebaseAuth`, `supabaseAuth`, `dropboxAuth` | sì (fallback: googleAuth) |
55
+ | emailSend | `gmail`; server: Resend, Brevo, Gmail | no |
56
+ | emailRead | server: Gmail (Pub/Sub), IMAP | no |
57
+ | ai | `openai`, `gemini`, `anthropic`, `deepseek`, `mistral`, `openrouter`, `opencode`, `cloudflare`, `groq` | no |
58
+ | credentials | `googleServiceAccount` | no |
59
+
60
+ ```tsx
61
+ <App providers={{ firebase: { apiKey, authDomain, databaseURL }, services: { data: 'dbRealtime', storage: 'firestorage', auth: 'googleAuth' } }} />
62
+ ```
63
+
64
+ ## Struttura componenti
65
+
66
+ ```
67
+ ui/ → primitivi stateless: Alert, Badge, Button, Card, DescriptionList, Image, Modal, Table, Gallery, Tab, Repeat, Icon, LocaleSwitcher, ThemeSwitcher, ProviderSwitcher, LayoutBuilder, ContextMenu
68
+ ui/fields/ → campi form: Input, Select, UploadImage, UploadDocument, UploadCSV, CodeEditor, ImageField, RichText, Prompt, Crop, ContextMenu
69
+ blocks/ → composizioni: Menu, Breadcrumbs, Notifications, Search, Carousel, Dropdown, Toolbar, SideNav, ListCard, AuthButton
70
+ widgets/ → smart (hanno stato e provider): Grid, Form, MarkdownReader, ImageEditor, TabDynamic, Chatbot, ChatMarkdown, ChatReasoning, ChatQuestions, ChatActivity
71
+ ```
72
+
73
+ Tutti gli import da `'@llmnative/react'` — mai da sottodirectory.
74
+
75
+ ## Non adatto per
76
+
77
+ - Marketing sites, e-commerce, landing page con layout custom
78
+ - UI pixel-perfect che non mappano a CRUD
79
+ - Applicazioni senza dati strutturati
80
+
81
+ ## Riferimento completo
82
+
83
+ → [llms-full.txt](llms-full.txt) — API di ogni componente, ogni provider, ogni hook, tipi TypeScript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llmnative/react",
3
- "version": "2.3.0",
3
+ "version": "3.0.0",
4
4
  "description": "LLM-native React framework for deterministic UI generation. Data-driven by default, schema-driven optional. Ultra-low token consumption — built for AI agents, not just humans.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -87,7 +87,9 @@
87
87
  "@phosphor-icons/react": "^2.1.0",
88
88
  "@supabase/supabase-js": "^2.110.0",
89
89
  "firebase": "^12.16.0",
90
+ "imapflow": "^1.0.0 || ^2.0.0",
90
91
  "lucide-react": "^1.26.0",
92
+ "mailparser": "^3.6.0",
91
93
  "react": "^19.2.0",
92
94
  "react-dom": "^19.2.0",
93
95
  "react-router-dom": "^7.18.1"
@@ -96,8 +98,20 @@
96
98
  "@phosphor-icons/react": {
97
99
  "optional": true
98
100
  },
101
+ "@supabase/supabase-js": {
102
+ "optional": true
103
+ },
104
+ "firebase": {
105
+ "optional": true
106
+ },
107
+ "imapflow": {
108
+ "optional": true
109
+ },
99
110
  "lucide-react": {
100
111
  "optional": true
112
+ },
113
+ "mailparser": {
114
+ "optional": true
101
115
  }
102
116
  },
103
117
  "devDependencies": {
@@ -111,6 +125,7 @@
111
125
  "@testing-library/react": "^16.3.3",
112
126
  "@testing-library/user-event": "^14.6.7",
113
127
  "@types/google.maps": "^3.66.4",
128
+ "@types/mailparser": "^3.9.0",
114
129
  "@types/papaparse": "^5.5.2",
115
130
  "@types/prismjs": "^1.26.6",
116
131
  "@types/react": "^19.3.0",
@@ -120,8 +135,10 @@
120
135
  "autoprefixer": "^10.6.1",
121
136
  "firebase": "^12.19.0",
122
137
  "happy-dom": "^20.14.5",
138
+ "imapflow": "^2.11.0",
123
139
  "jsdom": "^30.1.1",
124
140
  "lucide-react": "^1.49.0",
141
+ "mailparser": "^3.14.3",
125
142
  "postcss": "^8.5.28",
126
143
  "react": "^19.3.0",
127
144
  "react-dom": "^19.3.0",
@@ -1,183 +0,0 @@
1
- //#region src/providers/ai/serverCredentials.ts
2
- var e = "llmnative-server-credential", t = /^[A-Za-z0-9_-]+$/, n = /llmnative-server-credential:([A-Za-z0-9_-]+):([a-z-]+)/g, r = (n, r) => {
3
- if (!t.test(n)) throw Error(`Server credential scope "${n}" may only contain letters, digits, "-" and "_".`);
4
- return `${e}:${n}:${r}`;
5
- }, i = (e) => [...e.matchAll(n)].map(([e, t, n]) => ({
6
- placeholder: e,
7
- scope: t,
8
- provider: n
9
- })), a = (t) => t.includes(`${e}:`), o = {
10
- openai: "api.openai.com",
11
- openrouter: "openrouter.ai",
12
- opencode: "opencode.ai",
13
- "opencode-go": "opencode.ai",
14
- deepseek: "api.deepseek.com",
15
- gemini: "generativelanguage.googleapis.com",
16
- anthropic: "api.anthropic.com",
17
- mistral: "api.mistral.ai",
18
- glm: "open.bigmodel.cn",
19
- groq: "api.groq.com",
20
- cloudflare: "api.cloudflare.com"
21
- }, s = (e) => e.replace(/\/+$/, ""), c = (e, t) => {
22
- if (e.id === "openai-compatible") {
23
- if (!e.baseUrl) return !1;
24
- let n = new URL(e.baseUrl), r = s(n.pathname);
25
- return t.origin === n.origin && (t.pathname === r || t.pathname.startsWith(`${r}/`));
26
- }
27
- return t.protocol !== "https:" || t.host !== o[e.id] ? !1 : e.id !== "cloudflare" || !!e.accountId && t.pathname.startsWith(`/client/v4/accounts/${encodeURIComponent(e.accountId.trim())}/`);
28
- }, l = "https://models.dev/api.json", u = "ai.pricing.modelsdev", d = 864e5, f = {
29
- openai: ["openai"],
30
- openrouter: ["openrouter"],
31
- opencode: ["opencode"],
32
- "opencode-go": ["opencode-go"],
33
- deepseek: ["deepseek"],
34
- gemini: ["google"],
35
- anthropic: ["anthropic"],
36
- mistral: ["mistral"],
37
- glm: ["zhipuai", "zai"],
38
- cloudflare: ["cloudflare-workers-ai"],
39
- groq: ["groq"]
40
- }, p = null, m = () => {
41
- if (typeof localStorage > "u") return null;
42
- try {
43
- let e = localStorage.getItem(u);
44
- if (!e) return null;
45
- let t = JSON.parse(e);
46
- return !t.fetchedAt || !t.index || Date.now() - t.fetchedAt > d ? null : t.index;
47
- } catch {
48
- return null;
49
- }
50
- }, h = (e) => {
51
- if (typeof localStorage < "u") try {
52
- localStorage.setItem(u, JSON.stringify({
53
- fetchedAt: Date.now(),
54
- index: e
55
- }));
56
- } catch {}
57
- }, g = (e) => {
58
- let t = new Set(Object.values(f).flat()), n = {};
59
- for (let r of t) {
60
- let t = e[r]?.models ?? {}, i = {};
61
- for (let [e, n] of Object.entries(t)) {
62
- let t = n?.cost?.input, r = n?.cost?.output;
63
- typeof t == "number" && typeof r == "number" && (i[e] = [t, r]);
64
- }
65
- n[r] = i;
66
- }
67
- return n;
68
- }, _ = () => {
69
- let e = m();
70
- return e ? Promise.resolve(e) : (p ||= fetch(l).then((e) => e.ok ? e.json() : Promise.reject(/* @__PURE__ */ Error(`HTTP ${e.status}`))).then((e) => {
71
- let t = g(e);
72
- return h(t), t;
73
- }).catch((e) => (console.warn("models.dev pricing unavailable", e), {})).finally(() => {
74
- p = null;
75
- }), p);
76
- }, v = (e) => {
77
- let t = e.toLowerCase(), n = t.replace(/-(\d{8}|\d{4}-\d{2}-\d{2})$/, "");
78
- return [.../* @__PURE__ */ new Set([
79
- e,
80
- t,
81
- n
82
- ])];
83
- }, y = (e, t, n) => {
84
- for (let r of f[t] ?? []) {
85
- let t = e[r];
86
- if (t) for (let e of v(n)) {
87
- let n = t[e];
88
- if (n) return {
89
- input: n[0],
90
- output: n[1],
91
- currency: "USD",
92
- source: "models.dev"
93
- };
94
- }
95
- }
96
- }, b = async (e, t) => {
97
- if (!f[e] || t.every((e) => e.pricing)) return t;
98
- let n = await _();
99
- return t.map((t) => {
100
- if (t.pricing) return t;
101
- let r = y(n, e, t.model);
102
- return r ? {
103
- ...t,
104
- pricing: r
105
- } : t;
106
- });
107
- }, x = /* @__PURE__ */ new Set(["free", "latest"]), S = /^(\d{4}|\d{8}|\d{4}-\d{2}-\d{2})$/, C = /\s*\(([^()]*)\)\s*$/, w = /^(thinking|reasoning|non-reasoning|fast)$/i, T = (e) => {
108
- let t = e.trim(), n = [];
109
- for (let e = C.exec(t); e; e = C.exec(t)) w.test(e[1].trim()) && n.unshift(e[1].trim()), t = t.slice(0, e.index);
110
- let r = t.toLowerCase().replace(/[()[\]]/g, " ").split(/\s+/).filter(Boolean);
111
- for (; r.length > 1 && (x.has(r[r.length - 1]) || S.test(r[r.length - 1]));) r.pop();
112
- return [...r, ...n.map((e) => e.toLowerCase())].join(" ");
113
- }, E = ({ id: e, name: t }) => {
114
- let n = t.toLowerCase().split(/\s+/);
115
- return (e.startsWith("~") ? 8 : 0) + (n.includes("free") ? 4 : 0) + (n.includes("latest") ? 2 : 0) + +!!S.test(n[n.length - 1] ?? "") + +!!C.test(t);
116
- }, D = (e, t) => E(e) - E(t) || e.id.length - t.id.length || e.id.localeCompare(t.id);
117
- function O(e) {
118
- let t = {}, n = {};
119
- for (let [r, { name: i, models: a = {} }] of Object.entries(e)) {
120
- n[r] = i || r;
121
- for (let [e, n] of Object.entries(a)) {
122
- let i = n?.name?.trim() || e, a = T(i), o = t[a] ??= {
123
- key: a,
124
- name: i,
125
- providers: {}
126
- };
127
- !o.family && n?.family && (o.family = n.family);
128
- let s = n?.cost?.input, c = n?.cost?.output, l = typeof s == "number" && typeof c == "number" ? {
129
- input: s,
130
- output: c
131
- } : void 0;
132
- (o.providers[r] ??= []).push({
133
- id: e,
134
- name: i,
135
- ...l ? { cost: l } : {}
136
- }), E({
137
- id: e,
138
- name: i
139
- }) < E({
140
- id: "",
141
- name: o.name
142
- }) && (o.name = i);
143
- }
144
- }
145
- for (let e of Object.values(t)) for (let t of Object.values(e.providers)) t.sort(D);
146
- return {
147
- models: t,
148
- providers: n
149
- };
150
- }
151
- function k(e, t, n) {
152
- let r = e.models[T(t)];
153
- if (r) for (let e of n) {
154
- let t = r.providers[e]?.[0];
155
- if (t) return {
156
- provider: e,
157
- model: t.id,
158
- ref: `${e}/${t.id}`
159
- };
160
- }
161
- }
162
- function A(e, t, n) {
163
- let r = e.models[T(t)];
164
- return r ? (n ?? Object.keys(r.providers).sort()).flatMap((e) => {
165
- let t = r.providers[e]?.[0];
166
- return t ? [{
167
- provider: e,
168
- model: t.id,
169
- ref: `${e}/${t.id}`,
170
- ...t.cost ? { cost: t.cost } : {}
171
- }] : [];
172
- }) : [];
173
- }
174
- var j = null, M = null;
175
- function N(e = globalThis.fetch) {
176
- return M ? Promise.resolve(M) : (j ??= e(l).then((e) => e.ok ? e.json() : Promise.reject(/* @__PURE__ */ Error(`models.dev: HTTP ${e.status}`))).then((e) => M = O(e)).finally(() => {
177
- j = null;
178
- }), j);
179
- }
180
- //#endregion
181
- export { k as a, e as c, c as d, r as f, A as i, a as l, T as n, b as o, N as r, o as s, O as t, i as u };
182
-
183
- //# sourceMappingURL=modelIdentity-CGWliGDU.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"modelIdentity-CGWliGDU.mjs","names":["pending","SERVER_CREDENTIAL_PREFIX","SCOPE_PATTERN","PLACEHOLDER_PATTERN","serverCredential","scope","provider","findServerCredentials","text","placeholder","containsServerCredential","AI_PROVIDER_HOSTS","trimSlashes","path","isAIProviderTarget","connection","base","basePath","target","MODELS_DEV_URL","CACHE_KEY","CACHE_TTL_MS","MODELS_DEV_PROVIDER_KEYS","pending$1","readCache","raw","cached","writeCache","buildPricingIndex","wanted","index","key","models","catalog","prices","modelId","entry","input","output","loadPricingIndex","response","error","candidateIds","lower","model","undated","lookupModelsDevPricing","providerId","id","hit","withModelsDevPricing","m","pricing","VARIANT_WORDS","DATE_WORD","TRAILING_GROUP","MODE_GROUP","canonicalModelName","rest","name","modes","group","words","mode","instability","byStability","left","right","buildModelIdentityIndex","providers","providerName","entries","identity","cost","resolveModel","best","modelOffers","pending","loaded","loadModelIdentityIndex","fetchFn"],"sources":["../src/providers/ai/serverCredentials.ts","../src/providers/ai/modelsDevPricing.ts","../src/providers/ai/modelIdentity.ts"],"sourcesContent":["// Server-held AI credentials (CR-102). An app that cannot hold provider keys in the browser builds\n// its adapters with a placeholder where the key goes; its proxy swaps the placeholder for the real\n// key, only towards a host of that provider. Pure module: no React, no config — the browser\n// registry, proxyFetch and the Node entry (`@llmnative/react/server`) all share it.\n\nexport const SERVER_CREDENTIAL_PREFIX = 'llmnative-server-credential';\n\nconst SCOPE_PATTERN = /^[A-Za-z0-9_-]+$/;\nconst PLACEHOLDER_PATTERN = /llmnative-server-credential:([A-Za-z0-9_-]+):([a-z-]+)/g;\n\n/** A provider connected on the server: its id, and what its URLs are built from. Never the key. */\nexport type ServerAIConnection = {\n id: string;\n /** Cloudflare: the account in every Workers AI URL. */\n accountId?: string;\n /** openai-compatible: the endpoint. */\n baseUrl?: string;\n};\n\n/** The placeholder an adapter writes where the key goes: `llmnative-server-credential:<scope>:<provider>`. */\nexport const serverCredential = (scope: string, provider: string): string => {\n if (!SCOPE_PATTERN.test(scope)) throw new Error(`Server credential scope \"${scope}\" may only contain letters, digits, \"-\" and \"_\".`);\n return `${SERVER_CREDENTIAL_PREFIX}:${scope}:${provider}`;\n};\n\n/** Every placeholder in `text`, as scope and provider. */\nexport const findServerCredentials = (text: string): Array<{ placeholder: string; scope: string; provider: string }> =>\n [...text.matchAll(PLACEHOLDER_PATTERN)].map(([placeholder, scope, provider]) => ({ placeholder, scope, provider }));\n\nexport const containsServerCredential = (text: string): boolean => text.includes(`${SERVER_CREDENTIAL_PREFIX}:`);\n\n/** The fixed API host of each built-in provider (openai-compatible and Cloudflare depend on the connection). */\nexport const AI_PROVIDER_HOSTS: Readonly<Record<string, string>> = {\n openai: 'api.openai.com',\n openrouter: 'openrouter.ai',\n opencode: 'opencode.ai',\n 'opencode-go': 'opencode.ai',\n deepseek: 'api.deepseek.com',\n gemini: 'generativelanguage.googleapis.com',\n anthropic: 'api.anthropic.com',\n mistral: 'api.mistral.ai',\n glm: 'open.bigmodel.cn',\n groq: 'api.groq.com',\n cloudflare: 'api.cloudflare.com',\n};\n\nconst trimSlashes = (path: string) => path.replace(/\\/+$/, '');\n\n/** Whether `target` is an API URL of the provider of `connection`: where its key may go. */\nexport const isAIProviderTarget = (connection: ServerAIConnection, target: URL): boolean => {\n if (connection.id === 'openai-compatible') {\n if (!connection.baseUrl) return false;\n const base = new URL(connection.baseUrl);\n const basePath = trimSlashes(base.pathname);\n return target.origin === base.origin && (target.pathname === basePath || target.pathname.startsWith(`${basePath}/`));\n }\n if (target.protocol !== 'https:' || target.host !== AI_PROVIDER_HOSTS[connection.id]) return false;\n // Cloudflare: one host for every account, so the key only goes to the account it belongs to.\n if (connection.id === 'cloudflare') {\n return Boolean(connection.accountId) && target.pathname.startsWith(`/client/v4/accounts/${encodeURIComponent(connection.accountId!.trim())}/`);\n }\n return true;\n};\n","import type { AIModelDescriptor, AIModelPricing } from './AIProvider';\r\n\r\n/** Public, CORS-open catalog (`access-control-allow-origin: *`) of models with their token\r\n * prices, in USD per 1M tokens — the same unit AIModelPricing uses. */\r\nexport const MODELS_DEV_URL = 'https://models.dev/api.json';\r\n\r\nconst CACHE_KEY = 'ai.pricing.modelsdev';\r\nconst CACHE_TTL_MS = 1000 * 60 * 60 * 24;\r\n\r\n/** Built-in provider id → models.dev provider keys, in lookup order. `openai-compatible` has\r\n * no entry: its endpoint is arbitrary, so no catalog can know its prices. */\r\nconst MODELS_DEV_PROVIDER_KEYS: Record<string, string[]> = {\r\n openai: ['openai'],\r\n openrouter: ['openrouter'],\r\n opencode: ['opencode'],\r\n 'opencode-go': ['opencode-go'],\r\n deepseek: ['deepseek'],\r\n gemini: ['google'],\r\n anthropic: ['anthropic'],\r\n mistral: ['mistral'],\r\n // The GLM adapter targets open.bigmodel.cn (zhipuai); `zai` is the international twin.\r\n glm: ['zhipuai', 'zai'],\r\n cloudflare: ['cloudflare-workers-ai'],\r\n groq: ['groq'],\r\n};\r\n\r\n/** Compact cache shape: providerKey → modelId → [input, output]. The raw catalog is ~5 MB,\r\n * this keeps only the providers above (~700 entries). */\r\ntype PricingIndex = Record<string, Record<string, [number, number]>>;\r\n\r\ntype ModelsDevCatalog = Record<string, { models?: Record<string, { cost?: { input?: unknown; output?: unknown } }> }>;\r\n\r\nlet pending: Promise<PricingIndex> | null = null;\r\n\r\nconst readCache = (): PricingIndex | null => {\r\n if (typeof localStorage === 'undefined') return null;\r\n try {\r\n const raw = localStorage.getItem(CACHE_KEY);\r\n if (!raw) return null;\r\n const cached = JSON.parse(raw) as { fetchedAt?: number; index?: PricingIndex };\r\n if (!cached.fetchedAt || !cached.index || Date.now() - cached.fetchedAt > CACHE_TTL_MS) return null;\r\n return cached.index;\r\n } catch {\r\n return null;\r\n }\r\n};\r\n\r\nconst writeCache = (index: PricingIndex) => {\r\n if (typeof localStorage === 'undefined') return;\r\n try {\r\n localStorage.setItem(CACHE_KEY, JSON.stringify({ fetchedAt: Date.now(), index }));\r\n } catch {\r\n // Ignore quota/storage failures: pricing still works in-memory for this session.\r\n }\r\n};\r\n\r\nexport const buildPricingIndex = (catalog: ModelsDevCatalog): PricingIndex => {\r\n const wanted = new Set(Object.values(MODELS_DEV_PROVIDER_KEYS).flat());\r\n const index: PricingIndex = {};\r\n for (const key of wanted) {\r\n const models = catalog[key]?.models ?? {};\r\n const prices: Record<string, [number, number]> = {};\r\n for (const [modelId, entry] of Object.entries(models)) {\r\n const input = entry?.cost?.input;\r\n const output = entry?.cost?.output;\r\n if (typeof input === 'number' && typeof output === 'number') prices[modelId] = [input, output];\r\n }\r\n index[key] = prices;\r\n }\r\n return index;\r\n};\r\n\r\n/** One fetch per session at most (in-flight promise shared by every provider), then the\r\n * localStorage copy for 24h. A failed fetch resolves to an empty index — every model then\r\n * shows \"price not found\" — and is not cached, so the next catalog load retries. */\r\nconst loadPricingIndex = (): Promise<PricingIndex> => {\r\n const cached = readCache();\r\n if (cached) return Promise.resolve(cached);\r\n if (!pending) {\r\n pending = fetch(MODELS_DEV_URL)\r\n .then((response) => (response.ok ? response.json() : Promise.reject(new Error(`HTTP ${response.status}`))))\r\n .then((catalog: ModelsDevCatalog) => {\r\n const index = buildPricingIndex(catalog);\r\n writeCache(index);\r\n return index;\r\n })\r\n .catch((error) => {\r\n console.warn('models.dev pricing unavailable', error);\r\n return {};\r\n })\r\n .finally(() => { pending = null; });\r\n }\r\n return pending;\r\n};\r\n\r\n/** Provider ids often carry a release date models.dev lists only under the undated alias\r\n * (e.g. `gpt-4o-mini-2024-07-18`); exact match wins, the stripped id is the fallback. */\r\nconst candidateIds = (model: string): string[] => {\r\n const lower = model.toLowerCase();\r\n const undated = lower.replace(/-(\\d{8}|\\d{4}-\\d{2}-\\d{2})$/, '');\r\n return [...new Set([model, lower, undated])];\r\n};\r\n\r\nexport const lookupModelsDevPricing = (index: PricingIndex, providerId: string, model: string): AIModelPricing | undefined => {\r\n for (const key of MODELS_DEV_PROVIDER_KEYS[providerId] ?? []) {\r\n const prices = index[key];\r\n if (!prices) continue;\r\n for (const id of candidateIds(model)) {\r\n const hit = prices[id];\r\n if (hit) return { input: hit[0], output: hit[1], currency: 'USD', source: 'models.dev' };\r\n }\r\n }\r\n return undefined;\r\n};\r\n\r\n/** Fills `pricing` from models.dev on the models the provider listing left without one.\r\n * Models are never dropped: a model with no match simply keeps `pricing` undefined. */\r\nexport const withModelsDevPricing = async (providerId: string, models: AIModelDescriptor[]): Promise<AIModelDescriptor[]> => {\r\n if (!MODELS_DEV_PROVIDER_KEYS[providerId] || models.every((m) => m.pricing)) return models;\r\n const index = await loadPricingIndex();\r\n return models.map((m) => {\r\n if (m.pricing) return m;\r\n const pricing = lookupModelsDevPricing(index, providerId, m.model);\r\n return pricing ? { ...m, pricing } : m;\r\n });\r\n};\r\n\r\n/** Test-only: drops the in-flight promise between cases. */\r\nexport const resetModelsDevPricingForTests = () => { pending = null; };\r\n","// Provider-agnostic identity of AI models (CR-103). models.dev names a model the same way on every\r\n// provider once release dates and variants are removed: \"DeepSeek V4.1 Flash\" is\r\n// `deepseek/deepseek-v4.1-flash` on OpenRouter, `deepseek-flash` on DeepSeek and\r\n// `@cf/deepseek-ai/deepseek-v4.1-flash-1017` on Cloudflare. Pure module: no React, no browser API.\r\n\r\nimport { MODELS_DEV_URL } from './modelsDevPricing';\r\n\r\n/** Words that name a variant of a model, not another model. */\r\nconst VARIANT_WORDS = new Set(['free', 'latest']);\r\n/** A trailing release date: MMDD, YYYYMMDD or YYYY-MM-DD. */\r\nconst DATE_WORD = /^(\\d{4}|\\d{8}|\\d{4}-\\d{2}-\\d{2})$/;\r\n/** A trailing group in parentheses: who hosts it, a region or a variant (\"(Alibaba Cloud)\", \"(EU)\", \"(Latest)\"). */\r\nconst TRAILING_GROUP = /\\s*\\(([^()]*)\\)\\s*$/;\r\n/** Groups that name another way of running the model, not another host: they stay in the name. */\r\nconst MODE_GROUP = /^(thinking|reasoning|non-reasoning|fast)$/i;\r\n\r\n/**\r\n * The key of a model name on any provider: lower case, without the host, region, release date, Free\r\n * or Latest that some providers add; a mode (\"(Thinking)\") stays.\r\n */\r\nexport const canonicalModelName = (name: string): string => {\r\n let rest = name.trim();\r\n const modes: string[] = [];\r\n for (let group = TRAILING_GROUP.exec(rest); group; group = TRAILING_GROUP.exec(rest)) {\r\n if (MODE_GROUP.test(group[1].trim())) modes.unshift(group[1].trim());\r\n rest = rest.slice(0, group.index);\r\n }\r\n const words = rest.toLowerCase().replace(/[()[\\]]/g, ' ').split(/\\s+/).filter(Boolean);\r\n while (words.length > 1 && (VARIANT_WORDS.has(words[words.length - 1]) || DATE_WORD.test(words[words.length - 1]))) words.pop();\r\n return [...words, ...modes.map((mode) => mode.toLowerCase())].join(' ');\r\n};\r\n\r\n/** One model of a provider, as models.dev lists it, with its price there when models.dev has one (CR-119). */\r\nexport type ModelIdentityEntry = { id: string; name: string; cost?: ModelCost };\r\n\r\n/** What a model costs on a provider: USD per 1M tokens. */\r\nexport type ModelCost = { input: number; output: number };\r\n\r\nexport type ModelIdentity = {\r\n /** canonicalModelName() of its name. */\r\n key: string;\r\n /** The name to show: the one of a stable id, without date or variant when there is one. */\r\n name: string;\r\n family?: string;\r\n /** models.dev provider key (the ids opencode uses) → the ids of the model there. */\r\n providers: Record<string, ModelIdentityEntry[]>;\r\n};\r\n\r\nexport type ModelIdentityIndex = {\r\n models: Record<string, ModelIdentity>;\r\n /** models.dev provider key → its name. */\r\n providers: Record<string, string>;\r\n};\r\n\r\ntype ModelsDevCatalog = Record<string, { name?: string; models?: Record<string, { name?: string; family?: string; cost?: { input?: unknown; output?: unknown } }> }>;\r\n\r\n/** How far an id is from the stable one: `~` aliases, free tiers, Latest and dated ids come last. */\r\nconst instability = ({ id, name }: ModelIdentityEntry): number => {\r\n const words = name.toLowerCase().split(/\\s+/);\r\n return (id.startsWith('~') ? 8 : 0)\r\n + (words.includes('free') ? 4 : 0)\r\n + (words.includes('latest') ? 2 : 0)\r\n + (DATE_WORD.test(words[words.length - 1] ?? '') ? 1 : 0)\r\n // A host or region in parentheses: the plain name of the same provider goes first.\r\n + (TRAILING_GROUP.test(name) ? 1 : 0);\r\n};\r\n\r\nconst byStability = (left: ModelIdentityEntry, right: ModelIdentityEntry) =>\r\n instability(left) - instability(right) || left.id.length - right.id.length || left.id.localeCompare(right.id);\r\n\r\n/** One entry per canonical model, with its ids on every provider of the models.dev catalog. */\r\nexport function buildModelIdentityIndex(catalog: ModelsDevCatalog): ModelIdentityIndex {\r\n const models: Record<string, ModelIdentity> = {};\r\n const providers: Record<string, string> = {};\r\n for (const [provider, { name: providerName, models: entries = {} }] of Object.entries(catalog)) {\r\n providers[provider] = providerName || provider;\r\n for (const [id, entry] of Object.entries(entries)) {\r\n const name = entry?.name?.trim() || id;\r\n const key = canonicalModelName(name);\r\n const identity = models[key] ??= { key, name, providers: {} };\r\n if (!identity.family && entry?.family) identity.family = entry.family;\r\n const input = entry?.cost?.input;\r\n const output = entry?.cost?.output;\r\n const cost = typeof input === 'number' && typeof output === 'number' ? { input, output } : undefined;\r\n (identity.providers[provider] ??= []).push({ id, name, ...(cost ? { cost } : {}) });\r\n // The name to show is the one of the most stable id seen so far.\r\n if (instability({ id, name }) < instability({ id: '', name: identity.name })) identity.name = name;\r\n }\r\n }\r\n for (const identity of Object.values(models)) {\r\n for (const entries of Object.values(identity.providers)) entries.sort(byStability);\r\n }\r\n return { models, providers };\r\n}\r\n\r\nexport type ResolvedModel = {\r\n /** models.dev provider key. */\r\n provider: string;\r\n /** The id of the model on that provider. */\r\n model: string;\r\n /** `provider/model`: the form opencode reads. */\r\n ref: string;\r\n};\r\n\r\n/**\r\n * The id of `model` (a name or a canonical key) on the first of `providers` that serves it, the most\r\n * stable id of that provider; nothing when none of them does.\r\n */\r\nexport function resolveModel(index: ModelIdentityIndex, model: string, providers: readonly string[]): ResolvedModel | undefined {\r\n const identity = index.models[canonicalModelName(model)];\r\n if (!identity) return undefined;\r\n for (const provider of providers) {\r\n const best = identity.providers[provider]?.[0];\r\n if (best) return { provider, model: best.id, ref: `${provider}/${best.id}` };\r\n }\r\n return undefined;\r\n}\r\n\r\n/** A provider that serves a model: its most stable id there, and what it costs there when known. */\r\nexport type ModelOffer = ResolvedModel & { cost?: ModelCost };\r\n\r\n/**\r\n * Every provider that serves `model` (a name or a canonical key), each with its most stable id and its\r\n * price: of `providers`, in their order, when given; else of the whole catalog, by provider key (CR-119).\r\n */\r\nexport function modelOffers(index: ModelIdentityIndex, model: string, providers?: readonly string[]): ModelOffer[] {\r\n const identity = index.models[canonicalModelName(model)];\r\n if (!identity) return [];\r\n const keys = providers ?? Object.keys(identity.providers).sort();\r\n return keys.flatMap((provider) => {\r\n const best = identity.providers[provider]?.[0];\r\n return best ? [{ provider, model: best.id, ref: `${provider}/${best.id}`, ...(best.cost ? { cost: best.cost } : {}) }] : [];\r\n });\r\n}\r\n\r\nlet pending: Promise<ModelIdentityIndex> | null = null;\r\nlet loaded: ModelIdentityIndex | null = null;\r\n\r\n/** The models.dev index, fetched once per session (the catalog is several MB: not cached on disk). */\r\nexport function loadModelIdentityIndex(fetchFn: typeof fetch = globalThis.fetch): Promise<ModelIdentityIndex> {\r\n if (loaded) return Promise.resolve(loaded);\r\n pending ??= fetchFn(MODELS_DEV_URL)\r\n .then((response) => (response.ok ? response.json() : Promise.reject(new Error(`models.dev: HTTP ${response.status}`))))\r\n .then((catalog: ModelsDevCatalog) => (loaded = buildModelIdentityIndex(catalog)))\r\n .finally(() => { pending = null; });\r\n return pending;\r\n}\r\n"],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- var e=`llmnative-server-credential`,t=/^[A-Za-z0-9_-]+$/,n=/llmnative-server-credential:([A-Za-z0-9_-]+):([a-z-]+)/g,r=(n,r)=>{if(!t.test(n))throw Error(`Server credential scope "${n}" may only contain letters, digits, "-" and "_".`);return`${e}:${n}:${r}`},i=e=>[...e.matchAll(n)].map(([e,t,n])=>({placeholder:e,scope:t,provider:n})),a=t=>t.includes(`${e}:`),o={openai:`api.openai.com`,openrouter:`openrouter.ai`,opencode:`opencode.ai`,"opencode-go":`opencode.ai`,deepseek:`api.deepseek.com`,gemini:`generativelanguage.googleapis.com`,anthropic:`api.anthropic.com`,mistral:`api.mistral.ai`,glm:`open.bigmodel.cn`,groq:`api.groq.com`,cloudflare:`api.cloudflare.com`},s=e=>e.replace(/\/+$/,``),c=(e,t)=>{if(e.id===`openai-compatible`){if(!e.baseUrl)return!1;let n=new URL(e.baseUrl),r=s(n.pathname);return t.origin===n.origin&&(t.pathname===r||t.pathname.startsWith(`${r}/`))}return t.protocol!==`https:`||t.host!==o[e.id]?!1:e.id!==`cloudflare`||!!e.accountId&&t.pathname.startsWith(`/client/v4/accounts/${encodeURIComponent(e.accountId.trim())}/`)},l=`https://models.dev/api.json`,u=`ai.pricing.modelsdev`,d=864e5,f={openai:[`openai`],openrouter:[`openrouter`],opencode:[`opencode`],"opencode-go":[`opencode-go`],deepseek:[`deepseek`],gemini:[`google`],anthropic:[`anthropic`],mistral:[`mistral`],glm:[`zhipuai`,`zai`],cloudflare:[`cloudflare-workers-ai`],groq:[`groq`]},p=null,m=()=>{if(typeof localStorage>`u`)return null;try{let e=localStorage.getItem(u);if(!e)return null;let t=JSON.parse(e);return!t.fetchedAt||!t.index||Date.now()-t.fetchedAt>d?null:t.index}catch{return null}},h=e=>{if(typeof localStorage<`u`)try{localStorage.setItem(u,JSON.stringify({fetchedAt:Date.now(),index:e}))}catch{}},g=e=>{let t=new Set(Object.values(f).flat()),n={};for(let r of t){let t=e[r]?.models??{},i={};for(let[e,n]of Object.entries(t)){let t=n?.cost?.input,r=n?.cost?.output;typeof t==`number`&&typeof r==`number`&&(i[e]=[t,r])}n[r]=i}return n},_=()=>{let e=m();return e?Promise.resolve(e):(p||=fetch(l).then(e=>e.ok?e.json():Promise.reject(Error(`HTTP ${e.status}`))).then(e=>{let t=g(e);return h(t),t}).catch(e=>(console.warn(`models.dev pricing unavailable`,e),{})).finally(()=>{p=null}),p)},v=e=>{let t=e.toLowerCase(),n=t.replace(/-(\d{8}|\d{4}-\d{2}-\d{2})$/,``);return[...new Set([e,t,n])]},y=(e,t,n)=>{for(let r of f[t]??[]){let t=e[r];if(t)for(let e of v(n)){let n=t[e];if(n)return{input:n[0],output:n[1],currency:`USD`,source:`models.dev`}}}},b=async(e,t)=>{if(!f[e]||t.every(e=>e.pricing))return t;let n=await _();return t.map(t=>{if(t.pricing)return t;let r=y(n,e,t.model);return r?{...t,pricing:r}:t})},x=new Set([`free`,`latest`]),S=/^(\d{4}|\d{8}|\d{4}-\d{2}-\d{2})$/,C=/\s*\(([^()]*)\)\s*$/,w=/^(thinking|reasoning|non-reasoning|fast)$/i,T=e=>{let t=e.trim(),n=[];for(let e=C.exec(t);e;e=C.exec(t))w.test(e[1].trim())&&n.unshift(e[1].trim()),t=t.slice(0,e.index);let r=t.toLowerCase().replace(/[()[\]]/g,` `).split(/\s+/).filter(Boolean);for(;r.length>1&&(x.has(r[r.length-1])||S.test(r[r.length-1]));)r.pop();return[...r,...n.map(e=>e.toLowerCase())].join(` `)},E=({id:e,name:t})=>{let n=t.toLowerCase().split(/\s+/);return(e.startsWith(`~`)?8:0)+(n.includes(`free`)?4:0)+(n.includes(`latest`)?2:0)+ +!!S.test(n[n.length-1]??``)+ +!!C.test(t)},D=(e,t)=>E(e)-E(t)||e.id.length-t.id.length||e.id.localeCompare(t.id);function O(e){let t={},n={};for(let[r,{name:i,models:a={}}]of Object.entries(e)){n[r]=i||r;for(let[e,n]of Object.entries(a)){let i=n?.name?.trim()||e,a=T(i),o=t[a]??={key:a,name:i,providers:{}};!o.family&&n?.family&&(o.family=n.family);let s=n?.cost?.input,c=n?.cost?.output,l=typeof s==`number`&&typeof c==`number`?{input:s,output:c}:void 0;(o.providers[r]??=[]).push({id:e,name:i,...l?{cost:l}:{}}),E({id:e,name:i})<E({id:``,name:o.name})&&(o.name=i)}}for(let e of Object.values(t))for(let t of Object.values(e.providers))t.sort(D);return{models:t,providers:n}}function k(e,t,n){let r=e.models[T(t)];if(r)for(let e of n){let t=r.providers[e]?.[0];if(t)return{provider:e,model:t.id,ref:`${e}/${t.id}`}}}function A(e,t,n){let r=e.models[T(t)];return r?(n??Object.keys(r.providers).sort()).flatMap(e=>{let t=r.providers[e]?.[0];return t?[{provider:e,model:t.id,ref:`${e}/${t.id}`,...t.cost?{cost:t.cost}:{}}]:[]}):[]}var j=null,M=null;function N(e=globalThis.fetch){return M?Promise.resolve(M):(j??=e(l).then(e=>e.ok?e.json():Promise.reject(Error(`models.dev: HTTP ${e.status}`))).then(e=>M=O(e)).finally(()=>{j=null}),j)}Object.defineProperty(exports,"a",{enumerable:!0,get:function(){return k}}),Object.defineProperty(exports,"c",{enumerable:!0,get:function(){return e}}),Object.defineProperty(exports,"d",{enumerable:!0,get:function(){return c}}),Object.defineProperty(exports,"f",{enumerable:!0,get:function(){return r}}),Object.defineProperty(exports,"i",{enumerable:!0,get:function(){return A}}),Object.defineProperty(exports,"l",{enumerable:!0,get:function(){return a}}),Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return T}}),Object.defineProperty(exports,"o",{enumerable:!0,get:function(){return b}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return N}}),Object.defineProperty(exports,"s",{enumerable:!0,get:function(){return o}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return O}}),Object.defineProperty(exports,"u",{enumerable:!0,get:function(){return i}});
2
- //# sourceMappingURL=modelIdentity-CgRMrC5H.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"modelIdentity-CgRMrC5H.js","names":["pending","SERVER_CREDENTIAL_PREFIX","SCOPE_PATTERN","PLACEHOLDER_PATTERN","serverCredential","scope","provider","findServerCredentials","text","placeholder","containsServerCredential","AI_PROVIDER_HOSTS","trimSlashes","path","isAIProviderTarget","connection","base","basePath","target","MODELS_DEV_URL","CACHE_KEY","CACHE_TTL_MS","MODELS_DEV_PROVIDER_KEYS","pending$1","readCache","raw","cached","writeCache","buildPricingIndex","wanted","index","key","models","catalog","prices","modelId","entry","input","output","loadPricingIndex","response","error","candidateIds","lower","model","undated","lookupModelsDevPricing","providerId","id","hit","withModelsDevPricing","m","pricing","VARIANT_WORDS","DATE_WORD","TRAILING_GROUP","MODE_GROUP","canonicalModelName","rest","name","modes","group","words","mode","instability","byStability","left","right","buildModelIdentityIndex","providers","providerName","entries","identity","cost","resolveModel","best","modelOffers","pending","loaded","loadModelIdentityIndex","fetchFn"],"sources":["../src/providers/ai/serverCredentials.ts","../src/providers/ai/modelsDevPricing.ts","../src/providers/ai/modelIdentity.ts"],"sourcesContent":["// Server-held AI credentials (CR-102). An app that cannot hold provider keys in the browser builds\n// its adapters with a placeholder where the key goes; its proxy swaps the placeholder for the real\n// key, only towards a host of that provider. Pure module: no React, no config — the browser\n// registry, proxyFetch and the Node entry (`@llmnative/react/server`) all share it.\n\nexport const SERVER_CREDENTIAL_PREFIX = 'llmnative-server-credential';\n\nconst SCOPE_PATTERN = /^[A-Za-z0-9_-]+$/;\nconst PLACEHOLDER_PATTERN = /llmnative-server-credential:([A-Za-z0-9_-]+):([a-z-]+)/g;\n\n/** A provider connected on the server: its id, and what its URLs are built from. Never the key. */\nexport type ServerAIConnection = {\n id: string;\n /** Cloudflare: the account in every Workers AI URL. */\n accountId?: string;\n /** openai-compatible: the endpoint. */\n baseUrl?: string;\n};\n\n/** The placeholder an adapter writes where the key goes: `llmnative-server-credential:<scope>:<provider>`. */\nexport const serverCredential = (scope: string, provider: string): string => {\n if (!SCOPE_PATTERN.test(scope)) throw new Error(`Server credential scope \"${scope}\" may only contain letters, digits, \"-\" and \"_\".`);\n return `${SERVER_CREDENTIAL_PREFIX}:${scope}:${provider}`;\n};\n\n/** Every placeholder in `text`, as scope and provider. */\nexport const findServerCredentials = (text: string): Array<{ placeholder: string; scope: string; provider: string }> =>\n [...text.matchAll(PLACEHOLDER_PATTERN)].map(([placeholder, scope, provider]) => ({ placeholder, scope, provider }));\n\nexport const containsServerCredential = (text: string): boolean => text.includes(`${SERVER_CREDENTIAL_PREFIX}:`);\n\n/** The fixed API host of each built-in provider (openai-compatible and Cloudflare depend on the connection). */\nexport const AI_PROVIDER_HOSTS: Readonly<Record<string, string>> = {\n openai: 'api.openai.com',\n openrouter: 'openrouter.ai',\n opencode: 'opencode.ai',\n 'opencode-go': 'opencode.ai',\n deepseek: 'api.deepseek.com',\n gemini: 'generativelanguage.googleapis.com',\n anthropic: 'api.anthropic.com',\n mistral: 'api.mistral.ai',\n glm: 'open.bigmodel.cn',\n groq: 'api.groq.com',\n cloudflare: 'api.cloudflare.com',\n};\n\nconst trimSlashes = (path: string) => path.replace(/\\/+$/, '');\n\n/** Whether `target` is an API URL of the provider of `connection`: where its key may go. */\nexport const isAIProviderTarget = (connection: ServerAIConnection, target: URL): boolean => {\n if (connection.id === 'openai-compatible') {\n if (!connection.baseUrl) return false;\n const base = new URL(connection.baseUrl);\n const basePath = trimSlashes(base.pathname);\n return target.origin === base.origin && (target.pathname === basePath || target.pathname.startsWith(`${basePath}/`));\n }\n if (target.protocol !== 'https:' || target.host !== AI_PROVIDER_HOSTS[connection.id]) return false;\n // Cloudflare: one host for every account, so the key only goes to the account it belongs to.\n if (connection.id === 'cloudflare') {\n return Boolean(connection.accountId) && target.pathname.startsWith(`/client/v4/accounts/${encodeURIComponent(connection.accountId!.trim())}/`);\n }\n return true;\n};\n","import type { AIModelDescriptor, AIModelPricing } from './AIProvider';\r\n\r\n/** Public, CORS-open catalog (`access-control-allow-origin: *`) of models with their token\r\n * prices, in USD per 1M tokens — the same unit AIModelPricing uses. */\r\nexport const MODELS_DEV_URL = 'https://models.dev/api.json';\r\n\r\nconst CACHE_KEY = 'ai.pricing.modelsdev';\r\nconst CACHE_TTL_MS = 1000 * 60 * 60 * 24;\r\n\r\n/** Built-in provider id → models.dev provider keys, in lookup order. `openai-compatible` has\r\n * no entry: its endpoint is arbitrary, so no catalog can know its prices. */\r\nconst MODELS_DEV_PROVIDER_KEYS: Record<string, string[]> = {\r\n openai: ['openai'],\r\n openrouter: ['openrouter'],\r\n opencode: ['opencode'],\r\n 'opencode-go': ['opencode-go'],\r\n deepseek: ['deepseek'],\r\n gemini: ['google'],\r\n anthropic: ['anthropic'],\r\n mistral: ['mistral'],\r\n // The GLM adapter targets open.bigmodel.cn (zhipuai); `zai` is the international twin.\r\n glm: ['zhipuai', 'zai'],\r\n cloudflare: ['cloudflare-workers-ai'],\r\n groq: ['groq'],\r\n};\r\n\r\n/** Compact cache shape: providerKey → modelId → [input, output]. The raw catalog is ~5 MB,\r\n * this keeps only the providers above (~700 entries). */\r\ntype PricingIndex = Record<string, Record<string, [number, number]>>;\r\n\r\ntype ModelsDevCatalog = Record<string, { models?: Record<string, { cost?: { input?: unknown; output?: unknown } }> }>;\r\n\r\nlet pending: Promise<PricingIndex> | null = null;\r\n\r\nconst readCache = (): PricingIndex | null => {\r\n if (typeof localStorage === 'undefined') return null;\r\n try {\r\n const raw = localStorage.getItem(CACHE_KEY);\r\n if (!raw) return null;\r\n const cached = JSON.parse(raw) as { fetchedAt?: number; index?: PricingIndex };\r\n if (!cached.fetchedAt || !cached.index || Date.now() - cached.fetchedAt > CACHE_TTL_MS) return null;\r\n return cached.index;\r\n } catch {\r\n return null;\r\n }\r\n};\r\n\r\nconst writeCache = (index: PricingIndex) => {\r\n if (typeof localStorage === 'undefined') return;\r\n try {\r\n localStorage.setItem(CACHE_KEY, JSON.stringify({ fetchedAt: Date.now(), index }));\r\n } catch {\r\n // Ignore quota/storage failures: pricing still works in-memory for this session.\r\n }\r\n};\r\n\r\nexport const buildPricingIndex = (catalog: ModelsDevCatalog): PricingIndex => {\r\n const wanted = new Set(Object.values(MODELS_DEV_PROVIDER_KEYS).flat());\r\n const index: PricingIndex = {};\r\n for (const key of wanted) {\r\n const models = catalog[key]?.models ?? {};\r\n const prices: Record<string, [number, number]> = {};\r\n for (const [modelId, entry] of Object.entries(models)) {\r\n const input = entry?.cost?.input;\r\n const output = entry?.cost?.output;\r\n if (typeof input === 'number' && typeof output === 'number') prices[modelId] = [input, output];\r\n }\r\n index[key] = prices;\r\n }\r\n return index;\r\n};\r\n\r\n/** One fetch per session at most (in-flight promise shared by every provider), then the\r\n * localStorage copy for 24h. A failed fetch resolves to an empty index — every model then\r\n * shows \"price not found\" — and is not cached, so the next catalog load retries. */\r\nconst loadPricingIndex = (): Promise<PricingIndex> => {\r\n const cached = readCache();\r\n if (cached) return Promise.resolve(cached);\r\n if (!pending) {\r\n pending = fetch(MODELS_DEV_URL)\r\n .then((response) => (response.ok ? response.json() : Promise.reject(new Error(`HTTP ${response.status}`))))\r\n .then((catalog: ModelsDevCatalog) => {\r\n const index = buildPricingIndex(catalog);\r\n writeCache(index);\r\n return index;\r\n })\r\n .catch((error) => {\r\n console.warn('models.dev pricing unavailable', error);\r\n return {};\r\n })\r\n .finally(() => { pending = null; });\r\n }\r\n return pending;\r\n};\r\n\r\n/** Provider ids often carry a release date models.dev lists only under the undated alias\r\n * (e.g. `gpt-4o-mini-2024-07-18`); exact match wins, the stripped id is the fallback. */\r\nconst candidateIds = (model: string): string[] => {\r\n const lower = model.toLowerCase();\r\n const undated = lower.replace(/-(\\d{8}|\\d{4}-\\d{2}-\\d{2})$/, '');\r\n return [...new Set([model, lower, undated])];\r\n};\r\n\r\nexport const lookupModelsDevPricing = (index: PricingIndex, providerId: string, model: string): AIModelPricing | undefined => {\r\n for (const key of MODELS_DEV_PROVIDER_KEYS[providerId] ?? []) {\r\n const prices = index[key];\r\n if (!prices) continue;\r\n for (const id of candidateIds(model)) {\r\n const hit = prices[id];\r\n if (hit) return { input: hit[0], output: hit[1], currency: 'USD', source: 'models.dev' };\r\n }\r\n }\r\n return undefined;\r\n};\r\n\r\n/** Fills `pricing` from models.dev on the models the provider listing left without one.\r\n * Models are never dropped: a model with no match simply keeps `pricing` undefined. */\r\nexport const withModelsDevPricing = async (providerId: string, models: AIModelDescriptor[]): Promise<AIModelDescriptor[]> => {\r\n if (!MODELS_DEV_PROVIDER_KEYS[providerId] || models.every((m) => m.pricing)) return models;\r\n const index = await loadPricingIndex();\r\n return models.map((m) => {\r\n if (m.pricing) return m;\r\n const pricing = lookupModelsDevPricing(index, providerId, m.model);\r\n return pricing ? { ...m, pricing } : m;\r\n });\r\n};\r\n\r\n/** Test-only: drops the in-flight promise between cases. */\r\nexport const resetModelsDevPricingForTests = () => { pending = null; };\r\n","// Provider-agnostic identity of AI models (CR-103). models.dev names a model the same way on every\r\n// provider once release dates and variants are removed: \"DeepSeek V4.1 Flash\" is\r\n// `deepseek/deepseek-v4.1-flash` on OpenRouter, `deepseek-flash` on DeepSeek and\r\n// `@cf/deepseek-ai/deepseek-v4.1-flash-1017` on Cloudflare. Pure module: no React, no browser API.\r\n\r\nimport { MODELS_DEV_URL } from './modelsDevPricing';\r\n\r\n/** Words that name a variant of a model, not another model. */\r\nconst VARIANT_WORDS = new Set(['free', 'latest']);\r\n/** A trailing release date: MMDD, YYYYMMDD or YYYY-MM-DD. */\r\nconst DATE_WORD = /^(\\d{4}|\\d{8}|\\d{4}-\\d{2}-\\d{2})$/;\r\n/** A trailing group in parentheses: who hosts it, a region or a variant (\"(Alibaba Cloud)\", \"(EU)\", \"(Latest)\"). */\r\nconst TRAILING_GROUP = /\\s*\\(([^()]*)\\)\\s*$/;\r\n/** Groups that name another way of running the model, not another host: they stay in the name. */\r\nconst MODE_GROUP = /^(thinking|reasoning|non-reasoning|fast)$/i;\r\n\r\n/**\r\n * The key of a model name on any provider: lower case, without the host, region, release date, Free\r\n * or Latest that some providers add; a mode (\"(Thinking)\") stays.\r\n */\r\nexport const canonicalModelName = (name: string): string => {\r\n let rest = name.trim();\r\n const modes: string[] = [];\r\n for (let group = TRAILING_GROUP.exec(rest); group; group = TRAILING_GROUP.exec(rest)) {\r\n if (MODE_GROUP.test(group[1].trim())) modes.unshift(group[1].trim());\r\n rest = rest.slice(0, group.index);\r\n }\r\n const words = rest.toLowerCase().replace(/[()[\\]]/g, ' ').split(/\\s+/).filter(Boolean);\r\n while (words.length > 1 && (VARIANT_WORDS.has(words[words.length - 1]) || DATE_WORD.test(words[words.length - 1]))) words.pop();\r\n return [...words, ...modes.map((mode) => mode.toLowerCase())].join(' ');\r\n};\r\n\r\n/** One model of a provider, as models.dev lists it, with its price there when models.dev has one (CR-119). */\r\nexport type ModelIdentityEntry = { id: string; name: string; cost?: ModelCost };\r\n\r\n/** What a model costs on a provider: USD per 1M tokens. */\r\nexport type ModelCost = { input: number; output: number };\r\n\r\nexport type ModelIdentity = {\r\n /** canonicalModelName() of its name. */\r\n key: string;\r\n /** The name to show: the one of a stable id, without date or variant when there is one. */\r\n name: string;\r\n family?: string;\r\n /** models.dev provider key (the ids opencode uses) → the ids of the model there. */\r\n providers: Record<string, ModelIdentityEntry[]>;\r\n};\r\n\r\nexport type ModelIdentityIndex = {\r\n models: Record<string, ModelIdentity>;\r\n /** models.dev provider key → its name. */\r\n providers: Record<string, string>;\r\n};\r\n\r\ntype ModelsDevCatalog = Record<string, { name?: string; models?: Record<string, { name?: string; family?: string; cost?: { input?: unknown; output?: unknown } }> }>;\r\n\r\n/** How far an id is from the stable one: `~` aliases, free tiers, Latest and dated ids come last. */\r\nconst instability = ({ id, name }: ModelIdentityEntry): number => {\r\n const words = name.toLowerCase().split(/\\s+/);\r\n return (id.startsWith('~') ? 8 : 0)\r\n + (words.includes('free') ? 4 : 0)\r\n + (words.includes('latest') ? 2 : 0)\r\n + (DATE_WORD.test(words[words.length - 1] ?? '') ? 1 : 0)\r\n // A host or region in parentheses: the plain name of the same provider goes first.\r\n + (TRAILING_GROUP.test(name) ? 1 : 0);\r\n};\r\n\r\nconst byStability = (left: ModelIdentityEntry, right: ModelIdentityEntry) =>\r\n instability(left) - instability(right) || left.id.length - right.id.length || left.id.localeCompare(right.id);\r\n\r\n/** One entry per canonical model, with its ids on every provider of the models.dev catalog. */\r\nexport function buildModelIdentityIndex(catalog: ModelsDevCatalog): ModelIdentityIndex {\r\n const models: Record<string, ModelIdentity> = {};\r\n const providers: Record<string, string> = {};\r\n for (const [provider, { name: providerName, models: entries = {} }] of Object.entries(catalog)) {\r\n providers[provider] = providerName || provider;\r\n for (const [id, entry] of Object.entries(entries)) {\r\n const name = entry?.name?.trim() || id;\r\n const key = canonicalModelName(name);\r\n const identity = models[key] ??= { key, name, providers: {} };\r\n if (!identity.family && entry?.family) identity.family = entry.family;\r\n const input = entry?.cost?.input;\r\n const output = entry?.cost?.output;\r\n const cost = typeof input === 'number' && typeof output === 'number' ? { input, output } : undefined;\r\n (identity.providers[provider] ??= []).push({ id, name, ...(cost ? { cost } : {}) });\r\n // The name to show is the one of the most stable id seen so far.\r\n if (instability({ id, name }) < instability({ id: '', name: identity.name })) identity.name = name;\r\n }\r\n }\r\n for (const identity of Object.values(models)) {\r\n for (const entries of Object.values(identity.providers)) entries.sort(byStability);\r\n }\r\n return { models, providers };\r\n}\r\n\r\nexport type ResolvedModel = {\r\n /** models.dev provider key. */\r\n provider: string;\r\n /** The id of the model on that provider. */\r\n model: string;\r\n /** `provider/model`: the form opencode reads. */\r\n ref: string;\r\n};\r\n\r\n/**\r\n * The id of `model` (a name or a canonical key) on the first of `providers` that serves it, the most\r\n * stable id of that provider; nothing when none of them does.\r\n */\r\nexport function resolveModel(index: ModelIdentityIndex, model: string, providers: readonly string[]): ResolvedModel | undefined {\r\n const identity = index.models[canonicalModelName(model)];\r\n if (!identity) return undefined;\r\n for (const provider of providers) {\r\n const best = identity.providers[provider]?.[0];\r\n if (best) return { provider, model: best.id, ref: `${provider}/${best.id}` };\r\n }\r\n return undefined;\r\n}\r\n\r\n/** A provider that serves a model: its most stable id there, and what it costs there when known. */\r\nexport type ModelOffer = ResolvedModel & { cost?: ModelCost };\r\n\r\n/**\r\n * Every provider that serves `model` (a name or a canonical key), each with its most stable id and its\r\n * price: of `providers`, in their order, when given; else of the whole catalog, by provider key (CR-119).\r\n */\r\nexport function modelOffers(index: ModelIdentityIndex, model: string, providers?: readonly string[]): ModelOffer[] {\r\n const identity = index.models[canonicalModelName(model)];\r\n if (!identity) return [];\r\n const keys = providers ?? Object.keys(identity.providers).sort();\r\n return keys.flatMap((provider) => {\r\n const best = identity.providers[provider]?.[0];\r\n return best ? [{ provider, model: best.id, ref: `${provider}/${best.id}`, ...(best.cost ? { cost: best.cost } : {}) }] : [];\r\n });\r\n}\r\n\r\nlet pending: Promise<ModelIdentityIndex> | null = null;\r\nlet loaded: ModelIdentityIndex | null = null;\r\n\r\n/** The models.dev index, fetched once per session (the catalog is several MB: not cached on disk). */\r\nexport function loadModelIdentityIndex(fetchFn: typeof fetch = globalThis.fetch): Promise<ModelIdentityIndex> {\r\n if (loaded) return Promise.resolve(loaded);\r\n pending ??= fetchFn(MODELS_DEV_URL)\r\n .then((response) => (response.ok ? response.json() : Promise.reject(new Error(`models.dev: HTTP ${response.status}`))))\r\n .then((catalog: ModelsDevCatalog) => (loaded = buildModelIdentityIndex(catalog)))\r\n .finally(() => { pending = null; });\r\n return pending;\r\n}\r\n"],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- const e=require("./modelIdentity-CgRMrC5H.js");var t=class extends Error{status;constructor(e,t){super(e),this.status=t}},n=`https://github.com`,r=`https://api.github.com`;function i(e,i=globalThis.fetch){let a=async(e,n)=>{let a=await i(`${r}${e}`,{headers:{Accept:`application/vnd.github+json`,Authorization:`Bearer ${n}`,"X-GitHub-Api-Version":`2022-11-28`}});if(!a.ok)throw new t(`GitHub ${e}: HTTP ${a.status}`,a.status);return a.json()};return{authorizeUrl({state:t,redirectUri:r,scopes:i=e.scopes,codeChallenge:a}){let o=new URLSearchParams({client_id:e.clientId,redirect_uri:r,state:t});return i?.length&&o.set(`scope`,i.join(` `)),a&&(o.set(`code_challenge`,a),o.set(`code_challenge_method`,`S256`)),`${n}/login/oauth/authorize?${o.toString()}`},async exchangeCode({code:r,redirectUri:a,codeVerifier:o}){let s=await i(`${n}/login/oauth/access_token`,{method:`POST`,headers:{Accept:`application/json`,"Content-Type":`application/json`},body:JSON.stringify({client_id:e.clientId,client_secret:e.clientSecret,code:r,redirect_uri:a,...o?{code_verifier:o}:{}})}),c=await s.json().catch(()=>({}));if(!s.ok||typeof c.access_token!=`string`)throw new t(String(c.error_description??c.error??`HTTP ${s.status}`),s.status);return{accessToken:c.access_token,scope:String(c.scope??``)}},async getUser(e){let t=await a(`/user`,e);return{login:t.login,name:t.name??null,avatarUrl:t.avatar_url??null,email:t.email??null}},async getOrganizations(e){return(await a(`/user/memberships/orgs?state=active&per_page=100`,e)).map(({role:e,organization:t})=>({login:t.login,avatarUrl:t.avatar_url??null,role:e===`admin`?`admin`:`member`}))}}}async function a(t,n){let r=[t.url,...Object.values(t.headers)].flatMap(e.u);if(r.length===0)return{ok:!0,url:t.url,headers:t.headers};let i=new URL(t.url),{url:a}=t,o={...t.headers},s=new Set;for(let{placeholder:t,scope:c,provider:l}of r){if(s.has(t))continue;s.add(t);let r=await n(c,l);if(!r?.apiKey)return{ok:!1,status:404,error:`unknown-credential`,provider:l};if(!e.d({...r,id:l},i))return{ok:!1,status:403,error:`forbidden-target`,provider:l};a=a.split(t).join(encodeURIComponent(r.apiKey)),o=Object.fromEntries(Object.entries(o).map(([e,n])=>[e,n.split(t).join(r.apiKey)]))}return{ok:!0,url:a,headers:o}}Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return t}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return i}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return a}});
2
- //# sourceMappingURL=server-CCbjPJEz.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"server-CCbjPJEz.js","names":["findServerCredentials","isAIProviderTarget","require_modelIdentity","GitHubOAuthError","message","status","GITHUB","API","createGitHubOAuth","api","response","fetchFn","path","token","config","params","redirectUri","scopes","codeChallenge","codeVerifier","body","user","organization","role","resolveServerCredentials","found","request","target","headers","done","placeholder","secret","lookup","scope","provider","url","name","value"],"sources":["../src/providers/auth/github/githubOAuth.ts","../src/providers/ai/server.ts"],"sourcesContent":["// The GitHub side of the sign-in of an app whose server holds the session (CR-104): the authorize\n// URL, the code exchange (it needs the client secret: server only), the profile and the\n// organizations of the person. Pure: no React, no browser API; `fetchFn` is replaceable in tests.\n\nexport type GitHubOAuthConfig = {\n clientId: string;\n clientSecret: string;\n /** Default scopes, e.g. `['read:org']` to list the organizations of the person. */\n scopes?: string[];\n};\n\nexport type GitHubOAuthUser = { login: string; name: string | null; avatarUrl: string | null; email: string | null };\n\nexport type GitHubOAuthOrganization = { login: string; avatarUrl: string | null; role: 'admin' | 'member' };\n\nexport class GitHubOAuthError extends Error {\n constructor(message: string, readonly status?: number) {\n super(message);\n }\n}\n\nconst GITHUB = 'https://github.com';\nconst API = 'https://api.github.com';\n\nexport function createGitHubOAuth(config: GitHubOAuthConfig, fetchFn: typeof fetch = globalThis.fetch) {\n const api = async <T>(path: string, token: string): Promise<T> => {\n const response = await fetchFn(`${API}${path}`, {\n headers: { Accept: 'application/vnd.github+json', Authorization: `Bearer ${token}`, 'X-GitHub-Api-Version': '2022-11-28' },\n });\n if (!response.ok) throw new GitHubOAuthError(`GitHub ${path}: HTTP ${response.status}`, response.status);\n return response.json() as Promise<T>;\n };\n\n return {\n /** Where the person authorizes the app. `state` ties the callback to this request; `codeChallenge` (S256) adds PKCE. */\n authorizeUrl({ state, redirectUri, scopes = config.scopes, codeChallenge }: { state: string; redirectUri: string; scopes?: string[]; codeChallenge?: string }): string {\n const params = new URLSearchParams({ client_id: config.clientId, redirect_uri: redirectUri, state });\n if (scopes?.length) params.set('scope', scopes.join(' '));\n if (codeChallenge) {\n params.set('code_challenge', codeChallenge);\n params.set('code_challenge_method', 'S256');\n }\n return `${GITHUB}/login/oauth/authorize?${params.toString()}`;\n },\n\n /** The access token for the code GitHub sent back. */\n async exchangeCode({ code, redirectUri, codeVerifier }: { code: string; redirectUri: string; codeVerifier?: string }): Promise<{ accessToken: string; scope: string }> {\n const response = await fetchFn(`${GITHUB}/login/oauth/access_token`, {\n method: 'POST',\n headers: { Accept: 'application/json', 'Content-Type': 'application/json' },\n body: JSON.stringify({\n client_id: config.clientId,\n client_secret: config.clientSecret,\n code,\n redirect_uri: redirectUri,\n ...(codeVerifier ? { code_verifier: codeVerifier } : {}),\n }),\n });\n const body = await response.json().catch(() => ({})) as Record<string, unknown>;\n if (!response.ok || typeof body.access_token !== 'string') {\n throw new GitHubOAuthError(String(body.error_description ?? body.error ?? `HTTP ${response.status}`), response.status);\n }\n return { accessToken: body.access_token, scope: String(body.scope ?? '') };\n },\n\n async getUser(token: string): Promise<GitHubOAuthUser> {\n const user = await api<{ login: string; name?: string | null; avatar_url?: string | null; email?: string | null }>('/user', token);\n return { login: user.login, name: user.name ?? null, avatarUrl: user.avatar_url ?? null, email: user.email ?? null };\n },\n\n /** The organizations the person belongs to, with their role (needs `read:org`). */\n async getOrganizations(token: string): Promise<GitHubOAuthOrganization[]> {\n const memberships = await api<Array<{ role: string; organization: { login: string; avatar_url?: string | null } }>>('/user/memberships/orgs?state=active&per_page=100', token);\n return memberships.map(({ role, organization }) => ({\n login: organization.login,\n avatarUrl: organization.avatar_url ?? null,\n role: role === 'admin' ? 'admin' : 'member',\n }));\n },\n };\n}\n\nexport type GitHubOAuth = ReturnType<typeof createGitHubOAuth>;\n","// `@llmnative/react/server`: what a Node proxy needs for server-held AI credentials (CR-102).\r\n// No React and no browser API: it runs in the app's own server or in the Vite dev proxy.\r\n\r\nimport { findServerCredentials, isAIProviderTarget, type ServerAIConnection } from './serverCredentials';\r\n\r\nexport { AI_PROVIDER_HOSTS, isAIProviderTarget, serverCredential, SERVER_CREDENTIAL_PREFIX } from './serverCredentials';\r\nexport type { ServerAIConnection } from './serverCredentials';\r\n// Sign-in with GitHub held by the app's server (CR-104): authorize URL, code exchange, profile, organizations.\r\nexport { createGitHubOAuth, GitHubOAuthError } from '../auth/github/githubOAuth';\r\nexport type { GitHubOAuth, GitHubOAuthConfig, GitHubOAuthOrganization, GitHubOAuthUser } from '../auth/github/githubOAuth';\r\n// Provider-agnostic identity of models (CR-103): pure, so a server or a generator can resolve them too.\r\nexport { canonicalModelName, buildModelIdentityIndex, resolveModel, modelOffers, loadModelIdentityIndex } from './modelIdentity';\r\nexport type { ModelIdentity, ModelIdentityEntry, ModelIdentityIndex, ResolvedModel, ModelOffer, ModelCost } from './modelIdentity';\r\n\r\n/** The connection of a provider as the server keeps it, key included. */\r\nexport type ServerAISecret = ServerAIConnection & { apiKey: string };\r\n\r\n/** The key of `provider` for `scope` (e.g. a company), or nothing when it is not connected. */\r\nexport type ServerCredentialLookup = (scope: string, provider: string) => ServerAISecret | undefined | Promise<ServerAISecret | undefined>;\r\n\r\nexport type ServerCredentialRequest = { url: string; headers: Record<string, string> };\r\n\r\nexport type ServerCredentialResolution =\r\n | { ok: true; url: string; headers: Record<string, string> }\r\n /** 404: the provider is not connected for that scope; 403: the target is not a host of that provider. */\r\n | { ok: false; status: 403 | 404; error: 'unknown-credential' | 'forbidden-target'; provider: string };\r\n\r\n/**\r\n * Swaps every server-credential placeholder of a proxied request (headers and URL) for the key it\r\n * stands for. The key only goes to a host of its provider: any other target is refused, so a\r\n * placeholder can never carry a key elsewhere. A request without placeholders passes unchanged.\r\n */\r\nexport async function resolveServerCredentials(request: ServerCredentialRequest, lookup: ServerCredentialLookup): Promise<ServerCredentialResolution> {\r\n const found = [request.url, ...Object.values(request.headers)].flatMap(findServerCredentials);\r\n if (found.length === 0) return { ok: true, url: request.url, headers: request.headers };\r\n\r\n const target = new URL(request.url);\r\n let { url } = request;\r\n let headers = { ...request.headers };\r\n const done = new Set<string>();\r\n for (const { placeholder, scope, provider } of found) {\r\n if (done.has(placeholder)) continue;\r\n done.add(placeholder);\r\n const secret = await lookup(scope, provider);\r\n if (!secret?.apiKey) return { ok: false, status: 404, error: 'unknown-credential', provider };\r\n if (!isAIProviderTarget({ ...secret, id: provider }, target)) return { ok: false, status: 403, error: 'forbidden-target', provider };\r\n url = url.split(placeholder).join(encodeURIComponent(secret.apiKey));\r\n headers = Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, value.split(placeholder).join(secret.apiKey)]));\r\n }\r\n return { ok: true, url, headers };\r\n}\r\n"],"mappings":"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"}