modeldispatcher-browser-agent 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +203 -0
- package/dist/agent.d.ts +64 -0
- package/dist/agent.js +160 -0
- package/dist/agent.js.map +1 -0
- package/dist/config.d.ts +43 -0
- package/dist/config.js +91 -0
- package/dist/config.js.map +1 -0
- package/dist/externalChat.d.ts +46 -0
- package/dist/externalChat.js +86 -0
- package/dist/externalChat.js.map +1 -0
- package/dist/externalChatFavorite.d.ts +20 -0
- package/dist/externalChatFavorite.js +33 -0
- package/dist/externalChatFavorite.js.map +1 -0
- package/dist/features.d.ts +30 -0
- package/dist/features.js +28 -0
- package/dist/features.js.map +1 -0
- package/dist/http.d.ts +29 -0
- package/dist/http.js +91 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/messages.d.ts +26 -0
- package/dist/messages.js +41 -0
- package/dist/messages.js.map +1 -0
- package/dist/providers/anthropic.d.ts +10 -0
- package/dist/providers/anthropic.js +64 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/gemini.d.ts +8 -0
- package/dist/providers/gemini.js +66 -0
- package/dist/providers/gemini.js.map +1 -0
- package/dist/providers/ollama.d.ts +10 -0
- package/dist/providers/ollama.js +61 -0
- package/dist/providers/ollama.js.map +1 -0
- package/dist/providers/openaiCompatible.d.ts +9 -0
- package/dist/providers/openaiCompatible.js +64 -0
- package/dist/providers/openaiCompatible.js.map +1 -0
- package/dist/registry.d.ts +18 -0
- package/dist/registry.js +77 -0
- package/dist/registry.js.map +1 -0
- package/dist/types.d.ts +97 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/package.json +40 -0
- package/src/agent.ts +206 -0
- package/src/config.ts +117 -0
- package/src/externalChat.ts +115 -0
- package/src/externalChatFavorite.ts +43 -0
- package/src/features.ts +40 -0
- package/src/http.ts +100 -0
- package/src/index.ts +52 -0
- package/src/messages.ts +61 -0
- package/src/providers/anthropic.ts +86 -0
- package/src/providers/gemini.ts +95 -0
- package/src/providers/ollama.ts +81 -0
- package/src/providers/openaiCompatible.ts +89 -0
- package/src/registry.ts +82 -0
- package/src/types.ts +108 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Free agent" escape hatch: hand a question off to a free, public AI chat
|
|
3
|
+
* web product instead of calling any API. For a BYOK app, this is what a
|
|
4
|
+
* user with no API key configured (or who just doesn't want to set one up)
|
|
5
|
+
* falls back to — open one of the well-known consumer chat products in a new
|
|
6
|
+
* tab, with the question pre-filled where the product supports it.
|
|
7
|
+
*
|
|
8
|
+
* The query-prefill URL parameters below are undocumented, reverse-engineered
|
|
9
|
+
* conventions (the kind of thing browser address-bar integrations and
|
|
10
|
+
* "Ask X" share links rely on) — not a stable public API any vendor promises
|
|
11
|
+
* to keep working, and they can change or disappear without notice. Treat
|
|
12
|
+
* them as best-effort: `openExternalChat` always copies the question to the
|
|
13
|
+
* clipboard too, regardless of whether a prefill parameter exists, so a
|
|
14
|
+
* broken/removed parameter still leaves the user able to paste it in by hand
|
|
15
|
+
* instead of silently losing their question.
|
|
16
|
+
*/
|
|
17
|
+
export const EXTERNAL_CHAT_PROVIDERS = {
|
|
18
|
+
chatgpt: {
|
|
19
|
+
id: "chatgpt",
|
|
20
|
+
name: "ChatGPT",
|
|
21
|
+
homeUrl: "https://chatgpt.com/",
|
|
22
|
+
buildUrl: (question) => `https://chatgpt.com/?${new URLSearchParams({ q: question, hints: "search" })}`,
|
|
23
|
+
},
|
|
24
|
+
claude: {
|
|
25
|
+
id: "claude",
|
|
26
|
+
name: "Claude",
|
|
27
|
+
homeUrl: "https://claude.ai/new",
|
|
28
|
+
buildUrl: (question) => `https://claude.ai/new?${new URLSearchParams({ q: question })}`,
|
|
29
|
+
},
|
|
30
|
+
gemini: {
|
|
31
|
+
id: "gemini",
|
|
32
|
+
name: "Gemini (Google AI Mode)",
|
|
33
|
+
homeUrl: "https://www.google.com/",
|
|
34
|
+
// gemini.google.com itself has no known prefill parameter. Google
|
|
35
|
+
// Search's AI Mode does (`udm=50`), and is the more reliable target for
|
|
36
|
+
// "ask Google's AI this" — that's what this actually opens.
|
|
37
|
+
buildUrl: (question) => `https://www.google.com/search?${new URLSearchParams({ q: question, udm: "50" })}`,
|
|
38
|
+
},
|
|
39
|
+
groq: {
|
|
40
|
+
id: "groq",
|
|
41
|
+
name: "Groq",
|
|
42
|
+
homeUrl: "https://groq.com/",
|
|
43
|
+
// No known query-prefill parameter for Groq's consumer chat product —
|
|
44
|
+
// opens the plain homepage; the question still goes to the clipboard.
|
|
45
|
+
buildUrl: null,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
function defaultOpen(url) {
|
|
49
|
+
if (typeof window === "undefined")
|
|
50
|
+
return;
|
|
51
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
52
|
+
}
|
|
53
|
+
function resolveClipboard(clipboard) {
|
|
54
|
+
if (clipboard)
|
|
55
|
+
return clipboard;
|
|
56
|
+
if (typeof navigator !== "undefined" && navigator.clipboard)
|
|
57
|
+
return navigator.clipboard;
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Open `provider`'s chat with `question`, pre-filled where a prefill
|
|
62
|
+
* parameter is known, and always copy `question` to the clipboard as well.
|
|
63
|
+
*
|
|
64
|
+
* Never throws: a clipboard failure (permissions, a non-secure context, a
|
|
65
|
+
* non-browser host) is reported via `copiedToClipboard: false` rather than
|
|
66
|
+
* rejecting, since the tab still opens either way.
|
|
67
|
+
*/
|
|
68
|
+
export async function openExternalChat(provider, question, deps = {}) {
|
|
69
|
+
const info = EXTERNAL_CHAT_PROVIDERS[provider];
|
|
70
|
+
const prefilled = info.buildUrl !== null;
|
|
71
|
+
const url = info.buildUrl ? info.buildUrl(question) : info.homeUrl;
|
|
72
|
+
const clipboard = resolveClipboard(deps.clipboard);
|
|
73
|
+
let copiedToClipboard = false;
|
|
74
|
+
if (clipboard) {
|
|
75
|
+
try {
|
|
76
|
+
await clipboard.writeText(question);
|
|
77
|
+
copiedToClipboard = true;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
copiedToClipboard = false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
(deps.open ?? defaultOpen)(url);
|
|
84
|
+
return { provider, url, prefilled, copiedToClipboard };
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=externalChat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"externalChat.js","sourceRoot":"","sources":["../src/externalChat.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,MAAM,CAAC,MAAM,uBAAuB,GAA6D;IAC/F,OAAO,EAAE;QACP,EAAE,EAAE,SAAS;QACb,IAAI,EAAE,SAAS;QACf,OAAO,EAAE,sBAAsB;QAC/B,QAAQ,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,wBAAwB,IAAI,eAAe,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,EAAE;KACxG;IACD,MAAM,EAAE;QACN,EAAE,EAAE,QAAQ;QACZ,IAAI,EAAE,QAAQ;QACd,OAAO,EAAE,uBAAuB;QAChC,QAAQ,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,yBAAyB,IAAI,eAAe,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE;KACxF;IACD,MAAM,EAAE;QACN,EAAE,EAAE,QAAQ;QACZ,IAAI,EAAE,yBAAyB;QAC/B,OAAO,EAAE,yBAAyB;QAClC,kEAAkE;QAClE,wEAAwE;QACxE,4DAA4D;QAC5D,QAAQ,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,iCAAiC,IAAI,eAAe,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,EAAE;KAC3G;IACD,IAAI,EAAE;QACJ,EAAE,EAAE,MAAM;QACV,IAAI,EAAE,MAAM;QACZ,OAAO,EAAE,mBAAmB;QAC5B,sEAAsE;QACtE,sEAAsE;QACtE,QAAQ,EAAE,IAAI;KACf;CACF,CAAC;AAqBF,SAAS,WAAW,CAAC,GAAW;IAC9B,IAAI,OAAO,MAAM,KAAK,WAAW;QAAE,OAAO;IAC1C,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,qBAAqB,CAAC,CAAC;AACpD,CAAC;AAED,SAAS,gBAAgB,CACvB,SAA6C;IAE7C,IAAI,SAAS;QAAE,OAAO,SAAS,CAAC;IAChC,IAAI,OAAO,SAAS,KAAK,WAAW,IAAI,SAAS,CAAC,SAAS;QAAE,OAAO,SAAS,CAAC,SAAS,CAAC;IACxF,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,QAAgC,EAChC,QAAgB,EAChB,OAA6B,EAAE;IAE/B,MAAM,IAAI,GAAG,uBAAuB,CAAC,QAAQ,CAAC,CAAC;IAC/C,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC;IACzC,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC;IAEnE,MAAM,SAAS,GAAG,gBAAgB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACnD,IAAI,iBAAiB,GAAG,KAAK,CAAC;IAC9B,IAAI,SAAS,EAAE,CAAC;QACd,IAAI,CAAC;YACH,MAAM,SAAS,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;YACpC,iBAAiB,GAAG,IAAI,CAAC;QAC3B,CAAC;QAAC,MAAM,CAAC;YACP,iBAAiB,GAAG,KAAK,CAAC;QAC5B,CAAC;IACH,CAAC;IAED,CAAC,IAAI,CAAC,IAAI,IAAI,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC;IAEhC,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,EAAE,iBAAiB,EAAE,CAAC;AACzD,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence for a visitor's saved "ask externally" favorite.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately separate from {@link ./config.js}'s `AgentConfig`
|
|
5
|
+
* (provider/key/model for BYOK dispatch): picking a favorite here means the
|
|
6
|
+
* visitor wants the app to offer opening this free chat product instead of
|
|
7
|
+
* calling any API, so it has nothing to do with which provider/key is
|
|
8
|
+
* configured for BYOK and must never be conflated with it. A settings screen
|
|
9
|
+
* showing both is choosing between two independent, saveable preferences —
|
|
10
|
+
* selecting a favorite here never itself opens anything (see
|
|
11
|
+
* {@link ./externalChat.js}'s `openExternalChat` for the action that does).
|
|
12
|
+
*/
|
|
13
|
+
import { type ConfigStorage } from "./config.js";
|
|
14
|
+
import type { ExternalChatProviderId } from "./types.js";
|
|
15
|
+
export declare const DEFAULT_EXTERNAL_CHAT_FAVORITE_KEY = "aiExternalChatFavorite";
|
|
16
|
+
/** Read the saved favorite, or `null` if none is set (or the stored value is
|
|
17
|
+
* no longer a recognised provider). */
|
|
18
|
+
export declare function loadExternalChatFavorite(storage?: ConfigStorage, key?: string): ExternalChatProviderId | null;
|
|
19
|
+
/** Persist the favorite; pass `null` to clear it. */
|
|
20
|
+
export declare function saveExternalChatFavorite(favorite: ExternalChatProviderId | null, storage?: ConfigStorage, key?: string): void;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence for a visitor's saved "ask externally" favorite.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately separate from {@link ./config.js}'s `AgentConfig`
|
|
5
|
+
* (provider/key/model for BYOK dispatch): picking a favorite here means the
|
|
6
|
+
* visitor wants the app to offer opening this free chat product instead of
|
|
7
|
+
* calling any API, so it has nothing to do with which provider/key is
|
|
8
|
+
* configured for BYOK and must never be conflated with it. A settings screen
|
|
9
|
+
* showing both is choosing between two independent, saveable preferences —
|
|
10
|
+
* selecting a favorite here never itself opens anything (see
|
|
11
|
+
* {@link ./externalChat.js}'s `openExternalChat` for the action that does).
|
|
12
|
+
*/
|
|
13
|
+
import { EXTERNAL_CHAT_PROVIDERS } from "./externalChat.js";
|
|
14
|
+
import { resolveStorage } from "./config.js";
|
|
15
|
+
export const DEFAULT_EXTERNAL_CHAT_FAVORITE_KEY = "aiExternalChatFavorite";
|
|
16
|
+
function isKnownExternalChatProvider(id) {
|
|
17
|
+
return Object.hasOwn(EXTERNAL_CHAT_PROVIDERS, id);
|
|
18
|
+
}
|
|
19
|
+
/** Read the saved favorite, or `null` if none is set (or the stored value is
|
|
20
|
+
* no longer a recognised provider). */
|
|
21
|
+
export function loadExternalChatFavorite(storage, key = DEFAULT_EXTERNAL_CHAT_FAVORITE_KEY) {
|
|
22
|
+
const raw = resolveStorage(storage).getItem(key);
|
|
23
|
+
return raw !== null && isKnownExternalChatProvider(raw) ? raw : null;
|
|
24
|
+
}
|
|
25
|
+
/** Persist the favorite; pass `null` to clear it. */
|
|
26
|
+
export function saveExternalChatFavorite(favorite, storage, key = DEFAULT_EXTERNAL_CHAT_FAVORITE_KEY) {
|
|
27
|
+
const store = resolveStorage(storage);
|
|
28
|
+
if (favorite === null)
|
|
29
|
+
store.removeItem(key);
|
|
30
|
+
else
|
|
31
|
+
store.setItem(key, favorite);
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=externalChatFavorite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"externalChatFavorite.js","sourceRoot":"","sources":["../src/externalChatFavorite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAsB,MAAM,aAAa,CAAC;AAGjE,MAAM,CAAC,MAAM,kCAAkC,GAAG,wBAAwB,CAAC;AAE3E,SAAS,2BAA2B,CAAC,EAAU;IAC7C,OAAO,MAAM,CAAC,MAAM,CAAC,uBAAuB,EAAE,EAAE,CAAC,CAAC;AACpD,CAAC;AAED;uCACuC;AACvC,MAAM,UAAU,wBAAwB,CACtC,OAAuB,EACvB,MAAc,kCAAkC;IAEhD,MAAM,GAAG,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACjD,OAAO,GAAG,KAAK,IAAI,IAAI,2BAA2B,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AACvE,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,wBAAwB,CACtC,QAAuC,EACvC,OAAuB,EACvB,MAAc,kCAAkC;IAEhD,MAAM,KAAK,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC;IACtC,IAAI,QAAQ,KAAK,IAAI;QAAE,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;;QACxC,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;AACpC,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-app opt-in/opt-out for the two things this package supplies. This is a
|
|
3
|
+
* developer/deploy-time decision made once when an app is wired up — never a
|
|
4
|
+
* switch an end user sees or controls. Both flags default to enabled, so
|
|
5
|
+
* adopting the shared framework is opt-OUT per app, not opt-in-everywhere.
|
|
6
|
+
*
|
|
7
|
+
* This package never reads environment variables itself: every bundler
|
|
8
|
+
* exposes them differently (`import.meta.env` for Vite, `process.env` for
|
|
9
|
+
* Next.js/CRA), so resolving env vars is left to each app's own config
|
|
10
|
+
* module, which passes the result in as `overrides`.
|
|
11
|
+
*/
|
|
12
|
+
/** Which parts of the shared framework this app has enabled. */
|
|
13
|
+
export interface DispatcherFeatureFlags {
|
|
14
|
+
/** Render `<ModelPicker>`/`<AskExternallyButton>`, or this app's own settings UI. */
|
|
15
|
+
readonly ui: boolean;
|
|
16
|
+
/** Route AI calls through `complete()`/`streamComplete()`'s fallback dispatch,
|
|
17
|
+
* or this app's own existing call path. */
|
|
18
|
+
readonly dispatch: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** Both features enabled — the framework's out-of-the-box behavior. */
|
|
21
|
+
export declare const DEFAULT_DISPATCHER_FEATURES: DispatcherFeatureFlags;
|
|
22
|
+
/**
|
|
23
|
+
* Resolves the effective feature flags for this app: enabled-by-default,
|
|
24
|
+
* merged with whatever this app's own config module decided to override.
|
|
25
|
+
*
|
|
26
|
+
* Call this once at your app's own composition root and pass the result down
|
|
27
|
+
* to wherever the UI/dispatch branching actually happens — see
|
|
28
|
+
* `docs/USAGE.md` for the full pattern.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveDispatcherFeatures(overrides?: Partial<DispatcherFeatureFlags>): DispatcherFeatureFlags;
|
package/dist/features.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-app opt-in/opt-out for the two things this package supplies. This is a
|
|
3
|
+
* developer/deploy-time decision made once when an app is wired up — never a
|
|
4
|
+
* switch an end user sees or controls. Both flags default to enabled, so
|
|
5
|
+
* adopting the shared framework is opt-OUT per app, not opt-in-everywhere.
|
|
6
|
+
*
|
|
7
|
+
* This package never reads environment variables itself: every bundler
|
|
8
|
+
* exposes them differently (`import.meta.env` for Vite, `process.env` for
|
|
9
|
+
* Next.js/CRA), so resolving env vars is left to each app's own config
|
|
10
|
+
* module, which passes the result in as `overrides`.
|
|
11
|
+
*/
|
|
12
|
+
/** Both features enabled — the framework's out-of-the-box behavior. */
|
|
13
|
+
export const DEFAULT_DISPATCHER_FEATURES = Object.freeze({
|
|
14
|
+
ui: true,
|
|
15
|
+
dispatch: true,
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* Resolves the effective feature flags for this app: enabled-by-default,
|
|
19
|
+
* merged with whatever this app's own config module decided to override.
|
|
20
|
+
*
|
|
21
|
+
* Call this once at your app's own composition root and pass the result down
|
|
22
|
+
* to wherever the UI/dispatch branching actually happens — see
|
|
23
|
+
* `docs/USAGE.md` for the full pattern.
|
|
24
|
+
*/
|
|
25
|
+
export function resolveDispatcherFeatures(overrides) {
|
|
26
|
+
return { ...DEFAULT_DISPATCHER_FEATURES, ...overrides };
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=features.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"features.js","sourceRoot":"","sources":["../src/features.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAWH,uEAAuE;AACvE,MAAM,CAAC,MAAM,2BAA2B,GAA2B,MAAM,CAAC,MAAM,CAAC;IAC/E,EAAE,EAAE,IAAI;IACR,QAAQ,EAAE,IAAI;CACf,CAAC,CAAC;AAEH;;;;;;;GAOG;AACH,MAAM,UAAU,yBAAyB,CACvC,SAA2C;IAE3C,OAAO,EAAE,GAAG,2BAA2B,EAAE,GAAG,SAAS,EAAE,CAAC;AAC1D,CAAC"}
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared HTTP plumbing every provider call needs: a request timeout merged
|
|
3
|
+
* with the caller's own AbortSignal, one retry on a transient (429/5xx)
|
|
4
|
+
* failure, Ollama URL validation, and a line-buffered stream reader so each
|
|
5
|
+
* provider's stream parser only has to know its own line format.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DEFAULT_TIMEOUT_MS = 60000;
|
|
8
|
+
/** A hung provider (dropped connection, stalled proxy) would otherwise hang
|
|
9
|
+
* forever — fetch() has no default timeout. Merges an optional caller signal
|
|
10
|
+
* (e.g. "abort because the component unmounted") with a hard ceiling. */
|
|
11
|
+
export declare function requestSignal(signal?: AbortSignal, timeoutMs?: number): AbortSignal;
|
|
12
|
+
/**
|
|
13
|
+
* fetch() with one retry on a 429/5xx or a network failure. An
|
|
14
|
+
* externally-aborted request (unmount, a newer request superseding this
|
|
15
|
+
* one) is never retried — that's a deliberate cancellation, not a
|
|
16
|
+
* transient failure, and retrying it would just start a request nobody
|
|
17
|
+
* wants the result of anymore.
|
|
18
|
+
*/
|
|
19
|
+
export declare function fetchWithRetry(url: string, init: RequestInit, externalSignal?: AbortSignal, timeoutMs?: number): Promise<Response>;
|
|
20
|
+
/** Reject a remote Ollama URL that isn't HTTPS (localhost is exempt — that's
|
|
21
|
+
* the whole point of running it locally). Returns the normalised origin. */
|
|
22
|
+
export declare function validateOllamaUrl(url: string): string;
|
|
23
|
+
/** Feeds each complete line of a streamed response body to `onLine`, so
|
|
24
|
+
* every provider's stream parser only needs to know its own line format
|
|
25
|
+
* (SSE "data: " frames vs. bare NDJSON). */
|
|
26
|
+
export declare function consumeLines(res: Response, onLine: (line: string) => void): Promise<void>;
|
|
27
|
+
/** Extract a provider's error message from a JSON error body, falling back
|
|
28
|
+
* to the raw HTTP status when the body isn't shaped as expected. */
|
|
29
|
+
export declare function describeHttpError(res: Response): Promise<string>;
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared HTTP plumbing every provider call needs: a request timeout merged
|
|
3
|
+
* with the caller's own AbortSignal, one retry on a transient (429/5xx)
|
|
4
|
+
* failure, Ollama URL validation, and a line-buffered stream reader so each
|
|
5
|
+
* provider's stream parser only has to know its own line format.
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_TIMEOUT_MS = 60_000;
|
|
8
|
+
const RETRY_DELAY_MS = 600;
|
|
9
|
+
/** A hung provider (dropped connection, stalled proxy) would otherwise hang
|
|
10
|
+
* forever — fetch() has no default timeout. Merges an optional caller signal
|
|
11
|
+
* (e.g. "abort because the component unmounted") with a hard ceiling. */
|
|
12
|
+
export function requestSignal(signal, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
13
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
14
|
+
return signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
15
|
+
}
|
|
16
|
+
function isRetryableStatus(status) {
|
|
17
|
+
return status === 429 || (status >= 500 && status < 600);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* fetch() with one retry on a 429/5xx or a network failure. An
|
|
21
|
+
* externally-aborted request (unmount, a newer request superseding this
|
|
22
|
+
* one) is never retried — that's a deliberate cancellation, not a
|
|
23
|
+
* transient failure, and retrying it would just start a request nobody
|
|
24
|
+
* wants the result of anymore.
|
|
25
|
+
*/
|
|
26
|
+
export async function fetchWithRetry(url, init, externalSignal, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
27
|
+
for (let attempt = 0;; attempt++) {
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch(url, { ...init, signal: requestSignal(externalSignal, timeoutMs) });
|
|
30
|
+
if (attempt === 0 && isRetryableStatus(res.status)) {
|
|
31
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
return res;
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
if (externalSignal?.aborted || attempt > 0)
|
|
38
|
+
throw err;
|
|
39
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Reject a remote Ollama URL that isn't HTTPS (localhost is exempt — that's
|
|
44
|
+
* the whole point of running it locally). Returns the normalised origin. */
|
|
45
|
+
export function validateOllamaUrl(url) {
|
|
46
|
+
let parsed;
|
|
47
|
+
try {
|
|
48
|
+
parsed = new URL(url);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
throw new Error(`Invalid Ollama URL: ${err.message}`, { cause: err });
|
|
52
|
+
}
|
|
53
|
+
const isLocalhost = ["localhost", "127.0.0.1", "::1"].includes(parsed.hostname);
|
|
54
|
+
if (!isLocalhost && parsed.protocol !== "https:") {
|
|
55
|
+
throw new Error("Remote Ollama must use HTTPS. For local testing, use http://localhost:11434");
|
|
56
|
+
}
|
|
57
|
+
return parsed.origin;
|
|
58
|
+
}
|
|
59
|
+
/** A 200 with no body (adblocker, misbehaving proxy) would otherwise throw a
|
|
60
|
+
* raw TypeError on getReader() deep inside the parser. */
|
|
61
|
+
function getBodyReader(res) {
|
|
62
|
+
if (!res.body)
|
|
63
|
+
throw new Error("Empty response body from AI provider.");
|
|
64
|
+
return res.body.getReader();
|
|
65
|
+
}
|
|
66
|
+
/** Feeds each complete line of a streamed response body to `onLine`, so
|
|
67
|
+
* every provider's stream parser only needs to know its own line format
|
|
68
|
+
* (SSE "data: " frames vs. bare NDJSON). */
|
|
69
|
+
export async function consumeLines(res, onLine) {
|
|
70
|
+
const reader = getBodyReader(res);
|
|
71
|
+
const decoder = new TextDecoder();
|
|
72
|
+
let buffer = "";
|
|
73
|
+
for (;;) {
|
|
74
|
+
const { done, value } = await reader.read();
|
|
75
|
+
if (done)
|
|
76
|
+
break;
|
|
77
|
+
buffer += decoder.decode(value, { stream: true });
|
|
78
|
+
const lines = buffer.split("\n");
|
|
79
|
+
buffer = lines.pop() ?? "";
|
|
80
|
+
for (const line of lines)
|
|
81
|
+
onLine(line);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Extract a provider's error message from a JSON error body, falling back
|
|
85
|
+
* to the raw HTTP status when the body isn't shaped as expected. */
|
|
86
|
+
export async function describeHttpError(res) {
|
|
87
|
+
const body = await res.json().catch(() => ({}));
|
|
88
|
+
const message = body?.error?.message;
|
|
89
|
+
return message ?? `HTTP ${res.status}`;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=http.js.map
|
package/dist/http.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* modeldispatcher-browser-agent — public API surface.
|
|
3
|
+
*
|
|
4
|
+
* A browser-native, bring-your-own-key AI agent core: the client-side
|
|
5
|
+
* sibling to `model-dispatcher` for apps with no backend to run the Python
|
|
6
|
+
* gateway on. Talks directly to Gemini/OpenAI/Anthropic/Groq/Ollama from the
|
|
7
|
+
* browser, using whatever key the end user supplies — no vendor SDK, no
|
|
8
|
+
* server, nothing sent anywhere but the provider itself.
|
|
9
|
+
*/
|
|
10
|
+
export { AllProvidersExhaustedError, complete, NoProviderConfiguredError, streamChat, streamComplete, } from "./agent.js";
|
|
11
|
+
export { clearConfig, DEFAULT_CONFIG_KEYS, isConfigReady, isProviderConfigured, loadConfig, resolveStorage, saveConfig, } from "./config.js";
|
|
12
|
+
export type { ConfigKeys, ConfigStorage } from "./config.js";
|
|
13
|
+
export { DEFAULT_TIMEOUT_MS, describeHttpError, validateOllamaUrl } from "./http.js";
|
|
14
|
+
export { EXTERNAL_CHAT_PROVIDERS, openExternalChat } from "./externalChat.js";
|
|
15
|
+
export type { OpenExternalChatDeps, OpenExternalChatResult } from "./externalChat.js";
|
|
16
|
+
export { DEFAULT_EXTERNAL_CHAT_FAVORITE_KEY, loadExternalChatFavorite, saveExternalChatFavorite, } from "./externalChatFavorite.js";
|
|
17
|
+
export { DEFAULT_DISPATCHER_FEATURES, resolveDispatcherFeatures } from "./features.js";
|
|
18
|
+
export type { DispatcherFeatureFlags } from "./features.js";
|
|
19
|
+
export { buildMessages } from "./messages.js";
|
|
20
|
+
export type { BuildMessagesOptions } from "./messages.js";
|
|
21
|
+
export { isKnownProvider, modelOptionsFor, MODEL_OPTIONS, PROVIDERS } from "./registry.js";
|
|
22
|
+
export type { AgentConfig, ChatMessage, ChatRole, ExternalChatProviderId, ExternalChatProviderInfo, ProviderCredential, ProviderId, ProviderInfo, RequestOptions, StreamOptions, } from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* modeldispatcher-browser-agent — public API surface.
|
|
3
|
+
*
|
|
4
|
+
* A browser-native, bring-your-own-key AI agent core: the client-side
|
|
5
|
+
* sibling to `model-dispatcher` for apps with no backend to run the Python
|
|
6
|
+
* gateway on. Talks directly to Gemini/OpenAI/Anthropic/Groq/Ollama from the
|
|
7
|
+
* browser, using whatever key the end user supplies — no vendor SDK, no
|
|
8
|
+
* server, nothing sent anywhere but the provider itself.
|
|
9
|
+
*/
|
|
10
|
+
export { AllProvidersExhaustedError, complete, NoProviderConfiguredError, streamChat, streamComplete, } from "./agent.js";
|
|
11
|
+
export { clearConfig, DEFAULT_CONFIG_KEYS, isConfigReady, isProviderConfigured, loadConfig, resolveStorage, saveConfig, } from "./config.js";
|
|
12
|
+
export { DEFAULT_TIMEOUT_MS, describeHttpError, validateOllamaUrl } from "./http.js";
|
|
13
|
+
export { EXTERNAL_CHAT_PROVIDERS, openExternalChat } from "./externalChat.js";
|
|
14
|
+
export { DEFAULT_EXTERNAL_CHAT_FAVORITE_KEY, loadExternalChatFavorite, saveExternalChatFavorite, } from "./externalChatFavorite.js";
|
|
15
|
+
export { DEFAULT_DISPATCHER_FEATURES, resolveDispatcherFeatures } from "./features.js";
|
|
16
|
+
export { buildMessages } from "./messages.js";
|
|
17
|
+
export { isKnownProvider, modelOptionsFor, MODEL_OPTIONS, PROVIDERS } from "./registry.js";
|
|
18
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,0BAA0B,EAC1B,QAAQ,EACR,yBAAyB,EACzB,UAAU,EACV,cAAc,GACf,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,aAAa,EACb,oBAAoB,EACpB,UAAU,EACV,cAAc,EACd,UAAU,GACX,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AACrF,OAAO,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE9E,OAAO,EACL,kCAAkC,EAClC,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,2BAA2B,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAEvF,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalise a UI's chat history into a provider-safe message list: strict
|
|
3
|
+
* role validation, length capping, and merging consecutive same-role
|
|
4
|
+
* messages — Anthropic and Gemini both require strict user/assistant
|
|
5
|
+
* alternation starting with "user", which raw UI state doesn't guarantee.
|
|
6
|
+
*/
|
|
7
|
+
import type { ChatMessage } from "./types.js";
|
|
8
|
+
export interface BuildMessagesOptions {
|
|
9
|
+
/** If true, always append a trailing filler user turn (e.g. to kick off a
|
|
10
|
+
* greeting flow) instead of only when the history would otherwise start
|
|
11
|
+
* with "assistant". */
|
|
12
|
+
forceTrailingFiller?: boolean;
|
|
13
|
+
/** The filler turn's content when one is needed to satisfy user-first
|
|
14
|
+
* ordering. Defaults to "begin". */
|
|
15
|
+
fillerContent?: string;
|
|
16
|
+
/** Marker content to drop entirely rather than treat as a real turn
|
|
17
|
+
* (e.g. a UI-internal "simulate opening" sentinel). */
|
|
18
|
+
dropContent?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Build a provider-safe chat history from UI messages: validates/coerces
|
|
22
|
+
* roles, caps message length, drops sentinel content, ensures user-first
|
|
23
|
+
* ordering, and merges consecutive same-role turns (required by
|
|
24
|
+
* Anthropic/Gemini's strict alternation).
|
|
25
|
+
*/
|
|
26
|
+
export declare function buildMessages(uiMessages: readonly ChatMessage[], options?: BuildMessagesOptions): ChatMessage[];
|
package/dist/messages.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalise a UI's chat history into a provider-safe message list: strict
|
|
3
|
+
* role validation, length capping, and merging consecutive same-role
|
|
4
|
+
* messages — Anthropic and Gemini both require strict user/assistant
|
|
5
|
+
* alternation starting with "user", which raw UI state doesn't guarantee.
|
|
6
|
+
*/
|
|
7
|
+
const VALID_TURN_ROLES = new Set(["user", "assistant"]);
|
|
8
|
+
const MAX_MESSAGE_LENGTH = 4000;
|
|
9
|
+
/**
|
|
10
|
+
* Build a provider-safe chat history from UI messages: validates/coerces
|
|
11
|
+
* roles, caps message length, drops sentinel content, ensures user-first
|
|
12
|
+
* ordering, and merges consecutive same-role turns (required by
|
|
13
|
+
* Anthropic/Gemini's strict alternation).
|
|
14
|
+
*/
|
|
15
|
+
export function buildMessages(uiMessages, options = {}) {
|
|
16
|
+
const { forceTrailingFiller = false, fillerContent = "begin", dropContent } = options;
|
|
17
|
+
let messages = uiMessages
|
|
18
|
+
.map(({ role, content }) => ({
|
|
19
|
+
role: VALID_TURN_ROLES.has(role) ? role : "user",
|
|
20
|
+
content: String(content ?? "").trim().slice(0, MAX_MESSAGE_LENGTH),
|
|
21
|
+
}))
|
|
22
|
+
.filter((m) => m.content.length > 0 && (dropContent === undefined || m.content !== dropContent));
|
|
23
|
+
if (forceTrailingFiller) {
|
|
24
|
+
messages = [...messages, { role: "user", content: fillerContent }];
|
|
25
|
+
}
|
|
26
|
+
else if (messages.length > 0 && messages[0]?.role === "assistant") {
|
|
27
|
+
messages = [{ role: "user", content: fillerContent }, ...messages];
|
|
28
|
+
}
|
|
29
|
+
const merged = [];
|
|
30
|
+
for (const message of messages) {
|
|
31
|
+
const last = merged.at(-1);
|
|
32
|
+
if (last && last.role === message.role) {
|
|
33
|
+
merged[merged.length - 1] = { role: last.role, content: `${last.content}\n\n${message.content}` };
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
merged.push(message);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return merged.length > 0 ? merged : [{ role: "user", content: fillerContent }];
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=messages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"messages.js","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAW,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;AAClE,MAAM,kBAAkB,GAAG,IAAI,CAAC;AAehC;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAC3B,UAAkC,EAClC,UAAgC,EAAE;IAElC,MAAM,EAAE,mBAAmB,GAAG,KAAK,EAAE,aAAa,GAAG,OAAO,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC;IAEtF,IAAI,QAAQ,GAAG,UAAU;SACtB,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAe,EAAE,CAAC,CAAC;QACxC,IAAI,EAAE,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM;QAChD,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,kBAAkB,CAAC;KACnE,CAAC,CAAC;SACF,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,WAAW,KAAK,SAAS,IAAI,CAAC,CAAC,OAAO,KAAK,WAAW,CAAC,CAAC,CAAC;IAEnG,IAAI,mBAAmB,EAAE,CAAC;QACxB,QAAQ,GAAG,CAAC,GAAG,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;IACrE,CAAC;SAAM,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,KAAK,WAAW,EAAE,CAAC;QACpE,QAAQ,GAAG,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,EAAE,GAAG,QAAQ,CAAC,CAAC;IACrE,CAAC;IAED,MAAM,MAAM,GAAkB,EAAE,CAAC;IACjC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,IAAI,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3B,IAAI,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,CAAC,IAAI,EAAE,CAAC;YACvC,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,OAAO,OAAO,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;QACpG,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACvB,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;AACjF,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic Claude — direct browser call via raw fetch, no SDK dependency.
|
|
3
|
+
* `anthropic-dangerous-direct-browser-access` is the header the official SDK
|
|
4
|
+
* sends under `dangerouslyAllowBrowser` — acceptable here because the user
|
|
5
|
+
* supplies their own key on their own machine; there is no backend to hide
|
|
6
|
+
* it behind in the first place.
|
|
7
|
+
*/
|
|
8
|
+
import type { ChatMessage, ProviderCallConfig, RequestOptions } from "../types.js";
|
|
9
|
+
export declare function completeAnthropic(cfg: ProviderCallConfig, messages: readonly ChatMessage[], options?: RequestOptions): Promise<string>;
|
|
10
|
+
export declare function streamAnthropic(cfg: ProviderCallConfig, messages: readonly ChatMessage[], onChunk: (fullTextSoFar: string) => void, options?: RequestOptions): Promise<string>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic Claude — direct browser call via raw fetch, no SDK dependency.
|
|
3
|
+
* `anthropic-dangerous-direct-browser-access` is the header the official SDK
|
|
4
|
+
* sends under `dangerouslyAllowBrowser` — acceptable here because the user
|
|
5
|
+
* supplies their own key on their own machine; there is no backend to hide
|
|
6
|
+
* it behind in the first place.
|
|
7
|
+
*/
|
|
8
|
+
import { consumeLines, describeHttpError, fetchWithRetry } from "../http.js";
|
|
9
|
+
const DEFAULT_MAX_TOKENS = 1024;
|
|
10
|
+
const ANTHROPIC_URL = "https://api.anthropic.com/v1/messages";
|
|
11
|
+
function toApiMessages(messages) {
|
|
12
|
+
return messages.filter((m) => m.role !== "system").map((m) => ({ role: m.role, content: m.content }));
|
|
13
|
+
}
|
|
14
|
+
function systemOf(messages, explicit) {
|
|
15
|
+
return explicit ?? messages.find((m) => m.role === "system")?.content;
|
|
16
|
+
}
|
|
17
|
+
function buildBody(cfg, messages, options, stream) {
|
|
18
|
+
const system = systemOf(messages, options.systemInstruction);
|
|
19
|
+
return {
|
|
20
|
+
model: cfg.model,
|
|
21
|
+
max_tokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
22
|
+
...(system ? { system } : {}),
|
|
23
|
+
messages: toApiMessages(messages),
|
|
24
|
+
...(stream ? { stream: true } : {}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function headers(cfg) {
|
|
28
|
+
return {
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
"x-api-key": cfg.apiKey,
|
|
31
|
+
"anthropic-version": "2023-06-01",
|
|
32
|
+
"anthropic-dangerous-direct-browser-access": "true",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export async function completeAnthropic(cfg, messages, options = {}) {
|
|
36
|
+
const res = await fetchWithRetry(ANTHROPIC_URL, { method: "POST", headers: headers(cfg), body: JSON.stringify(buildBody(cfg, messages, options, false)) }, options.signal);
|
|
37
|
+
if (!res.ok)
|
|
38
|
+
throw new Error(await describeHttpError(res));
|
|
39
|
+
const data = await res.json();
|
|
40
|
+
return data.content?.[0]?.text ?? "";
|
|
41
|
+
}
|
|
42
|
+
export async function streamAnthropic(cfg, messages, onChunk, options = {}) {
|
|
43
|
+
const res = await fetchWithRetry(ANTHROPIC_URL, { method: "POST", headers: headers(cfg), body: JSON.stringify(buildBody(cfg, messages, options, true)) }, options.signal);
|
|
44
|
+
if (!res.ok)
|
|
45
|
+
throw new Error(await describeHttpError(res));
|
|
46
|
+
let full = "";
|
|
47
|
+
await consumeLines(res, (line) => {
|
|
48
|
+
if (!line.startsWith("data: "))
|
|
49
|
+
return;
|
|
50
|
+
const raw = line.slice(6).trim();
|
|
51
|
+
try {
|
|
52
|
+
const event = JSON.parse(raw);
|
|
53
|
+
if (event.type === "content_block_delta" && event.delta?.type === "text_delta") {
|
|
54
|
+
full += event.delta.text;
|
|
55
|
+
onChunk(full);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Skip a malformed/partial SSE frame.
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return full;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=anthropic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anthropic.js","sourceRoot":"","sources":["../../src/providers/anthropic.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Gemini — direct browser call, no SDK. The key travels in a header
|
|
3
|
+
* (`x-goog-api-key`) rather than the `?key=` query string: a URL ends up in
|
|
4
|
+
* proxy/server access logs and browser history, a header does not.
|
|
5
|
+
*/
|
|
6
|
+
import type { ChatMessage, ProviderCallConfig, RequestOptions } from "../types.js";
|
|
7
|
+
export declare function completeGemini(cfg: ProviderCallConfig, messages: readonly ChatMessage[], options?: RequestOptions): Promise<string>;
|
|
8
|
+
export declare function streamGemini(cfg: ProviderCallConfig, messages: readonly ChatMessage[], onChunk: (fullTextSoFar: string) => void, options?: RequestOptions): Promise<string>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Gemini — direct browser call, no SDK. The key travels in a header
|
|
3
|
+
* (`x-goog-api-key`) rather than the `?key=` query string: a URL ends up in
|
|
4
|
+
* proxy/server access logs and browser history, a header does not.
|
|
5
|
+
*/
|
|
6
|
+
import { consumeLines, describeHttpError, fetchWithRetry } from "../http.js";
|
|
7
|
+
const DEFAULT_MAX_TOKENS = 1024;
|
|
8
|
+
function toContents(messages) {
|
|
9
|
+
return messages
|
|
10
|
+
.filter((m) => m.role !== "system")
|
|
11
|
+
.map((m) => ({ role: m.role === "assistant" ? "model" : "user", parts: [{ text: m.content }] }));
|
|
12
|
+
}
|
|
13
|
+
function systemInstructionOf(messages, explicit) {
|
|
14
|
+
return explicit ?? messages.find((m) => m.role === "system")?.content;
|
|
15
|
+
}
|
|
16
|
+
function buildBody(messages, options) {
|
|
17
|
+
const system = systemInstructionOf(messages, options.systemInstruction);
|
|
18
|
+
return {
|
|
19
|
+
contents: toContents(messages),
|
|
20
|
+
generationConfig: {
|
|
21
|
+
maxOutputTokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
22
|
+
...(options.jsonMode ? { responseMimeType: "application/json" } : {}),
|
|
23
|
+
},
|
|
24
|
+
...(system ? { systemInstruction: { parts: [{ text: system }] } } : {}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export async function completeGemini(cfg, messages, options = {}) {
|
|
28
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(cfg.model)}:generateContent`;
|
|
29
|
+
const res = await fetchWithRetry(url, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { "Content-Type": "application/json", "x-goog-api-key": cfg.apiKey },
|
|
32
|
+
body: JSON.stringify(buildBody(messages, options)),
|
|
33
|
+
}, options.signal);
|
|
34
|
+
if (!res.ok)
|
|
35
|
+
throw new Error(await describeHttpError(res));
|
|
36
|
+
const data = await res.json();
|
|
37
|
+
return data.candidates?.[0]?.content?.parts?.[0]?.text ?? "";
|
|
38
|
+
}
|
|
39
|
+
export async function streamGemini(cfg, messages, onChunk, options = {}) {
|
|
40
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(cfg.model)}:streamGenerateContent?alt=sse`;
|
|
41
|
+
const res = await fetchWithRetry(url, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: { "Content-Type": "application/json", "x-goog-api-key": cfg.apiKey },
|
|
44
|
+
body: JSON.stringify(buildBody(messages, options)),
|
|
45
|
+
}, options.signal);
|
|
46
|
+
if (!res.ok)
|
|
47
|
+
throw new Error(await describeHttpError(res));
|
|
48
|
+
let full = "";
|
|
49
|
+
await consumeLines(res, (line) => {
|
|
50
|
+
if (!line.startsWith("data: "))
|
|
51
|
+
return;
|
|
52
|
+
const raw = line.slice(6).trim();
|
|
53
|
+
try {
|
|
54
|
+
const text = JSON.parse(raw).candidates?.[0]?.content?.parts?.[0]?.text;
|
|
55
|
+
if (text) {
|
|
56
|
+
full += text;
|
|
57
|
+
onChunk(full);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Skip a malformed/partial SSE frame — the next line may complete it.
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
return full;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=gemini.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gemini.js","sourceRoot":"","sources":["../../src/providers/gemini.ts"],"names":[],"mappings":"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"}
|