smoltalk 0.5.1 → 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 (77) hide show
  1. package/README.md +67 -10
  2. package/dist/classes/message/UserMessage.d.ts +66 -5
  3. package/dist/classes/message/UserMessage.js +100 -21
  4. package/dist/classes/message/contentParts.d.ts +259 -0
  5. package/dist/classes/message/contentParts.js +44 -0
  6. package/dist/classes/message/index.d.ts +8 -2
  7. package/dist/classes/message/index.js +12 -0
  8. package/dist/classes/message/renderers/AnthropicRenderer.d.ts +43 -0
  9. package/dist/classes/message/renderers/AnthropicRenderer.js +19 -0
  10. package/dist/classes/message/renderers/GoogleRenderer.d.ts +35 -0
  11. package/dist/classes/message/renderers/GoogleRenderer.js +26 -0
  12. package/dist/classes/message/renderers/JSONRenderer.d.ts +8 -0
  13. package/dist/classes/message/renderers/JSONRenderer.js +21 -0
  14. package/dist/classes/message/renderers/OpenAIChatRenderer.d.ts +30 -0
  15. package/dist/classes/message/renderers/OpenAIChatRenderer.js +20 -0
  16. package/dist/classes/message/renderers/OpenAIResponsesRenderer.d.ts +39 -0
  17. package/dist/classes/message/renderers/OpenAIResponsesRenderer.js +23 -0
  18. package/dist/classes/message/renderers/PartRenderer.d.ts +14 -0
  19. package/dist/classes/message/renderers/PartRenderer.js +16 -0
  20. package/dist/client.d.ts +4 -0
  21. package/dist/client.js +37 -16
  22. package/dist/clients/anthropic.d.ts +24 -2
  23. package/dist/clients/anthropic.js +9 -4
  24. package/dist/clients/baseClient.d.ts +7 -0
  25. package/dist/clients/baseClient.js +39 -0
  26. package/dist/clients/deepinfra.d.ts +17 -0
  27. package/dist/clients/deepinfra.js +27 -0
  28. package/dist/clients/google.js +6 -4
  29. package/dist/clients/litellm.d.ts +19 -0
  30. package/dist/clients/litellm.js +32 -0
  31. package/dist/clients/ollama.js +12 -9
  32. package/dist/clients/openai.d.ts +41 -5
  33. package/dist/clients/openai.js +78 -14
  34. package/dist/clients/openaiCompat.d.ts +22 -0
  35. package/dist/clients/openaiCompat.js +33 -0
  36. package/dist/clients/openaiResponses.js +6 -4
  37. package/dist/clients/openrouter.d.ts +23 -0
  38. package/dist/clients/openrouter.js +76 -0
  39. package/dist/clients/resolveAttachments.d.ts +11 -0
  40. package/dist/clients/resolveAttachments.js +108 -0
  41. package/dist/embed/openai.d.ts +7 -1
  42. package/dist/embed/openai.js +8 -2
  43. package/dist/embed.d.ts +18 -4
  44. package/dist/embed.js +32 -4
  45. package/dist/files/BaseFileProvider.d.ts +29 -0
  46. package/dist/files/BaseFileProvider.js +38 -0
  47. package/dist/files/anthropic.d.ts +16 -0
  48. package/dist/files/anthropic.js +20 -0
  49. package/dist/files/google.d.ts +18 -0
  50. package/dist/files/google.js +38 -0
  51. package/dist/files/openai.d.ts +16 -0
  52. package/dist/files/openai.js +23 -0
  53. package/dist/files.d.ts +46 -0
  54. package/dist/files.js +70 -0
  55. package/dist/image/openaiCompat.d.ts +9 -0
  56. package/dist/image/openaiCompat.js +60 -0
  57. package/dist/image.d.ts +13 -2
  58. package/dist/image.js +28 -3
  59. package/dist/index.d.ts +4 -0
  60. package/dist/index.js +3 -0
  61. package/dist/models.d.ts +22 -3
  62. package/dist/models.js +46 -3
  63. package/dist/statelogClient.js +2 -1
  64. package/dist/types.d.ts +28 -18
  65. package/dist/types.js +1 -4
  66. package/dist/util/attachments.d.ts +34 -0
  67. package/dist/util/attachments.js +62 -0
  68. package/dist/util/hostedTools.js +5 -1
  69. package/dist/util/imageRef.d.ts +16 -1
  70. package/dist/util/imageRef.js +95 -10
  71. package/dist/util/modalities.d.ts +2 -0
  72. package/dist/util/modalities.js +31 -0
  73. package/dist/util/provider.d.ts +32 -6
  74. package/dist/util/provider.js +38 -4
  75. package/dist/util/redact.d.ts +8 -0
  76. package/dist/util/redact.js +42 -0
  77. package/package.json +1 -1
