@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,349 @@
1
+ import { formatConnectionError, normalizeHttpError } from "../shared/errors.js";
2
+ import { assertOnlySupportedFiles, buildAssistantMessage } from "../shared/messages.js";
3
+ import { ensureRecord, safeJsonParse, stringifyJson } from "../shared/json.js";
4
+ import { openSSEStream } from "../shared/stream-helpers.js";
5
+ import { normalizeProviderStream } from "../shared/tool-stream-normalizer.js";
6
+ import { createStrictToolCallIdFactory } from "../shared/tool-call-ids.js";
7
+ import { toOpenAITools } from "../shared/tools.js";
8
+ import { applyCredits, makeUsage } from "../shared/usage.js";
9
+ const normalizeToolCallIds = (mode) => mode?.toolCallIdPolicy === "strict9";
10
+ const mapFinishReason = (reason, hasTools) => {
11
+ if (reason === "tool_calls")
12
+ return "tool_use";
13
+ if (reason === "length")
14
+ return "max_tokens";
15
+ if (reason === "content_filter")
16
+ return "error";
17
+ if (hasTools)
18
+ return "tool_use";
19
+ return "stop";
20
+ };
21
+ const responseFormatName = (name) => {
22
+ const safe = (name ?? "structured_output").replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
23
+ return safe || "structured_output";
24
+ };
25
+ const applyResponseFormat = (body, request, config) => {
26
+ if (!request.responseFormat)
27
+ return;
28
+ const mode = config.compat?.structuredOutput ?? "response_format";
29
+ if (mode === false)
30
+ return;
31
+ if (mode === "vllm_structured_outputs") {
32
+ body.structured_outputs = { json: request.responseFormat.schema };
33
+ return;
34
+ }
35
+ body.response_format = {
36
+ type: "json_schema",
37
+ json_schema: {
38
+ name: responseFormatName(request.responseFormat.name),
39
+ schema: request.responseFormat.schema,
40
+ strict: true,
41
+ },
42
+ };
43
+ };
44
+ const convertMessages = (messages, systemPrompt, config) => {
45
+ const result = [];
46
+ const strictIds = normalizeToolCallIds(config.compat);
47
+ const makeStrictId = createStrictToolCallIdFactory();
48
+ const pendingToolIds = new Map();
49
+ if (systemPrompt)
50
+ result.push({ role: "system", content: systemPrompt });
51
+ for (const message of messages) {
52
+ if (message.role === "user") {
53
+ assertOnlySupportedFiles(message.content, true, config.name);
54
+ const parts = [];
55
+ for (const part of message.content) {
56
+ if (typeof part === "string")
57
+ parts.push({ type: "text", text: part });
58
+ else if (part.type === "text")
59
+ parts.push({ type: "text", text: part.text });
60
+ else {
61
+ parts.push({
62
+ type: "image_url",
63
+ image_url: { url: `data:${part.mediaType};base64,${part.data}` },
64
+ });
65
+ }
66
+ }
67
+ if (parts.length === 1 && parts[0]?.type === "text")
68
+ result.push({ role: "user", content: parts[0].text });
69
+ else
70
+ result.push({ role: "user", content: parts });
71
+ continue;
72
+ }
73
+ if (message.role === "assistant") {
74
+ let text = "";
75
+ const toolCalls = [];
76
+ for (const block of message.content) {
77
+ if (block.type === "text")
78
+ text += block.text;
79
+ else if (block.type === "tool_call") {
80
+ const mappedId = strictIds ? makeStrictId(block.id) : block.id;
81
+ if (strictIds) {
82
+ const queue = pendingToolIds.get(block.id) ?? [];
83
+ queue.push(mappedId);
84
+ pendingToolIds.set(block.id, queue);
85
+ }
86
+ toolCalls.push({
87
+ id: mappedId,
88
+ type: "function",
89
+ function: { name: block.name, arguments: stringifyJson(block.args) },
90
+ });
91
+ }
92
+ }
93
+ const out = { role: "assistant", content: text || null };
94
+ if (toolCalls.length > 0)
95
+ out.tool_calls = toolCalls;
96
+ result.push(out);
97
+ continue;
98
+ }
99
+ let toolCallId = message.callId;
100
+ if (strictIds) {
101
+ const queue = pendingToolIds.get(message.callId);
102
+ const mapped = queue?.shift();
103
+ if (!mapped)
104
+ continue;
105
+ toolCallId = mapped;
106
+ if (queue && queue.length === 0)
107
+ pendingToolIds.delete(message.callId);
108
+ }
109
+ const toolMessage = {
110
+ role: "tool",
111
+ tool_call_id: toolCallId,
112
+ content: stringifyJson(message.result),
113
+ };
114
+ if (config.compat?.requiresToolResultName)
115
+ toolMessage.name = message.name;
116
+ result.push(toolMessage);
117
+ }
118
+ return result;
119
+ };
120
+ const usageFromChunk = (chunk, config) => {
121
+ if (!chunk.usage)
122
+ return undefined;
123
+ return applyCredits(makeUsage(chunk.usage.prompt_tokens ?? 0, chunk.usage.completion_tokens ?? 0), config.creditsPerInputToken, config.creditsPerOutputToken);
124
+ };
125
+ const thinkingFromDelta = (delta, config) => {
126
+ if (config.compat?.thinkingFormat === "text")
127
+ return delta.reasoning ?? "";
128
+ if (config.compat?.thinkingFormat === "reasoning_details") {
129
+ return (delta.reasoning_details ?? [])
130
+ .map((detail) => detail.text ?? detail.summary ?? "")
131
+ .join("");
132
+ }
133
+ return "";
134
+ };
135
+ const parseCompletionResponse = async (response, config) => {
136
+ const payload = safeJsonParse(await response.text());
137
+ if (!payload)
138
+ throw new Error(`${config.name} returned invalid JSON.`);
139
+ const choice = payload.choices?.[0];
140
+ const message = choice?.message;
141
+ const content = message?.content ?? "";
142
+ const toolCalls = (message?.tool_calls ?? []).map((call, index) => ({
143
+ type: "tool_call",
144
+ id: call.id ?? `${config.name}-${index}`,
145
+ name: call.function?.name ?? "",
146
+ args: ensureRecord(safeJsonParse(call.function?.arguments ?? "{}")),
147
+ }));
148
+ const usage = usageFromChunk(payload, config);
149
+ const finishReason = mapFinishReason(choice?.finish_reason, toolCalls.length > 0);
150
+ return {
151
+ message: buildAssistantMessage(config.model, content ?? "", "", toolCalls, usage, finishReason),
152
+ usage,
153
+ finishReason,
154
+ providerMeta: {
155
+ model: config.model,
156
+ requestId: response.headers.get("x-request-id") ?? response.headers.get("request-id") ?? undefined,
157
+ },
158
+ };
159
+ };
160
+ export const openAICompatible = (config) => {
161
+ const baseURL = config.baseURL.replace(/\/+$/, "");
162
+ const contextWindow = config.contextWindow ?? 128_000;
163
+ const resolveTemperature = (request) => request.temperature ?? config.temperature;
164
+ const provider = {
165
+ name: config.name,
166
+ family: "openai-compatible",
167
+ model: config.model,
168
+ contextWindow,
169
+ capabilities: {
170
+ streaming: true,
171
+ tools: true,
172
+ images: true,
173
+ thinking: config.compat?.thinkingFormat !== "none",
174
+ usage: true,
175
+ structuredOutput: config.compat?.structuredOutput !== undefined && config.compat.structuredOutput !== false,
176
+ },
177
+ async complete(request) {
178
+ const messages = convertMessages(request.messages, request.systemPrompt, config);
179
+ const tools = request.tools?.length ? toOpenAITools(request.tools) : undefined;
180
+ const body = {
181
+ model: config.model,
182
+ messages,
183
+ stream: false,
184
+ };
185
+ if (tools)
186
+ body.tools = tools;
187
+ applyResponseFormat(body, request, config);
188
+ if (request.maxOutputTokens !== undefined) {
189
+ body[config.compat?.maxTokensField ?? "max_completion_tokens"] = request.maxOutputTokens;
190
+ }
191
+ if (request.disableReasoning)
192
+ body.reasoning_effort = "low";
193
+ const temperature = resolveTemperature(request);
194
+ if (temperature !== undefined)
195
+ body.temperature = temperature;
196
+ const headers = {
197
+ "Content-Type": "application/json",
198
+ ...config.headers,
199
+ };
200
+ if (config.apiKey)
201
+ headers.Authorization = `Bearer ${config.apiKey}`;
202
+ const response = await fetch(`${baseURL}/chat/completions`, {
203
+ method: "POST",
204
+ headers,
205
+ body: JSON.stringify(body),
206
+ signal: request.signal,
207
+ }).catch((error) => {
208
+ throw new Error(formatConnectionError(config.name, error));
209
+ });
210
+ if (!response.ok) {
211
+ const normalized = await normalizeHttpError(config.name, response);
212
+ throw new Error(normalized.error);
213
+ }
214
+ return parseCompletionResponse(response, config);
215
+ },
216
+ stream(request) {
217
+ const raw = async function* () {
218
+ const messages = convertMessages(request.messages, request.systemPrompt, config);
219
+ const tools = request.tools?.length ? toOpenAITools(request.tools) : undefined;
220
+ const body = {
221
+ model: config.model,
222
+ messages,
223
+ stream: true,
224
+ };
225
+ if (config.compat?.supportsUsageInStreaming !== false) {
226
+ body.stream_options = { include_usage: true };
227
+ }
228
+ if (tools)
229
+ body.tools = tools;
230
+ applyResponseFormat(body, request, config);
231
+ const temperature = resolveTemperature(request);
232
+ if (temperature !== undefined)
233
+ body.temperature = temperature;
234
+ if (request.maxOutputTokens !== undefined) {
235
+ body[config.compat?.maxTokensField ?? "max_completion_tokens"] = request.maxOutputTokens;
236
+ }
237
+ if (request.disableReasoning)
238
+ body.reasoning_effort = "low";
239
+ const headers = {
240
+ "Content-Type": "application/json",
241
+ ...config.headers,
242
+ };
243
+ if (config.apiKey)
244
+ headers.Authorization = `Bearer ${config.apiKey}`;
245
+ const result = await openSSEStream(`${baseURL}/chat/completions`, headers, body, config.name, request.signal, contextWindow, config.timeouts);
246
+ if (!result.ok) {
247
+ yield result.error;
248
+ return;
249
+ }
250
+ const toolBuffers = new Map();
251
+ let latestUsage;
252
+ let latestFinishReason;
253
+ const startToolCall = function* (buffer) {
254
+ if (buffer.started || !buffer.name.trim())
255
+ return;
256
+ buffer.started = true;
257
+ yield { type: "tool_start", callId: buffer.callId, name: buffer.name };
258
+ if (buffer.argsBuffer)
259
+ yield { type: "tool_delta", callId: buffer.callId, argsDelta: buffer.argsBuffer };
260
+ };
261
+ const flushToolCalls = function* () {
262
+ for (const [, buffer] of toolBuffers) {
263
+ yield* startToolCall(buffer);
264
+ yield {
265
+ type: "tool_call",
266
+ callId: buffer.callId,
267
+ name: buffer.name,
268
+ args: ensureRecord(safeJsonParse(buffer.argsBuffer || "{}")),
269
+ };
270
+ }
271
+ toolBuffers.clear();
272
+ };
273
+ for await (const event of result.events) {
274
+ if (event.data === "[DONE]")
275
+ break;
276
+ const chunk = safeJsonParse(event.data);
277
+ if (!chunk)
278
+ continue;
279
+ const choice = chunk.choices?.[0];
280
+ const usage = usageFromChunk(chunk, config);
281
+ if (!choice) {
282
+ if (usage) {
283
+ latestUsage = usage;
284
+ yield { type: "usage", usage };
285
+ }
286
+ continue;
287
+ }
288
+ const delta = choice.delta;
289
+ if (delta.content)
290
+ yield { type: "text", delta: delta.content };
291
+ const thinking = thinkingFromDelta(delta, config);
292
+ if (thinking)
293
+ yield { type: "thinking", delta: thinking };
294
+ if (delta.tool_calls) {
295
+ for (const toolCall of delta.tool_calls) {
296
+ const existing = toolBuffers.get(toolCall.index);
297
+ if (!existing) {
298
+ const callId = toolCall.id ?? `${config.name}-${toolCall.index}`;
299
+ const name = toolCall.function?.name ?? "";
300
+ const argsDelta = toolCall.function?.arguments ?? "";
301
+ const buffer = {
302
+ callId,
303
+ name,
304
+ argsBuffer: argsDelta,
305
+ started: false,
306
+ };
307
+ toolBuffers.set(toolCall.index, buffer);
308
+ yield* startToolCall(buffer);
309
+ }
310
+ else {
311
+ if (toolCall.function?.name)
312
+ existing.name = toolCall.function.name;
313
+ const argsDelta = toolCall.function?.arguments ?? "";
314
+ if (argsDelta)
315
+ existing.argsBuffer += argsDelta;
316
+ const wasStarted = existing.started;
317
+ yield* startToolCall(existing);
318
+ if (wasStarted && argsDelta) {
319
+ yield { type: "tool_delta", callId: existing.callId, argsDelta };
320
+ }
321
+ }
322
+ }
323
+ }
324
+ if (choice.finish_reason === "tool_calls") {
325
+ yield* flushToolCalls();
326
+ }
327
+ latestFinishReason = mapFinishReason(choice.finish_reason, false);
328
+ if (usage) {
329
+ latestUsage = usage;
330
+ yield { type: "usage", usage };
331
+ }
332
+ }
333
+ if (toolBuffers.size > 0) {
334
+ latestFinishReason = "tool_use";
335
+ yield* flushToolCalls();
336
+ }
337
+ if (latestFinishReason) {
338
+ yield {
339
+ type: "usage",
340
+ usage: latestUsage ?? makeUsage(),
341
+ finishReason: latestFinishReason,
342
+ };
343
+ }
344
+ };
345
+ return normalizeProviderStream(raw(), { suppressTextAfterMalformedTool: true });
346
+ },
347
+ };
348
+ return provider;
349
+ };
@@ -0,0 +1,12 @@
1
+ import type { Provider, ProviderTimeouts } from "../types.js";
2
+ export type OpenAIOptions = {
3
+ apiKey?: string;
4
+ baseURL?: string;
5
+ contextWindow?: number;
6
+ temperature?: number;
7
+ creditsPerInputToken?: number;
8
+ creditsPerOutputToken?: number;
9
+ normalizeToolCallIds?: "auto" | "never" | "strict9";
10
+ timeouts?: ProviderTimeouts;
11
+ };
12
+ export declare const openai: (model: string, options?: OpenAIOptions) => Provider;
@@ -0,0 +1,22 @@
1
+ import { openAICompatible } from "./openai-compatible.js";
2
+ export const openai = (model, options) => {
3
+ const config = {
4
+ name: "openai",
5
+ model,
6
+ baseURL: options?.baseURL ?? "https://api.openai.com/v1",
7
+ apiKey: options?.apiKey ?? globalThis.process?.env?.OPENAI_API_KEY,
8
+ contextWindow: options?.contextWindow,
9
+ temperature: options?.temperature,
10
+ creditsPerInputToken: options?.creditsPerInputToken,
11
+ creditsPerOutputToken: options?.creditsPerOutputToken,
12
+ timeouts: options?.timeouts,
13
+ compat: {
14
+ toolCallIdPolicy: options?.normalizeToolCallIds === "strict9" ? "strict9" : "passthrough",
15
+ supportsUsageInStreaming: true,
16
+ thinkingFormat: "none",
17
+ maxTokensField: "max_completion_tokens",
18
+ structuredOutput: "response_format",
19
+ },
20
+ };
21
+ return openAICompatible(config);
22
+ };
@@ -0,0 +1,13 @@
1
+ import type { Provider, ProviderTimeouts } from "../types.js";
2
+ export type OpenRouterOptions = {
3
+ apiKey?: string;
4
+ baseURL?: string;
5
+ contextWindow?: number;
6
+ temperature?: number;
7
+ referer?: string;
8
+ title?: string;
9
+ creditsPerInputToken?: number;
10
+ creditsPerOutputToken?: number;
11
+ timeouts?: ProviderTimeouts;
12
+ };
13
+ export declare const openrouter: (model: string, options?: OpenRouterOptions) => Provider;
@@ -0,0 +1,28 @@
1
+ import { openAICompatible } from "./openai-compatible.js";
2
+ export const openrouter = (model, options) => {
3
+ const headers = {};
4
+ if (options?.referer)
5
+ headers["HTTP-Referer"] = options.referer;
6
+ if (options?.title)
7
+ headers["X-Title"] = options.title;
8
+ const config = {
9
+ name: "openrouter",
10
+ model,
11
+ baseURL: options?.baseURL ?? "https://openrouter.ai/api/v1",
12
+ apiKey: options?.apiKey ?? globalThis.process?.env?.OPENROUTER_API_KEY,
13
+ contextWindow: options?.contextWindow,
14
+ temperature: options?.temperature,
15
+ creditsPerInputToken: options?.creditsPerInputToken,
16
+ creditsPerOutputToken: options?.creditsPerOutputToken,
17
+ timeouts: options?.timeouts,
18
+ headers,
19
+ compat: {
20
+ toolCallIdPolicy: "passthrough",
21
+ supportsUsageInStreaming: true,
22
+ thinkingFormat: "reasoning_details",
23
+ maxTokensField: "max_tokens",
24
+ structuredOutput: "response_format",
25
+ },
26
+ };
27
+ return openAICompatible(config);
28
+ };
@@ -0,0 +1,11 @@
1
+ import type { Provider, ProviderTimeouts } from "../types.js";
2
+ export type VLLMOptions = {
3
+ apiKey?: string;
4
+ baseURL?: string;
5
+ contextWindow?: number;
6
+ temperature?: number;
7
+ creditsPerInputToken?: number;
8
+ creditsPerOutputToken?: number;
9
+ timeouts?: ProviderTimeouts;
10
+ };
11
+ export declare const vllm: (model: string, options?: VLLMOptions) => Provider;
@@ -0,0 +1,22 @@
1
+ import { openAICompatible } from "./openai-compatible.js";
2
+ export const vllm = (model, options) => {
3
+ const config = {
4
+ name: "vllm",
5
+ model,
6
+ baseURL: options?.baseURL ?? "http://localhost:8000/v1",
7
+ apiKey: options?.apiKey,
8
+ contextWindow: options?.contextWindow,
9
+ temperature: options?.temperature,
10
+ creditsPerInputToken: options?.creditsPerInputToken,
11
+ creditsPerOutputToken: options?.creditsPerOutputToken,
12
+ timeouts: options?.timeouts,
13
+ compat: {
14
+ toolCallIdPolicy: "passthrough",
15
+ supportsUsageInStreaming: true,
16
+ thinkingFormat: "none",
17
+ maxTokensField: "max_tokens",
18
+ structuredOutput: "vllm_structured_outputs",
19
+ },
20
+ };
21
+ return openAICompatible(config);
22
+ };
@@ -0,0 +1,15 @@
1
+ export declare const isRetryableStatus: (status: number) => boolean;
2
+ export declare const isContextOverflow: (status: number, message: string) => boolean;
3
+ /**
4
+ * Try to extract an overflow ratio from a provider error message.
5
+ * Many providers include token counts like:
6
+ * "maximum context length is 32768 tokens ... your prompt contains 45000 tokens"
7
+ */
8
+ export declare const parseOverflowRatio: (message: string) => number | undefined;
9
+ export declare const normalizeHttpError: (label: string, response: Response, retryableOverride?: boolean) => Promise<{
10
+ error: string;
11
+ retryable: boolean;
12
+ contextOverflow: boolean;
13
+ overflowRatio: number | undefined;
14
+ }>;
15
+ export declare const formatConnectionError: (label: string, error: unknown) => string;
@@ -0,0 +1,56 @@
1
+ import { safeJsonParse } from "./json.js";
2
+ const parseErrorMessage = (rawText) => {
3
+ const parsed = safeJsonParse(rawText);
4
+ if (!parsed)
5
+ return { message: rawText };
6
+ const nestedError = parsed.error;
7
+ const message = (typeof nestedError?.message === "string" && nestedError.message) ||
8
+ (typeof parsed.message === "string" && parsed.message) ||
9
+ rawText;
10
+ const code = (typeof nestedError?.code === "string" && nestedError.code) ||
11
+ (typeof parsed.code === "string" && parsed.code) ||
12
+ undefined;
13
+ return { message, code };
14
+ };
15
+ export const isRetryableStatus = (status) => status === 408 || status === 409 || status === 425 || status === 429 || status >= 500;
16
+ export const isContextOverflow = (status, message) => {
17
+ if (status !== 400 && status !== 413 && status !== 422)
18
+ return false;
19
+ const lower = message.toLowerCase();
20
+ return lower.includes("context")
21
+ || lower.includes("too long")
22
+ || lower.includes("maximum")
23
+ || lower.includes("max tokens")
24
+ || lower.includes("context window")
25
+ || lower.includes("token limit")
26
+ || lower.includes("prompt is too long")
27
+ || lower.includes("reduce the length")
28
+ || lower.includes("exceeded");
29
+ };
30
+ /**
31
+ * Try to extract an overflow ratio from a provider error message.
32
+ * Many providers include token counts like:
33
+ * "maximum context length is 32768 tokens ... your prompt contains 45000 tokens"
34
+ */
35
+ export const parseOverflowRatio = (message) => {
36
+ const maxMatch = message.match(/(?:maximum|max)\s+(?:context\s+)?(?:length|window|limit)\s+(?:is|of)\s+([\d,]+)/i);
37
+ const actualMatch = message.match(/(?:resulted?\s+in|contains?\s+(?:at\s+least\s+)?|received|totale?\s+of\s+(?:at\s+least\s+)?)\s*([\d,]+)\s*(?:tokens|input)/i);
38
+ if (!maxMatch?.[1] || !actualMatch?.[1])
39
+ return undefined;
40
+ const max = parseInt(maxMatch[1].replace(/,/g, ""), 10);
41
+ const actual = parseInt(actualMatch[1].replace(/,/g, ""), 10);
42
+ return max > 0 ? actual / max : undefined;
43
+ };
44
+ export const normalizeHttpError = async (label, response, retryableOverride) => {
45
+ const rawText = await response.text().catch(() => "");
46
+ const { message, code } = parseErrorMessage(rawText);
47
+ const fullMessage = code ? `${message} (code: ${code})` : message || `HTTP ${response.status}`;
48
+ const contextOverflow = isContextOverflow(response.status, fullMessage);
49
+ return {
50
+ error: `${label} ${response.status}: ${fullMessage}`,
51
+ retryable: retryableOverride ?? isRetryableStatus(response.status),
52
+ contextOverflow,
53
+ overflowRatio: contextOverflow ? parseOverflowRatio(fullMessage) : undefined,
54
+ };
55
+ };
56
+ export const formatConnectionError = (label, error) => `${label} connection failed: ${error instanceof Error ? error.message : String(error)}`;
@@ -0,0 +1,3 @@
1
+ export declare const safeJsonParse: <T>(input: string) => T | null;
2
+ export declare const stringifyJson: (value: unknown) => string;
3
+ export declare const ensureRecord: (value: unknown) => Record<string, unknown>;
@@ -0,0 +1,15 @@
1
+ export const safeJsonParse = (input) => {
2
+ try {
3
+ return JSON.parse(input);
4
+ }
5
+ catch {
6
+ return null;
7
+ }
8
+ };
9
+ export const stringifyJson = (value) => typeof value === "string" ? value : JSON.stringify(value);
10
+ export const ensureRecord = (value) => {
11
+ if (value && typeof value === "object" && !Array.isArray(value)) {
12
+ return value;
13
+ }
14
+ return {};
15
+ };
@@ -0,0 +1,15 @@
1
+ import type { AssistantMessage, AssistantContentBlock, ContentPart, InputFilePart, Message, TextBlock, ThinkingBlock, ToolCallBlock } from "../types.js";
2
+ export declare const textPart: (text: string) => {
3
+ type: "text";
4
+ text: string;
5
+ };
6
+ export declare const textBlock: (text: string) => TextBlock;
7
+ export declare const thinkingBlock: (thinking: string) => ThinkingBlock;
8
+ export declare const toolCallBlock: (id: string, name: string, args: Record<string, unknown>) => ToolCallBlock;
9
+ export declare const appendAssistantContentBlock: (content: AssistantContentBlock[], block: AssistantContentBlock) => void;
10
+ export declare const contentPartToText: (part: ContentPart) => string;
11
+ export declare const isImageFilePart: (part: ContentPart) => part is InputFilePart;
12
+ export declare const assertOnlySupportedFiles: (parts: ContentPart[], supportImages: boolean, label: string) => void;
13
+ export declare const buildAssistantMessage: (model: string, text: string, thinking: string, toolCalls: ToolCallBlock[], usage?: AssistantMessage["usage"], stopReason?: AssistantMessage["stopReason"]) => AssistantMessage;
14
+ export declare const buildAssistantMessageFromContent: (model: string, content: AssistantContentBlock[], usage?: AssistantMessage["usage"], stopReason?: AssistantMessage["stopReason"]) => AssistantMessage;
15
+ export declare const extractAssistantText: (message: Message) => string;
@@ -0,0 +1,58 @@
1
+ export const textPart = (text) => ({ type: "text", text });
2
+ export const textBlock = (text) => ({ type: "text", text });
3
+ export const thinkingBlock = (thinking) => ({ type: "thinking", thinking });
4
+ export const toolCallBlock = (id, name, args) => ({ type: "tool_call", id, name, args });
5
+ export const appendAssistantContentBlock = (content, block) => {
6
+ if (block.type === "text" && block.text.length === 0)
7
+ return;
8
+ if (block.type === "thinking" && block.thinking.length === 0)
9
+ return;
10
+ const last = content.at(-1);
11
+ if (block.type === "text" && last?.type === "text") {
12
+ last.text += block.text;
13
+ return;
14
+ }
15
+ if (block.type === "thinking" && last?.type === "thinking") {
16
+ last.thinking += block.thinking;
17
+ return;
18
+ }
19
+ content.push(block);
20
+ };
21
+ const cloneAssistantContentBlock = (block) => ({ ...block });
22
+ export const contentPartToText = (part) => {
23
+ if (typeof part === "string")
24
+ return part;
25
+ if (part.type === "text")
26
+ return part.text;
27
+ return "";
28
+ };
29
+ export const isImageFilePart = (part) => typeof part !== "string" && part.type === "file" && part.mediaType.startsWith("image/");
30
+ export const assertOnlySupportedFiles = (parts, supportImages, label) => {
31
+ for (const part of parts) {
32
+ if (typeof part === "string" || part.type === "text")
33
+ continue;
34
+ if (supportImages && part.mediaType.startsWith("image/"))
35
+ continue;
36
+ throw new Error(`${label} does not support input file type '${part.mediaType}'.`);
37
+ }
38
+ };
39
+ export const buildAssistantMessage = (model, text, thinking, toolCalls, usage, stopReason) => {
40
+ const content = [
41
+ ...(text ? [textBlock(text)] : []),
42
+ ...(thinking ? [thinkingBlock(thinking)] : []),
43
+ ...toolCalls,
44
+ ];
45
+ return buildAssistantMessageFromContent(model, content, usage, stopReason);
46
+ };
47
+ export const buildAssistantMessageFromContent = (model, content, usage, stopReason) => {
48
+ const clonedContent = content.map(cloneAssistantContentBlock);
49
+ return { role: "assistant", content: clonedContent, model, usage, stopReason };
50
+ };
51
+ export const extractAssistantText = (message) => {
52
+ if (message.role !== "assistant")
53
+ return "";
54
+ return message.content
55
+ .filter((block) => block.type === "text")
56
+ .map((block) => block.text)
57
+ .join("");
58
+ };
@@ -0,0 +1,4 @@
1
+ export declare const parseNDJSON: <T>(reader: ReadableStreamDefaultReader<Uint8Array>, timeouts?: {
2
+ firstByteMs?: number;
3
+ idleMs?: number;
4
+ }) => AsyncGenerator<T>;