smoltalk 0.6.0 → 0.7.1
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/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/clients/anthropic.d.ts +24 -0
- package/dist/clients/anthropic.js +4 -3
- package/dist/clients/baseClient.d.ts +7 -0
- package/dist/clients/baseClient.js +39 -0
- package/dist/clients/google.js +3 -2
- package/dist/clients/ollama.js +7 -6
- package/dist/clients/openai.js +3 -2
- package/dist/clients/openaiResponses.js +3 -2
- package/dist/clients/resolveAttachments.d.ts +11 -0
- package/dist/clients/resolveAttachments.js +108 -0
- 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/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/models.d.ts +17 -2
- package/dist/models.js +29 -2
- package/dist/statelogClient.js +2 -1
- package/dist/types.d.ts +6 -8
- 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/redact.d.ts +8 -0
- package/dist/util/redact.js +42 -0
- 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 };
|
package/dist/clients/google.js
CHANGED
|
@@ -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 {
|
package/dist/clients/ollama.js
CHANGED
|
@@ -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.
|
|
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
|
|
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.
|
|
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
|
|
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;
|
package/dist/clients/openai.js
CHANGED
|
@@ -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
|
+
}
|