smoltalk 0.6.0 → 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.
Files changed (55) hide show
  1. package/dist/classes/message/UserMessage.d.ts +66 -5
  2. package/dist/classes/message/UserMessage.js +100 -21
  3. package/dist/classes/message/contentParts.d.ts +259 -0
  4. package/dist/classes/message/contentParts.js +44 -0
  5. package/dist/classes/message/index.d.ts +8 -2
  6. package/dist/classes/message/index.js +12 -0
  7. package/dist/classes/message/renderers/AnthropicRenderer.d.ts +43 -0
  8. package/dist/classes/message/renderers/AnthropicRenderer.js +19 -0
  9. package/dist/classes/message/renderers/GoogleRenderer.d.ts +35 -0
  10. package/dist/classes/message/renderers/GoogleRenderer.js +26 -0
  11. package/dist/classes/message/renderers/JSONRenderer.d.ts +8 -0
  12. package/dist/classes/message/renderers/JSONRenderer.js +21 -0
  13. package/dist/classes/message/renderers/OpenAIChatRenderer.d.ts +30 -0
  14. package/dist/classes/message/renderers/OpenAIChatRenderer.js +20 -0
  15. package/dist/classes/message/renderers/OpenAIResponsesRenderer.d.ts +39 -0
  16. package/dist/classes/message/renderers/OpenAIResponsesRenderer.js +23 -0
  17. package/dist/classes/message/renderers/PartRenderer.d.ts +14 -0
  18. package/dist/classes/message/renderers/PartRenderer.js +16 -0
  19. package/dist/clients/anthropic.d.ts +24 -0
  20. package/dist/clients/anthropic.js +4 -3
  21. package/dist/clients/baseClient.d.ts +7 -0
  22. package/dist/clients/baseClient.js +39 -0
  23. package/dist/clients/google.js +3 -2
  24. package/dist/clients/ollama.js +7 -6
  25. package/dist/clients/openai.js +3 -2
  26. package/dist/clients/openaiResponses.js +3 -2
  27. package/dist/clients/resolveAttachments.d.ts +11 -0
  28. package/dist/clients/resolveAttachments.js +108 -0
  29. package/dist/files/BaseFileProvider.d.ts +29 -0
  30. package/dist/files/BaseFileProvider.js +38 -0
  31. package/dist/files/anthropic.d.ts +16 -0
  32. package/dist/files/anthropic.js +20 -0
  33. package/dist/files/google.d.ts +18 -0
  34. package/dist/files/google.js +38 -0
  35. package/dist/files/openai.d.ts +16 -0
  36. package/dist/files/openai.js +23 -0
  37. package/dist/files.d.ts +46 -0
  38. package/dist/files.js +70 -0
  39. package/dist/index.d.ts +4 -0
  40. package/dist/index.js +3 -0
  41. package/dist/models.d.ts +17 -2
  42. package/dist/models.js +29 -2
  43. package/dist/statelogClient.js +2 -1
  44. package/dist/types.d.ts +6 -8
  45. package/dist/types.js +1 -4
  46. package/dist/util/attachments.d.ts +34 -0
  47. package/dist/util/attachments.js +62 -0
  48. package/dist/util/hostedTools.js +5 -1
  49. package/dist/util/imageRef.d.ts +16 -1
  50. package/dist/util/imageRef.js +95 -10
  51. package/dist/util/modalities.d.ts +2 -0
  52. package/dist/util/modalities.js +31 -0
  53. package/dist/util/redact.d.ts +8 -0
  54. package/dist/util/redact.js +42 -0
  55. package/package.json +1 -1