@@ -8,7 +8,8 @@ import type { AssistantMessageJSON } from "./AssistantMessage.js";
8
8
  import type { DeveloperMessageJSON } from "./DeveloperMessage.js";
9
9
  import type { SystemMessageJSON } from "./SystemMessage.js";
10
10
  import type { ToolMessageJSON } from "./ToolMessage.js";
11
- import { CostEstimate, TextPart, TokenUsage } from "../../types.js";
11
+ import { CostEstimate, TextPart, TokenUsage, UserContentInput, ImagePart, FilePart } from "../../types.js";
12
+ import type { ImageRef } from "../../util/imageRef.js";
12
13
  export * from "./AssistantMessage.js";
13
14
  export * from "./BaseMessage.js";
14
15
  export * from "./DeveloperMessage.js";
@@ -16,10 +17,15 @@ export * from "./SystemMessage.js";
16
17
  export * from "./ToolMessage.js";
17
18
  export * from "./UserMessage.js";
18
19
  export declare function messageFromJSON(json: MessageJSON): Message;
19
- export declare function userMessage(content: string, options?: {
20
+ export declare function userMessage(content: UserContentInput, options?: {
20
21
  name?: string;
21
22
  rawData?: any;
22
23
  }): UserMessage;
24
+ export declare function textPart(text: string): TextPart;
25
+ export declare function imagePart(source: ImageRef): ImagePart;
26
+ export declare function filePart(source: ImageRef, options?: {
27
+ filename?: string;
28
+ }): FilePart;
23
29
  export declare function assistantMessage(content: string | Array<TextPart> | null, options?: {
24
30
  name?: string;
25
31
  audio?: any | null;
@@ -28,6 +28,18 @@ export function messageFromJSON(json) {
28
28
  export function userMessage(content, options = {}) {
29
29
  return new UserMessage(content, options);
30
30
  }
31
+ export function textPart(text) {
32
+ return { type: "text", text };
33
+ }
34
+ export function imagePart(source) {
35
+ return { type: "image", source };
36
+ }
37
+ export function filePart(source, options = {}) {
38
+ if (options.filename === undefined) {
39
+ return { type: "file", source };
40
+ }
41
+ return { type: "file", source, filename: options.filename };
42
+ }
31
43
  export function assistantMessage(content, options = {}) {
32
44
  return new AssistantMessage(content, options);
33
45
  }
@@ -0,0 +1,43 @@
1
+ import type { PartRenderer } from "./PartRenderer.js";
2
+ import type { TextPart, ImagePart, FilePart } from "../contentParts.js";
3
+ /** Renders parts for the Anthropic Messages API. */
4
+ export declare class AnthropicRenderer implements PartRenderer<any> {
5
+ text(part: TextPart): {
6
+ type: string;
7
+ text: string;
8
+ };
9
+ image(part: ImagePart): {
10
+ type: string;
11
+ source: {
12
+ type: string;
13
+ file_id: string;
14
+ };
15
+ } | {
16
+ type: string;
17
+ source: {
18
+ type: "url";
19
+ url: string;
20
+ } | {
21
+ type: "base64";
22
+ media_type: string;
23
+ data: string;
24
+ };
25
+ };
26
+ file(part: FilePart): {
27
+ type: string;
28
+ source: {
29
+ type: string;
30
+ file_id: string;
31
+ };
32
+ } | {
33
+ type: string;
34
+ source: {
35
+ type: "url";
36
+ url: string;
37
+ } | {
38
+ type: "base64";
39
+ media_type: string;
40
+ data: string;
41
+ };
42
+ };
43
+ }
@@ -0,0 +1,19 @@
1
+ import { anthropicSource } from "../../../util/attachments.js";
2
+ /** Renders parts for the Anthropic Messages API. */
3
+ export class AnthropicRenderer {
4
+ text(part) {
5
+ return { type: "text", text: part.text };
6
+ }
7
+ image(part) {
8
+ if (part.source.kind === "providerFile") {
9
+ return { type: "image", source: { type: "file", file_id: part.source.id } };
10
+ }
11
+ return { type: "image", source: anthropicSource(part.source) };
12
+ }
13
+ file(part) {
14
+ if (part.source.kind === "providerFile") {
15
+ return { type: "document", source: { type: "file", file_id: part.source.id } };
16
+ }
17
+ return { type: "document", source: anthropicSource(part.source) };
18
+ }
19
+ }
@@ -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
+ }
package/dist/client.d.ts CHANGED
@@ -4,6 +4,10 @@ export * from "./clients/openai.js";
4
4
  export * from "./clients/openaiResponses.js";
5
5
  export * from "./clients/baseClient.js";
6
6
  export * from "./clients/ollama.js";
7
+ export * from "./clients/openaiCompat.js";
8
+ export * from "./clients/openrouter.js";
9
+ export * from "./clients/deepinfra.js";
10
+ export * from "./clients/litellm.js";
7
11
  import { BaseClient } from "./clients/baseClient.js";
8
12
  import { SmolClientConfig } from "./types.js";
9
13
  export declare function registerProvider(providerName: string, clientClass: typeof BaseClient): void;
package/dist/client.js CHANGED
@@ -4,11 +4,19 @@ export * from "./clients/openai.js";
4
4
  export * from "./clients/openaiResponses.js";
5
5
  export * from "./clients/baseClient.js";
6
6
  export * from "./clients/ollama.js";
7
+ export * from "./clients/openaiCompat.js";
8
+ export * from "./clients/openrouter.js";
9
+ export * from "./clients/deepinfra.js";
10
+ export * from "./clients/litellm.js";
7
11
  import { SmolAnthropic } from "./clients/anthropic.js";
8
12
  import { SmolGoogle } from "./clients/google.js";
9
13
  import { SmolOllama } from "./clients/ollama.js";
10
14
  import { SmolOpenAi } from "./clients/openai.js";
11
15
  import { SmolOpenAiResponses } from "./clients/openaiResponses.js";
16
+ import { SmolOpenAiCompat } from "./clients/openaiCompat.js";
17
+ import { SmolOpenRouter } from "./clients/openrouter.js";
18
+ import { SmolDeepInfra } from "./clients/deepinfra.js";
19
+ import { SmolLiteLlm } from "./clients/litellm.js";
12
20
  import { getModel, isTextModel } from "./models.js";
13
21
  import { SmolError } from "./smolError.js";
14
22
  import { resolveApiKey, resolveProvider } from "./util/provider.js";
@@ -36,43 +44,56 @@ export function getClient(config) {
36
44
  throw new SmolError(`Only text models are supported currently. ${modelName} is a ${model?.type} model.`);
37
45
  }
38
46
  }
47
+ // Resolve API keys from config or env vars and inject them back into the
48
+ // nested apiKey map. The client constructors also read env vars directly,
49
+ // but this lets getClient surface a clear error before construction.
39
50
  const resolvedKeys = {
40
- openAiApiKey: resolveApiKey("openai", config),
41
- googleApiKey: resolveApiKey("google", config),
42
- anthropicApiKey: resolveApiKey("anthropic", config),
51
+ openAi: resolveApiKey("openai", config),
52
+ google: resolveApiKey("google", config),
53
+ anthropic: resolveApiKey("anthropic", config),
54
+ ollama: resolveApiKey("ollama", config),
55
+ openRouter: resolveApiKey("openrouter", config),
56
+ deepInfra: resolveApiKey("deepinfra", config),
57
+ liteLlm: resolveApiKey("litellm", config),
58
+ openAiCompat: resolveApiKey("openai-compat", config),
43
59
  };
44
60
  const clientConfig = {
45
61
  messages: [],
46
62
  ...config,
47
- ...resolvedKeys,
63
+ apiKey: { ...resolvedKeys, ...config.apiKey },
48
64
  model: modelName,
49
65
  };
50
66
  switch (provider) {
51
67
  case "anthropic":
52
- if (!resolvedKeys.anthropicApiKey) {
53
- throw new SmolError("No Anthropic API key provided. Please provide an Anthropic API key in the config using anthropicApiKey, or set the ANTHROPIC_API_KEY environment variable.");
68
+ if (!resolvedKeys.anthropic) {
69
+ throw new SmolError("No Anthropic API key provided. Please provide an Anthropic API key in the config using apiKey.anthropic, or set the ANTHROPIC_API_KEY environment variable.");
54
70
  }
55
- return new SmolAnthropic({
56
- ...clientConfig,
57
- anthropicApiKey: resolvedKeys.anthropicApiKey,
58
- });
71
+ return new SmolAnthropic(clientConfig);
59
72
  case "openai":
60
- if (!resolvedKeys.openAiApiKey) {
61
- throw new SmolError("No OpenAI API key provided. Please provide an OpenAI API key in the config using openAiApiKey, or set the OPENAI_API_KEY environment variable.");
73
+ if (!resolvedKeys.openAi) {
74
+ throw new SmolError("No OpenAI API key provided. Please provide an OpenAI API key in the config using apiKey.openAi, or set the OPENAI_API_KEY environment variable.");
62
75
  }
63
76
  return new SmolOpenAi(clientConfig);
64
77
  case "openai-responses":
65
- if (!resolvedKeys.openAiApiKey) {
66
- throw new SmolError("No OpenAI API key provided. Please provide an OpenAI API key in the config using openAiApiKey, or set the OPENAI_API_KEY environment variable.");
78
+ if (!resolvedKeys.openAi) {
79
+ throw new SmolError("No OpenAI API key provided. Please provide an OpenAI API key in the config using apiKey.openAi, or set the OPENAI_API_KEY environment variable.");
67
80
  }
68
81
  return new SmolOpenAiResponses(clientConfig);
69
82
  case "google":
70
- if (!resolvedKeys.googleApiKey) {
71
- throw new SmolError("No Google API key provided. Please provide a Google API key in the config using googleApiKey, or set the GEMINI_API_KEY environment variable.");
83
+ if (!resolvedKeys.google) {
84
+ throw new SmolError("No Google API key provided. Please provide a Google API key in the config using apiKey.google, or set the GEMINI_API_KEY environment variable.");
72
85
  }
73
86
  return new SmolGoogle(clientConfig);
74
87
  case "ollama":
75
88
  return new SmolOllama(clientConfig);
89
+ case "openrouter":
90
+ return new SmolOpenRouter(clientConfig);
91
+ case "deepinfra":
92
+ return new SmolDeepInfra(clientConfig);
93
+ case "litellm":
94
+ return new SmolLiteLlm(clientConfig);
95
+ case "openai-compat":
96
+ return new SmolOpenAiCompat(clientConfig);
76
97
  default:
77
98
  if (provider in registeredProviders) {
78
99
  const ClientClass = registeredProviders[provider];
@@ -1,11 +1,32 @@
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[];
6
- export type SmolAnthropicConfig = SmolConfig & {
7
- anthropicApiKey: string;
7
+ type EphemeralCacheControl = {
8
+ type: "ephemeral";
8
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;
29
+ export type SmolAnthropicConfig = SmolConfig;
9
30
  export declare class SmolAnthropic extends BaseClient implements SmolClient {
10
31
  private client;
11
32
  private logger;
@@ -25,3 +46,4 @@ export declare class SmolAnthropic extends BaseClient implements SmolClient {
25
46
  _textSync(config: SmolConfig): Promise<Result<PromptResult>>;
26
47
  _textStream(config: SmolConfig): AsyncGenerator<StreamChunk>;
27
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;
@@ -149,7 +150,11 @@ export class SmolAnthropic extends BaseClient {
149
150
  model;
150
151
  constructor(config) {
151
152
  super(config);
152
- this.client = new Anthropic({ apiKey: config.anthropicApiKey });
153
+ const apiKey = config.apiKey?.anthropic || process.env.ANTHROPIC_API_KEY;
154
+ if (!apiKey) {
155
+ throw new Error("Anthropic API key is required for SmolAnthropic client.");
156
+ }
157
+ this.client = new Anthropic({ apiKey });
153
158
  this.logger = getLogger();
154
159
  this.model = new Model(config.model, undefined, config.modelData);
155
160
  }
@@ -289,7 +294,7 @@ export class SmolAnthropic extends BaseClient {
289
294
  thinking,
290
295
  output_config: outputConfig,
291
296
  };
292
- this.logger.debug("Sending request to Anthropic:", debugData);
297
+ this.logger.debug("Sending request to Anthropic:", redactAttachments(debugData));
293
298
  this.statelogClient?.promptRequest(debugData);
294
299
  const signal = this.getAbortSignal(config);
295
300
  let response;
@@ -358,7 +363,7 @@ export class SmolAnthropic extends BaseClient {
358
363
  thinking,
359
364
  output_config: outputConfig,
360
365
  };
361
- this.logger.debug("Sending streaming request to Anthropic:", streamDebugData);
366
+ this.logger.debug("Sending streaming request to Anthropic:", redactAttachments(streamDebugData));
362
367
  this.statelogClient?.promptRequest(streamDebugData);
363
368
  const signal = this.getAbortSignal(config);
364
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 };
@@ -0,0 +1,17 @@
1
+ import { SmolOpenAiCompat } from "./openaiCompat.js";
2
+ import type { SmolConfig } from "../types.js";
3
+ /**
4
+ * DeepInfra client (https://deepinfra.com). OpenAI-compatible chat completions
5
+ * at the `/v1/openai` base URL.
6
+ *
7
+ * - Baked base URL `https://api.deepinfra.com/v1/openai` (override via config.baseUrl.deepInfra).
8
+ * - Key: config.apiKey.deepInfra or env DEEPINFRA_API_KEY.
9
+ * - Cost: reads `usage.estimated_cost` (USD).
10
+ */
11
+ export declare class SmolDeepInfra extends SmolOpenAiCompat {
12
+ protected resolveClientOptions(config: SmolConfig): {
13
+ apiKey: string;
14
+ baseURL: string;
15
+ };
16
+ protected resolveCostUsd(usage: any): number | undefined;
17
+ }