smoltalk 0.4.2 → 0.5.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 +139 -0
- package/dist/classes/message/AssistantMessage.d.ts +1 -0
- package/dist/client.js +5 -3
- package/dist/clients/anthropic.d.ts +3 -1
- package/dist/clients/anthropic.js +65 -10
- package/dist/clients/baseClient.js +11 -1
- package/dist/clients/google.d.ts +3 -0
- package/dist/clients/google.js +76 -7
- package/dist/clients/ollama.js +1 -1
- package/dist/clients/openai.js +1 -1
- package/dist/clients/openaiResponses.d.ts +3 -0
- package/dist/clients/openaiResponses.js +54 -4
- package/dist/embed/openai.js +3 -3
- package/dist/embed.d.ts +5 -2
- package/dist/embed.js +14 -3
- package/dist/image/google.js +3 -3
- package/dist/image/openai.js +6 -6
- package/dist/image.d.ts +5 -2
- package/dist/image.js +14 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/model.d.ts +4 -2
- package/dist/model.js +7 -5
- package/dist/modelData.d.ts +40 -0
- package/dist/modelData.js +222 -0
- package/dist/models.d.ts +658 -760
- package/dist/models.js +836 -60
- package/dist/types/costEstimate.d.ts +2 -0
- package/dist/types/costEstimate.js +1 -0
- package/dist/types.d.ts +30 -1
- package/dist/types.js +2 -1
- package/dist/util/hostedTools.d.ts +19 -0
- package/dist/util/hostedTools.js +104 -0
- package/dist/util/provider.d.ts +2 -1
- package/dist/util/provider.js +2 -2
- package/package.json +7 -2
package/dist/embed.js
CHANGED
|
@@ -3,11 +3,17 @@ import { resolveProvider, resolveApiKey } 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";
|
|
6
|
+
// Null-prototype so provider names like "toString"/"__proto__" can't collide
|
|
7
|
+
// with Object.prototype or pollute the registry.
|
|
8
|
+
const registeredEmbedProviders = Object.create(null);
|
|
9
|
+
export function registerEmbeddingProvider(name, fn) {
|
|
10
|
+
registeredEmbedProviders[name] = fn;
|
|
11
|
+
}
|
|
6
12
|
export async function embed(input, config) {
|
|
7
13
|
const inputs = Array.isArray(input) ? input : [input];
|
|
8
14
|
let provider;
|
|
9
15
|
try {
|
|
10
|
-
provider = resolveProvider(config.model, config.provider);
|
|
16
|
+
provider = resolveProvider(config.model, config.provider, config.modelData);
|
|
11
17
|
}
|
|
12
18
|
catch (err) {
|
|
13
19
|
return failure(err instanceof Error ? err.message : "Failed to resolve provider");
|
|
@@ -29,7 +35,12 @@ export async function embed(input, config) {
|
|
|
29
35
|
}
|
|
30
36
|
case "ollama":
|
|
31
37
|
return ollamaEmbed(inputs, config, apiKey, config.ollamaHost);
|
|
32
|
-
default:
|
|
33
|
-
|
|
38
|
+
default: {
|
|
39
|
+
const custom = registeredEmbedProviders[provider];
|
|
40
|
+
if (custom) {
|
|
41
|
+
return custom(inputs, config);
|
|
42
|
+
}
|
|
43
|
+
return failure(`Provider "${provider}" does not support embeddings. Register one with registerEmbeddingProvider(name, fn).`);
|
|
44
|
+
}
|
|
34
45
|
}
|
|
35
46
|
}
|
package/dist/image/google.js
CHANGED
|
@@ -41,15 +41,15 @@ export async function googleImage(input, config, apiKey) {
|
|
|
41
41
|
return success({
|
|
42
42
|
images,
|
|
43
43
|
model: config.model,
|
|
44
|
-
costEstimate: calculateGoogleImageCost(config.model, images.length),
|
|
44
|
+
costEstimate: calculateGoogleImageCost(config.model, images.length, config.modelData),
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
catch (err) {
|
|
48
48
|
return failure(err instanceof Error ? err.message : "Google image request failed");
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
function calculateGoogleImageCost(modelName, imageCount) {
|
|
52
|
-
const model = getModel(modelName);
|
|
51
|
+
function calculateGoogleImageCost(modelName, imageCount, modelData) {
|
|
52
|
+
const model = getModel(modelName, modelData);
|
|
53
53
|
if (!model || !isImageModel(model) || !model.costPerImage)
|
|
54
54
|
return undefined;
|
|
55
55
|
const totalCost = round(model.costPerImage * imageCount, COST_DECIMAL_PLACES);
|
package/dist/image/openai.js
CHANGED
|
@@ -24,8 +24,8 @@ export async function openaiImage(input, config, apiKey) {
|
|
|
24
24
|
}));
|
|
25
25
|
const tokenUsage = extractUsage(response);
|
|
26
26
|
const costEstimate = tokenUsage
|
|
27
|
-
? calculateImageCost(config.model, tokenUsage)
|
|
28
|
-
: calculatePerImageCost(config.model, images.length);
|
|
27
|
+
? calculateImageCost(config.model, tokenUsage, config.modelData)
|
|
28
|
+
: calculatePerImageCost(config.model, images.length, config.modelData);
|
|
29
29
|
return success({
|
|
30
30
|
images,
|
|
31
31
|
model: config.model,
|
|
@@ -97,8 +97,8 @@ function extractUsage(response) {
|
|
|
97
97
|
totalTokens: u.total_tokens,
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
|
-
function calculateImageCost(modelName, usage) {
|
|
101
|
-
const model = getModel(modelName);
|
|
100
|
+
function calculateImageCost(modelName, usage, modelData) {
|
|
101
|
+
const model = getModel(modelName, modelData);
|
|
102
102
|
if (!model || !isImageModel(model))
|
|
103
103
|
return undefined;
|
|
104
104
|
const totalIn = usage.inputTokens ?? 0;
|
|
@@ -131,8 +131,8 @@ function calculateImageCost(modelName, usage) {
|
|
|
131
131
|
currency: "USD",
|
|
132
132
|
};
|
|
133
133
|
}
|
|
134
|
-
function calculatePerImageCost(modelName, imageCount) {
|
|
135
|
-
const model = getModel(modelName);
|
|
134
|
+
function calculatePerImageCost(modelName, imageCount, modelData) {
|
|
135
|
+
const model = getModel(modelName, modelData);
|
|
136
136
|
if (!model || !isImageModel(model) || !model.costPerImage)
|
|
137
137
|
return undefined;
|
|
138
138
|
const totalCost = round(model.costPerImage * imageCount, COST_DECIMAL_PLACES);
|
package/dist/image.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ModelDataBlob } from "./modelData.js";
|
|
2
2
|
import { Result } from "./types/result.js";
|
|
3
3
|
import { TokenUsage } from "./types/tokenUsage.js";
|
|
4
4
|
import { CostEstimate } from "./types/costEstimate.js";
|
|
@@ -11,7 +11,7 @@ export type ImageInput = string | {
|
|
|
11
11
|
};
|
|
12
12
|
export type ImageConfig = {
|
|
13
13
|
model: string;
|
|
14
|
-
provider?:
|
|
14
|
+
provider?: string;
|
|
15
15
|
n?: number;
|
|
16
16
|
size?: string;
|
|
17
17
|
quality?: "low" | "medium" | "high" | "auto";
|
|
@@ -20,6 +20,7 @@ export type ImageConfig = {
|
|
|
20
20
|
openAiApiKey?: string;
|
|
21
21
|
googleApiKey?: string;
|
|
22
22
|
metadata?: Record<string, unknown>;
|
|
23
|
+
modelData?: ModelDataBlob;
|
|
23
24
|
};
|
|
24
25
|
export type GeneratedImage = {
|
|
25
26
|
data: Uint8Array;
|
|
@@ -32,4 +33,6 @@ export type ImageGenResult = {
|
|
|
32
33
|
tokenUsage?: TokenUsage;
|
|
33
34
|
costEstimate?: CostEstimate;
|
|
34
35
|
};
|
|
36
|
+
export type ImageProvider = (input: ImageInput, config: ImageConfig) => Promise<Result<ImageGenResult>>;
|
|
37
|
+
export declare function registerImageProvider(name: string, fn: ImageProvider): void;
|
|
35
38
|
export declare function image(input: ImageInput, config: ImageConfig): Promise<Result<ImageGenResult>>;
|
package/dist/image.js
CHANGED
|
@@ -2,10 +2,16 @@ import { failure } from "./types/result.js";
|
|
|
2
2
|
import { resolveProvider, resolveApiKey } from "./util/provider.js";
|
|
3
3
|
import { openaiImage } from "./image/openai.js";
|
|
4
4
|
import { googleImage } from "./image/google.js";
|
|
5
|
+
// Null-prototype so provider names like "toString"/"__proto__" can't collide
|
|
6
|
+
// with Object.prototype or pollute the registry.
|
|
7
|
+
const registeredImageProviders = Object.create(null);
|
|
8
|
+
export function registerImageProvider(name, fn) {
|
|
9
|
+
registeredImageProviders[name] = fn;
|
|
10
|
+
}
|
|
5
11
|
export async function image(input, config) {
|
|
6
12
|
let provider;
|
|
7
13
|
try {
|
|
8
|
-
provider = resolveProvider(config.model, config.provider);
|
|
14
|
+
provider = resolveProvider(config.model, config.provider, config.modelData);
|
|
9
15
|
}
|
|
10
16
|
catch (err) {
|
|
11
17
|
return failure(err instanceof Error ? err.message : "Failed to resolve provider");
|
|
@@ -31,7 +37,12 @@ export async function image(input, config) {
|
|
|
31
37
|
}
|
|
32
38
|
return googleImage(input, config, apiKey);
|
|
33
39
|
}
|
|
34
|
-
default:
|
|
35
|
-
|
|
40
|
+
default: {
|
|
41
|
+
const custom = registeredImageProviders[provider];
|
|
42
|
+
if (custom) {
|
|
43
|
+
return custom(input, config);
|
|
44
|
+
}
|
|
45
|
+
return failure(`Provider "${provider}" does not support image generation. Register one with registerImageProvider(name, fn).`);
|
|
46
|
+
}
|
|
36
47
|
}
|
|
37
48
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/model.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { ModelName, Provider } from "./models.js";
|
|
2
2
|
import { ModelLike } from "./types.js";
|
|
3
|
+
import type { ModelDataBlob } from "./modelData.js";
|
|
3
4
|
export declare class Model {
|
|
4
5
|
private model;
|
|
5
6
|
private provider?;
|
|
6
|
-
|
|
7
|
+
private modelData?;
|
|
8
|
+
constructor(model: ModelName, provider?: Provider, modelData?: ModelDataBlob);
|
|
7
9
|
getModel(): ModelName;
|
|
8
10
|
getProvider(): Provider | undefined;
|
|
9
11
|
private lookupProvider;
|
|
@@ -22,5 +24,5 @@ export declare class Model {
|
|
|
22
24
|
} | null;
|
|
23
25
|
toString(): string;
|
|
24
26
|
toJSON(): ModelName;
|
|
25
|
-
static create(model: ModelLike, provider?: Provider): Model;
|
|
27
|
+
static create(model: ModelLike, provider?: Provider, modelData?: ModelDataBlob): Model;
|
|
26
28
|
}
|
package/dist/model.js
CHANGED
|
@@ -4,11 +4,13 @@ import { round } from "./util/util.js";
|
|
|
4
4
|
export class Model {
|
|
5
5
|
model;
|
|
6
6
|
provider;
|
|
7
|
-
|
|
7
|
+
modelData;
|
|
8
|
+
constructor(model, provider, modelData) {
|
|
8
9
|
if (!ModelNameSchema.safeParse(model).success) {
|
|
9
10
|
throw new SmolError(`Model ${JSON.stringify(model)} is not recognized. Please specify a known model name.`);
|
|
10
11
|
}
|
|
11
12
|
this.model = model;
|
|
13
|
+
this.modelData = modelData;
|
|
12
14
|
this.provider = provider || this.lookupProvider();
|
|
13
15
|
}
|
|
14
16
|
getModel() {
|
|
@@ -18,11 +20,11 @@ export class Model {
|
|
|
18
20
|
return this.provider;
|
|
19
21
|
}
|
|
20
22
|
lookupProvider() {
|
|
21
|
-
const modelInfo = getModel(this.model);
|
|
23
|
+
const modelInfo = getModel(this.model, this.modelData);
|
|
22
24
|
return modelInfo ? modelInfo.provider : undefined;
|
|
23
25
|
}
|
|
24
26
|
calculateCost(usage) {
|
|
25
|
-
const model = getModel(this.model);
|
|
27
|
+
const model = getModel(this.model, this.modelData);
|
|
26
28
|
if (!model || !isTextModel(model)) {
|
|
27
29
|
return null;
|
|
28
30
|
}
|
|
@@ -79,10 +81,10 @@ export class Model {
|
|
|
79
81
|
toJSON() {
|
|
80
82
|
return this.model;
|
|
81
83
|
}
|
|
82
|
-
static create(model, provider) {
|
|
84
|
+
static create(model, provider, modelData) {
|
|
83
85
|
if (model instanceof Model) {
|
|
84
86
|
return model;
|
|
85
87
|
}
|
|
86
|
-
return new Model(model, provider);
|
|
88
|
+
return new Model(model, provider, modelData);
|
|
87
89
|
}
|
|
88
90
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ModelType } from "./models.js";
|
|
2
|
+
import { Result } from "./types/result.js";
|
|
3
|
+
export declare const SUPPORTED_SCHEMA_VERSION = 1;
|
|
4
|
+
export type HostedToolPrice = {
|
|
5
|
+
unit: "per_call" | "per_session" | "per_hour" | "per_gb_day" | "tokens" | "free";
|
|
6
|
+
amount?: number;
|
|
7
|
+
freeAllowance?: string;
|
|
8
|
+
note?: string;
|
|
9
|
+
};
|
|
10
|
+
export type HostedToolPricing = HostedToolPrice & {
|
|
11
|
+
perModel?: Record<string, Partial<HostedToolPrice>>;
|
|
12
|
+
};
|
|
13
|
+
export type HostedTool = {
|
|
14
|
+
name: string;
|
|
15
|
+
provider: string;
|
|
16
|
+
category?: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
providerToolId?: string;
|
|
19
|
+
models?: string[];
|
|
20
|
+
pricing?: HostedToolPricing;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
};
|
|
23
|
+
export type ModelDataBlob = {
|
|
24
|
+
schemaVersion: number;
|
|
25
|
+
generatedAt: string;
|
|
26
|
+
models: ModelType[];
|
|
27
|
+
hostedTools: HostedTool[];
|
|
28
|
+
};
|
|
29
|
+
export declare function parseModelDataBlob(raw: string): Result<ModelDataBlob>;
|
|
30
|
+
export declare function deepMergeEntry<T>(base: T, overlay: T): T;
|
|
31
|
+
export declare function mergeModelData(base: ModelType[], overlay: ModelType[]): ModelType[];
|
|
32
|
+
export declare function mergeHostedTools(base: HostedTool[], overlay: HostedTool[]): HostedTool[];
|
|
33
|
+
export declare const DEFAULT_MODEL_DATA_URL = "https://raw.githubusercontent.com/egonSchiele/smoltalk/main/packages/smoltalk/data/model-data.json";
|
|
34
|
+
export type Fetcher = (url: string, signal?: AbortSignal) => Promise<string>;
|
|
35
|
+
export type RefreshOptions = {
|
|
36
|
+
url?: string;
|
|
37
|
+
fetcher?: Fetcher;
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
};
|
|
40
|
+
export declare function refreshModels(opts?: RefreshOptions): Promise<Result<ModelDataBlob>>;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { success, failure } from "./types/result.js";
|
|
5
|
+
export const SUPPORTED_SCHEMA_VERSION = 1;
|
|
6
|
+
const EnvelopeSchema = z.object({
|
|
7
|
+
schemaVersion: z.number(),
|
|
8
|
+
generatedAt: z.string(),
|
|
9
|
+
models: z.array(z.unknown()),
|
|
10
|
+
hostedTools: z.array(z.unknown()).optional(),
|
|
11
|
+
});
|
|
12
|
+
// Permissive: require the fields smoltalk keys on, keep everything else.
|
|
13
|
+
const ModelEntrySchema = z
|
|
14
|
+
.object({
|
|
15
|
+
modelName: z.string(),
|
|
16
|
+
provider: z.string(),
|
|
17
|
+
type: z.string(),
|
|
18
|
+
})
|
|
19
|
+
.catchall(z.unknown());
|
|
20
|
+
// Per-model override: price fields only. No `.catchall`, so unknown keys
|
|
21
|
+
// (including a stray nested `perModel`) are stripped — the resolver can never
|
|
22
|
+
// reintroduce perModel into its result.
|
|
23
|
+
const PerModelPriceSchema = z.object({
|
|
24
|
+
unit: z.string().optional(),
|
|
25
|
+
amount: z.number().optional(),
|
|
26
|
+
freeAllowance: z.string().optional(),
|
|
27
|
+
note: z.string().optional(),
|
|
28
|
+
});
|
|
29
|
+
// `unit` stays a lenient string (forward-compat, mirroring ModelEntrySchema's
|
|
30
|
+
// `type: z.string()`); the closed union is the known/intended set.
|
|
31
|
+
const HostedToolPricingSchema = z
|
|
32
|
+
.object({
|
|
33
|
+
unit: z.string(),
|
|
34
|
+
amount: z.number().optional(),
|
|
35
|
+
freeAllowance: z.string().optional(),
|
|
36
|
+
note: z.string().optional(),
|
|
37
|
+
perModel: z.record(z.string(), PerModelPriceSchema).optional(),
|
|
38
|
+
})
|
|
39
|
+
.catchall(z.unknown());
|
|
40
|
+
const HostedToolSchema = z
|
|
41
|
+
.object({
|
|
42
|
+
name: z.string(),
|
|
43
|
+
provider: z.string(),
|
|
44
|
+
category: z.string().optional(),
|
|
45
|
+
description: z.string().optional(),
|
|
46
|
+
providerToolId: z.string().optional(),
|
|
47
|
+
models: z.array(z.string()).optional(),
|
|
48
|
+
pricing: HostedToolPricingSchema.optional(),
|
|
49
|
+
disabled: z.boolean().optional(),
|
|
50
|
+
})
|
|
51
|
+
.catchall(z.unknown());
|
|
52
|
+
export function parseModelDataBlob(raw) {
|
|
53
|
+
let json;
|
|
54
|
+
try {
|
|
55
|
+
json = JSON.parse(raw);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
return failure(`Model data is not valid JSON: ${String(err)}`);
|
|
59
|
+
}
|
|
60
|
+
const envelope = EnvelopeSchema.safeParse(json);
|
|
61
|
+
if (!envelope.success) {
|
|
62
|
+
return failure(`Model data blob is malformed: ${z.prettifyError(envelope.error)}`);
|
|
63
|
+
}
|
|
64
|
+
if (envelope.data.schemaVersion > SUPPORTED_SCHEMA_VERSION) {
|
|
65
|
+
return failure(`Model data schemaVersion ${envelope.data.schemaVersion} is newer than this smoltalk supports (${SUPPORTED_SCHEMA_VERSION}). Please upgrade smoltalk.`);
|
|
66
|
+
}
|
|
67
|
+
const models = [];
|
|
68
|
+
for (const entry of envelope.data.models) {
|
|
69
|
+
const parsed = ModelEntrySchema.safeParse(entry);
|
|
70
|
+
if (!parsed.success) {
|
|
71
|
+
console.warn(`Skipping invalid model entry: ${z.prettifyError(parsed.error)}`);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
models.push(parsed.data);
|
|
75
|
+
}
|
|
76
|
+
const hostedTools = [];
|
|
77
|
+
let rawTools = [];
|
|
78
|
+
if (envelope.data.hostedTools) {
|
|
79
|
+
rawTools = envelope.data.hostedTools;
|
|
80
|
+
}
|
|
81
|
+
for (const entry of rawTools) {
|
|
82
|
+
const parsed = HostedToolSchema.safeParse(entry);
|
|
83
|
+
if (!parsed.success) {
|
|
84
|
+
console.warn(`Skipping invalid hosted tool: ${z.prettifyError(parsed.error)}`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
hostedTools.push(parsed.data);
|
|
88
|
+
}
|
|
89
|
+
return success({
|
|
90
|
+
schemaVersion: envelope.data.schemaVersion,
|
|
91
|
+
generatedAt: envelope.data.generatedAt,
|
|
92
|
+
models,
|
|
93
|
+
hostedTools,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function isPlainObject(value) {
|
|
97
|
+
if (value === null) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
if (typeof value !== "object") {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
if (Array.isArray(value)) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
export function deepMergeEntry(base, overlay) {
|
|
109
|
+
if (!isPlainObject(base) || !isPlainObject(overlay)) {
|
|
110
|
+
return overlay;
|
|
111
|
+
}
|
|
112
|
+
const result = { ...base };
|
|
113
|
+
for (const key of Object.keys(overlay)) {
|
|
114
|
+
const overValue = overlay[key];
|
|
115
|
+
if (overValue === undefined) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const currentValue = result[key];
|
|
119
|
+
if (isPlainObject(currentValue) && isPlainObject(overValue)) {
|
|
120
|
+
result[key] = deepMergeEntry(currentValue, overValue);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
result[key] = overValue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
function mergeByKey(base, overlay, keyOf) {
|
|
129
|
+
const order = [];
|
|
130
|
+
const map = new Map();
|
|
131
|
+
for (const item of base) {
|
|
132
|
+
const key = keyOf(item);
|
|
133
|
+
if (!map.has(key)) {
|
|
134
|
+
order.push(key);
|
|
135
|
+
}
|
|
136
|
+
map.set(key, item);
|
|
137
|
+
}
|
|
138
|
+
for (const item of overlay) {
|
|
139
|
+
const key = keyOf(item);
|
|
140
|
+
const existing = map.get(key);
|
|
141
|
+
if (existing === undefined) {
|
|
142
|
+
order.push(key);
|
|
143
|
+
map.set(key, item);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
map.set(key, deepMergeEntry(existing, item));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return order.map((key) => map.get(key));
|
|
150
|
+
}
|
|
151
|
+
export function mergeModelData(base, overlay) {
|
|
152
|
+
return mergeByKey(base, overlay, (m) => `${m.provider}:${m.modelName}`);
|
|
153
|
+
}
|
|
154
|
+
export function mergeHostedTools(base, overlay) {
|
|
155
|
+
return mergeByKey(base, overlay, (t) => `${t.provider}:${t.name}`);
|
|
156
|
+
}
|
|
157
|
+
export const DEFAULT_MODEL_DATA_URL = "https://raw.githubusercontent.com/egonSchiele/smoltalk/main/packages/smoltalk/data/model-data.json";
|
|
158
|
+
const MAX_BYTES = 10_000_000;
|
|
159
|
+
const TIMEOUT_MS = 15_000;
|
|
160
|
+
function enforceSizeCap(text) {
|
|
161
|
+
if (Buffer.byteLength(text, "utf8") > MAX_BYTES) {
|
|
162
|
+
throw new Error(`Model data exceeds the ${MAX_BYTES}-byte cap`);
|
|
163
|
+
}
|
|
164
|
+
return text;
|
|
165
|
+
}
|
|
166
|
+
// The default fetcher supports remote https:// URLs and local file:// URLs.
|
|
167
|
+
// file:// lets users point at a self-hosted on-disk catalog (and powers the
|
|
168
|
+
// integration test). Any other scheme is rejected.
|
|
169
|
+
async function defaultFetcher(url, signal) {
|
|
170
|
+
if (url.startsWith("file://")) {
|
|
171
|
+
const path = fileURLToPath(url);
|
|
172
|
+
const text = await readFile(path, "utf8");
|
|
173
|
+
return enforceSizeCap(text);
|
|
174
|
+
}
|
|
175
|
+
if (url.startsWith("https://")) {
|
|
176
|
+
const res = await fetch(url, { signal });
|
|
177
|
+
if (!res.ok) {
|
|
178
|
+
throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
179
|
+
}
|
|
180
|
+
return enforceSizeCap(await res.text());
|
|
181
|
+
}
|
|
182
|
+
throw new Error(`Unsupported model-data URL scheme (expected https:// or file://): ${url}`);
|
|
183
|
+
}
|
|
184
|
+
export async function refreshModels(opts = {}) {
|
|
185
|
+
let url = DEFAULT_MODEL_DATA_URL;
|
|
186
|
+
if (process.env.SMOLTALK_MODEL_DATA_URL) {
|
|
187
|
+
url = process.env.SMOLTALK_MODEL_DATA_URL;
|
|
188
|
+
}
|
|
189
|
+
if (opts.url) {
|
|
190
|
+
url = opts.url;
|
|
191
|
+
}
|
|
192
|
+
let fetcher = defaultFetcher;
|
|
193
|
+
if (opts.fetcher) {
|
|
194
|
+
fetcher = opts.fetcher;
|
|
195
|
+
}
|
|
196
|
+
// Always drive the fetcher off our own controller so the timeout can abort
|
|
197
|
+
// it. Forward an externally-supplied signal into the same controller so both
|
|
198
|
+
// the timeout and the caller can cancel.
|
|
199
|
+
const controller = new AbortController();
|
|
200
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
201
|
+
if (opts.signal) {
|
|
202
|
+
if (opts.signal.aborted) {
|
|
203
|
+
controller.abort();
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
opts.signal.addEventListener("abort", () => controller.abort(), {
|
|
207
|
+
once: true,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
let raw;
|
|
212
|
+
try {
|
|
213
|
+
raw = await fetcher(url, controller.signal);
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
return failure(`Could not fetch model data from ${url}: ${String(err)}`);
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
}
|
|
221
|
+
return parseModelDataBlob(raw);
|
|
222
|
+
}
|