@@ -0,0 +1,35 @@
1
+ import type { PartRenderer } from "./PartRenderer.js";
2
+ import type { TextPart, ImagePart, FilePart } from "../contentParts.js";
3
+ /** Renders parts for the Google Gemini API. */
4
+ export declare class GoogleRenderer implements PartRenderer<any> {
5
+ text(part: TextPart): {
6
+ text: string;
7
+ };
8
+ image(part: ImagePart): {
9
+ fileData: {
10
+ fileUri: string;
11
+ mimeType: string;
12
+ };
13
+ inlineData?: undefined;
14
+ } | {
15
+ inlineData: {
16
+ mimeType: string;
17
+ data: string;
18
+ };
19
+ fileData?: undefined;
20
+ };
21
+ file(part: FilePart): {
22
+ fileData: {
23
+ fileUri: string;
24
+ mimeType: string;
25
+ };
26
+ inlineData?: undefined;
27
+ } | {
28
+ inlineData: {
29
+ mimeType: string;
30
+ data: string;
31
+ };
32
+ fileData?: undefined;
33
+ };
34
+ private sourcePart;
35
+ }
@@ -0,0 +1,26 @@
1
+ import { refToBase64 } from "../../../util/attachments.js";
2
+ /** Renders parts for the Google Gemini API. */
3
+ export class GoogleRenderer {
4
+ text(part) {
5
+ return { text: part.text };
6
+ }
7
+ image(part) {
8
+ return this.sourcePart(part.source);
9
+ }
10
+ file(part) {
11
+ return this.sourcePart(part.source);
12
+ }
13
+ sourcePart(source) {
14
+ if (source.kind === "providerFile") {
15
+ // `fileData` needs both a uri and a mimeType; a ref missing either (e.g. a
16
+ // hand-deserialized one) would otherwise emit `{ fileUri: undefined }` and
17
+ // fail with a confusing provider error, so reject it clearly here.
18
+ if (!source.uri || !source.mimeType) {
19
+ throw new Error("Google file reference is missing uri/mimeType; re-upload it via uploadFile({ provider: \"google\" }).");
20
+ }
21
+ return { fileData: { fileUri: source.uri, mimeType: source.mimeType } };
22
+ }
23
+ const { base64, mimeType } = refToBase64(source);
24
+ return { inlineData: { mimeType, data: base64 } };
25
+ }
26
+ }
@@ -0,0 +1,8 @@
1
+ import type { PartRenderer } from "./PartRenderer.js";
2
+ import type { TextPart, ImagePart, FilePart, UserContentPart } from "../contentParts.js";
3
+ /** Renders parts back to a JSON-safe UserContentPart (for toJSON serialization). */
4
+ export declare class JSONRenderer implements PartRenderer<UserContentPart> {
5
+ text(part: TextPart): UserContentPart;
6
+ image(part: ImagePart): UserContentPart;
7
+ file(part: FilePart): UserContentPart;
8
+ }
@@ -0,0 +1,21 @@
1
+ import { refToBase64 } from "../../../util/attachments.js";
2
+ /** In-memory `bytes` don't survive JSON, so materialize them as base64; other sources pass through. */
3
+ function bytesToBase64(source) {
4
+ if (source.kind === "bytes") {
5
+ const { base64, mimeType } = refToBase64(source);
6
+ return { kind: "base64", base64, mimeType };
7
+ }
8
+ return source;
9
+ }
10
+ /** Renders parts back to a JSON-safe UserContentPart (for toJSON serialization). */
11
+ export class JSONRenderer {
12
+ text(part) {
13
+ return part;
14
+ }
15
+ image(part) {
16
+ return { type: "image", source: bytesToBase64(part.source) };
17
+ }
18
+ file(part) {
19
+ return { type: "file", source: bytesToBase64(part.source), filename: part.filename };
20
+ }
21
+ }
@@ -0,0 +1,30 @@
1
+ import type { PartRenderer } from "./PartRenderer.js";
2
+ import type { TextPart, ImagePart, FilePart } from "../contentParts.js";
3
+ /** Renders parts for the OpenAI Chat Completions API. */
4
+ export declare class OpenAIChatRenderer implements PartRenderer<any> {
5
+ text(part: TextPart): {
6
+ type: string;
7
+ text: string;
8
+ };
9
+ image(part: ImagePart): {
10
+ type: string;
11
+ image_url: {
12
+ url: string;
13
+ };
14
+ };
15
+ file(part: FilePart): {
16
+ type: string;
17
+ file: {
18
+ file_id: string;
19
+ file_data?: undefined;
20
+ filename?: undefined;
21
+ };
22
+ } | {
23
+ type: string;
24
+ file: {
25
+ file_data: string;
26
+ filename: string;
27
+ file_id?: undefined;
28
+ };
29
+ };
30
+ }
@@ -0,0 +1,20 @@
1
+ import { refToBase64, toDataUri, openAiImageUrl, attachmentFilename } from "../../../util/attachments.js";
2
+ /** Renders parts for the OpenAI Chat Completions API. */
3
+ export class OpenAIChatRenderer {
4
+ text(part) {
5
+ return { type: "text", text: part.text };
6
+ }
7
+ image(part) {
8
+ if (part.source.kind === "providerFile") {
9
+ throw new Error("OpenAI Chat Completions cannot reference an image by file id; use the openai-responses provider or inline the image.");
10
+ }
11
+ return { type: "image_url", image_url: { url: openAiImageUrl(part.source) } };
12
+ }
13
+ file(part) {
14
+ if (part.source.kind === "providerFile") {
15
+ return { type: "file", file: { file_id: part.source.id } };
16
+ }
17
+ const { base64, mimeType } = refToBase64(part.source);
18
+ return { type: "file", file: { file_data: toDataUri(base64, mimeType), filename: attachmentFilename(part.filename) } };
19
+ }
20
+ }
@@ -0,0 +1,39 @@
1
+ import type { PartRenderer } from "./PartRenderer.js";
2
+ import type { TextPart, ImagePart, FilePart } from "../contentParts.js";
3
+ /** Renders parts for the OpenAI Responses API. */
4
+ export declare class OpenAIResponsesRenderer implements PartRenderer<any> {
5
+ text(part: TextPart): {
6
+ type: string;
7
+ text: string;
8
+ };
9
+ image(part: ImagePart): {
10
+ type: string;
11
+ file_id: string;
12
+ image_url?: undefined;
13
+ detail?: undefined;
14
+ } | {
15
+ type: string;
16
+ image_url: string;
17
+ detail: string;
18
+ file_id?: undefined;
19
+ };
20
+ file(part: FilePart): {
21
+ type: string;
22
+ file_id: string;
23
+ file_url?: undefined;
24
+ file_data?: undefined;
25
+ filename?: undefined;
26
+ } | {
27
+ type: string;
28
+ file_url: string;
29
+ file_id?: undefined;
30
+ file_data?: undefined;
31
+ filename?: undefined;
32
+ } | {
33
+ type: string;
34
+ file_data: string;
35
+ filename: string;
36
+ file_id?: undefined;
37
+ file_url?: undefined;
38
+ };
39
+ }
@@ -0,0 +1,23 @@
1
+ import { refToBase64, toDataUri, openAiImageUrl, attachmentFilename } from "../../../util/attachments.js";
2
+ /** Renders parts for the OpenAI Responses API. */
3
+ export class OpenAIResponsesRenderer {
4
+ text(part) {
5
+ return { type: "input_text", text: part.text };
6
+ }
7
+ image(part) {
8
+ if (part.source.kind === "providerFile") {
9
+ return { type: "input_image", file_id: part.source.id };
10
+ }
11
+ return { type: "input_image", image_url: openAiImageUrl(part.source), detail: "auto" };
12
+ }
13
+ file(part) {
14
+ if (part.source.kind === "providerFile") {
15
+ return { type: "input_file", file_id: part.source.id };
16
+ }
17
+ if (part.source.kind === "url") {
18
+ return { type: "input_file", file_url: part.source.url };
19
+ }
20
+ const { base64, mimeType } = refToBase64(part.source);
21
+ return { type: "input_file", file_data: toDataUri(base64, mimeType), filename: attachmentFilename(part.filename) };
22
+ }
23
+ }
@@ -0,0 +1,14 @@
1
+ import type { TextPart, ImagePart, FilePart, UserContentPart } from "../contentParts.js";
2
+ /**
3
+ * A per-provider renderer for user-content parts. Each provider implements one
4
+ * of these (one class per file, in this directory) so the message class stays
5
+ * thin: the serializer picks a renderer and walks the parts with {@link renderParts}.
6
+ * `text`/`image`/`file` return that provider's native representation of a part.
7
+ */
8
+ export interface PartRenderer<T> {
9
+ text(part: TextPart): T;
10
+ image(part: ImagePart): T;
11
+ file(part: FilePart): T;
12
+ }
13
+ /** Walk content parts, dispatching each to the renderer's matching method. */
14
+ export declare function renderParts<T>(parts: UserContentPart[], renderer: PartRenderer<T>): T[];
@@ -0,0 +1,16 @@
1
+ /** Walk content parts, dispatching each to the renderer's matching method. */
2
+ export function renderParts(parts, renderer) {
3
+ const out = [];
4
+ for (const part of parts) {
5
+ if (part.type === "text") {
6
+ out.push(renderer.text(part));
7
+ }
8
+ else if (part.type === "image") {
9
+ out.push(renderer.image(part));
10
+ }
11
+ else {
12
+ out.push(renderer.file(part));
13
+ }
14
+ }
15
+ return out;
16
+ }
@@ -1,8 +1,31 @@
1
+ import type { MessageParam, Tool } from "@anthropic-ai/sdk/resources/messages.js";
1
2
  import { PromptResult, Result, SmolClient, SmolConfig, StreamChunk, HostedToolResult } from "../types.js";
