@jussmor/sdk-ai 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/dist/conversation-kIkMQdYK.d.cts +105 -0
  2. package/dist/conversation-kIkMQdYK.d.ts +105 -0
  3. package/dist/conversation-store-CAyPuBjk.d.ts +10 -0
  4. package/dist/conversation-store-Cl42jpsA.d.cts +10 -0
  5. package/dist/index.cjs +1630 -0
  6. package/dist/index.cjs.map +1 -0
  7. package/dist/index.d.cts +251 -0
  8. package/dist/index.d.ts +251 -0
  9. package/dist/index.js +1536 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/memory-uBLqrQRY.d.cts +28 -0
  12. package/dist/memory-uBLqrQRY.d.ts +28 -0
  13. package/dist/providers/llm/anthropic.cjs +275 -0
  14. package/dist/providers/llm/anthropic.cjs.map +1 -0
  15. package/dist/providers/llm/anthropic.d.cts +22 -0
  16. package/dist/providers/llm/anthropic.d.ts +22 -0
  17. package/dist/providers/llm/anthropic.js +240 -0
  18. package/dist/providers/llm/anthropic.js.map +1 -0
  19. package/dist/providers/llm/ollama.cjs +195 -0
  20. package/dist/providers/llm/ollama.cjs.map +1 -0
  21. package/dist/providers/llm/ollama.d.cts +23 -0
  22. package/dist/providers/llm/ollama.d.ts +23 -0
  23. package/dist/providers/llm/ollama.js +170 -0
  24. package/dist/providers/llm/ollama.js.map +1 -0
  25. package/dist/providers/llm/openai.cjs +213 -0
  26. package/dist/providers/llm/openai.cjs.map +1 -0
  27. package/dist/providers/llm/openai.d.cts +22 -0
  28. package/dist/providers/llm/openai.d.ts +22 -0
  29. package/dist/providers/llm/openai.js +178 -0
  30. package/dist/providers/llm/openai.js.map +1 -0
  31. package/dist/providers/memory/filesystem.cjs +112 -0
  32. package/dist/providers/memory/filesystem.cjs.map +1 -0
  33. package/dist/providers/memory/filesystem.d.cts +17 -0
  34. package/dist/providers/memory/filesystem.d.ts +17 -0
  35. package/dist/providers/memory/filesystem.js +87 -0
  36. package/dist/providers/memory/filesystem.js.map +1 -0
  37. package/dist/providers/store/filesystem.cjs +87 -0
  38. package/dist/providers/store/filesystem.cjs.map +1 -0
  39. package/dist/providers/store/filesystem.d.cts +14 -0
  40. package/dist/providers/store/filesystem.d.ts +14 -0
  41. package/dist/providers/store/filesystem.js +62 -0
  42. package/dist/providers/store/filesystem.js.map +1 -0
  43. package/dist/providers/thread/memory.cjs +81 -0
  44. package/dist/providers/thread/memory.cjs.map +1 -0
  45. package/dist/providers/thread/memory.d.cts +14 -0
  46. package/dist/providers/thread/memory.d.ts +14 -0
  47. package/dist/providers/thread/memory.js +56 -0
  48. package/dist/providers/thread/memory.js.map +1 -0
  49. package/dist/providers/thread/sqlite.cjs +917 -0
  50. package/dist/providers/thread/sqlite.cjs.map +1 -0
  51. package/dist/providers/thread/sqlite.d.cts +17 -0
  52. package/dist/providers/thread/sqlite.d.ts +17 -0
  53. package/dist/providers/thread/sqlite.js +911 -0
  54. package/dist/providers/thread/sqlite.js.map +1 -0
  55. package/dist/providers/tokenizers/auto.cjs +136 -0
  56. package/dist/providers/tokenizers/auto.cjs.map +1 -0
  57. package/dist/providers/tokenizers/auto.d.cts +24 -0
  58. package/dist/providers/tokenizers/auto.d.ts +24 -0
  59. package/dist/providers/tokenizers/auto.js +107 -0
  60. package/dist/providers/tokenizers/auto.js.map +1 -0
  61. package/dist/streaming-B-P6Fw_k.d.cts +372 -0
  62. package/dist/streaming-BtD23BE0.d.ts +372 -0
  63. package/dist/thread-C2b9xRMJ.d.cts +30 -0
  64. package/dist/thread-C2b9xRMJ.d.ts +30 -0
  65. package/dist/tokenizer-BhG_RGUk.d.cts +13 -0
  66. package/dist/tokenizer-BhG_RGUk.d.ts +13 -0
  67. package/package.json +84 -0
  68. package/src/agent-loop.ts +311 -0
  69. package/src/agent-source.ts +12 -0
  70. package/src/artifact.ts +31 -0
  71. package/src/compaction.ts +75 -0
  72. package/src/context-budget.ts +65 -0
  73. package/src/conversation-store.ts +8 -0
  74. package/src/conversation.ts +42 -0
  75. package/src/dispatch.ts +207 -0
  76. package/src/engine.ts +53 -0
  77. package/src/execution-context.ts +31 -0
  78. package/src/index.ts +37 -0
  79. package/src/interrupt-store.ts +25 -0
  80. package/src/interrupt.ts +55 -0
  81. package/src/llm-router.ts +34 -0
  82. package/src/llm.ts +100 -0
  83. package/src/memory-selector.ts +38 -0
  84. package/src/memory.ts +34 -0
  85. package/src/mode.ts +81 -0
  86. package/src/permissions.ts +104 -0
  87. package/src/protocol.ts +1 -0
  88. package/src/providers/llm/anthropic.ts +298 -0
  89. package/src/providers/llm/ollama.ts +219 -0
  90. package/src/providers/llm/openai.ts +215 -0
  91. package/src/providers/memory/filesystem.ts +99 -0
  92. package/src/providers/store/filesystem.ts +64 -0
  93. package/src/providers/thread/memory.ts +67 -0
  94. package/src/providers/thread/sqlite.ts +147 -0
  95. package/src/providers/tokenizers/auto.ts +26 -0
  96. package/src/providers/tokenizers/byte.ts +27 -0
  97. package/src/providers/tokenizers/tiktoken.ts +91 -0
  98. package/src/reasoning.ts +7 -0
  99. package/src/rule-matcher.ts +32 -0
  100. package/src/runtime.ts +416 -0
  101. package/src/safety.ts +56 -0
  102. package/src/sandbox.ts +23 -0
  103. package/src/session-context.ts +33 -0
  104. package/src/skill-source.ts +21 -0
  105. package/src/streaming.ts +124 -0
  106. package/src/system-prompt.ts +71 -0
  107. package/src/system-reminder.ts +9 -0
  108. package/src/thread.ts +33 -0
  109. package/src/tokenizer.ts +31 -0
  110. package/src/tool.ts +175 -0
  111. package/src/tracing.ts +63 -0
