modeldispatcher-browser-agent 0.6.6 → 0.7.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.
@@ -61,4 +61,86 @@ export async function streamOpenAICompatible(provider, cfg, messages, onChunk, o
61
61
  });
62
62
  return full;
63
63
  }
64
+ function toToolApiMessages(messages, systemInstruction) {
65
+ const hasSystemTurn = messages.some((m) => m.role === "system");
66
+ const system = !hasSystemTurn && systemInstruction ? [{ role: "system", content: systemInstruction }] : [];
67
+ return [
68
+ ...system,
69
+ ...messages.flatMap((m) => {
70
+ if (m.role === "tool") {
71
+ return m.results.map((r) => ({ role: "tool", tool_call_id: r.callId, content: r.content }));
72
+ }
73
+ if ("toolCalls" in m) {
74
+ return [
75
+ {
76
+ role: "assistant",
77
+ content: m.content || null,
78
+ tool_calls: m.toolCalls.map((c) => ({
79
+ id: c.id,
80
+ type: "function",
81
+ function: { name: c.name, arguments: JSON.stringify(c.arguments) },
82
+ })),
83
+ },
84
+ ];
85
+ }
86
+ return [{ role: m.role, content: m.content }];
87
+ }),
88
+ ];
89
+ }
90
+ /** Parse the model's tool-call arguments: a JSON string that may be empty. */
91
+ export function parseArguments(raw, toolName) {
92
+ if (!raw || !raw.trim())
93
+ return {};
94
+ let parsed;
95
+ try {
96
+ parsed = JSON.parse(raw);
97
+ }
98
+ catch {
99
+ throw new Error(`Tool call ${toolName} sent arguments that are not valid JSON`);
100
+ }
101
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
102
+ throw new Error(`Tool call ${toolName} sent arguments that are not a JSON object`);
103
+ }
104
+ return parsed;
105
+ }
106
+ /** One non-streaming turn with tools available (OpenAI or Groq). */
107
+ export async function completeWithToolsOpenAICompatible(provider, cfg, messages, tools, options = {}) {
108
+ const res = await fetchWithRetry(BASE_URLS[provider], {
109
+ method: "POST",
110
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.apiKey}` },
111
+ body: JSON.stringify({
112
+ model: cfg.model,
113
+ max_tokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,
114
+ messages: toToolApiMessages(messages, options.systemInstruction),
115
+ ...(tools.length
116
+ ? {
117
+ tools: tools.map((t) => ({
118
+ type: "function",
119
+ function: { name: t.name, description: t.description, parameters: t.parameters },
120
+ })),
121
+ }
122
+ : {}),
123
+ }),
124
+ }, options.signal);
125
+ if (!res.ok)
126
+ throw new Error(await describeHttpError(res));
127
+ const data = await res.json();
128
+ const choice = data.choices?.[0];
129
+ if (!choice?.message)
130
+ throw new Error(`${provider} returned no answer`);
131
+ if (choice.finish_reason === "content_filter")
132
+ throw new Error(`${provider} filtered the answer`);
133
+ const message = choice.message;
134
+ const calls = message.tool_calls ?? [];
135
+ if (choice.finish_reason === "length" && calls.length) {
136
+ throw new Error(`${provider} ran out of tokens in the middle of a tool call`);
137
+ }
138
+ return {
139
+ text: message.content ?? "",
140
+ toolCalls: calls.map((c) => {
141
+ const name = c.function?.name ?? "";
142
+ return { id: c.id ?? "", name, arguments: parseArguments(c.function?.arguments, name) };
143
+ }),
144
+ };
145
+ }
64
146
  //# sourceMappingURL=openaiCompatible.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"openaiCompatible.js","sourceRoot":"","sources":["../../src/providers/openaiCompatible.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"openaiCompatible.js","sourceRoot":"","sources":["../../src/providers/openaiCompatible.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAU7E,MAAM,kBAAkB,GAAG,IAAI,CAAC;AAEhC,MAAM,SAAS,GAAsC;IACnD,MAAM,EAAE,4CAA4C;IACpD,IAAI,EAAE,iDAAiD;CACxD,CAAC;AAEF,SAAS,aAAa,CAAC,QAAgC,EAAE,iBAA0B;IACjF,MAAM,aAAa,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC;IAChE,MAAM,MAAM,GAAG,CAAC,aAAa,IAAI,iBAAiB,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC3G,OAAO,CAAC,GAAG,MAAM,EAAE,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;AACrF,CAAC;AAED,SAAS,SAAS,CAAC,GAAuB,EAAE,QAAgC,EAAE,OAAuB,EAAE,MAAe;IACpH,OAAO;QACL,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,UAAU,EAAE,OAAO,CAAC,SAAS,IAAI,kBAAkB;QACnD,QAAQ,EAAE,aAAa,CAAC,QAAQ,EAAE,OAAO,CAAC,iBAAiB,CAAC;QAC5D,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACpC,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,OAAO,CACpB,QAA2B,EAC3B,GAAuB,EACvB,QAAgC,EAChC,OAAuB,EACvB,MAAe;IAEf,MAAM,GAAG,GAAG,MAAM,cAAc,CAC9B,SAAS,CAAC,QAAQ,CAAC,EACnB;QACE,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,aAAa,EAAE,UAAU,GAAG,CAAC,MAAM,EAAE,EAAE;QACtF,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;KAChE,EACD,OAAO,CAAC,MAAM,CACf,CAAC;IACF,IAAI,CAAC,GAAG,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,MAAM,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3D,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,QAA2B,EAC3B,GAAuB,EACvB,QAAgC,EAChC,UAA0B,EAAE;IAE5B,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,QAAQ,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;IACnE,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;IAC9B,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,IAAI,EAAE,CAAC;AACnD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,QAA2B,EAC3B,GAAuB,EACvB,QAAgC,EAChC,OAAwC,EACxC,UAA0B,EAAE;IAE5B,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,QAAQ,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IAClE,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,MAAM,YAAY,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE;QAC/B,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;YAAE,OAAO;QACvC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACjC,IAAI,GAAG,KAAK,QAAQ;YAAE,OAAO;QAC7B,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC;YAC3D,IAAI,KAAK,EAAE,CAAC;gBACV,IAAI,IAAI,KAAK,CAAC;gBACd,OAAO,CAAC,IAAI,CAAC,CAAC;YAChB,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,sCAAsC;QACxC,CAAC;IACH,CAAC,CAAC,CAAC;IACH,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,iBAAiB,CAAC,QAAoC,EAAE,iBAA0B;IACzF,MAAM,aAAa,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC;IAChE,MAAM,MAAM,GAAG,CAAC,aAAa,IAAI,iBAAiB,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC3G,OAAO;QACL,GAAG,MAAM;QACT,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAA6B,EAAE;YACnD,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;gBACtB,OAAO,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YAC9F,CAAC;YACD,IAAI,WAAW,IAAI,CAAC,EAAE,CAAC;gBACrB,OAAO;oBACL;wBACE,IAAI,EAAE,WAAW;wBACjB,OAAO,EAAE,CAAC,CAAC,OAAO,IAAI,IAAI;wBAC1B,UAAU,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;4BAClC,EAAE,EAAE,CAAC,CAAC,EAAE;4BACR,IAAI,EAAE,UAAU;4BAChB,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE;yBACnE,CAAC,CAAC;qBACJ;iBACF,CAAC;YACJ,CAAC;YACD,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QAChD,CAAC,CAAC;KACH,CAAC;AACJ,CAAC;AAOD,8EAA8E;AAC9E,MAAM,UAAU,cAAc,CAAC,GAAuB,EAAE,QAAgB;IACtE,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,CAAC;IACnC,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,aAAa,QAAQ,yCAAyC,CAAC,CAAC;IAClF,CAAC;IACD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3E,MAAM,IAAI,KAAK,CAAC,aAAa,QAAQ,4CAA4C,CAAC,CAAC;IACrF,CAAC;IACD,OAAO,MAAiC,CAAC;AAC3C,CAAC;AAED,oEAAoE;AACpE,MAAM,CAAC,KAAK,UAAU,iCAAiC,CACrD,QAA2B,EAC3B,GAAuB,EACvB,QAAoC,EACpC,KAA0B,EAC1B,UAA0B,EAAE;IAE5B,MAAM,GAAG,GAAG,MAAM,cAAc,CAC9B,SAAS,CAAC,QAAQ,CAAC,EACnB;QACE,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,aAAa,EAAE,UAAU,GAAG,CAAC,MAAM,EAAE,EAAE;QACtF,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,KAAK,EAAE,GAAG,CAAC,KAAK;YAChB,UAAU,EAAE,OAAO,CAAC,SAAS,IAAI,kBAAkB;YACnD,QAAQ,EAAE,iBAAiB,CAAC,QAAQ,EAAE,OAAO,CAAC,iBAAiB,CAAC;YAChE,GAAG,CAAC,KAAK,CAAC,MAAM;gBACd,CAAC,CAAC;oBACE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;wBACvB,IAAI,EAAE,UAAU;wBAChB,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC,WAAW,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE;qBACjF,CAAC,CAAC;iBACJ;gBACH,CAAC,CAAC,EAAE,CAAC;SACR,CAAC;KACH,EACD,OAAO,CAAC,MAAM,CACf,CAAC;IACF,IAAI,CAAC,GAAG,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,MAAM,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3D,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;IAC9B,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;IACjC,IAAI,CAAC,MAAM,EAAE,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,QAAQ,qBAAqB,CAAC,CAAC;IACxE,IAAI,MAAM,CAAC,aAAa,KAAK,gBAAgB;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,QAAQ,sBAAsB,CAAC,CAAC;IAClG,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;IAC/B,MAAM,KAAK,GAAqB,OAAO,CAAC,UAAU,IAAI,EAAE,CAAC;IACzD,IAAI,MAAM,CAAC,aAAa,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QACtD,MAAM,IAAI,KAAK,CAAC,GAAG,QAAQ,iDAAiD,CAAC,CAAC;IAChF,CAAC;IACD,OAAO;QACL,IAAI,EAAE,OAAO,CAAC,OAAO,IAAI,EAAE;QAC3B,SAAS,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YACzB,MAAM,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC;YACpC,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,cAAc,CAAC,CAAC,CAAC,QAAQ,EAAE,SAAS,EAAE,IAAI,CAAC,EAAE,CAAC;QAC1F,CAAC,CAAC;KACH,CAAC;AACJ,CAAC"}
@@ -0,0 +1,4 @@
1
+ /** Helpers for the neutral tool-conversation history ({@link ToolChatMessage}). */
2
+ import type { ToolChatMessage } from "./types.js";
3
+ /** The content of the history's system turn, if it has one. */
4
+ export declare function systemTurnOf(messages: readonly ToolChatMessage[]): string | undefined;
@@ -0,0 +1,10 @@
1
+ /** Helpers for the neutral tool-conversation history ({@link ToolChatMessage}). */
2
+ /** The content of the history's system turn, if it has one. */
3
+ export function systemTurnOf(messages) {
4
+ for (const m of messages) {
5
+ if (m.role === "system")
6
+ return m.content;
7
+ }
8
+ return undefined;
9
+ }
10
+ //# sourceMappingURL=toolMessages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toolMessages.js","sourceRoot":"","sources":["../src/toolMessages.ts"],"names":[],"mappings":"AAAA,mFAAmF;AAInF,+DAA+D;AAC/D,MAAM,UAAU,YAAY,CAAC,QAAoC;IAC/D,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACzB,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,CAAC,CAAC,OAAO,CAAC;IAC5C,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Tool calling in the browser: the model asks for tools, the app runs them
3
+ * on the device, and the results go back until the model answers. The same
4
+ * bounded loop as the Python core's `AgentLoop`, client-side.
5
+ *
6
+ * - runWithTools() — a chat history and a set of tools in, the final answer
7
+ * and the tools used out
8
+ *
9
+ * Each turn is dispatched through the same fallback list as `complete()`, so
10
+ * a failing provider hands the run to the next one mid-conversation: the
11
+ * history is kept in a neutral form ({@link ToolChatMessage}) and translated
12
+ * per provider on every turn. Non-streaming: a turn may end in tool calls,
13
+ * so there is no single text stream to show.
14
+ */
15
+ import type { AgentConfig, ProviderId, RequestOptions, ToolCall, ToolChatMessage, ToolResult, ToolSpec } from "./types.js";
16
+ /** A tool the app runs itself: its declaration and its implementation. */
17
+ export interface Tool {
18
+ readonly spec: ToolSpec;
19
+ /** Runs on the device with the model's decoded arguments; its return value
20
+ * (or the message of what it throws) is what the model reads. */
21
+ readonly run: (args: Readonly<Record<string, unknown>>) => string | Promise<string>;
22
+ }
23
+ export interface RunWithToolsOptions extends Omit<RequestOptions, "jsonMode"> {
24
+ readonly tools: readonly Tool[];
25
+ /** Model turns allowed before giving up (each may request several tools).
26
+ * Defaults to {@link DEFAULT_MAX_STEPS}. */
27
+ readonly maxSteps?: number;
28
+ /** Called after each tool runs, e.g. to show "looked up AAPL" while waiting. */
29
+ readonly onToolResult?: (call: ToolCall, result: ToolResult) => void;
30
+ }
31
+ export type ToolRunStopReason = "completed" | "max_steps";
32
+ export interface ToolRunResult {
33
+ /** The model's final answer; may be empty when `stopReason` is "max_steps". */
34
+ readonly text: string;
35
+ readonly stopReason: ToolRunStopReason;
36
+ /** Names of the tools run, in order, repeats included. */
37
+ readonly toolsUsed: readonly string[];
38
+ /** The provider and model that produced the final turn. */
39
+ readonly provider: ProviderId;
40
+ readonly model: string;
41
+ /** The whole conversation including tool turns, to continue it later. */
42
+ readonly messages: readonly ToolChatMessage[];
43
+ }
44
+ export declare const DEFAULT_MAX_STEPS = 6;
45
+ /**
46
+ * Run a conversation in which the model may call `options.tools`, until it
47
+ * answers without asking for one or `maxSteps` turns pass. Tools run one at
48
+ * a time, in the order asked, so a write lands before a later read.
49
+ *
50
+ * Raises:
51
+ * NoProviderConfiguredError: `cfg.providers` has no usable credential.
52
+ * AllProvidersExhaustedError: every candidate failed on some turn.
53
+ */
54
+ export declare function runWithTools(cfg: AgentConfig, messages: readonly ToolChatMessage[], options: RunWithToolsOptions): Promise<ToolRunResult>;
package/dist/tools.js ADDED
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Tool calling in the browser: the model asks for tools, the app runs them
3
+ * on the device, and the results go back until the model answers. The same
4
+ * bounded loop as the Python core's `AgentLoop`, client-side.
5
+ *
6
+ * - runWithTools() — a chat history and a set of tools in, the final answer
7
+ * and the tools used out
8
+ *
9
+ * Each turn is dispatched through the same fallback list as `complete()`, so
10
+ * a failing provider hands the run to the next one mid-conversation: the
11
+ * history is kept in a neutral form ({@link ToolChatMessage}) and translated
12
+ * per provider on every turn. Non-streaming: a turn may end in tool calls,
13
+ * so there is no single text stream to show.
14
+ */
15
+ import { AllProvidersExhaustedError, candidatesOf, NoProviderConfiguredError, } from "./agent.js";
16
+ import { completeWithToolsAnthropic } from "./providers/anthropic.js";
17
+ import { completeWithToolsGemini } from "./providers/gemini.js";
18
+ import { completeWithToolsOllama } from "./providers/ollama.js";
19
+ import { completeWithToolsOpenAICompatible } from "./providers/openaiCompatible.js";
20
+ export const DEFAULT_MAX_STEPS = 6;
21
+ function completeTurn(candidate, messages, tools, options) {
22
+ switch (candidate.provider) {
23
+ case "gemini":
24
+ return completeWithToolsGemini(candidate, messages, tools, options);
25
+ case "anthropic":
26
+ return completeWithToolsAnthropic(candidate, messages, tools, options);
27
+ case "ollama":
28
+ return completeWithToolsOllama(candidate, messages, tools, options);
29
+ case "openai":
30
+ case "groq":
31
+ return completeWithToolsOpenAICompatible(candidate.provider, candidate, messages, tools, options);
32
+ }
33
+ }
34
+ async function dispatchTurn(candidates, messages, tools, options) {
35
+ const attempts = [];
36
+ for (const candidate of candidates) {
37
+ try {
38
+ return { turn: await completeTurn(candidate, messages, tools, options), candidate };
39
+ }
40
+ catch (err) {
41
+ if (options.signal?.aborted)
42
+ throw err;
43
+ attempts.push({ provider: candidate.provider, error: messageOf(err) });
44
+ }
45
+ }
46
+ throw new AllProvidersExhaustedError(attempts);
47
+ }
48
+ /**
49
+ * Give calls without a provider id (Gemini, Ollama) one unique in the whole
50
+ * conversation, counting the calls already in it: a continued conversation
51
+ * must not repeat an id (Anthropic rejects duplicates).
52
+ */
53
+ function numberCalls(calls, history) {
54
+ let next = history.reduce((n, m) => n + ("toolCalls" in m ? m.toolCalls.length : 0), 0);
55
+ return calls.map((call) => (call.id ? call : { ...call, id: `call_${next++}` }));
56
+ }
57
+ function messageOf(err) {
58
+ return err instanceof Error ? err.message : String(err);
59
+ }
60
+ /**
61
+ * The system prompt for every turn, decided once so a fallback doesn't change
62
+ * it: the explicit option, else every system turn in the history, joined.
63
+ */
64
+ function systemOf(messages, explicit) {
65
+ if (explicit !== undefined)
66
+ return explicit;
67
+ const turns = messages.flatMap((m) => (m.role === "system" ? [m.content] : []));
68
+ return turns.length ? turns.join("\n\n") : undefined;
69
+ }
70
+ /** What goes to the provider: no system turns (sent as the instruction) and no
71
+ * empty plain turns, which Anthropic and Gemini reject. */
72
+ function sendable(history) {
73
+ return history.filter((m) => m.role !== "system" && (m.role === "tool" || "toolCalls" in m || m.content !== ""));
74
+ }
75
+ async function runTool(tools, call) {
76
+ const tool = tools.get(call.name);
77
+ if (!tool)
78
+ return { callId: call.id, name: call.name, content: `unknown tool: ${call.name}`, isError: true };
79
+ try {
80
+ return { callId: call.id, name: call.name, content: await tool.run(call.arguments), isError: false };
81
+ }
82
+ catch (err) {
83
+ // Handed back to the model, which can retry or explain, as the Python core does.
84
+ return { callId: call.id, name: call.name, content: `tool error: ${messageOf(err)}`, isError: true };
85
+ }
86
+ }
87
+ /**
88
+ * Run a conversation in which the model may call `options.tools`, until it
89
+ * answers without asking for one or `maxSteps` turns pass. Tools run one at
90
+ * a time, in the order asked, so a write lands before a later read.
91
+ *
92
+ * Raises:
93
+ * NoProviderConfiguredError: `cfg.providers` has no usable credential.
94
+ * AllProvidersExhaustedError: every candidate failed on some turn.
95
+ */
96
+ export async function runWithTools(cfg, messages, options) {
97
+ const candidates = candidatesOf(cfg);
98
+ if (candidates.length === 0)
99
+ throw new NoProviderConfiguredError();
100
+ const { tools, maxSteps = DEFAULT_MAX_STEPS, onToolResult, systemInstruction, ...rest } = options;
101
+ const system = systemOf(messages, systemInstruction);
102
+ const requestOptions = system === undefined ? rest : { ...rest, systemInstruction: system };
103
+ const specs = tools.map((t) => t.spec);
104
+ const byName = new Map(tools.map((t) => [t.spec.name, t]));
105
+ const history = [...messages];
106
+ const toolsUsed = [];
107
+ let last;
108
+ for (let step = 0; step < maxSteps; step++) {
109
+ last = await dispatchTurn(candidates, sendable(history), specs, requestOptions);
110
+ const calls = numberCalls(last.turn.toolCalls, history);
111
+ if (calls.length === 0) {
112
+ if (last.turn.text)
113
+ history.push({ role: "assistant", content: last.turn.text });
114
+ return result(last, "completed", toolsUsed, history);
115
+ }
116
+ history.push({ role: "assistant", content: last.turn.text, toolCalls: calls });
117
+ const results = [];
118
+ for (const call of calls) {
119
+ const outcome = await runTool(byName, call);
120
+ toolsUsed.push(call.name);
121
+ results.push(outcome);
122
+ onToolResult?.(call, outcome);
123
+ }
124
+ history.push({ role: "tool", results });
125
+ }
126
+ if (!last)
127
+ throw new RangeError(`maxSteps must be at least 1, got ${maxSteps}`);
128
+ return result(last, "max_steps", toolsUsed, history);
129
+ }
130
+ function result(last, stopReason, toolsUsed, history) {
131
+ return {
132
+ text: last.turn.text,
133
+ stopReason,
134
+ toolsUsed,
135
+ provider: last.candidate.provider,
136
+ model: last.candidate.model,
137
+ messages: history,
138
+ };
139
+ }
140
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -59,6 +59,57 @@ export interface ChatMessage {
59
59
  readonly role: ChatRole;
60
60
  readonly content: string;
61
61
  }
62
+ /**
63
+ * A tool the model may call, declared by name, purpose, and a JSON Schema for
64
+ * its arguments — the same shape as the Python core's `ToolSpec`. Keep
65
+ * `parameters` to the schema subset every provider accepts (`type`,
66
+ * `properties`, `required`, `description`, `enum`, `items`); Gemini rejects
67
+ * the rest, so `additionalProperties` and `$schema` are dropped for it.
68
+ */
69
+ export interface ToolSpec {
70
+ readonly name: string;
71
+ readonly description: string;
72
+ readonly parameters: Readonly<Record<string, unknown>>;
73
+ }
74
+ /** A model's request to run one tool, decoded from a provider response. */
75
+ export interface ToolCall {
76
+ /** The provider's call id, or one generated for providers that send none
77
+ * (Gemini, Ollama). Pairs the call with its {@link ToolResult}. */
78
+ readonly id: string;
79
+ readonly name: string;
80
+ readonly arguments: Readonly<Record<string, unknown>>;
81
+ /** Gemini's `thoughtSignature`, echoed back on the next turn as its
82
+ * thinking models require. Absent for every other provider. */
83
+ readonly signature?: string;
84
+ }
85
+ /** What running a {@link ToolCall} produced, fed back to the model. */
86
+ export interface ToolResult {
87
+ readonly callId: string;
88
+ readonly name: string;
89
+ readonly content: string;
90
+ /** True when the tool threw or was unknown: the model sees the error text
91
+ * and can recover, instead of the whole run failing. */
92
+ readonly isError: boolean;
93
+ }
94
+ /**
95
+ * One turn of a tool-using conversation: a plain {@link ChatMessage}, the
96
+ * assistant asking for tools, or the results of every tool it asked for in
97
+ * that turn (kept together because Anthropic and Gemini want them in a
98
+ * single turn).
99
+ */
100
+ export type ToolChatMessage = ChatMessage | {
101
+ readonly role: "assistant";
102
+ readonly content: string;
103
+ readonly toolCalls: readonly ToolCall[];
104
+ } | {
105
+ readonly role: "tool";
106
+ readonly results: readonly ToolResult[];
107
+ };
108
+ /** One model turn in a tool loop: text, tool calls, or both. */
109
+ export interface ToolTurn {
110
+ readonly text: string;
111
+ readonly toolCalls: readonly ToolCall[];
112
+ }
62
113
  /** Options shared by both complete() and streamComplete()/streamChat(). */
63
114
  export interface RequestOptions {
64
115
  /** System prompt / instruction, applied via each provider's native mechanism. */
@@ -81,7 +132,7 @@ export interface StreamOptions extends RequestOptions {
81
132
  /** A free, public AI chat web product `openExternalChat` can hand a question
82
133
  * off to — the no-API-key escape hatch, distinct from the `ProviderId`s above
83
134
  * (which all require a BYOK credential). */
84
- export type ExternalChatProviderId = "chatgpt" | "claude" | "gemini" | "groq";
135
+ export type ExternalChatProviderId = "chatgpt" | "claude" | "gemini" | "geminiApp" | "groq";
85
136
  /** Static metadata about one external chat provider. */
86
137
  export interface ExternalChatProviderInfo {
87
138
  readonly id: ExternalChatProviderId;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modeldispatcher-browser-agent",
3
- "version": "0.6.6",
3
+ "version": "0.7.0",
4
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
5
  "license": "MIT",
6
6
  "repository": {
@@ -40,6 +40,6 @@
40
40
  ],
41
41
  "devDependencies": {
42
42
  "typescript": "^5.5.0",
43
- "vitest": "^2.0.0"
43
+ "vitest": "^5.1.0"
44
44
  }
45
45
  }
package/src/agent.ts CHANGED
@@ -29,7 +29,7 @@ import type {
29
29
 
30
30
  /** One candidate that failed, kept only to compose
31
31
  * {@link AllProvidersExhaustedError}'s message. */
32
- interface FailedAttempt {
32
+ export interface FailedAttempt {
33
33
  readonly provider: string;
34
34
  readonly error: string;
35
35
  }
@@ -64,7 +64,7 @@ export class AllProvidersExhaustedError extends Error {
64
64
  * provider with no usable credential is never offered as a candidate at
65
65
  * all, the same rule the Python core's registry-building applies.
66
66
  */
67
- function candidatesOf(cfg: AgentConfig): ProviderCallConfig[] {
67
+ export function candidatesOf(cfg: AgentConfig): ProviderCallConfig[] {
68
68
  const candidates: ProviderCallConfig[] = [];
69
69
  for (const cred of cfg.providers) {
70
70
  if (cred.provider === "ollama") {
@@ -39,6 +39,15 @@ export const EXTERNAL_CHAT_PROVIDERS: Record<ExternalChatProviderId, ExternalCha
39
39
  // "ask Google's AI this" — that's what this actually opens.
40
40
  buildUrl: (question) => `https://www.google.com/search?${new URLSearchParams({ q: question, udm: "50" })}`,
41
41
  },
42
+ geminiApp: {
43
+ id: "geminiApp",
44
+ name: "Gemini",
45
+ homeUrl: "https://gemini.google.com/app",
46
+ // The real Gemini app, distinct from the AI-Mode entry above. It has no
47
+ // known query-prefill parameter — opens the plain app; the question
48
+ // still goes to the clipboard.
49
+ buildUrl: null,
50
+ },
42
51
  groq: {
43
52
  id: "groq",
44
53
  name: "Groq",
package/src/index.ts CHANGED
@@ -38,6 +38,8 @@ export type { DispatcherFeatureFlags } from "./features.js";
38
38
  export { buildMessages } from "./messages.js";
39
39
  export type { BuildMessagesOptions } from "./messages.js";
40
40
  export { isKnownProvider, modelOptionsFor, MODEL_OPTIONS, PROVIDERS } from "./registry.js";
41
+ export { DEFAULT_MAX_STEPS, runWithTools } from "./tools.js";
42
+ export type { RunWithToolsOptions, Tool, ToolRunResult, ToolRunStopReason } from "./tools.js";
41
43
  export type {
42
44
  AgentConfig,
43
45
  ChatMessage,
@@ -49,4 +51,9 @@ export type {
49
51
  ProviderInfo,
50
52
  RequestOptions,
51
53
  StreamOptions,
54
+ ToolCall,
55
+ ToolChatMessage,
56
+ ToolResult,
57
+ ToolSpec,
58
+ ToolTurn,
52
59
  } from "./types.js";
@@ -6,8 +6,16 @@
6
6
  * it behind in the first place.
7
7
  */
8
8
 
9
+ import { systemTurnOf } from "../toolMessages.js";
9
10
  import { consumeLines, describeHttpError, fetchWithRetry } from "../http.js";
10
- import type { ChatMessage, ProviderCallConfig, RequestOptions } from "../types.js";
11
+ import type {
12
+ ChatMessage,
13
+ ProviderCallConfig,
14
+ RequestOptions,
15
+ ToolChatMessage,
16
+ ToolSpec,
17
+ ToolTurn,
18
+ } from "../types.js";
11
19
 
12
20
  const DEFAULT_MAX_TOKENS = 1024;
13
21
  const ANTHROPIC_URL = "https://api.anthropic.com/v1/messages";
@@ -84,3 +92,84 @@ export async function streamAnthropic(
84
92
  });
85
93
  return full;
86
94
  }
95
+
96
+ function toToolApiMessages(messages: readonly ToolChatMessage[]) {
97
+ return messages
98
+ .filter((m) => m.role !== "system")
99
+ .map((m) => {
100
+ if (m.role === "tool") {
101
+ return {
102
+ role: "user",
103
+ content: m.results.map((r) => ({
104
+ type: "tool_result",
105
+ tool_use_id: r.callId,
106
+ content: r.content,
107
+ ...(r.isError ? { is_error: true } : {}),
108
+ })),
109
+ };
110
+ }
111
+ if ("toolCalls" in m) {
112
+ return {
113
+ role: "assistant",
114
+ content: [
115
+ ...(m.content ? [{ type: "text", text: m.content }] : []),
116
+ ...m.toolCalls.map((c) => ({ type: "tool_use", id: c.id, name: c.name, input: c.arguments })),
117
+ ],
118
+ };
119
+ }
120
+ return { role: m.role, content: m.content };
121
+ });
122
+ }
123
+
124
+ interface AnthropicBlock {
125
+ type?: string;
126
+ text?: string;
127
+ id?: string;
128
+ name?: string;
129
+ input?: Record<string, unknown>;
130
+ }
131
+
132
+ /** One non-streaming turn with tools available. */
133
+ export async function completeWithToolsAnthropic(
134
+ cfg: ProviderCallConfig,
135
+ messages: readonly ToolChatMessage[],
136
+ tools: readonly ToolSpec[],
137
+ options: RequestOptions = {},
138
+ ): Promise<ToolTurn> {
139
+ const system = options.systemInstruction ?? systemTurnOf(messages);
140
+ const res = await fetchWithRetry(
141
+ ANTHROPIC_URL,
142
+ {
143
+ method: "POST",
144
+ headers: headers(cfg),
145
+ body: JSON.stringify({
146
+ model: cfg.model,
147
+ max_tokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,
148
+ ...(system ? { system } : {}),
149
+ messages: toToolApiMessages(messages),
150
+ ...(tools.length
151
+ ? { tools: tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.parameters })) }
152
+ : {}),
153
+ }),
154
+ },
155
+ options.signal,
156
+ );
157
+ if (!res.ok) throw new Error(await describeHttpError(res));
158
+ const data = await res.json();
159
+ const blocks: AnthropicBlock[] = data.content ?? [];
160
+ if (data.stop_reason === "refusal") throw new Error("Anthropic declined to answer");
161
+ if (data.stop_reason === "max_tokens" && blocks.some((b) => b.type === "tool_use")) {
162
+ // The tool input may be cut off: running it would act on partial arguments.
163
+ throw new Error("Anthropic ran out of tokens in the middle of a tool call");
164
+ }
165
+ if (blocks.length === 0) throw new Error(`Anthropic returned an empty turn (${data.stop_reason ?? "no reason"})`);
166
+ return {
167
+ text: blocks
168
+ .filter((b) => b.type === "text")
169
+ .map((b) => b.text ?? "")
170
+ .join(""),
171
+ toolCalls: blocks
172
+ .filter((b) => b.type === "tool_use")
173
+ .map((b) => ({ id: b.id ?? "", name: b.name ?? "", arguments: b.input ?? {} })),
174
+ };
175
+ }