smoltalk 0.5.0 → 0.6.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/client.d.ts +4 -0
- package/dist/client.js +37 -16
- package/dist/clients/anthropic.d.ts +1 -3
- package/dist/clients/anthropic.js +5 -1
- package/dist/clients/baseClient.js +2 -2
- package/dist/clients/deepinfra.d.ts +17 -0
- package/dist/clients/deepinfra.js +27 -0
- package/dist/clients/google.js +3 -2
- package/dist/clients/litellm.d.ts +19 -0
- package/dist/clients/litellm.js +32 -0
- package/dist/clients/ollama.js +5 -3
- package/dist/clients/openai.d.ts +41 -5
- package/dist/clients/openai.js +75 -12
- package/dist/clients/openaiCompat.d.ts +22 -0
- package/dist/clients/openaiCompat.js +33 -0
- package/dist/clients/openaiResponses.js +3 -2
- package/dist/clients/openrouter.d.ts +23 -0
- package/dist/clients/openrouter.js +76 -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/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/models.d.ts +5 -1
- package/dist/models.js +25 -2
- package/dist/types.d.ts +22 -10
- package/dist/util/hostedTools.d.ts +1 -1
- package/dist/util/hostedTools.js +19 -5
- package/dist/util/provider.d.ts +32 -6
- package/dist/util/provider.js +38 -4
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -53,10 +53,11 @@ export class SmolOpenAiResponses extends BaseClient {
|
|
|
53
53
|
model;
|
|
54
54
|
constructor(config) {
|
|
55
55
|
super(config);
|
|
56
|
-
|
|
56
|
+
const apiKey = config.apiKey?.openAi || process.env.OPENAI_API_KEY;
|
|
57
|
+
if (!apiKey) {
|
|
57
58
|
throw new Error("OpenAI API key is required for SmolOpenAiResponses client.");
|
|
58
59
|
}
|
|
59
|
-
this.client = new OpenAI({ apiKey
|
|
60
|
+
this.client = new OpenAI({ apiKey });
|
|
60
61
|
this.logger = getLogger();
|
|
61
62
|
this.model = new Model(config.model, undefined, config.modelData);
|
|
62
63
|
}
|
|
@@ -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
|
+
}
|
package/dist/embed/openai.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
import { EmbedConfig, EmbedResult } from "../embed.js";
|
|
2
2
|
import { Result } from "../types/result.js";
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* OpenAI-compatible embedding call. Used by openai directly and by other
|
|
5
|
+
* OpenAI-shape backends (deepinfra, litellm, openai-compat) which pass a
|
|
6
|
+
* custom `baseURL`. Cost comes from the smoltalk model registry; provider-
|
|
7
|
+
* returned cost fields aren't standardized on this endpoint.
|
|
8
|
+
*/
|
|
9
|
+
export declare function openaiEmbed(inputs: string[], config: EmbedConfig, apiKey: string, baseURL?: string): Promise<Result<EmbedResult>>;
|
package/dist/embed/openai.js
CHANGED
|
@@ -2,9 +2,15 @@ import OpenAI from "openai";
|
|
|
2
2
|
import { success, failure } from "../types/result.js";
|
|
3
3
|
import { getModel, isEmbeddingsModel } from "../models.js";
|
|
4
4
|
import { round } from "../util/util.js";
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* OpenAI-compatible embedding call. Used by openai directly and by other
|
|
7
|
+
* OpenAI-shape backends (deepinfra, litellm, openai-compat) which pass a
|
|
8
|
+
* custom `baseURL`. Cost comes from the smoltalk model registry; provider-
|
|
9
|
+
* returned cost fields aren't standardized on this endpoint.
|
|
10
|
+
*/
|
|
11
|
+
export async function openaiEmbed(inputs, config, apiKey, baseURL) {
|
|
6
12
|
try {
|
|
7
|
-
const client = new OpenAI({ apiKey });
|
|
13
|
+
const client = new OpenAI({ apiKey, ...(baseURL ? { baseURL } : {}) });
|
|
8
14
|
const response = await client.embeddings.create({
|
|
9
15
|
model: config.model,
|
|
10
16
|
input: inputs,
|
package/dist/embed.d.ts
CHANGED
|
@@ -6,10 +6,24 @@ export type EmbedConfig = {
|
|
|
6
6
|
model: string;
|
|
7
7
|
provider?: string;
|
|
8
8
|
dimensions?: number;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
/** API keys, nested by provider. Falls back to env vars
|
|
10
|
+
* (OPENAI_API_KEY / GEMINI_API_KEY / DEEPINFRA_API_KEY /
|
|
11
|
+
* LITELLM_API_KEY / OPENAI_COMPAT_API_KEY). */
|
|
12
|
+
apiKey?: {
|
|
13
|
+
openAi?: string;
|
|
14
|
+
google?: string;
|
|
15
|
+
ollama?: string;
|
|
16
|
+
deepInfra?: string;
|
|
17
|
+
liteLlm?: string;
|
|
18
|
+
openAiCompat?: string;
|
|
19
|
+
};
|
|
20
|
+
/** Custom base URLs, nested by provider. */
|
|
21
|
+
baseUrl?: {
|
|
22
|
+
ollama?: string;
|
|
23
|
+
deepInfra?: string;
|
|
24
|
+
liteLlm?: string;
|
|
25
|
+
openAiCompat?: string;
|
|
26
|
+
};
|
|
13
27
|
metadata?: Record<string, unknown>;
|
|
14
28
|
modelData?: ModelDataBlob;
|
|
15
29
|
};
|
package/dist/embed.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { failure } from "./types/result.js";
|
|
2
|
-
import { resolveProvider, resolveApiKey } from "./util/provider.js";
|
|
2
|
+
import { resolveProvider, resolveApiKey, resolveBaseUrl } from "./util/provider.js";
|
|
3
3
|
import { openaiEmbed } from "./embed/openai.js";
|
|
4
4
|
import { googleEmbed } from "./embed/google.js";
|
|
5
5
|
import { ollamaEmbed } from "./embed/ollama.js";
|
|
@@ -23,18 +23,46 @@ export async function embed(input, config) {
|
|
|
23
23
|
case "openai":
|
|
24
24
|
case "openai-responses": {
|
|
25
25
|
if (!apiKey) {
|
|
26
|
-
return failure("No OpenAI API key provided. Set
|
|
26
|
+
return failure("No OpenAI API key provided. Set config.apiKey.openAi or the OPENAI_API_KEY environment variable.");
|
|
27
27
|
}
|
|
28
28
|
return openaiEmbed(inputs, config, apiKey);
|
|
29
29
|
}
|
|
30
30
|
case "google": {
|
|
31
31
|
if (!apiKey) {
|
|
32
|
-
return failure("No Google API key provided. Set
|
|
32
|
+
return failure("No Google API key provided. Set config.apiKey.google or the GEMINI_API_KEY environment variable.");
|
|
33
33
|
}
|
|
34
34
|
return googleEmbed(inputs, config, apiKey);
|
|
35
35
|
}
|
|
36
36
|
case "ollama":
|
|
37
|
-
return ollamaEmbed(inputs, config, apiKey, config
|
|
37
|
+
return ollamaEmbed(inputs, config, apiKey, resolveBaseUrl("ollama", config));
|
|
38
|
+
case "openrouter":
|
|
39
|
+
return failure("openrouter does not expose an embeddings endpoint; use deepinfra, openai-compat, or litellm instead.");
|
|
40
|
+
case "deepinfra": {
|
|
41
|
+
if (!apiKey) {
|
|
42
|
+
return failure("No DeepInfra API key provided. Set config.apiKey.deepInfra or the DEEPINFRA_API_KEY environment variable.");
|
|
43
|
+
}
|
|
44
|
+
return openaiEmbed(inputs, config, apiKey, resolveBaseUrl("deepinfra", config));
|
|
45
|
+
}
|
|
46
|
+
case "litellm": {
|
|
47
|
+
if (!apiKey) {
|
|
48
|
+
return failure("No LiteLLM API key provided. Set config.apiKey.liteLlm or the LITELLM_API_KEY environment variable.");
|
|
49
|
+
}
|
|
50
|
+
const baseURL = resolveBaseUrl("litellm", config);
|
|
51
|
+
if (!baseURL) {
|
|
52
|
+
return failure("No LiteLLM base URL provided. Set config.baseUrl.liteLlm or the LITELLM_BASE_URL environment variable.");
|
|
53
|
+
}
|
|
54
|
+
return openaiEmbed(inputs, config, apiKey, baseURL);
|
|
55
|
+
}
|
|
56
|
+
case "openai-compat": {
|
|
57
|
+
if (!apiKey) {
|
|
58
|
+
return failure("No openai-compat API key provided. Set config.apiKey.openAiCompat or the OPENAI_COMPAT_API_KEY environment variable.");
|
|
59
|
+
}
|
|
60
|
+
const baseURL = resolveBaseUrl("openai-compat", config);
|
|
61
|
+
if (!baseURL) {
|
|
62
|
+
return failure("No openai-compat base URL provided. Set config.baseUrl.openAiCompat or the OPENAI_COMPAT_BASE_URL environment variable.");
|
|
63
|
+
}
|
|
64
|
+
return openaiEmbed(inputs, config, apiKey, baseURL);
|
|
65
|
+
}
|
|
38
66
|
default: {
|
|
39
67
|
const custom = registeredEmbedProviders[provider];
|
|
40
68
|
if (custom) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ImageInput, ImageConfig, ImageGenResult } from "../image.js";
|
|
2
|
+
import { Result } from "../types/result.js";
|
|
3
|
+
/**
|
|
4
|
+
* Generic OpenAI-compatible image generation. Used by litellm/openai-compat —
|
|
5
|
+
* any backend that speaks the OpenAI `/images/generations` shape. Image edits
|
|
6
|
+
* are not supported here (the multipart upload shape varies between backends);
|
|
7
|
+
* pass a plain prompt only.
|
|
8
|
+
*/
|
|
9
|
+
export declare function openaiCompatImage(input: ImageInput, config: ImageConfig, apiKey: string, baseURL: string): Promise<Result<ImageGenResult>>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
import { success, failure } from "../types/result.js";
|
|
3
|
+
import { omitUndefined } from "../util/util.js";
|
|
4
|
+
/**
|
|
5
|
+
* Generic OpenAI-compatible image generation. Used by litellm/openai-compat —
|
|
6
|
+
* any backend that speaks the OpenAI `/images/generations` shape. Image edits
|
|
7
|
+
* are not supported here (the multipart upload shape varies between backends);
|
|
8
|
+
* pass a plain prompt only.
|
|
9
|
+
*/
|
|
10
|
+
export async function openaiCompatImage(input, config, apiKey, baseURL) {
|
|
11
|
+
try {
|
|
12
|
+
const normalized = typeof input === "string" ? { prompt: input } : input;
|
|
13
|
+
const hasImages = !!(normalized.images && normalized.images.length > 0);
|
|
14
|
+
if (hasImages) {
|
|
15
|
+
return failure("openai-compat: image edits (passing `images`) are not supported on this generic adapter. Use the `openai` provider for OpenAI image edits.");
|
|
16
|
+
}
|
|
17
|
+
if (normalized.mask) {
|
|
18
|
+
return failure("openai-compat: image masks are not supported on this generic adapter.");
|
|
19
|
+
}
|
|
20
|
+
const client = new OpenAI({ apiKey, baseURL });
|
|
21
|
+
const params = omitUndefined({
|
|
22
|
+
model: config.model,
|
|
23
|
+
prompt: normalized.prompt,
|
|
24
|
+
n: config.n,
|
|
25
|
+
size: config.size,
|
|
26
|
+
quality: config.quality,
|
|
27
|
+
output_format: config.outputFormat,
|
|
28
|
+
background: config.background,
|
|
29
|
+
...(config.metadata ?? {}),
|
|
30
|
+
});
|
|
31
|
+
const response = await client.images.generate(params);
|
|
32
|
+
const mimeType = mimeFromFormat(config.outputFormat) ?? "image/png";
|
|
33
|
+
const images = (response.data ?? []).map((d) => ({
|
|
34
|
+
data: new Uint8Array(Buffer.from(d.b64_json, "base64")),
|
|
35
|
+
mimeType,
|
|
36
|
+
revisedPrompt: d.revised_prompt,
|
|
37
|
+
}));
|
|
38
|
+
return success({
|
|
39
|
+
images,
|
|
40
|
+
model: config.model,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
return failure(err instanceof Error
|
|
45
|
+
? err.message
|
|
46
|
+
: "OpenAI-compatible image request failed");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function mimeFromFormat(format) {
|
|
50
|
+
switch (format) {
|
|
51
|
+
case "png":
|
|
52
|
+
return "image/png";
|
|
53
|
+
case "jpeg":
|
|
54
|
+
return "image/jpeg";
|
|
55
|
+
case "webp":
|
|
56
|
+
return "image/webp";
|
|
57
|
+
default:
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
package/dist/image.d.ts
CHANGED
|
@@ -17,8 +17,19 @@ export type ImageConfig = {
|
|
|
17
17
|
quality?: "low" | "medium" | "high" | "auto";
|
|
18
18
|
outputFormat?: "png" | "jpeg" | "webp";
|
|
19
19
|
background?: "transparent" | "opaque" | "auto";
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
/** API keys, nested by provider. Falls back to env vars
|
|
21
|
+
* (OPENAI_API_KEY / GEMINI_API_KEY / LITELLM_API_KEY / OPENAI_COMPAT_API_KEY). */
|
|
22
|
+
apiKey?: {
|
|
23
|
+
openAi?: string;
|
|
24
|
+
google?: string;
|
|
25
|
+
liteLlm?: string;
|
|
26
|
+
openAiCompat?: string;
|
|
27
|
+
};
|
|
28
|
+
/** Custom base URLs, nested by provider. */
|
|
29
|
+
baseUrl?: {
|
|
30
|
+
liteLlm?: string;
|
|
31
|
+
openAiCompat?: string;
|
|
32
|
+
};
|
|
22
33
|
metadata?: Record<string, unknown>;
|
|
23
34
|
modelData?: ModelDataBlob;
|
|
24
35
|
};
|
package/dist/image.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { failure } from "./types/result.js";
|
|
2
|
-
import { resolveProvider, resolveApiKey } from "./util/provider.js";
|
|
2
|
+
import { resolveProvider, resolveApiKey, resolveBaseUrl } from "./util/provider.js";
|
|
3
3
|
import { openaiImage } from "./image/openai.js";
|
|
4
4
|
import { googleImage } from "./image/google.js";
|
|
5
|
+
import { openaiCompatImage } from "./image/openaiCompat.js";
|
|
5
6
|
// Null-prototype so provider names like "toString"/"__proto__" can't collide
|
|
6
7
|
// with Object.prototype or pollute the registry.
|
|
7
8
|
const registeredImageProviders = Object.create(null);
|
|
@@ -27,16 +28,40 @@ export async function image(input, config) {
|
|
|
27
28
|
case "openai":
|
|
28
29
|
case "openai-responses": {
|
|
29
30
|
if (!apiKey) {
|
|
30
|
-
return failure("No OpenAI API key provided. Set
|
|
31
|
+
return failure("No OpenAI API key provided. Set config.apiKey.openAi or the OPENAI_API_KEY environment variable.");
|
|
31
32
|
}
|
|
32
33
|
return openaiImage(input, config, apiKey);
|
|
33
34
|
}
|
|
34
35
|
case "google": {
|
|
35
36
|
if (!apiKey) {
|
|
36
|
-
return failure("No Google API key provided. Set
|
|
37
|
+
return failure("No Google API key provided. Set config.apiKey.google or the GEMINI_API_KEY environment variable.");
|
|
37
38
|
}
|
|
38
39
|
return googleImage(input, config, apiKey);
|
|
39
40
|
}
|
|
41
|
+
case "openrouter":
|
|
42
|
+
return failure("openrouter does not expose an OpenAI-compatible images endpoint; use openai-compat or litellm instead.");
|
|
43
|
+
case "deepinfra":
|
|
44
|
+
return failure("deepinfra exposes per-model image endpoints rather than the OpenAI `/images/generations` shape; use openai-compat with a specific backend or litellm instead.");
|
|
45
|
+
case "litellm": {
|
|
46
|
+
if (!apiKey) {
|
|
47
|
+
return failure("No LiteLLM API key provided. Set config.apiKey.liteLlm or the LITELLM_API_KEY environment variable.");
|
|
48
|
+
}
|
|
49
|
+
const baseURL = resolveBaseUrl("litellm", config);
|
|
50
|
+
if (!baseURL) {
|
|
51
|
+
return failure("No LiteLLM base URL provided. Set config.baseUrl.liteLlm or the LITELLM_BASE_URL environment variable.");
|
|
52
|
+
}
|
|
53
|
+
return openaiCompatImage(input, config, apiKey, baseURL);
|
|
54
|
+
}
|
|
55
|
+
case "openai-compat": {
|
|
56
|
+
if (!apiKey) {
|
|
57
|
+
return failure("No openai-compat API key provided. Set config.apiKey.openAiCompat or the OPENAI_COMPAT_API_KEY environment variable.");
|
|
58
|
+
}
|
|
59
|
+
const baseURL = resolveBaseUrl("openai-compat", config);
|
|
60
|
+
if (!baseURL) {
|
|
61
|
+
return failure("No openai-compat base URL provided. Set config.baseUrl.openAiCompat or the OPENAI_COMPAT_BASE_URL environment variable.");
|
|
62
|
+
}
|
|
63
|
+
return openaiCompatImage(input, config, apiKey, baseURL);
|
|
64
|
+
}
|
|
40
65
|
default: {
|
|
41
66
|
const custom = registeredImageProviders[provider];
|
|
42
67
|
if (custom) {
|
package/dist/models.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { type ModelDataBlob, type HostedTool, type HostedToolPrice } from "./modelData.js";
|
|
3
|
-
export declare const providers: readonly ["ollama", "openai", "openai-responses", "anthropic", "google", "replicate", "modal"];
|
|
3
|
+
export declare const providers: readonly ["ollama", "openai", "openai-responses", "anthropic", "google", "replicate", "modal", "openrouter", "deepinfra", "litellm", "openai-compat"];
|
|
4
4
|
export declare const ProviderSchema: z.ZodEnum<{
|
|
5
5
|
ollama: "ollama";
|
|
6
6
|
openai: "openai";
|
|
@@ -9,6 +9,10 @@ export declare const ProviderSchema: z.ZodEnum<{
|
|
|
9
9
|
google: "google";
|
|
10
10
|
replicate: "replicate";
|
|
11
11
|
modal: "modal";
|
|
12
|
+
openrouter: "openrouter";
|
|
13
|
+
deepinfra: "deepinfra";
|
|
14
|
+
litellm: "litellm";
|
|
15
|
+
"openai-compat": "openai-compat";
|
|
12
16
|
}>;
|
|
13
17
|
export type Provider = z.infer<typeof ProviderSchema>;
|
|
14
18
|
export type BaseModel = {
|
package/dist/models.js
CHANGED
|
@@ -8,6 +8,10 @@ export const providers = [
|
|
|
8
8
|
"google",
|
|
9
9
|
"replicate",
|
|
10
10
|
"modal",
|
|
11
|
+
"openrouter",
|
|
12
|
+
"deepinfra",
|
|
13
|
+
"litellm",
|
|
14
|
+
"openai-compat",
|
|
11
15
|
];
|
|
12
16
|
export const ProviderSchema = z.enum(providers);
|
|
13
17
|
export const speechToTextModels = [
|
|
@@ -1557,6 +1561,18 @@ export const hostedTools = [
|
|
|
1557
1561
|
models: ["gemini-3-pro-preview", "gemini-3.1-pro-preview", "gemini-3-flash-preview", "gemini-3.5-flash", "gemini-3.1-flash-lite"],
|
|
1558
1562
|
pricing: { unit: "per_call", note: "Gemini 3 family only; see Google pricing." },
|
|
1559
1563
|
},
|
|
1564
|
+
{
|
|
1565
|
+
name: "web_search",
|
|
1566
|
+
provider: "openrouter",
|
|
1567
|
+
category: "web_search",
|
|
1568
|
+
description: "Web search via OpenRouter's web plugin (Exa-backed). Adds citations as annotations.",
|
|
1569
|
+
providerToolId: "web",
|
|
1570
|
+
pricing: {
|
|
1571
|
+
unit: "per_call",
|
|
1572
|
+
amount: 0.02,
|
|
1573
|
+
note: "$4 per 1,000 results returned by the web plugin × default 5 results/call = $0.02/call. Plus token cost for inserted context. If you override max_results, scale this accordingly.",
|
|
1574
|
+
},
|
|
1575
|
+
},
|
|
1560
1576
|
];
|
|
1561
1577
|
export const registeredTextModels = [];
|
|
1562
1578
|
export function registerTextModel(model) {
|
|
@@ -1600,9 +1616,16 @@ export function getHostedTools(opts = {}) {
|
|
|
1600
1616
|
if (opts.modelData) {
|
|
1601
1617
|
tools = mergeHostedTools(tools, opts.modelData.hostedTools);
|
|
1602
1618
|
}
|
|
1619
|
+
// A provider override is authoritative: it decides which API the model routes
|
|
1620
|
+
// through, and therefore which hosted tools are available for it.
|
|
1603
1621
|
let modelProvider;
|
|
1604
1622
|
if (opts.model) {
|
|
1605
|
-
|
|
1623
|
+
if (opts.provider) {
|
|
1624
|
+
modelProvider = opts.provider;
|
|
1625
|
+
}
|
|
1626
|
+
else {
|
|
1627
|
+
modelProvider = getModel(opts.model, opts.modelData)?.provider;
|
|
1628
|
+
}
|
|
1606
1629
|
}
|
|
1607
1630
|
return tools.filter((tool) => {
|
|
1608
1631
|
if (tool.disabled && !opts.includeDisabled) {
|
|
@@ -1651,4 +1674,4 @@ export function isEmbeddingsModel(model) {
|
|
|
1651
1674
|
}
|
|
1652
1675
|
export const ModelNameSchema = z
|
|
1653
1676
|
.string()
|
|
1654
|
-
.regex(/^[a-zA-Z0-9._
|
|
1677
|
+
.regex(/^[a-zA-Z0-9._:@/-]+$/, "Model name must only contain letters, numbers, dots, underscores, hyphens, colons, slashes, and @");
|
package/dist/types.d.ts
CHANGED
|
@@ -16,16 +16,28 @@ export type SmolConfig = {
|
|
|
16
16
|
model: ModelName;
|
|
17
17
|
/** Override the provider for the given model (e.g., use a custom endpoint for an OpenAI-compatible model). */
|
|
18
18
|
provider?: string;
|
|
19
|
-
/** API
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
/** API keys, nested by provider. Each key falls back to its conventional env var
|
|
20
|
+
* if unset (e.g. apiKey.openAi → OPENAI_API_KEY). */
|
|
21
|
+
apiKey?: {
|
|
22
|
+
openAi?: string;
|
|
23
|
+
google?: string;
|
|
24
|
+
anthropic?: string;
|
|
25
|
+
ollama?: string;
|
|
26
|
+
openRouter?: string;
|
|
27
|
+
deepInfra?: string;
|
|
28
|
+
liteLlm?: string;
|
|
29
|
+
openAiCompat?: string;
|
|
30
|
+
};
|
|
31
|
+
/** Custom base URLs, nested by provider. Defaults are baked in where applicable
|
|
32
|
+
* (e.g. openrouter, deepinfra); litellm and openai-compat require an explicit URL. */
|
|
33
|
+
baseUrl?: {
|
|
34
|
+
/** Host URL for the Ollama server. Defaults to http://localhost:11434 (or $OLLAMA_HOST). */
|
|
35
|
+
ollama?: string;
|
|
36
|
+
openRouter?: string;
|
|
37
|
+
deepInfra?: string;
|
|
38
|
+
liteLlm?: string;
|
|
39
|
+
openAiCompat?: string;
|
|
40
|
+
};
|
|
29
41
|
/** Log level for internal debug logging. */
|
|
30
42
|
logLevel?: LogLevel;
|
|
31
43
|
/** Configuration for Statelog observability/tracing integration. */
|
|
@@ -3,7 +3,7 @@ import type { CostEstimate } from "../types/costEstimate.js";
|
|
|
3
3
|
import type { HostedToolResult, WebSearchSource, WebSearchCitation } from "../types.js";
|
|
4
4
|
export declare const WEB_SEARCH = "web_search";
|
|
5
5
|
export declare const IMPLEMENTED_HOSTED_TOOLS: Set<string>;
|
|
6
|
-
export declare function validateHostedTools(requested: string[] | undefined, model: string, modelData?: ModelDataBlob): string | null;
|
|
6
|
+
export declare function validateHostedTools(requested: string[] | undefined, model: string, provider?: string, modelData?: ModelDataBlob): string | null;
|
|
7
7
|
export declare function estimateHostedToolCost(result: HostedToolResult, model: string, modelData?: ModelDataBlob): number | undefined;
|
|
8
8
|
export declare function foldHostedToolCost(cost: CostEstimate | undefined, results: HostedToolResult[]): CostEstimate | undefined;
|
|
9
9
|
export declare function webSearchResult(provider: string, parts: {
|
package/dist/util/hostedTools.js
CHANGED
|
@@ -6,12 +6,15 @@ export const WEB_SEARCH = "web_search";
|
|
|
6
6
|
export const IMPLEMENTED_HOSTED_TOOLS = new Set([WEB_SEARCH]);
|
|
7
7
|
// Returns an error message if any requested capability is unusable for this
|
|
8
8
|
// model, else null. Capabilities are matched against the catalog `category`.
|
|
9
|
-
|
|
9
|
+
// A `provider` override is authoritative: the same model can route through a
|
|
10
|
+
// different API (e.g. gpt-4o-mini via "openai-responses"), which changes which
|
|
11
|
+
// hosted tools are available.
|
|
12
|
+
export function validateHostedTools(requested, model, provider, modelData) {
|
|
10
13
|
if (!requested || requested.length === 0) {
|
|
11
14
|
return null;
|
|
12
15
|
}
|
|
13
16
|
const allCategories = new Set(getHostedTools({ includeDisabled: true, modelData }).map((t) => t.category));
|
|
14
|
-
const availableCategories = new Set(getHostedTools({ model, modelData }).map((t) => t.category));
|
|
17
|
+
const availableCategories = new Set(getHostedTools({ model, provider, modelData }).map((t) => t.category));
|
|
15
18
|
for (const name of requested) {
|
|
16
19
|
if (!IMPLEMENTED_HOSTED_TOOLS.has(name)) {
|
|
17
20
|
if (allCategories.has(name)) {
|
|
@@ -20,8 +23,11 @@ export function validateHostedTools(requested, model, modelData) {
|
|
|
20
23
|
return `Unknown hosted tool "${name}".`;
|
|
21
24
|
}
|
|
22
25
|
if (!availableCategories.has(name)) {
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
let effectiveProvider = provider;
|
|
27
|
+
if (!effectiveProvider) {
|
|
28
|
+
effectiveProvider = getModel(model, modelData)?.provider ?? "unknown";
|
|
29
|
+
}
|
|
30
|
+
return `${name} is a hosted capability; ${model} (${effectiveProvider}) doesn't offer it — pass a search function as a tool instead.`;
|
|
25
31
|
}
|
|
26
32
|
}
|
|
27
33
|
return null;
|
|
@@ -32,7 +38,15 @@ export function estimateHostedToolCost(result, model, modelData) {
|
|
|
32
38
|
if (!result.callCount) {
|
|
33
39
|
return undefined;
|
|
34
40
|
}
|
|
35
|
-
|
|
41
|
+
// The result names the provider that produced it, which may differ from the
|
|
42
|
+
// model's registered provider (e.g. a base model routed through Responses).
|
|
43
|
+
// Use it so the catalog lookup isn't filtered out by the wrong provider.
|
|
44
|
+
const tools = getHostedTools({
|
|
45
|
+
model,
|
|
46
|
+
provider: result.provider,
|
|
47
|
+
includeDisabled: true,
|
|
48
|
+
modelData,
|
|
49
|
+
});
|
|
36
50
|
const tool = tools.find((t) => t.category === result.tool && t.provider === result.provider);
|
|
37
51
|
if (!tool) {
|
|
38
52
|
return undefined;
|
package/dist/util/provider.d.ts
CHANGED
|
@@ -5,14 +5,40 @@ import type { ModelDataBlob } from "../modelData.js";
|
|
|
5
5
|
* Otherwise looks up the model in the registry.
|
|
6
6
|
*/
|
|
7
7
|
export declare function resolveProvider(modelName: string, explicitProvider?: string, modelData?: ModelDataBlob): string;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Minimal shape needed to read API keys / base URLs. Kept local to avoid
|
|
10
|
+
* importing SmolConfig (which would create a circular import).
|
|
11
|
+
*/
|
|
12
|
+
type NestedKeyConfig = {
|
|
13
|
+
apiKey?: {
|
|
14
|
+
openAi?: string;
|
|
15
|
+
google?: string;
|
|
16
|
+
anthropic?: string;
|
|
17
|
+
ollama?: string;
|
|
18
|
+
openRouter?: string;
|
|
19
|
+
deepInfra?: string;
|
|
20
|
+
liteLlm?: string;
|
|
21
|
+
openAiCompat?: string;
|
|
22
|
+
};
|
|
23
|
+
baseUrl?: {
|
|
24
|
+
ollama?: string;
|
|
25
|
+
openRouter?: string;
|
|
26
|
+
deepInfra?: string;
|
|
27
|
+
liteLlm?: string;
|
|
28
|
+
openAiCompat?: string;
|
|
29
|
+
};
|
|
13
30
|
};
|
|
14
31
|
/**
|
|
15
32
|
* Resolve the API key for a provider, checking config then env vars.
|
|
16
33
|
*/
|
|
17
|
-
export declare function resolveApiKey(provider: string, config:
|
|
34
|
+
export declare function resolveApiKey(provider: string, config: NestedKeyConfig): string | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the base URL for a provider, checking config then env vars then a
|
|
37
|
+
* baked-in default. Returns `undefined` when a provider requires the caller to
|
|
38
|
+
* supply one (litellm, openai-compat) and nothing was given.
|
|
39
|
+
*
|
|
40
|
+
* Centralized here so we don't duplicate the env-fallback table across
|
|
41
|
+
* embed.ts, image.ts, and each client constructor.
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveBaseUrl(provider: string, config: NestedKeyConfig): string | undefined;
|
|
18
44
|
export {};
|