modeldispatcher-browser-agent 0.6.0 → 0.6.6
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/README.md +203 -203
- package/package.json +45 -40
- package/src/agent.ts +206 -206
- package/src/config.ts +117 -117
- package/src/externalChat.ts +115 -115
- package/src/externalChatFavorite.ts +43 -43
- package/src/features.ts +40 -40
- package/src/http.ts +100 -100
- package/src/index.ts +52 -52
- package/src/messages.ts +61 -61
- package/src/providers/anthropic.ts +86 -86
- package/src/providers/gemini.ts +95 -95
- package/src/providers/ollama.ts +81 -81
- package/src/providers/openaiCompatible.ts +89 -89
- package/src/registry.ts +82 -82
- package/src/types.ts +108 -108
package/README.md
CHANGED
|
@@ -1,203 +1,203 @@
|
|
|
1
|
-
# modeldispatcher-browser-agent
|
|
2
|
-
|
|
3
|
-
A browser-native, bring-your-own-key AI agent core. The client-side sibling
|
|
4
|
-
to `model-dispatcher` for apps with **no backend** to run the Python gateway
|
|
5
|
-
on: talks directly to Gemini, OpenAI, Anthropic, Groq, or a local Ollama from
|
|
6
|
-
the browser, using whatever key(s) the end user supplies. No vendor SDK, no
|
|
7
|
-
server, nothing sent anywhere but the provider itself.
|
|
8
|
-
|
|
9
|
-
`AgentConfig` is a **fallback list**, not a single selection: configure more
|
|
10
|
-
than one provider (and more than one pooled key per provider), and
|
|
11
|
-
`complete`/`streamComplete`/`streamChat` try each candidate in order,
|
|
12
|
-
falling through to the next key or provider on failure instead of failing
|
|
13
|
-
the whole request — the same idea as the Python core's fallback chain,
|
|
14
|
-
running client-side.
|
|
15
|
-
|
|
16
|
-
Extracted from four apps (StepByLearn, JobFlowTracker, KanDOne, HighFive)
|
|
17
|
-
that had each independently built the same thing — one canonical provider
|
|
18
|
-
registry, request/response handling, and streaming parser instead of four
|
|
19
|
-
near-duplicate copies.
|
|
20
|
-
|
|
21
|
-
## Install
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
npm install modeldispatcher-browser-agent
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
(Published to the public npm registry — no `.npmrc` or token needed.)
|
|
28
|
-
|
|
29
|
-
## Usage
|
|
30
|
-
|
|
31
|
-
```ts
|
|
32
|
-
import { loadConfig, complete, streamChat, PROVIDERS, MODEL_OPTIONS } from "modeldispatcher-browser-agent";
|
|
33
|
-
|
|
34
|
-
// Reads the whole fallback list from localStorage by default (one JSON
|
|
35
|
-
// blob under "aiConfig"); pass a ConfigStorage + ConfigKeys to use your
|
|
36
|
-
// app's own key name.
|
|
37
|
-
const cfg = loadConfig();
|
|
38
|
-
// cfg.providers: [{ provider: "gemini", model: "gemini-2.0-flash", apiKeys: ["AIza-..."] }, ...]
|
|
39
|
-
// cfg.ollamaUrl: "http://localhost:11434"
|
|
40
|
-
|
|
41
|
-
// One-shot, full text back — tries cfg.providers in order, and each
|
|
42
|
-
// provider's own pooled keys in order, until one succeeds:
|
|
43
|
-
const answer = await complete(cfg, "Summarise this in one sentence: ...");
|
|
44
|
-
|
|
45
|
-
// One-shot, incremental text back:
|
|
46
|
-
await streamComplete(cfg, "Write a haiku", { onChunk: (text) => render(text) });
|
|
47
|
-
|
|
48
|
-
// Multi-turn chat — normalises a raw UI message list (role validation,
|
|
49
|
-
// user-first ordering, merging) before dispatching:
|
|
50
|
-
await streamChat(cfg, uiMessages, {
|
|
51
|
-
systemInstruction: "You are a helpful assistant.",
|
|
52
|
-
onChunk: (text) => render(text),
|
|
53
|
-
});
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
`PROVIDERS` is the canonical registry (name, default model, key placeholder,
|
|
57
|
-
where to get a key) and `MODEL_OPTIONS`/`modelOptionsFor(id)` a curated
|
|
58
|
-
shortlist of current models per vendor — together enough to render a
|
|
59
|
-
provider `<select>` and a model `<select>`, the same shape every app's
|
|
60
|
-
`APIKeySettings`/`Settings` screen already builds by hand.
|
|
61
|
-
|
|
62
|
-
### Fallback errors
|
|
63
|
-
|
|
64
|
-
```ts
|
|
65
|
-
import { NoProviderConfiguredError, AllProvidersExhaustedError } from "modeldispatcher-browser-agent";
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
await complete(cfg, prompt);
|
|
69
|
-
} catch (err) {
|
|
70
|
-
if (err instanceof NoProviderConfiguredError) {
|
|
71
|
-
// cfg.providers has nothing usable — send the visitor to AI settings.
|
|
72
|
-
} else if (err instanceof AllProvidersExhaustedError) {
|
|
73
|
-
// Every configured provider/key was tried. err.attempts is
|
|
74
|
-
// [{ provider, error }, ...] for a detailed failure message.
|
|
75
|
-
} else {
|
|
76
|
-
throw err;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
A streaming call (`streamComplete`/`streamChat`) only falls back **before**
|
|
82
|
-
the first chunk of a given candidate reaches `onChunk`. Once any text has
|
|
83
|
-
streamed, a later failure on that same candidate is thrown immediately
|
|
84
|
-
instead of silently retried on the next provider — the caller has already
|
|
85
|
-
rendered a partial answer, and switching providers mid-stream would
|
|
86
|
-
duplicate or contradict it rather than complete it.
|
|
87
|
-
|
|
88
|
-
## No-API-key escape hatch: `openExternalChat`
|
|
89
|
-
|
|
90
|
-
For a user with no API key configured (or who just doesn't want to set one
|
|
91
|
-
up), `openExternalChat` hands their question off to a free, public AI chat
|
|
92
|
-
product instead — ChatGPT, Claude, Gemini (via Google Search's AI Mode), or
|
|
93
|
-
Groq — opening it in a new tab with the question pre-filled where the
|
|
94
|
-
product supports that, and always copying the question to the clipboard too
|
|
95
|
-
as a fallback:
|
|
96
|
-
|
|
97
|
-
```ts
|
|
98
|
-
import { openExternalChat, EXTERNAL_CHAT_PROVIDERS } from "modeldispatcher-browser-agent";
|
|
99
|
-
|
|
100
|
-
const result = await openExternalChat("claude", "Summarise this in one sentence: ...");
|
|
101
|
-
// result.prefilled — true if the question actually made it into the URL
|
|
102
|
-
// result.copiedToClipboard — true if it's also on the clipboard, for pasting
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
`EXTERNAL_CHAT_PROVIDERS` lists the four supported products for rendering a
|
|
106
|
-
picker, the same way `PROVIDERS` does for the BYOK providers above.
|
|
107
|
-
|
|
108
|
-
**Why this needs a clipboard fallback at all:** the URL query parameters that
|
|
109
|
-
pre-fill a provider's chat box (`claude.ai/new?q=...`, `chatgpt.com/?q=...`,
|
|
110
|
-
Google Search's `udm=50` AI Mode) are undocumented, reverse-engineered
|
|
111
|
-
conventions — not a stable API any vendor promises to keep working. Groq has
|
|
112
|
-
no known one at all, so it just opens the plain homepage. `openExternalChat`
|
|
113
|
-
always copies the question to the clipboard regardless, so a broken or
|
|
114
|
-
removed parameter never means the user's question is lost, just that they
|
|
115
|
-
paste instead of finding it already typed in.
|
|
116
|
-
|
|
117
|
-
## Saving a favorite for the escape hatch: `loadExternalChatFavorite`
|
|
118
|
-
|
|
119
|
-
`openExternalChat` is an *action* — it navigates the instant it's called, so
|
|
120
|
-
it belongs on a button next to wherever the user is actually about to ask
|
|
121
|
-
something, never inside a settings screen (clicking a settings option should
|
|
122
|
-
never itself open a new tab). What *does* belong in settings is letting the
|
|
123
|
-
visitor pick and save **which** free product they'd reach for, ahead of
|
|
124
|
-
time — that's what this is for:
|
|
125
|
-
|
|
126
|
-
```ts
|
|
127
|
-
import {
|
|
128
|
-
loadExternalChatFavorite,
|
|
129
|
-
saveExternalChatFavorite,
|
|
130
|
-
} from "modeldispatcher-browser-agent";
|
|
131
|
-
|
|
132
|
-
// Settings screen: save the pick. No network call, no navigation.
|
|
133
|
-
saveExternalChatFavorite("claude");
|
|
134
|
-
|
|
135
|
-
// Wherever the user composes a question: read it back to decide what
|
|
136
|
-
// the "ask externally" button should say/do.
|
|
137
|
-
const favorite = loadExternalChatFavorite(); // "claude" | null
|
|
138
|
-
if (favorite) {
|
|
139
|
-
await openExternalChat(favorite, currentQuestion);
|
|
140
|
-
}
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
Deliberately separate storage from `loadConfig`/`saveConfig` (BYOK
|
|
144
|
-
provider/key/model) — picking a favorite here has nothing to do with which
|
|
145
|
-
BYOK provider is configured, and a settings screen commonly offers both as
|
|
146
|
-
independent, unrelated choices.
|
|
147
|
-
|
|
148
|
-
## Making it optional per app: `resolveDispatcherFeatures`
|
|
149
|
-
|
|
150
|
-
Adopting this doesn't have to be all-or-nothing on day one. Two independent,
|
|
151
|
-
developer-set flags — never a switch an end user sees — decide what an app
|
|
152
|
-
actually turns on:
|
|
153
|
-
|
|
154
|
-
```ts
|
|
155
|
-
// modeldispatcher.config.ts — your app's own config module, not exported to users
|
|
156
|
-
import { resolveDispatcherFeatures } from "modeldispatcher-browser-agent";
|
|
157
|
-
|
|
158
|
-
export const dispatcherFeatures = resolveDispatcherFeatures({
|
|
159
|
-
ui: import.meta.env.VITE_MODEL_DISPATCHER_UI !== "false",
|
|
160
|
-
dispatch: import.meta.env.VITE_MODEL_DISPATCHER_DISPATCH !== "false",
|
|
161
|
-
});
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
```tsx
|
|
165
|
-
function AiSettingsScreen() {
|
|
166
|
-
// .ui: render <ModelPicker>/<AskExternallyButton>, or this app's own settings screen.
|
|
167
|
-
return dispatcherFeatures.ui ? <ModelPicker {...props} /> : <LegacyAiSettings />;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
async function askAi(question: string) {
|
|
171
|
-
// .dispatch: route through complete()'s fallback dispatch, or this app's own call path.
|
|
172
|
-
return dispatcherFeatures.dispatch ? complete(loadConfig(), question) : legacyAskAi(question);
|
|
173
|
-
}
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
Both default to `true` — resolving with no overrides at all turns everything
|
|
177
|
-
on — so an app opts *out* per feature during a gradual rollout rather than
|
|
178
|
-
opting in from a cold start. This package never reads environment variables
|
|
179
|
-
itself (every bundler exposes them differently); your app's config module
|
|
180
|
-
resolves its own env var and passes the boolean in. Full pattern, including
|
|
181
|
-
the non-Vite env var forms: [`docs/USAGE.md`](../../docs/USAGE.md).
|
|
182
|
-
|
|
183
|
-
## What's in scope, what isn't
|
|
184
|
-
|
|
185
|
-
This package owns the generic "talk to a provider" plumbing: provider
|
|
186
|
-
configs, request/response translation, SSE/NDJSON stream parsing, retry on
|
|
187
|
-
429/5xx, cross-provider/cross-key fallback dispatch, timeout/abort handling,
|
|
188
|
-
Ollama URL validation, and chat-history normalisation. It deliberately does
|
|
189
|
-
**not** know about any app's actual
|
|
190
|
-
prompts or business logic (job-search coaching, trip parsing, whatever) —
|
|
191
|
-
that stays in each app, calling through this the same way
|
|
192
|
-
`services/llm_model_dispatcher.py` in AppMyTrip calls through the Python
|
|
193
|
-
gateway without the gateway knowing what a "trip" is.
|
|
194
|
-
|
|
195
|
-
## Testing
|
|
196
|
-
|
|
197
|
-
```bash
|
|
198
|
-
npm run typecheck
|
|
199
|
-
npm test
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
All 81 tests run against mocked `fetch`/`ReadableStream`/`window.open`/
|
|
203
|
-
clipboard — no real network, no real API key needed.
|
|
1
|
+
# modeldispatcher-browser-agent
|
|
2
|
+
|
|
3
|
+
A browser-native, bring-your-own-key AI agent core. The client-side sibling
|
|
4
|
+
to `model-dispatcher` for apps with **no backend** to run the Python gateway
|
|
5
|
+
on: talks directly to Gemini, OpenAI, Anthropic, Groq, or a local Ollama from
|
|
6
|
+
the browser, using whatever key(s) the end user supplies. No vendor SDK, no
|
|
7
|
+
server, nothing sent anywhere but the provider itself.
|
|
8
|
+
|
|
9
|
+
`AgentConfig` is a **fallback list**, not a single selection: configure more
|
|
10
|
+
than one provider (and more than one pooled key per provider), and
|
|
11
|
+
`complete`/`streamComplete`/`streamChat` try each candidate in order,
|
|
12
|
+
falling through to the next key or provider on failure instead of failing
|
|
13
|
+
the whole request — the same idea as the Python core's fallback chain,
|
|
14
|
+
running client-side.
|
|
15
|
+
|
|
16
|
+
Extracted from four apps (StepByLearn, JobFlowTracker, KanDOne, HighFive)
|
|
17
|
+
that had each independently built the same thing — one canonical provider
|
|
18
|
+
registry, request/response handling, and streaming parser instead of four
|
|
19
|
+
near-duplicate copies.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install modeldispatcher-browser-agent
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
(Published to the public npm registry — no `.npmrc` or token needed.)
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { loadConfig, complete, streamChat, PROVIDERS, MODEL_OPTIONS } from "modeldispatcher-browser-agent";
|
|
33
|
+
|
|
34
|
+
// Reads the whole fallback list from localStorage by default (one JSON
|
|
35
|
+
// blob under "aiConfig"); pass a ConfigStorage + ConfigKeys to use your
|
|
36
|
+
// app's own key name.
|
|
37
|
+
const cfg = loadConfig();
|
|
38
|
+
// cfg.providers: [{ provider: "gemini", model: "gemini-2.0-flash", apiKeys: ["AIza-..."] }, ...]
|
|
39
|
+
// cfg.ollamaUrl: "http://localhost:11434"
|
|
40
|
+
|
|
41
|
+
// One-shot, full text back — tries cfg.providers in order, and each
|
|
42
|
+
// provider's own pooled keys in order, until one succeeds:
|
|
43
|
+
const answer = await complete(cfg, "Summarise this in one sentence: ...");
|
|
44
|
+
|
|
45
|
+
// One-shot, incremental text back:
|
|
46
|
+
await streamComplete(cfg, "Write a haiku", { onChunk: (text) => render(text) });
|
|
47
|
+
|
|
48
|
+
// Multi-turn chat — normalises a raw UI message list (role validation,
|
|
49
|
+
// user-first ordering, merging) before dispatching:
|
|
50
|
+
await streamChat(cfg, uiMessages, {
|
|
51
|
+
systemInstruction: "You are a helpful assistant.",
|
|
52
|
+
onChunk: (text) => render(text),
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`PROVIDERS` is the canonical registry (name, default model, key placeholder,
|
|
57
|
+
where to get a key) and `MODEL_OPTIONS`/`modelOptionsFor(id)` a curated
|
|
58
|
+
shortlist of current models per vendor — together enough to render a
|
|
59
|
+
provider `<select>` and a model `<select>`, the same shape every app's
|
|
60
|
+
`APIKeySettings`/`Settings` screen already builds by hand.
|
|
61
|
+
|
|
62
|
+
### Fallback errors
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { NoProviderConfiguredError, AllProvidersExhaustedError } from "modeldispatcher-browser-agent";
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
await complete(cfg, prompt);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (err instanceof NoProviderConfiguredError) {
|
|
71
|
+
// cfg.providers has nothing usable — send the visitor to AI settings.
|
|
72
|
+
} else if (err instanceof AllProvidersExhaustedError) {
|
|
73
|
+
// Every configured provider/key was tried. err.attempts is
|
|
74
|
+
// [{ provider, error }, ...] for a detailed failure message.
|
|
75
|
+
} else {
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
A streaming call (`streamComplete`/`streamChat`) only falls back **before**
|
|
82
|
+
the first chunk of a given candidate reaches `onChunk`. Once any text has
|
|
83
|
+
streamed, a later failure on that same candidate is thrown immediately
|
|
84
|
+
instead of silently retried on the next provider — the caller has already
|
|
85
|
+
rendered a partial answer, and switching providers mid-stream would
|
|
86
|
+
duplicate or contradict it rather than complete it.
|
|
87
|
+
|
|
88
|
+
## No-API-key escape hatch: `openExternalChat`
|
|
89
|
+
|
|
90
|
+
For a user with no API key configured (or who just doesn't want to set one
|
|
91
|
+
up), `openExternalChat` hands their question off to a free, public AI chat
|
|
92
|
+
product instead — ChatGPT, Claude, Gemini (via Google Search's AI Mode), or
|
|
93
|
+
Groq — opening it in a new tab with the question pre-filled where the
|
|
94
|
+
product supports that, and always copying the question to the clipboard too
|
|
95
|
+
as a fallback:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import { openExternalChat, EXTERNAL_CHAT_PROVIDERS } from "modeldispatcher-browser-agent";
|
|
99
|
+
|
|
100
|
+
const result = await openExternalChat("claude", "Summarise this in one sentence: ...");
|
|
101
|
+
// result.prefilled — true if the question actually made it into the URL
|
|
102
|
+
// result.copiedToClipboard — true if it's also on the clipboard, for pasting
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`EXTERNAL_CHAT_PROVIDERS` lists the four supported products for rendering a
|
|
106
|
+
picker, the same way `PROVIDERS` does for the BYOK providers above.
|
|
107
|
+
|
|
108
|
+
**Why this needs a clipboard fallback at all:** the URL query parameters that
|
|
109
|
+
pre-fill a provider's chat box (`claude.ai/new?q=...`, `chatgpt.com/?q=...`,
|
|
110
|
+
Google Search's `udm=50` AI Mode) are undocumented, reverse-engineered
|
|
111
|
+
conventions — not a stable API any vendor promises to keep working. Groq has
|
|
112
|
+
no known one at all, so it just opens the plain homepage. `openExternalChat`
|
|
113
|
+
always copies the question to the clipboard regardless, so a broken or
|
|
114
|
+
removed parameter never means the user's question is lost, just that they
|
|
115
|
+
paste instead of finding it already typed in.
|
|
116
|
+
|
|
117
|
+
## Saving a favorite for the escape hatch: `loadExternalChatFavorite`
|
|
118
|
+
|
|
119
|
+
`openExternalChat` is an *action* — it navigates the instant it's called, so
|
|
120
|
+
it belongs on a button next to wherever the user is actually about to ask
|
|
121
|
+
something, never inside a settings screen (clicking a settings option should
|
|
122
|
+
never itself open a new tab). What *does* belong in settings is letting the
|
|
123
|
+
visitor pick and save **which** free product they'd reach for, ahead of
|
|
124
|
+
time — that's what this is for:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import {
|
|
128
|
+
loadExternalChatFavorite,
|
|
129
|
+
saveExternalChatFavorite,
|
|
130
|
+
} from "modeldispatcher-browser-agent";
|
|
131
|
+
|
|
132
|
+
// Settings screen: save the pick. No network call, no navigation.
|
|
133
|
+
saveExternalChatFavorite("claude");
|
|
134
|
+
|
|
135
|
+
// Wherever the user composes a question: read it back to decide what
|
|
136
|
+
// the "ask externally" button should say/do.
|
|
137
|
+
const favorite = loadExternalChatFavorite(); // "claude" | null
|
|
138
|
+
if (favorite) {
|
|
139
|
+
await openExternalChat(favorite, currentQuestion);
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Deliberately separate storage from `loadConfig`/`saveConfig` (BYOK
|
|
144
|
+
provider/key/model) — picking a favorite here has nothing to do with which
|
|
145
|
+
BYOK provider is configured, and a settings screen commonly offers both as
|
|
146
|
+
independent, unrelated choices.
|
|
147
|
+
|
|
148
|
+
## Making it optional per app: `resolveDispatcherFeatures`
|
|
149
|
+
|
|
150
|
+
Adopting this doesn't have to be all-or-nothing on day one. Two independent,
|
|
151
|
+
developer-set flags — never a switch an end user sees — decide what an app
|
|
152
|
+
actually turns on:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
// modeldispatcher.config.ts — your app's own config module, not exported to users
|
|
156
|
+
import { resolveDispatcherFeatures } from "modeldispatcher-browser-agent";
|
|
157
|
+
|
|
158
|
+
export const dispatcherFeatures = resolveDispatcherFeatures({
|
|
159
|
+
ui: import.meta.env.VITE_MODEL_DISPATCHER_UI !== "false",
|
|
160
|
+
dispatch: import.meta.env.VITE_MODEL_DISPATCHER_DISPATCH !== "false",
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
function AiSettingsScreen() {
|
|
166
|
+
// .ui: render <ModelPicker>/<AskExternallyButton>, or this app's own settings screen.
|
|
167
|
+
return dispatcherFeatures.ui ? <ModelPicker {...props} /> : <LegacyAiSettings />;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function askAi(question: string) {
|
|
171
|
+
// .dispatch: route through complete()'s fallback dispatch, or this app's own call path.
|
|
172
|
+
return dispatcherFeatures.dispatch ? complete(loadConfig(), question) : legacyAskAi(question);
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Both default to `true` — resolving with no overrides at all turns everything
|
|
177
|
+
on — so an app opts *out* per feature during a gradual rollout rather than
|
|
178
|
+
opting in from a cold start. This package never reads environment variables
|
|
179
|
+
itself (every bundler exposes them differently); your app's config module
|
|
180
|
+
resolves its own env var and passes the boolean in. Full pattern, including
|
|
181
|
+
the non-Vite env var forms: [`docs/USAGE.md`](../../docs/USAGE.md).
|
|
182
|
+
|
|
183
|
+
## What's in scope, what isn't
|
|
184
|
+
|
|
185
|
+
This package owns the generic "talk to a provider" plumbing: provider
|
|
186
|
+
configs, request/response translation, SSE/NDJSON stream parsing, retry on
|
|
187
|
+
429/5xx, cross-provider/cross-key fallback dispatch, timeout/abort handling,
|
|
188
|
+
Ollama URL validation, and chat-history normalisation. It deliberately does
|
|
189
|
+
**not** know about any app's actual
|
|
190
|
+
prompts or business logic (job-search coaching, trip parsing, whatever) —
|
|
191
|
+
that stays in each app, calling through this the same way
|
|
192
|
+
`services/llm_model_dispatcher.py` in AppMyTrip calls through the Python
|
|
193
|
+
gateway without the gateway knowing what a "trip" is.
|
|
194
|
+
|
|
195
|
+
## Testing
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
npm run typecheck
|
|
199
|
+
npm test
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
All 81 tests run against mocked `fetch`/`ReadableStream`/`window.open`/
|
|
203
|
+
clipboard — no real network, no real API key needed.
|
package/package.json
CHANGED
|
@@ -1,40 +1,45 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "modeldispatcher-browser-agent",
|
|
3
|
-
"version": "0.6.
|
|
4
|
-
"description": "Browser-native, bring-your-own-key AI agent core with cross-provider/cross-key fallback dispatch: multi-provider (Gemini/OpenAI/Anthropic/Groq/Ollama), streaming and non-streaming, no backend and no vendor SDK required. The client-side sibling to model-dispatcher for apps with no server to run it on.",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "modeldispatcher-browser-agent",
|
|
3
|
+
"version": "0.6.6",
|
|
4
|
+
"description": "Browser-native, bring-your-own-key AI agent core with cross-provider/cross-key fallback dispatch: multi-provider (Gemini/OpenAI/Anthropic/Groq/Ollama), streaming and non-streaming, no backend and no vendor SDK required. The client-side sibling to model-dispatcher for apps with no server to run it on.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/joka-7/ModelDispatcher.git",
|
|
9
|
+
"directory": "clients/browser-agent"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"module": "./dist/index.js",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"src"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc -p tsconfig.build.json",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"test": "vitest run"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"llm",
|
|
32
|
+
"ai-agent",
|
|
33
|
+
"browser",
|
|
34
|
+
"gemini",
|
|
35
|
+
"openai",
|
|
36
|
+
"anthropic",
|
|
37
|
+
"groq",
|
|
38
|
+
"ollama",
|
|
39
|
+
"streaming"
|
|
40
|
+
],
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"typescript": "^5.5.0",
|
|
43
|
+
"vitest": "^2.0.0"
|
|
44
|
+
}
|
|
45
|
+
}
|