@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,141 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Fusions from the registry, config and app config entries (#4753 owns
|
|
4
|
+
* their meaning). Registry and config fusions are read-only; app entries
|
|
5
|
+
* are edited as JSON. There is deliberately no authoring form.
|
|
6
|
+
*/
|
|
7
|
+
import type { InferenceFusionChange, InferenceFusionView } from '../types.js';
|
|
8
|
+
|
|
9
|
+
interface InferenceFusionListProps {
|
|
10
|
+
fusions: InferenceFusionView[];
|
|
11
|
+
editable: boolean;
|
|
12
|
+
busy: boolean;
|
|
13
|
+
onSaveFusion?: (change: InferenceFusionChange) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { fusions, editable, busy, onSaveFusion }: InferenceFusionListProps =
|
|
17
|
+
$props();
|
|
18
|
+
|
|
19
|
+
let errors = $state<Record<string, string>>({});
|
|
20
|
+
|
|
21
|
+
function parse(name: string, raw: string): Record<string, unknown> | null {
|
|
22
|
+
try {
|
|
23
|
+
const value = JSON.parse(raw) as unknown;
|
|
24
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
25
|
+
errors = { ...errors, [name]: 'A fusion is a JSON object.' };
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
errors = { ...errors, [name]: '' };
|
|
29
|
+
return value as Record<string, unknown>;
|
|
30
|
+
} catch {
|
|
31
|
+
errors = { ...errors, [name]: 'Not valid JSON.' };
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function save(event: SubmitEvent, existing?: string): void {
|
|
37
|
+
event.preventDefault();
|
|
38
|
+
if (!onSaveFusion) return;
|
|
39
|
+
const fields = new FormData(event.currentTarget as HTMLFormElement);
|
|
40
|
+
const name = existing ?? String(fields.get('fusionName') ?? '').trim();
|
|
41
|
+
if (!name) return;
|
|
42
|
+
const definition = parse(
|
|
43
|
+
existing ?? '__new__',
|
|
44
|
+
String(fields.get('definition') ?? ''),
|
|
45
|
+
);
|
|
46
|
+
if (definition) onSaveFusion({ name, definition });
|
|
47
|
+
}
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<div class="ifl" data-testid="inference-fusions">
|
|
51
|
+
{#each fusions as fusion (fusion.name)}
|
|
52
|
+
<div class="ifl-item">
|
|
53
|
+
<div class="ifl-head">
|
|
54
|
+
<strong>{fusion.name}</strong>
|
|
55
|
+
<span class="ifl-label">{fusion.source}</span>
|
|
56
|
+
{#if fusion.shadows?.length}<span class="ifl-label">replaces {fusion.shadows.join(', ')}</span>{/if}
|
|
57
|
+
{#if fusion.description}<span class="ifl-label">{fusion.description}</span>{/if}
|
|
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}
|
|
64
|
+
{#if editable && fusion.editable}
|
|
65
|
+
<form onsubmit={(event) => save(event, fusion.name)}>
|
|
66
|
+
<textarea
|
|
67
|
+
name="definition"
|
|
68
|
+
aria-label={`${fusion.name} JSON`}
|
|
69
|
+
rows="6"
|
|
70
|
+
disabled={busy}
|
|
71
|
+
value={JSON.stringify(fusion.definition, null, 2)}
|
|
72
|
+
></textarea>
|
|
73
|
+
{#if errors[fusion.name]}<p class="ifl-label" role="alert">{errors[fusion.name]}</p>{/if}
|
|
74
|
+
<div class="ifl-actions">
|
|
75
|
+
<button type="submit" disabled={busy || !onSaveFusion}>Save</button>
|
|
76
|
+
<button
|
|
77
|
+
type="button"
|
|
78
|
+
disabled={busy || !onSaveFusion}
|
|
79
|
+
onclick={() => onSaveFusion?.({ name: fusion.name, definition: null })}
|
|
80
|
+
>
|
|
81
|
+
Remove
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
</form>
|
|
85
|
+
{:else}
|
|
86
|
+
<pre class="ifl-pre">{JSON.stringify(fusion.definition, null, 2)}</pre>
|
|
87
|
+
{/if}
|
|
88
|
+
</div>
|
|
89
|
+
{:else}
|
|
90
|
+
<p class="ifl-label">No fusions are defined.</p>
|
|
91
|
+
{/each}
|
|
92
|
+
{#if editable && onSaveFusion}
|
|
93
|
+
<form class="ifl-item" onsubmit={(event) => save(event)} data-testid="inference-fusion-new">
|
|
94
|
+
<label>
|
|
95
|
+
<span class="ifl-label">New config entry</span>
|
|
96
|
+
<input name="fusionName" required disabled={busy} />
|
|
97
|
+
</label>
|
|
98
|
+
<textarea name="definition" aria-label="New fusion JSON" rows="4" disabled={busy}>{'{}'}</textarea>
|
|
99
|
+
{#if errors.__new__}<p class="ifl-label" role="alert">{errors.__new__}</p>{/if}
|
|
100
|
+
<button type="submit" disabled={busy}>Add</button>
|
|
101
|
+
</form>
|
|
102
|
+
{/if}
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<style>
|
|
106
|
+
@layer smrt.reset, smrt.structure, smrt.theme;
|
|
107
|
+
@layer smrt.theme {
|
|
108
|
+
.ifl {
|
|
109
|
+
display: flex;
|
|
110
|
+
flex-direction: column;
|
|
111
|
+
gap: 0.75rem;
|
|
112
|
+
}
|
|
113
|
+
.ifl-item {
|
|
114
|
+
display: flex;
|
|
115
|
+
flex-direction: column;
|
|
116
|
+
gap: 0.375rem;
|
|
117
|
+
}
|
|
118
|
+
.ifl-head,
|
|
119
|
+
.ifl-actions {
|
|
120
|
+
display: flex;
|
|
121
|
+
gap: 0.5rem;
|
|
122
|
+
align-items: center;
|
|
123
|
+
}
|
|
124
|
+
.ifl-pre,
|
|
125
|
+
textarea {
|
|
126
|
+
margin: 0;
|
|
127
|
+
padding: 0.5rem;
|
|
128
|
+
border-radius: 0.375rem;
|
|
129
|
+
background: var(--smrt-color-surface-variant);
|
|
130
|
+
font-family: ui-monospace, monospace;
|
|
131
|
+
font-size: 0.8125rem;
|
|
132
|
+
white-space: pre-wrap;
|
|
133
|
+
width: 100%;
|
|
134
|
+
}
|
|
135
|
+
.ifl-label {
|
|
136
|
+
margin: 0;
|
|
137
|
+
font-size: 0.8125rem;
|
|
138
|
+
color: var(--smrt-color-on-surface-variant);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fusions from the registry, config and app config entries (#4753 owns
|
|
3
|
+
* their meaning). Registry and config fusions are read-only; app entries
|
|
4
|
+
* are edited as JSON. There is deliberately no authoring form.
|
|
5
|
+
*/
|
|
6
|
+
import type { InferenceFusionChange, InferenceFusionView } from '../types.js';
|
|
7
|
+
interface InferenceFusionListProps {
|
|
8
|
+
fusions: InferenceFusionView[];
|
|
9
|
+
editable: boolean;
|
|
10
|
+
busy: boolean;
|
|
11
|
+
onSaveFusion?: (change: InferenceFusionChange) => void;
|
|
12
|
+
}
|
|
13
|
+
declare const InferenceFusionList: import("svelte").Component<InferenceFusionListProps, {}, "">;
|
|
14
|
+
type InferenceFusionList = ReturnType<typeof InferenceFusionList>;
|
|
15
|
+
export default InferenceFusionList;
|
|
16
|
+
//# sourceMappingURL=InferenceFusionList.svelte.d.ts.map
|
|
@@ -0,0 +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;AAyFD,QAAA,MAAM,mBAAmB,8DAAwC,CAAC;AAClE,KAAK,mBAAmB,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAClE,eAAe,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* InferenceSettingsPanel — the shared inference settings surface (#4754).
|
|
4
|
+
*
|
|
5
|
+
* Purely presentational, like smrt-prompts' `PromptSettingsPanel`: rows and
|
|
6
|
+
* callbacks in, no fetching, no authorization decision. The host loads rows
|
|
7
|
+
* with `InferenceSettingsService` (`@happyvertical/smrt-inference/server`)
|
|
8
|
+
* and wires each callback to its own permission-checked server action.
|
|
9
|
+
*
|
|
10
|
+
* The default view is the whole screen for a one-provider app. Advanced
|
|
11
|
+
* (tier ladder, `auto`'s default tier, aliases, fusions, transcripts) is collapsed and
|
|
12
|
+
* not rendered until opened. Keys leave only through `onSaveKey`; the panel
|
|
13
|
+
* never receives one back (`defaults.keySet` says whether one is stored).
|
|
14
|
+
*/
|
|
15
|
+
import type {
|
|
16
|
+
InferenceAliasChange,
|
|
17
|
+
InferenceAliasView,
|
|
18
|
+
InferenceCandidateView,
|
|
19
|
+
InferenceConnectionRequest,
|
|
20
|
+
InferenceDefaultsChange,
|
|
21
|
+
InferenceDefaultsView,
|
|
22
|
+
InferenceDefaultTierChange,
|
|
23
|
+
InferenceDefaultTierView,
|
|
24
|
+
InferenceDiscoveryView,
|
|
25
|
+
InferenceFusionChange,
|
|
26
|
+
InferenceFusionView,
|
|
27
|
+
InferenceKeyChange,
|
|
28
|
+
InferenceProviderView,
|
|
29
|
+
InferenceSettingsScope,
|
|
30
|
+
InferenceSettingsTier,
|
|
31
|
+
InferenceTestResultView,
|
|
32
|
+
InferenceTierChange,
|
|
33
|
+
InferenceTierView,
|
|
34
|
+
InferenceTranscriptLinkFilters,
|
|
35
|
+
InferenceTranscriptSettingsChange,
|
|
36
|
+
InferenceTranscriptSettingsView,
|
|
37
|
+
InferenceUsageDimension,
|
|
38
|
+
InferenceUsageView,
|
|
39
|
+
} from '../types.js';
|
|
40
|
+
import {
|
|
41
|
+
defaultTierRevertPreview,
|
|
42
|
+
INFERENCE_SETTINGS_TIERS,
|
|
43
|
+
inferenceLevelLabel,
|
|
44
|
+
isRefAllowed,
|
|
45
|
+
} from '../types.js';
|
|
46
|
+
import InferenceAliasEditor from './InferenceAliasEditor.svelte';
|
|
47
|
+
import InferenceDefaultView from './InferenceDefaultView.svelte';
|
|
48
|
+
import InferenceFusionList from './InferenceFusionList.svelte';
|
|
49
|
+
import InferenceTierRow from './InferenceTierRow.svelte';
|
|
50
|
+
import InferenceTranscriptSettings from './InferenceTranscriptSettings.svelte';
|
|
51
|
+
import InferenceUsageTable from './InferenceUsageTable.svelte';
|
|
52
|
+
|
|
53
|
+
export interface InferenceSettingsPanelProps {
|
|
54
|
+
/** The current user (or tenant) choices for the default view. */
|
|
55
|
+
defaults?: InferenceDefaultsView;
|
|
56
|
+
/** Provider choices (`InferenceSettingsService.listProviderOptions()`). */
|
|
57
|
+
providers?: InferenceProviderView[];
|
|
58
|
+
/** Local discovery results, when the host ran the #4674 probe. */
|
|
59
|
+
discoveredModels?: InferenceDiscoveryView[];
|
|
60
|
+
/** Tier rows (`listInferenceSettings()`). */
|
|
61
|
+
tiers?: InferenceTierView[];
|
|
62
|
+
/** `auto`'s default tier (`getDefaultTierSetting()`). */
|
|
63
|
+
defaultTier?: InferenceDefaultTierView | null;
|
|
64
|
+
/** Aliases (`listInferenceAliases()`). */
|
|
65
|
+
aliases?: InferenceAliasView[];
|
|
66
|
+
/** Fusions (`listInferenceFusions()`); render-only dependency on #4753. */
|
|
67
|
+
fusions?: InferenceFusionView[];
|
|
68
|
+
/** Usage report (`usage()`); the Usage tab shows when this or `onUsageGroupBy` is set. */
|
|
69
|
+
usage?: InferenceUsageView | null;
|
|
70
|
+
/**
|
|
71
|
+
* The app's transcript setting (`getTranscriptSettings()`, #4781), shown
|
|
72
|
+
* in Advanced and, when off, as a status line in the Usage tab.
|
|
73
|
+
*/
|
|
74
|
+
transcripts?: InferenceTranscriptSettingsView | null;
|
|
75
|
+
/**
|
|
76
|
+
* The host is a platform admin that may change app-level settings
|
|
77
|
+
* (#4781). Only then is the transcript setting editable (with
|
|
78
|
+
* `onSaveTranscripts`); tenants cannot override it. Default `false`.
|
|
79
|
+
*/
|
|
80
|
+
appEditable?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* The host's transcript route for a Usage-tab row (#4781). Rows with
|
|
83
|
+
* stored transcripts link their count and outcome labels there; the panel
|
|
84
|
+
* renders no transcript content.
|
|
85
|
+
*/
|
|
86
|
+
transcriptHref?: (
|
|
87
|
+
filters: InferenceTranscriptLinkFilters,
|
|
88
|
+
) => string | null | undefined;
|
|
89
|
+
/**
|
|
90
|
+
* Which scope Advanced edits write. `tenant` restricts choices to the
|
|
91
|
+
* allow-list (display only; the server enforces it) and makes aliases and
|
|
92
|
+
* fusions read-only (they are app-scope definitions).
|
|
93
|
+
*/
|
|
94
|
+
scope?: InferenceSettingsScope;
|
|
95
|
+
/** Open Advanced on first render. Default `false`. */
|
|
96
|
+
advancedOpen?: boolean;
|
|
97
|
+
/** Disable every control while a save is in flight. */
|
|
98
|
+
busy?: boolean;
|
|
99
|
+
message?: string | null;
|
|
100
|
+
error?: string | null;
|
|
101
|
+
/**
|
|
102
|
+
* Identity of what is being edited (tenant or user id). Unsaved edits are
|
|
103
|
+
* discarded whenever it changes, or, when omitted, whenever `tiers` or
|
|
104
|
+
* `defaults` is replaced (a reloaded server load).
|
|
105
|
+
*/
|
|
106
|
+
contextKey?: unknown;
|
|
107
|
+
onSaveDefaults?: (change: InferenceDefaultsChange) => void;
|
|
108
|
+
/** The only way a key leaves the panel. The host stores it through smrt-secrets. */
|
|
109
|
+
onSaveKey?: (change: InferenceKeyChange) => void | Promise<void>;
|
|
110
|
+
onTestConnection?: (
|
|
111
|
+
request: InferenceConnectionRequest,
|
|
112
|
+
) => Promise<InferenceTestResultView>;
|
|
113
|
+
onDiscoverModels?: () => void;
|
|
114
|
+
onSaveTier?: (change: InferenceTierChange) => void;
|
|
115
|
+
onSaveDefaultTier?: (change: InferenceDefaultTierChange) => void;
|
|
116
|
+
onSaveAlias?: (change: InferenceAliasChange) => void;
|
|
117
|
+
onTestCandidate?: (
|
|
118
|
+
candidate: InferenceCandidateView,
|
|
119
|
+
) => Promise<InferenceTestResultView>;
|
|
120
|
+
onSaveFusion?: (change: InferenceFusionChange) => void;
|
|
121
|
+
onUsageGroupBy?: (by: InferenceUsageDimension[]) => void;
|
|
122
|
+
/** Writes the app row (never a tenant row). Used only with `appEditable`. */
|
|
123
|
+
onSaveTranscripts?: (change: InferenceTranscriptSettingsChange) => void;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const {
|
|
127
|
+
defaults = {},
|
|
128
|
+
providers = [],
|
|
129
|
+
discoveredModels = [],
|
|
130
|
+
tiers = [],
|
|
131
|
+
defaultTier = null,
|
|
132
|
+
aliases = [],
|
|
133
|
+
fusions = [],
|
|
134
|
+
usage = null,
|
|
135
|
+
transcripts = null,
|
|
136
|
+
appEditable = false,
|
|
137
|
+
transcriptHref,
|
|
138
|
+
scope = 'app',
|
|
139
|
+
advancedOpen = false,
|
|
140
|
+
busy = false,
|
|
141
|
+
message = null,
|
|
142
|
+
error = null,
|
|
143
|
+
contextKey,
|
|
144
|
+
onSaveDefaults,
|
|
145
|
+
onSaveKey,
|
|
146
|
+
onTestConnection,
|
|
147
|
+
onDiscoverModels,
|
|
148
|
+
onSaveTier,
|
|
149
|
+
onSaveDefaultTier,
|
|
150
|
+
onSaveAlias,
|
|
151
|
+
onTestCandidate,
|
|
152
|
+
onSaveFusion,
|
|
153
|
+
onUsageGroupBy,
|
|
154
|
+
onSaveTranscripts,
|
|
155
|
+
}: InferenceSettingsPanelProps = $props();
|
|
156
|
+
|
|
157
|
+
let tab = $state<'settings' | 'usage'>('settings');
|
|
158
|
+
// svelte-ignore state_referenced_locally
|
|
159
|
+
let advanced = $state(advancedOpen);
|
|
160
|
+
|
|
161
|
+
const showUsage = $derived(usage !== null || onUsageGroupBy !== undefined);
|
|
162
|
+
const definitionsEditable = $derived(scope === 'app');
|
|
163
|
+
// Remount on a context change so unsaved edits never cross tenants/users.
|
|
164
|
+
const defaultsKey = $derived(contextKey ?? defaults);
|
|
165
|
+
const advancedKey = $derived(contextKey ?? tiers);
|
|
166
|
+
|
|
167
|
+
function saveDefaultTier(event: SubmitEvent): void {
|
|
168
|
+
event.preventDefault();
|
|
169
|
+
if (!onSaveDefaultTier) return;
|
|
170
|
+
const value = String(
|
|
171
|
+
new FormData(event.currentTarget as HTMLFormElement).get('autoTier') ?? '',
|
|
172
|
+
);
|
|
173
|
+
onSaveDefaultTier({
|
|
174
|
+
scope,
|
|
175
|
+
tier: value === '' ? null : (value as InferenceSettingsTier),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function tierAllowed(tier: InferenceSettingsTier): boolean {
|
|
180
|
+
return (
|
|
181
|
+
scope !== 'tenant' ||
|
|
182
|
+
isRefAllowed(`tier:${tier}`, defaultTier?.tenantAllowList)
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
</script>
|
|
186
|
+
|
|
187
|
+
<section class="isp" data-testid="inference-settings-panel" data-smrt-component="InferenceSettingsPanel">
|
|
188
|
+
{#if message}<p class="isp-flash" role="status">{message}</p>{/if}
|
|
189
|
+
{#if error}<p class="isp-flash isp-flash-error" role="alert">{error}</p>{/if}
|
|
190
|
+
|
|
191
|
+
{#if showUsage}
|
|
192
|
+
<div class="isp-tabs" role="tablist">
|
|
193
|
+
<button
|
|
194
|
+
type="button"
|
|
195
|
+
role="tab"
|
|
196
|
+
aria-selected={tab === 'settings'}
|
|
197
|
+
onclick={() => (tab = 'settings')}
|
|
198
|
+
>
|
|
199
|
+
Settings
|
|
200
|
+
</button>
|
|
201
|
+
<button
|
|
202
|
+
type="button"
|
|
203
|
+
role="tab"
|
|
204
|
+
aria-selected={tab === 'usage'}
|
|
205
|
+
onclick={() => (tab = 'usage')}
|
|
206
|
+
>
|
|
207
|
+
Usage
|
|
208
|
+
</button>
|
|
209
|
+
</div>
|
|
210
|
+
{/if}
|
|
211
|
+
|
|
212
|
+
{#if tab === 'usage' && showUsage}
|
|
213
|
+
<div role="tabpanel">
|
|
214
|
+
<InferenceUsageTable
|
|
215
|
+
{usage}
|
|
216
|
+
{busy}
|
|
217
|
+
transcriptsEnabled={transcripts ? transcripts.enabled : null}
|
|
218
|
+
{transcriptHref}
|
|
219
|
+
{onUsageGroupBy}
|
|
220
|
+
/>
|
|
221
|
+
</div>
|
|
222
|
+
{:else}
|
|
223
|
+
<div role={showUsage ? 'tabpanel' : undefined} class="isp-settings">
|
|
224
|
+
{#key defaultsKey}
|
|
225
|
+
<InferenceDefaultView
|
|
226
|
+
{defaults}
|
|
227
|
+
{providers}
|
|
228
|
+
{discoveredModels}
|
|
229
|
+
{busy}
|
|
230
|
+
{onSaveDefaults}
|
|
231
|
+
{onSaveKey}
|
|
232
|
+
{onTestConnection}
|
|
233
|
+
{onDiscoverModels}
|
|
234
|
+
/>
|
|
235
|
+
{/key}
|
|
236
|
+
|
|
237
|
+
<button
|
|
238
|
+
type="button"
|
|
239
|
+
class="isp-advanced-toggle"
|
|
240
|
+
aria-expanded={advanced}
|
|
241
|
+
aria-controls="isp-advanced"
|
|
242
|
+
onclick={() => (advanced = !advanced)}
|
|
243
|
+
>
|
|
244
|
+
Advanced
|
|
245
|
+
</button>
|
|
246
|
+
|
|
247
|
+
{#if advanced}
|
|
248
|
+
<div id="isp-advanced" class="isp-advanced" data-testid="inference-advanced">
|
|
249
|
+
{#key advancedKey}
|
|
250
|
+
<section class="isp-section">
|
|
251
|
+
<h3>Tier ladder</h3>
|
|
252
|
+
{#each tiers as row (row.tier)}
|
|
253
|
+
<InferenceTierRow
|
|
254
|
+
{row}
|
|
255
|
+
{scope}
|
|
256
|
+
{providers}
|
|
257
|
+
{aliases}
|
|
258
|
+
{fusions}
|
|
259
|
+
{busy}
|
|
260
|
+
{onSaveTier}
|
|
261
|
+
/>
|
|
262
|
+
{:else}
|
|
263
|
+
<p class="isp-hint">No tier rows were provided.</p>
|
|
264
|
+
{/each}
|
|
265
|
+
</section>
|
|
266
|
+
|
|
267
|
+
{#if defaultTier}
|
|
268
|
+
<section class="isp-section">
|
|
269
|
+
<h3><code>auto</code> default tier</h3>
|
|
270
|
+
<form class="isp-auto" onsubmit={saveDefaultTier} data-testid="inference-auto-tier">
|
|
271
|
+
<span>
|
|
272
|
+
Now {defaultTier.effectiveTier} ({inferenceLevelLabel(defaultTier.supplyingLevel)})
|
|
273
|
+
</span>
|
|
274
|
+
<label>
|
|
275
|
+
<span class="isp-hint">auto resolves to</span>
|
|
276
|
+
<select
|
|
277
|
+
name="autoTier"
|
|
278
|
+
disabled={busy}
|
|
279
|
+
value={(scope === 'app' ? defaultTier.appTier : defaultTier.tenantTier) ?? ''}
|
|
280
|
+
>
|
|
281
|
+
<option value="">Inherit ({defaultTierRevertPreview(defaultTier, scope)})</option>
|
|
282
|
+
{#each INFERENCE_SETTINGS_TIERS as tier (tier)}
|
|
283
|
+
<option value={tier} disabled={!tierAllowed(tier)}>{tier}</option>
|
|
284
|
+
{/each}
|
|
285
|
+
</select>
|
|
286
|
+
</label>
|
|
287
|
+
<button type="submit" disabled={busy || !onSaveDefaultTier}>Save</button>
|
|
288
|
+
</form>
|
|
289
|
+
</section>
|
|
290
|
+
{/if}
|
|
291
|
+
|
|
292
|
+
<section class="isp-section">
|
|
293
|
+
<h3>Aliases</h3>
|
|
294
|
+
{#each aliases as alias (alias.name)}
|
|
295
|
+
<InferenceAliasEditor
|
|
296
|
+
{alias}
|
|
297
|
+
editable={definitionsEditable && !!onSaveAlias}
|
|
298
|
+
{busy}
|
|
299
|
+
{onSaveAlias}
|
|
300
|
+
{onTestCandidate}
|
|
301
|
+
/>
|
|
302
|
+
{:else}
|
|
303
|
+
<p class="isp-hint">No aliases are defined.</p>
|
|
304
|
+
{/each}
|
|
305
|
+
{#if definitionsEditable && onSaveAlias}
|
|
306
|
+
<InferenceAliasEditor
|
|
307
|
+
alias={{ name: '', candidates: [] }}
|
|
308
|
+
editable
|
|
309
|
+
isNew
|
|
310
|
+
{busy}
|
|
311
|
+
{onSaveAlias}
|
|
312
|
+
{onTestCandidate}
|
|
313
|
+
/>
|
|
314
|
+
{/if}
|
|
315
|
+
</section>
|
|
316
|
+
|
|
317
|
+
<section class="isp-section">
|
|
318
|
+
<h3>Fusions</h3>
|
|
319
|
+
<InferenceFusionList
|
|
320
|
+
{fusions}
|
|
321
|
+
editable={definitionsEditable}
|
|
322
|
+
{busy}
|
|
323
|
+
{onSaveFusion}
|
|
324
|
+
/>
|
|
325
|
+
</section>
|
|
326
|
+
|
|
327
|
+
{#if transcripts}
|
|
328
|
+
<section class="isp-section">
|
|
329
|
+
<h3>Transcripts</h3>
|
|
330
|
+
<InferenceTranscriptSettings
|
|
331
|
+
settings={transcripts}
|
|
332
|
+
editable={appEditable && !!onSaveTranscripts}
|
|
333
|
+
{busy}
|
|
334
|
+
{onSaveTranscripts}
|
|
335
|
+
/>
|
|
336
|
+
</section>
|
|
337
|
+
{/if}
|
|
338
|
+
{/key}
|
|
339
|
+
</div>
|
|
340
|
+
{/if}
|
|
341
|
+
</div>
|
|
342
|
+
{/if}
|
|
343
|
+
</section>
|
|
344
|
+
|
|
345
|
+
<style>
|
|
346
|
+
@layer smrt.reset, smrt.structure, smrt.theme;
|
|
347
|
+
@layer smrt.theme {
|
|
348
|
+
.isp,
|
|
349
|
+
.isp-settings,
|
|
350
|
+
.isp-advanced {
|
|
351
|
+
display: flex;
|
|
352
|
+
flex-direction: column;
|
|
353
|
+
gap: 1rem;
|
|
354
|
+
}
|
|
355
|
+
.isp-tabs {
|
|
356
|
+
display: flex;
|
|
357
|
+
gap: 0.5rem;
|
|
358
|
+
border-bottom: var(--smrt-hairline);
|
|
359
|
+
}
|
|
360
|
+
.isp-tabs [aria-selected='true'] {
|
|
361
|
+
font-weight: 600;
|
|
362
|
+
}
|
|
363
|
+
.isp-flash {
|
|
364
|
+
margin: 0;
|
|
365
|
+
padding: 0.625rem 0.75rem;
|
|
366
|
+
border-radius: 0.375rem;
|
|
367
|
+
background: var(--smrt-color-surface-variant);
|
|
368
|
+
}
|
|
369
|
+
.isp-flash-error {
|
|
370
|
+
color: var(--smrt-color-error);
|
|
371
|
+
background: var(--smrt-color-error-container);
|
|
372
|
+
}
|
|
373
|
+
.isp-advanced-toggle {
|
|
374
|
+
align-self: flex-start;
|
|
375
|
+
}
|
|
376
|
+
.isp-section h3 {
|
|
377
|
+
margin: 0 0 0.5rem;
|
|
378
|
+
font-size: 1rem;
|
|
379
|
+
}
|
|
380
|
+
.isp-auto {
|
|
381
|
+
display: flex;
|
|
382
|
+
gap: 0.75rem;
|
|
383
|
+
align-items: flex-end;
|
|
384
|
+
flex-wrap: wrap;
|
|
385
|
+
}
|
|
386
|
+
.isp-hint {
|
|
387
|
+
margin: 0;
|
|
388
|
+
font-size: 0.8125rem;
|
|
389
|
+
color: var(--smrt-color-on-surface-variant);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
</style>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InferenceSettingsPanel — the shared inference settings surface (#4754).
|
|
3
|
+
*
|
|
4
|
+
* Purely presentational, like smrt-prompts' `PromptSettingsPanel`: rows and
|
|
5
|
+
* callbacks in, no fetching, no authorization decision. The host loads rows
|
|
6
|
+
* with `InferenceSettingsService` (`@happyvertical/smrt-inference/server`)
|
|
7
|
+
* and wires each callback to its own permission-checked server action.
|
|
8
|
+
*
|
|
9
|
+
* The default view is the whole screen for a one-provider app. Advanced
|
|
10
|
+
* (tier ladder, `auto`'s default tier, aliases, fusions, transcripts) is collapsed and
|
|
11
|
+
* not rendered until opened. Keys leave only through `onSaveKey`; the panel
|
|
12
|
+
* never receives one back (`defaults.keySet` says whether one is stored).
|
|
13
|
+
*/
|
|
14
|
+
import type { InferenceAliasChange, InferenceAliasView, InferenceCandidateView, InferenceConnectionRequest, InferenceDefaultsChange, InferenceDefaultsView, InferenceDefaultTierChange, InferenceDefaultTierView, InferenceDiscoveryView, InferenceFusionChange, InferenceFusionView, InferenceKeyChange, InferenceProviderView, InferenceSettingsScope, InferenceTestResultView, InferenceTierChange, InferenceTierView, InferenceTranscriptLinkFilters, InferenceTranscriptSettingsChange, InferenceTranscriptSettingsView, InferenceUsageDimension, InferenceUsageView } from '../types.js';
|
|
15
|
+
export interface InferenceSettingsPanelProps {
|
|
16
|
+
/** The current user (or tenant) choices for the default view. */
|
|
17
|
+
defaults?: InferenceDefaultsView;
|
|
18
|
+
/** Provider choices (`InferenceSettingsService.listProviderOptions()`). */
|
|
19
|
+
providers?: InferenceProviderView[];
|
|
20
|
+
/** Local discovery results, when the host ran the #4674 probe. */
|
|
21
|
+
discoveredModels?: InferenceDiscoveryView[];
|
|
22
|
+
/** Tier rows (`listInferenceSettings()`). */
|
|
23
|
+
tiers?: InferenceTierView[];
|
|
24
|
+
/** `auto`'s default tier (`getDefaultTierSetting()`). */
|
|
25
|
+
defaultTier?: InferenceDefaultTierView | null;
|
|
26
|
+
/** Aliases (`listInferenceAliases()`). */
|
|
27
|
+
aliases?: InferenceAliasView[];
|
|
28
|
+
/** Fusions (`listInferenceFusions()`); render-only dependency on #4753. */
|
|
29
|
+
fusions?: InferenceFusionView[];
|
|
30
|
+
/** Usage report (`usage()`); the Usage tab shows when this or `onUsageGroupBy` is set. */
|
|
31
|
+
usage?: InferenceUsageView | null;
|
|
32
|
+
/**
|
|
33
|
+
* The app's transcript setting (`getTranscriptSettings()`, #4781), shown
|
|
34
|
+
* in Advanced and, when off, as a status line in the Usage tab.
|
|
35
|
+
*/
|
|
36
|
+
transcripts?: InferenceTranscriptSettingsView | null;
|
|
37
|
+
/**
|
|
38
|
+
* The host is a platform admin that may change app-level settings
|
|
39
|
+
* (#4781). Only then is the transcript setting editable (with
|
|
40
|
+
* `onSaveTranscripts`); tenants cannot override it. Default `false`.
|
|
41
|
+
*/
|
|
42
|
+
appEditable?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* The host's transcript route for a Usage-tab row (#4781). Rows with
|
|
45
|
+
* stored transcripts link their count and outcome labels there; the panel
|
|
46
|
+
* renders no transcript content.
|
|
47
|
+
*/
|
|
48
|
+
transcriptHref?: (filters: InferenceTranscriptLinkFilters) => string | null | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Which scope Advanced edits write. `tenant` restricts choices to the
|
|
51
|
+
* allow-list (display only; the server enforces it) and makes aliases and
|
|
52
|
+
* fusions read-only (they are app-scope definitions).
|
|
53
|
+
*/
|
|
54
|
+
scope?: InferenceSettingsScope;
|
|
55
|
+
/** Open Advanced on first render. Default `false`. */
|
|
56
|
+
advancedOpen?: boolean;
|
|
57
|
+
/** Disable every control while a save is in flight. */
|
|
58
|
+
busy?: boolean;
|
|
59
|
+
message?: string | null;
|
|
60
|
+
error?: string | null;
|
|
61
|
+
/**
|
|
62
|
+
* Identity of what is being edited (tenant or user id). Unsaved edits are
|
|
63
|
+
* discarded whenever it changes, or, when omitted, whenever `tiers` or
|
|
64
|
+
* `defaults` is replaced (a reloaded server load).
|
|
65
|
+
*/
|
|
66
|
+
contextKey?: unknown;
|
|
67
|
+
onSaveDefaults?: (change: InferenceDefaultsChange) => void;
|
|
68
|
+
/** The only way a key leaves the panel. The host stores it through smrt-secrets. */
|
|
69
|
+
onSaveKey?: (change: InferenceKeyChange) => void | Promise<void>;
|
|
70
|
+
onTestConnection?: (request: InferenceConnectionRequest) => Promise<InferenceTestResultView>;
|
|
71
|
+
onDiscoverModels?: () => void;
|
|
72
|
+
onSaveTier?: (change: InferenceTierChange) => void;
|
|
73
|
+
onSaveDefaultTier?: (change: InferenceDefaultTierChange) => void;
|
|
74
|
+
onSaveAlias?: (change: InferenceAliasChange) => void;
|
|
75
|
+
onTestCandidate?: (candidate: InferenceCandidateView) => Promise<InferenceTestResultView>;
|
|
76
|
+
onSaveFusion?: (change: InferenceFusionChange) => void;
|
|
77
|
+
onUsageGroupBy?: (by: InferenceUsageDimension[]) => void;
|
|
78
|
+
/** Writes the app row (never a tenant row). Used only with `appEditable`. */
|
|
79
|
+
onSaveTranscripts?: (change: InferenceTranscriptSettingsChange) => void;
|
|
80
|
+
}
|
|
81
|
+
declare const InferenceSettingsPanel: import("svelte").Component<InferenceSettingsPanelProps, {}, "">;
|
|
82
|
+
type InferenceSettingsPanel = ReturnType<typeof InferenceSettingsPanel>;
|
|
83
|
+
export default InferenceSettingsPanel;
|
|
84
|
+
//# sourceMappingURL=InferenceSettingsPanel.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InferenceSettingsPanel.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/InferenceSettingsPanel.svelte.ts"],"names":[],"mappings":"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"}
|