@happyvertical/smrt-core 0.60.8 → 0.60.9

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
@@ -465,6 +465,42 @@ const isExpensive = await product.is('costs more than the average product');
465
465
  const description = await product.do('Write a short marketing description');
466
466
  ```
467
467
 
468
+ ### BYO inference presets and local discovery
469
+
470
+ From Node application code, import `resolveInferencePreset` and
471
+ `discoverLocalInferenceModels` from `@happyvertical/smrt-core/byo-inference`.
472
+ Pass the options from `resolveInferencePreset(id, { model, apiKey })` to
473
+ `getAI` from `@happyvertical/ai` after checking for `null`. Each preset uses
474
+ the OpenAI-compatible SDK provider and requires an explicit model ID.
475
+
476
+ | Preset ID | Default API base URL | API key |
477
+ | --- | --- | --- |
478
+ | `openrouter` | `https://openrouter.ai/api/v1` | Required |
479
+ | `lemonade` | `http://127.0.0.1:13305/v1` | Supply if server authentication is enabled |
480
+ | `llama.cpp` | `http://127.0.0.1:9931/v1` | Supply if server authentication is enabled |
481
+ | `lm-studio` | `http://127.0.0.1:1234/v1` | Supply if server authentication is enabled |
482
+
483
+ The resolver returns `null` for unknown preset IDs, blank model IDs, and
484
+ missing OpenRouter keys. The SDK requires a nonempty key, so keyless local
485
+ options use a fixed synthetic credential.
486
+
487
+ Call `discoverLocalInferenceModels()` explicitly when the application needs
488
+ to suggest models. It makes parallel, credential-free GET requests to Ollama
489
+ (`127.0.0.1:11434/api/tags`) and Lemonade
490
+ (`127.0.0.1:13305/v1/models`). Each probe has a one-second deadline and
491
+ a 1 MiB response limit. A reachable service with an empty model list returns
492
+ `available: true` and `models: []`. An unreachable service, a service that
493
+ requires authentication, or a failed probe returns `available: false`.
494
+ Discovery excludes Ollama models with upstream model or host metadata.
495
+ Ollama's `baseUrl` is suitable for the SDK's `ollama` provider. Lemonade
496
+ model IDs can be used with the `lemonade` preset.
497
+
498
+ Provider references are available for [OpenRouter](https://openrouter.ai/developers),
499
+ [Lemonade](https://lemonade-server.ai/docs/api/openai/#get-v1models),
500
+ [llama.cpp](https://github.com/ggml-org/llama.cpp/tree/master/tools/server#quick-start),
501
+ [LM Studio](https://lmstudio.ai/docs/developer/openai-compat), and
502
+ [Ollama](https://github.com/ollama/ollama/blob/main/docs/openapi.yaml).
503
+
468
504
  ### Opt-In Read Cache
469
505
 
470
506
  SSR apps that re-query read-heavy / write-rare collections on every request
@@ -0,0 +1,55 @@
1
+ import type { OpenAIOptions } from '@happyvertical/ai';
2
+ declare const PRESETS: {
3
+ readonly openrouter: {
4
+ readonly baseUrl: "https://openrouter.ai/api/v1";
5
+ readonly requiresApiKey: true;
6
+ };
7
+ readonly lemonade: {
8
+ readonly baseUrl: "http://127.0.0.1:13305/v1";
9
+ readonly requiresApiKey: false;
10
+ };
11
+ readonly 'llama.cpp': {
12
+ readonly baseUrl: "http://127.0.0.1:9931/v1";
13
+ readonly requiresApiKey: false;
14
+ };
15
+ readonly 'lm-studio': {
16
+ readonly baseUrl: "http://127.0.0.1:1234/v1";
17
+ readonly requiresApiKey: false;
18
+ };
19
+ };
20
+ export type InferencePresetId = keyof typeof PRESETS;
21
+ /**
22
+ * Resolve a named preset with an explicit model. Returns null when a preset,
23
+ * model, or required API key is missing or invalid.
24
+ */
25
+ export declare function resolveInferencePreset(id: string, input: {
26
+ model: string;
27
+ apiKey?: string;
28
+ }): OpenAIOptions | null;
29
+ declare const LOCAL_SERVICES: readonly [{
30
+ readonly service: "ollama";
31
+ readonly baseUrl: "http://127.0.0.1:11434";
32
+ readonly modelPath: "/api/tags";
33
+ readonly listField: "models";
34
+ readonly idField: "name";
35
+ }, {
36
+ readonly service: "lemonade";
37
+ readonly baseUrl: "http://127.0.0.1:13305/v1";
38
+ readonly modelPath: "/models";
39
+ readonly listField: "data";
40
+ readonly idField: "id";
41
+ }];
42
+ type LocalService = (typeof LOCAL_SERVICES)[number];
43
+ export type LocalInferenceDiscovery = {
44
+ service: LocalService['service'];
45
+ baseUrl: string;
46
+ } & ({
47
+ available: true;
48
+ models: string[];
49
+ } | {
50
+ available: false;
51
+ });
52
+ /** Probe the default loopback Ollama and Lemonade servers without credentials. */
53
+ export declare function discoverLocalInferenceModels(): Promise<LocalInferenceDiscovery[]>;
54
+ export {};
55
+ //# sourceMappingURL=byo-inference.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"byo-inference.d.ts","sourceRoot":"","sources":["../src/byo-inference.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAIvD,QAAA,MAAM,OAAO;;;;;;;;;;;;;;;;;CAiBH,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,MAAM,OAAO,OAAO,CAAC;AAErD;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,MAAM,EACV,KAAK,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GACxC,aAAa,GAAG,IAAI,CAiBtB;AAED,QAAA,MAAM,cAAc;;;;;;;;;;;;EAeV,CAAC;AAEX,KAAK,YAAY,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAEpD,MAAM,MAAM,uBAAuB,GAAG;IACpC,OAAO,EAAE,YAAY,CAAC,SAAS,CAAC,CAAC;IACjC,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,CAAC;IAAE,SAAS,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG;IAAE,SAAS,EAAE,KAAK,CAAA;CAAE,CAAC,CAAC;AAmFnE,kFAAkF;AAClF,wBAAgB,4BAA4B,IAAI,OAAO,CACrD,uBAAuB,EAAE,CAC1B,CAEA"}
@@ -0,0 +1,113 @@
1
+ import { get } from "node:http";
2
+ //#region src/byo-inference.ts
3
+ var LEMONADE_BASE_URL = "http://127.0.0.1:13305/v1";
4
+ var PRESETS = {
5
+ openrouter: {
6
+ baseUrl: "https://openrouter.ai/api/v1",
7
+ requiresApiKey: true
8
+ },
9
+ lemonade: {
10
+ baseUrl: LEMONADE_BASE_URL,
11
+ requiresApiKey: false
12
+ },
13
+ "llama.cpp": {
14
+ baseUrl: "http://127.0.0.1:9931/v1",
15
+ requiresApiKey: false
16
+ },
17
+ "lm-studio": {
18
+ baseUrl: "http://127.0.0.1:1234/v1",
19
+ requiresApiKey: false
20
+ }
21
+ };
22
+ /**
23
+ * Resolve a named preset with an explicit model. Returns null when a preset,
24
+ * model, or required API key is missing or invalid.
25
+ */
26
+ function resolveInferencePreset(id, input) {
27
+ if (!Object.hasOwn(PRESETS, id)) return null;
28
+ const preset = PRESETS[id];
29
+ const model = typeof input.model === "string" ? input.model.trim() : "";
30
+ const apiKey = typeof input.apiKey === "string" ? input.apiKey.trim() : void 0;
31
+ if (!model || !apiKey && preset.requiresApiKey) return null;
32
+ return {
33
+ type: "openai",
34
+ baseUrl: preset.baseUrl,
35
+ defaultModel: model,
36
+ apiKey: apiKey || "smrt-local"
37
+ };
38
+ }
39
+ var LOCAL_SERVICES = [{
40
+ service: "ollama",
41
+ baseUrl: "http://127.0.0.1:11434",
42
+ modelPath: "/api/tags",
43
+ listField: "models",
44
+ idField: "name"
45
+ }, {
46
+ service: "lemonade",
47
+ baseUrl: LEMONADE_BASE_URL,
48
+ modelPath: "/models",
49
+ listField: "data",
50
+ idField: "id"
51
+ }];
52
+ var PROBE_TIMEOUT_MS = 1e3;
53
+ var MAX_RESPONSE_BYTES = 1024 * 1024;
54
+ function readField(value, field) {
55
+ if (value !== null && typeof value === "object" && Object.hasOwn(value, field)) return value[field];
56
+ }
57
+ async function readModels(response, service) {
58
+ if (response.statusCode !== 200) {
59
+ response.destroy();
60
+ return null;
61
+ }
62
+ const chunks = [];
63
+ let size = 0;
64
+ for await (const chunk of response) {
65
+ size += chunk.length;
66
+ if (size > MAX_RESPONSE_BYTES) return null;
67
+ chunks.push(chunk);
68
+ }
69
+ const entries = readField(JSON.parse(Buffer.concat(chunks).toString("utf8")), service.listField);
70
+ if (!Array.isArray(entries)) return null;
71
+ const models = /* @__PURE__ */ new Set();
72
+ for (const entry of entries) {
73
+ const id = readField(entry, service.idField);
74
+ if (typeof id !== "string" || !id.trim()) continue;
75
+ if (service.service === "ollama" && (readField(entry, "remote_model") || readField(entry, "remote_host"))) continue;
76
+ if (service.service === "lemonade" && (readField(entry, "recipe") === "cloud" || readField(entry, "downloaded") === false)) continue;
77
+ models.add(id);
78
+ }
79
+ return [...models];
80
+ }
81
+ async function probe(service) {
82
+ const identity = {
83
+ service: service.service,
84
+ baseUrl: service.baseUrl
85
+ };
86
+ try {
87
+ const models = await new Promise((resolve, reject) => {
88
+ get(service.baseUrl + service.modelPath, {
89
+ agent: false,
90
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
91
+ }, (response) => {
92
+ readModels(response, service).then(resolve, reject);
93
+ }).once("error", reject);
94
+ });
95
+ if (models) return {
96
+ ...identity,
97
+ available: true,
98
+ models
99
+ };
100
+ } catch {}
101
+ return {
102
+ ...identity,
103
+ available: false
104
+ };
105
+ }
106
+ /** Probe the default loopback Ollama and Lemonade servers without credentials. */
107
+ function discoverLocalInferenceModels() {
108
+ return Promise.all(LOCAL_SERVICES.map(probe));
109
+ }
110
+ //#endregion
111
+ export { discoverLocalInferenceModels, resolveInferencePreset };
112
+
113
+ //# sourceMappingURL=byo-inference.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"byo-inference.js","names":[],"sources":["../src/byo-inference.ts"],"sourcesContent":["import { get, type IncomingMessage } from 'node:http';\nimport type { OpenAIOptions } from '@happyvertical/ai';\n\nconst LEMONADE_BASE_URL = 'http://127.0.0.1:13305/v1';\n\nconst PRESETS = {\n openrouter: {\n baseUrl: 'https://openrouter.ai/api/v1',\n requiresApiKey: true,\n },\n lemonade: {\n baseUrl: LEMONADE_BASE_URL,\n requiresApiKey: false,\n },\n 'llama.cpp': {\n baseUrl: 'http://127.0.0.1:9931/v1',\n requiresApiKey: false,\n },\n 'lm-studio': {\n baseUrl: 'http://127.0.0.1:1234/v1',\n requiresApiKey: false,\n },\n} as const;\n\nexport type InferencePresetId = keyof typeof PRESETS;\n\n/**\n * Resolve a named preset with an explicit model. Returns null when a preset,\n * model, or required API key is missing or invalid.\n */\nexport function resolveInferencePreset(\n id: string,\n input: { model: string; apiKey?: string },\n): OpenAIOptions | null {\n if (!Object.hasOwn(PRESETS, id)) return null;\n\n const preset = PRESETS[id as InferencePresetId];\n const model = typeof input.model === 'string' ? input.model.trim() : '';\n const apiKey =\n typeof input.apiKey === 'string' ? input.apiKey.trim() : undefined;\n if (!model || (!apiKey && preset.requiresApiKey)) return null;\n\n return {\n type: 'openai',\n baseUrl: preset.baseUrl,\n defaultModel: model,\n // The SDK requires a nonempty key. A fixed placeholder keeps keyless\n // local servers from receiving credentials inherited from the environment.\n apiKey: apiKey || 'smrt-local',\n };\n}\n\nconst LOCAL_SERVICES = [\n {\n service: 'ollama',\n baseUrl: 'http://127.0.0.1:11434',\n modelPath: '/api/tags',\n listField: 'models',\n idField: 'name',\n },\n {\n service: 'lemonade',\n baseUrl: LEMONADE_BASE_URL,\n modelPath: '/models',\n listField: 'data',\n idField: 'id',\n },\n] as const;\n\ntype LocalService = (typeof LOCAL_SERVICES)[number];\n\nexport type LocalInferenceDiscovery = {\n service: LocalService['service'];\n baseUrl: string;\n} & ({ available: true; models: string[] } | { available: false });\n\nconst PROBE_TIMEOUT_MS = 1_000;\nconst MAX_RESPONSE_BYTES = 1024 * 1024;\n\nfunction readField(value: unknown, field: string): unknown {\n if (\n value !== null &&\n typeof value === 'object' &&\n Object.hasOwn(value, field)\n ) {\n return (value as Record<string, unknown>)[field];\n }\n}\n\nasync function readModels(\n response: IncomingMessage,\n service: LocalService,\n): Promise<string[] | null> {\n if (response.statusCode !== 200) {\n response.destroy();\n return null;\n }\n\n const chunks: Buffer[] = [];\n let size = 0;\n for await (const chunk of response as AsyncIterable<Buffer>) {\n size += chunk.length;\n if (size > MAX_RESPONSE_BYTES) return null;\n chunks.push(chunk);\n }\n\n const payload: unknown = JSON.parse(Buffer.concat(chunks).toString('utf8'));\n const entries = readField(payload, service.listField);\n if (!Array.isArray(entries)) return null;\n\n const models = new Set<string>();\n for (const entry of entries) {\n const id = readField(entry, service.idField);\n if (typeof id !== 'string' || !id.trim()) continue;\n // Ollama's local inventory can include aliases for upstream models.\n if (\n service.service === 'ollama' &&\n (readField(entry, 'remote_model') || readField(entry, 'remote_host'))\n ) {\n continue;\n }\n // Lemonade can also list hosted models and older versions can list models\n // that still need a download. Keep only local models from those responses.\n if (\n service.service === 'lemonade' &&\n (readField(entry, 'recipe') === 'cloud' ||\n readField(entry, 'downloaded') === false)\n ) {\n continue;\n }\n models.add(id);\n }\n return [...models];\n}\n\nasync function probe(service: LocalService): Promise<LocalInferenceDiscovery> {\n const identity = { service: service.service, baseUrl: service.baseUrl };\n try {\n const models = await new Promise<string[] | null>((resolve, reject) => {\n // A one-use agent bypasses any global HTTP proxy. Node HTTP also does\n // not follow redirects. The signal bounds the full request and body read.\n const request = get(\n service.baseUrl + service.modelPath,\n { agent: false, signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) },\n (response) => {\n readModels(response, service).then(resolve, reject);\n },\n );\n request.once('error', reject);\n });\n if (models) return { ...identity, available: true, models };\n } catch {\n // Errors and response bodies may include sensitive local metadata.\n }\n return { ...identity, available: false };\n}\n\n/** Probe the default loopback Ollama and Lemonade servers without credentials. */\nexport function discoverLocalInferenceModels(): Promise<\n LocalInferenceDiscovery[]\n> {\n return Promise.all(LOCAL_SERVICES.map(probe));\n}\n"],"mappings":";;AAGA,IAAM,oBAAoB;AAE1B,IAAM,UAAU;CACd,YAAY;EACV,SAAS;EACT,gBAAgB;CAClB;CACA,UAAU;EACR,SAAS;EACT,gBAAgB;CAClB;CACA,aAAa;EACX,SAAS;EACT,gBAAgB;CAClB;CACA,aAAa;EACX,SAAS;EACT,gBAAgB;CAClB;AACF;;;;;AAQA,SAAgB,uBACd,IACA,OACsB;CACtB,IAAI,CAAC,OAAO,OAAO,SAAS,EAAE,GAAG,OAAO;CAExC,MAAM,SAAS,QAAQ;CACvB,MAAM,QAAQ,OAAO,MAAM,UAAU,WAAW,MAAM,MAAM,KAAK,IAAI;CACrE,MAAM,SACJ,OAAO,MAAM,WAAW,WAAW,MAAM,OAAO,KAAK,IAAI,KAAA;CAC3D,IAAI,CAAC,SAAU,CAAC,UAAU,OAAO,gBAAiB,OAAO;CAEzD,OAAO;EACL,MAAM;EACN,SAAS,OAAO;EAChB,cAAc;EAGd,QAAQ,UAAU;CACpB;AACF;AAEA,IAAM,iBAAiB,CACrB;CACE,SAAS;CACT,SAAS;CACT,WAAW;CACX,WAAW;CACX,SAAS;AACX,GACA;CACE,SAAS;CACT,SAAS;CACT,WAAW;CACX,WAAW;CACX,SAAS;AACX,CACF;AASA,IAAM,mBAAmB;AACzB,IAAM,qBAAqB,OAAO;AAElC,SAAS,UAAU,OAAgB,OAAwB;CACzD,IACE,UAAU,QACV,OAAO,UAAU,YACjB,OAAO,OAAO,OAAO,KAAK,GAE1B,OAAQ,MAAkC;AAE9C;AAEA,eAAe,WACb,UACA,SAC0B;CAC1B,IAAI,SAAS,eAAe,KAAK;EAC/B,SAAS,QAAQ;EACjB,OAAO;CACT;CAEA,MAAM,SAAmB,CAAC;CAC1B,IAAI,OAAO;CACX,WAAW,MAAM,SAAS,UAAmC;EAC3D,QAAQ,MAAM;EACd,IAAI,OAAO,oBAAoB,OAAO;EACtC,OAAO,KAAK,KAAK;CACnB;CAGA,MAAM,UAAU,UADS,KAAK,MAAM,OAAO,OAAO,MAAM,CAAC,CAAC,SAAS,MAAM,CAC/C,GAAS,QAAQ,SAAS;CACpD,IAAI,CAAC,MAAM,QAAQ,OAAO,GAAG,OAAO;CAEpC,MAAM,yBAAS,IAAI,IAAY;CAC/B,KAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,KAAK,UAAU,OAAO,QAAQ,OAAO;EAC3C,IAAI,OAAO,OAAO,YAAY,CAAC,GAAG,KAAK,GAAG;EAE1C,IACE,QAAQ,YAAY,aACnB,UAAU,OAAO,cAAc,KAAK,UAAU,OAAO,aAAa,IAEnE;EAIF,IACE,QAAQ,YAAY,eACnB,UAAU,OAAO,QAAQ,MAAM,WAC9B,UAAU,OAAO,YAAY,MAAM,QAErC;EAEF,OAAO,IAAI,EAAE;CACf;CACA,OAAO,CAAC,GAAG,MAAM;AACnB;AAEA,eAAe,MAAM,SAAyD;CAC5E,MAAM,WAAW;EAAE,SAAS,QAAQ;EAAS,SAAS,QAAQ;CAAQ;CACtE,IAAI;EACF,MAAM,SAAS,MAAM,IAAI,SAA0B,SAAS,WAAW;GAUrE,IANE,QAAQ,UAAU,QAAQ,WAC1B;IAAE,OAAO;IAAO,QAAQ,YAAY,QAAQ,gBAAgB;GAAE,IAC7D,aAAa;IACZ,WAAW,UAAU,OAAO,CAAC,CAAC,KAAK,SAAS,MAAM;GACpD,CAEF,CAAA,CAAQ,KAAK,SAAS,MAAM;EAC9B,CAAC;EACD,IAAI,QAAQ,OAAO;GAAE,GAAG;GAAU,WAAW;GAAM;EAAO;CAC5D,QAAQ,CAER;CACA,OAAO;EAAE,GAAG;EAAU,WAAW;CAAM;AACzC;;AAGA,SAAgB,+BAEd;CACA,OAAO,QAAQ,IAAI,eAAe,IAAI,KAAK,CAAC;AAC9C"}
@@ -3,7 +3,7 @@ var staticManifest = {
3
3
  "version": "1.0.0",
4
4
  "timestamp": 0,
5
5
  "packageName": "@happyvertical/smrt-core",
6
- "packageVersion": "0.60.8",
6
+ "packageVersion": "0.60.9",
7
7
  "objects": {
8
8
  "@happyvertical/smrt-core:SmrtClass": {
9
9
  "name": "smrtclass",