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.
- package/README.md +67 -10
- package/dist/classes/message/UserMessage.d.ts +66 -5
- package/dist/classes/message/UserMessage.js +100 -21
- package/dist/classes/message/contentParts.d.ts +259 -0
- package/dist/classes/message/contentParts.js +44 -0
- package/dist/classes/message/index.d.ts +8 -2
- package/dist/classes/message/index.js +12 -0
- package/dist/classes/message/renderers/AnthropicRenderer.d.ts +43 -0
- package/dist/classes/message/renderers/AnthropicRenderer.js +19 -0
- package/dist/classes/message/renderers/GoogleRenderer.d.ts +35 -0
- package/dist/classes/message/renderers/GoogleRenderer.js +26 -0
- package/dist/classes/message/renderers/JSONRenderer.d.ts +8 -0
- package/dist/classes/message/renderers/JSONRenderer.js +21 -0
- package/dist/classes/message/renderers/OpenAIChatRenderer.d.ts +30 -0
- package/dist/classes/message/renderers/OpenAIChatRenderer.js +20 -0
- package/dist/classes/message/renderers/OpenAIResponsesRenderer.d.ts +39 -0
- package/dist/classes/message/renderers/OpenAIResponsesRenderer.js +23 -0
- package/dist/classes/message/renderers/PartRenderer.d.ts +14 -0
- package/dist/classes/message/renderers/PartRenderer.js +16 -0
- package/dist/client.d.ts +4 -0
- package/dist/client.js +37 -16
- package/dist/clients/anthropic.d.ts +24 -2
- package/dist/clients/anthropic.js +9 -4
- package/dist/clients/baseClient.d.ts +7 -0
- package/dist/clients/baseClient.js +39 -0
- package/dist/clients/deepinfra.d.ts +17 -0
- package/dist/clients/deepinfra.js +27 -0
- package/dist/clients/google.js +6 -4
- package/dist/clients/litellm.d.ts +19 -0
- package/dist/clients/litellm.js +32 -0
- package/dist/clients/ollama.js +12 -9
- package/dist/clients/openai.d.ts +41 -5
- package/dist/clients/openai.js +78 -14
- package/dist/clients/openaiCompat.d.ts +22 -0
- package/dist/clients/openaiCompat.js +33 -0
- package/dist/clients/openaiResponses.js +6 -4
- package/dist/clients/openrouter.d.ts +23 -0
- package/dist/clients/openrouter.js +76 -0
- package/dist/clients/resolveAttachments.d.ts +11 -0
- package/dist/clients/resolveAttachments.js +108 -0
- package/dist/embed/openai.d.ts +7 -1
- package/dist/embed/openai.js +8 -2
- package/dist/embed.d.ts +18 -4
- package/dist/embed.js +32 -4
- package/dist/files/BaseFileProvider.d.ts +29 -0
- package/dist/files/BaseFileProvider.js +38 -0
- package/dist/files/anthropic.d.ts +16 -0
- package/dist/files/anthropic.js +20 -0
- package/dist/files/google.d.ts +18 -0
- package/dist/files/google.js +38 -0
- package/dist/files/openai.d.ts +16 -0
- package/dist/files/openai.js +23 -0
- package/dist/files.d.ts +46 -0
- package/dist/files.js +70 -0
- package/dist/image/openaiCompat.d.ts +9 -0
- package/dist/image/openaiCompat.js +60 -0
- package/dist/image.d.ts +13 -2
- package/dist/image.js +28 -3
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/models.d.ts +22 -3
- package/dist/models.js +46 -3
- package/dist/statelogClient.js +2 -1
- package/dist/types.d.ts +28 -18
- package/dist/types.js +1 -4
- package/dist/util/attachments.d.ts +34 -0
- package/dist/util/attachments.js +62 -0
- package/dist/util/hostedTools.js +5 -1
- package/dist/util/imageRef.d.ts +16 -1
- package/dist/util/imageRef.js +95 -10
- package/dist/util/modalities.d.ts +2 -0
- package/dist/util/modalities.js +31 -0
- package/dist/util/provider.d.ts +32 -6
- package/dist/util/provider.js +38 -4
- package/dist/util/redact.d.ts +8 -0
- package/dist/util/redact.js +42 -0
- 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:
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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.
|
|
53
|
-
throw new SmolError("No Anthropic API key provided. Please provide an Anthropic API key in the config using
|
|
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.
|
|
61
|
-
throw new SmolError("No OpenAI API key provided. Please provide an OpenAI API key in the config using
|
|
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.
|
|
66
|
-
throw new SmolError("No OpenAI API key provided. Please provide an OpenAI API key in the config using
|
|
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.
|
|
71
|
-
throw new SmolError("No Google API key provided. Please provide a Google API key in the config using
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
+
}
|