@@ -0,0 +1,104 @@
1
+ import type { Tool } from "./tool.js";
2
+ import type { InterruptRequester } from "./interrupt.js";
3
+ import type { InterruptRequest } from "./interrupt.js";
4
+ import { randomUUID } from "node:crypto";
5
+
6
+ export type PermissionBehavior = "allow" | "deny" | "ask";
7
+
8
+ export interface PermissionRule {
9
+ toolName?: string;
10
+ pathPrefix?: string;
11
+ behavior: PermissionBehavior;
12
+ reason?: string;
13
+ }
14
+
15
+ export interface PermissionDecisionResult {
16
+ behavior: PermissionBehavior;
17
+ reason?: string;
18
+ updatedInput?: Record<string, unknown>;
19
+ }
20
+
21
+ export interface PermissionApprover {
22
+ ask(
23
+ tool: Tool,
24
+ args: Record<string, unknown>,
25
+ signal?: AbortSignal,
26
+ ): Promise<PermissionDecisionResult>;
27
+ }
28
+
29
+ export class InterruptApprover implements PermissionApprover {
30
+ constructor(private requester: InterruptRequester) {}
31
+
32
+ async ask(
33
+ tool: Tool,
34
+ args: Record<string, unknown>,
35
+ signal?: AbortSignal,
36
+ ): Promise<PermissionDecisionResult> {
37
+ const req: InterruptRequest = {
38
+ id: randomUUID(),
39
+ kind: "approval",
40
+ title: `Allow ${tool.name}?`,
41
+ description: tool.description,
42
+ toolName: tool.name,
43
+ toolArgs: args,
44
+ createdAt: new Date(),
45
+ };
46
+ const resp = await this.requester(req, signal);
47
+ if (resp.approved) {
48
+ return { behavior: "allow" };
49
+ }
50
+ return { behavior: "deny", reason: "user denied" };
51
+ }
52
+ }
53
+
54
+ export class PermissionEngine {
55
+ private rules: PermissionRule[] = [];
56
+ private approver?: PermissionApprover;
57
+
58
+ withRules(rules: PermissionRule[]): this {
59
+ this.rules = rules;
60
+ return this;
61
+ }
62
+
63
+ withApprover(approver: PermissionApprover): this {
64
+ this.approver = approver;
65
+ return this;
66
+ }
67
+
68
+ async decide(
69
+ tool: Tool,
70
+ args: Record<string, unknown>,
71
+ signal?: AbortSignal,
72
+ ): Promise<PermissionDecisionResult> {
73
+ for (const rule of this.rules) {
74
+ if (rule.toolName && rule.toolName !== tool.name) continue;
75
+ if (rule.pathPrefix) {
76
+ const path =
77
+ typeof args["path"] === "string" ? args["path"] : "";
78
+ if (!path.startsWith(rule.pathPrefix)) continue;
79
+ }
80
+ if (rule.behavior === "ask") break;
81
+ return { behavior: rule.behavior, reason: rule.reason };
82
+ }
83
+
84
+ if (tool.checkPermissions) {
85
+ const result = await tool.checkPermissions(signal, args);
86
+ if (result.decision === "deny" || result.decision === "ask_user") {
87
+ if (this.approver && result.decision === "ask_user") {
88
+ return this.approver.ask(tool, args, signal);
89
+ }
90
+ return { behavior: "deny", reason: result.reason };
91
+ }
92
+ return {
93
+ behavior: "allow",
94
+ updatedInput: result.updatedArgs as Record<string, unknown> | undefined,
95
+ };
96
+ }
97
+
98
+ if (this.approver && tool.isDestructive?.(args)) {
99
+ return this.approver.ask(tool, args, signal);
100
+ }
101
+
102
+ return { behavior: "allow" };
103
+ }
104
+ }
@@ -0,0 +1 @@
1
+ export const PROTOCOL_VERSION = "1" as const;
@@ -0,0 +1,298 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+ import type {
3
+ LLMProvider,
4
+ ChatRequest,
5
+ ChatResponse,
6
+ ChatMessage,
7
+ ToolDef,
8
+ } from "../../llm.js";
9
+ import type { StreamingLLMProvider, StreamEvent } from "../../streaming.js";
10
+
11
+ function toAnthropicMessages(
12
+ messages: ChatMessage[],
13
+ ): Anthropic.MessageParam[] {
14
+ const out: Anthropic.MessageParam[] = [];
15
+
16
+ for (const msg of messages) {
17
+ if (msg.role === "system") continue;
18
+
19
+ if (msg.role === "tool") {
20
+ const last = out[out.length - 1];
21
+ const toolResultBlock: Anthropic.ToolResultBlockParam = {
22
+ type: "tool_result",
23
+ tool_use_id: msg.toolCallId ?? "",
24
+ content: msg.content,
25
+ };
26
+ if (last?.role === "user" && Array.isArray(last.content)) {
27
+ (last.content as Anthropic.ToolResultBlockParam[]).push(toolResultBlock);
28
+ } else {
29
+ out.push({ role: "user", content: [toolResultBlock] });
30
+ }
31
+ continue;
32
+ }
33
+
34
+ if (msg.role === "assistant" && msg.toolCalls?.length) {
35
+ const content: Anthropic.ContentBlockParam[] = [];
36
+ if (msg.content) {
37
+ content.push({ type: "text", text: msg.content });
38
+ }
39
+ for (const tc of msg.toolCalls) {
40
+ let input: Record<string, unknown> = {};
41
+ try {
42
+ input = JSON.parse(tc.arguments) as Record<string, unknown>;
43
+ } catch {}
44
+ content.push({
45
+ type: "tool_use",
46
+ id: tc.id,
47
+ name: tc.name,
48
+ input,
49
+ });
50
+ }
51
+ out.push({ role: "assistant", content });
52
+ continue;
53
+ }
54
+
55
+ if (msg.role === "user" && (msg.images?.length || msg.documents?.length)) {
56
+ const content: Anthropic.ContentBlockParam[] = [];
57
+ for (const img of msg.images ?? []) {
58
+ if (img.url) {
59
+ // Anthropic doesn't support URL images natively — fetch as base64 or skip
60
+ content.push({
61
+ type: "text",
62
+ text: `[Image URL: ${img.url}]`,
63
+ });
64
+ } else if (img.source) {
65
+ content.push({
66
+ type: "image",
67
+ source: {
68
+ type: "base64",
69
+ media_type: (img.mediaType ?? "image/jpeg") as
70
+ | "image/jpeg"
71
+ | "image/png"
72
+ | "image/gif"
73
+ | "image/webp",
74
+ data: img.source,
75
+ },
76
+ });
77
+ }
78
+ }
79
+ if (msg.content) content.push({ type: "text", text: msg.content });
80
+ out.push({ role: "user", content });
81
+ continue;
82
+ }
83
+
84
+ out.push({
85
+ role: msg.role as "user" | "assistant",
86
+ content: msg.content,
87
+ });
88
+ }
89
+
90
+ return out;
91
+ }
92
+
93
+ function toAnthropicTools(tools: ToolDef[]): Anthropic.Tool[] {
94
+ return tools.map((t) => ({
95
+ name: t.function.name,
96
+ description: t.function.description,
97
+ input_schema: t.function.parameters as Anthropic.Tool.InputSchema,
98
+ }));
99
+ }
100
+
101
+ function extractSystemPrompt(messages: ChatMessage[]): string {
102
+ return messages
103
+ .filter((m) => m.role === "system")
104
+ .map((m) => m.content)
105
+ .join("\n\n");
106
+ }
107
+
108
+ export interface AnthropicProviderOptions {
109
+ apiKey?: string;
110
+ baseURL?: string;
111
+ defaultModel?: string;
112
+ defaultMaxTokens?: number;
113
+ }
114
+
115
+ export class AnthropicProvider implements StreamingLLMProvider {
116
+ private client: Anthropic;
117
+ private defaultModel: string;
118
+ private defaultMaxTokens: number;
119
+
120
+ constructor(opts: AnthropicProviderOptions = {}) {
121
+ this.client = new Anthropic({
122
+ apiKey: opts.apiKey ?? process.env["ANTHROPIC_API_KEY"],
123
+ baseURL: opts.baseURL,
124
+ });
125
+ this.defaultModel = opts.defaultModel ?? "claude-sonnet-4-6-20251001";
126
+ this.defaultMaxTokens = opts.defaultMaxTokens ?? 8096;
127
+ }
128
+
129
+ async chat(req: ChatRequest, signal?: AbortSignal): Promise<ChatResponse> {
130
+ const system = extractSystemPrompt(req.messages);
131
+ const messages = toAnthropicMessages(req.messages);
132
+ const tools = req.tools?.length ? toAnthropicTools(req.tools) : undefined;
133
+
134
+ const params: Anthropic.MessageCreateParamsNonStreaming = {
135
+ model: req.model ?? this.defaultModel,
136
+ max_tokens: req.maxTokens ?? this.defaultMaxTokens,
137
+ messages,
138
+ ...(system ? { system } : {}),
139
+ ...(tools ? { tools } : {}),
140
+ ...(req.temperature != null ? { temperature: req.temperature } : {}),
141
+ ...(req.topP != null ? { top_p: req.topP } : {}),
142
+ ...(req.topK != null ? { top_k: req.topK } : {}),
143
+ ...(req.stop?.length ? { stop_sequences: req.stop } : {}),
144
+ ...(req.thinkingBudget
145
+ ? {
146
+ thinking: {
147
+ type: "enabled",
148
+ budget_tokens: req.thinkingBudget,
149
+ } as Anthropic.ThinkingConfigParam,
150
+ }
151
+ : {}),
152
+ };
153
+
154
+ const resp = await this.client.messages.create(params, {
155
+ signal,
156
+ });
157
+
158
+ let content = "";
159
+ let thinkingContent = "";
160
+ const toolCalls: ChatResponse["toolCalls"] = [];
161
+
162
+ for (const block of resp.content) {
163
+ if (block.type === "text") {
164
+ content += block.text;
165
+ } else if (block.type === "thinking") {
166
+ thinkingContent += block.thinking;
167
+ } else if (block.type === "tool_use") {
168
+ toolCalls.push({
169
+ id: block.id,
170
+ name: block.name,
171
+ arguments: JSON.stringify(block.input),
172
+ });
173
+ }
174
+ }
175
+
176
+ return {
177
+ content,
178
+ thinkingContent: thinkingContent || undefined,
179
+ toolCalls: toolCalls.length ? toolCalls : undefined,
180
+ finishReason: resp.stop_reason ?? "stop",
181
+ usage: {
182
+ promptTokens: resp.usage.input_tokens,
183
+ completionTokens: resp.usage.output_tokens,
184
+ totalTokens: resp.usage.input_tokens + resp.usage.output_tokens,
185
+ },
186
+ model: resp.model,
187
+ };
188
+ }
189
+
190
+ async *chatStream(
191
+ req: ChatRequest,
192
+ signal?: AbortSignal,
193
+ ): AsyncGenerator<StreamEvent> {
194
+ const system = extractSystemPrompt(req.messages);
195
+ const messages = toAnthropicMessages(req.messages);
196
+ const tools = req.tools?.length ? toAnthropicTools(req.tools) : undefined;
197
+
198
+ const params: Anthropic.MessageCreateParamsStreaming = {
199
+ model: req.model ?? this.defaultModel,
200
+ max_tokens: req.maxTokens ?? this.defaultMaxTokens,
201
+ messages,
202
+ stream: true,
203
+ ...(system ? { system } : {}),
204
+ ...(tools ? { tools } : {}),
205
+ ...(req.temperature != null ? { temperature: req.temperature } : {}),
206
+ ...(req.topP != null ? { top_p: req.topP } : {}),
207
+ ...(req.topK != null ? { top_k: req.topK } : {}),
208
+ ...(req.stop?.length ? { stop_sequences: req.stop } : {}),
209
+ ...(req.thinkingBudget
210
+ ? {
211
+ thinking: {
212
+ type: "enabled",
213
+ budget_tokens: req.thinkingBudget,
214
+ } as Anthropic.ThinkingConfigParam,
215
+ }
216
+ : {}),
217
+ };
218
+
219
+ const stream = this.client.messages.stream(params, { signal });
220
+
221
+ let promptTokens = 0;
222
+ let completionTokens = 0;
223
+
224
+ const toolCallMap = new Map<
225
+ number,
226
+ { id: string; name: string; argsBuf: string }
227
+ >();
228
+
229
+ for await (const chunk of stream) {
230
+ if (signal?.aborted) break;
231
+
232
+ if (chunk.type === "message_start") {
233
+ promptTokens = chunk.message.usage.input_tokens;
234
+ completionTokens = chunk.message.usage.output_tokens;
235
+ continue;
236
+ }
237
+
238
+ if (chunk.type === "message_delta") {
239
+ completionTokens = chunk.usage.output_tokens;
240
+ continue;
241
+ }
242
+
243
+ if (chunk.type === "content_block_start") {
244
+ if (chunk.content_block.type === "tool_use") {
245
+ toolCallMap.set(chunk.index, {
246
+ id: chunk.content_block.id,
247
+ name: chunk.content_block.name,
248
+ argsBuf: "",
249
+ });
250
+ }
251
+ continue;
252
+ }
253
+
254
+ if (chunk.type === "content_block_delta") {
255
+ if (chunk.delta.type === "text_delta") {
256
+ yield { type: "delta", delta: chunk.delta.text };
257
+ } else if (chunk.delta.type === "thinking_delta") {
258
+ yield { type: "thinking", thinking: chunk.delta.thinking };
259
+ } else if (chunk.delta.type === "input_json_delta") {
260
+ const tc = toolCallMap.get(chunk.index);
261
+ if (tc) tc.argsBuf += chunk.delta.partial_json;
262
+ }
263
+ continue;
264
+ }
265
+
266
+ if (chunk.type === "content_block_stop") {
267
+ const tc = toolCallMap.get(chunk.index);
268
+ if (tc) {
269
+ yield {
270
+ type: "tool_call",
271
+ toolCall: { id: tc.id, name: tc.name, arguments: tc.argsBuf },
272
+ };
273
+ toolCallMap.delete(chunk.index);
274
+ }
275
+ continue;
276
+ }
277
+
278
+ if (chunk.type === "message_stop") {
279
+ yield {
280
+ type: "done",
281
+ final: {
282
+ finalContent: "",
283
+ providerReasoning: "",
284
+ totalTurns: 1,
285
+ totalUsage: {
286
+ promptTokens,
287
+ completionTokens,
288
+ totalTokens: promptTokens + completionTokens,
289
+ },
290
+ messages: [],
291
+ reasoningTrace: [],
292
+ stopReason: "complete",
293
+ },
294
+ };
295
+ }
296
+ }
297
+ }
298
+ }
@@ -0,0 +1,219 @@
1
+ import type {
2
+ LLMProvider,
3
+ ChatRequest,
4
+ ChatResponse,
5
+ ChatMessage,
6
+ ToolCallEntry,
7
+ } from "../../llm.js";
8
+ import type { StreamingLLMProvider, StreamEvent } from "../../streaming.js";
9
+
10
+ interface OllamaMessage {
11
+ role: string;
12
+ content: string;
13
+ images?: string[];
14
+ tool_calls?: Array<{
15
+ function: { name: string; arguments: Record<string, unknown> };
16
+ }>;
17
+ }
18
+
19
+ interface OllamaChatResponse {
20
+ model: string;
21
+ message: OllamaMessage;
22
+ done: boolean;
23
+ done_reason?: string;
24
+ prompt_eval_count?: number;
25
+ eval_count?: number;
26
+ }
27
+
28
+ function toOllamaMessages(messages: ChatMessage[]): OllamaMessage[] {
29
+ const out: OllamaMessage[] = [];
30
+ for (const msg of messages) {
31
+ if (msg.role === "system") {
32
+ out.push({ role: "system", content: msg.content });
33
+ continue;
34
+ }
35
+ if (msg.role === "tool") {
36
+ out.push({ role: "tool", content: msg.content });
37
+ continue;
38
+ }
39
+ const om: OllamaMessage = { role: msg.role, content: msg.content };
40
+ if (msg.images?.length) {
41
+ om.images = msg.images
42
+ .filter((i) => i.source)
43
+ .map((i) => i.source!);
44
+ }
45
+ if (msg.toolCalls?.length) {
46
+ om.tool_calls = msg.toolCalls.map((tc) => ({
47
+ function: {
48
+ name: tc.name,
49
+ arguments: JSON.parse(tc.arguments) as Record<string, unknown>,
50
+ },
51
+ }));
52
+ }
53
+ out.push(om);
54
+ }
55
+ return out;
56
+ }
57
+
58
+ export interface OllamaProviderOptions {
59
+ baseURL?: string;
60
+ defaultModel?: string;
61
+ nativeToolCalls?: boolean;
62
+ temperature?: number;
63
+ }
64
+
65
+ export class OllamaProvider implements StreamingLLMProvider {
66
+ private baseURL: string;
67
+ private defaultModel: string;
68
+ private nativeToolCalls: boolean;
69
+ private temperature?: number;
70
+
71
+ constructor(opts: OllamaProviderOptions = {}) {
72
+ this.baseURL =
73
+ (opts.baseURL ?? process.env["OLLAMA_BASE_URL"] ?? "http://localhost:11434").replace(/\/$/, "");
74
+ this.defaultModel = opts.defaultModel ?? "llama3.1";
75
+ this.nativeToolCalls = opts.nativeToolCalls ?? false;
76
+ this.temperature = opts.temperature;
77
+ }
78
+
79
+ async chat(req: ChatRequest, signal?: AbortSignal): Promise<ChatResponse> {
80
+ const body: Record<string, unknown> = {
81
+ model: req.model ?? this.defaultModel,
82
+ messages: toOllamaMessages(req.messages),
83
+ stream: false,
84
+ options: this.temperature != null ? { temperature: this.temperature } : undefined,
85
+ };
86
+
87
+ if (this.nativeToolCalls && req.tools?.length) {
88
+ body["tools"] = req.tools.map((t) => ({
89
+ type: "function",
90
+ function: {
91
+ name: t.function.name,
92
+ description: t.function.description,
93
+ parameters: t.function.parameters,
94
+ },
95
+ }));
96
+ }
97
+
98
+ const resp = await fetch(`${this.baseURL}/api/chat`, {
99
+ method: "POST",
100
+ headers: { "Content-Type": "application/json" },
101
+ body: JSON.stringify(body),
102
+ signal,
103
+ });
104
+
105
+ if (!resp.ok) {
106
+ throw new Error(`Ollama error: ${resp.status} ${await resp.text()}`);
107
+ }
108
+
109
+ const data = (await resp.json()) as OllamaChatResponse;
110
+ const toolCalls: ToolCallEntry[] = [];
111
+
112
+ if (data.message.tool_calls?.length) {
113
+ for (let i = 0; i < data.message.tool_calls.length; i++) {
114
+ const tc = data.message.tool_calls[i]!;
115
+ toolCalls.push({
116
+ id: `ollama-${i}`,
117
+ name: tc.function.name,
118
+ arguments: JSON.stringify(tc.function.arguments),
119
+ });
120
+ }
121
+ }
122
+
123
+ const promptTokens = data.prompt_eval_count ?? 0;
124
+ const completionTokens = data.eval_count ?? 0;
125
+
126
+ return {
127
+ content: data.message.content,
128
+ toolCalls: toolCalls.length ? toolCalls : undefined,
129
+ finishReason: data.done_reason ?? "stop",
130
+ usage: {
131
+ promptTokens,
132
+ completionTokens,
133
+ totalTokens: promptTokens + completionTokens,
134
+ },
135
+ model: data.model,
136
+ };
137
+ }
138
+
139
+ async *chatStream(
140
+ req: ChatRequest,
141
+ signal?: AbortSignal,
142
+ ): AsyncGenerator<StreamEvent> {
143
+ const body: Record<string, unknown> = {
144
+ model: req.model ?? this.defaultModel,
145
+ messages: toOllamaMessages(req.messages),
146
+ stream: true,
147
+ options: this.temperature != null ? { temperature: this.temperature } : undefined,
148
+ };
149
+
150
+ if (this.nativeToolCalls && req.tools?.length) {
151
+ body["tools"] = req.tools.map((t) => ({
152
+ type: "function",
153
+ function: {
154
+ name: t.function.name,
155
+ description: t.function.description,
156
+ parameters: t.function.parameters,
157
+ },
158
+ }));
159
+ }
160
+
161
+ const resp = await fetch(`${this.baseURL}/api/chat`, {
162
+ method: "POST",
163
+ headers: { "Content-Type": "application/json" },
164
+ body: JSON.stringify(body),
165
+ signal,
166
+ });
167
+
168
+ if (!resp.ok) {
169
+ throw new Error(`Ollama error: ${resp.status} ${await resp.text()}`);
170
+ }
171
+
172
+ const reader = resp.body?.getReader();
173
+ if (!reader) throw new Error("Ollama: no response body");
174
+
175
+ const decoder = new TextDecoder();
176
+ let buf = "";
177
+ let promptTokens = 0;
178
+ let completionTokens = 0;
179
+ let fullContent = "";
180
+
181
+ while (true) {
182
+ const { done, value } = await reader.read();
183
+ if (done) break;
184
+ if (signal?.aborted) { reader.cancel(); break; }
185
+ buf += decoder.decode(value, { stream: true });
186
+ const lines = buf.split("\n");
187
+ buf = lines.pop() ?? "";
188
+ for (const line of lines) {
189
+ const trimmed = line.trim();
190
+ if (!trimmed) continue;
191
+ const data = JSON.parse(trimmed) as OllamaChatResponse;
192
+ if (data.message?.content) {
193
+ fullContent += data.message.content;
194
+ yield { type: "delta", delta: data.message.content };
195
+ }
196
+ if (data.done) {
197
+ promptTokens = data.prompt_eval_count ?? 0;
198
+ completionTokens = data.eval_count ?? 0;
199
+ yield {
200
+ type: "done",
201
+ final: {
202
+ finalContent: fullContent,
203
+ providerReasoning: "",
204
+ totalTurns: 1,
205
+ totalUsage: {
206
+ promptTokens,
207
+ completionTokens,
208
+ totalTokens: promptTokens + completionTokens,
209
+ },
210
+ messages: [],
211
+ reasoningTrace: [],
212
+ stopReason: "complete",
213
+ },
214
+ };
215
+ }
216
+ }
217
+ }
218
+ }
219
+ }