@happyvertical/smrt-inference 0.0.0-stage → 0.60.16
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/AGENTS.md +206 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/README.md +73 -2
- package/agents/fusions.md +105 -0
- package/agents/hosted.md +161 -0
- package/agents/resolver.md +65 -0
- package/agents/settings.md +107 -0
- package/dist/__smrt-register__.d.ts +2 -0
- package/dist/__smrt-register__.d.ts.map +1 -0
- package/dist/chunks/InferenceOverrideCollection-Dat4TrK5.js +423 -0
- package/dist/chunks/InferenceOverrideCollection-Dat4TrK5.js.map +1 -0
- package/dist/chunks/provider-config-BSpIQRVM.js +1873 -0
- package/dist/chunks/provider-config-BSpIQRVM.js.map +1 -0
- package/dist/classify.d.ts +32 -0
- package/dist/classify.d.ts.map +1 -0
- package/dist/config.d.ts +89 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/errors.d.ts +51 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/executor.d.ts +45 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/fusion-store.d.ts +16 -0
- package/dist/fusion-store.d.ts.map +1 -0
- package/dist/fusion.d.ts +155 -0
- package/dist/fusion.d.ts.map +1 -0
- package/dist/gate.d.ts +143 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +94 -0
- package/dist/gate.js.map +1 -0
- package/dist/hosted.d.ts +461 -0
- package/dist/hosted.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.json +453 -0
- package/dist/models/InferenceOverride.d.ts +38 -0
- package/dist/models/InferenceOverride.d.ts.map +1 -0
- package/dist/models/InferenceOverrideCollection.d.ts +35 -0
- package/dist/models/InferenceOverrideCollection.d.ts.map +1 -0
- package/dist/models.d.ts +11 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +2 -0
- package/dist/node.d.ts +110 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +1414 -0
- package/dist/node.js.map +1 -0
- package/dist/provider-config.d.ts +113 -0
- package/dist/provider-config.d.ts.map +1 -0
- package/dist/register.d.ts +30 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/resolver.d.ts +166 -0
- package/dist/resolver.d.ts.map +1 -0
- package/dist/resolver.js +291 -0
- package/dist/resolver.js.map +1 -0
- package/dist/server.d.ts +25 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +1138 -0
- package/dist/server.js.map +1 -0
- package/dist/settings/cache.d.ts +5 -0
- package/dist/settings/cache.d.ts.map +1 -0
- package/dist/settings/keys.d.ts +70 -0
- package/dist/settings/keys.d.ts.map +1 -0
- package/dist/settings/layers.d.ts +68 -0
- package/dist/settings/layers.d.ts.map +1 -0
- package/dist/settings/override-service.d.ts +119 -0
- package/dist/settings/override-service.d.ts.map +1 -0
- package/dist/settings/preferences.d.ts +137 -0
- package/dist/settings/preferences.d.ts.map +1 -0
- package/dist/settings/resolver.d.ts +41 -0
- package/dist/settings/resolver.d.ts.map +1 -0
- package/dist/settings/settings-service.d.ts +355 -0
- package/dist/settings/settings-service.d.ts.map +1 -0
- package/dist/settings/targets.d.ts +112 -0
- package/dist/settings/targets.d.ts.map +1 -0
- package/dist/settings/transcripts.d.ts +33 -0
- package/dist/settings/transcripts.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +341 -0
- package/dist/smrt-knowledge.okf.md +80 -0
- package/dist/svelte/components/InferenceAliasEditor.svelte +210 -0
- package/dist/svelte/components/InferenceAliasEditor.svelte.d.ts +13 -0
- package/dist/svelte/components/InferenceAliasEditor.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InferenceDefaultView.svelte +330 -0
- package/dist/svelte/components/InferenceDefaultView.svelte.d.ts +15 -0
- package/dist/svelte/components/InferenceDefaultView.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InferenceFusionList.svelte +141 -0
- package/dist/svelte/components/InferenceFusionList.svelte.d.ts +16 -0
- package/dist/svelte/components/InferenceFusionList.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InferenceSettingsPanel.svelte +392 -0
- package/dist/svelte/components/InferenceSettingsPanel.svelte.d.ts +84 -0
- package/dist/svelte/components/InferenceSettingsPanel.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InferenceTierRow.svelte +199 -0
- package/dist/svelte/components/InferenceTierRow.svelte.d.ts +14 -0
- package/dist/svelte/components/InferenceTierRow.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InferenceTranscriptSettings.svelte +165 -0
- package/dist/svelte/components/InferenceTranscriptSettings.svelte.d.ts +18 -0
- package/dist/svelte/components/InferenceTranscriptSettings.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InferenceUsageTable.svelte +177 -0
- package/dist/svelte/components/InferenceUsageTable.svelte.d.ts +21 -0
- package/dist/svelte/components/InferenceUsageTable.svelte.d.ts.map +1 -0
- package/dist/svelte/index.d.ts +17 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +14 -0
- package/dist/svelte/types.d.ts +262 -0
- package/dist/svelte/types.d.ts.map +1 -0
- package/dist/svelte/types.js +124 -0
- package/dist/types.d.ts +47 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -0
- package/package.json +105 -3
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The vocabulary of stored inference bindings (#4754): binding ids, the
|
|
3
|
+
* target shapes a settings write may store, the allow-list references a
|
|
4
|
+
* tenant write is checked against, and key-free labels.
|
|
5
|
+
*
|
|
6
|
+
* Pure and browser-safe: no `node:`, no SDK, no smrt-config.
|
|
7
|
+
*/
|
|
8
|
+
import { type InferenceAlias, type InferenceTarget, type InferenceTier } from '@happyvertical/smrt-core';
|
|
9
|
+
/** How a user or app gets inference. Stored as a user choice, never a binding. */
|
|
10
|
+
export declare const INFERENCE_MODES: readonly ["byo", "local", "hosted", "off"];
|
|
11
|
+
export type InferenceMode = (typeof INFERENCE_MODES)[number];
|
|
12
|
+
/** Binding id of `auto`'s default tier. */
|
|
13
|
+
export declare const DEFAULT_TIER_BINDING = "default-tier";
|
|
14
|
+
/**
|
|
15
|
+
* Binding id of the app's transcript setting (#4781). App scope only:
|
|
16
|
+
* tenants cannot override transcripts (owner decision 2026-11-08).
|
|
17
|
+
*/
|
|
18
|
+
export declare const TRANSCRIPTS_BINDING = "transcripts";
|
|
19
|
+
/** A binding id stored in `_smrt_inference_overrides.binding`. */
|
|
20
|
+
export type InferenceBindingId = `tier:${InferenceTier}` | typeof DEFAULT_TIER_BINDING | typeof TRANSCRIPTS_BINDING | `alias:${string}` | `fusion:${string}`;
|
|
21
|
+
/** `inference.aliases` / fusion names: what a binding id may carry after its prefix. */
|
|
22
|
+
export declare const INFERENCE_BINDING_NAME_PATTERN: RegExp;
|
|
23
|
+
export type InferenceBindingKind = 'tier' | 'default-tier' | 'transcripts' | 'alias' | 'fusion';
|
|
24
|
+
/** A parsed binding id. */
|
|
25
|
+
export interface ParsedInferenceBinding {
|
|
26
|
+
kind: InferenceBindingKind;
|
|
27
|
+
/** The tier, alias or fusion name (absent for `default-tier`). */
|
|
28
|
+
name?: string;
|
|
29
|
+
}
|
|
30
|
+
/** Thrown for a malformed binding id or an invalid stored value. Nothing is written. */
|
|
31
|
+
export declare class InferenceBindingValueError extends Error {
|
|
32
|
+
readonly binding: string;
|
|
33
|
+
readonly status = 400;
|
|
34
|
+
constructor(binding: string, reason: string);
|
|
35
|
+
}
|
|
36
|
+
/** Parse a binding id, or `null` when it is not one this package stores. */
|
|
37
|
+
export declare function parseInferenceBinding(binding: unknown): ParsedInferenceBinding | null;
|
|
38
|
+
/** The binding id of one tier. */
|
|
39
|
+
export declare function tierBinding(tier: InferenceTier): `tier:${InferenceTier}`;
|
|
40
|
+
/**
|
|
41
|
+
* A deep copy of `value` with every credential-shaped member removed. Used
|
|
42
|
+
* on every config-backed value a settings screen receives: config tiers and
|
|
43
|
+
* alias candidates may legitimately carry a literal `apiKey` (resolvers and
|
|
44
|
+
* endpoint-bound config), and none of it may reach a browser.
|
|
45
|
+
*/
|
|
46
|
+
export declare function stripKeyMembers<T>(value: T): T;
|
|
47
|
+
/** Dotted path of the first key-shaped member anywhere in `value`, or `null`. */
|
|
48
|
+
export declare function findKeyMember(value: unknown, path?: string): string | null;
|
|
49
|
+
/**
|
|
50
|
+
* The tier target a settings write may store. A tier binds exactly one of:
|
|
51
|
+
*
|
|
52
|
+
* - a model: `{ provider?, model }` (no `baseUrl`: an endpoint needs its own
|
|
53
|
+
* key, and keys are never stored here; bind an alias whose candidate names
|
|
54
|
+
* `baseUrl` + `apiKeyEnv` or a preset instead);
|
|
55
|
+
* - an alias: `{ alias }`;
|
|
56
|
+
* - a fusion (#4753): `{ kind: 'fusion', fusion }`.
|
|
57
|
+
*
|
|
58
|
+
* `null` means "same as default" (inherit the lower layer).
|
|
59
|
+
*/
|
|
60
|
+
export declare function normalizeTierTarget(binding: string, value: unknown): InferenceTarget | null;
|
|
61
|
+
/**
|
|
62
|
+
* An alias definition a settings write may store: `{ candidates, description? }`,
|
|
63
|
+
* every candidate exactly one of provider/model, preset/model or alias, and
|
|
64
|
+
* never a literal key (`apiKeyEnv` names a variable and needs `baseUrl`).
|
|
65
|
+
* Structural checks only; cross-references are checked against the layered
|
|
66
|
+
* config by the settings service.
|
|
67
|
+
*/
|
|
68
|
+
export declare function normalizeAliasDefinition(binding: string, value: unknown): InferenceAlias | null;
|
|
69
|
+
/**
|
|
70
|
+
* A fusion config entry (#4753) a settings write may store: a JSON object
|
|
71
|
+
* with no credential members. Its meaning belongs to the fusion interpreter;
|
|
72
|
+
* this package only stores it (app scope only).
|
|
73
|
+
*/
|
|
74
|
+
export declare function normalizeFusionDefinition(binding: string, value: unknown): Record<string, unknown> | null;
|
|
75
|
+
/** `default-tier` stores `{ tier }`. */
|
|
76
|
+
export declare function normalizeDefaultTier(binding: string, value: unknown): InferenceTier | null;
|
|
77
|
+
/** The stored transcript setting (#4781): core's app settings layer. */
|
|
78
|
+
export interface InferenceTranscriptSettingsValue {
|
|
79
|
+
enabled?: boolean;
|
|
80
|
+
/** Success capture rate, 0..1. */
|
|
81
|
+
sample?: number;
|
|
82
|
+
/** Per-row byte cap. */
|
|
83
|
+
maxBytes?: number;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* `transcripts` stores `{ enabled?, sample?, maxBytes? }`, validated by
|
|
87
|
+
* core's `assertInferenceTranscriptsConfig()`. Per-prompt sampling stays in
|
|
88
|
+
* config; any other member is refused.
|
|
89
|
+
*/
|
|
90
|
+
export declare function normalizeTranscriptSettings(binding: string, value: unknown): InferenceTranscriptSettingsValue | null;
|
|
91
|
+
/**
|
|
92
|
+
* The allow-list reference of a tier target: `model:<provider>/<model>`
|
|
93
|
+
* (`model:default/<model>` without a provider), `alias:<name>` or
|
|
94
|
+
* `fusion:<name>`; for the default tier, `tier:<name>`. Key-free.
|
|
95
|
+
*/
|
|
96
|
+
export declare function inferenceTargetRef(target: InferenceTarget): string;
|
|
97
|
+
/**
|
|
98
|
+
* Whether `ref` matches one allow-list pattern. A pattern is an exact
|
|
99
|
+
* reference, a prefix ending in `*` (`model:openai/*`, `alias:*`), or `*`.
|
|
100
|
+
*/
|
|
101
|
+
export declare function matchesAllowPattern(ref: string, pattern: string): boolean;
|
|
102
|
+
/**
|
|
103
|
+
* The tenant allow-list check: a tenant may bind only a reference the app
|
|
104
|
+
* row's list matches. No list (or an empty one) allows nothing, so a tenant
|
|
105
|
+
* cannot bind a tier the operator has not opened.
|
|
106
|
+
*/
|
|
107
|
+
export declare function isAllowedByList(ref: string, allowList: readonly string[] | null | undefined): boolean;
|
|
108
|
+
/** Validate and normalize an allow-list (patterns are short, key-free references). */
|
|
109
|
+
export declare function normalizeAllowList(binding: string, value: unknown): string[] | null;
|
|
110
|
+
/** Key-free description of a tier target for screens and reports. */
|
|
111
|
+
export declare function describeInferenceTarget(target: InferenceTarget | null | undefined): string;
|
|
112
|
+
//# sourceMappingURL=targets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"targets.d.ts","sourceRoot":"","sources":["../../src/settings/targets.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAGL,KAAK,cAAc,EAEnB,KAAK,eAAe,EACpB,KAAK,aAAa,EAGnB,MAAM,0BAA0B,CAAC;AAElC,kFAAkF;AAClF,eAAO,MAAM,eAAe,4CAA6C,CAAC;AAC1E,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAE7D,2CAA2C;AAC3C,eAAO,MAAM,oBAAoB,iBAAiB,CAAC;AAEnD;;;GAGG;AACH,eAAO,MAAM,mBAAmB,gBAAgB,CAAC;AAEjD,kEAAkE;AAClE,MAAM,MAAM,kBAAkB,GAC1B,QAAQ,aAAa,EAAE,GACvB,OAAO,oBAAoB,GAC3B,OAAO,mBAAmB,GAC1B,SAAS,MAAM,EAAE,GACjB,UAAU,MAAM,EAAE,CAAC;AAEvB,wFAAwF;AACxF,eAAO,MAAM,8BAA8B,QAAiC,CAAC;AAE7E,MAAM,MAAM,oBAAoB,GAC5B,MAAM,GACN,cAAc,GACd,aAAa,GACb,OAAO,GACP,QAAQ,CAAC;AAEb,2BAA2B;AAC3B,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,oBAAoB,CAAC;IAC3B,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wFAAwF;AACxF,qBAAa,0BAA2B,SAAQ,KAAK;IAGjD,QAAQ,CAAC,OAAO,EAAE,MAAM;IAF1B,QAAQ,CAAC,MAAM,OAAO;gBAEX,OAAO,EAAE,MAAM,EACxB,MAAM,EAAE,MAAM;CAKjB;AAED,4EAA4E;AAC5E,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,OAAO,GACf,sBAAsB,GAAG,IAAI,CAY/B;AAED,kCAAkC;AAClC,wBAAgB,WAAW,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,aAAa,EAAE,CAExE;AA6BD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAS9C;AAED,iFAAiF;AACjF,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,SAAK,GAAG,MAAM,GAAG,IAAI,CAgBtE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,OAAO,GACb,eAAe,GAAG,IAAI,CAsDxB;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,OAAO,GACb,cAAc,GAAG,IAAI,CAgEvB;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,OAAO,GACb,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAahC;AAED,wCAAwC;AACxC,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,OAAO,GACb,aAAa,GAAG,IAAI,CAUtB;AAED,wEAAwE;AACxE,MAAM,WAAW,gCAAgC;IAC/C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kCAAkC;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wBAAwB;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAID;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,OAAO,GACb,gCAAgC,GAAG,IAAI,CA8BzC;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAOlE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAIzE;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,GAAG,SAAS,GAC9C,OAAO,CAET;AAED,sFAAsF;AACtF,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,OAAO,GACb,MAAM,EAAE,GAAG,IAAI,CAsBjB;AAED,qEAAqE;AACrE,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,eAAe,GAAG,IAAI,GAAG,SAAS,GACzC,MAAM,CAYR"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The app's transcript setting (#4781): stored as the app-scope
|
|
3
|
+
* `transcripts` row of `_smrt_inference_overrides` and applied as core's
|
|
4
|
+
* transcript app settings layer (`setAiTranscriptAppSettings()`), between
|
|
5
|
+
* `config()` and `SMRT_INFERENCE_TRANSCRIPTS`. Tenants cannot override it.
|
|
6
|
+
*
|
|
7
|
+
* A write through `InferenceSettingsService.setTranscriptSettings()` applies
|
|
8
|
+
* in its own process at once; the override resolver re-applies the row
|
|
9
|
+
* whenever it reads rows (within its TTL), and processes without it call
|
|
10
|
+
* {@link loadInferenceTranscriptSettings} at startup.
|
|
11
|
+
*/
|
|
12
|
+
import { type SmrtClassOptions } from '@happyvertical/smrt-core';
|
|
13
|
+
import { type InferenceOverride } from '../models/InferenceOverride.js';
|
|
14
|
+
import { InferenceOverrideCollection } from '../models/InferenceOverrideCollection.js';
|
|
15
|
+
import { type InferenceTranscriptSettingsValue } from './targets.js';
|
|
16
|
+
/** The stored value of the app's `transcripts` row, or `null`. */
|
|
17
|
+
export declare function storedTranscriptSettings(appRows: ReadonlyMap<string, InferenceOverride>): InferenceTranscriptSettingsValue | null;
|
|
18
|
+
/**
|
|
19
|
+
* Install the stored app row as core's transcript app settings layer
|
|
20
|
+
* (`null` clears it, so a removed row reverts to config). Returns it.
|
|
21
|
+
*/
|
|
22
|
+
export declare function applyStoredTranscriptSettings(appRows: ReadonlyMap<string, InferenceOverride>): InferenceTranscriptSettingsValue | null;
|
|
23
|
+
/**
|
|
24
|
+
* Read the app's stored transcript setting and install it as core's app
|
|
25
|
+
* settings layer. Call once at startup in a process that makes AI calls
|
|
26
|
+
* without the override resolver (a worker); the resolver keeps it current
|
|
27
|
+
* otherwise.
|
|
28
|
+
*/
|
|
29
|
+
export declare function loadInferenceTranscriptSettings(options?: {
|
|
30
|
+
db?: SmrtClassOptions;
|
|
31
|
+
overrides?: InferenceOverrideCollection;
|
|
32
|
+
}): Promise<InferenceTranscriptSettingsValue | null>;
|
|
33
|
+
//# sourceMappingURL=transcripts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transcripts.d.ts","sourceRoot":"","sources":["../../src/settings/transcripts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EACL,KAAK,gBAAgB,EAGtB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,2BAA2B,EAAE,MAAM,0CAA0C,CAAC;AACvF,OAAO,EACL,KAAK,gCAAgC,EAEtC,MAAM,cAAc,CAAC;AAEtB,kEAAkE;AAClE,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,iBAAiB,CAAC,GAC9C,gCAAgC,GAAG,IAAI,CASzC;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,iBAAiB,CAAC,GAC9C,gCAAgC,GAAG,IAAI,CAIzC;AAED;;;;;GAKG;AACH,wBAAsB,+BAA+B,CACnD,OAAO,GAAE;IACP,EAAE,CAAC,EAAE,gBAAgB,CAAC;IACtB,SAAS,CAAC,EAAE,2BAA2B,CAAC;CACpC,GACL,OAAO,CAAC,gCAAgC,GAAG,IAAI,CAAC,CAWlD"}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"sensitiveFieldsExcluded": true,
|
|
4
|
+
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
|
+
"packageName": "@happyvertical/smrt-inference",
|
|
6
|
+
"packageVersion": "0.60.16",
|
|
7
|
+
"sourceManifestPath": "dist/manifest.json",
|
|
8
|
+
"agentDocPath": "AGENTS.md",
|
|
9
|
+
"sourceHashes": {
|
|
10
|
+
"manifest": "54e44c7fcf61ccd93d4425a74df15c3be1e3b6df8463cfe3941669f5c0cfcbca",
|
|
11
|
+
"packageJson": "a3257e0445774817a9c973105afb20f77e561125e89770f1bf46ab107deeb06e",
|
|
12
|
+
"agents": "7ee5ec0379fbee95035e9809a03a625a8c7addd992629ad5dff83693eb7fb9a7",
|
|
13
|
+
"moduleDoc:agents/resolver.md": "71de7295b6988b27b3b27e2ae82fd71e525ef53613c7714a1deea6c45ba92ef6",
|
|
14
|
+
"moduleDoc:agents/hosted.md": "563141ef3ff09c453de8cfe99db3500da77fa28efe6bc3990e3f84a6cf32653d",
|
|
15
|
+
"moduleDoc:agents/fusions.md": "e1dc3f94fe20b1989c8d6e07dbe28e5bd72ae1d327bc3edbc9b8d4365d6dc231",
|
|
16
|
+
"moduleDoc:agents/settings.md": "1ba893ced5676ea41cd10f4454991db7de8c949d93d030ea1d1e78024e9f0cce"
|
|
17
|
+
},
|
|
18
|
+
"exports": [
|
|
19
|
+
".",
|
|
20
|
+
"./gate",
|
|
21
|
+
"./manifest",
|
|
22
|
+
"./manifest.json",
|
|
23
|
+
"./models",
|
|
24
|
+
"./node",
|
|
25
|
+
"./resolver",
|
|
26
|
+
"./server",
|
|
27
|
+
"./svelte"
|
|
28
|
+
],
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@happyvertical/ai": "catalog:",
|
|
31
|
+
"@happyvertical/smrt-config": "workspace:*",
|
|
32
|
+
"@happyvertical/smrt-core": "workspace:*",
|
|
33
|
+
"@happyvertical/smrt-prompts": "workspace:*",
|
|
34
|
+
"@happyvertical/smrt-secrets": "workspace:*",
|
|
35
|
+
"@happyvertical/smrt-tenancy": "workspace:*",
|
|
36
|
+
"@happyvertical/smrt-users": "workspace:*",
|
|
37
|
+
"@happyvertical/sql": "catalog:",
|
|
38
|
+
"@happyvertical/smrt-vitest": "workspace:*",
|
|
39
|
+
"@sveltejs/package": "^2.5.8",
|
|
40
|
+
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
41
|
+
"@types/node": "24.13.6",
|
|
42
|
+
"jsdom": "^29.1.1",
|
|
43
|
+
"svelte": "^5.57.0",
|
|
44
|
+
"svelte-check": "^4.7.6",
|
|
45
|
+
"typescript": "6.0.3",
|
|
46
|
+
"vite": "8.1.6",
|
|
47
|
+
"vitest": "4.1.10"
|
|
48
|
+
},
|
|
49
|
+
"smrtDependencies": [
|
|
50
|
+
"@happyvertical/smrt-config",
|
|
51
|
+
"@happyvertical/smrt-core",
|
|
52
|
+
"@happyvertical/smrt-prompts",
|
|
53
|
+
"@happyvertical/smrt-secrets",
|
|
54
|
+
"@happyvertical/smrt-tenancy",
|
|
55
|
+
"@happyvertical/smrt-users",
|
|
56
|
+
"@happyvertical/smrt-vitest"
|
|
57
|
+
],
|
|
58
|
+
"sdkDependencies": [
|
|
59
|
+
"@happyvertical/ai",
|
|
60
|
+
"@happyvertical/sql"
|
|
61
|
+
],
|
|
62
|
+
"tags": [
|
|
63
|
+
"ai",
|
|
64
|
+
"cross-package",
|
|
65
|
+
"fallback",
|
|
66
|
+
"inference",
|
|
67
|
+
"routing",
|
|
68
|
+
"settings",
|
|
69
|
+
"smrt",
|
|
70
|
+
"svelte"
|
|
71
|
+
],
|
|
72
|
+
"risks": [
|
|
73
|
+
"sensitive-fields-excluded"
|
|
74
|
+
],
|
|
75
|
+
"objects": [
|
|
76
|
+
{
|
|
77
|
+
"name": "InferenceOverride",
|
|
78
|
+
"qualifiedName": "@happyvertical/smrt-inference:InferenceOverride",
|
|
79
|
+
"collection": "inferenceoverrides",
|
|
80
|
+
"tableName": "_smrt_inference_overrides",
|
|
81
|
+
"packageName": "@happyvertical/smrt-inference",
|
|
82
|
+
"extends": "SmrtObject",
|
|
83
|
+
"fields": [
|
|
84
|
+
{
|
|
85
|
+
"name": "binding",
|
|
86
|
+
"type": "text",
|
|
87
|
+
"required": true,
|
|
88
|
+
"columnType": "TEXT",
|
|
89
|
+
"default": ""
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"name": "tenantId",
|
|
93
|
+
"type": "text",
|
|
94
|
+
"required": false,
|
|
95
|
+
"columnType": "UUID"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"name": "target",
|
|
99
|
+
"type": "text",
|
|
100
|
+
"required": false,
|
|
101
|
+
"columnType": "TEXT"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"name": "tenantAllowList",
|
|
105
|
+
"type": "text",
|
|
106
|
+
"required": false,
|
|
107
|
+
"columnType": "TEXT"
|
|
108
|
+
}
|
|
109
|
+
],
|
|
110
|
+
"relationships": [],
|
|
111
|
+
"methods": [
|
|
112
|
+
"delete",
|
|
113
|
+
"getTenantAllowList",
|
|
114
|
+
"getValue",
|
|
115
|
+
"save"
|
|
116
|
+
],
|
|
117
|
+
"methodSignatures": [
|
|
118
|
+
{
|
|
119
|
+
"name": "delete",
|
|
120
|
+
"async": true,
|
|
121
|
+
"returns": "Promise<void>"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"name": "getTenantAllowList",
|
|
125
|
+
"returns": "string[] | null"
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"name": "getValue",
|
|
129
|
+
"returns": "unknown"
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"name": "save",
|
|
133
|
+
"async": true,
|
|
134
|
+
"params": [
|
|
135
|
+
"options?: SmrtSaveOptions"
|
|
136
|
+
],
|
|
137
|
+
"returns": "Promise<this>"
|
|
138
|
+
}
|
|
139
|
+
],
|
|
140
|
+
"conflictColumns": [
|
|
141
|
+
"binding",
|
|
142
|
+
"context"
|
|
143
|
+
],
|
|
144
|
+
"surfaces": [],
|
|
145
|
+
"withheldSurfaces": [
|
|
146
|
+
{
|
|
147
|
+
"kind": "api",
|
|
148
|
+
"operation": "delete",
|
|
149
|
+
"code": "api-disabled",
|
|
150
|
+
"reason": "api is disabled",
|
|
151
|
+
"objectName": "@happyvertical/smrt-inference:InferenceOverride"
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"kind": "api",
|
|
155
|
+
"operation": "getTenantAllowList",
|
|
156
|
+
"code": "api-disabled",
|
|
157
|
+
"reason": "api is disabled",
|
|
158
|
+
"objectName": "@happyvertical/smrt-inference:InferenceOverride"
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"kind": "api",
|
|
162
|
+
"operation": "getValue",
|
|
163
|
+
"code": "api-disabled",
|
|
164
|
+
"reason": "api is disabled",
|
|
165
|
+
"objectName": "@happyvertical/smrt-inference:InferenceOverride"
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"kind": "api",
|
|
169
|
+
"operation": "save",
|
|
170
|
+
"code": "api-disabled",
|
|
171
|
+
"reason": "api is disabled",
|
|
172
|
+
"objectName": "@happyvertical/smrt-inference:InferenceOverride"
|
|
173
|
+
}
|
|
174
|
+
],
|
|
175
|
+
"relationshipFeatures": [
|
|
176
|
+
"uuidColumns"
|
|
177
|
+
],
|
|
178
|
+
"tags": [],
|
|
179
|
+
"risks": []
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"name": "InferenceOverrideCollection",
|
|
183
|
+
"qualifiedName": "@happyvertical/smrt-inference:InferenceOverrideCollection",
|
|
184
|
+
"collection": "inferenceoverrides",
|
|
185
|
+
"description": "Reads and unauthorized writes of _smrt_inference_overrides.",
|
|
186
|
+
"tableName": "_smrt_inference_overrides",
|
|
187
|
+
"packageName": "@happyvertical/smrt-inference",
|
|
188
|
+
"extends": "SmrtCollection",
|
|
189
|
+
"fields": [],
|
|
190
|
+
"relationships": [],
|
|
191
|
+
"methods": [
|
|
192
|
+
"findByScope",
|
|
193
|
+
"getScopeMap",
|
|
194
|
+
"removeOverride",
|
|
195
|
+
"setOverride"
|
|
196
|
+
],
|
|
197
|
+
"methodSignatures": [
|
|
198
|
+
{
|
|
199
|
+
"name": "findByScope",
|
|
200
|
+
"async": true,
|
|
201
|
+
"params": [
|
|
202
|
+
"binding: string",
|
|
203
|
+
"scopeType: InferenceOverrideScopeType",
|
|
204
|
+
"scopeId: string"
|
|
205
|
+
],
|
|
206
|
+
"returns": "Promise<InferenceOverride | null>"
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"name": "getScopeMap",
|
|
210
|
+
"async": true,
|
|
211
|
+
"params": [
|
|
212
|
+
"scopeType: InferenceOverrideScopeType",
|
|
213
|
+
"scopeId: string"
|
|
214
|
+
],
|
|
215
|
+
"returns": "Promise<Map<string, InferenceOverride>>"
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"name": "removeOverride",
|
|
219
|
+
"async": true,
|
|
220
|
+
"params": [
|
|
221
|
+
"binding: string",
|
|
222
|
+
"scopeType: InferenceOverrideScopeType",
|
|
223
|
+
"scopeId: string"
|
|
224
|
+
],
|
|
225
|
+
"returns": "Promise<boolean>"
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"name": "setOverride",
|
|
229
|
+
"async": true,
|
|
230
|
+
"params": [
|
|
231
|
+
"binding: string",
|
|
232
|
+
"scopeType: InferenceOverrideScopeType",
|
|
233
|
+
"scopeId: string",
|
|
234
|
+
"change: object"
|
|
235
|
+
],
|
|
236
|
+
"returns": "Promise<InferenceOverride | null>"
|
|
237
|
+
}
|
|
238
|
+
],
|
|
239
|
+
"surfaces": [],
|
|
240
|
+
"withheldSurfaces": [
|
|
241
|
+
{
|
|
242
|
+
"kind": "api",
|
|
243
|
+
"operation": "findByScope",
|
|
244
|
+
"code": "api-disabled",
|
|
245
|
+
"reason": "api is disabled",
|
|
246
|
+
"objectName": "@happyvertical/smrt-inference:InferenceOverrideCollection"
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"kind": "api",
|
|
250
|
+
"operation": "getScopeMap",
|
|
251
|
+
"code": "api-disabled",
|
|
252
|
+
"reason": "api is disabled",
|
|
253
|
+
"objectName": "@happyvertical/smrt-inference:InferenceOverrideCollection"
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
"kind": "api",
|
|
257
|
+
"operation": "removeOverride",
|
|
258
|
+
"code": "api-disabled",
|
|
259
|
+
"reason": "api is disabled",
|
|
260
|
+
"objectName": "@happyvertical/smrt-inference:InferenceOverrideCollection"
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"kind": "api",
|
|
264
|
+
"operation": "setOverride",
|
|
265
|
+
"code": "api-disabled",
|
|
266
|
+
"reason": "api is disabled",
|
|
267
|
+
"objectName": "@happyvertical/smrt-inference:InferenceOverrideCollection"
|
|
268
|
+
}
|
|
269
|
+
],
|
|
270
|
+
"relationshipFeatures": [
|
|
271
|
+
"uuidColumns"
|
|
272
|
+
],
|
|
273
|
+
"tags": [],
|
|
274
|
+
"risks": []
|
|
275
|
+
}
|
|
276
|
+
],
|
|
277
|
+
"surfaces": [],
|
|
278
|
+
"prompts": [
|
|
279
|
+
{
|
|
280
|
+
"filePath": "src/__tests__/fusion-integration.test.ts"
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
"filePath": "src/__tests__/issue-4800-panel-fusions.test.ts"
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
"filePath": "src/config.ts"
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
"filePath": "src/fusion.ts"
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
"filePath": "src/svelte/components/InferenceSettingsPanel.test.ts"
|
|
293
|
+
}
|
|
294
|
+
],
|
|
295
|
+
"relationshipsV2": {
|
|
296
|
+
"foreignKeyFields": 0,
|
|
297
|
+
"crossPackageRefFields": 0,
|
|
298
|
+
"junctionCollections": 0,
|
|
299
|
+
"hierarchicalObjects": 0,
|
|
300
|
+
"polymorphicAssociations": 0,
|
|
301
|
+
"uuidColumns": 4
|
|
302
|
+
},
|
|
303
|
+
"agentDoc": "# smrt-inference\n\nBinding behavior for core's inference tiers (epic #4749). Core owns the names\nand the seam ([core inference.md](../core/agents/inference.md)); this package\nowns alias chains, classified fallback, ladder degradation and\n`smrt doctor --ai` (#4752), the fusion interpreter (#4753), the settings\npanel with layered binding storage (#4754), per-user and per-tenant\nbindings (#4756), candidate capability checks (#4758), and hosted\nprovisioning and the spend gate (#4759, #4760). App-author guide:\n[docs/content/inference.md](../../docs/content/inference.md).\n\n## Entries\n\n- Root (browser-safe): importing it calls `registerInference()`, which sets\n the executor (`alias`, `candidates`, `fusion`) and a validating resolver\n (`setInferenceResolver()`). The resolver binds nothing: it validates chain\n config, then defers to a resolver registered earlier (#4756 per-user\n bindings, `./resolver`) or to core's built-in config resolution. No aliases and no chain\n targets means no change in behavior. `browser-safe.test.ts` holds the graph\n free of `node:`, the AI SDK and smrt-config by walking its import graph;\n the bundle-gate browser fixture also builds it (the package has a model).\n- `./node`: `checkInferenceBindings()`, `probeInferenceCandidate()`,\n `inferenceProbeUrl()`, `formatInferenceDoctorReport()`; the CLI's\n `doctor --ai` and #4754's test connection call these. Also hosted\n provisioning (`src/hosted.ts`, #4759).\n- `./resolver` (Node-only, #4756): `createUserInferenceResolver()` and\n `registerUserInferenceResolver()` over smrt-secrets / tenancy / users. The\n root re-exports only the browser-safe stored-config shape\n (`src/provider-config.ts`): parse, redact, tier → target map.\n- `./models` (browser-safe, held by `browser-safe.test.ts` and the\n bundle-gate browser fixture): the `_smrt_inference_overrides` model;\n generated consumer registration imports it from here.\n- `./server` (Node-only): settings service, override service, override\n resolver, key rule, preference kind (#4754). `./svelte`: the panel.\n- `./gate` (Node-only, #4760): the hosted spend gate and kill switch\n (below). `browser-safe.test.ts` keeps it off the root graph.\n\n## Per-user and per-tenant bindings (#4756)\n\n`createUserInferenceResolver()` resolves per tier: the user's stored config,\nthe tenant's, `fallback`, the app `inference` block, then the configured\nclient; every resolution carries `target.source` (`byo | local | hosted |\napp`, the `source` usage tag). Keys live only in the smrt-secrets type\n`inference.provider-config` (`context = tenantId`), every stored binding is\nself-contained (own `baseUrl` + `apiKey`, or a preset), the user layer\napplies only to a principal of that tenant, and agents pass `userId: null`\n(tenant layer only). The stored config also carries `mode` (#4757).\nDetails: [agents/resolver.md](agents/resolver.md).\n\n## Resolver composition\n\nOne resolver is registered, outermost first: the #4752 validating wrapper,\nthe #4756 user resolver, its `fallback`, then core's config resolution.\nHosts using the #4754 override table pass\n`createInferenceOverrideResolver({ db })` as that `fallback`:\n`registerUserInferenceResolver(createUserInferenceResolver({ db, fallback:\ncreateInferenceOverrideResolver({ db }) }))`. Order per tier: user stored\nbinding, tenant stored binding, allowed tenant override row, app override\nrow, `inference` config, configured client. Keys are stored once, in\n`inference.provider-config`: the host turns #4754's origin-bound\n`bindInferenceKey()` record into a self-contained binding with\n`inferenceProviderConfigFromKey(record, { model })` and writes it through\nthe user resolver's `write(level, config)`; never a second secret type.\n\n## Hosted inference (#4759, #4760)\n\n`./node` provisions per-user gateway keys through `ai.admin` of the\n`bifrost` / `litellm` provider (`SMRT_INFERENCE_HOSTED_*`, read once), storing\nthe key only in the user's `inference.provider-config` (`source: 'hosted'`).\nPlan changes update the key budget, revoke ends local use and then deletes the\nkey (failures are owed and retried by reconcile), and the tenant project is\nlooked up before one is created (`@happyvertical/ai` >= 0.111.7). `./gate`'s\n`registerHostedInferenceGate({ killSwitch, spend })` is core's\n`InferenceGate`: kill switch `inference.hosted.enabled` (a global DISABLE\nalways wins), tenant, entitlement, then plan spend caps; a block throws\n`InferenceUnavailableError` (`entitlement`) and writes a `blocked` usage row.\nDetails: [agents/hosted.md](agents/hosted.md).\n\n## Config\n\n```ts\nconfig({ inference: {\n aliases: { frontier: { candidates: [\n { provider: 'anthropic', model: 'claude-…' }, // provider key var\n { provider: 'openai', model: 'gpt-…', baseUrl, apiKeyEnv }, // own key + endpoint\n { preset: 'lemonade', model: 'qwen…' }, // #4674 preset\n { alias: 'local' }, // expanded in place\n ] } },\n tiers: { great: { alias: 'frontier' }, cheap: { candidates: [ … ] } },\n degrade: true, // default\n} });\n```\n\nA candidate is exactly one of provider/model, preset/model, or alias.\n`apiKeyEnv` is read at call time and needs a `baseUrl` (core's pair rule: a\nkey only travels with its own endpoint); a named variable that is unset skips\nthe candidate as `credential`. `findInferenceConfigIssues()` /\n`validateInferenceConfig()` name every offender by path: empty alias, unknown\nalias (tier or nested), cycle, key without endpoint, unknown preset. The\nexecutor also validates every resolved target it runs (a resolver's chains\nnever pass through config), and a candidate key is never dropped: without\nits own `baseUrl` core's pair rule refuses it.\n\n## Executor (`src/executor.ts`)\n\n- Free of SDK imports: it drives the clients core builds through\n `context.clientFor(target)`; tests use fake clients.\n- Failure classes (`classify.ts`): `quota | credential | missing-model |\n transient | refusal | missing-capability`, from error class names, `code`, `retryable` and HTTP\n status across `cause` / `error` / `context.originalError`, never message\n text. The SDK maps 5xx and other HTTP failures to `API_ERROR` /\n `UNKNOWN_ERROR` without a status or cause (happyvertical/sdk#1493); those\n count as `transient`. A definitive client error that survives in the chain\n (other 4xx, `invalid_request_error`, context length) is not a routing\n failure. Core's typed bind refusal (`INFERENCE_TARGET_UNBOUND`: pair rule,\n `HAVE_AI_*`) is `credential` and advances. Aborts and unclassified errors stop the walk and are rethrown (an\n unclassified one is still recorded).\n- `transient` retries the same candidate once after 200–500 ms of jitter;\n every class then advances. A `refusal` finish reason with nothing streamed\n also advances; finish callbacks are replayed only for the kept answer.\n- Streamed text is never extended: a candidate that emitted text and then\n failed ends the walk with a `partial` substitution and\n `InferenceExhaustedError` (`reason: 'partial'`, `partialText`), unless\n `allowPartial` returns the text. A hosted-gate block (`AI_UNAVAILABLE`\n from `clientFor()`, #4760) stops the walk and records nothing (core wrote\n the `blocked` row). A step whose target names a `source` (a ladder step's\n resolved binding) records that source over the chain's, so hosted spend\n meters as hosted; `expandTarget()` hands a chain's own `source` to its\n candidate steps, so a hosted chain reached on a ladder step is gated in\n `clientFor()` and meters as hosted too. A `stream()` consumer that stops reading\n aborts the provider stream (internal signal merged with the caller's).\n- Ladder: when a tier's chain is spent and `degrade !== false`, resolve the\n next lower tier through `context.resolveTier()`, stopping below\n `minimumTier`. Never up. Steps already tried (same identity) are skipped,\n so unbound lower tiers try the configured client once.\n- Tags on the answering call: caller tags, `tierRequested`, `tierResolved`\n (the tier that answered), and `substitutions` (JSON array of\n `{ from, to, reason }`, labels `provider/model`, `preset:<id>/model`,\n `default`). A call that never answered records one zero-token row through\n `context.recordUsage()` (core's usage handlers; no second writer) with\n `outcome: failed | partial`, `reason` and the full trail ending `to: null` (reason `unclassified` when a non-routing error ended it),\n then throws `InferenceExhaustedError` (`INFERENCE_EXHAUSTED`).\n- Capabilities (#4758): before calling a candidate, a call that sends\n `responseFormat` (JSON) or `tools` checks `context.capabilitiesFor(target)`\n (core's detection, [inference.md](../core/agents/inference.md)). A\n mismatch the caller's `capabilityFallback` option permits (`json: 'text'`,\n `tools: 'drop'`) runs the candidate without the feature, tagged\n `capabilityFallback`; any other is a `missing-capability` substitution and\n the walk advances with no model call. The option never reaches a provider.\n Core's `InferenceUnavailableError` (`reason: 'capability'`) also classifies\n as `missing-capability`. No detector (older core) or no requirement: no\n check. Context limits are core's (single targets only).\n- Add a class to `INFERENCE_FAILURE_CLASSES` and a kind to `EXECUTOR_KINDS`\n (`createChainExecutor()` in `fusion.ts` dispatches on it); consumers\n ignore kinds they do not run.\n\n## Fusions (#4753)\n\n`{ fusion: '<key>' }` tier targets run consumer-defined fusions (`single` 1\ncall, `pair` 3, `handoff` 2) from, highest first, the resolver's stored app\nrows (core's `lookupFusion()`, #4800), `inference.fusions`, or\n`defineFusion()`; every source is re-validated before it runs. SMRT ships the interpreter, never a fusion. Seats are\nsmrt-prompts keys run through `createChainClient()`; a dead seat never\naborts a `pair`, all seats dead throws `InferenceFusionError`; each run writes\none `_smrt_ai_runs` row. Fusions are defined at app scope only; a tenant\nmay bind a tier to an allow-listed fusion, never define one. Details:\n[agents/fusions.md](agents/fusions.md).\n\n## Doctor (`src/node.ts`)\n\nPer tier, alias and fusion (structure only): provider recognized (AI SDK provider list or preset id),\nkey variable present (a provider's default variable only warns: a\nsame-provider target inherits the configured key), model named (warns),\nalias resolvable, cycle-free. `probe` GETs `/models` (OpenAI-compatible,\nAnthropic, Gemini) or Ollama `/api/tags` with a 3 s timeout, sending the key\nonly to its own endpoint. A binding errors when its config is invalid or no\ncandidate can answer; one bad candidate in a working chain warns. Keys never\nappear in the report. With `probe` each candidate also reports its\ncapabilities and their source (#4758, `formatInferenceCapabilities()`): the\nlisting's context length, `supported_parameters` and input modalities\n(`inferenceCapabilitiesFromModelListing()`) are the `probe` layer; the SDK\n`provider` layer only through the `sdkCapabilities` option (the doctor builds\nno SDK clients).\n\n## Settings UI and binding storage (#4754)\n\n`./svelte` `InferenceSettingsPanel`, `./server` `InferenceSettingsService` /\n`InferenceOverrideService` / `createInferenceOverrideResolver()`, `./models`\nthe `_smrt_inference_overrides` model (ships through `smrt db:migrate`).\nKeys go to smrt-secrets through the host and are bound to their endpoint's\norigin; user choices are the smrt-preferences kind `inference`; app/tenant\nbindings are override rows, tenants limited to the app row's allow-list and\nthe `tenantTargetPolicy` hook. The app's transcript setting (#4781) is the\napp-only `transcripts` row, editable in the panel only with `appEditable`,\napplied as core's transcript app settings layer; the Usage tab shows\ntranscript counts and links to a host route, never content. Details: [agents/settings.md](agents/settings.md),\nconsumer guide [docs/content/inference-settings.md](../../docs/content/inference-settings.md).\n\n## Validation\n\n```bash\npnpm --filter @happyvertical/smrt-inference test\npnpm --filter @happyvertical/smrt-inference test:postgres # hosted provisioning on PostgreSQL (advisory lock, runOnce)\npnpm --filter @happyvertical/smrt-inference typecheck\npnpm --filter @happyvertical/smrt-inference build\n```\n",
|
|
304
|
+
"moduleDocs": [
|
|
305
|
+
{
|
|
306
|
+
"path": "agents/resolver.md",
|
|
307
|
+
"module": "resolver",
|
|
308
|
+
"content": "# Per-user and per-tenant bindings (`src/resolver.ts`, #4756)\n\n`createUserInferenceResolver({ db | secrets, fallback?, ttlMs?, principal? })`\nis core's `InferenceResolver`. Per tier, highest first: the user's stored\nconfig (its `tiers[tier]`, then its default binding), the tenant's (same\norder), `fallback`, the app `inference` block, then the configured client\n(`SMRT_AI_*` / process `ai`). Every resolution carries `target.source`\n(`byo | local | hosted | app`); core records it as the `source` usage tag.\n\n- **Registration order.** `registerUserInferenceResolver(r)` installs\n `createInferenceResolver(r)`: the #4752 validating resolver stays outermost\n (chain config is validated first), then the user resolver, then its\n `fallback`, then core's config resolution. A later `registerInference()`\n keeps it (the wrapper is marked as this package's own); a bare\n `setInferenceResolver(r)` would drop the validation.\n- **Storage.** One smrt-secrets type, `inference.provider-config`\n (`category`), via `SecretService.store()` / `retrieve()` only, with\n `context = tenantId`. Names: `inference.provider-config:user:<userId>` and\n `inference.provider-config:tenant`. The value is\n `{ provider, baseUrl, model, apiKey, preset, params, source?, mode?,\n gatewayKeyAlias?, tiers? }`; hosted (#4759) writes the same type with\n `source: 'hosted'` and `gatewayKeyAlias` (refused on any other source;\n every target of the config carries it as `usageTags`).\n `parseInferenceProviderConfig()` requires every binding to be\n self-contained (`baseUrl` and `apiKey`, `'smrt-local'` for keyless local\n servers, or a preset that supplies the endpoint), so a stored binding never\n borrows the server's environment credentials; it also refuses a `baseUrl`\n with userinfo, query or fragment, `params` beyond the scalar generation\n options in `INFERENCE_PROVIDER_PARAM_KEYS`, a preset without a model,\n unknown fields, and `source: 'app'`. Redaction copies known non-secret\n fields only. A corrupt stored value fails the call with\n `InferenceProviderConfigError` (paths only, never values); a missing,\n disabled or expired secret is simply no layer.\n- **Identity.** The ambient principal (tenancy context, then the users\n session context: smrt-preferences' order). With a principal: its tenant\n and user. A request for another tenant (an object of tenant B used by a\n principal of A) reads nothing stored and falls through to app config,\n unless `allowCrossTenant` (then B's tenant layer, no user). No principal\n (jobs): the request's tenant, no user layer, never a guess.\n `request.userId === null` (core `resolveInferenceTarget({ userId: null })`)\n skips the user layer for clients shared beyond one user (agents); a string\n `userId` is ignored.\n- **Cache.** Decrypted layers per `(tenantId, userId)` for `ttlMs` (30 s);\n failed loads are evicted. `write(level, config)` / `remove(level)` store\n for the current principal and invalidate; `invalidate()` takes all, one\n tenant (every user), or one user. Who may write the tenant config is the\n host server action's decision (#4754).\n- **Mode (#4757).** A stored config may carry `mode` (`byo | local | hosted\n | off`) and may hold only a mode (no binding, no key). The user's mode,\n else the tenant's, applies: `off` answers every tier `{ unavailable:\n 'off' }` and `mode()` (untiered calls) `{ mode: 'off', unavailable: 'off' }`;\n `byo` / `local` / `hosted` bind only a stored binding of that `source`\n (user, then tenant), else `unavailable: 'unconfigured'`, never app config.\n No mode: the order above, reporting the binding's source as `mode`.\n `mode()` returns an explicit mode, which core uses to route untiered calls\n through `auto` (so they bind the same way).\n `setMode(level, mode | null)` keeps the binding (deletes a config left\n empty) and invalidates; `write()` replaces the whole config, mode\n included. `resolveBindings()` adds `mode: { mode, layer }`. The #4752\n wrapper forwards `mode()` only when the base has one.\n- **Keys.** Never logged or tagged. `resolveBindings()` returns the tier map\n and both stored configs redacted by default (`hasApiKey`, no `apiKey`), the\n shape the settings panel reads; `redact: false` is server-only.\n- Raw `getAI()` sites take the binding through core's\n `resolveInferenceTarget()` (see [core inference.md](../../core/agents/inference.md)).\n"
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
"path": "agents/hosted.md",
|
|
312
|
+
"module": "hosted",
|
|
313
|
+
"content": "# Hosted inference (#4759, #4760)\n\n## Provisioning (`src/hosted.ts`, #4759)\n\n`provisionHostedInference({ db, tenantId, userId, budget })`,\n`revokeHostedInference({ db, tenantId, userId })`, or one\n`createHostedInferenceProvisioner({ db, secrets?, gateway?, admin?, actor? })`\n(`provision`, `revoke`, `syncBudget`, `status`, `listActive`, `listKnown`,\n`revokeTenant`, `syncTenantBudget`, `reconcileTenant`,\n`acknowledgeGatewayRevocation` to drop an owed deletion an operator did by\nhand).\n\n- **Gateway.** Admin calls go through `ai.admin` (`AIAdminInterface`) of the\n `bifrost` / `litellm` provider of `@happyvertical/ai` >= 0.111.7\n (happyvertical/sdk#1494); never reimplement gateway administration here.\n `HostedGatewayAdmin` is `HOSTED_GATEWAY_ADMIN_METHODS` (`createProject`,\n `createVirtualKey`, `updateVirtualKey`, `deleteVirtualKey`,\n `listProjects`); a provider missing one is `HOSTED_GATEWAY_UNSUPPORTED`.\n SMRT persists only `AIVirtualKey.id`, the non-secret handle (Bifrost id,\n LiteLLM `token_id`); an id that equals or looks like a key value (`sk-…`)\n is dropped on create and when older state is read, so that key counts as\n having no id. Budget sync sends\n `updateVirtualKey(id, { budget })` only (Bifrost refuses `models` /\n `metadata`, LiteLLM `providerConfigs`: `ADMIN_UNSUPPORTED_FIELD`);\n `unsupported` now means the grant has no key id. Revoke ends local use\n before touching the gateway (config restored, state retired with the key\n owed, then `deleteVirtualKey`; `false` is `missing`, an already revoked\n key, nothing owed). A deletion the gateway did not perform (`failed`, a\n key a failed provision left, `unsupported`: no admin configured or no key\n id; entries from before 0.111.7 also read `unsupported`) goes to the\n state's `pendingGatewayRevocations` (id + alias, never the key), is\n audited with `manualGatewayRevocation`, and is retried by `revoke()` /\n `reconcileTenant()`; each retry that settles one writes\n `inference.hosted.revoke-retry` with `settled: [{ keyId, keyAlias,\n gateway }]`, closing those records. Entries without an id stay for an\n operator (`acknowledgeGatewayRevocation`).\n- **Config, read once from the environment** (`getHostedGatewayConfig()`;\n unset provider = hosted off): `SMRT_INFERENCE_HOSTED_PROVIDER`\n (`bifrost|litellm`), `_BASE_URL` (the OpenAI-compatible endpoint keys are\n used against), `_ADMIN_URL`, `_ADMIN_KEY` (LiteLLM master key, required for\n litellm; or a Bifrost admin key), `_ADMIN_USER` / `_ADMIN_PASSWORD`\n (Bifrost Basic auth), `_MODEL`, `_<TIER>_MODEL`, `_MODELS` (comma list the\n key may use; default the binding's models), `_KEY_DURATION`,\n `_ADMIN_TIMEOUT_MS` (deadline per admin call, default 30 s, also passed as\n the SDK `timeout`; a call past it is a failure, so a stalled gateway never\n holds the lifecycle lock). A create that fails ambiguously (timeout,\n transport reset, 5xx, unclassified; anything but a credential, quota,\n missing-model or refusal answer per `classifyInferenceFailure`) records\n its alias as owed, since the gateway may have minted the key.\n Errors name missing fields, never values.\n- **Storage** (smrt-secrets, `context = tenantId`): the key lives only in\n the user's `inference.provider-config:user:<id>` (`source: 'hosted'`,\n `mode: 'hosted'`, `gatewayKeyAlias`, a binding per configured tier);\n `inference.hosted:user:<id>` (category `inference.hosted`) holds status,\n generation, key id and alias, budget and the user's prior config;\n `inference.hosted:project` the tenant's gateway project (created once\n through `runOnce()`, name `smrt tenant <tenantId>`, LiteLLM id\n `smrt-<tenantId>-<nonce>`). Without a stored record, `listProjects({\n tenantId })` runs first and adopts the lowest-id project with that name\n (and id prefix on LiteLLM), audited `adopted: true`, so a crash between\n create and persist never makes a second project; a failed lookup fails\n the provision rather than create blind. Every write passes\n `auditDetails.operation` (`inference.hosted.provision|revoke|budget|project`)\n to `SecretAuditLog`.\n- **Idempotency and ordering.** An active grant whose binding is still the\n stored one is returned (`created: false`), its budget synced when it\n differs. The create path runs under `runOnce()` keyed by `(tenantId,\n userId, generation)`; revoke advances the generation. Aliases\n (`smrt-<tenant>-<user>-g<n>-<nonce>`) and LiteLLM project ids\n (`smrt-<tenant>-<nonce>`) are unique per attempt, so a failed attempt's\n leftovers never block the retry. Lifecycle operations run one at a time\n per process (a process-wide chain: rare operations, and it bounds the\n pooled connections held) and, on PostgreSQL, under a per-tenant\n `pg_advisory_xact_lock` held in a transaction for the whole operation\n (its writes use other pooled connections: at most three per process), so\n a tenant-wide revoke or reconcile never misses an in-flight provision.\n The optional `entitled(tenantId)` check runs inside that lock before a\n key is minted (`HOSTED_NOT_ENTITLED`) and before an entitled reconcile\n acts (a stale snapshot then revokes), so a stale gain or late retry after\n a downgrade does not activate. Hosts running the listener or reconciler\n across processes should set it. A failed binding write deletes or records the new\n key and restores the previous state after the claim rolls back (no delete\n inside the claim transaction: single-connection adapters would wait on\n it). A user who replaced the hosted binding keeps their config: an\n explicit `provision()` retires the stale key and mints anew; a reconcile\n only retires it and marks the state `optedOut`, which later reconciles\n respect until an explicit `provision()`. Writes through the #4756\n resolver (the settings panel) do not take this lock.\n- **Revoke** restores the prior config when it holds a binding or a mode,\n else writes `{ mode: 'off' }`; a config the user replaced is kept.\n- **Budget** is integer micro-dollars (`{ limitMicros, window? }`, `day |\n week | month | year`, default month), converted to the gateway's USD\n `maxLimit` and `1d|7d|30d|365d` only at the admin call. #4760's spend gate\n is the authoritative pre-call check; the gateway budget is defense in depth.\n An unchanged budget is never re-sent (a gateway update may restart the\n spend window); after a failed or timed-out update (`budgetSyncUncertainAt`,\n it may still land) syncs re-apply the stored budget until a confirmed\n update 15 minutes later.\n- **Entitlement hook.** `createHostedInferenceEntitlementListener({\n provisioner, users, budgetFor?, defaultBudget? })` structurally matches\n smrt-subscriptions' `EntitlementChangeListener` (register it with\n `registerEntitlementChangeListener()`; the host wraps its webhook write in\n `trackEntitlementChange()`). A change touching `inference.hosted`\n reconciles the tenant to the change's `after` snapshot through\n `createHostedInferenceReconciler()`: entitled provisions `users(tenantId)`\n (membership is the host's; exclude users an operator revoked) unless\n they opted out, and syncs budgets; not entitled revokes every grant; both\n retry owed deletions (also for active grants: they are old keys). The\n plan's budget is the grant's `metadata.budget` (`{ limitMicros, window }`).\n A replayed webhook diffs to \"no change\", so hosts also call the reconciler\n with `snapshotEntitlements()` on redelivery and on a schedule; that is\n what repairs a reaction lost to a crash. Per-user failures are thrown\n together after all users were tried.\n- **Keys.** Never in results, errors (gateway failures carry only the\n operation and error code), audit details, `runOnce()` results or logs.\n Usage rows carry `source: hosted` and `gatewayKeyAlias` (from\n `InferenceTarget.usageTags`). Other processes see a new binding after the\n resolver cache expires; this process is invalidated through\n `invalidateUserInferenceResolvers()`.\n\n## Spend gate and kill switch (`src/gate.ts`, #4760)\n\n`createHostedInferenceGate({ killSwitch, spend, principal?,\nrequireSubscription?, onWarning? })` is core's `InferenceGate` (seam:\n[core inference.md](../../core/agents/inference.md), \"Hosted gate\");\n`registerHostedInferenceGate(gate | options)` installs it and the\nprincipal's user as the `userId` usage tag. Host wiring:\n\n```ts\nregisterHostedInferenceGate({\n killSwitch: new KillSwitchService({ db }), // smrt-features\n spend: await HostedAiSpendEvaluator.create({ db }), // smrt-subscriptions\n});\n```\n\nBoth stores are structural: smrt-features and smrt-subscriptions build with\nthe CLI, which depends on this package, so a dependency on either is a turbo\ncycle. Checks, in order, on every `source: hosted` call:\n\n1. **Kill switch** `inference.hosted.enabled` (`HOSTED_INFERENCE_KILL_SWITCH`):\n global DISABLE, then the tenant's DISABLE, blocks (`policy:\n 'kill-switch'`, `disabledBy`). Uncached, no model call: **the incident\n lever** (`smrt inference:hosted disable [--tenant <id>]`; programmatic\n `setHostedInferenceEnabled(store, { enabled, tenantId? })`,\n `getHostedInferenceSwitch()`). Stored as smrt-features override rows\n with kill-switch semantics, not as a flag: a tenant ENABLE can never\n lift a global DISABLE, and `inference.hosted` (the plan key) stays a pure\n entitlement. Hosts with an admin UI write the same DISABLE rows through\n `FeatureOverrideService` and their authorizer.\n2. **Tenant**: request tenant, else the principal's; none blocks (`no-tenant`).\n3. **Entitlement**: no entitled subscription blocks (`subscription`) unless\n `requireSubscription: false`.\n4. **Spend**: the plan's tenant and per-user caps (smrt-subscriptions\n AGENTS, \"Hosted AI spend caps\"): `blocked` blocks with `cap`, `spend`\n (micro-dollars) and `resetsAt`; `warn` returns `warn` and calls\n `onWarning` (errors swallowed). The user is the principal's, only in its\n own tenant, and none for `userId: null` (agents).\n\nAny thrown error propagates and core blocks with `gate-error`. The plan\ncheck does not test the `inference.hosted` feature key: provisioning the\nhosted binding (#4759) is where entitlement to hosted is granted.\n"
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"path": "agents/fusions.md",
|
|
317
|
+
"module": "fusions",
|
|
318
|
+
"content": "# Fusions (`src/fusion.ts`, #4753)\n\n`src/fusion.ts` runs `{ fusion: '<key>' }` tier targets. SMRT ships the\ninterpreter and the vocabulary, never a fusion: modes are code, fusions are\nconfig. A fusion comes from, highest first (each replaces a lower one whole):\nthe registered resolver's stored layer through core's\n`context.lookupFusion(key)` (#4800; the override resolver's app rows),\n`inference.fusions.<key>` (app config), or\n`defineFusion({ key, mode, seats, gate?, description? })` (process registry\non `globalThis`). A seat is `{ name?, prompt, target, temperature?, output? }`:\n`prompt` is a smrt-prompts key whose resolved template (code default,\nconfig, app and tenant overrides, read through `context.db`) is the seat's\nsystem message, so tenant overrides of a seat need nothing new; `target` is\na tier (resolved through the same resolver with the seat's prompt key, so\nper-user bindings apply), `{ alias }`, `{ candidates }` or an inline\n`{ provider, model }`, never a fusion. Each seat runs through\n`createChainClient()`, so chains, classified fallback and (tier seats only)\nthe ladder apply per seat. `name` defaults to the prompt key and must be\nunique.\n\nExample (a consumer's code; SMRT does not ship it):\n\n```ts\ndefinePrompt({ key: 'contracts.skeptic', template: 'List the risks…' });\ndefinePrompt({ key: 'contracts.advocate', template: 'List the benefits…' });\ndefinePrompt({ key: 'contracts.synthesis', template: 'Weigh both reviews…' });\nconfig({ inference: {\n tiers: { best: { fusion: 'contract-review' } },\n fusions: { 'contract-review': {\n mode: 'pair',\n seats: [\n { name: 'skeptic', prompt: 'contracts.skeptic', target: 'good' },\n { name: 'advocate', prompt: 'contracts.advocate', target: { alias: 'frontier' } },\n { name: 'synthesis', prompt: 'contracts.synthesis', target: 'great' },\n ],\n gate: { criteria: 'Both reviews agree on every material risk.',\n sufficientWhen: { choice: 'agree', minConfidence: 0.85 } },\n } },\n} });\nawait contract.do('Summarize this contract', { tier: 'best' }); // unchanged caller\n```\n\n- **Modes and call counts** (model calls; a seat's fallback candidates are\n substitutions of that seat, not extra seats): `single` 1 (one seat);\n `pair` 3 (seats 0 and 1 in parallel, then seat 2 merges both outputs,\n labelled by seat and model); `handoff` 2 (seat 0 drafts, seat 1 receives\n the draft and answers). A mode that adds a call is a bug.\n- **Gate** (`pair`, `handoff`): one choice question through\n `context.decide()` (core's typed-decision client, the `evaluate()` path)\n over the outputs so far, labels `sufficientWhen.choice` and `continue`.\n The chosen label at `minConfidence` or above skips the remaining stage,\n recorded `skipped`; the answer is then seat 0's output (`pair`: the first\n live one) or the draft. No decision client (`unavailable`) or a decision\n error runs the stage. Its usage row has `seat: 'gate'`, a reserved seat\n name.\n- **Failure semantics.** A dead seat never aborts a `pair`: the merge seat\n is told which seat failed and why (class and message). A dead merge or\n handoff answer seat falls back to the first live output, recorded\n `degraded`; the handoff answer seat is told when the draft died. A run\n whose seats all died throws `InferenceFusionError`\n (`INFERENCE_FUSION_FAILED`, `result` holds every seat) with the run\n recorded `failed`, never an empty answer. A seat whose setup fails (prompt\n override read, its tier's resolver) dies like a model failure. A config\n error, a caller abort, or a final seat that already streamed text\n (`partial`) rethrows, after both `pair` seats have settled.\n- **JSON**: `output: 'json'` sends `responseFormat: json_object` and parses;\n a seat whose text does not parse keeps it under `raw` (with `parseError`)\n in `FusionSeatResult` and hands the raw text on, labelled. A candidate\n without JSON mode runs the seat in text on that recovery path\n (`capabilityFallback: { json: 'text' }`, #4758) instead of being skipped.\n- **Caller options**: generation options (`responseFormat`, `tools`,\n `onProgress`, …) reach the final seat only; `signal` / `timeout` reach every\n seat; `history` is the seat's instructions. `onFusionResult(result)`\n receives the structured `FusionRunResult`. Only `message()` runs a fusion.\n- **Record**: every seat's usage row carries `runId`, `fusionKey`, `seat`\n and the seat's prompt tags; `context.recordRun()` writes one\n `_smrt_ai_runs` row (`ok | degraded | failed`; a seat is degraded when it\n died or answered through a substitution) with identities and totals only\n (`../../core/agents/ai-usage.md`). `smrt ai:usage --by fusion` totals runs.\n- **Validation**: `findInferenceConfigIssues()` names structural offenders\n by path (`fusion-invalid`, `fusion-unknown`, `fusion-nested`); an\n unregistered seat prompt key is a load error (`fusion-prompt-unknown`) from\n `validateInferenceConfig()`, the resolver, the executor and\n `defineFusion()`, but not the doctor (it runs without the app's prompts).\n- Seat prompts resolve through `@happyvertical/smrt-prompts` (a dependency;\n never a peer, #1582), imported by a variable specifier from this package\n (strict layouts find it; bundlers do not follow it, so the root stays browser-safe),\n then core's `importOptionalDependency()` (a module registered with\n `registerOptionalDependency()` in bundled builds).\n `setFusionPromptResolver()` replaces it.\n- **Fusions are app-scope only.** A fusion multiplies hosted model spend\n per call, so a tenant never defines or redefines one; with #4754's\n allow-list a tenant may bind a tier to a listed fusion (`fusion:<n>`).\n- **Stored fusions (#4800).** Core hands the executor\n `context.lookupFusion(key)`: the instance's (or registered) resolver's\n optional `lookupFusion(key, request, context)`, then the merged\n `inference.fusions`; `undefined` falls to the registry. Without it (older\n cores) the executor reads config, then the registry, and validates at bind\n time; with it the lookup starts at bind time and an unknown or invalid\n fusion fails the first call before any seat runs (nothing recorded). Every\n definition, whatever its source, is validated before it runs (structure\n against the call's aliases, every seat prompt key); a resolver's answer\n fails under path `resolver.fusions.<key>`. The #4752 validating wrapper\n and the #4756 user resolver forward `lookupFusion` to their base /\n `fallback` (stored user and tenant configs never define a fusion).\n"
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
"path": "agents/settings.md",
|
|
322
|
+
"module": "settings",
|
|
323
|
+
"content": "# Settings UI and binding storage (#4754)\n\nConsumer guide: [docs/content/inference-settings.md](../../../docs/content/inference-settings.md).\n\n- **Storage split, enforced by API shape.** Keys: the panel emits\n `onSaveKey({ provider, key, preset?, baseUrl? })` (the endpoint shown when\n the key was typed; the key input remounts on a provider or base URL\n change), the host binds it with `bindInferenceKey()` and stores it once,\n as the #4756 `inference.provider-config` secret, through the user\n resolver's `write()` (`inferenceProviderConfigFromKey()` converts the\n record and re-checks the bound origin), and the panel only ever gets\n `keySet` back. User choices (mode `byo | local | hosted |\n off`, provider, base URL, model, default tier): smrt-preferences kind\n `inference` (`inferencePreferenceKind`, permissions `inference.customize`\n / `inference.personalize`; the host registers both, smrt-preferences is\n not a dependency). App/tenant bindings: `_smrt_inference_overrides`.\n- **Key rule** (`src/settings/keys.ts`, ported from the planner's BYO\n screen): a key is bound when saved to the origin of its endpoint\n (explicit base URL, else preset, else `inferenceProbeUrl()`'s provider\n default, a provider id that is a preset id counting as that preset;\n https only, plain http for loopback; gateways such as\n `litellm`/`bifrost` need a base URL) and `inferenceKeyFor()` releases it\n only for a URL on that origin. `testConnection()` reads no environment key\n and never probes another origin with a stored key; details pass\n `redactInferenceSecret()`. Binding rows, preference payloads and tested\n candidates refuse credential-shaped members (`findKeyMember()`; `key` is\n allowed because #4753 fusions carry one).\n- **Override table**: one row per `(binding, context)`, binding\n `tier:<tier> | default-tier | alias:<name> | fusion:<name>`, `context`\n `__app__` or the tenant id (the prompts pattern), `target` JSON,\n `tenant_allow_list` JSON on app rows. Generated surfaces are closed; ships\n through `smrt db:migrate` (`override-migration.test.ts`). A tier target is\n exactly a model `{ provider?, model }`, an alias `{ alias }` or a fusion\n `{ kind: 'fusion', fusion }`: no endpoint, key or inline chain (those go\n in an alias). Layers, low to high: code default (configured client),\n `inference` config, app row, tenant row; each target replaced whole.\n- **Tenant allow-list**: a tenant writes only `tier:*` and `default-tier`,\n only to a reference (`model:<provider>/<model>`, `alias:<n>`,\n `fusion:<n>`, `tier:<t>`; trailing `*` and `*` match) on the app row's\n list (no list allows nothing), and only when the\n `tenantTargetPolicy` hook returns `true`; this hook is where #4667 caps\n the maximum hosted tier. Both are re-checked on every layered read, so a\n narrowed list or policy disables existing tenant rows (`tenantBlocked`).\n Aliases and fusions are app-scope only (a tenant definition could\n redefine an allow-listed name).\n- **`InferenceSettingsService`** mirrors `PromptSettingsService`:\n `listInferenceSettings()` returns one row per tier with the effective\n target, each scope's raw value and both revert previews computed\n separately (`appRevertTarget` = config, never the app row;\n `inheritedTarget` = config + app row), plus `getDefaultTierSetting()`,\n `listInferenceAliases()`, `listInferenceFusions()` (registry: the\n `registryFusions` option, default #4753's `defineFusion()` registry;\n config `inference.fusions`; editable app rows), each entry with\n `shadows` (lower sources it replaces by name) and `issues` (what would make\n the executor refuse it, prompt keys included), the executor's precedence\n (#4800). `setFusion()` refuses credential members, then a definition that\n adds a structural issue. Writes go through `InferenceOverrideService`\n (authorizer first, fails closed; without `authorize` the service is\n read-only) and refuse a value that adds a config issue to the layer it\n lands in; removing an app alias or fusion that any row (every tenant) or\n config still names is refused (`InferenceBindingInUseError`, tenant rows\n only counted, checked after the authorizer allowed the write) unless\n config or the registry keeps defining it. Every config-backed value a row method\n returns passes `stripKeyMembers()` (config may carry literal keys);\n `getEffectiveConfig()` is server-side only. `testConnection()`, `testCandidate()` and `doctor()` reuse\n `./node`; `testCandidate()` reads a key variable only for a candidate\n already in the effective config (an unsaved one is probed keyless);\n `usage()` is `reportAiUsage()` by model (default), tier, prompt, fusion,\n template, with `transcripts: true` for per-bucket counts (#4781).\n- **Transcript setting** (#4781, `src/settings/transcripts.ts`): binding\n `transcripts`, app row only (service and model refuse a tenant row),\n value `{ enabled?, sample?, maxBytes? }` checked by core's\n `assertInferenceTranscriptsConfig()`. `setTranscriptSettings()` writes it\n through the authorizer and installs it as core's\n `setAiTranscriptAppSettings()` layer (config < app row < env); the\n override resolver re-applies the app rows on every resolve (rows cached\n for `ttlMs`), `loadInferenceTranscriptSettings()` is the worker startup\n hook. `getTranscriptSettings()` adds the revert preview, the env override\n (detected by forcing both values through the layer) and the sweep's\n retention days, read-only (the sweep runs with its caller's policy).\n- **`createInferenceOverrideResolver({ db, base })`** applies stored\n bindings at call time and defers unbound tiers to `base` or config; an\n app-only alias is returned as its inline candidate chain because the\n executor reads only the process config. Rows (not layers) are cached per\n tenant for `ttlMs` (30 s) and invalidated by any write in the process; the\n allow-list and policy are re-applied on every call, and only the requested\n tier's chain is validated, so one dangling reference fails one tier.\n Compose it as the per-user resolver's `fallback` (see AGENTS.md,\n \"Resolver composition\"). Its `lookupFusion()` (#4800) answers core's\n executor lookup from app rows only (read with no tenant, so a tenant row\n never defines a fusion), validated against config + app rows with every\n seat prompt key (`InferenceConfigError`, paths `fusion:<name>…`), app-only\n seat aliases inlined like tier aliases; an app row that exists but cannot\n be read fails `fusion-invalid` (never its value) rather than deferring (the\n list shows it as the app winner with that issue); no app row defers to\n `base`, then config. An app row shadows a config fusion of the same name.\n- **Panel** (`src/svelte`): built like `PromptSettingsPanel` (presentational,\n `svelte` optional peer, prune script, two-project Vitest, `svelte-check`,\n named `$props()` interfaces, types free of core). The default view is the\n whole screen for one provider; Advanced (tier ladder, `auto` tier,\n aliases, fusions) is not rendered until opened. The key input is its own\n form, never in state, cleared on submit. Tenant scope disables targets\n off the allow-list (display only) and makes aliases/fusions read-only.\n Rows remount on `contextKey`. The transcript section is read-only unless\n `appEditable` and `onSaveTranscripts` are both passed; the Usage tab\n links counts through the host's `transcriptHref` and renders no\n transcript content (#4781).\n"
|
|
324
|
+
}
|
|
325
|
+
],
|
|
326
|
+
"browser": {
|
|
327
|
+
"status": "server-only",
|
|
328
|
+
"issues": [
|
|
329
|
+
"#3617",
|
|
330
|
+
"#3624",
|
|
331
|
+
"#3626",
|
|
332
|
+
"#3627"
|
|
333
|
+
],
|
|
334
|
+
"via": [
|
|
335
|
+
"@happyvertical/smrt-profiles",
|
|
336
|
+
"@happyvertical/smrt-secrets",
|
|
337
|
+
"@happyvertical/smrt-tenancy",
|
|
338
|
+
"@happyvertical/smrt-users"
|
|
339
|
+
]
|
|
340
|
+
}
|
|
341
|
+
}
|