2
3
  import { BaseClient } from "./baseClient.js";
3
4
  import { ModelName } from "../models.js";
4
5
  export declare function anthropicWebSearchEntries(hostedTools?: string[]): any[];
5
6
  export declare function parseAnthropicHostedTools(response: any, provider: string): HostedToolResult[];
7
+ type EphemeralCacheControl = {
8
+ type: "ephemeral";
9
+ };
10
+ type SystemBlock = {
11
+ type: "text";
12
+ text: string;
13
+ cache_control?: EphemeralCacheControl;
14
+ };
15
+ type AnthropicRequestShape = {
16
+ system: string | SystemBlock[] | undefined;
17
+ messages: MessageParam[];
18
+ tools: Tool[] | undefined;
19
+ };
20
+ /**
21
+ * Attach ephemeral cache_control breakpoints to (up to) three places:
22
+ * 1. the last tool definition
23
+ * 2. the last system block (promoting system from string to array form)
24
+ * 3. the last block of the last user message
25
+ *
26
+ * Anthropic enforces minimum prefix sizes; smaller prefixes silently no-op.
27
+ */
28
+ export declare function applyCacheBreakpoints(req: AnthropicRequestShape): AnthropicRequestShape;
6
29
  export type SmolAnthropicConfig = SmolConfig;
