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
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { SmolOpenAiCompat } from "./openaiCompat.js";
|
|
2
|
+
import { resolveApiKey, resolveBaseUrl } from "../util/provider.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 class SmolDeepInfra extends SmolOpenAiCompat {
|
|
12
|
+
resolveClientOptions(config) {
|
|
13
|
+
const apiKey = resolveApiKey("deepinfra", config);
|
|
14
|
+
// resolveBaseUrl bakes in the deepinfra default, so this is always defined.
|
|
15
|
+
const baseURL = resolveBaseUrl("deepinfra", config);
|
|
16
|
+
if (!apiKey) {
|
|
17
|
+
throw new Error("deepinfra: API key required (config.apiKey.deepInfra or DEEPINFRA_API_KEY).");
|
|
18
|
+
}
|
|
19
|
+
return { apiKey, baseURL };
|
|
20
|
+
}
|
|
21
|
+
resolveCostUsd(usage) {
|
|
22
|
+
// DeepInfra returns usage.estimated_cost (USD) on chat completions.
|
|
23
|
+
return typeof usage?.estimated_cost === "number"
|
|
24
|
+
? usage.estimated_cost
|
|
25
|
+
: undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
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";
|
|
@@ -68,10 +69,11 @@ export class SmolGoogle extends BaseClient {
|
|
|
68
69
|
model;
|
|
69
70
|
constructor(config) {
|
|
70
71
|
super(config);
|
|
71
|
-
|
|
72
|
+
const apiKey = config.apiKey?.google || process.env.GEMINI_API_KEY;
|
|
73
|
+
if (!apiKey) {
|
|
72
74
|
throw new Error("Google API key is required for SmolGoogle client.");
|
|
73
75
|
}
|
|
74
|
-
this.client = new GoogleGenAI({ apiKey
|
|
76
|
+
this.client = new GoogleGenAI({ apiKey });
|
|
75
77
|
this.logger = getLogger();
|
|
76
78
|
this.model = new Model(config.model, undefined, config.modelData);
|
|
77
79
|
}
|
|
@@ -247,7 +249,7 @@ export class SmolGoogle extends BaseClient {
|
|
|
247
249
|
});
|
|
248
250
|
}
|
|
249
251
|
async __textSync(request) {
|
|
250
|
-
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));
|
|
251
253
|
this.statelogClient?.promptRequest(request);
|
|
252
254
|
let result;
|
|
253
255
|
try {
|
|
@@ -325,7 +327,7 @@ export class SmolGoogle extends BaseClient {
|
|
|
325
327
|
request.config.responseMimeType = undefined;
|
|
326
328
|
request.config.responseJsonSchema = undefined;
|
|
327
329
|
}
|
|
328
|
-
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));
|
|
329
331
|
this.statelogClient?.promptRequest(request);
|
|
330
332
|
let stream;
|
|
331
333
|
try {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { SmolOpenAiCompat } from "./openaiCompat.js";
|
|
2
|
+
import type { SmolConfig } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* LiteLLM proxy client (https://docs.litellm.ai/docs/simple_proxy). OpenAI-
|
|
5
|
+
* compatible chat completions through a user-run LiteLLM proxy.
|
|
6
|
+
*
|
|
7
|
+
* - Base URL is REQUIRED from config.baseUrl.liteLlm or env LITELLM_BASE_URL.
|
|
8
|
+
* - Key: config.apiKey.liteLlm or env LITELLM_API_KEY.
|
|
9
|
+
* - Cost: reads the `x-litellm-response-cost` header (USD). Only available
|
|
10
|
+
* for non-streaming responses; streaming requests fall back to the
|
|
11
|
+
* smoltalk model registry.
|
|
12
|
+
*/
|
|
13
|
+
export declare class SmolLiteLlm extends SmolOpenAiCompat {
|
|
14
|
+
protected resolveClientOptions(config: SmolConfig): {
|
|
15
|
+
apiKey: string;
|
|
16
|
+
baseURL: string;
|
|
17
|
+
};
|
|
18
|
+
protected resolveCostUsd(_usage: any, rawResponse?: Response): number | undefined;
|
|
19
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { SmolOpenAiCompat } from "./openaiCompat.js";
|
|
2
|
+
import { resolveApiKey, resolveBaseUrl } from "../util/provider.js";
|
|
3
|
+
/**
|
|
4
|
+
* LiteLLM proxy client (https://docs.litellm.ai/docs/simple_proxy). OpenAI-
|
|
5
|
+
* compatible chat completions through a user-run LiteLLM proxy.
|
|
6
|
+
*
|
|
7
|
+
* - Base URL is REQUIRED from config.baseUrl.liteLlm or env LITELLM_BASE_URL.
|
|
8
|
+
* - Key: config.apiKey.liteLlm or env LITELLM_API_KEY.
|
|
9
|
+
* - Cost: reads the `x-litellm-response-cost` header (USD). Only available
|
|
10
|
+
* for non-streaming responses; streaming requests fall back to the
|
|
11
|
+
* smoltalk model registry.
|
|
12
|
+
*/
|
|
13
|
+
export class SmolLiteLlm extends SmolOpenAiCompat {
|
|
14
|
+
resolveClientOptions(config) {
|
|
15
|
+
const apiKey = resolveApiKey("litellm", config);
|
|
16
|
+
const baseURL = resolveBaseUrl("litellm", config);
|
|
17
|
+
if (!apiKey) {
|
|
18
|
+
throw new Error("litellm: API key required (config.apiKey.liteLlm or LITELLM_API_KEY).");
|
|
19
|
+
}
|
|
20
|
+
if (!baseURL) {
|
|
21
|
+
throw new Error("litellm: base URL required (config.baseUrl.liteLlm or LITELLM_BASE_URL).");
|
|
22
|
+
}
|
|
23
|
+
return { apiKey, baseURL };
|
|
24
|
+
}
|
|
25
|
+
resolveCostUsd(_usage, rawResponse) {
|
|
26
|
+
const header = rawResponse?.headers?.get?.("x-litellm-response-cost");
|
|
27
|
+
if (!header)
|
|
28
|
+
return undefined;
|
|
29
|
+
const n = Number(header);
|
|
30
|
+
return Number.isFinite(n) ? n : undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
package/dist/clients/ollama.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
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";
|
|
8
|
+
import { resolveBaseUrl } from "../util/provider.js";
|
|
7
9
|
import { BaseClient } from "./baseClient.js";
|
|
8
10
|
import { SmolContextWindowExceededError, smolErrorForStatus, } from "../smolError.js";
|
|
9
11
|
import { extractHttpErrorFields } from "../util/httpError.js";
|
|
@@ -17,14 +19,15 @@ export class SmolOllama extends BaseClient {
|
|
|
17
19
|
super(config);
|
|
18
20
|
this.logger = getLogger();
|
|
19
21
|
this.model = new Model(config.model, undefined, config.modelData);
|
|
20
|
-
|
|
22
|
+
const apiKey = config.apiKey?.ollama;
|
|
23
|
+
if (apiKey) {
|
|
21
24
|
this.client = new Ollama({
|
|
22
25
|
host: "https://cloud.ollama.com",
|
|
23
|
-
headers: { Authorization: "Bearer " +
|
|
26
|
+
headers: { Authorization: "Bearer " + apiKey },
|
|
24
27
|
});
|
|
25
28
|
}
|
|
26
29
|
else {
|
|
27
|
-
const host = config
|
|
30
|
+
const host = resolveBaseUrl("ollama", config) || DEFAULT_OLLAMA_HOST;
|
|
28
31
|
this.client = new Ollama({ host });
|
|
29
32
|
}
|
|
30
33
|
}
|
|
@@ -64,14 +67,14 @@ export class SmolOllama extends BaseClient {
|
|
|
64
67
|
throw error;
|
|
65
68
|
}
|
|
66
69
|
async _textSync(config) {
|
|
67
|
-
const messages = config.messages.map((msg) => msg.
|
|
70
|
+
const messages = config.messages.map((msg) => msg.toOllamaMessage());
|
|
68
71
|
const tools = (config.tools || []).map((tool) => {
|
|
69
72
|
return zodToGoogleTool(tool.name, tool.schema, {
|
|
70
73
|
description: tool.description,
|
|
71
74
|
});
|
|
72
75
|
});
|
|
73
76
|
const request = {
|
|
74
|
-
messages
|
|
77
|
+
messages,
|
|
75
78
|
model: this.getModel(),
|
|
76
79
|
};
|
|
77
80
|
if (tools.length > 0) {
|
|
@@ -81,7 +84,7 @@ export class SmolOllama extends BaseClient {
|
|
|
81
84
|
request.format = config.responseFormat.toJSONSchema();
|
|
82
85
|
}
|
|
83
86
|
Object.assign(request, sanitizeAttributes(config.rawAttributes));
|
|
84
|
-
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));
|
|
85
88
|
this.statelogClient?.promptRequest(request);
|
|
86
89
|
const signal = this.getAbortSignal(config);
|
|
87
90
|
const abortHandler = signal ? () => this.client.abort() : undefined;
|
|
@@ -117,14 +120,14 @@ export class SmolOllama extends BaseClient {
|
|
|
117
120
|
return success({ output, toolCalls, usage, cost, model: this.getModel() });
|
|
118
121
|
}
|
|
119
122
|
async *_textStream(config) {
|
|
120
|
-
const messages = config.messages.map((msg) => msg.
|
|
123
|
+
const messages = config.messages.map((msg) => msg.toOllamaMessage());
|
|
121
124
|
const tools = (config.tools || []).map((tool) => {
|
|
122
125
|
return zodToGoogleTool(tool.name, tool.schema, {
|
|
123
126
|
description: tool.description,
|
|
124
127
|
});
|
|
125
128
|
});
|
|
126
129
|
const request = {
|
|
127
|
-
messages
|
|
130
|
+
messages,
|
|
128
131
|
model: this.getModel(),
|
|
129
132
|
stream: true,
|
|
130
133
|
};
|
|
@@ -135,7 +138,7 @@ export class SmolOllama extends BaseClient {
|
|
|
135
138
|
request.format = config.responseFormat.toJSONSchema();
|
|
136
139
|
}
|
|
137
140
|
Object.assign(request, sanitizeAttributes(config.rawAttributes));
|
|
138
|
-
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));
|
|
139
142
|
this.statelogClient?.promptRequest(request);
|
|
140
143
|
const signal = this.getAbortSignal(config);
|
|
141
144
|
const abortHandler = signal ? () => this.client.abort() : undefined;
|
package/dist/clients/openai.d.ts
CHANGED
|
@@ -1,16 +1,52 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
|
-
import { PromptResult, Result, SmolClient, SmolConfig, StreamChunk } from "../types.js";
|
|
2
|
+
import { PromptResult, Result, SmolClient, SmolConfig, StreamChunk, HostedToolResult } from "../types.js";
|
|
3
|
+
import { EgonLog } from "../util/logger.js";
|
|
3
4
|
import { BaseClient } from "./baseClient.js";
|
|
4
5
|
import { ModelName } from "../models.js";
|
|
6
|
+
import { Model } from "../model.js";
|
|
7
|
+
import { CostEstimate, TokenUsage } from "../types.js";
|
|
5
8
|
export type SmolOpenAiConfig = SmolConfig;
|
|
6
9
|
export declare class SmolOpenAi extends BaseClient implements SmolClient {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
protected client: OpenAI;
|
|
11
|
+
protected logger: EgonLog;
|
|
12
|
+
protected model: Model;
|
|
10
13
|
constructor(config: SmolOpenAiConfig);
|
|
14
|
+
/**
|
|
15
|
+
* Build the `new OpenAI({...})` options. Subclasses override to inject a
|
|
16
|
+
* different baseURL or to pull the key from a different config field.
|
|
17
|
+
*
|
|
18
|
+
* Default behavior: read OPENAI_API_KEY (or config.apiKey.openAi). Throws
|
|
19
|
+
* if the key is missing — subclasses with required keys do their own check.
|
|
20
|
+
*/
|
|
21
|
+
protected resolveClientOptions(config: SmolConfig): {
|
|
22
|
+
apiKey?: string;
|
|
23
|
+
baseURL?: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Inspect the provider-returned usage (and optionally the raw HTTP response)
|
|
27
|
+
* for a USD cost figure. Returns `undefined` if the provider didn't surface
|
|
28
|
+
* one, in which case `calculateUsageAndCost` falls back to the smoltalk
|
|
29
|
+
* model registry. Subclasses (OpenRouter, DeepInfra, LiteLLM) override this.
|
|
30
|
+
*/
|
|
31
|
+
protected resolveCostUsd(_usage: any, _rawResponse?: Response): number | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Extra request body fields injected on every call. Subclasses override to
|
|
34
|
+
* add provider-specific request shapes (e.g. OpenRouter's `usage: { include: true }`
|
|
35
|
+
* or its `plugins: [{ id: "web" }]` for web search). Merged into the request
|
|
36
|
+
* after the standard params so it can override them.
|
|
37
|
+
*/
|
|
38
|
+
protected buildRequestExtras(_config: SmolConfig): Record<string, unknown>;
|
|
39
|
+
/**
|
|
40
|
+
* Extract provider-specific hosted-tool results (e.g. OpenRouter web_search
|
|
41
|
+
* annotations) from a completion. Subclasses override; default returns none.
|
|
42
|
+
*/
|
|
43
|
+
protected parseHostedToolResults(_completion: any, _config: SmolConfig): HostedToolResult[];
|
|
11
44
|
getClient(): OpenAI;
|
|
12
45
|
getModel(): ModelName;
|
|
13
|
-
|
|
46
|
+
protected calculateUsageAndCost(usageData: any, rawResponse?: Response): {
|
|
47
|
+
usage?: TokenUsage;
|
|
48
|
+
cost?: CostEstimate;
|
|
49
|
+
};
|
|
14
50
|
private buildRequest;
|
|
15
51
|
private rethrowAsSmolError;
|
|
16
52
|
_textSync(config: SmolConfig): Promise<Result<PromptResult>>;
|
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";
|
|
@@ -14,20 +15,57 @@ export class SmolOpenAi extends BaseClient {
|
|
|
14
15
|
model;
|
|
15
16
|
constructor(config) {
|
|
16
17
|
super(config);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
this.client = new OpenAI({ apiKey: config.openAiApiKey });
|
|
18
|
+
const options = this.resolveClientOptions(config);
|
|
19
|
+
this.client = new OpenAI(options);
|
|
21
20
|
this.logger = getLogger();
|
|
22
21
|
this.model = new Model(config.model, undefined, config.modelData);
|
|
23
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Build the `new OpenAI({...})` options. Subclasses override to inject a
|
|
25
|
+
* different baseURL or to pull the key from a different config field.
|
|
26
|
+
*
|
|
27
|
+
* Default behavior: read OPENAI_API_KEY (or config.apiKey.openAi). Throws
|
|
28
|
+
* if the key is missing — subclasses with required keys do their own check.
|
|
29
|
+
*/
|
|
30
|
+
resolveClientOptions(config) {
|
|
31
|
+
const apiKey = config.apiKey?.openAi || process.env.OPENAI_API_KEY;
|
|
32
|
+
if (!apiKey) {
|
|
33
|
+
throw new Error("OpenAI API key is required for SmolOpenAi client.");
|
|
34
|
+
}
|
|
35
|
+
return { apiKey };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Inspect the provider-returned usage (and optionally the raw HTTP response)
|
|
39
|
+
* for a USD cost figure. Returns `undefined` if the provider didn't surface
|
|
40
|
+
* one, in which case `calculateUsageAndCost` falls back to the smoltalk
|
|
41
|
+
* model registry. Subclasses (OpenRouter, DeepInfra, LiteLLM) override this.
|
|
42
|
+
*/
|
|
43
|
+
resolveCostUsd(_usage, _rawResponse) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Extra request body fields injected on every call. Subclasses override to
|
|
48
|
+
* add provider-specific request shapes (e.g. OpenRouter's `usage: { include: true }`
|
|
49
|
+
* or its `plugins: [{ id: "web" }]` for web search). Merged into the request
|
|
50
|
+
* after the standard params so it can override them.
|
|
51
|
+
*/
|
|
52
|
+
buildRequestExtras(_config) {
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Extract provider-specific hosted-tool results (e.g. OpenRouter web_search
|
|
57
|
+
* annotations) from a completion. Subclasses override; default returns none.
|
|
58
|
+
*/
|
|
59
|
+
parseHostedToolResults(_completion, _config) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
24
62
|
getClient() {
|
|
25
63
|
return this.client;
|
|
26
64
|
}
|
|
27
65
|
getModel() {
|
|
28
66
|
return this.model.getModel();
|
|
29
67
|
}
|
|
30
|
-
calculateUsageAndCost(usageData) {
|
|
68
|
+
calculateUsageAndCost(usageData, rawResponse) {
|
|
31
69
|
let usage;
|
|
32
70
|
let cost;
|
|
33
71
|
if (usageData) {
|
|
@@ -40,9 +78,23 @@ export class SmolOpenAi extends BaseClient {
|
|
|
40
78
|
if (cached > 0) {
|
|
41
79
|
usage.cachedInputTokens = cached;
|
|
42
80
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
81
|
+
// Prefer provider-supplied cost when available (e.g. OpenRouter
|
|
82
|
+
// usage.cost, DeepInfra usage.estimated_cost, LiteLLM header).
|
|
83
|
+
// Fall back to the smoltalk model-registry calculation.
|
|
84
|
+
const providerCost = this.resolveCostUsd(usageData, rawResponse);
|
|
85
|
+
if (typeof providerCost === "number") {
|
|
86
|
+
cost = {
|
|
87
|
+
inputCost: 0,
|
|
88
|
+
outputCost: 0,
|
|
89
|
+
totalCost: providerCost,
|
|
90
|
+
currency: "USD",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const calculatedCost = this.model.calculateCost(usage);
|
|
95
|
+
if (calculatedCost) {
|
|
96
|
+
cost = calculatedCost;
|
|
97
|
+
}
|
|
46
98
|
}
|
|
47
99
|
}
|
|
48
100
|
return { usage, cost };
|
|
@@ -61,6 +113,7 @@ export class SmolOpenAi extends BaseClient {
|
|
|
61
113
|
reasoning_effort: config.reasoningEffort,
|
|
62
114
|
}),
|
|
63
115
|
...sanitizeAttributes(config.rawAttributes),
|
|
116
|
+
...this.buildRequestExtras(config),
|
|
64
117
|
};
|
|
65
118
|
if (config.responseFormat) {
|
|
66
119
|
request.response_format = {
|
|
@@ -88,15 +141,20 @@ export class SmolOpenAi extends BaseClient {
|
|
|
88
141
|
}
|
|
89
142
|
async _textSync(config) {
|
|
90
143
|
const request = this.buildRequest(config);
|
|
91
|
-
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));
|
|
92
145
|
this.statelogClient?.promptRequest(request);
|
|
93
146
|
const signal = this.getAbortSignal(config);
|
|
94
147
|
let completion;
|
|
148
|
+
let rawResponse;
|
|
95
149
|
try {
|
|
96
|
-
|
|
150
|
+
const result = await this.client.chat.completions
|
|
151
|
+
.create({
|
|
97
152
|
...request,
|
|
98
153
|
stream: false,
|
|
99
|
-
}, { ...(signal && { signal }) })
|
|
154
|
+
}, { ...(signal && { signal }) })
|
|
155
|
+
.withResponse();
|
|
156
|
+
completion = result.data;
|
|
157
|
+
rawResponse = result.response;
|
|
100
158
|
}
|
|
101
159
|
catch (error) {
|
|
102
160
|
this.rethrowAsSmolError(error);
|
|
@@ -123,19 +181,22 @@ export class SmolOpenAi extends BaseClient {
|
|
|
123
181
|
}
|
|
124
182
|
}
|
|
125
183
|
}
|
|
126
|
-
// Extract usage and calculate cost
|
|
127
|
-
|
|
184
|
+
// Extract usage and calculate cost. rawResponse lets subclasses read
|
|
185
|
+
// response headers (e.g. LiteLLM's x-litellm-response-cost).
|
|
186
|
+
const { usage, cost } = this.calculateUsageAndCost(completion.usage, rawResponse);
|
|
187
|
+
const hostedToolResults = this.parseHostedToolResults(completion, config);
|
|
128
188
|
return success({
|
|
129
189
|
output,
|
|
130
190
|
toolCalls,
|
|
131
191
|
usage,
|
|
132
192
|
cost,
|
|
133
193
|
model: this.getModel(),
|
|
194
|
+
...(hostedToolResults.length > 0 ? { hostedToolResults } : {}),
|
|
134
195
|
});
|
|
135
196
|
}
|
|
136
197
|
async *_textStream(config) {
|
|
137
198
|
const request = this.buildRequest(config);
|
|
138
|
-
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));
|
|
139
200
|
this.statelogClient?.promptRequest(request);
|
|
140
201
|
const signal = this.getAbortSignal(config);
|
|
141
202
|
let completion;
|
|
@@ -156,6 +217,9 @@ export class SmolOpenAi extends BaseClient {
|
|
|
156
217
|
for await (const chunk of completion) {
|
|
157
218
|
// Extract usage from the final chunk
|
|
158
219
|
if (chunk.usage) {
|
|
220
|
+
// Header-based cost (LiteLLM) is unsupported while streaming.
|
|
221
|
+
// Body-based cost (OpenRouter/DeepInfra) still works because it
|
|
222
|
+
// comes through chunk.usage.
|
|
159
223
|
const usageAndCost = this.calculateUsageAndCost(chunk.usage);
|
|
160
224
|
usage = usageAndCost.usage;
|
|
161
225
|
cost = usageAndCost.cost;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { SmolOpenAi } from "./openai.js";
|
|
2
|
+
import type { SmolConfig } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Generic OpenAI-compatible client. Use when pointing smoltalk at any
|
|
5
|
+
* arbitrary OpenAI-shaped endpoint (vLLM, TGI, LM Studio, OpenLLM, etc.).
|
|
6
|
+
*
|
|
7
|
+
* Both `config.apiKey.openAiCompat` and `config.baseUrl.openAiCompat` are
|
|
8
|
+
* required (or their env-var fallbacks: `OPENAI_COMPAT_API_KEY`,
|
|
9
|
+
* `OPENAI_COMPAT_BASE_URL`).
|
|
10
|
+
*
|
|
11
|
+
* Cost: opportunistically reads `usage.cost`, `usage.estimated_cost`, or
|
|
12
|
+
* `usage.cost_usd` if the backend includes one of them. Otherwise falls back
|
|
13
|
+
* to the smoltalk model registry (which won't have these custom models, so
|
|
14
|
+
* cost stays undefined — that's expected for arbitrary backends).
|
|
15
|
+
*/
|
|
16
|
+
export declare class SmolOpenAiCompat extends SmolOpenAi {
|
|
17
|
+
protected resolveClientOptions(config: SmolConfig): {
|
|
18
|
+
apiKey: string;
|
|
19
|
+
baseURL: string;
|
|
20
|
+
};
|
|
21
|
+
protected resolveCostUsd(usage: any): number | undefined;
|
|
22
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { SmolOpenAi } from "./openai.js";
|
|
2
|
+
import { resolveApiKey, resolveBaseUrl } from "../util/provider.js";
|
|
3
|
+
/**
|
|
4
|
+
* Generic OpenAI-compatible client. Use when pointing smoltalk at any
|
|
5
|
+
* arbitrary OpenAI-shaped endpoint (vLLM, TGI, LM Studio, OpenLLM, etc.).
|
|
6
|
+
*
|
|
7
|
+
* Both `config.apiKey.openAiCompat` and `config.baseUrl.openAiCompat` are
|
|
8
|
+
* required (or their env-var fallbacks: `OPENAI_COMPAT_API_KEY`,
|
|
9
|
+
* `OPENAI_COMPAT_BASE_URL`).
|
|
10
|
+
*
|
|
11
|
+
* Cost: opportunistically reads `usage.cost`, `usage.estimated_cost`, or
|
|
12
|
+
* `usage.cost_usd` if the backend includes one of them. Otherwise falls back
|
|
13
|
+
* to the smoltalk model registry (which won't have these custom models, so
|
|
14
|
+
* cost stays undefined — that's expected for arbitrary backends).
|
|
15
|
+
*/
|
|
16
|
+
export class SmolOpenAiCompat extends SmolOpenAi {
|
|
17
|
+
resolveClientOptions(config) {
|
|
18
|
+
const apiKey = resolveApiKey("openai-compat", config);
|
|
19
|
+
const baseURL = resolveBaseUrl("openai-compat", config);
|
|
20
|
+
if (!apiKey) {
|
|
21
|
+
throw new Error("openai-compat: API key required (config.apiKey.openAiCompat or OPENAI_COMPAT_API_KEY).");
|
|
22
|
+
}
|
|
23
|
+
if (!baseURL) {
|
|
24
|
+
throw new Error("openai-compat: base URL required (config.baseUrl.openAiCompat or OPENAI_COMPAT_BASE_URL).");
|
|
25
|
+
}
|
|
26
|
+
return { apiKey, baseURL };
|
|
27
|
+
}
|
|
28
|
+
resolveCostUsd(usage) {
|
|
29
|
+
// Try the three common conventions across OpenAI-compatible providers.
|
|
30
|
+
const c = usage?.cost ?? usage?.estimated_cost ?? usage?.cost_usd;
|
|
31
|
+
return typeof c === "number" ? c : undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -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";
|
|
@@ -53,10 +54,11 @@ export class SmolOpenAiResponses extends BaseClient {
|
|
|
53
54
|
model;
|
|
54
55
|
constructor(config) {
|
|
55
56
|
super(config);
|
|
56
|
-
|
|
57
|
+
const apiKey = config.apiKey?.openAi || process.env.OPENAI_API_KEY;
|
|
58
|
+
if (!apiKey) {
|
|
57
59
|
throw new Error("OpenAI API key is required for SmolOpenAiResponses client.");
|
|
58
60
|
}
|
|
59
|
-
this.client = new OpenAI({ apiKey
|
|
61
|
+
this.client = new OpenAI({ apiKey });
|
|
60
62
|
this.logger = getLogger();
|
|
61
63
|
this.model = new Model(config.model, undefined, config.modelData);
|
|
62
64
|
}
|
|
@@ -165,7 +167,7 @@ export class SmolOpenAiResponses extends BaseClient {
|
|
|
165
167
|
}
|
|
166
168
|
async _textSync(config) {
|
|
167
169
|
const request = this.buildRequest(config);
|
|
168
|
-
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));
|
|
169
171
|
this.statelogClient?.promptRequest(request);
|
|
170
172
|
const signal = this.getAbortSignal(config);
|
|
171
173
|
let response;
|
|
@@ -204,7 +206,7 @@ export class SmolOpenAiResponses extends BaseClient {
|
|
|
204
206
|
}
|
|
205
207
|
async *_textStream(config) {
|
|
206
208
|
const request = this.buildRequest(config);
|
|
207
|
-
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));
|
|
208
210
|
this.statelogClient?.promptRequest(request);
|
|
209
211
|
const signal = this.getAbortSignal(config);
|
|
210
212
|
let stream;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { SmolOpenAiCompat } from "./openaiCompat.js";
|
|
2
|
+
import type { SmolConfig, HostedToolResult } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* OpenRouter client (https://openrouter.ai). OpenAI-compatible chat completions.
|
|
5
|
+
*
|
|
6
|
+
* - Baked base URL `https://openrouter.ai/api/v1` (override via config.baseUrl.openRouter).
|
|
7
|
+
* - Key: config.apiKey.openRouter or env OPENROUTER_API_KEY.
|
|
8
|
+
* - Cost: reads `usage.cost` (USD). OpenRouter only returns this when
|
|
9
|
+
* `usage: { include: true }` is set in the request body, which this client
|
|
10
|
+
* injects automatically via buildRequestExtras.
|
|
11
|
+
* - Hosted web_search: when config.hostedTools includes "web_search",
|
|
12
|
+
* injects `plugins: [{ id: "web", max_results: 5 }]` and parses
|
|
13
|
+
* message.annotations into a HostedToolResult.
|
|
14
|
+
*/
|
|
15
|
+
export declare class SmolOpenRouter extends SmolOpenAiCompat {
|
|
16
|
+
protected resolveClientOptions(config: SmolConfig): {
|
|
17
|
+
apiKey: string;
|
|
18
|
+
baseURL: string;
|
|
19
|
+
};
|
|
20
|
+
protected resolveCostUsd(usage: any): number | undefined;
|
|
21
|
+
protected buildRequestExtras(config: SmolConfig): Record<string, unknown>;
|
|
22
|
+
protected parseHostedToolResults(completion: any, config: SmolConfig): HostedToolResult[];
|
|
23
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { SmolOpenAiCompat } from "./openaiCompat.js";
|
|
2
|
+
import { webSearchResult } from "../util/hostedTools.js";
|
|
3
|
+
import { resolveApiKey, resolveBaseUrl } from "../util/provider.js";
|
|
4
|
+
/**
|
|
5
|
+
* OpenRouter client (https://openrouter.ai). OpenAI-compatible chat completions.
|
|
6
|
+
*
|
|
7
|
+
* - Baked base URL `https://openrouter.ai/api/v1` (override via config.baseUrl.openRouter).
|
|
8
|
+
* - Key: config.apiKey.openRouter or env OPENROUTER_API_KEY.
|
|
9
|
+
* - Cost: reads `usage.cost` (USD). OpenRouter only returns this when
|
|
10
|
+
* `usage: { include: true }` is set in the request body, which this client
|
|
11
|
+
* injects automatically via buildRequestExtras.
|
|
12
|
+
* - Hosted web_search: when config.hostedTools includes "web_search",
|
|
13
|
+
* injects `plugins: [{ id: "web", max_results: 5 }]` and parses
|
|
14
|
+
* message.annotations into a HostedToolResult.
|
|
15
|
+
*/
|
|
16
|
+
export class SmolOpenRouter extends SmolOpenAiCompat {
|
|
17
|
+
resolveClientOptions(config) {
|
|
18
|
+
const apiKey = resolveApiKey("openrouter", config);
|
|
19
|
+
// resolveBaseUrl bakes in the openrouter default, so this is always defined.
|
|
20
|
+
const baseURL = resolveBaseUrl("openrouter", config);
|
|
21
|
+
if (!apiKey) {
|
|
22
|
+
throw new Error("openrouter: API key required (config.apiKey.openRouter or OPENROUTER_API_KEY).");
|
|
23
|
+
}
|
|
24
|
+
return { apiKey, baseURL };
|
|
25
|
+
}
|
|
26
|
+
resolveCostUsd(usage) {
|
|
27
|
+
return typeof usage?.cost === "number" ? usage.cost : undefined;
|
|
28
|
+
}
|
|
29
|
+
buildRequestExtras(config) {
|
|
30
|
+
// OpenRouter only returns usage.cost when this is set.
|
|
31
|
+
const extras = { usage: { include: true } };
|
|
32
|
+
if (config.hostedTools?.includes("web_search")) {
|
|
33
|
+
extras.plugins = [{ id: "web", max_results: 5 }];
|
|
34
|
+
}
|
|
35
|
+
return extras;
|
|
36
|
+
}
|
|
37
|
+
parseHostedToolResults(completion, config) {
|
|
38
|
+
if (!config.hostedTools?.includes("web_search"))
|
|
39
|
+
return [];
|
|
40
|
+
const annotations = completion?.choices?.[0]?.message?.annotations;
|
|
41
|
+
if (!Array.isArray(annotations) || annotations.length === 0)
|
|
42
|
+
return [];
|
|
43
|
+
const sources = [];
|
|
44
|
+
const citations = [];
|
|
45
|
+
for (const a of annotations) {
|
|
46
|
+
if (a?.type !== "url_citation" || !a.url_citation?.url)
|
|
47
|
+
continue;
|
|
48
|
+
const uc = a.url_citation;
|
|
49
|
+
const source = {
|
|
50
|
+
url: uc.url,
|
|
51
|
+
};
|
|
52
|
+
if (uc.title)
|
|
53
|
+
source.title = uc.title;
|
|
54
|
+
if (uc.content)
|
|
55
|
+
source.snippet = uc.content;
|
|
56
|
+
sources.push(source);
|
|
57
|
+
const citation = { url: uc.url };
|
|
58
|
+
if (uc.title)
|
|
59
|
+
citation.title = uc.title;
|
|
60
|
+
if (typeof uc.start_index === "number")
|
|
61
|
+
citation.startIndex = uc.start_index;
|
|
62
|
+
if (typeof uc.end_index === "number")
|
|
63
|
+
citation.endIndex = uc.end_index;
|
|
64
|
+
citations.push(citation);
|
|
65
|
+
}
|
|
66
|
+
if (sources.length === 0)
|
|
67
|
+
return [];
|
|
68
|
+
return [
|
|
69
|
+
webSearchResult("openrouter", {
|
|
70
|
+
sources,
|
|
71
|
+
citations,
|
|
72
|
+
callCount: 1,
|
|
73
|
+
}),
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -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[]>>;
|