@happyvertical/smrt-inference 0.60.15 → 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 +10 -5
- package/agents/fusions.md +16 -3
- package/agents/hosted.md +29 -13
- package/agents/settings.md +30 -7
- package/dist/chunks/{InferenceOverrideCollection-wl7sI-3C.js → InferenceOverrideCollection-Dat4TrK5.js} +29 -4
- package/dist/chunks/InferenceOverrideCollection-Dat4TrK5.js.map +1 -0
- package/dist/chunks/{provider-config-4Bx8N52X.js → provider-config-BSpIQRVM.js} +37 -11
- package/dist/chunks/{provider-config-4Bx8N52X.js.map → provider-config-BSpIQRVM.js.map} +1 -1
- package/dist/fusion.d.ts.map +1 -1
- package/dist/hosted.d.ts +31 -24
- package/dist/hosted.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/models/InferenceOverride.d.ts.map +1 -1
- package/dist/models.js +1 -1
- package/dist/node.js +86 -33
- package/dist/node.js.map +1 -1
- package/dist/register.d.ts.map +1 -1
- package/dist/resolver.d.ts.map +1 -1
- package/dist/resolver.js +4 -1
- package/dist/resolver.js.map +1 -1
- package/dist/server.d.ts +3 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +194 -41
- package/dist/server.js.map +1 -1
- package/dist/settings/layers.d.ts +14 -0
- package/dist/settings/layers.d.ts.map +1 -1
- package/dist/settings/override-service.d.ts.map +1 -1
- package/dist/settings/resolver.d.ts +7 -0
- package/dist/settings/resolver.d.ts.map +1 -1
- package/dist/settings/settings-service.d.ts +80 -7
- package/dist/settings/settings-service.d.ts.map +1 -1
- package/dist/settings/targets.d.ts +21 -2
- package/dist/settings/targets.d.ts.map +1 -1
- package/dist/settings/transcripts.d.ts +33 -0
- package/dist/settings/transcripts.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +17 -11
- package/dist/smrt-knowledge.okf.md +2 -2
- package/dist/svelte/components/InferenceFusionList.svelte +6 -0
- package/dist/svelte/components/InferenceFusionList.svelte.d.ts.map +1 -1
- package/dist/svelte/components/InferenceSettingsPanel.svelte +49 -2
- package/dist/svelte/components/InferenceSettingsPanel.svelte.d.ts +21 -2
- package/dist/svelte/components/InferenceSettingsPanel.svelte.d.ts.map +1 -1
- 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 +64 -14
- package/dist/svelte/components/InferenceUsageTable.svelte.d.ts +10 -4
- package/dist/svelte/components/InferenceUsageTable.svelte.d.ts.map +1 -1
- package/dist/svelte/index.d.ts +1 -1
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +1 -1
- package/dist/svelte/types.d.ts +66 -1
- package/dist/svelte/types.d.ts.map +1 -1
- package/dist/svelte/types.js +28 -0
- package/package.json +10 -10
- package/dist/chunks/InferenceOverrideCollection-wl7sI-3C.js.map +0 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { type AiUsageReport, type AiUsageReportDimension, type InferenceAlias, type InferenceCandidate, type InferenceConfig, type InferenceTarget, type InferenceTier, type SmrtClassOptions } from '@happyvertical/smrt-core';
|
|
1
|
+
import { type AiUsageReport, type AiUsageReportDimension, type InferenceAlias, type InferenceCandidate, type InferenceConfig, type InferenceTarget, type InferenceTier, type InferenceTranscriptsConfig, type RetentionPolicy, type SmrtClassOptions } from '@happyvertical/smrt-core';
|
|
2
2
|
import { InferenceOverrideCollection, type InferenceOverrideScopeType } from '../models/InferenceOverrideCollection.js';
|
|
3
3
|
import { type InferenceDoctorReport } from '../node.js';
|
|
4
4
|
import type { InferenceConfigIssue } from '../types.js';
|
|
5
5
|
import { type InferenceKeyRecord } from './keys.js';
|
|
6
6
|
import { type InferenceSettingsConfig } from './layers.js';
|
|
7
7
|
import { type InferenceOverrideAuthorizer, type InferenceTenantTargetPolicy } from './override-service.js';
|
|
8
|
-
import { DEFAULT_TIER_BINDING } from './targets.js';
|
|
8
|
+
import { DEFAULT_TIER_BINDING, type InferenceTranscriptSettingsValue, TRANSCRIPTS_BINDING } from './targets.js';
|
|
9
9
|
/** Which level supplied an effective value. */
|
|
10
10
|
export type InferenceSettingsLevel = 'default' | 'config' | 'app' | 'tenant';
|
|
11
11
|
/** One tier, rendered for a settings screen. */
|
|
@@ -74,6 +74,50 @@ export interface InferenceFusionSetting {
|
|
|
74
74
|
description?: string;
|
|
75
75
|
/** Config entries (app rows) can be edited as JSON; registry fusions are code. */
|
|
76
76
|
editable: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Lower sources this definition replaces by name (#4800): an app row
|
|
79
|
+
* shadows config and the registry, config shadows the registry, exactly
|
|
80
|
+
* as the executor picks. Absent when nothing is shadowed.
|
|
81
|
+
*/
|
|
82
|
+
shadows?: Array<'registry' | 'config'>;
|
|
83
|
+
/**
|
|
84
|
+
* Why the executor would refuse to run this definition (structure,
|
|
85
|
+
* aliases, unregistered seat prompt keys), checked as the executor
|
|
86
|
+
* checks it (#4800). Absent when it would run.
|
|
87
|
+
*/
|
|
88
|
+
issues?: InferenceConfigIssue[];
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The app's transcript setting (#4781), for the panel's Advanced section.
|
|
92
|
+
* App level only: tenants cannot override it, so it is the same for every
|
|
93
|
+
* tenant. Never carries transcript content.
|
|
94
|
+
*/
|
|
95
|
+
export interface InferenceTranscriptSettings {
|
|
96
|
+
binding: typeof TRANSCRIPTS_BINDING;
|
|
97
|
+
/** Effective values: config < app row < `SMRT_INFERENCE_TRANSCRIPTS`. */
|
|
98
|
+
enabled: boolean;
|
|
99
|
+
sample: number;
|
|
100
|
+
maxBytes: number;
|
|
101
|
+
/** Which level supplied `enabled`. */
|
|
102
|
+
supplyingLevel: 'default' | 'config' | 'app' | 'env';
|
|
103
|
+
/** The app row's stored value (`null`: none). */
|
|
104
|
+
appValue: InferenceTranscriptSettingsValue | null;
|
|
105
|
+
/** What reverting the app row produces (config and env only). */
|
|
106
|
+
appRevert: {
|
|
107
|
+
enabled: boolean;
|
|
108
|
+
sample: number;
|
|
109
|
+
maxBytes: number;
|
|
110
|
+
};
|
|
111
|
+
/** `SMRT_INFERENCE_TRANSCRIPTS` forces `enabled` to this (`null`: unset). */
|
|
112
|
+
envEnabled: boolean | null;
|
|
113
|
+
/**
|
|
114
|
+
* Days transcripts are kept by the retention sweep
|
|
115
|
+
* (`retention.aiTranscripts.maxAgeDays`); `null` when the sweep skips
|
|
116
|
+
* them. Read-only here: the sweep runs with its caller's policy.
|
|
117
|
+
*/
|
|
118
|
+
retentionDays: number | null;
|
|
119
|
+
/** Prompt keys with their own sample rate in config (read-only). */
|
|
120
|
+
promptOverrides: string[];
|
|
77
121
|
}
|
|
78
122
|
/** One provider choice for the default view. */
|
|
79
123
|
export interface InferenceProviderOption {
|
|
@@ -123,6 +167,13 @@ export interface InferenceSettingsServiceOptions {
|
|
|
123
167
|
* Registered fusions. Default: #4753's process registry (`defineFusion()`).
|
|
124
168
|
*/
|
|
125
169
|
registryFusions?: () => Iterable<RegisteredFusionLike>;
|
|
170
|
+
/**
|
|
171
|
+
* The transcript config with an app layer (#4781). Default core's
|
|
172
|
+
* `getInferenceTranscriptsConfig()` (config < app < env).
|
|
173
|
+
*/
|
|
174
|
+
transcriptsConfig?: (app: InferenceTranscriptsConfig | null) => InferenceTranscriptsConfig | undefined;
|
|
175
|
+
/** The retention policy the sweep runs with. Default core `config().retention`. */
|
|
176
|
+
retention?: () => RetentionPolicy | undefined;
|
|
126
177
|
}
|
|
127
178
|
/** Thrown when a write would leave the layered config invalid. Nothing is written. */
|
|
128
179
|
export declare class InferenceSettingsConfigError extends Error {
|
|
@@ -144,6 +195,8 @@ export declare class InferenceBindingInUseError extends Error {
|
|
|
144
195
|
/** How many tenant rows name it (never which tenants). */
|
|
145
196
|
tenantReferences: number);
|
|
146
197
|
}
|
|
198
|
+
/** Usage tab dimensions (`usage()`). */
|
|
199
|
+
export type InferenceUsageReportDimension = Extract<AiUsageReportDimension, 'model' | 'tier' | 'prompt' | 'fusion' | 'template'>;
|
|
147
200
|
/**
|
|
148
201
|
* Read and write side of an inference settings screen. See the module doc.
|
|
149
202
|
*/
|
|
@@ -181,7 +234,9 @@ export declare class InferenceSettingsService {
|
|
|
181
234
|
/**
|
|
182
235
|
* Fusions from the host's registry (code, read-only), the config's
|
|
183
236
|
* `inference.fusions`, and app rows (editable JSON). A later source
|
|
184
|
-
* shadows an earlier one by name
|
|
237
|
+
* shadows an earlier one by name, the executor's precedence (#4800): app
|
|
238
|
+
* row, then config, then registry. Each entry carries the issues that
|
|
239
|
+
* would make the executor refuse it, so the list shows what would run.
|
|
185
240
|
*/
|
|
186
241
|
listInferenceFusions(): Promise<InferenceFusionSetting[]>;
|
|
187
242
|
/**
|
|
@@ -260,15 +315,33 @@ export declare class InferenceSettingsService {
|
|
|
260
315
|
fetch?: typeof fetch;
|
|
261
316
|
}): Promise<InferenceDoctorReport>;
|
|
262
317
|
/**
|
|
263
|
-
* The
|
|
264
|
-
*
|
|
265
|
-
*
|
|
318
|
+
* The app's transcript setting (#4781): effective values, the stored app
|
|
319
|
+
* row, its revert preview, the env override and the retention window.
|
|
320
|
+
* The same for every tenant (no tenant override).
|
|
321
|
+
*/
|
|
322
|
+
getTranscriptSettings(): Promise<InferenceTranscriptSettings>;
|
|
323
|
+
/**
|
|
324
|
+
* Store (or, with `null`, remove) the app's transcript setting. Always
|
|
325
|
+
* the app row: there is no tenant scope. Goes through the authorizer
|
|
326
|
+
* (request `{ binding: 'transcripts', scopeType: 'app' }`); on success the
|
|
327
|
+
* value is installed as core's app settings layer in this process.
|
|
328
|
+
*/
|
|
329
|
+
setTranscriptSettings(request: {
|
|
330
|
+
settings: InferenceTranscriptSettingsValue | null;
|
|
331
|
+
}): Promise<void>;
|
|
332
|
+
/**
|
|
333
|
+
* The Usage tab: `reportAiUsage()` (#4751) grouped by model, tier, prompt
|
|
334
|
+
* key, fusion and/or template hash. Pass `by: ['model']` where tier and
|
|
335
|
+
* prompt tags do not exist yet; NULL dimensions are their own bucket,
|
|
336
|
+
* never dropped. `transcripts: true` adds per-bucket transcript counts
|
|
337
|
+
* (#4781; ids only, never content).
|
|
266
338
|
*/
|
|
267
339
|
usage(options?: {
|
|
268
|
-
by?:
|
|
340
|
+
by?: InferenceUsageReportDimension[];
|
|
269
341
|
since?: Date;
|
|
270
342
|
until?: Date;
|
|
271
343
|
tenantId?: string | null;
|
|
344
|
+
transcripts?: boolean;
|
|
272
345
|
}): Promise<AiUsageReport>;
|
|
273
346
|
/** Local discovery for the model field (#4674's loopback probe). */
|
|
274
347
|
discoverModels(): Promise<Array<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings-service.d.ts","sourceRoot":"","sources":["../../src/settings/settings-service.ts"],"names":[],"mappings":"AAYA,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,sBAAsB,
|
|
1
|
+
{"version":3,"file":"settings-service.d.ts","sourceRoot":"","sources":["../../src/settings/settings-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -11,11 +11,16 @@ export declare const INFERENCE_MODES: readonly ["byo", "local", "hosted", "off"]
|
|
|
11
11
|
export type InferenceMode = (typeof INFERENCE_MODES)[number];
|
|
12
12
|
/** Binding id of `auto`'s default tier. */
|
|
13
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";
|
|
14
19
|
/** A binding id stored in `_smrt_inference_overrides.binding`. */
|
|
15
|
-
export type InferenceBindingId = `tier:${InferenceTier}` | typeof DEFAULT_TIER_BINDING | `alias:${string}` | `fusion:${string}`;
|
|
20
|
+
export type InferenceBindingId = `tier:${InferenceTier}` | typeof DEFAULT_TIER_BINDING | typeof TRANSCRIPTS_BINDING | `alias:${string}` | `fusion:${string}`;
|
|
16
21
|
/** `inference.aliases` / fusion names: what a binding id may carry after its prefix. */
|
|
17
22
|
export declare const INFERENCE_BINDING_NAME_PATTERN: RegExp;
|
|
18
|
-
export type InferenceBindingKind = 'tier' | 'default-tier' | 'alias' | 'fusion';
|
|
23
|
+
export type InferenceBindingKind = 'tier' | 'default-tier' | 'transcripts' | 'alias' | 'fusion';
|
|
19
24
|
/** A parsed binding id. */
|
|
20
25
|
export interface ParsedInferenceBinding {
|
|
21
26
|
kind: InferenceBindingKind;
|
|
@@ -69,6 +74,20 @@ export declare function normalizeAliasDefinition(binding: string, value: unknown
|
|
|
69
74
|
export declare function normalizeFusionDefinition(binding: string, value: unknown): Record<string, unknown> | null;
|
|
70
75
|
/** `default-tier` stores `{ tier }`. */
|
|
71
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;
|
|
72
91
|
/**
|
|
73
92
|
* The allow-list reference of a tier target: `model:<provider>/<model>`
|
|
74
93
|
* (`model:default/<model>` without a provider), `alias:<name>` or
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"targets.d.ts","sourceRoot":"","sources":["../../src/settings/targets.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,
|
|
1
|
+
{"version":3,"file":"targets.d.ts","sourceRoot":"","sources":["../../src/settings/targets.ts"],"names":[],"mappings":"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"}
|
|
@@ -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"}
|
package/dist/smrt-knowledge.json
CHANGED
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-inference",
|
|
6
|
-
"packageVersion": "0.60.
|
|
6
|
+
"packageVersion": "0.60.16",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
12
|
-
"agents": "
|
|
10
|
+
"manifest": "54e44c7fcf61ccd93d4425a74df15c3be1e3b6df8463cfe3941669f5c0cfcbca",
|
|
11
|
+
"packageJson": "a3257e0445774817a9c973105afb20f77e561125e89770f1bf46ab107deeb06e",
|
|
12
|
+
"agents": "7ee5ec0379fbee95035e9809a03a625a8c7addd992629ad5dff83693eb7fb9a7",
|
|
13
13
|
"moduleDoc:agents/resolver.md": "71de7295b6988b27b3b27e2ae82fd71e525ef53613c7714a1deea6c45ba92ef6",
|
|
14
|
-
"moduleDoc:agents/hosted.md": "
|
|
15
|
-
"moduleDoc:agents/fusions.md": "
|
|
16
|
-
"moduleDoc:agents/settings.md": "
|
|
14
|
+
"moduleDoc:agents/hosted.md": "563141ef3ff09c453de8cfe99db3500da77fa28efe6bc3990e3f84a6cf32653d",
|
|
15
|
+
"moduleDoc:agents/fusions.md": "e1dc3f94fe20b1989c8d6e07dbe28e5bd72ae1d327bc3edbc9b8d4365d6dc231",
|
|
16
|
+
"moduleDoc:agents/settings.md": "1ba893ced5676ea41cd10f4454991db7de8c949d93d030ea1d1e78024e9f0cce"
|
|
17
17
|
},
|
|
18
18
|
"exports": [
|
|
19
19
|
".",
|
|
@@ -279,11 +279,17 @@
|
|
|
279
279
|
{
|
|
280
280
|
"filePath": "src/__tests__/fusion-integration.test.ts"
|
|
281
281
|
},
|
|
282
|
+
{
|
|
283
|
+
"filePath": "src/__tests__/issue-4800-panel-fusions.test.ts"
|
|
284
|
+
},
|
|
282
285
|
{
|
|
283
286
|
"filePath": "src/config.ts"
|
|
284
287
|
},
|
|
285
288
|
{
|
|
286
289
|
"filePath": "src/fusion.ts"
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
"filePath": "src/svelte/components/InferenceSettingsPanel.test.ts"
|
|
287
293
|
}
|
|
288
294
|
],
|
|
289
295
|
"relationshipsV2": {
|
|
@@ -294,7 +300,7 @@
|
|
|
294
300
|
"polymorphicAssociations": 0,
|
|
295
301
|
"uuidColumns": 4
|
|
296
302
|
},
|
|
297
|
-
"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'`).\nBudget update and key delete wait on happyvertical/sdk#1494 (feature-detected;\nrevoke ends local use first and records owed deletions). `./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 `inference.fusions` (wins) or\n`defineFusion()`. 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. 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",
|
|
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",
|
|
298
304
|
"moduleDocs": [
|
|
299
305
|
{
|
|
300
306
|
"path": "agents/resolver.md",
|
|
@@ -304,17 +310,17 @@
|
|
|
304
310
|
{
|
|
305
311
|
"path": "agents/hosted.md",
|
|
306
312
|
"module": "hosted",
|
|
307
|
-
"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`; never reimplement\n gateway administration here. The SDK has `createProject` and\n `createVirtualKey` only: `updateVirtualKey` / `deleteVirtualKey` are\n feature-detected on `HostedGatewayAdmin` (happyvertical/sdk#1494). Without\n them budget sync returns `unsupported` (state keeps the old budget so a\n later sync retries). Revoke ends local use before touching the gateway\n (config restored, state retired with the key owed, then the delete); a\n deletion the gateway cannot perform (`unsupported`,\n `failed`, or a key a failed provision left) goes to the state's\n `pendingGatewayRevocations` (id + alias, never the key), is audited with\n `manualGatewayRevocation`, and is retried by `revoke()` /\n `reconcileTenant()`.\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()`, LiteLLM id `smrt-<tenantId>-<nonce>`). 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"
|
|
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"
|
|
308
314
|
},
|
|
309
315
|
{
|
|
310
316
|
"path": "agents/fusions.md",
|
|
311
317
|
"module": "fusions",
|
|
312
|
-
"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 `inference.fusions.<key>` (app config,
|
|
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"
|
|
313
319
|
},
|
|
314
320
|
{
|
|
315
321
|
"path": "agents/settings.md",
|
|
316
322
|
"module": "settings",
|
|
317
|
-
"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). 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.\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\").
|
|
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"
|
|
318
324
|
}
|
|
319
325
|
],
|
|
320
326
|
"browser": {
|
|
@@ -5,10 +5,10 @@ description: "Inference alias chains with classified failures, recorded fallback
|
|
|
5
5
|
status: "stable"
|
|
6
6
|
generated:
|
|
7
7
|
by: "smrt/scripts/generate-knowledge-okf.ts"
|
|
8
|
-
at: "2026-11-
|
|
8
|
+
at: "2026-11-09T19:46:30Z"
|
|
9
9
|
audience: "internal"
|
|
10
10
|
evidence_level: "source-inspected"
|
|
11
|
-
source_revision: "sha256:
|
|
11
|
+
source_revision: "sha256:fb27bf4e97321e173bd1236d388442a1f292afec20e1d71b7075c6004b2d318b"
|
|
12
12
|
sources:
|
|
13
13
|
- id: "manifest"
|
|
14
14
|
resource: "https://github.com/happyvertical/smrt/blob/main/packages/inference/package.json"
|
|
@@ -53,8 +53,14 @@ function save(event: SubmitEvent, existing?: string): void {
|
|
|
53
53
|
<div class="ifl-head">
|
|
54
54
|
<strong>{fusion.name}</strong>
|
|
55
55
|
<span class="ifl-label">{fusion.source}</span>
|
|
56
|
+
{#if fusion.shadows?.length}<span class="ifl-label">replaces {fusion.shadows.join(', ')}</span>{/if}
|
|
56
57
|
{#if fusion.description}<span class="ifl-label">{fusion.description}</span>{/if}
|
|
57
58
|
</div>
|
|
59
|
+
{#if fusion.issues?.length}
|
|
60
|
+
<ul class="ifl-label" role="alert" data-testid="inference-fusion-issues">
|
|
61
|
+
{#each fusion.issues as issue (issue.path + issue.message)}<li>{issue.message}</li>{/each}
|
|
62
|
+
</ul>
|
|
63
|
+
{/if}
|
|
58
64
|
{#if editable && fusion.editable}
|
|
59
65
|
<form onsubmit={(event) => save(event, fusion.name)}>
|
|
60
66
|
<textarea
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InferenceFusionList.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/InferenceFusionList.svelte.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,OAAO,KAAK,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAG9E,UAAU,wBAAwB;IAChC,OAAO,EAAE,mBAAmB,EAAE,CAAC;IAC/B,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,qBAAqB,KAAK,IAAI,CAAC;CACxD;
|
|
1
|
+
{"version":3,"file":"InferenceFusionList.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/InferenceFusionList.svelte.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,OAAO,KAAK,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAG9E,UAAU,wBAAwB;IAChC,OAAO,EAAE,mBAAmB,EAAE,CAAC;IAC/B,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,qBAAqB,KAAK,IAAI,CAAC;CACxD;AAyFD,QAAA,MAAM,mBAAmB,8DAAwC,CAAC;AAClE,KAAK,mBAAmB,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAClE,eAAe,mBAAmB,CAAC"}
|