smoltalk 0.10.1 → 0.11.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 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.10.1",
3
+ "version": "0.11.0",
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.2.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",