@gajae-code/coding-agent 0.12.21 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +67 -2
- package/dist/types/cli/args.d.ts +5 -2
- package/dist/types/cli/root-flags.d.ts +246 -0
- package/dist/types/cli.d.ts +11 -19
- package/dist/types/commands/daemon.d.ts +0 -30
- package/dist/types/commands/launch.d.ts +11 -19
- package/dist/types/commands/quick-lane.d.ts +32 -0
- package/dist/types/commands/sdk.d.ts +21 -15
- package/dist/types/config/file-lock-gc.d.ts +2 -0
- package/dist/types/config/model-equivalence.d.ts +20 -0
- package/dist/types/config/model-profile-activation.d.ts +62 -17
- package/dist/types/config/model-profiles.d.ts +10 -0
- package/dist/types/config/model-registry.d.ts +68 -1
- package/dist/types/config/model-resolver.d.ts +15 -4
- package/dist/types/config/models-config-schema.d.ts +7 -2
- package/dist/types/config/provider-selection-policy.d.ts +62 -0
- package/dist/types/config/settings-schema.d.ts +36 -78
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/edit/renderer.d.ts +5 -2
- package/dist/types/edit/streaming.d.ts +13 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
- package/dist/types/extensibility/extensions/compact-handler.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +19 -5
- package/dist/types/internal-urls/local-protocol.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +0 -2
- package/dist/types/modes/components/provider-order-context.d.ts +45 -0
- package/dist/types/modes/components/provider-order-editor.d.ts +24 -0
- package/dist/types/modes/components/settings-defs.d.ts +5 -1
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/quick-lane/classify.d.ts +40 -0
- package/dist/types/runtime/stt-settings-setup.d.ts +18 -0
- package/dist/types/sdk/broker/broker.d.ts +56 -2
- package/dist/types/sdk/broker/operation-receipt-ledger.d.ts +25 -0
- package/dist/types/sdk/broker/session-index.d.ts +83 -2
- package/dist/types/sdk/broker/state-version.d.ts +1 -1
- package/dist/types/sdk/bus/notification-service.d.ts +6 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +8 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +26 -0
- package/dist/types/sdk/cli/rows.d.ts +98 -0
- package/dist/types/sdk/cli/session-cli.d.ts +35 -3
- package/dist/types/sdk/elevation/allowlist.d.ts +20 -0
- package/dist/types/sdk/elevation/audit.d.ts +93 -0
- package/dist/types/sdk/elevation/capability.d.ts +3 -0
- package/dist/types/sdk/elevation/digest.d.ts +72 -0
- package/dist/types/sdk/elevation/dispatch-receipt.d.ts +85 -0
- package/dist/types/sdk/elevation/grant-ledger.d.ts +163 -0
- package/dist/types/sdk/elevation/owner.d.ts +48 -0
- package/dist/types/sdk/guides/cache.d.ts +98 -0
- package/dist/types/sdk/guides/catalog.d.ts +156 -0
- package/dist/types/sdk/guides/cli.d.ts +39 -0
- package/dist/types/sdk/guides/manifest.d.ts +66 -0
- package/dist/types/sdk/guides/verify.d.ts +66 -0
- package/dist/types/sdk/host/control/dispatch.d.ts +8 -0
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +3 -2
- package/dist/types/sdk/host/query/cursor.d.ts +10 -0
- package/dist/types/sdk/host/query/handlers.d.ts +21 -0
- package/dist/types/sdk/host/session-runtime.d.ts +9 -0
- package/dist/types/sdk/mcp/server.d.ts +2 -2
- package/dist/types/sdk/prompt-status.d.ts +11 -0
- package/dist/types/sdk/session.d.ts +4 -0
- package/dist/types/sdk/transport/index.d.ts +1 -0
- package/dist/types/sdk/transport/relay.d.ts +1 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +20 -1
- package/dist/types/session/agent-session.d.ts +32 -18
- package/dist/types/session/blob-store.d.ts +13 -7
- package/dist/types/session/fallback-chain-controller.d.ts +15 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +25 -1
- package/dist/types/session/internal/session-memory-sidecar.d.ts +5 -5
- package/dist/types/session/session-manager.d.ts +139 -10
- package/dist/types/session/session-storage.d.ts +48 -0
- package/dist/types/setup/provider-onboarding.d.ts +7 -1
- package/dist/types/slash-commands/builtin-registry.d.ts +14 -0
- package/dist/types/stt/setup.d.ts +2 -0
- package/dist/types/task/executor.d.ts +10 -2
- package/dist/types/tools/browser/runtime-diagnostics.d.ts +65 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +4 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker.d.ts +2 -0
- package/dist/types/tools/browser.d.ts +2 -0
- package/dist/types/tools/image-gen.d.ts +14 -18
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/resource-gc.d.ts +2 -0
- package/dist/types/utils/pasted-image-path.d.ts +6 -0
- package/package.json +9 -9
- package/scripts/generate-sdk-operation-inventory.ts +8 -0
- package/scripts/run-test-manifest.ts +61 -21
- package/scripts/verify-gjc-sdk-canonicalization.ts +9 -4
- package/src/cli/args.ts +62 -9
- package/src/cli/completion-cli.ts +1 -0
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/root-flags.ts +100 -0
- package/src/cli/setup-cli.ts +6 -2
- package/src/cli.ts +42 -65
- package/src/commands/acp.ts +1 -1
- package/src/commands/daemon.ts +2 -31
- package/src/commands/gc.ts +1 -1
- package/src/commands/launch.ts +8 -129
- package/src/commands/quick-lane.ts +51 -0
- package/src/commands/sdk.ts +200 -26
- package/src/commands/setup.ts +4 -2
- package/src/config/file-lock-gc.ts +4 -1
- package/src/config/model-equivalence.ts +76 -7
- package/src/config/model-profile-activation.ts +1115 -265
- package/src/config/model-profiles.ts +134 -2
- package/src/config/model-registry.ts +352 -50
- package/src/config/model-resolver.ts +289 -38
- package/src/config/models-config-schema.ts +11 -4
- package/src/config/provider-selection-policy.ts +146 -0
- package/src/config/settings-schema.ts +50 -77
- package/src/config/settings.ts +40 -0
- package/src/coordinator-mcp/server.ts +32 -2
- package/src/edit/renderer.ts +175 -223
- package/src/edit/streaming.ts +108 -1
- package/src/export/html/index.ts +57 -7
- package/src/extensibility/custom-tools/types.ts +2 -0
- package/src/extensibility/extensions/compact-handler.ts +3 -2
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/gjc-runtime/gc-runtime.ts +379 -20
- package/src/gjc-runtime/launch-worktree.ts +5 -1
- package/src/gjc-runtime/team-runtime.ts +59 -2
- package/src/gjc-runtime/tmux-owner-isolation.ts +19 -0
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +16 -1
- package/src/internal-urls/docs-index.generated.ts +17 -13
- package/src/internal-urls/local-protocol.ts +1 -0
- package/src/main.ts +40 -3
- package/src/memories/index.ts +4 -2
- package/src/modes/acp/acp-agent.ts +235 -91
- package/src/modes/components/model-selector.ts +206 -42
- package/src/modes/components/provider-order-context.ts +197 -0
- package/src/modes/components/provider-order-editor.ts +441 -0
- package/src/modes/components/settings-defs.ts +21 -1
- package/src/modes/components/settings-selector.ts +45 -2
- package/src/modes/components/tool-execution.ts +47 -15
- package/src/modes/controllers/command-controller.ts +6 -3
- package/src/modes/controllers/extension-ui-controller.ts +2 -0
- package/src/modes/controllers/input-controller.ts +73 -2
- package/src/modes/controllers/selector-controller.ts +260 -153
- package/src/modes/prompt-suggestion-controller.ts +1 -1
- package/src/modes/runtime-init.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +18 -0
- package/src/prompts/tools/browser.md +1 -0
- package/src/quick-lane/classify.ts +257 -0
- package/src/rlm/index.ts +1 -1
- package/src/runtime/stt-settings-setup.ts +57 -0
- package/src/sdk/acp/adapter.ts +2 -1
- package/src/sdk/broker/broker.ts +780 -12
- package/src/sdk/broker/lifecycle.ts +70 -29
- package/src/sdk/broker/operation-receipt-ledger.ts +144 -0
- package/src/sdk/broker/session-index.ts +463 -48
- package/src/sdk/broker/state-version.ts +13 -9
- package/src/sdk/broker/transport.ts +14 -1
- package/src/sdk/bus/index.ts +34 -13
- package/src/sdk/bus/kind-aware-reconciliation.ts +21 -2
- package/src/sdk/bus/notification-service.ts +47 -1
- package/src/sdk/bus/prompt-reconciliation.ts +22 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
- package/src/sdk/bus/telegram-daemon.ts +54 -3
- package/src/sdk/cli/rows.ts +247 -0
- package/src/sdk/cli/session-cli.ts +930 -79
- package/src/sdk/elevation/allowlist.ts +54 -0
- package/src/sdk/elevation/audit.ts +208 -0
- package/src/sdk/elevation/capability.ts +41 -0
- package/src/sdk/elevation/digest.ts +129 -0
- package/src/sdk/elevation/dispatch-receipt.ts +184 -0
- package/src/sdk/elevation/grant-ledger.ts +1142 -0
- package/src/sdk/elevation/owner.ts +109 -0
- package/src/sdk/guides/cache.ts +363 -0
- package/src/sdk/guides/catalog.ts +554 -0
- package/src/sdk/guides/cli.ts +235 -0
- package/src/sdk/guides/manifest.ts +152 -0
- package/src/sdk/guides/verify.ts +178 -0
- package/src/sdk/host/control/dispatch.ts +49 -1
- package/src/sdk/host/control/index.ts +1 -0
- package/src/sdk/host/control/operations.ts +8 -1
- package/src/sdk/host/query/cursor.ts +47 -3
- package/src/sdk/host/query/handlers.ts +155 -2
- package/src/sdk/host/sdk-surface-parity.test.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +391 -1
- package/src/sdk/host/session-runtime.ts +308 -73
- package/src/sdk/host/surface-policy.ts +3 -0
- package/src/sdk/host/websocket-transport.ts +7 -1
- package/src/sdk/mcp/server.ts +88 -15
- package/src/sdk/prompt-status.ts +8 -0
- package/src/sdk/protocol/operation-inventory.generated.json +62 -0
- package/src/sdk/protocol/operation-registry.ts +3 -2
- package/src/sdk/session.ts +87 -39
- package/src/sdk/transport/index.ts +1 -0
- package/src/sdk/transport/relay.ts +3 -0
- package/src/sdk/transport/serve-cli.ts +111 -24
- package/src/secrets/obfuscator.ts +92 -12
- package/src/session/agent-session.ts +322 -93
- package/src/session/blob-store.ts +60 -8
- package/src/session/fallback-chain-controller.ts +35 -0
- package/src/session/internal/managed-session-scope.ts +25 -3
- package/src/session/internal/managed-session-storage.ts +574 -73
- package/src/session/internal/session-memory-sidecar.ts +19 -29
- package/src/session/session-manager.ts +2166 -743
- package/src/session/session-storage.ts +715 -48
- package/src/setup/provider-onboarding.ts +19 -3
- package/src/setup/provider-presets.json +3 -1
- package/src/slash-commands/builtin-registry.ts +85 -3
- package/src/stt/downloader.ts +2 -3
- package/src/stt/setup.ts +29 -4
- package/src/task/executor.ts +22 -18
- package/src/task/index.ts +24 -1
- package/src/tools/archive-reader.ts +11 -2
- package/src/tools/browser/runtime-diagnostics.ts +249 -0
- package/src/tools/browser/tab-protocol.ts +4 -0
- package/src/tools/browser/tab-supervisor.ts +5 -0
- package/src/tools/browser/tab-worker.ts +36 -1
- package/src/tools/browser.ts +7 -0
- package/src/tools/image-gen.ts +110 -166
- package/src/tools/index.ts +2 -0
- package/src/tools/read.ts +1 -1
- package/src/tools/resource-gc.ts +6 -1
- package/src/tools/tool-catalog.generated.ts +5 -1
- package/src/utils/pasted-image-path.ts +93 -0
- package/src/web/search/index.ts +3 -3
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { ThinkingLevel } from "@gajae-code/agent-core";
|
|
2
2
|
import type { Api, Model } from "@gajae-code/ai/core";
|
|
3
|
-
import type { AgentSession } from "../session/agent-session";
|
|
4
|
-
import { formatClampedModelSelector } from "../thinking";
|
|
3
|
+
import type { AgentSession, DefaultFallbackRuntimeState } from "../session/agent-session";
|
|
4
|
+
import { clampExplicitThinkingLevelForModel, formatClampedModelSelector } from "../thinking";
|
|
5
5
|
import { validateModelProfileName } from "./model-profile-contract";
|
|
6
6
|
import {
|
|
7
7
|
aggregateModelProfileRequiredProviders,
|
|
8
8
|
formatModelProfileDisplayLabel,
|
|
9
|
+
PROXY_ROUTABLE_PROVIDER_IDS,
|
|
9
10
|
resolveProfileBindings,
|
|
10
11
|
} from "./model-profiles";
|
|
11
12
|
|
|
@@ -20,9 +21,11 @@ import {
|
|
|
20
21
|
} from "./model-registry";
|
|
21
22
|
import {
|
|
22
23
|
formatModelSelectorValue,
|
|
24
|
+
formatModelString,
|
|
23
25
|
parseModelString,
|
|
24
26
|
resolveModelChainWithAuth,
|
|
25
27
|
resolveModelRoleValue,
|
|
28
|
+
splitSelectorThinkingSuffix,
|
|
26
29
|
} from "./model-resolver";
|
|
27
30
|
import { type ModelSelectorValue, normalizeModelSelectorValue } from "./model-selector-value";
|
|
28
31
|
import type { Settings } from "./settings";
|
|
@@ -31,6 +34,7 @@ type ModelProfileActivationSession = Pick<
|
|
|
31
34
|
AgentSession,
|
|
32
35
|
"model" | "thinkingLevel" | "sessionId" | "getConfiguredModelChain" | "setConfiguredModelChain"
|
|
33
36
|
> & {
|
|
37
|
+
credentialSessionId?: string;
|
|
34
38
|
setModelTemporary?: AgentSession["setModelTemporary"];
|
|
35
39
|
setActiveModelProfile?: (name: string | undefined) => void;
|
|
36
40
|
getActiveModelProfile?: () => string | undefined;
|
|
@@ -45,8 +49,30 @@ type ModelProfileActivationSession = Pick<
|
|
|
45
49
|
/** Current profile-installed override keys, for deriving the activation base. */
|
|
46
50
|
getProfileInstalledOverrideKeys?: () => { modelRoles: readonly string[]; agentModelOverrides: readonly string[] };
|
|
47
51
|
getSessionDefaultModelSelector?: () => string | undefined;
|
|
48
|
-
recordResumeDefaultModel?: (selector: string) => void;
|
|
52
|
+
recordResumeDefaultModel?: (selector: string | undefined) => void;
|
|
49
53
|
seedDefaultFallbackResolution?: (activeIndex: number, skips: Array<{ selector: string; reason: string }>) => void;
|
|
54
|
+
getDefaultFallbackRuntimeState?: () => DefaultFallbackRuntimeState;
|
|
55
|
+
restoreDefaultFallbackRuntimeState?: (state: DefaultFallbackRuntimeState) => void;
|
|
56
|
+
restoreModelSelectionForRollback?: AgentSession["restoreModelSelectionForRollback"];
|
|
57
|
+
modelRegistry?: Pick<
|
|
58
|
+
ModelRegistry,
|
|
59
|
+
| "getSessionCanonicalVariant"
|
|
60
|
+
| "restoreSessionCanonicalVariant"
|
|
61
|
+
| "clearCanonicalVariant"
|
|
62
|
+
| "getAvailable"
|
|
63
|
+
| "lookupAliasExists"
|
|
64
|
+
| "resolveModelByLookupAlias"
|
|
65
|
+
| "authStorage"
|
|
66
|
+
| "isCredentiallessProvider"
|
|
67
|
+
>;
|
|
68
|
+
getConfiguredModelChainState?: (role: string) => ConfiguredModelChainState | undefined;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
type ConfiguredModelChainState = {
|
|
72
|
+
entries: readonly string[];
|
|
73
|
+
origin: string;
|
|
74
|
+
identity?: string;
|
|
75
|
+
explicitHead: boolean;
|
|
50
76
|
};
|
|
51
77
|
|
|
52
78
|
export interface PrepareModelProfileActivationOptions {
|
|
@@ -61,8 +87,23 @@ export interface PrepareModelProfileActivationOptions {
|
|
|
61
87
|
| "resolveCanonicalModel"
|
|
62
88
|
| "getCanonicalVariants"
|
|
63
89
|
| "getCanonicalId"
|
|
64
|
-
> &
|
|
65
|
-
|
|
90
|
+
> &
|
|
91
|
+
Partial<
|
|
92
|
+
Pick<
|
|
93
|
+
ModelRegistry,
|
|
94
|
+
| "getAvailable"
|
|
95
|
+
| "resolveModelByLookupAlias"
|
|
96
|
+
| "lookupAliasExists"
|
|
97
|
+
| "clearCanonicalVariant"
|
|
98
|
+
| "seedCanonicalVariant"
|
|
99
|
+
| "getSessionCanonicalVariant"
|
|
100
|
+
| "restoreSessionCanonicalVariant"
|
|
101
|
+
| "getConfiguredProviderIds"
|
|
102
|
+
>
|
|
103
|
+
> & {
|
|
104
|
+
getError?: ModelRegistry["getError"];
|
|
105
|
+
};
|
|
106
|
+
settings: Pick<Settings, "get" | "getGlobal" | "getOverride">;
|
|
66
107
|
profileName: string;
|
|
67
108
|
}
|
|
68
109
|
export interface ApplyModelProfileActivationOptions {
|
|
@@ -72,14 +113,27 @@ export interface ApplyModelProfileActivationOptions {
|
|
|
72
113
|
export interface PreparedModelProfileActivation {
|
|
73
114
|
profileName: string;
|
|
74
115
|
session: ModelProfileActivationSession & { setModelTemporary: AgentSession["setModelTemporary"] };
|
|
75
|
-
settings: Pick<
|
|
116
|
+
settings: Pick<
|
|
117
|
+
Settings,
|
|
118
|
+
"clearOverride" | "get" | "getGlobal" | "getOverride" | "override" | "set" | "unset" | "flush" | "flushOrThrow"
|
|
119
|
+
>;
|
|
76
120
|
previousModel: Model<Api> | undefined;
|
|
77
121
|
previousThinkingLevel: ThinkingLevel | undefined;
|
|
78
122
|
previousAgentModelOverrides: Record<string, ModelSelectorValue>;
|
|
79
123
|
previousModelRoles: Record<string, ModelSelectorValue>;
|
|
124
|
+
previousPersistedModelRoles: Record<string, ModelSelectorValue> | undefined;
|
|
125
|
+
previousPersistedAgentModelOverrides: Record<string, ModelSelectorValue> | undefined;
|
|
126
|
+
previousModelRolesOverride: Record<string, ModelSelectorValue> | undefined;
|
|
127
|
+
previousAgentModelOverridesOverride: Record<string, ModelSelectorValue> | undefined;
|
|
128
|
+
previousDefaultProfileOverride: string | undefined;
|
|
129
|
+
previousPersistedDefaultProfile: string | undefined;
|
|
130
|
+
previousPersistedDefaultThinkingLevel: Exclude<ThinkingLevel, "inherit"> | undefined;
|
|
131
|
+
previousDefaultThinkingLevelOverride: ThinkingLevel | undefined;
|
|
80
132
|
baseAgentModelOverrides: Record<string, ModelSelectorValue>;
|
|
81
133
|
baseModelRoles: Record<string, ModelSelectorValue>;
|
|
82
134
|
previousDefaultChain: readonly string[] | undefined;
|
|
135
|
+
previousDefaultChainState: ConfiguredModelChainState | undefined;
|
|
136
|
+
previousDefaultFallbackRuntimeState: DefaultFallbackRuntimeState | undefined;
|
|
83
137
|
defaultModel: Model<Api> | undefined;
|
|
84
138
|
defaultThinkingLevel: ThinkingLevel | undefined;
|
|
85
139
|
/** Full configured default fallback chain with resolvable entries clamped. */
|
|
@@ -99,13 +153,36 @@ export interface PreparedModelProfileActivation {
|
|
|
99
153
|
* resume default without promoting a transient model to it.
|
|
100
154
|
*/
|
|
101
155
|
previousSessionDefaultModel: string | undefined;
|
|
156
|
+
/**
|
|
157
|
+
* Exact concrete sticky selector ("provider/id") snapshotted from the
|
|
158
|
+
* registry BEFORE the session sticky canonical variant is invalidated during
|
|
159
|
+
* prepare. Captured verbatim — never re-derived from the live model — so a
|
|
160
|
+
* transient live-model switch cannot corrupt the restored sticky. Rollback
|
|
161
|
+
* restores this exact selector when present; otherwise the sticky is cleared
|
|
162
|
+
* so a stale provider cannot silently resurrect.
|
|
163
|
+
*/
|
|
164
|
+
previousCanonicalVariant: string | undefined;
|
|
165
|
+
/** Registry used to resolve and restore the session sticky canonical variant. */
|
|
166
|
+
modelRegistry: PrepareModelProfileActivationOptions["modelRegistry"];
|
|
102
167
|
}
|
|
103
168
|
export interface MaterializeModelProfileAssignmentOptions {
|
|
104
169
|
session: Pick<
|
|
105
170
|
ModelProfileActivationSession,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
171
|
+
| "model"
|
|
172
|
+
| "thinkingLevel"
|
|
173
|
+
| "sessionId"
|
|
174
|
+
| "credentialSessionId"
|
|
175
|
+
| "getConfiguredModelChain"
|
|
176
|
+
| "getConfiguredModelChainState"
|
|
177
|
+
| "setConfiguredModelChain"
|
|
178
|
+
| "setActiveModelProfile"
|
|
179
|
+
| "getActiveModelProfile"
|
|
180
|
+
| "getDefaultFallbackRuntimeState"
|
|
181
|
+
| "restoreDefaultFallbackRuntimeState"
|
|
182
|
+
| "modelRegistry"
|
|
183
|
+
| "clearProfileInstalledOverrides"
|
|
184
|
+
>;
|
|
185
|
+
settings: Pick<Settings, "clearOverride" | "get" | "getGlobal" | "getOverride" | "override" | "set" | "unset">;
|
|
109
186
|
role: GjcModelAssignmentTargetId;
|
|
110
187
|
selector: string;
|
|
111
188
|
}
|
|
@@ -113,9 +190,21 @@ export interface MaterializeModelProfileAssignmentOptions {
|
|
|
113
190
|
export interface MaterializeModelProfileAssignmentsOptions {
|
|
114
191
|
session: Pick<
|
|
115
192
|
ModelProfileActivationSession,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
193
|
+
| "model"
|
|
194
|
+
| "thinkingLevel"
|
|
195
|
+
| "sessionId"
|
|
196
|
+
| "credentialSessionId"
|
|
197
|
+
| "getConfiguredModelChain"
|
|
198
|
+
| "getConfiguredModelChainState"
|
|
199
|
+
| "setConfiguredModelChain"
|
|
200
|
+
| "setActiveModelProfile"
|
|
201
|
+
| "getActiveModelProfile"
|
|
202
|
+
| "getDefaultFallbackRuntimeState"
|
|
203
|
+
| "restoreDefaultFallbackRuntimeState"
|
|
204
|
+
| "modelRegistry"
|
|
205
|
+
| "clearProfileInstalledOverrides"
|
|
206
|
+
>;
|
|
207
|
+
settings: Pick<Settings, "clearOverride" | "get" | "getGlobal" | "getOverride" | "override" | "set" | "unset">;
|
|
119
208
|
assignments: ReadonlyMap<GjcModelAssignmentTargetId, string> | Partial<Record<GjcModelAssignmentTargetId, string>>;
|
|
120
209
|
}
|
|
121
210
|
|
|
@@ -141,30 +230,160 @@ function getMaterializedAssignments(
|
|
|
141
230
|
function materializeConfiguredDefaultChain(
|
|
142
231
|
session: Pick<ModelProfileActivationSession, "model" | "thinkingLevel" | "getConfiguredModelChain">,
|
|
143
232
|
): ModelSelectorValue | undefined {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
? undefined
|
|
148
|
-
: configuredChain.length === 1
|
|
149
|
-
? configuredChain[0]
|
|
150
|
-
: [...configuredChain];
|
|
151
|
-
}
|
|
233
|
+
if (!session.model) return undefined;
|
|
234
|
+
return formatModelSelectorValue(`${session.model.provider}/${session.model.id}`, session.thinkingLevel);
|
|
235
|
+
}
|
|
152
236
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
237
|
+
function concretizeMaterializedAssignmentValues(
|
|
238
|
+
options: MaterializeModelProfileAssignmentOptions | MaterializeModelProfileAssignmentsOptions,
|
|
239
|
+
assignments: Record<string, ModelSelectorValue>,
|
|
240
|
+
): Record<string, ModelSelectorValue> {
|
|
241
|
+
const modelRegistry = options.session.modelRegistry;
|
|
242
|
+
const sessionId = (options.session as { sessionId?: string }).sessionId;
|
|
243
|
+
const credentialSessionId = options.session.credentialSessionId ?? sessionId;
|
|
244
|
+
if (!modelRegistry || !sessionId) return assignments;
|
|
245
|
+
const availableModels = modelRegistry.getAvailable();
|
|
246
|
+
const authenticatedModels = availableModels.filter(model => {
|
|
247
|
+
const isCredentiallessProvider = modelRegistry.isCredentiallessProvider?.bind(modelRegistry);
|
|
248
|
+
const hasUsableAuth = modelRegistry.authStorage?.hasUsableAuth?.bind(modelRegistry.authStorage);
|
|
249
|
+
if (!isCredentiallessProvider || !hasUsableAuth) return true;
|
|
250
|
+
return isCredentiallessProvider(model.provider) || hasUsableAuth(model.provider);
|
|
251
|
+
});
|
|
252
|
+
return Object.fromEntries(
|
|
253
|
+
Object.entries(assignments).map(([role, selectorValue]) => {
|
|
254
|
+
const concrete = normalizeModelSelectorValue(selectorValue)
|
|
255
|
+
.map(selector => {
|
|
256
|
+
if (splitSelectorThinkingSuffix(selector).selector.includes("/")) return selector;
|
|
257
|
+
const resolved = resolveModelRoleValue(selector, authenticatedModels, {
|
|
258
|
+
settings: options.settings as Settings,
|
|
259
|
+
modelRegistry,
|
|
260
|
+
sessionId,
|
|
261
|
+
credentialSessionId,
|
|
262
|
+
aliasIntent: "preset-equivalent",
|
|
263
|
+
});
|
|
264
|
+
if (!resolved.model) return undefined;
|
|
265
|
+
const concreteSelector = `${resolved.model.provider}/${resolved.model.id}`;
|
|
266
|
+
return resolved.explicitThinkingLevel && resolved.thinkingLevel
|
|
267
|
+
? formatModelSelectorValue(concreteSelector, resolved.thinkingLevel)
|
|
268
|
+
: concreteSelector;
|
|
269
|
+
})
|
|
270
|
+
.filter((selector): selector is string => selector !== undefined);
|
|
271
|
+
if (concrete.length === 0) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
`Active model profile assignment ${role} could not be concretized: ${normalizeModelSelectorValue(selectorValue).join(", ")}`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
return [role, concrete.length === 1 && typeof selectorValue === "string" ? concrete[0] : concrete];
|
|
277
|
+
}),
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function commitMaterializedProfileAssignments(
|
|
282
|
+
options: MaterializeModelProfileAssignmentOptions | MaterializeModelProfileAssignmentsOptions,
|
|
283
|
+
modelRoles: Record<string, ModelSelectorValue>,
|
|
284
|
+
agentModelOverrides: Record<string, ModelSelectorValue>,
|
|
285
|
+
): boolean {
|
|
286
|
+
const previousPersistedModelRoles = options.settings.getGlobal("modelRoles");
|
|
287
|
+
const previousPersistedAgentModelOverrides = options.settings.getGlobal("task.agentModelOverrides");
|
|
288
|
+
const previousPersistedDefaultProfile = options.settings.getGlobal("modelProfile.default");
|
|
289
|
+
const previousModelRolesOverride = options.settings.getOverride("modelRoles");
|
|
290
|
+
const previousAgentModelOverridesOverride = options.settings.getOverride("task.agentModelOverrides");
|
|
291
|
+
const previousDefaultProfileOverride = options.settings.getOverride("modelProfile.default");
|
|
292
|
+
const previousActiveProfile = options.session.getActiveModelProfile?.();
|
|
293
|
+
const previousChain = options.session.getConfiguredModelChainState?.("default");
|
|
294
|
+
const previousFallbackRuntimeState = options.session.getDefaultFallbackRuntimeState?.();
|
|
295
|
+
const previousCanonicalVariant = options.session.modelRegistry?.getSessionCanonicalVariant?.(
|
|
296
|
+
(options.session as { sessionId?: string }).sessionId ?? "",
|
|
156
297
|
);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
298
|
+
try {
|
|
299
|
+
const nextModelRoles = concretizeMaterializedAssignmentValues(options, modelRoles);
|
|
300
|
+
const nextAgentModelOverrides = concretizeMaterializedAssignmentValues(options, agentModelOverrides);
|
|
301
|
+
options.settings.set("modelRoles", nextModelRoles);
|
|
302
|
+
options.settings.set("task.agentModelOverrides", nextAgentModelOverrides);
|
|
303
|
+
options.settings.unset("modelProfile.default");
|
|
304
|
+
options.settings.clearOverride("modelProfile.default");
|
|
305
|
+
options.settings.override("modelRoles", nextModelRoles);
|
|
306
|
+
options.settings.override("task.agentModelOverrides", nextAgentModelOverrides);
|
|
307
|
+
options.session.setConfiguredModelChain(
|
|
308
|
+
"default",
|
|
309
|
+
normalizeModelSelectorValue(nextModelRoles.default),
|
|
310
|
+
"modelRoles",
|
|
311
|
+
undefined,
|
|
312
|
+
true,
|
|
313
|
+
);
|
|
314
|
+
options.session.setActiveModelProfile?.(undefined);
|
|
315
|
+
return true;
|
|
316
|
+
} catch (error) {
|
|
317
|
+
const rollbackErrors: unknown[] = [];
|
|
318
|
+
const restore = (action: () => void): void => {
|
|
319
|
+
try {
|
|
320
|
+
action();
|
|
321
|
+
} catch (rollbackError) {
|
|
322
|
+
rollbackErrors.push(rollbackError);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
restore(() =>
|
|
326
|
+
options.session.setConfiguredModelChain(
|
|
327
|
+
"default",
|
|
328
|
+
previousChain?.entries ?? [],
|
|
329
|
+
previousChain?.origin ?? "rollback",
|
|
330
|
+
previousChain?.identity,
|
|
331
|
+
previousChain?.explicitHead ?? true,
|
|
332
|
+
),
|
|
333
|
+
);
|
|
334
|
+
if (previousFallbackRuntimeState) {
|
|
335
|
+
restore(() => options.session.restoreDefaultFallbackRuntimeState?.(previousFallbackRuntimeState));
|
|
336
|
+
}
|
|
337
|
+
if (options.session.modelRegistry) {
|
|
338
|
+
restore(() => {
|
|
339
|
+
const sessionId = (options.session as { sessionId?: string }).sessionId;
|
|
340
|
+
if (!sessionId) return;
|
|
341
|
+
if (previousCanonicalVariant) {
|
|
342
|
+
options.session.modelRegistry?.restoreSessionCanonicalVariant?.(sessionId, previousCanonicalVariant);
|
|
343
|
+
} else {
|
|
344
|
+
options.session.modelRegistry?.clearCanonicalVariant?.(sessionId);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
restore(() =>
|
|
349
|
+
previousPersistedModelRoles === undefined
|
|
350
|
+
? options.settings.unset("modelRoles")
|
|
351
|
+
: options.settings.set("modelRoles", previousPersistedModelRoles),
|
|
352
|
+
);
|
|
353
|
+
restore(() =>
|
|
354
|
+
previousPersistedAgentModelOverrides === undefined
|
|
355
|
+
? options.settings.unset("task.agentModelOverrides")
|
|
356
|
+
: options.settings.set("task.agentModelOverrides", previousPersistedAgentModelOverrides),
|
|
357
|
+
);
|
|
358
|
+
restore(() =>
|
|
359
|
+
previousPersistedDefaultProfile === undefined
|
|
360
|
+
? options.settings.unset("modelProfile.default")
|
|
361
|
+
: options.settings.set("modelProfile.default", previousPersistedDefaultProfile),
|
|
362
|
+
);
|
|
363
|
+
restore(() =>
|
|
364
|
+
previousModelRolesOverride === undefined
|
|
365
|
+
? options.settings.clearOverride("modelRoles")
|
|
366
|
+
: options.settings.override("modelRoles", previousModelRolesOverride),
|
|
367
|
+
);
|
|
368
|
+
restore(() =>
|
|
369
|
+
previousAgentModelOverridesOverride === undefined
|
|
370
|
+
? options.settings.clearOverride("task.agentModelOverrides")
|
|
371
|
+
: options.settings.override("task.agentModelOverrides", previousAgentModelOverridesOverride),
|
|
372
|
+
);
|
|
373
|
+
restore(() =>
|
|
374
|
+
previousDefaultProfileOverride === undefined
|
|
375
|
+
? options.settings.clearOverride("modelProfile.default")
|
|
376
|
+
: options.settings.override("modelProfile.default", previousDefaultProfileOverride),
|
|
377
|
+
);
|
|
378
|
+
restore(() => options.session.setActiveModelProfile?.(previousActiveProfile));
|
|
379
|
+
if (rollbackErrors.length > 0) {
|
|
380
|
+
throw new AggregateError(
|
|
381
|
+
[error, ...rollbackErrors],
|
|
382
|
+
"Failed to materialize model profile and fully restore prior state",
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
168
387
|
}
|
|
169
388
|
|
|
170
389
|
export function materializeActiveModelProfileAssignment(options: MaterializeModelProfileAssignmentOptions): boolean {
|
|
@@ -188,15 +407,9 @@ export function materializeActiveModelProfileAssignment(options: MaterializeMode
|
|
|
188
407
|
nextAgentModelOverrides[options.role] = options.selector;
|
|
189
408
|
}
|
|
190
409
|
|
|
191
|
-
options
|
|
192
|
-
options.
|
|
193
|
-
|
|
194
|
-
options.settings.clearOverride("modelProfile.default");
|
|
195
|
-
options.settings.override("modelRoles", nextModelRoles);
|
|
196
|
-
options.settings.override("task.agentModelOverrides", nextAgentModelOverrides);
|
|
197
|
-
options.session.setActiveModelProfile?.(undefined);
|
|
198
|
-
options.session.clearProfileInstalledOverrides?.();
|
|
199
|
-
return true;
|
|
410
|
+
const committed = commitMaterializedProfileAssignments(options, nextModelRoles, nextAgentModelOverrides);
|
|
411
|
+
if (committed) options.session.clearProfileInstalledOverrides?.();
|
|
412
|
+
return committed;
|
|
200
413
|
}
|
|
201
414
|
|
|
202
415
|
export function materializeActiveModelProfileAssignments(options: MaterializeModelProfileAssignmentsOptions): boolean {
|
|
@@ -224,27 +437,23 @@ export function materializeActiveModelProfileAssignments(options: MaterializeMod
|
|
|
224
437
|
}
|
|
225
438
|
}
|
|
226
439
|
|
|
227
|
-
options
|
|
228
|
-
options.
|
|
229
|
-
|
|
230
|
-
options.settings.clearOverride("modelProfile.default");
|
|
231
|
-
options.settings.override("modelRoles", nextModelRoles);
|
|
232
|
-
options.settings.override("task.agentModelOverrides", nextAgentModelOverrides);
|
|
233
|
-
options.session.setActiveModelProfile?.(undefined);
|
|
234
|
-
options.session.clearProfileInstalledOverrides?.();
|
|
235
|
-
return true;
|
|
440
|
+
const committed = commitMaterializedProfileAssignments(options, nextModelRoles, nextAgentModelOverrides);
|
|
441
|
+
if (committed) options.session.clearProfileInstalledOverrides?.();
|
|
442
|
+
return committed;
|
|
236
443
|
}
|
|
237
444
|
|
|
238
445
|
export class ModelProfileCredentialError extends Error {
|
|
239
446
|
readonly code = "authentication_failed";
|
|
240
447
|
readonly profileLabel: string;
|
|
241
448
|
readonly providers: readonly string[];
|
|
449
|
+
readonly role: string | undefined;
|
|
242
450
|
|
|
243
|
-
constructor(profileLabel: string, providers: readonly string[]) {
|
|
451
|
+
constructor(profileLabel: string, providers: readonly string[], role?: string) {
|
|
244
452
|
super(formatModelProfileCredentialError(profileLabel, providers));
|
|
245
453
|
this.name = "ModelProfileCredentialError";
|
|
246
454
|
this.profileLabel = profileLabel;
|
|
247
455
|
this.providers = [...providers];
|
|
456
|
+
this.role = role;
|
|
248
457
|
}
|
|
249
458
|
}
|
|
250
459
|
|
|
@@ -319,39 +528,336 @@ function rewriteBindingsProviders(
|
|
|
319
528
|
),
|
|
320
529
|
};
|
|
321
530
|
}
|
|
531
|
+
/**
|
|
532
|
+
* Resolve the explicitly configured OpenAI-compatible proxy provider id for a
|
|
533
|
+
* preset. Returns undefined when unset or empty. Passwords/labels are never
|
|
534
|
+
* treated as proxy ids here; only lowercase provider ids from settings.
|
|
535
|
+
*/
|
|
536
|
+
function resolveProxyProviderId(settings: Pick<Settings, "get" | "getGlobal" | "getOverride">): string | undefined {
|
|
537
|
+
const value = settings.get("modelProfile.proxyProvider");
|
|
538
|
+
if (typeof value !== "string" || value.trim() === "") return undefined;
|
|
539
|
+
const id = value.trim().toLowerCase();
|
|
540
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/.test(id)) {
|
|
541
|
+
throw new Error(
|
|
542
|
+
`modelProfile.proxyProvider must be a lowercase provider id (got "${value.trim()}"). Configure an OpenAI-compatible proxy with \`gjc setup provider\`, then set its id here.`,
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
return id;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function resolveProxyMode(settings: Pick<Settings, "get" | "getGlobal" | "getOverride">): "fallback" | "always" {
|
|
549
|
+
const value = settings.get("modelProfile.proxyMode");
|
|
550
|
+
if (value === undefined || value === "fallback" || value === "always") return value ?? "fallback";
|
|
551
|
+
throw new Error(`modelProfile.proxyMode must be "fallback" or "always" (got "${String(value)}")`);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Rewrite a qualified provider selector so its model is served through the
|
|
556
|
+
* configured proxy. The proxy catalog uses sub-provider-prefixed model ids
|
|
557
|
+
* (e.g. `xiaomi/mimo-v2.5-pro` under `litellm`). Exact provider-prefixed
|
|
558
|
+
* matches win; a unique final-segment match is accepted for gateways that do
|
|
559
|
+
* not retain the upstream provider prefix. Missing or ambiguous matches fail
|
|
560
|
+
* closed rather than leaving an unauthenticated role selector behind.
|
|
561
|
+
*/
|
|
562
|
+
function rewriteSelectorForProxy(
|
|
563
|
+
selector: string,
|
|
564
|
+
proxyProvider: string,
|
|
565
|
+
proxyMode: "fallback" | "always",
|
|
566
|
+
allModels: Model<Api>[],
|
|
567
|
+
directlyAuthenticated: ReadonlySet<string>,
|
|
568
|
+
): string {
|
|
569
|
+
const suffix = splitSelectorThinkingSuffix(selector);
|
|
570
|
+
const baseSelector = suffix.selector;
|
|
571
|
+
const slash = baseSelector.indexOf("/");
|
|
572
|
+
const proxyModels = allModels.filter(model => model.provider === proxyProvider);
|
|
573
|
+
if (slash < 0) {
|
|
574
|
+
if (proxyMode === "fallback") return selector;
|
|
575
|
+
const matches = proxyModels.filter(model => model.id === baseSelector);
|
|
576
|
+
if (matches.length !== 1) {
|
|
577
|
+
throw new Error(
|
|
578
|
+
`Configured proxy "${proxyProvider}" does not expose an unambiguous model for "${baseSelector}"`,
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
const rewritten = `${proxyProvider}/${matches[0]!.id}`;
|
|
582
|
+
return suffix.thinkingLevel ? formatModelSelectorValue(rewritten, suffix.thinkingLevel) : rewritten;
|
|
583
|
+
}
|
|
584
|
+
const directProvider = baseSelector.substring(0, slash);
|
|
585
|
+
if (proxyMode === "fallback" && directlyAuthenticated.has(directProvider)) return selector;
|
|
586
|
+
if (!PROXY_ROUTABLE_PROVIDER_IDS.has(directProvider)) return selector;
|
|
587
|
+
if (proxyProvider === directProvider) {
|
|
588
|
+
throw new Error(`Configured proxy "${proxyProvider}" cannot route its own direct selector "${baseSelector}"`);
|
|
589
|
+
}
|
|
590
|
+
const directModelId = baseSelector.substring(slash + 1);
|
|
591
|
+
const exactMatches = proxyModels.filter(model => model.id === `${directProvider}/${directModelId}`);
|
|
592
|
+
const flatMatches = proxyModels.filter(model => model.id === directModelId);
|
|
593
|
+
const matches = exactMatches.length > 0 ? exactMatches : flatMatches;
|
|
594
|
+
if (matches.length === 0) {
|
|
595
|
+
throw new Error(`Configured proxy "${proxyProvider}" does not expose a model for "${baseSelector}"`);
|
|
596
|
+
}
|
|
597
|
+
if (matches.length > 1) {
|
|
598
|
+
throw new Error(`Configured proxy "${proxyProvider}" has ambiguous models for "${baseSelector}"`);
|
|
599
|
+
}
|
|
600
|
+
const rewritten = `${proxyProvider}/${matches[0]!.id}`;
|
|
601
|
+
return suffix.thinkingLevel ? formatModelSelectorValue(rewritten, suffix.thinkingLevel) : rewritten;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function rewriteSelectorValueForProxy(
|
|
605
|
+
selectorValue: ModelSelectorValue,
|
|
606
|
+
proxyProvider: string,
|
|
607
|
+
proxyMode: "fallback" | "always",
|
|
608
|
+
allModels: Model<Api>[],
|
|
609
|
+
directlyAuthenticated: ReadonlySet<string>,
|
|
610
|
+
): ModelSelectorValue {
|
|
611
|
+
const selectors = normalizeModelSelectorValue(selectorValue).map(selector =>
|
|
612
|
+
rewriteSelectorForProxy(selector, proxyProvider, proxyMode, allModels, directlyAuthenticated),
|
|
613
|
+
);
|
|
614
|
+
return selectors.length === 1 && typeof selectorValue === "string" ? selectors[0] : selectors;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function rewriteBindingsForProxy(
|
|
618
|
+
bindings: {
|
|
619
|
+
defaultSelector?: ModelSelectorValue;
|
|
620
|
+
modelRoles: Record<string, ModelSelectorValue>;
|
|
621
|
+
agentModelOverrides: Record<string, ModelSelectorValue>;
|
|
622
|
+
},
|
|
623
|
+
proxyProvider: string,
|
|
624
|
+
proxyMode: "fallback" | "always",
|
|
625
|
+
allModels: Model<Api>[],
|
|
626
|
+
directlyAuthenticated: ReadonlySet<string>,
|
|
627
|
+
): {
|
|
628
|
+
defaultSelector?: ModelSelectorValue;
|
|
629
|
+
modelRoles: Record<string, ModelSelectorValue>;
|
|
630
|
+
agentModelOverrides: Record<string, ModelSelectorValue>;
|
|
631
|
+
} {
|
|
632
|
+
return {
|
|
633
|
+
defaultSelector: bindings.defaultSelector
|
|
634
|
+
? rewriteSelectorValueForProxy(
|
|
635
|
+
bindings.defaultSelector,
|
|
636
|
+
proxyProvider,
|
|
637
|
+
proxyMode,
|
|
638
|
+
allModels,
|
|
639
|
+
directlyAuthenticated,
|
|
640
|
+
)
|
|
641
|
+
: undefined,
|
|
642
|
+
modelRoles: Object.fromEntries(
|
|
643
|
+
Object.entries(bindings.modelRoles).map(([role, selector]) => [
|
|
644
|
+
role,
|
|
645
|
+
rewriteSelectorValueForProxy(selector, proxyProvider, proxyMode, allModels, directlyAuthenticated),
|
|
646
|
+
]),
|
|
647
|
+
),
|
|
648
|
+
agentModelOverrides: Object.fromEntries(
|
|
649
|
+
Object.entries(bindings.agentModelOverrides).map(([role, selector]) => [
|
|
650
|
+
role,
|
|
651
|
+
rewriteSelectorValueForProxy(selector, proxyProvider, proxyMode, allModels, directlyAuthenticated),
|
|
652
|
+
]),
|
|
653
|
+
),
|
|
654
|
+
};
|
|
655
|
+
}
|
|
322
656
|
|
|
323
657
|
function formatMaterializedSelector(selector: string, model: Model<Api>): string {
|
|
658
|
+
const suffix = splitSelectorThinkingSuffix(selector);
|
|
659
|
+
if (!suffix.selector.includes("/") && suffix.thinkingLevel && model.thinking) {
|
|
660
|
+
const clamped = clampExplicitThinkingLevelForModel(model, suffix.thinkingLevel);
|
|
661
|
+
return clamped && clamped !== ThinkingLevel.Inherit
|
|
662
|
+
? formatModelSelectorValue(suffix.selector, clamped)
|
|
663
|
+
: suffix.selector;
|
|
664
|
+
}
|
|
324
665
|
const clampedSelector = formatClampedModelSelector(selector, model);
|
|
325
666
|
const explicitThinkingLevel = parseModelString(selector)?.thinkingLevel;
|
|
326
667
|
if (!explicitThinkingLevel || parseModelString(clampedSelector)?.thinkingLevel) return clampedSelector;
|
|
327
668
|
return formatModelSelectorValue(clampedSelector, explicitThinkingLevel);
|
|
328
669
|
}
|
|
670
|
+
function getBareSelectorCredentialProviders(selector: string, modelRegistry: ModelRegistry): string[] {
|
|
671
|
+
const suffix = splitSelectorThinkingSuffix(selector);
|
|
672
|
+
const alias = (suffix.thinkingLevel ? suffix.selector : selector).trim().toLowerCase();
|
|
673
|
+
const providers = modelRegistry
|
|
674
|
+
.getAll()
|
|
675
|
+
.filter(model => {
|
|
676
|
+
const modelId = model.id.trim().toLowerCase();
|
|
677
|
+
return modelId === alias || modelId.slice(modelId.lastIndexOf("/") + 1) === alias;
|
|
678
|
+
})
|
|
679
|
+
.map(model => model.provider);
|
|
680
|
+
return [...new Set(providers)];
|
|
681
|
+
}
|
|
329
682
|
|
|
330
|
-
function resolveAndClampSelectorValue(
|
|
683
|
+
async function resolveAndClampSelectorValue(
|
|
331
684
|
selectorValue: ModelSelectorValue,
|
|
332
685
|
availableModels: Model<Api>[],
|
|
333
|
-
options: {
|
|
686
|
+
options: {
|
|
687
|
+
settings: Settings;
|
|
688
|
+
modelRegistry: ModelRegistry;
|
|
689
|
+
sessionId: string;
|
|
690
|
+
credentialSessionId: string;
|
|
691
|
+
aliasIntent: "preset-equivalent";
|
|
692
|
+
requireQualifiedResolution?: boolean;
|
|
693
|
+
},
|
|
334
694
|
profileLabel: string,
|
|
335
695
|
role: string,
|
|
336
|
-
): ModelSelectorValue {
|
|
696
|
+
): Promise<ModelSelectorValue> {
|
|
337
697
|
const selectors = normalizeModelSelectorValue(selectorValue);
|
|
338
698
|
const clamped: string[] = [];
|
|
699
|
+
const unresolvedKnownBareProviders = new Set<string>();
|
|
700
|
+
const unresolvedQualifiedSelectors: string[] = [];
|
|
701
|
+
let everySelectorIsKnownBare = selectors.length > 1;
|
|
339
702
|
let resolvedAny = false;
|
|
340
703
|
for (const selector of selectors) {
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
704
|
+
const bareAlias = !splitSelectorThinkingSuffix(selector).selector.includes("/");
|
|
705
|
+
let resolved = resolveModelRoleValue(selector, availableModels, options);
|
|
706
|
+
if (bareAlias) {
|
|
707
|
+
const authenticated = await resolveModelChainWithAuth(
|
|
708
|
+
[selector],
|
|
709
|
+
{
|
|
710
|
+
getAvailable: () => availableModels,
|
|
711
|
+
getApiKey: model =>
|
|
712
|
+
options.modelRegistry.getApiKeyForProvider(
|
|
713
|
+
model.provider,
|
|
714
|
+
options.credentialSessionId,
|
|
715
|
+
model.baseUrl,
|
|
716
|
+
),
|
|
717
|
+
resolveCanonicalModel: options.modelRegistry.resolveCanonicalModel.bind(options.modelRegistry),
|
|
718
|
+
resolveModelByLookupAlias: options.modelRegistry.resolveModelByLookupAlias?.bind(options.modelRegistry),
|
|
719
|
+
lookupAliasExists: options.modelRegistry.lookupAliasExists?.bind(options.modelRegistry),
|
|
720
|
+
clearCanonicalVariant: options.modelRegistry.clearCanonicalVariant?.bind(options.modelRegistry),
|
|
721
|
+
},
|
|
722
|
+
options.settings,
|
|
723
|
+
options.credentialSessionId,
|
|
724
|
+
{
|
|
725
|
+
managedFallback: true,
|
|
726
|
+
aliasIntent: options.aliasIntent,
|
|
727
|
+
canonicalSessionId: options.sessionId,
|
|
728
|
+
credentialSessionId: options.credentialSessionId,
|
|
729
|
+
},
|
|
730
|
+
);
|
|
731
|
+
resolved = {
|
|
732
|
+
model: authenticated.model,
|
|
733
|
+
thinkingLevel: authenticated.thinkingLevel,
|
|
734
|
+
explicitThinkingLevel: authenticated.explicitThinkingLevel,
|
|
735
|
+
warning: undefined,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
if (!resolved.model) {
|
|
739
|
+
if (bareAlias) {
|
|
740
|
+
const providers = getBareSelectorCredentialProviders(selector, options.modelRegistry);
|
|
741
|
+
const selectorSuffix = splitSelectorThinkingSuffix(selector);
|
|
742
|
+
const bareSelector = selectorSuffix.thinkingLevel ? selectorSuffix.selector : selector;
|
|
743
|
+
const aliasKnown =
|
|
744
|
+
options.modelRegistry.lookupAliasExists?.(bareSelector.toLowerCase()) ?? providers.length > 0;
|
|
745
|
+
if (selectors.length === 1) {
|
|
746
|
+
if (!aliasKnown) {
|
|
747
|
+
throw new Error(
|
|
748
|
+
`Model profile "${profileLabel}" ${role} selector "${bareSelector}" does not match any catalog model`,
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
throw new ModelProfileCredentialError(
|
|
752
|
+
profileLabel,
|
|
753
|
+
providers.length > 0 ? providers : [bareSelector],
|
|
754
|
+
role,
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
if (aliasKnown) {
|
|
758
|
+
for (const provider of providers.length > 0 ? providers : [bareSelector]) {
|
|
759
|
+
unresolvedKnownBareProviders.add(provider);
|
|
760
|
+
}
|
|
761
|
+
} else {
|
|
762
|
+
everySelectorIsKnownBare = false;
|
|
763
|
+
}
|
|
764
|
+
} else {
|
|
765
|
+
unresolvedQualifiedSelectors.push(selector);
|
|
766
|
+
everySelectorIsKnownBare = false;
|
|
767
|
+
}
|
|
346
768
|
clamped.push(selector);
|
|
769
|
+
continue;
|
|
347
770
|
}
|
|
771
|
+
resolvedAny = true;
|
|
772
|
+
clamped.push(formatMaterializedSelector(selector, resolved.model));
|
|
348
773
|
}
|
|
349
|
-
if (!resolvedAny &&
|
|
350
|
-
throw new
|
|
774
|
+
if (!resolvedAny && everySelectorIsKnownBare && unresolvedKnownBareProviders.size > 0) {
|
|
775
|
+
throw new ModelProfileCredentialError(profileLabel, [...unresolvedKnownBareProviders].sort(), role);
|
|
776
|
+
}
|
|
777
|
+
if (options.requireQualifiedResolution && !resolvedAny && unresolvedQualifiedSelectors.length > 0) {
|
|
778
|
+
throw new Error(
|
|
779
|
+
`Model profile "${profileLabel}" ${role} selectors do not match any catalog model: ${unresolvedQualifiedSelectors.join(", ")}`,
|
|
780
|
+
);
|
|
351
781
|
}
|
|
352
782
|
return clamped.length === 1 && typeof selectorValue === "string" ? clamped[0] : clamped;
|
|
353
783
|
}
|
|
354
784
|
|
|
785
|
+
async function concretizeProfileSelectorValue(
|
|
786
|
+
selectorValue: ModelSelectorValue,
|
|
787
|
+
prepared: PreparedModelProfileActivation,
|
|
788
|
+
): Promise<ModelSelectorValue> {
|
|
789
|
+
const candidates = prepared.modelRegistry.getAvailable?.() ?? prepared.modelRegistry.getAll();
|
|
790
|
+
const credentialSessionId = prepared.session.credentialSessionId ?? prepared.session.sessionId;
|
|
791
|
+
const concrete = await Promise.all(
|
|
792
|
+
normalizeModelSelectorValue(selectorValue).map(async selector => {
|
|
793
|
+
const bareAlias = !splitSelectorThinkingSuffix(selector).selector.includes("/");
|
|
794
|
+
const resolved = bareAlias
|
|
795
|
+
? await resolveModelChainWithAuth(
|
|
796
|
+
[selector],
|
|
797
|
+
{
|
|
798
|
+
getAvailable: () => candidates,
|
|
799
|
+
getApiKey: model =>
|
|
800
|
+
prepared.modelRegistry.getApiKeyForProvider(model.provider, credentialSessionId, model.baseUrl),
|
|
801
|
+
resolveCanonicalModel: prepared.modelRegistry.resolveCanonicalModel.bind(prepared.modelRegistry),
|
|
802
|
+
resolveModelByLookupAlias: prepared.modelRegistry.resolveModelByLookupAlias?.bind(
|
|
803
|
+
prepared.modelRegistry,
|
|
804
|
+
),
|
|
805
|
+
lookupAliasExists: prepared.modelRegistry.lookupAliasExists?.bind(prepared.modelRegistry),
|
|
806
|
+
clearCanonicalVariant: prepared.modelRegistry.clearCanonicalVariant?.bind(prepared.modelRegistry),
|
|
807
|
+
},
|
|
808
|
+
prepared.settings as Settings,
|
|
809
|
+
credentialSessionId,
|
|
810
|
+
{
|
|
811
|
+
managedFallback: true,
|
|
812
|
+
aliasIntent: "preset-equivalent",
|
|
813
|
+
canonicalSessionId: prepared.session.sessionId,
|
|
814
|
+
credentialSessionId,
|
|
815
|
+
},
|
|
816
|
+
)
|
|
817
|
+
: resolveModelRoleValue(selector, candidates, {
|
|
818
|
+
settings: prepared.settings as Settings,
|
|
819
|
+
modelRegistry: prepared.modelRegistry,
|
|
820
|
+
sessionId: prepared.session.sessionId,
|
|
821
|
+
credentialSessionId,
|
|
822
|
+
aliasIntent: "preset-equivalent",
|
|
823
|
+
});
|
|
824
|
+
if (!resolved.model) {
|
|
825
|
+
if (bareAlias) return undefined;
|
|
826
|
+
return selector;
|
|
827
|
+
}
|
|
828
|
+
const concreteSelector = `${resolved.model.provider}/${resolved.model.id}`;
|
|
829
|
+
return resolved.explicitThinkingLevel && resolved.thinkingLevel
|
|
830
|
+
? formatModelSelectorValue(concreteSelector, resolved.thinkingLevel)
|
|
831
|
+
: concreteSelector;
|
|
832
|
+
}),
|
|
833
|
+
);
|
|
834
|
+
const resolvedConcrete = concrete.filter((selector): selector is string => selector !== undefined);
|
|
835
|
+
if (resolvedConcrete.length === 0) {
|
|
836
|
+
throw new Error(
|
|
837
|
+
`Model profile deletion could not concretize authenticated selector: ${normalizeModelSelectorValue(selectorValue)[0]}`,
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
return resolvedConcrete.length === 1 && typeof selectorValue === "string" ? resolvedConcrete[0]! : resolvedConcrete;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Restore the session's canonical sticky variant after a failed activation.
|
|
845
|
+
* The exact pre-clear sticky selector (snapshotted verbatim, never re-derived
|
|
846
|
+
* from the live model) is restored when present; otherwise any stale sticky
|
|
847
|
+
* variant is cleared so a previous provider cannot silently resurrect.
|
|
848
|
+
*/
|
|
849
|
+
function restoreCanonicalVariant(
|
|
850
|
+
modelRegistry: PrepareModelProfileActivationOptions["modelRegistry"],
|
|
851
|
+
sessionId: string,
|
|
852
|
+
previousCanonicalVariant: string | undefined,
|
|
853
|
+
): void {
|
|
854
|
+
if (previousCanonicalVariant !== undefined) {
|
|
855
|
+
const restored = modelRegistry.restoreSessionCanonicalVariant?.(sessionId, previousCanonicalVariant) === true;
|
|
856
|
+
if (!restored) modelRegistry.clearCanonicalVariant?.(sessionId);
|
|
857
|
+
} else {
|
|
858
|
+
modelRegistry.clearCanonicalVariant?.(sessionId);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
355
861
|
export async function prepareModelProfileActivation(
|
|
356
862
|
options: PrepareModelProfileActivationOptions,
|
|
357
863
|
): Promise<PreparedModelProfileActivation> {
|
|
@@ -360,160 +866,277 @@ export async function prepareModelProfileActivation(
|
|
|
360
866
|
const profile = profiles.get(profileName) ?? options.modelRegistry.getModelProfile(profileName)!;
|
|
361
867
|
const profileLabel = formatModelProfileDisplayLabel(profile);
|
|
362
868
|
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
869
|
+
const previousModel = options.session.model;
|
|
870
|
+
// Snapshot the exact pre-clear sticky selector (verbatim, not re-derived from
|
|
871
|
+
// the live model) so a failed prepare/apply/materialize rollback restores the
|
|
872
|
+
// genuinely-sticky provider even when the live model is a transient switch.
|
|
873
|
+
const credentialSessionId = options.session.credentialSessionId ?? options.session.sessionId;
|
|
874
|
+
const previousCanonicalVariant = options.modelRegistry.getSessionCanonicalVariant?.(options.session.sessionId);
|
|
366
875
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (apiKey !== kNoAuth && !isAuthenticated(apiKey)) {
|
|
372
|
-
missingProviders.push(provider);
|
|
373
|
-
} else {
|
|
374
|
-
authenticatedProviders.push(provider);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
876
|
+
// Explicit profile activation/reselection invalidates the session's sticky
|
|
877
|
+
// canonical variant BEFORE the new profile's aliases resolve, so the old
|
|
878
|
+
// provider's sticky variant cannot win the new profile's resolution.
|
|
879
|
+
options.modelRegistry.clearCanonicalVariant?.(options.session.sessionId);
|
|
377
880
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
881
|
+
try {
|
|
882
|
+
const requiredProviders = aggregateModelProfileRequiredProviders(profile.requiredProviders, profile);
|
|
883
|
+
const alternativeGroups = profile.alternativeProviderGroups ?? [];
|
|
884
|
+
const alternativeSet = new Set(alternativeGroups.flat());
|
|
885
|
+
|
|
886
|
+
const missingProviders: string[] = [];
|
|
887
|
+
const authenticatedProviders: string[] = [];
|
|
888
|
+
for (const provider of requiredProviders) {
|
|
889
|
+
const apiKey = await options.modelRegistry.getApiKeyForProvider(provider, credentialSessionId);
|
|
890
|
+
if (apiKey !== kNoAuth && !isAuthenticated(apiKey)) {
|
|
891
|
+
missingProviders.push(provider);
|
|
892
|
+
} else {
|
|
893
|
+
authenticatedProviders.push(provider);
|
|
894
|
+
}
|
|
388
895
|
}
|
|
389
|
-
}
|
|
390
896
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
);
|
|
421
|
-
const defaultModel = defaultResolution.model;
|
|
422
|
-
const defaultThinkingLevel = defaultResolution.thinkingLevel;
|
|
423
|
-
const defaultActiveIndex = defaultModel ? defaultResolution.activeIndex : undefined;
|
|
424
|
-
const defaultResolutionSkips = defaultResolution.skips;
|
|
425
|
-
if (bindings.defaultSelector && !defaultModel) {
|
|
426
|
-
throw new Error(`Model profile "${profileLabel}" default selectors did not resolve to an authenticated model`);
|
|
427
|
-
}
|
|
897
|
+
// Required providers are the only activation prerequisites. Mapped fallback
|
|
898
|
+
// providers are resolution-time candidates and intentionally do not gate here.
|
|
899
|
+
// A proxy-routable strict provider is satisfied through the configured
|
|
900
|
+
// OpenAI-compatible proxy when that proxy is itself authenticated; otherwise
|
|
901
|
+
// we fail closed pointing at the proxy (or the provider when none is set).
|
|
902
|
+
const proxyProvider = profile.source === "builtin" ? resolveProxyProviderId(options.settings) : undefined;
|
|
903
|
+
const proxyMode = profile.source === "builtin" ? resolveProxyMode(options.settings) : "fallback";
|
|
904
|
+
if (proxyMode === "always" && proxyProvider === undefined) {
|
|
905
|
+
throw new Error('modelProfile.proxyMode "always" requires modelProfile.proxyProvider');
|
|
906
|
+
}
|
|
907
|
+
if (proxyProvider !== undefined) {
|
|
908
|
+
const configuredProxyProviders = options.modelRegistry.getConfiguredProviderIds?.();
|
|
909
|
+
if (!configuredProxyProviders?.includes(proxyProvider)) {
|
|
910
|
+
throw new Error(
|
|
911
|
+
`modelProfile.proxyProvider "${proxyProvider}" is not configured. Configure it with \`gjc setup provider\` before activating a preset.`,
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
const proxyApiKey =
|
|
916
|
+
proxyProvider === undefined
|
|
917
|
+
? undefined
|
|
918
|
+
: await options.modelRegistry.getApiKeyForProvider(proxyProvider, credentialSessionId);
|
|
919
|
+
const proxyAuthenticated =
|
|
920
|
+
proxyProvider !== undefined &&
|
|
921
|
+
proxyApiKey !== undefined &&
|
|
922
|
+
(proxyApiKey === kNoAuth || isAuthenticated(proxyApiKey));
|
|
923
|
+
if (proxyMode === "always" && !proxyAuthenticated) {
|
|
924
|
+
throw new ModelProfileCredentialError(profileLabel, [proxyProvider!]);
|
|
925
|
+
}
|
|
428
926
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
GjcModelAssignmentTargetId,
|
|
432
|
-
ModelSelectorValue,
|
|
433
|
-
][]) {
|
|
434
|
-
modelRoles[role] = resolveAndClampSelectorValue(
|
|
435
|
-
selectorValue,
|
|
436
|
-
availableModels,
|
|
437
|
-
{ settings: options.settings as Settings, modelRegistry: options.modelRegistry as ModelRegistry },
|
|
438
|
-
profileLabel,
|
|
439
|
-
role,
|
|
927
|
+
const strictMissing = missingProviders.filter(
|
|
928
|
+
provider => !PROXY_ROUTABLE_PROVIDER_IDS.has(provider) && !alternativeSet.has(provider),
|
|
440
929
|
);
|
|
441
|
-
|
|
930
|
+
if (strictMissing.length > 0) {
|
|
931
|
+
throw new ModelProfileCredentialError(profileLabel, strictMissing);
|
|
932
|
+
}
|
|
933
|
+
const strictRoutableMissing = missingProviders.filter(
|
|
934
|
+
provider => PROXY_ROUTABLE_PROVIDER_IDS.has(provider) && !alternativeSet.has(provider),
|
|
935
|
+
);
|
|
936
|
+
if (strictRoutableMissing.length > 0 && (proxyProvider === undefined || !proxyAuthenticated)) {
|
|
937
|
+
throw new ModelProfileCredentialError(
|
|
938
|
+
profileLabel,
|
|
939
|
+
proxyProvider === undefined ? strictRoutableMissing : [proxyProvider],
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
for (const group of alternativeGroups) {
|
|
943
|
+
const groupAuthenticated = group.some(provider => authenticatedProviders.includes(provider));
|
|
944
|
+
if (groupAuthenticated) continue;
|
|
945
|
+
const allRoutable = group.every(provider => PROXY_ROUTABLE_PROVIDER_IDS.has(provider));
|
|
946
|
+
if (allRoutable && proxyAuthenticated) continue;
|
|
947
|
+
throw new ModelProfileCredentialError(
|
|
948
|
+
profileLabel,
|
|
949
|
+
allRoutable && proxyProvider !== undefined ? [proxyProvider] : [...group],
|
|
950
|
+
);
|
|
951
|
+
}
|
|
442
952
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
953
|
+
const availableModels = options.modelRegistry.getAvailable?.() ?? options.modelRegistry.getAll();
|
|
954
|
+
let bindings = resolveProfileBindings(profile);
|
|
955
|
+
if (missingProviders.length > 0 && alternativeGroups.length > 0) {
|
|
956
|
+
bindings = rewriteBindingsProviders(bindings, new Set(authenticatedProviders), alternativeGroups);
|
|
957
|
+
}
|
|
958
|
+
// Built-in preset selectors are routed through a configured authenticated
|
|
959
|
+
// proxy according to the selected mode. This session-scoped rewrite is never
|
|
960
|
+
// persisted to models.yml.
|
|
961
|
+
if (proxyProvider !== undefined && proxyAuthenticated && profile.source === "builtin") {
|
|
962
|
+
bindings = rewriteBindingsForProxy(
|
|
963
|
+
bindings,
|
|
964
|
+
proxyProvider,
|
|
965
|
+
proxyMode,
|
|
966
|
+
availableModels,
|
|
967
|
+
new Set(authenticatedProviders),
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
const defaultSelectors = bindings.defaultSelector ? normalizeModelSelectorValue(bindings.defaultSelector) : [];
|
|
971
|
+
const defaultChain =
|
|
972
|
+
defaultSelectors.length > 0
|
|
973
|
+
? normalizeModelSelectorValue(
|
|
974
|
+
await resolveAndClampSelectorValue(
|
|
975
|
+
bindings.defaultSelector!,
|
|
976
|
+
availableModels,
|
|
977
|
+
{
|
|
978
|
+
settings: options.settings as Settings,
|
|
979
|
+
modelRegistry: options.modelRegistry as ModelRegistry,
|
|
980
|
+
sessionId: options.session.sessionId,
|
|
981
|
+
credentialSessionId,
|
|
982
|
+
aliasIntent: "preset-equivalent",
|
|
983
|
+
},
|
|
984
|
+
profileLabel,
|
|
985
|
+
"default",
|
|
986
|
+
),
|
|
987
|
+
)
|
|
988
|
+
: [];
|
|
989
|
+
const defaultResolution = await resolveModelChainWithAuth(
|
|
990
|
+
defaultChain,
|
|
991
|
+
{
|
|
992
|
+
getAvailable: () => availableModels,
|
|
993
|
+
getApiKey: (model, sessionId) =>
|
|
994
|
+
options.modelRegistry.getApiKeyForProvider(model.provider, sessionId, model.baseUrl),
|
|
995
|
+
resolveCanonicalModel: options.modelRegistry.resolveCanonicalModel?.bind(options.modelRegistry),
|
|
996
|
+
getCanonicalVariants: options.modelRegistry.getCanonicalVariants?.bind(options.modelRegistry),
|
|
997
|
+
getCanonicalId: options.modelRegistry.getCanonicalId?.bind(options.modelRegistry),
|
|
998
|
+
resolveModelByLookupAlias: options.modelRegistry.resolveModelByLookupAlias?.bind(options.modelRegistry),
|
|
999
|
+
lookupAliasExists: options.modelRegistry.lookupAliasExists?.bind(options.modelRegistry),
|
|
1000
|
+
clearCanonicalVariant: options.modelRegistry.clearCanonicalVariant?.bind(options.modelRegistry),
|
|
1001
|
+
} as ModelRegistry,
|
|
1002
|
+
options.settings as Settings,
|
|
1003
|
+
credentialSessionId,
|
|
1004
|
+
{
|
|
1005
|
+
managedFallback: true,
|
|
1006
|
+
aliasIntent: "preset-equivalent",
|
|
1007
|
+
canonicalSessionId: options.session.sessionId,
|
|
1008
|
+
credentialSessionId,
|
|
1009
|
+
},
|
|
454
1010
|
);
|
|
455
|
-
|
|
1011
|
+
const defaultModel = defaultResolution.model;
|
|
1012
|
+
const defaultThinkingLevel = defaultResolution.thinkingLevel;
|
|
1013
|
+
const defaultActiveIndex = defaultModel ? defaultResolution.activeIndex : undefined;
|
|
1014
|
+
const defaultResolutionSkips = defaultResolution.skips;
|
|
1015
|
+
if (bindings.defaultSelector && !defaultModel) {
|
|
1016
|
+
const configuredDefaults = normalizeModelSelectorValue(bindings.defaultSelector);
|
|
1017
|
+
if (configuredDefaults.length === 1) {
|
|
1018
|
+
throw new Error(
|
|
1019
|
+
`Model profile "${profileLabel}" default selector did not resolve: ${configuredDefaults[0]}`,
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
throw new Error(`Model profile "${profileLabel}" default selectors did not resolve to an authenticated model`);
|
|
1023
|
+
}
|
|
456
1024
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
1025
|
+
const modelRoles: Record<string, ModelSelectorValue> = {};
|
|
1026
|
+
for (const [role, selectorValue] of Object.entries(bindings.modelRoles) as [
|
|
1027
|
+
GjcModelAssignmentTargetId,
|
|
1028
|
+
ModelSelectorValue,
|
|
1029
|
+
][]) {
|
|
1030
|
+
modelRoles[role] = await resolveAndClampSelectorValue(
|
|
1031
|
+
selectorValue,
|
|
1032
|
+
availableModels,
|
|
1033
|
+
{
|
|
1034
|
+
settings: options.settings as Settings,
|
|
1035
|
+
modelRegistry: options.modelRegistry as ModelRegistry,
|
|
1036
|
+
sessionId: options.session.sessionId,
|
|
1037
|
+
credentialSessionId,
|
|
1038
|
+
aliasIntent: "preset-equivalent",
|
|
1039
|
+
requireQualifiedResolution: profile.source !== "builtin",
|
|
1040
|
+
},
|
|
1041
|
+
profileLabel,
|
|
1042
|
+
role,
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const agentModelOverrides: Record<string, ModelSelectorValue> = {};
|
|
1047
|
+
for (const [role, selectorValue] of Object.entries(bindings.agentModelOverrides) as [
|
|
1048
|
+
GjcModelAssignmentTargetId,
|
|
1049
|
+
ModelSelectorValue,
|
|
1050
|
+
][]) {
|
|
1051
|
+
agentModelOverrides[role] = await resolveAndClampSelectorValue(
|
|
1052
|
+
selectorValue,
|
|
1053
|
+
availableModels,
|
|
1054
|
+
{
|
|
1055
|
+
settings: options.settings as Settings,
|
|
1056
|
+
modelRegistry: options.modelRegistry as ModelRegistry,
|
|
1057
|
+
sessionId: options.session.sessionId,
|
|
1058
|
+
credentialSessionId,
|
|
1059
|
+
aliasIntent: "preset-equivalent",
|
|
1060
|
+
requireQualifiedResolution: profile.source !== "builtin",
|
|
1061
|
+
},
|
|
1062
|
+
profileLabel,
|
|
1063
|
+
role,
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
return {
|
|
1068
|
+
profileName,
|
|
1069
|
+
session: options.session as PreparedModelProfileActivation["session"],
|
|
1070
|
+
settings: options.settings as PreparedModelProfileActivation["settings"],
|
|
1071
|
+
modelRegistry: options.modelRegistry,
|
|
1072
|
+
previousModel,
|
|
1073
|
+
previousCanonicalVariant,
|
|
1074
|
+
previousThinkingLevel: options.session.thinkingLevel,
|
|
1075
|
+
previousAgentModelOverrides: { ...options.settings.get("task.agentModelOverrides") },
|
|
1076
|
+
previousModelRoles: { ...options.settings.get("modelRoles") },
|
|
1077
|
+
baseAgentModelOverrides: Object.fromEntries(
|
|
1078
|
+
Object.entries(options.settings.get("task.agentModelOverrides") ?? {}).filter(
|
|
1079
|
+
([key]) =>
|
|
1080
|
+
!(options.session.getProfileInstalledOverrideKeys?.().agentModelOverrides ?? []).includes(key),
|
|
1081
|
+
),
|
|
472
1082
|
),
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
1083
|
+
baseModelRoles: Object.fromEntries(
|
|
1084
|
+
Object.entries(options.settings.get("modelRoles") ?? {}).filter(
|
|
1085
|
+
([key]) => !(options.session.getProfileInstalledOverrideKeys?.().modelRoles ?? []).includes(key),
|
|
1086
|
+
),
|
|
477
1087
|
),
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
1088
|
+
previousPersistedModelRoles: options.settings.getGlobal("modelRoles"),
|
|
1089
|
+
previousPersistedAgentModelOverrides: options.settings.getGlobal("task.agentModelOverrides"),
|
|
1090
|
+
previousModelRolesOverride: options.settings.getOverride("modelRoles"),
|
|
1091
|
+
previousAgentModelOverridesOverride: options.settings.getOverride("task.agentModelOverrides"),
|
|
1092
|
+
previousDefaultProfileOverride: options.settings.getOverride("modelProfile.default"),
|
|
1093
|
+
previousPersistedDefaultProfile: options.settings.getGlobal("modelProfile.default"),
|
|
1094
|
+
previousPersistedDefaultThinkingLevel: options.settings.getGlobal("defaultThinkingLevel") as
|
|
1095
|
+
| Exclude<ThinkingLevel, "inherit">
|
|
1096
|
+
| undefined,
|
|
1097
|
+
previousDefaultThinkingLevelOverride: options.settings.getOverride("defaultThinkingLevel"),
|
|
1098
|
+
previousDefaultChain: options.session.getConfiguredModelChain("default"),
|
|
1099
|
+
previousDefaultChainState: options.session.getConfiguredModelChainState?.("default"),
|
|
1100
|
+
|
|
1101
|
+
defaultModel,
|
|
1102
|
+
defaultThinkingLevel,
|
|
1103
|
+
defaultActiveIndex,
|
|
1104
|
+
defaultResolutionSkips,
|
|
1105
|
+
defaultChain,
|
|
1106
|
+
modelRoles,
|
|
1107
|
+
agentModelOverrides,
|
|
1108
|
+
previousActiveModelProfile: options.session.getActiveModelProfile?.(),
|
|
1109
|
+
previousSessionDefaultModel: options.session.getSessionDefaultModelSelector?.(),
|
|
1110
|
+
previousDefaultFallbackRuntimeState: options.session.getDefaultFallbackRuntimeState?.(),
|
|
1111
|
+
};
|
|
1112
|
+
} catch (error) {
|
|
1113
|
+
restoreCanonicalVariant(options.modelRegistry, options.session.sessionId, previousCanonicalVariant);
|
|
1114
|
+
throw error;
|
|
1115
|
+
}
|
|
491
1116
|
}
|
|
492
1117
|
|
|
493
1118
|
export async function applyPreparedModelProfileActivation(
|
|
494
1119
|
prepared: PreparedModelProfileActivation,
|
|
495
1120
|
options: ApplyModelProfileActivationOptions = {},
|
|
496
1121
|
): Promise<void> {
|
|
497
|
-
|
|
498
|
-
const previousThinkingLevel = prepared.previousThinkingLevel;
|
|
499
|
-
const previousAgentModelOverrides = prepared.previousAgentModelOverrides;
|
|
500
|
-
const previousModelRoles = prepared.previousModelRoles;
|
|
501
|
-
const previousPersistedDefault = prepared.settings.getGlobal("modelProfile.default");
|
|
502
|
-
const previousDefaultThinkingLevel = prepared.settings.get("defaultThinkingLevel");
|
|
503
|
-
const previousActiveModelProfile = prepared.previousActiveModelProfile;
|
|
504
|
-
const previousSessionDefaultModel = prepared.previousSessionDefaultModel;
|
|
505
|
-
let modelChanged = false;
|
|
1122
|
+
let modelMutationStarted = false;
|
|
506
1123
|
let overridesChanged = false;
|
|
507
|
-
let defaultChanged = false;
|
|
508
1124
|
let modelRolesChanged = false;
|
|
509
|
-
let
|
|
1125
|
+
let persistentMutationStarted = false;
|
|
510
1126
|
let defaultChainChanged = false;
|
|
1127
|
+
let resumeDefaultChanged = false;
|
|
511
1128
|
|
|
512
1129
|
try {
|
|
513
|
-
|
|
1130
|
+
const ownedDefaultChain =
|
|
1131
|
+
prepared.defaultChain.length > 0
|
|
1132
|
+
? prepared.defaultChain
|
|
1133
|
+
: (prepared.previousDefaultChain ??
|
|
1134
|
+
(prepared.previousModel ? [formatModelString(prepared.previousModel)] : []));
|
|
1135
|
+
if (ownedDefaultChain.length > 0) {
|
|
1136
|
+
defaultChainChanged = true;
|
|
514
1137
|
prepared.session.setConfiguredModelChain(
|
|
515
1138
|
"default",
|
|
516
|
-
|
|
1139
|
+
ownedDefaultChain,
|
|
517
1140
|
"profile-activation",
|
|
518
1141
|
prepared.profileName,
|
|
519
1142
|
true,
|
|
@@ -524,24 +1147,22 @@ export async function applyPreparedModelProfileActivation(
|
|
|
524
1147
|
prepared.defaultResolutionSkips,
|
|
525
1148
|
);
|
|
526
1149
|
}
|
|
527
|
-
|
|
528
|
-
defaultChainChanged = true;
|
|
529
1150
|
}
|
|
530
1151
|
if (prepared.defaultModel) {
|
|
1152
|
+
modelMutationStarted = true;
|
|
531
1153
|
await prepared.session.setModelTemporary(
|
|
532
1154
|
prepared.defaultModel,
|
|
533
1155
|
options.thinkingLevelOverride ?? prepared.defaultThinkingLevel,
|
|
534
|
-
{
|
|
535
|
-
persistAsSessionDefault: true,
|
|
536
|
-
cause: "profile-activation",
|
|
537
|
-
},
|
|
1156
|
+
{ cause: "profile-activation" },
|
|
538
1157
|
);
|
|
539
|
-
modelChanged = true;
|
|
540
|
-
}
|
|
541
|
-
if (Object.keys(prepared.modelRoles).length > 0) {
|
|
542
|
-
prepared.settings.override("modelRoles", { ...prepared.baseModelRoles, ...prepared.modelRoles });
|
|
543
|
-
modelRolesChanged = true;
|
|
544
1158
|
}
|
|
1159
|
+
// Always reinstall the model role layer from the durable base plus the
|
|
1160
|
+
// new profile's roles so omitted roles from the previous profile are dropped.
|
|
1161
|
+
prepared.settings.override("modelRoles", {
|
|
1162
|
+
...prepared.baseModelRoles,
|
|
1163
|
+
...prepared.modelRoles,
|
|
1164
|
+
});
|
|
1165
|
+
modelRolesChanged = true;
|
|
545
1166
|
// Always reinstall the agent role layer from the durable base plus the
|
|
546
1167
|
// new profile's roles: a default-only or role-free successor must drop
|
|
547
1168
|
// the previous profile's role-agent mappings rather than inheriting them.
|
|
@@ -551,68 +1172,128 @@ export async function applyPreparedModelProfileActivation(
|
|
|
551
1172
|
});
|
|
552
1173
|
overridesChanged = true;
|
|
553
1174
|
if (options.persistDefault) {
|
|
1175
|
+
persistentMutationStarted = true;
|
|
554
1176
|
prepared.settings.set("modelRoles", {});
|
|
555
1177
|
prepared.settings.set("task.agentModelOverrides", {});
|
|
556
1178
|
if (prepared.defaultThinkingLevel !== undefined && prepared.defaultThinkingLevel !== ThinkingLevel.Inherit) {
|
|
557
1179
|
prepared.settings.set("defaultThinkingLevel", prepared.defaultThinkingLevel);
|
|
558
|
-
defaultThinkingChanged = true;
|
|
559
1180
|
}
|
|
560
1181
|
prepared.settings.set("modelProfile.default", prepared.profileName);
|
|
561
|
-
|
|
562
|
-
await prepared.settings.flush();
|
|
1182
|
+
await prepared.settings.flushOrThrow();
|
|
563
1183
|
}
|
|
564
1184
|
prepared.session.setActiveModelProfile?.(prepared.profileName);
|
|
1185
|
+
if (prepared.defaultModel) {
|
|
1186
|
+
prepared.modelRegistry.seedCanonicalVariant?.(prepared.session.sessionId, prepared.defaultModel);
|
|
1187
|
+
resumeDefaultChanged = true;
|
|
1188
|
+
prepared.session.recordResumeDefaultModel?.(`${prepared.defaultModel.provider}/${prepared.defaultModel.id}`);
|
|
1189
|
+
}
|
|
565
1190
|
prepared.session.noteProfileInstalledOverrides?.(
|
|
566
1191
|
Object.keys(prepared.modelRoles),
|
|
567
1192
|
Object.keys(prepared.agentModelOverrides),
|
|
568
|
-
|
|
569
|
-
previousModel,
|
|
1193
|
+
prepared.previousModel,
|
|
570
1194
|
);
|
|
571
1195
|
} catch (error) {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
1196
|
+
const rollbackErrors: unknown[] = [];
|
|
1197
|
+
const restore = (action: () => void): void => {
|
|
1198
|
+
try {
|
|
1199
|
+
action();
|
|
1200
|
+
} catch (rollbackError) {
|
|
1201
|
+
rollbackErrors.push(rollbackError);
|
|
578
1202
|
}
|
|
1203
|
+
};
|
|
1204
|
+
if (persistentMutationStarted) {
|
|
1205
|
+
restore(() =>
|
|
1206
|
+
prepared.previousPersistedDefaultProfile === undefined
|
|
1207
|
+
? prepared.settings.unset("modelProfile.default")
|
|
1208
|
+
: prepared.settings.set("modelProfile.default", prepared.previousPersistedDefaultProfile),
|
|
1209
|
+
);
|
|
1210
|
+
restore(() =>
|
|
1211
|
+
prepared.previousPersistedModelRoles === undefined
|
|
1212
|
+
? prepared.settings.unset("modelRoles")
|
|
1213
|
+
: prepared.settings.set("modelRoles", prepared.previousPersistedModelRoles),
|
|
1214
|
+
);
|
|
1215
|
+
restore(() =>
|
|
1216
|
+
prepared.previousPersistedAgentModelOverrides === undefined
|
|
1217
|
+
? prepared.settings.unset("task.agentModelOverrides")
|
|
1218
|
+
: prepared.settings.set("task.agentModelOverrides", prepared.previousPersistedAgentModelOverrides),
|
|
1219
|
+
);
|
|
1220
|
+
restore(() =>
|
|
1221
|
+
prepared.previousPersistedDefaultThinkingLevel === undefined
|
|
1222
|
+
? prepared.settings.unset("defaultThinkingLevel")
|
|
1223
|
+
: prepared.settings.set("defaultThinkingLevel", prepared.previousPersistedDefaultThinkingLevel),
|
|
1224
|
+
);
|
|
579
1225
|
}
|
|
580
1226
|
if (modelRolesChanged) {
|
|
581
|
-
|
|
1227
|
+
restore(() =>
|
|
1228
|
+
prepared.previousModelRolesOverride === undefined
|
|
1229
|
+
? prepared.settings.clearOverride("modelRoles")
|
|
1230
|
+
: prepared.settings.override("modelRoles", prepared.previousModelRolesOverride),
|
|
1231
|
+
);
|
|
582
1232
|
}
|
|
583
1233
|
if (overridesChanged) {
|
|
584
|
-
|
|
1234
|
+
restore(() =>
|
|
1235
|
+
prepared.previousAgentModelOverridesOverride === undefined
|
|
1236
|
+
? prepared.settings.clearOverride("task.agentModelOverrides")
|
|
1237
|
+
: prepared.settings.override("task.agentModelOverrides", prepared.previousAgentModelOverridesOverride),
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
if (modelMutationStarted) {
|
|
1241
|
+
try {
|
|
1242
|
+
if (prepared.session.restoreModelSelectionForRollback) {
|
|
1243
|
+
await prepared.session.restoreModelSelectionForRollback(
|
|
1244
|
+
prepared.previousModel,
|
|
1245
|
+
prepared.previousThinkingLevel,
|
|
1246
|
+
);
|
|
1247
|
+
} else if (prepared.previousModel) {
|
|
1248
|
+
await prepared.session.setModelTemporary(prepared.previousModel, prepared.previousThinkingLevel, {
|
|
1249
|
+
cause: "rollback",
|
|
1250
|
+
});
|
|
1251
|
+
} else {
|
|
1252
|
+
throw new Error("Model-less profile activation rollback is unavailable");
|
|
1253
|
+
}
|
|
1254
|
+
} catch (rollbackError) {
|
|
1255
|
+
rollbackErrors.push(rollbackError);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
if (resumeDefaultChanged) {
|
|
1259
|
+
restore(() => prepared.session.recordResumeDefaultModel?.(prepared.previousSessionDefaultModel));
|
|
585
1260
|
}
|
|
586
1261
|
if (defaultChainChanged) {
|
|
587
|
-
prepared.
|
|
588
|
-
|
|
589
|
-
prepared.
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
1262
|
+
const previousChain = prepared.previousDefaultChainState;
|
|
1263
|
+
restore(() =>
|
|
1264
|
+
prepared.session.setConfiguredModelChain(
|
|
1265
|
+
"default",
|
|
1266
|
+
previousChain?.entries ??
|
|
1267
|
+
prepared.previousDefaultChain ??
|
|
1268
|
+
(prepared.previousModel ? [`${prepared.previousModel.provider}/${prepared.previousModel.id}`] : []),
|
|
1269
|
+
previousChain?.origin ?? "rollback",
|
|
1270
|
+
previousChain?.identity,
|
|
1271
|
+
previousChain?.explicitHead ?? true,
|
|
1272
|
+
),
|
|
593
1273
|
);
|
|
594
1274
|
}
|
|
595
|
-
prepared.
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
const restoreDefaultSelector =
|
|
610
|
-
previousSessionDefaultModel ??
|
|
611
|
-
(previousModel ? `${previousModel.provider}/${previousModel.id}` : undefined);
|
|
612
|
-
if (restoreDefaultSelector) {
|
|
613
|
-
prepared.session.recordResumeDefaultModel?.(restoreDefaultSelector);
|
|
1275
|
+
if (prepared.previousDefaultFallbackRuntimeState) {
|
|
1276
|
+
restore(() =>
|
|
1277
|
+
prepared.session.restoreDefaultFallbackRuntimeState?.(prepared.previousDefaultFallbackRuntimeState!),
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
restore(() => prepared.session.setActiveModelProfile?.(prepared.previousActiveModelProfile));
|
|
1281
|
+
restore(() =>
|
|
1282
|
+
restoreCanonicalVariant(prepared.modelRegistry, prepared.session.sessionId, prepared.previousCanonicalVariant),
|
|
1283
|
+
);
|
|
1284
|
+
if (persistentMutationStarted) {
|
|
1285
|
+
try {
|
|
1286
|
+
await prepared.settings.flushOrThrow();
|
|
1287
|
+
} catch (rollbackError) {
|
|
1288
|
+
rollbackErrors.push(rollbackError);
|
|
614
1289
|
}
|
|
615
1290
|
}
|
|
1291
|
+
if (rollbackErrors.length > 0) {
|
|
1292
|
+
throw new AggregateError(
|
|
1293
|
+
[error, ...rollbackErrors],
|
|
1294
|
+
"Model profile activation failed and rollback was incomplete",
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
616
1297
|
throw error;
|
|
617
1298
|
}
|
|
618
1299
|
}
|
|
@@ -622,32 +1303,68 @@ export interface MaterializeModelProfileForDeletionResult {
|
|
|
622
1303
|
agentModelOverrides: Record<string, ModelSelectorValue>;
|
|
623
1304
|
previousModelRoles: Record<string, ModelSelectorValue>;
|
|
624
1305
|
previousAgentModelOverrides: Record<string, ModelSelectorValue>;
|
|
1306
|
+
previousPersistedModelRoles: Record<string, ModelSelectorValue> | undefined;
|
|
1307
|
+
previousPersistedAgentModelOverrides: Record<string, ModelSelectorValue> | undefined;
|
|
1308
|
+
previousModelRolesOverride: Record<string, ModelSelectorValue> | undefined;
|
|
1309
|
+
previousAgentModelOverridesOverride: Record<string, ModelSelectorValue> | undefined;
|
|
1310
|
+
previousDefaultProfileOverride: string | undefined;
|
|
625
1311
|
previousDefaultProfile: string | undefined;
|
|
626
1312
|
previousPersistedDefaultProfile: string | undefined;
|
|
627
1313
|
previousActiveModelProfile: string | undefined;
|
|
1314
|
+
previousDefaultChainState: ConfiguredModelChainState | undefined;
|
|
1315
|
+
previousDefaultFallbackRuntimeState: DefaultFallbackRuntimeState | undefined;
|
|
1316
|
+
/**
|
|
1317
|
+
* Restores the session sticky canonical variant that was snapshotted before
|
|
1318
|
+
* materialization cleared it. Internal closure capturing the registry,
|
|
1319
|
+
* session, and exact pre-clear selector — avoids threading a registry
|
|
1320
|
+
* through the restore options. Optional so callers that never materialized
|
|
1321
|
+
* (or use a registry without sticky support) can still restore settings.
|
|
1322
|
+
*/
|
|
1323
|
+
restoreSessionCanonicalVariant?: () => void;
|
|
628
1324
|
}
|
|
629
1325
|
|
|
630
1326
|
export async function materializeModelProfileForDeletion(
|
|
631
1327
|
options: PrepareModelProfileActivationOptions & {
|
|
632
|
-
settings: Pick<Settings, "clearOverride" | "
|
|
1328
|
+
settings: Pick<Settings, "clearOverride" | "flushOrThrow" | "get" | "getGlobal" | "override" | "set" | "unset">;
|
|
633
1329
|
},
|
|
634
1330
|
): Promise<MaterializeModelProfileForDeletionResult> {
|
|
635
1331
|
const prepared = await prepareModelProfileActivation(options);
|
|
636
1332
|
const previousDefaultProfile = prepared.settings.get("modelProfile.default");
|
|
637
1333
|
const previousPersistedDefaultProfile = prepared.settings.getGlobal("modelProfile.default");
|
|
1334
|
+
const concretizeForDeletion = async (selector: ModelSelectorValue): Promise<ModelSelectorValue> => {
|
|
1335
|
+
try {
|
|
1336
|
+
return await concretizeProfileSelectorValue(selector, prepared);
|
|
1337
|
+
} catch (error) {
|
|
1338
|
+
restoreCanonicalVariant(prepared.modelRegistry, prepared.session.sessionId, prepared.previousCanonicalVariant);
|
|
1339
|
+
throw error;
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
const concreteDefaultChain =
|
|
1343
|
+
prepared.defaultChain.length > 0
|
|
1344
|
+
? normalizeModelSelectorValue(await concretizeForDeletion(prepared.defaultChain))
|
|
1345
|
+
: [];
|
|
1346
|
+
const concreteModelRoles: Record<string, ModelSelectorValue> = {};
|
|
1347
|
+
for (const [role, selector] of Object.entries(prepared.modelRoles)) {
|
|
1348
|
+
concreteModelRoles[role] = await concretizeForDeletion(selector);
|
|
1349
|
+
}
|
|
1350
|
+
const concreteAgentModelOverrides: Record<string, ModelSelectorValue> = {};
|
|
1351
|
+
for (const [role, selector] of Object.entries(prepared.agentModelOverrides)) {
|
|
1352
|
+
concreteAgentModelOverrides[role] = await concretizeForDeletion(selector);
|
|
1353
|
+
}
|
|
638
1354
|
const nextModelRoles = {
|
|
639
1355
|
...prepared.previousModelRoles,
|
|
640
|
-
...(
|
|
1356
|
+
...(concreteDefaultChain.length > 0
|
|
641
1357
|
? {
|
|
642
|
-
default:
|
|
1358
|
+
default: concreteDefaultChain.length === 1 ? concreteDefaultChain[0] : [...concreteDefaultChain],
|
|
643
1359
|
}
|
|
644
1360
|
: {}),
|
|
645
|
-
...
|
|
1361
|
+
...concreteModelRoles,
|
|
646
1362
|
};
|
|
647
1363
|
const nextAgentModelOverrides = {
|
|
648
1364
|
...prepared.previousAgentModelOverrides,
|
|
649
|
-
...
|
|
1365
|
+
...concreteAgentModelOverrides,
|
|
650
1366
|
};
|
|
1367
|
+
let defaultChainChanged = false;
|
|
651
1368
|
|
|
652
1369
|
try {
|
|
653
1370
|
prepared.settings.set("modelRoles", nextModelRoles);
|
|
@@ -657,15 +1374,85 @@ export async function materializeModelProfileForDeletion(
|
|
|
657
1374
|
prepared.settings.override("modelRoles", nextModelRoles);
|
|
658
1375
|
prepared.settings.override("task.agentModelOverrides", nextAgentModelOverrides);
|
|
659
1376
|
prepared.session.setActiveModelProfile?.(undefined);
|
|
660
|
-
|
|
1377
|
+
if (prepared.defaultChain.length > 0) {
|
|
1378
|
+
defaultChainChanged = true;
|
|
1379
|
+
prepared.session.setConfiguredModelChain(
|
|
1380
|
+
"default",
|
|
1381
|
+
concreteDefaultChain,
|
|
1382
|
+
"profile-deletion-materialized",
|
|
1383
|
+
undefined,
|
|
1384
|
+
true,
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
await prepared.settings.flushOrThrow();
|
|
1388
|
+
prepared.session.clearProfileInstalledOverrides?.();
|
|
661
1389
|
} catch (error) {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
1390
|
+
const previousChain = prepared.previousDefaultChainState;
|
|
1391
|
+
const rollbackErrors: unknown[] = [];
|
|
1392
|
+
const restore = (action: () => void): void => {
|
|
1393
|
+
try {
|
|
1394
|
+
action();
|
|
1395
|
+
} catch (rollbackError) {
|
|
1396
|
+
rollbackErrors.push(rollbackError);
|
|
1397
|
+
}
|
|
1398
|
+
};
|
|
1399
|
+
restore(() =>
|
|
1400
|
+
restoreCanonicalVariant(prepared.modelRegistry, prepared.session.sessionId, prepared.previousCanonicalVariant),
|
|
1401
|
+
);
|
|
1402
|
+
if (defaultChainChanged) {
|
|
1403
|
+
restore(() =>
|
|
1404
|
+
prepared.session.setConfiguredModelChain(
|
|
1405
|
+
"default",
|
|
1406
|
+
previousChain?.entries ?? prepared.previousDefaultChain ?? [],
|
|
1407
|
+
previousChain?.origin ?? "rollback",
|
|
1408
|
+
previousChain?.identity,
|
|
1409
|
+
previousChain?.explicitHead ?? true,
|
|
1410
|
+
),
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
if (prepared.previousDefaultFallbackRuntimeState) {
|
|
1414
|
+
restore(() =>
|
|
1415
|
+
prepared.session.restoreDefaultFallbackRuntimeState?.(prepared.previousDefaultFallbackRuntimeState!),
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
restore(() =>
|
|
1419
|
+
prepared.previousPersistedModelRoles === undefined
|
|
1420
|
+
? prepared.settings.unset("modelRoles")
|
|
1421
|
+
: prepared.settings.set("modelRoles", prepared.previousPersistedModelRoles),
|
|
1422
|
+
);
|
|
1423
|
+
restore(() =>
|
|
1424
|
+
prepared.previousPersistedAgentModelOverrides === undefined
|
|
1425
|
+
? prepared.settings.unset("task.agentModelOverrides")
|
|
1426
|
+
: prepared.settings.set("task.agentModelOverrides", prepared.previousPersistedAgentModelOverrides),
|
|
1427
|
+
);
|
|
1428
|
+
restore(() => prepared.settings.set("modelProfile.default", previousPersistedDefaultProfile));
|
|
1429
|
+
restore(() =>
|
|
1430
|
+
prepared.previousModelRolesOverride === undefined
|
|
1431
|
+
? prepared.settings.clearOverride("modelRoles")
|
|
1432
|
+
: prepared.settings.override("modelRoles", prepared.previousModelRolesOverride),
|
|
1433
|
+
);
|
|
1434
|
+
restore(() =>
|
|
1435
|
+
prepared.previousAgentModelOverridesOverride === undefined
|
|
1436
|
+
? prepared.settings.clearOverride("task.agentModelOverrides")
|
|
1437
|
+
: prepared.settings.override("task.agentModelOverrides", prepared.previousAgentModelOverridesOverride),
|
|
1438
|
+
);
|
|
1439
|
+
restore(() =>
|
|
1440
|
+
prepared.previousDefaultProfileOverride === undefined
|
|
1441
|
+
? prepared.settings.clearOverride("modelProfile.default")
|
|
1442
|
+
: prepared.settings.override("modelProfile.default", prepared.previousDefaultProfileOverride),
|
|
1443
|
+
);
|
|
1444
|
+
restore(() => prepared.session.setActiveModelProfile?.(prepared.previousActiveModelProfile));
|
|
1445
|
+
try {
|
|
1446
|
+
await prepared.settings.flushOrThrow();
|
|
1447
|
+
} catch (rollbackError) {
|
|
1448
|
+
rollbackErrors.push(rollbackError);
|
|
1449
|
+
}
|
|
1450
|
+
if (rollbackErrors.length > 0) {
|
|
1451
|
+
throw new AggregateError(
|
|
1452
|
+
[error, ...rollbackErrors],
|
|
1453
|
+
"Profile deletion materialization failed and rollback was incomplete",
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
669
1456
|
throw error;
|
|
670
1457
|
}
|
|
671
1458
|
|
|
@@ -674,25 +1461,88 @@ export async function materializeModelProfileForDeletion(
|
|
|
674
1461
|
agentModelOverrides: nextAgentModelOverrides,
|
|
675
1462
|
previousModelRoles: prepared.previousModelRoles,
|
|
676
1463
|
previousAgentModelOverrides: prepared.previousAgentModelOverrides,
|
|
1464
|
+
previousPersistedModelRoles: prepared.previousPersistedModelRoles,
|
|
1465
|
+
previousPersistedAgentModelOverrides: prepared.previousPersistedAgentModelOverrides,
|
|
1466
|
+
previousModelRolesOverride: prepared.previousModelRolesOverride,
|
|
1467
|
+
previousAgentModelOverridesOverride: prepared.previousAgentModelOverridesOverride,
|
|
1468
|
+
previousDefaultProfileOverride: prepared.previousDefaultProfileOverride,
|
|
677
1469
|
previousDefaultProfile,
|
|
678
1470
|
previousPersistedDefaultProfile,
|
|
679
1471
|
previousActiveModelProfile: prepared.previousActiveModelProfile,
|
|
1472
|
+
previousDefaultChainState: prepared.previousDefaultChainState,
|
|
1473
|
+
previousDefaultFallbackRuntimeState: prepared.previousDefaultFallbackRuntimeState,
|
|
1474
|
+
restoreSessionCanonicalVariant: () =>
|
|
1475
|
+
restoreCanonicalVariant(prepared.modelRegistry, prepared.session.sessionId, prepared.previousCanonicalVariant),
|
|
680
1476
|
};
|
|
681
1477
|
}
|
|
682
1478
|
|
|
683
1479
|
export async function restoreMaterializedModelProfileForDeletion(options: {
|
|
684
|
-
settings: Pick<Settings, "
|
|
685
|
-
session: Pick<
|
|
1480
|
+
settings: Pick<Settings, "clearOverride" | "flushOrThrow" | "override" | "set" | "unset">;
|
|
1481
|
+
session: Pick<
|
|
1482
|
+
ModelProfileActivationSession,
|
|
1483
|
+
"setActiveModelProfile" | "setConfiguredModelChain" | "restoreDefaultFallbackRuntimeState"
|
|
1484
|
+
>;
|
|
686
1485
|
snapshot: MaterializeModelProfileForDeletionResult;
|
|
687
1486
|
}): Promise<void> {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
1487
|
+
const restoreErrors: unknown[] = [];
|
|
1488
|
+
const restore = (action: () => void): void => {
|
|
1489
|
+
try {
|
|
1490
|
+
action();
|
|
1491
|
+
} catch (error) {
|
|
1492
|
+
restoreErrors.push(error);
|
|
1493
|
+
}
|
|
1494
|
+
};
|
|
1495
|
+
restore(() => options.snapshot.restoreSessionCanonicalVariant?.());
|
|
1496
|
+
const previousChain = options.snapshot.previousDefaultChainState;
|
|
1497
|
+
restore(() =>
|
|
1498
|
+
options.session.setConfiguredModelChain(
|
|
1499
|
+
"default",
|
|
1500
|
+
previousChain?.entries ?? [],
|
|
1501
|
+
previousChain?.origin ?? "rollback",
|
|
1502
|
+
previousChain?.identity,
|
|
1503
|
+
previousChain?.explicitHead ?? true,
|
|
1504
|
+
),
|
|
1505
|
+
);
|
|
1506
|
+
if (options.snapshot.previousDefaultFallbackRuntimeState) {
|
|
1507
|
+
restore(() =>
|
|
1508
|
+
options.session.restoreDefaultFallbackRuntimeState?.(options.snapshot.previousDefaultFallbackRuntimeState!),
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1511
|
+
restore(() =>
|
|
1512
|
+
options.snapshot.previousPersistedModelRoles === undefined
|
|
1513
|
+
? options.settings.unset("modelRoles")
|
|
1514
|
+
: options.settings.set("modelRoles", options.snapshot.previousPersistedModelRoles),
|
|
1515
|
+
);
|
|
1516
|
+
restore(() =>
|
|
1517
|
+
options.snapshot.previousPersistedAgentModelOverrides === undefined
|
|
1518
|
+
? options.settings.unset("task.agentModelOverrides")
|
|
1519
|
+
: options.settings.set("task.agentModelOverrides", options.snapshot.previousPersistedAgentModelOverrides),
|
|
1520
|
+
);
|
|
1521
|
+
restore(() => options.settings.set("modelProfile.default", options.snapshot.previousPersistedDefaultProfile));
|
|
1522
|
+
restore(() =>
|
|
1523
|
+
options.snapshot.previousModelRolesOverride === undefined
|
|
1524
|
+
? options.settings.clearOverride("modelRoles")
|
|
1525
|
+
: options.settings.override("modelRoles", options.snapshot.previousModelRolesOverride),
|
|
1526
|
+
);
|
|
1527
|
+
restore(() =>
|
|
1528
|
+
options.snapshot.previousAgentModelOverridesOverride === undefined
|
|
1529
|
+
? options.settings.clearOverride("task.agentModelOverrides")
|
|
1530
|
+
: options.settings.override("task.agentModelOverrides", options.snapshot.previousAgentModelOverridesOverride),
|
|
1531
|
+
);
|
|
1532
|
+
restore(() =>
|
|
1533
|
+
options.snapshot.previousDefaultProfileOverride === undefined
|
|
1534
|
+
? options.settings.clearOverride("modelProfile.default")
|
|
1535
|
+
: options.settings.override("modelProfile.default", options.snapshot.previousDefaultProfileOverride),
|
|
1536
|
+
);
|
|
1537
|
+
restore(() => options.session.setActiveModelProfile?.(options.snapshot.previousActiveModelProfile));
|
|
1538
|
+
try {
|
|
1539
|
+
await options.settings.flushOrThrow();
|
|
1540
|
+
} catch (error) {
|
|
1541
|
+
restoreErrors.push(error);
|
|
1542
|
+
}
|
|
1543
|
+
if (restoreErrors.length > 0) {
|
|
1544
|
+
throw new AggregateError(restoreErrors, "Failed to fully restore materialized model profile deletion state");
|
|
1545
|
+
}
|
|
696
1546
|
}
|
|
697
1547
|
|
|
698
1548
|
export async function activateModelProfile(
|