smoltalk 0.10.1 → 0.11.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/README.md +34 -0
- package/dist/client.d.ts +6 -0
- package/dist/client.js +12 -0
- package/dist/clients/llamaCppLoader.d.ts +42 -0
- package/dist/clients/llamaCppLoader.js +109 -0
- package/dist/functions.js +8 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -507,6 +507,40 @@ Text, transcription, and speech are classes: a base class owns the shared
|
|
|
507
507
|
behavior (validation, cost, error handling) and the subclass implements only
|
|
508
508
|
the provider call. Embeddings and images are one-shot functions.
|
|
509
509
|
|
|
510
|
+
## Local models (llama-cpp)
|
|
511
|
+
|
|
512
|
+
Install the optional plugin and name the provider — no wiring code:
|
|
513
|
+
|
|
514
|
+
```bash
|
|
515
|
+
npm i smoltalk-llama-cpp
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
```typescript
|
|
519
|
+
import { textSync, userMessage } from "smoltalk";
|
|
520
|
+
|
|
521
|
+
const result = await textSync({
|
|
522
|
+
provider: "llama-cpp",
|
|
523
|
+
model: "/path/to/llama-3.gguf",
|
|
524
|
+
messages: [userMessage("Hello!")],
|
|
525
|
+
});
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
smoltalk lazily imports and registers the plugin on the first `llama-cpp`
|
|
529
|
+
call; if the package is missing you get an install hint instead of a
|
|
530
|
+
resolution stack trace. Hosts with unusual layouts (e.g. a globally-installed
|
|
531
|
+
CLI with the plugin installed globally beside it) can hand smoltalk the
|
|
532
|
+
plugin's entry path explicitly and skip Node resolution:
|
|
533
|
+
|
|
534
|
+
```typescript
|
|
535
|
+
import { loadLlamaCpp } from "smoltalk";
|
|
536
|
+
|
|
537
|
+
const { resolveModel } = await loadLlamaCpp({
|
|
538
|
+
entryPath: "/path/to/smoltalk-llama-cpp/dist/index.js",
|
|
539
|
+
});
|
|
540
|
+
// resolveModel downloads hf: URIs (and absolutizes existing local paths):
|
|
541
|
+
const modelPath = await resolveModel("hf:org/repo/model.gguf", "/models/cache");
|
|
542
|
+
```
|
|
543
|
+
|
|
510
544
|
## Audio (STT/TTS)
|
|
511
545
|
|
|
512
546
|
Three audio primitives. `transcribe()` (speech-to-text) and `speak()`
|
package/dist/client.d.ts
CHANGED
|
@@ -12,4 +12,10 @@ import { BaseClient } from "./clients/baseClient.js";
|
|
|
12
12
|
import { SmolClientConfig } from "./types.js";
|
|
13
13
|
export declare function registerProvider(providerName: string, clientClass: typeof BaseClient): void;
|
|
14
14
|
export declare function unregisterProvider(providerName: string): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* True when `providerName` has been registered via registerProvider().
|
|
17
|
+
* Built-in providers (the switch cases in getClient) are not its concern —
|
|
18
|
+
* this only consults the custom registry.
|
|
19
|
+
*/
|
|
20
|
+
export declare function hasProvider(providerName: string): boolean;
|
|
15
21
|
export declare function getClient(config: SmolClientConfig): BaseClient;
|
package/dist/client.js
CHANGED
|
@@ -33,6 +33,14 @@ export function unregisterProvider(providerName) {
|
|
|
33
33
|
}
|
|
34
34
|
return false;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* True when `providerName` has been registered via registerProvider().
|
|
38
|
+
* Built-in providers (the switch cases in getClient) are not its concern —
|
|
39
|
+
* this only consults the custom registry.
|
|
40
|
+
*/
|
|
41
|
+
export function hasProvider(providerName) {
|
|
42
|
+
return providerName in registeredProviders;
|
|
43
|
+
}
|
|
36
44
|
export function getClient(config) {
|
|
37
45
|
const modelName = config.model;
|
|
38
46
|
const provider = resolveProvider(modelName, config.provider, config.modelData);
|
|
@@ -100,6 +108,10 @@ export function getClient(config) {
|
|
|
100
108
|
const ClientClass = registeredProviders[provider];
|
|
101
109
|
return new ClientClass(clientConfig);
|
|
102
110
|
}
|
|
111
|
+
if (provider === "llama-cpp") {
|
|
112
|
+
throw new SmolError("The llama-cpp provider loads automatically when called through text()/textSync()/textStream(). " +
|
|
113
|
+
"For direct getClient() use, await loadLlamaCpp() first (install smoltalk-llama-cpp if it is missing).");
|
|
114
|
+
}
|
|
103
115
|
throw new SmolError(`Model provider ${provider} is not supported. To use a custom provider, register it first via registerProvider(name, ClientClass).`);
|
|
104
116
|
}
|
|
105
117
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { BaseClient } from "./baseClient.js";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal structural view of smoltalk-llama-cpp's module. Declared here (not
|
|
4
|
+
* imported from the plugin) so smoltalk compiles without the plugin installed
|
|
5
|
+
* and the workspace gains no build-order cycle.
|
|
6
|
+
*/
|
|
7
|
+
export type LlamaCppModule = {
|
|
8
|
+
LlamaCPP: typeof BaseClient;
|
|
9
|
+
resolveModel: (uriOrPath: string, cacheDir: string) => Promise<string>;
|
|
10
|
+
};
|
|
11
|
+
type ImportFn = (specifier: string) => Promise<Record<string, unknown>>;
|
|
12
|
+
/**
|
|
13
|
+
* Test-only: swap the dynamic import (pass undefined to restore the real one)
|
|
14
|
+
* and clear the load cache. Deliberately NOT exported from the package index.
|
|
15
|
+
*/
|
|
16
|
+
export declare function _setImportForTests(fn?: ImportFn): void;
|
|
17
|
+
/**
|
|
18
|
+
* Load and register the optional smoltalk-llama-cpp plugin, once per process.
|
|
19
|
+
*
|
|
20
|
+
* - Without options, imports the bare specifier "smoltalk-llama-cpp" using
|
|
21
|
+
* Node resolution from smoltalk's location. The optional peer declaration
|
|
22
|
+
* in package.json is what makes that resolvable under pnpm's strict layout.
|
|
23
|
+
* - `entryPath` is the escape hatch for hosts whose plugin install is not
|
|
24
|
+
* resolvable from smoltalk (e.g. globally-installed CLIs): the file is
|
|
25
|
+
* imported directly and Node resolution is skipped. Hosts own discovering
|
|
26
|
+
* that path; smoltalk never probes global npm roots and reads no env vars.
|
|
27
|
+
* - Registers the module's LlamaCPP class under "llama-cpp" unless that name
|
|
28
|
+
* is already registered. An existing registration is left untouched, but
|
|
29
|
+
* the module is still imported, validated, and returned — an existing
|
|
30
|
+
* registration wins the registry, never the return value. Registration is
|
|
31
|
+
* re-ensured from the cached module on EVERY call, so a later
|
|
32
|
+
* unregisterProvider("llama-cpp") is undone by the next load call without
|
|
33
|
+
* a second import.
|
|
34
|
+
* - Concurrent first calls share one in-flight load. A failed load clears
|
|
35
|
+
* the cache so a later call can retry (e.g. after installing the package).
|
|
36
|
+
* A second call with a different entryPath after a successful load returns
|
|
37
|
+
* the already-loaded module (first load wins).
|
|
38
|
+
*/
|
|
39
|
+
export declare function loadLlamaCpp(options?: {
|
|
40
|
+
entryPath?: string;
|
|
41
|
+
}): Promise<LlamaCppModule>;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { pathToFileURL } from "url";
|
|
2
|
+
import { hasProvider, registerProvider } from "../client.js";
|
|
3
|
+
import { SmolError } from "../smolError.js";
|
|
4
|
+
const realImport = (specifier) => import(specifier);
|
|
5
|
+
let importFn = realImport;
|
|
6
|
+
let cachedLoad;
|
|
7
|
+
/**
|
|
8
|
+
* Test-only: swap the dynamic import (pass undefined to restore the real one)
|
|
9
|
+
* and clear the load cache. Deliberately NOT exported from the package index.
|
|
10
|
+
*/
|
|
11
|
+
export function _setImportForTests(fn) {
|
|
12
|
+
if (fn) {
|
|
13
|
+
importFn = fn;
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
importFn = realImport;
|
|
17
|
+
}
|
|
18
|
+
cachedLoad = undefined;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Load and register the optional smoltalk-llama-cpp plugin, once per process.
|
|
22
|
+
*
|
|
23
|
+
* - Without options, imports the bare specifier "smoltalk-llama-cpp" using
|
|
24
|
+
* Node resolution from smoltalk's location. The optional peer declaration
|
|
25
|
+
* in package.json is what makes that resolvable under pnpm's strict layout.
|
|
26
|
+
* - `entryPath` is the escape hatch for hosts whose plugin install is not
|
|
27
|
+
* resolvable from smoltalk (e.g. globally-installed CLIs): the file is
|
|
28
|
+
* imported directly and Node resolution is skipped. Hosts own discovering
|
|
29
|
+
* that path; smoltalk never probes global npm roots and reads no env vars.
|
|
30
|
+
* - Registers the module's LlamaCPP class under "llama-cpp" unless that name
|
|
31
|
+
* is already registered. An existing registration is left untouched, but
|
|
32
|
+
* the module is still imported, validated, and returned — an existing
|
|
33
|
+
* registration wins the registry, never the return value. Registration is
|
|
34
|
+
* re-ensured from the cached module on EVERY call, so a later
|
|
35
|
+
* unregisterProvider("llama-cpp") is undone by the next load call without
|
|
36
|
+
* a second import.
|
|
37
|
+
* - Concurrent first calls share one in-flight load. A failed load clears
|
|
38
|
+
* the cache so a later call can retry (e.g. after installing the package).
|
|
39
|
+
* A second call with a different entryPath after a successful load returns
|
|
40
|
+
* the already-loaded module (first load wins).
|
|
41
|
+
*/
|
|
42
|
+
export function loadLlamaCpp(options) {
|
|
43
|
+
if (!cachedLoad) {
|
|
44
|
+
const load = doLoad(options?.entryPath);
|
|
45
|
+
cachedLoad = load;
|
|
46
|
+
load.catch(() => {
|
|
47
|
+
if (cachedLoad === load) {
|
|
48
|
+
cachedLoad = undefined;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
// Registration happens on EVERY call, not only inside the first load: a
|
|
53
|
+
// consumer can unregisterProvider("llama-cpp") after a successful load,
|
|
54
|
+
// and the cached module must be re-registered on the next call or the
|
|
55
|
+
// provider stays missing for the life of the process.
|
|
56
|
+
return cachedLoad.then((plugin) => {
|
|
57
|
+
if (!hasProvider("llama-cpp")) {
|
|
58
|
+
registerProvider("llama-cpp", plugin.LlamaCPP);
|
|
59
|
+
}
|
|
60
|
+
return plugin;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async function doLoad(entryPath) {
|
|
64
|
+
let importSource;
|
|
65
|
+
if (entryPath) {
|
|
66
|
+
importSource = pathToFileURL(entryPath).href;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
importSource = "smoltalk-llama-cpp";
|
|
70
|
+
}
|
|
71
|
+
let mod;
|
|
72
|
+
try {
|
|
73
|
+
mod = await importFn(importSource);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (!entryPath && isPluginNotInstalledError(error)) {
|
|
77
|
+
throw new SmolError("The llama-cpp provider needs the optional smoltalk-llama-cpp package. " +
|
|
78
|
+
"Install it (npm i smoltalk-llama-cpp) and try again.", { cause: error });
|
|
79
|
+
}
|
|
80
|
+
throw new SmolError(`Failed to load smoltalk-llama-cpp from ${importSource}: ${errorMessage(error)}`, { cause: error });
|
|
81
|
+
}
|
|
82
|
+
if (typeof mod.LlamaCPP !== "function") {
|
|
83
|
+
throw new SmolError(`The module imported as ${importSource} does not export LlamaCPP — ` +
|
|
84
|
+
"it does not appear to be the smoltalk-llama-cpp package.");
|
|
85
|
+
}
|
|
86
|
+
if (typeof mod.resolveModel !== "function") {
|
|
87
|
+
throw new SmolError("Your installed smoltalk-llama-cpp is too old for this version of smoltalk. " +
|
|
88
|
+
"Upgrade it (npm i smoltalk-llama-cpp@latest; >=0.2.0 required).");
|
|
89
|
+
}
|
|
90
|
+
return mod;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* True only when the smoltalk-llama-cpp specifier itself failed to resolve —
|
|
94
|
+
* not when the package exists but its own import chain broke (e.g. a
|
|
95
|
+
* node-llama-cpp binary problem), where an install hint would mislead.
|
|
96
|
+
*/
|
|
97
|
+
function isPluginNotInstalledError(error) {
|
|
98
|
+
const code = error?.code;
|
|
99
|
+
if (code !== "ERR_MODULE_NOT_FOUND") {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return errorMessage(error).includes("'smoltalk-llama-cpp'");
|
|
103
|
+
}
|
|
104
|
+
function errorMessage(error) {
|
|
105
|
+
if (error instanceof Error) {
|
|
106
|
+
return error.message;
|
|
107
|
+
}
|
|
108
|
+
return String(error);
|
|
109
|
+
}
|
package/dist/functions.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BaseMessage, messageFromJSON, } from "./classes/message/index.js";
|
|
2
|
-
import { getClient } from "./client.js";
|
|
2
|
+
import { getClient, hasProvider } from "./client.js";
|
|
3
|
+
import { loadLlamaCpp } from "./clients/llamaCppLoader.js";
|
|
3
4
|
import { getLogger } from "./util/logger.js";
|
|
4
5
|
function fixMessagesIfNecessary(messages) {
|
|
5
6
|
if (messages && messages.length > 0) {
|
|
@@ -16,10 +17,16 @@ export function text(config) {
|
|
|
16
17
|
return textSync(config);
|
|
17
18
|
}
|
|
18
19
|
export async function textSync(config) {
|
|
20
|
+
if (config.provider === "llama-cpp" && !hasProvider("llama-cpp")) {
|
|
21
|
+
await loadLlamaCpp();
|
|
22
|
+
}
|
|
19
23
|
config.messages = fixMessagesIfNecessary(config.messages);
|
|
20
24
|
return getClient(config).textSync(config);
|
|
21
25
|
}
|
|
22
26
|
export async function* textStream(config) {
|
|
27
|
+
if (config.provider === "llama-cpp" && !hasProvider("llama-cpp")) {
|
|
28
|
+
await loadLlamaCpp();
|
|
29
|
+
}
|
|
23
30
|
config.messages = fixMessagesIfNecessary(config.messages);
|
|
24
31
|
yield* getClient(config).textStream(config);
|
|
25
32
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export * from "./util/util.js";
|
|
|
8
8
|
export * from "./util/tool.js";
|
|
9
9
|
export * from "./classes/message/index.js";
|
|
10
10
|
export * from "./functions.js";
|
|
11
|
+
export { loadLlamaCpp } from "./clients/llamaCppLoader.js";
|
|
12
|
+
export type { LlamaCppModule } from "./clients/llamaCppLoader.js";
|
|
11
13
|
export * from "./classes/ToolCall.js";
|
|
12
14
|
export * from "./embed.js";
|
|
13
15
|
export * from "./image.js";
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,8 @@ export * from "./util/util.js";
|
|
|
8
8
|
export * from "./util/tool.js";
|
|
9
9
|
export * from "./classes/message/index.js";
|
|
10
10
|
export * from "./functions.js";
|
|
11
|
+
// Explicit (not `export *`) so the test-only `_setImportForTests` stays off the public surface.
|
|
12
|
+
export { loadLlamaCpp } from "./clients/llamaCppLoader.js";
|
|
11
13
|
export * from "./classes/ToolCall.js";
|
|
12
14
|
export * from "./embed.js";
|
|
13
15
|
export * from "./image.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smoltalk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "A common interface for LLM APIs",
|
|
5
5
|
"homepage": "https://github.com/egonSchiele/smoltalk",
|
|
6
6
|
"files": [
|
|
@@ -38,6 +38,14 @@
|
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"tsx": "^4.19.2"
|
|
40
40
|
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"smoltalk-llama-cpp": ">=0.3.0 <1.0.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependenciesMeta": {
|
|
45
|
+
"smoltalk-llama-cpp": {
|
|
46
|
+
"optional": true
|
|
47
|
+
}
|
|
48
|
+
},
|
|
41
49
|
"scripts": {
|
|
42
50
|
"test": "vitest --exclude=**/*.live.test.ts",
|
|
43
51
|
"test:live": "vitest run lib/clients/*.live.test.ts lib/embed/*.live.test.ts lib/image/*.live.test.ts",
|