@k2b/nessi 0.10.0-rc.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 (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +251 -0
  3. package/aggregates.d.ts +7 -0
  4. package/aggregates.js +115 -0
  5. package/ai/complete-from-stream.d.ts +2 -0
  6. package/ai/complete-from-stream.js +36 -0
  7. package/ai/index.d.ts +10 -0
  8. package/ai/index.js +9 -0
  9. package/ai/providers/anthropic.d.ts +13 -0
  10. package/ai/providers/anthropic.js +266 -0
  11. package/ai/providers/gemini.d.ts +12 -0
  12. package/ai/providers/gemini.js +192 -0
  13. package/ai/providers/mistral.d.ts +12 -0
  14. package/ai/providers/mistral.js +287 -0
  15. package/ai/providers/ollama.d.ts +10 -0
  16. package/ai/providers/ollama.js +241 -0
  17. package/ai/providers/openai-compatible.d.ts +2 -0
  18. package/ai/providers/openai-compatible.js +349 -0
  19. package/ai/providers/openai.d.ts +12 -0
  20. package/ai/providers/openai.js +22 -0
  21. package/ai/providers/openrouter.d.ts +13 -0
  22. package/ai/providers/openrouter.js +28 -0
  23. package/ai/providers/vllm.d.ts +11 -0
  24. package/ai/providers/vllm.js +22 -0
  25. package/ai/shared/errors.d.ts +15 -0
  26. package/ai/shared/errors.js +56 -0
  27. package/ai/shared/json.d.ts +3 -0
  28. package/ai/shared/json.js +15 -0
  29. package/ai/shared/messages.d.ts +15 -0
  30. package/ai/shared/messages.js +58 -0
  31. package/ai/shared/ndjson.d.ts +4 -0
  32. package/ai/shared/ndjson.js +60 -0
  33. package/ai/shared/sse.d.ts +15 -0
  34. package/ai/shared/sse.js +79 -0
  35. package/ai/shared/stream-helpers.d.ts +13 -0
  36. package/ai/shared/stream-helpers.js +105 -0
  37. package/ai/shared/tool-call-ids.d.ts +5 -0
  38. package/ai/shared/tool-call-ids.js +38 -0
  39. package/ai/shared/tool-stream-normalizer.d.ts +6 -0
  40. package/ai/shared/tool-stream-normalizer.js +271 -0
  41. package/ai/shared/tools.d.ts +29 -0
  42. package/ai/shared/tools.js +25 -0
  43. package/ai/shared/usage.d.ts +3 -0
  44. package/ai/shared/usage.js +5 -0
  45. package/ai/types.d.ts +252 -0
  46. package/ai/types.js +0 -0
  47. package/compact.d.ts +5 -0
  48. package/compact.js +108 -0
  49. package/index.d.ts +11 -0
  50. package/index.js +12 -0
  51. package/nessi.d.ts +2 -0
  52. package/nessi.js +1250 -0
  53. package/package.json +80 -0
  54. package/providers/ollama.d.ts +2 -0
  55. package/providers/ollama.js +1 -0
  56. package/providers/openai.d.ts +2 -0
  57. package/providers/openai.js +1 -0
  58. package/providers/openrouter.d.ts +2 -0
  59. package/providers/openrouter.js +1 -0
  60. package/stores.d.ts +11 -0
  61. package/stores.js +42 -0
  62. package/structured.d.ts +9 -0
  63. package/structured.js +413 -0
  64. package/tools.d.ts +25 -0
  65. package/tools.js +36 -0
  66. package/types.d.ts +290 -0
  67. package/types.js +3 -0
  68. package/utils.d.ts +15 -0
  69. package/utils.js +47 -0
@@ -0,0 +1,60 @@
1
+ export const parseNDJSON = async function* (reader, timeouts = {}) {
2
+ const decoder = new TextDecoder();
3
+ let buffer = "";
4
+ let readCount = 0;
5
+ const readWithTimeout = async () => {
6
+ const scope = readCount === 0 ? "provider_first_byte" : "provider_idle";
7
+ const timeoutMs = readCount === 0 ? timeouts.firstByteMs : timeouts.idleMs;
8
+ if (!timeoutMs || timeoutMs <= 0)
9
+ return reader.read();
10
+ let timeout;
11
+ try {
12
+ return await Promise.race([
13
+ reader.read(),
14
+ new Promise((_, reject) => {
15
+ timeout = setTimeout(() => {
16
+ const error = {
17
+ scope,
18
+ message: `NDJSON stream ${scope === "provider_first_byte" ? "first byte" : "idle"} timeout after ${timeoutMs}ms.`,
19
+ };
20
+ void reader.cancel?.(error).catch(() => { });
21
+ reject(error);
22
+ }, timeoutMs);
23
+ }),
24
+ ]);
25
+ }
26
+ finally {
27
+ if (timeout)
28
+ clearTimeout(timeout);
29
+ }
30
+ };
31
+ while (true) {
32
+ const { done, value } = await readWithTimeout();
33
+ readCount++;
34
+ if (done)
35
+ break;
36
+ buffer += decoder.decode(value, { stream: true });
37
+ const lines = buffer.split("\n");
38
+ buffer = lines.pop() ?? "";
39
+ for (const line of lines) {
40
+ const trimmed = line.trim();
41
+ if (!trimmed)
42
+ continue;
43
+ try {
44
+ yield JSON.parse(trimmed);
45
+ }
46
+ catch {
47
+ // silently skip malformed records
48
+ }
49
+ }
50
+ }
51
+ const trimmed = buffer.trim();
52
+ if (trimmed) {
53
+ try {
54
+ yield JSON.parse(trimmed);
55
+ }
56
+ catch {
57
+ // silently skip trailing malformed records
58
+ }
59
+ }
60
+ };
@@ -0,0 +1,15 @@
1
+ export type SSEEvent = {
2
+ event?: string;
3
+ data: string;
4
+ id?: string;
5
+ };
6
+ export type SSETimeoutScope = "provider_first_byte" | "provider_idle";
7
+ export declare class SSETimeoutError extends Error {
8
+ readonly scope: SSETimeoutScope;
9
+ constructor(scope: SSETimeoutScope, timeoutMs: number);
10
+ }
11
+ export type SSETimeouts = {
12
+ firstByteMs?: number;
13
+ idleMs?: number;
14
+ };
15
+ export declare const parseSSE: (reader: ReadableStreamDefaultReader<Uint8Array>, timeouts?: SSETimeouts) => AsyncGenerator<SSEEvent>;
@@ -0,0 +1,79 @@
1
+ export class SSETimeoutError extends Error {
2
+ scope;
3
+ constructor(scope, timeoutMs) {
4
+ super(`SSE stream ${scope === "provider_first_byte" ? "first byte" : "idle"} timeout after ${timeoutMs}ms.`);
5
+ this.name = "SSETimeoutError";
6
+ this.scope = scope;
7
+ }
8
+ }
9
+ const parseFrame = (frame) => {
10
+ const lines = frame.split(/\r?\n/);
11
+ const dataLines = [];
12
+ let event;
13
+ let id;
14
+ for (const rawLine of lines) {
15
+ if (!rawLine || rawLine.startsWith(":"))
16
+ continue;
17
+ const idx = rawLine.indexOf(":");
18
+ const field = idx === -1 ? rawLine : rawLine.slice(0, idx);
19
+ let value = idx === -1 ? "" : rawLine.slice(idx + 1);
20
+ if (value.startsWith(" "))
21
+ value = value.slice(1);
22
+ if (field === "event")
23
+ event = value;
24
+ else if (field === "data")
25
+ dataLines.push(value);
26
+ else if (field === "id")
27
+ id = value;
28
+ }
29
+ if (dataLines.length === 0)
30
+ return null;
31
+ return { event, data: dataLines.join("\n"), id };
32
+ };
33
+ const readWithTimeout = async (reader, scope, timeoutMs) => {
34
+ if (!timeoutMs || timeoutMs <= 0)
35
+ return reader.read();
36
+ let timeout;
37
+ try {
38
+ return await Promise.race([
39
+ reader.read(),
40
+ new Promise((_, reject) => {
41
+ timeout = setTimeout(() => {
42
+ const error = new SSETimeoutError(scope, timeoutMs);
43
+ void reader.cancel?.(error).catch(() => { });
44
+ reject(error);
45
+ }, timeoutMs);
46
+ }),
47
+ ]);
48
+ }
49
+ finally {
50
+ if (timeout)
51
+ clearTimeout(timeout);
52
+ }
53
+ };
54
+ export const parseSSE = async function* (reader, timeouts = {}) {
55
+ const decoder = new TextDecoder();
56
+ let buffer = "";
57
+ let readCount = 0;
58
+ while (true) {
59
+ const scope = readCount === 0 ? "provider_first_byte" : "provider_idle";
60
+ const timeoutMs = readCount === 0 ? timeouts.firstByteMs : timeouts.idleMs;
61
+ const { done, value } = await readWithTimeout(reader, scope, timeoutMs);
62
+ readCount++;
63
+ if (done)
64
+ break;
65
+ buffer += decoder.decode(value, { stream: true });
66
+ const normalized = buffer.replace(/\r\n/g, "\n");
67
+ const frames = normalized.split("\n\n");
68
+ buffer = frames.pop() ?? "";
69
+ for (const frame of frames) {
70
+ const parsed = parseFrame(frame);
71
+ if (parsed)
72
+ yield parsed;
73
+ }
74
+ }
75
+ const finalFrame = buffer.replace(/\r\n/g, "\n");
76
+ const parsed = parseFrame(finalFrame);
77
+ if (parsed)
78
+ yield parsed;
79
+ };
@@ -0,0 +1,13 @@
1
+ import type { SSEEvent } from "./sse.js";
2
+ import type { ProviderTimeouts, RawStreamEvent } from "../types.js";
3
+ type SSEStreamResult = {
4
+ ok: true;
5
+ events: AsyncGenerator<SSEEvent>;
6
+ } | {
7
+ ok: false;
8
+ error: Extract<RawStreamEvent, {
9
+ type: "error" | "timeout";
10
+ }>;
11
+ };
12
+ export declare const openSSEStream: (url: string, headers: Record<string, string>, body: unknown, label: string, signal?: AbortSignal, contextWindow?: number, timeouts?: ProviderTimeouts) => Promise<SSEStreamResult>;
13
+ export {};
@@ -0,0 +1,105 @@
1
+ import { formatConnectionError, normalizeHttpError } from "./errors.js";
2
+ import { parseSSE, SSETimeoutError } from "./sse.js";
3
+ export const openSSEStream = async (url, headers, body, label, signal, contextWindow, timeouts) => {
4
+ const serializedBody = JSON.stringify(body);
5
+ let response;
6
+ const controller = new AbortController();
7
+ const abortExternal = () => controller.abort(signal?.reason);
8
+ const cleanupExternalAbort = () => {
9
+ if (signal)
10
+ signal.removeEventListener("abort", abortExternal);
11
+ };
12
+ if (signal) {
13
+ if (signal.aborted)
14
+ controller.abort(signal.reason);
15
+ else
16
+ signal.addEventListener("abort", abortExternal, { once: true });
17
+ }
18
+ let firstByteTimeout;
19
+ const firstByteDeadline = timeouts?.firstByteMs && timeouts.firstByteMs > 0
20
+ ? Date.now() + timeouts.firstByteMs
21
+ : undefined;
22
+ try {
23
+ response = await Promise.race([
24
+ fetch(url, {
25
+ method: "POST",
26
+ headers,
27
+ body: serializedBody,
28
+ signal: controller.signal,
29
+ }),
30
+ new Promise((_, reject) => {
31
+ if (!timeouts?.firstByteMs || timeouts.firstByteMs <= 0)
32
+ return;
33
+ firstByteTimeout = setTimeout(() => {
34
+ controller.abort();
35
+ reject(new SSETimeoutError("provider_first_byte", timeouts.firstByteMs));
36
+ }, timeouts.firstByteMs);
37
+ }),
38
+ ]);
39
+ }
40
+ catch (error) {
41
+ cleanupExternalAbort();
42
+ if (error instanceof SSETimeoutError) {
43
+ return {
44
+ ok: false,
45
+ error: { type: "timeout", scope: error.scope, message: error.message, retryable: true },
46
+ };
47
+ }
48
+ // Heuristic: if the request body is large relative to the context window,
49
+ // a network error likely means the server rejected it for context overflow
50
+ // (browsers hide the actual HTTP 400 body behind CORS on error responses).
51
+ const estimatedTokens = serializedBody.length / 4;
52
+ const isLikelyOverflow = typeof contextWindow === "number"
53
+ && contextWindow > 0
54
+ && estimatedTokens > contextWindow * 0.85;
55
+ if (isLikelyOverflow) {
56
+ const ratio = estimatedTokens / contextWindow;
57
+ return {
58
+ ok: false,
59
+ error: {
60
+ type: "error",
61
+ error: `${label}: context window likely exceeded (~${Math.round(estimatedTokens)} tokens estimated, limit ${contextWindow})`,
62
+ retryable: false,
63
+ contextOverflow: true,
64
+ overflowRatio: ratio,
65
+ },
66
+ };
67
+ }
68
+ return {
69
+ ok: false,
70
+ error: { type: "error", error: formatConnectionError(label, error), retryable: true },
71
+ };
72
+ }
73
+ finally {
74
+ if (firstByteTimeout)
75
+ clearTimeout(firstByteTimeout);
76
+ }
77
+ if (!response.ok) {
78
+ const normalized = await normalizeHttpError(label, response);
79
+ cleanupExternalAbort();
80
+ return { ok: false, error: { type: "error", ...normalized } };
81
+ }
82
+ const reader = response.body?.getReader();
83
+ if (!reader) {
84
+ cleanupExternalAbort();
85
+ return {
86
+ ok: false,
87
+ error: { type: "error", error: `${label} response body missing`, retryable: false },
88
+ };
89
+ }
90
+ const streamTimeouts = timeouts ? { ...timeouts } : undefined;
91
+ if (firstByteDeadline && streamTimeouts) {
92
+ streamTimeouts.firstByteMs = Math.max(1, firstByteDeadline - Date.now());
93
+ }
94
+ const events = async function* () {
95
+ try {
96
+ yield* parseSSE(reader, streamTimeouts);
97
+ }
98
+ finally {
99
+ cleanupExternalAbort();
100
+ controller.abort();
101
+ await reader.cancel().catch(() => { });
102
+ }
103
+ };
104
+ return { ok: true, events: events() };
105
+ };
@@ -0,0 +1,5 @@
1
+ declare const ALNUM = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
2
+ declare const hash32: (input: string) => number;
3
+ declare const encodeBase62: (seed: number, length: number) => string;
4
+ declare const createStrictToolCallIdFactory: () => (seed: string) => string;
5
+ export { ALNUM, hash32, encodeBase62, createStrictToolCallIdFactory };
@@ -0,0 +1,38 @@
1
+ const ALNUM = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
2
+ const hash32 = (input) => {
3
+ let h = 2166136261;
4
+ for (let i = 0; i < input.length; i++) {
5
+ h ^= input.charCodeAt(i);
6
+ h = Math.imul(h, 16777619);
7
+ }
8
+ return h >>> 0;
9
+ };
10
+ const encodeBase62 = (seed, length) => {
11
+ let n = seed >>> 0;
12
+ let out = "";
13
+ for (let i = 0; i < length; i++) {
14
+ if (n === 0)
15
+ n = hash32(`${seed}:${i}:${out.length}`);
16
+ out += ALNUM[n % ALNUM.length];
17
+ n = Math.floor(n / ALNUM.length);
18
+ }
19
+ return out;
20
+ };
21
+ const createStrictToolCallIdFactory = () => {
22
+ const used = new Set();
23
+ let seq = 0;
24
+ return (seed) => {
25
+ let attempt = 0;
26
+ while (attempt < 50_000) {
27
+ const candidate = encodeBase62(hash32(`${seed}:${seq}:${attempt}`), 9);
28
+ if (!used.has(candidate)) {
29
+ used.add(candidate);
30
+ seq++;
31
+ return candidate;
32
+ }
33
+ attempt++;
34
+ }
35
+ throw new Error("Failed to generate unique strict tool call id");
36
+ };
37
+ };
38
+ export { ALNUM, hash32, encodeBase62, createStrictToolCallIdFactory };
@@ -0,0 +1,6 @@
1
+ import type { RawStreamEvent, StreamEvent } from "../types.js";
2
+ type NormalizerOptions = {
3
+ suppressTextAfterMalformedTool?: boolean;
4
+ };
5
+ export declare function normalizeProviderStream(events: AsyncIterable<RawStreamEvent>, options?: NormalizerOptions): AsyncIterable<StreamEvent>;
6
+ export {};
@@ -0,0 +1,271 @@
1
+ import { safeJsonParse, stringifyJson } from "./json.js";
2
+ const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
3
+ const parseArgsText = (argsText) => {
4
+ const trimmed = argsText.trim();
5
+ if (!trimmed)
6
+ return { ok: true };
7
+ return isRecord(safeJsonParse(trimmed))
8
+ ? { ok: true }
9
+ : { ok: false };
10
+ };
11
+ const issueMessage = (reason, tool) => {
12
+ const label = tool?.name ? `"${tool.name}"` : "unknown tool";
13
+ switch (reason) {
14
+ case "text_during_tool_call":
15
+ return `Received text while a tool call for ${label} was still open.`;
16
+ case "thinking_during_tool_call":
17
+ return `Received thinking text while a tool call for ${label} was still open.`;
18
+ case "tool_delta_without_start":
19
+ return "Received a tool argument delta before a matching tool_start event.";
20
+ case "missing_tool_name":
21
+ return "Received a tool call without a usable tool name.";
22
+ case "invalid_tool_arguments":
23
+ return `Received malformed JSON arguments for ${label}.`;
24
+ case "stream_ended_before_tool_call":
25
+ return `The stream ended before a pending tool call for ${label} became executable.`;
26
+ case "provider_error_before_tool_call":
27
+ return `The provider errored before a pending tool call for ${label} became executable.`;
28
+ }
29
+ };
30
+ function toolIssue(kind, pending, reason, extra) {
31
+ return {
32
+ kind,
33
+ callId: pending?.callId,
34
+ name: pending?.name,
35
+ reason,
36
+ message: issueMessage(reason, pending),
37
+ argsText: pending?.argsText,
38
+ textDelta: extra?.textDelta,
39
+ };
40
+ }
41
+ const blockFromOpen = (block) => block.kind === "thinking"
42
+ ? { type: "thinking", thinking: block.text }
43
+ : { type: "text", text: block.text };
44
+ const isProviderTimeoutError = (error) => Boolean(error)
45
+ && typeof error === "object"
46
+ && (error.scope === "provider_first_byte" || error.scope === "provider_idle")
47
+ && typeof error.message === "string";
48
+ export async function* normalizeProviderStream(events, options = {}) {
49
+ const pending = new Map();
50
+ const malformedCallIds = new Set();
51
+ let openBlock;
52
+ let nextBlockIndex = 0;
53
+ let emittedToolCallCount = 0;
54
+ let issueCount = 0;
55
+ let suppressMalformedTextSpan = false;
56
+ const emitIssue = function* (value) {
57
+ issueCount++;
58
+ yield { type: "issue", issue: value };
59
+ };
60
+ const closeOpenBlock = function* () {
61
+ if (!openBlock)
62
+ return;
63
+ const closing = openBlock;
64
+ openBlock = undefined;
65
+ yield {
66
+ type: "block_end",
67
+ blockId: closing.blockId,
68
+ index: closing.index,
69
+ block: blockFromOpen(closing),
70
+ };
71
+ };
72
+ const appendTextBlock = function* (kind, delta) {
73
+ if (delta.length === 0)
74
+ return;
75
+ if (!openBlock && delta.trim().length === 0)
76
+ return;
77
+ if (openBlock?.kind !== kind) {
78
+ yield* closeOpenBlock();
79
+ if (delta.trim().length === 0)
80
+ return;
81
+ const index = nextBlockIndex++;
82
+ openBlock = { blockId: `block-${index}`, index, kind, text: "" };
83
+ yield { type: "block_start", blockId: openBlock.blockId, index, kind };
84
+ }
85
+ openBlock.text += delta;
86
+ yield { type: "block_delta", blockId: openBlock.blockId, delta };
87
+ };
88
+ const emitToolCallBlock = function* (toolCall) {
89
+ yield* closeOpenBlock();
90
+ const index = nextBlockIndex++;
91
+ const blockId = `block-${index}`;
92
+ yield {
93
+ type: "block_start",
94
+ blockId,
95
+ index,
96
+ kind: "tool_call",
97
+ callId: toolCall.id,
98
+ name: toolCall.name,
99
+ };
100
+ yield { type: "block_delta", blockId, delta: stringifyJson(toolCall.args) };
101
+ yield { type: "block_end", blockId, index, block: toolCall };
102
+ };
103
+ const malformedPending = function* (reason, textDelta) {
104
+ yield* closeOpenBlock();
105
+ for (const tool of pending.values()) {
106
+ malformedCallIds.add(tool.callId);
107
+ yield* emitIssue(toolIssue("malformed_tool_call", tool, reason, { textDelta }));
108
+ }
109
+ pending.clear();
110
+ if (options.suppressTextAfterMalformedTool)
111
+ suppressMalformedTextSpan = true;
112
+ };
113
+ const cancelPending = function* (reason) {
114
+ if (pending.size === 0)
115
+ return;
116
+ yield* closeOpenBlock();
117
+ for (const tool of pending.values()) {
118
+ yield* emitIssue(toolIssue("cancelled_tool_call", tool, reason));
119
+ }
120
+ pending.clear();
121
+ };
122
+ try {
123
+ for await (const event of events) {
124
+ switch (event.type) {
125
+ case "text":
126
+ if (pending.size > 0) {
127
+ yield* malformedPending("text_during_tool_call", event.delta);
128
+ if (options.suppressTextAfterMalformedTool)
129
+ break;
130
+ }
131
+ if (suppressMalformedTextSpan)
132
+ break;
133
+ yield* appendTextBlock("text", event.delta);
134
+ break;
135
+ case "thinking":
136
+ if (pending.size > 0) {
137
+ yield* malformedPending("thinking_during_tool_call", event.delta);
138
+ if (options.suppressTextAfterMalformedTool)
139
+ break;
140
+ }
141
+ if (suppressMalformedTextSpan)
142
+ break;
143
+ yield* appendTextBlock("thinking", event.delta);
144
+ break;
145
+ case "tool_start":
146
+ suppressMalformedTextSpan = false;
147
+ yield* closeOpenBlock();
148
+ pending.set(event.callId, { callId: event.callId, name: event.name, argsText: "", argsDeltas: [] });
149
+ break;
150
+ case "tool_delta": {
151
+ suppressMalformedTextSpan = false;
152
+ if (malformedCallIds.has(event.callId))
153
+ break;
154
+ const tool = pending.get(event.callId);
155
+ if (!tool) {
156
+ yield* emitIssue({
157
+ kind: "malformed_tool_call",
158
+ reason: "tool_delta_without_start",
159
+ message: issueMessage("tool_delta_without_start", undefined),
160
+ callId: event.callId,
161
+ argsText: event.argsDelta,
162
+ });
163
+ break;
164
+ }
165
+ tool.argsText += event.argsDelta;
166
+ tool.argsDeltas.push(event.argsDelta);
167
+ break;
168
+ }
169
+ case "tool_call": {
170
+ suppressMalformedTextSpan = false;
171
+ const tool = pending.get(event.callId);
172
+ if (malformedCallIds.has(event.callId)) {
173
+ malformedCallIds.delete(event.callId);
174
+ pending.delete(event.callId);
175
+ break;
176
+ }
177
+ if (!event.name.trim()) {
178
+ malformedCallIds.add(event.callId);
179
+ pending.delete(event.callId);
180
+ yield* emitIssue(toolIssue("malformed_tool_call", tool ?? { callId: event.callId, name: event.name, argsText: "", argsDeltas: [] }, "missing_tool_name"));
181
+ break;
182
+ }
183
+ if (tool && !parseArgsText(tool.argsText).ok) {
184
+ malformedCallIds.add(event.callId);
185
+ pending.delete(event.callId);
186
+ yield* emitIssue(toolIssue("malformed_tool_call", tool, "invalid_tool_arguments"));
187
+ break;
188
+ }
189
+ pending.delete(event.callId);
190
+ emittedToolCallCount++;
191
+ yield* emitToolCallBlock({ type: "tool_call", id: event.callId, name: event.name, args: event.args });
192
+ break;
193
+ }
194
+ case "tool_error":
195
+ case "tool_cancel": {
196
+ suppressMalformedTextSpan = false;
197
+ if (event.callId)
198
+ pending.delete(event.callId);
199
+ yield* closeOpenBlock();
200
+ yield* emitIssue({
201
+ kind: event.type === "tool_error" ? "malformed_tool_call" : "cancelled_tool_call",
202
+ reason: event.reason,
203
+ message: event.message,
204
+ callId: event.callId,
205
+ name: event.name,
206
+ argsText: event.argsText,
207
+ textDelta: event.textDelta,
208
+ });
209
+ break;
210
+ }
211
+ case "usage":
212
+ suppressMalformedTextSpan = false;
213
+ if (event.finishReason && pending.size > 0) {
214
+ yield* cancelPending("stream_ended_before_tool_call");
215
+ }
216
+ yield* closeOpenBlock();
217
+ yield {
218
+ ...event,
219
+ finishReason: event.finishReason === "tool_use" && emittedToolCallCount === 0 && issueCount > 0
220
+ ? "stop"
221
+ : event.finishReason,
222
+ };
223
+ break;
224
+ case "error":
225
+ suppressMalformedTextSpan = false;
226
+ yield* cancelPending("provider_error_before_tool_call");
227
+ yield* closeOpenBlock();
228
+ yield* emitIssue({
229
+ kind: "provider_error",
230
+ message: event.error,
231
+ retryable: event.retryable,
232
+ contextOverflow: event.contextOverflow,
233
+ overflowRatio: event.overflowRatio,
234
+ });
235
+ break;
236
+ case "timeout":
237
+ suppressMalformedTextSpan = false;
238
+ yield* cancelPending("provider_error_before_tool_call");
239
+ yield* closeOpenBlock();
240
+ yield* emitIssue({
241
+ kind: "timeout",
242
+ scope: event.scope,
243
+ message: event.message,
244
+ retryable: event.retryable,
245
+ });
246
+ break;
247
+ }
248
+ }
249
+ }
250
+ catch (error) {
251
+ yield* cancelPending("provider_error_before_tool_call");
252
+ yield* closeOpenBlock();
253
+ if (isProviderTimeoutError(error)) {
254
+ yield* emitIssue({
255
+ kind: "timeout",
256
+ scope: error.scope,
257
+ message: error.message,
258
+ retryable: true,
259
+ });
260
+ return;
261
+ }
262
+ yield* emitIssue({
263
+ kind: "provider_error",
264
+ message: error instanceof Error ? error.message : String(error),
265
+ retryable: true,
266
+ });
267
+ return;
268
+ }
269
+ yield* cancelPending("stream_ended_before_tool_call");
270
+ yield* closeOpenBlock();
271
+ }
@@ -0,0 +1,29 @@
1
+ import type { ToolSpec } from "../types.js";
2
+ export declare const toOpenAITools: (tools: ToolSpec[]) => {
3
+ type: "function";
4
+ function: {
5
+ name: string;
6
+ description: string;
7
+ parameters: unknown;
8
+ };
9
+ }[];
10
+ export declare const toAnthropicTools: (tools: ToolSpec[]) => {
11
+ name: string;
12
+ description: string;
13
+ input_schema: unknown;
14
+ }[];
15
+ export declare const toGeminiTools: (tools: ToolSpec[]) => {
16
+ functionDeclarations: {
17
+ name: string;
18
+ description: string;
19
+ parameters: unknown;
20
+ }[];
21
+ }[];
22
+ export declare const toOllamaTools: (tools: ToolSpec[]) => {
23
+ type: "function";
24
+ function: {
25
+ name: string;
26
+ description: string;
27
+ parameters: unknown;
28
+ };
29
+ }[];
@@ -0,0 +1,25 @@
1
+ export const toOpenAITools = (tools) => tools.map((tool) => ({
2
+ type: "function",
3
+ function: {
4
+ name: tool.name,
5
+ description: tool.description,
6
+ parameters: tool.inputSchema,
7
+ },
8
+ }));
9
+ export const toAnthropicTools = (tools) => tools.map((tool) => ({
10
+ name: tool.name,
11
+ description: tool.description,
12
+ input_schema: tool.inputSchema,
13
+ }));
14
+ export const toGeminiTools = (tools) => {
15
+ if (tools.length === 0)
16
+ return [];
17
+ return [{
18
+ functionDeclarations: tools.map((tool) => ({
19
+ name: tool.name,
20
+ description: tool.description,
21
+ parameters: tool.inputSchema,
22
+ })),
23
+ }];
24
+ };
25
+ export const toOllamaTools = toOpenAITools;
@@ -0,0 +1,3 @@
1
+ import type { Usage } from "../types.js";
2
+ export declare const applyCredits: (usage: Usage, creditsPerInputToken?: number, creditsPerOutputToken?: number) => Usage;
3
+ export declare const makeUsage: (input?: number, output?: number) => Usage;
@@ -0,0 +1,5 @@
1
+ export const applyCredits = (usage, creditsPerInputToken = 0, creditsPerOutputToken = 0) => ({
2
+ ...usage,
3
+ creditsUsed: creditsPerInputToken * usage.input + creditsPerOutputToken * usage.output,
4
+ });
5
+ export const makeUsage = (input = 0, output = 0) => ({ input, output, total: input + output });