7
30
  export declare class SmolAnthropic extends BaseClient implements SmolClient {
8
31
  private client;
@@ -23,3 +46,4 @@ export declare class SmolAnthropic extends BaseClient implements SmolClient {
23
46
  _textSync(config: SmolConfig): Promise<Result<PromptResult>>;
24
47
  _textStream(config: SmolConfig): AsyncGenerator<StreamChunk>;
25
48
  }
49
+ export {};
@@ -2,6 +2,7 @@ import Anthropic from "@anthropic-ai/sdk";
2
2
  import { ToolCall } from "../classes/ToolCall.js";
3
3
  import { SystemMessage, DeveloperMessage } from "../classes/message/index.js";
4
4
  import { getLogger } from "../util/logger.js";
5
+ import { redactAttachments } from "../util/redact.js";
5
6
  import { success, } from "../types.js";
6
7
  import { WEB_SEARCH, webSearchResult, applyHostedToolCost } from "../util/hostedTools.js";
7
8
  import { zodToAnthropicTool } from "../util/tool.js";
@@ -75,7 +76,7 @@ function thinkingStyleFor(modelName, modelData) {
75
76
  *
76
77
  * Anthropic enforces minimum prefix sizes; smaller prefixes silently no-op.
77
78
  */
78
- function applyCacheBreakpoints(req) {
79
+ export function applyCacheBreakpoints(req) {
79
80
  const cc = { type: "ephemeral" };
80
81
  // Tools: mark the last tool.
81
82
  let tools = req.tools;
@@ -293,7 +294,7 @@ export class SmolAnthropic extends BaseClient {
293
294
  thinking,
294
295
  output_config: outputConfig,
295
296
  };
296
- this.logger.debug("Sending request to Anthropic:", debugData);
297
+ this.logger.debug("Sending request to Anthropic:", redactAttachments(debugData));
297
298
  this.statelogClient?.promptRequest(debugData);
298
299
  const signal = this.getAbortSignal(config);
299
300
  let response;
@@ -362,7 +363,7 @@ export class SmolAnthropic extends BaseClient {
362
363
  thinking,
363
364
  output_config: outputConfig,
364
365
  };
365
- this.logger.debug("Sending streaming request to Anthropic:", streamDebugData);
366
+ this.logger.debug("Sending streaming request to Anthropic:", redactAttachments(streamDebugData));
366
367
  this.statelogClient?.promptRequest(streamDebugData);
367
368
  const signal = this.getAbortSignal(config);
368
369
  let stream;
@@ -13,6 +13,13 @@ export declare class BaseClient implements SmolClient {
13
13
  stream?: false;
14
14
  }): Promise<Result<PromptResult>>;
15
15
  checkMessageLimit(promptConfig: SmolConfig): Result<PromptResult> | null;
16
+ /**
17
+ * Gate on input modalities and resolve any image/PDF attachment refs
18
+ * (path/url/bytes → base64) before the synchronous serializers run. Returns
19
+ * the (possibly rewritten) config on success, or a Failure to surface. Shared
20
+ * by textSync and textStream so the two paths can't diverge.
21
+ */
22
+ protected prepareAttachments(config: SmolConfig): Promise<Result<SmolConfig>>;
16
23
  textSync(promptConfig: SmolConfig): Promise<Result<PromptResult>>;
17
24
  checkForToolLoops(promptConfig: SmolConfig): {
18
25
  continue: boolean;
@@ -5,6 +5,9 @@ import { getStatelogClient } from "../statelogClient.js";
5
5
  import { stripCodeFence } from "../util/util.js";
6
6
  import { success, failure, } from "../types.js";
7
7
  import { validateHostedTools } from "../util/hostedTools.js";
8
+ import { resolveMessageAttachments, messagesHaveAttachments, DEFAULT_MAX_ATTACHMENT_BYTES } from "./resolveAttachments.js";
9
+ import { validateModalities } from "../util/modalities.js";
10
+ import { resolveProvider } from "../util/provider.js";
8
11
  import { z } from "zod";
9
12
  const DEFAULT_NUM_RETRIES = 2;
10
13
  export class BaseClient {
@@ -52,10 +55,40 @@ export class BaseClient {
52
55
  }
53
56
  return null;
54
57
  }
58
+ /**
59
+ * Gate on input modalities and resolve any image/PDF attachment refs
60
+ * (path/url/bytes → base64) before the synchronous serializers run. Returns
61
+ * the (possibly rewritten) config on success, or a Failure to surface. Shared
62
+ * by textSync and textStream so the two paths can't diverge.
63
+ */
64
+ async prepareAttachments(config) {
65
+ const modalityResult = validateModalities(config);
66
+ if (modalityResult && modalityResult.success === false) {
67
+ return modalityResult;
68
+ }
69
+ if (!messagesHaveAttachments(config.messages)) {
70
+ return success(config);
71
+ }
72
+ const provider = resolveProvider(config.model, config.provider, config.modelData);
73
+ const maxBytes = config.attachments?.maxBytes ?? DEFAULT_MAX_ATTACHMENT_BYTES;
74
+ const resolved = await resolveMessageAttachments(config.messages, {
75
+ provider,
76
+ maxBytes,
77
+ });
78
+ if (!resolved.success) {
79
+ return resolved;
80
+ }
81
+ return success({ ...config, messages: resolved.value });
82
+ }
55
83
  async textSync(promptConfig) {
56
84
  const messageLimitResult = this.checkMessageLimit(promptConfig);
57
85
  if (messageLimitResult)
58
86
  return messageLimitResult;
87
+ const prepared = await this.prepareAttachments(promptConfig);
88
+ if (!prepared.success) {
89
+ return prepared;
90
+ }
91
+ promptConfig = prepared.value;
59
92
  const hostedError = validateHostedTools(promptConfig.hostedTools, promptConfig.model, promptConfig.provider, promptConfig.modelData);
60
93
  if (hostedError) {
61
94
  return failure(hostedError);
@@ -269,6 +302,12 @@ export class BaseClient {
269
302
  };
270
303
  return;
271
304
  }
305
+ const prepared = await this.prepareAttachments(config);
306
+ if (!prepared.success) {
307
+ yield { type: "error", error: prepared.error };
308
+ return;
309
+ }
310
+ config = prepared.value;
272
311
  const hostedError = validateHostedTools(config.hostedTools, config.model, config.provider, config.modelData);
273
312
  if (hostedError) {
274
313
  yield { type: "error", error: hostedError };
@@ -1,6 +1,7 @@
1
1
  import { GoogleGenAI } from "@google/genai";
2
2
  import { ToolCall } from "../classes/ToolCall.js";
3
3
  import { getLogger } from "../util/logger.js";
4
+ import { redactAttachments } from "../util/redact.js";
4
5
  import { addCosts, addTokenUsage, success, } from "../types.js";
5
6
  import { zodToGoogleTool } from "../util/tool.js";
6
7
  import { SmolContentPolicyError, SmolContextWindowExceededError, smolErrorForStatus, } from "../smolError.js";
@@ -248,7 +249,7 @@ export class SmolGoogle extends BaseClient {
248
249
  });
249
250
  }
250
251
  async __textSync(request) {
251
- this.logger.debug("Sending request to Google Gemini:", JSON.stringify(request, null, 2));
252
+ this.logger.debug("Sending request to Google Gemini:", JSON.stringify(redactAttachments(request), null, 2));
252
253
  this.statelogClient?.promptRequest(request);
253
254
  let result;
254
255
  try {
@@ -326,7 +327,7 @@ export class SmolGoogle extends BaseClient {
326
327
  request.config.responseMimeType = undefined;
327
328
  request.config.responseJsonSchema = undefined;
328
329
  }
329
- this.logger.debug("Sending streaming request to Google Gemini:", JSON.stringify(request, null, 2));
330
+ this.logger.debug("Sending streaming request to Google Gemini:", JSON.stringify(redactAttachments(request), null, 2));
330
331
  this.statelogClient?.promptRequest(request);
331
332
  let stream;
332
333
  try {
@@ -1,6 +1,7 @@
1
1
  import { Ollama } from "ollama";
2
2
  import { ToolCall } from "../classes/ToolCall.js";
3
3
  import { getLogger } from "../util/logger.js";
4
+ import { redactAttachments } from "../util/redact.js";
4
5
  import { success, } from "../types.js";
5
6
  import { zodToGoogleTool } from "../util/tool.js";
6
7
  import { sanitizeAttributes } from "../util/util.js";
@@ -66,14 +67,14 @@ export class SmolOllama extends BaseClient {
66
67
  throw error;
67
68
  }
68
69
  async _textSync(config) {
69
- const messages = config.messages.map((msg) => msg.toOpenAIMessage());
70
+ const messages = config.messages.map((msg) => msg.toOllamaMessage());
70
71
  const tools = (config.tools || []).map((tool) => {
71
72
  return zodToGoogleTool(tool.name, tool.schema, {
72
73
  description: tool.description,
73
74
  });
74
75
  });
75
76
  const request = {
76
- messages: messages,
77
+ messages,
77
78
  model: this.getModel(),
78
79
  };
79
80
  if (tools.length > 0) {
@@ -83,7 +84,7 @@ export class SmolOllama extends BaseClient {
83
84
  request.format = config.responseFormat.toJSONSchema();
84
85
  }
85
86
  Object.assign(request, sanitizeAttributes(config.rawAttributes));
86
- this.logger.debug("Sending request to Ollama:", JSON.stringify(request, null, 2));
87
+ this.logger.debug("Sending request to Ollama:", JSON.stringify(redactAttachments(request), null, 2));
87
88
  this.statelogClient?.promptRequest(request);
88
89
  const signal = this.getAbortSignal(config);
89
90
  const abortHandler = signal ? () => this.client.abort() : undefined;
@@ -119,14 +120,14 @@ export class SmolOllama extends BaseClient {
119
120
  return success({ output, toolCalls, usage, cost, model: this.getModel() });
120
121
  }
121
122
  async *_textStream(config) {
122
- const messages = config.messages.map((msg) => msg.toOpenAIMessage());
123
+ const messages = config.messages.map((msg) => msg.toOllamaMessage());
123
124
  const tools = (config.tools || []).map((tool) => {
124
125
  return zodToGoogleTool(tool.name, tool.schema, {
125
126
  description: tool.description,
126
127
  });
127
128
  });
128
129
  const request = {
129
- messages: messages,
130
+ messages,
130
131
  model: this.getModel(),
131
132
  stream: true,
132
133
  };
@@ -137,7 +138,7 @@ export class SmolOllama extends BaseClient {
137
138
  request.format = config.responseFormat.toJSONSchema();
138
139
  }
139
140
  Object.assign(request, sanitizeAttributes(config.rawAttributes));
140
- this.logger.debug("Sending streaming request to Ollama:", JSON.stringify(request, null, 2));
141
+ this.logger.debug("Sending streaming request to Ollama:", JSON.stringify(redactAttachments(request), null, 2));
141
142
  this.statelogClient?.promptRequest(request);
142
143
  const signal = this.getAbortSignal(config);
143
144
  const abortHandler = signal ? () => this.client.abort() : undefined;
@@ -3,6 +3,7 @@ import { success, } from "../types.js";
3
3
  import { ToolCall } from "../classes/ToolCall.js";
4
4
  import { isFunctionToolCall, sanitizeAttributes } from "../util/util.js";
5
5
  import { getLogger } from "../util/logger.js";
6
+ import { redactAttachments } from "../util/redact.js";
6
7
  import { BaseClient } from "./baseClient.js";
7
8
  import { SmolContentPolicyError, SmolContextWindowExceededError, smolErrorForStatus, } from "../smolError.js";
8
9
  import { extractHttpErrorFields } from "../util/httpError.js";
@@ -140,7 +141,7 @@ export class SmolOpenAi extends BaseClient {
140
141
  }
141
142
  async _textSync(config) {
142
143
  const request = this.buildRequest(config);
143
- this.logger.debug("Sending request to OpenAI:", JSON.stringify(request, null, 2));
144
+ this.logger.debug("Sending request to OpenAI:", JSON.stringify(redactAttachments(request), null, 2));
144
145
  this.statelogClient?.promptRequest(request);
145
146
  const signal = this.getAbortSignal(config);
146
147
  let completion;
@@ -195,7 +196,7 @@ export class SmolOpenAi extends BaseClient {
195
196
  }
196
197
  async *_textStream(config) {
197
198
  const request = this.buildRequest(config);
198
- this.logger.debug("Sending streaming request to OpenAI:", JSON.stringify(request, null, 2));
199
+ this.logger.debug("Sending streaming request to OpenAI:", JSON.stringify(redactAttachments(request), null, 2));
199
200
  this.statelogClient?.promptRequest(request);
200
201
  const signal = this.getAbortSignal(config);
201
202
  let completion;
@@ -2,6 +2,7 @@ import OpenAI from "openai";
2
2
  import { success, } from "../types.js";
3
3
  import { ToolCall } from "../classes/ToolCall.js";
4
4
  import { getLogger } from "../util/logger.js";
5
+ import { redactAttachments } from "../util/redact.js";
5
6
  import { BaseClient } from "./baseClient.js";
6
7
  import { zodToOpenAIResponsesTool } from "../util/tool.js";
7
8
  import { sanitizeAttributes } from "../util/util.js";
@@ -166,7 +167,7 @@ export class SmolOpenAiResponses extends BaseClient {
166
167
  }
167
168
  async _textSync(config) {
168
169
  const request = this.buildRequest(config);
169
- this.logger.debug("Sending request to OpenAI Responses API:", JSON.stringify(request, null, 2));
170
+ this.logger.debug("Sending request to OpenAI Responses API:", JSON.stringify(redactAttachments(request), null, 2));
170
171
  this.statelogClient?.promptRequest(request);
171
172
  const signal = this.getAbortSignal(config);
172
173
  let response;
@@ -205,7 +206,7 @@ export class SmolOpenAiResponses extends BaseClient {
205
206
  }
206
207
  async *_textStream(config) {
207
208
  const request = this.buildRequest(config);
208
- this.logger.debug("Sending streaming request to OpenAI Responses API:", JSON.stringify(request, null, 2));
209
+ this.logger.debug("Sending streaming request to OpenAI Responses API:", JSON.stringify(redactAttachments(request), null, 2));
209
210
  this.statelogClient?.promptRequest(request);
210
211
  const signal = this.getAbortSignal(config);
211
212
  let stream;
@@ -0,0 +1,11 @@
1
+ import { Message } from "../classes/message/index.js";
2
+ import { Result } from "../types.js";
3
+ export declare const DEFAULT_MAX_ATTACHMENT_BYTES: number;
4
+ /** Whether any user message carries an image/file attachment part. */
5
+ export declare function messagesHaveAttachments(messages: Message[]): boolean;
6
+ /** Whether `provider` accepts a remote URL directly for this part type. */
7
+ export declare function acceptsRemoteUrl(provider: string, partType: "image" | "file"): boolean;
8
+ export declare function resolveMessageAttachments(messages: Message[], options: {
9
+ provider: string;
10
+ maxBytes: number;
11
+ }): Promise<Result<Message[]>>;
@@ -0,0 +1,108 @@
1
+ import { UserMessage } from "../classes/message/index.js";
2
+ import { normalizeImageRef } from "../util/imageRef.js";
3
+ import { fileFamily } from "../util/attachments.js";
4
+ import { success, failure } from "../types.js";
5
+ export const DEFAULT_MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
6
+ const URL_IMAGE_PROVIDERS = new Set([
7
+ "openai",
8
+ "openai-responses",
9
+ "openai-compat",
10
+ "openrouter",
11
+ "deepinfra",
12
+ "litellm",
13
+ "anthropic",
14
+ ]);
15
+ const URL_PDF_PROVIDERS = new Set(["openai-responses", "anthropic"]);
16
+ /** Whether any user message carries an image/file attachment part. */
17
+ export function messagesHaveAttachments(messages) {
18
+ for (const msg of messages) {
19
+ if (!(msg instanceof UserMessage)) {
20
+ continue;
21
+ }
22
+ const parts = msg.getContentParts();
23
+ if (parts === null) {
24
+ continue;
25
+ }
26
+ for (const part of parts) {
27
+ if (part.type === "image" || part.type === "file") {
28
+ return true;
29
+ }
30
+ }
31
+ }
32
+ return false;
33
+ }
34
+ /** Whether `provider` accepts a remote URL directly for this part type. */
35
+ export function acceptsRemoteUrl(provider, partType) {
36
+ if (partType === "image") {
37
+ return URL_IMAGE_PROVIDERS.has(provider);
38
+ }
39
+ return URL_PDF_PROVIDERS.has(provider);
40
+ }
41
+ export async function resolveMessageAttachments(messages, options) {
42
+ const out = [];
43
+ for (const msg of messages) {
44
+ if (!(msg instanceof UserMessage)) {
45
+ out.push(msg);
46
+ continue;
47
+ }
48
+ const parts = msg.getContentParts();
49
+ if (parts === null) {
50
+ out.push(msg);
51
+ continue;
52
+ }
53
+ const resolvedParts = [];
54
+ for (const part of parts) {
55
+ if (part.type === "text") {
56
+ resolvedParts.push(part);
57
+ continue;
58
+ }
59
+ // Provider file references are validated and passed through (no download/cap).
60
+ if (part.source.kind === "providerFile") {
61
+ const family = fileFamily(options.provider);
62
+ if (family === null || part.source.provider !== family) {
63
+ return failure(`Attachment references a "${part.source.provider}" file, but this call targets provider ` +
64
+ `"${options.provider}" (file family ${family ?? "none"}).`);
65
+ }
66
+ if (part.type === "image" && options.provider === "openai") {
67
+ return failure("An image file reference requires the openai-responses provider (OpenAI Chat Completions has no image-by-file_id form).");
68
+ }
69
+ resolvedParts.push(part);
70
+ continue;
71
+ }
72
+ // Passthrough: keep a url ref when the target provider accepts a remote URL.
73
+ if (part.source.kind === "url" && acceptsRemoteUrl(options.provider, part.type)) {
74
+ resolvedParts.push(part);
75
+ continue;
76
+ }
77
+ let allowed;
78
+ if (part.type === "image") {
79
+ allowed = ["image/"];
80
+ }
81
+ else {
82
+ allowed = ["application/pdf"];
83
+ }
84
+ try {
85
+ const { data, mimeType } = await normalizeImageRef(part.source, {
86
+ allowedMimePrefixes: allowed,
87
+ maxBytes: options.maxBytes,
88
+ });
89
+ const source = {
90
+ kind: "base64",
91
+ base64: Buffer.from(data).toString("base64"),
92
+ mimeType,
93
+ };
94
+ if (part.type === "image") {
95
+ resolvedParts.push({ type: "image", source });
96
+ }
97
+ else {
98
+ resolvedParts.push({ type: "file", source, filename: part.filename });
99
+ }
100
+ }
101
+ catch (err) {
102
+ return failure(`Failed to load ${part.type} attachment: ${err.message}`);
103
+ }
104
+ }
105
+ out.push(new UserMessage(resolvedParts, { name: msg.name, rawData: msg.rawData }));
106
+ }
107
+ return success(out);
108
+ }