@agentclientprotocol/claude-agent-acp 0.75.2-preview.1 → 0.75.2-preview.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/acp-agent.d.ts +26 -68
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +118 -483
- package/dist/air-extension.d.ts +1 -0
- package/dist/air-extension.d.ts.map +1 -1
- package/dist/air-extension.js +1 -0
- package/dist/clear-context-coordinator.d.ts +1 -0
- package/dist/clear-context-coordinator.d.ts.map +1 -1
- package/dist/clear-context-coordinator.js +3 -4
- package/dist/session-effort.d.ts +14 -0
- package/dist/session-effort.d.ts.map +1 -0
- package/dist/session-effort.js +93 -0
- package/dist/session-model.d.ts +41 -0
- package/dist/session-model.d.ts.map +1 -0
- package/dist/session-model.js +395 -0
- package/package.json +1 -1
package/dist/acp-agent.js
CHANGED
|
@@ -4,7 +4,7 @@ import { GOAL_ACTIONS, GOAL_CONTROL_METHOD, GOAL_EXTENSION_VERSION, goalUpdateFr
|
|
|
4
4
|
import { sanitizeTitle, SessionTitles } from "./session-titles.js";
|
|
5
5
|
import { AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY, asSdkSessionNotification, clientSupportsSubagents, } from "./acp-subagents.js";
|
|
6
6
|
import { isNativeSubagentControlTool, isNativeSubagentControlUpdate, NativeSubagentRuntime, } from "./native-subagents.js";
|
|
7
|
-
import { AIR_ASYNC_TASKS_CAPABILITY, withAirMeta } from "./air-extension.js";
|
|
7
|
+
import { AIR_ASYNC_TASKS_CAPABILITY, AIR_RECOMMENDED_CONFIG_VALUE_CAPABILITY, clientSupportsAirCapability, withAirMeta, } from "./air-extension.js";
|
|
8
8
|
import { AsyncTaskRuntime, backgroundBashTaskFromToolResult, clientSupportsAsyncTasks, } from "./async-tasks.js";
|
|
9
9
|
import { AUTH_STATUS_PROBE_TIMEOUT_MS, AUTH_STATUS_UPDATE_METHOD, authStatusCapability, fromAccountInfo, fromCliStatus, gatewayAuthStatus, mergeAuthStatus, notLoggedInAuthStatus, sameAuthStatus, } from "./auth-status.js";
|
|
10
10
|
import { execFile } from "node:child_process";
|
|
@@ -32,11 +32,15 @@ import { ContextCompactionLifecycle, contextCompactionMetadataFromBoundary, } fr
|
|
|
32
32
|
import { applyTaskCreate, applyTaskList, applyTaskUpdate, clearHookCallbacks, completeHookCallback, createPostToolUseHook, createTaskHook, parseTaskCreateOutput, parseTaskListOutput, parseTaskUpdateOutput, planEntries, registerHookCallback, taskStateToPlanEntries, toolInfoFromToolUse, toolUpdateFromDiffToolResponse, toolUpdateFromToolResult, unregisterHookCallback, } from "./tools.js";
|
|
33
33
|
import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
|
|
34
34
|
import { acceptedPlanToolResult, ExitPlanCoordinator, executionDiagnostic, exitPlanModeRawOutput, observeExitPlanToolResults, } from "./exit-plan.js";
|
|
35
|
-
import { DEFAULT_AGENT_ID
|
|
35
|
+
import { DEFAULT_AGENT_ID } from "./session-config-ids.js";
|
|
36
36
|
import { parseToolResultMeta } from "./tool-result-meta.js";
|
|
37
37
|
import { formatUsageResponse, isUsageCommandText, parseUsageResponse } from "./usage-markdown.js";
|
|
38
|
-
export { DEFAULT_AGENT_ID
|
|
38
|
+
export { DEFAULT_AGENT_ID } from "./session-config-ids.js";
|
|
39
39
|
import { MODE_CONFIG_ID, SessionModeManager } from "./session-mode.js";
|
|
40
|
+
import { applyAvailableModelsAllowlist, buildModelConfigOption, getAvailableModels, MODEL_CONFIG_ID, resolveModelPreference, } from "./session-model.js";
|
|
41
|
+
import { buildEffortConfigOption, EFFORT_CONFIG_ID, mergeEffortSettings, settingsEffortForModel, toSdkEffortLevel, } from "./session-effort.js";
|
|
42
|
+
export { EFFORT_CONFIG_ID, settingsEffortForModel } from "./session-effort.js";
|
|
43
|
+
export { applyAvailableModelsAllowlist, matchResumedModel, MODEL_CONFIG_ID, resolveModelPreference, } from "./session-model.js";
|
|
40
44
|
export const CLAUDE_CONFIG_DIR = process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
|
|
41
45
|
const execFileAsync = promisify(execFile);
|
|
42
46
|
const MAX_INLINE_FAILURE_TITLE_LENGTH = 256;
|
|
@@ -940,7 +944,7 @@ export class ClaudeAcpAgent {
|
|
|
940
944
|
// steering extension contract: advertises the `_session/steering` request
|
|
941
945
|
// so clients know they may inject a follow-up into a running turn.
|
|
942
946
|
_meta: {
|
|
943
|
-
...airSessionFailureCapabilityMeta(AGENT_FILE_CHANGE_REPORT_CAPABILITY, AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY, AIR_ASYNC_TASKS_CAPABILITY),
|
|
947
|
+
...airSessionFailureCapabilityMeta(AGENT_FILE_CHANGE_REPORT_CAPABILITY, AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY, AIR_ASYNC_TASKS_CAPABILITY, AIR_RECOMMENDED_CONFIG_VALUE_CAPABILITY),
|
|
944
948
|
steering: {
|
|
945
949
|
supported: true,
|
|
946
950
|
},
|
|
@@ -5521,9 +5525,19 @@ export class ClaudeAcpAgent {
|
|
|
5521
5525
|
// along.
|
|
5522
5526
|
const effortOpt = session.configOptions.find((o) => o.id === EFFORT_CONFIG_ID);
|
|
5523
5527
|
const currentEffort = typeof effortOpt?.currentValue === "string" ? effortOpt.currentValue : undefined;
|
|
5524
|
-
const
|
|
5525
|
-
|
|
5526
|
-
|
|
5528
|
+
const useRecommendedValue = clientSupportsRecommendedConfigValue(this.clientCapabilities);
|
|
5529
|
+
const pinnedEffort = session.effortPinnedLevel;
|
|
5530
|
+
const effortWasPinned = pinnedEffort !== undefined;
|
|
5531
|
+
const appliedEffortBeforeSwitch = session.appliedEffortLevel ??
|
|
5532
|
+
(useRecommendedValue && typeof currentEffort === "string" && currentEffort !== "default"
|
|
5533
|
+
? currentEffort
|
|
5534
|
+
: pinnedEffort);
|
|
5535
|
+
const effortPinnedForNewModel = effortWasPinned &&
|
|
5536
|
+
newModelInfo?.supportsEffort === true &&
|
|
5537
|
+
newModelInfo.supportedEffortLevels?.some((level) => level === pinnedEffort) === true;
|
|
5538
|
+
const seedEffort = effortPinnedForNewModel
|
|
5539
|
+
? pinnedEffort
|
|
5540
|
+
: settingsEffortForModel(mergeEffortSettings(session.settingsManager.getSettings(), session.effortSettingsOverride), newModelInfo, value);
|
|
5527
5541
|
session.configOptions = buildConfigOptions(session.modes, session.models, session.modelInfos, seedEffort, session.agents, session.currentAgent, {
|
|
5528
5542
|
// The toggle follows the newly selected model: it disappears when the
|
|
5529
5543
|
// model lacks fast support and reappears (with the retained user
|
|
@@ -5532,26 +5546,65 @@ export class ClaudeAcpAgent {
|
|
|
5532
5546
|
enabled: session.fastModeEnabled,
|
|
5533
5547
|
useBooleanOption: clientSupportsBooleanConfigOptions(this.clientCapabilities),
|
|
5534
5548
|
disabledReason: session.fastModeDisabledReason,
|
|
5549
|
+
}, {
|
|
5550
|
+
useRecommendedValue,
|
|
5535
5551
|
});
|
|
5536
|
-
//
|
|
5552
|
+
// Opted-in clients apply the concrete displayed effort on every switch,
|
|
5553
|
+
// including settings-derived values, so a previous automatic flag cannot
|
|
5554
|
+
// shadow the new model's settings. This does not create a user pin.
|
|
5555
|
+
// For legacy clients, sync only when a user pin changed across the
|
|
5537
5556
|
// switch — i.e. the new model clamped it away (buildConfigOptions
|
|
5538
5557
|
// validated the seed against the new model's levels), where the flag
|
|
5539
5558
|
// must be cleared too or the SDK would keep running the old pin
|
|
5540
5559
|
// invisibly. Settings-derived seeds are display-only: the CLI resolves
|
|
5541
5560
|
// persisted effort itself, and pinning it at the flag layer would
|
|
5542
5561
|
// shadow the per-model values on every later switch.
|
|
5543
|
-
|
|
5562
|
+
const shouldSyncEffort = useRecommendedValue || (effortWasPinned && !effortPinnedForNewModel);
|
|
5563
|
+
if (shouldSyncEffort) {
|
|
5544
5564
|
const newEffortOpt = session.configOptions.find((o) => o.id === EFFORT_CONFIG_ID);
|
|
5545
5565
|
const newEffort = typeof newEffortOpt?.currentValue === "string" ? newEffortOpt.currentValue : undefined;
|
|
5546
|
-
|
|
5566
|
+
try {
|
|
5547
5567
|
await session.query.applyFlagSettings({
|
|
5548
|
-
|
|
5568
|
+
// A legacy client's unpinned effort is display-only: the CLI
|
|
5569
|
+
// resolves the persisted value for the new model. When an old
|
|
5570
|
+
// user pin is no longer supported, clear the flag layer instead
|
|
5571
|
+
// of replacing it with that displayed value. Opted-in clients
|
|
5572
|
+
// deliberately apply their concrete displayed effort.
|
|
5573
|
+
effortLevel: useRecommendedValue ? toSdkEffortLevel(newEffort) : null,
|
|
5549
5574
|
});
|
|
5550
|
-
|
|
5551
|
-
|
|
5575
|
+
session.effortPinnedLevel = effortPinnedForNewModel ? pinnedEffort : undefined;
|
|
5576
|
+
session.appliedEffortLevel =
|
|
5577
|
+
useRecommendedValue && newEffort !== "default" ? newEffort : undefined;
|
|
5578
|
+
}
|
|
5579
|
+
catch (error) {
|
|
5580
|
+
// setModel has already succeeded. Effort synchronization is a
|
|
5581
|
+
// secondary, best-effort operation: propagating this error would
|
|
5582
|
+
// make the RPC report failure (or suppress an external-switch
|
|
5583
|
+
// notification) even though the SDK is already on the new model.
|
|
5584
|
+
// Preserve the old SDK/pin bookkeeping and never advertise the
|
|
5585
|
+
// unapplied value: restore the last applied value when the new model
|
|
5586
|
+
// can select it, otherwise omit the effort option until a later
|
|
5587
|
+
// successful switch rebuilds it.
|
|
5588
|
+
session.effortPinnedLevel = pinnedEffort;
|
|
5589
|
+
session.appliedEffortLevel = appliedEffortBeforeSwitch;
|
|
5590
|
+
const appliedValueStillSelectable = appliedEffortBeforeSwitch !== undefined &&
|
|
5591
|
+
newEffortOpt?.type === "select" &&
|
|
5592
|
+
newEffortOpt.options.some((option) => "value" in option
|
|
5593
|
+
? option.value === appliedEffortBeforeSwitch
|
|
5594
|
+
: option.options.some((nested) => nested.value === appliedEffortBeforeSwitch));
|
|
5595
|
+
if (newEffortOpt?.type === "select" && appliedValueStillSelectable) {
|
|
5596
|
+
newEffortOpt.currentValue = appliedEffortBeforeSwitch;
|
|
5552
5597
|
}
|
|
5598
|
+
else {
|
|
5599
|
+
session.configOptions = session.configOptions.filter((option) => option.id !== EFFORT_CONFIG_ID);
|
|
5600
|
+
}
|
|
5601
|
+
this.logger.error(`Failed to synchronize effort after model switch to "${value}":`, error);
|
|
5553
5602
|
}
|
|
5554
5603
|
}
|
|
5604
|
+
else if (effortPinnedForNewModel) {
|
|
5605
|
+
session.effortPinnedLevel = pinnedEffort;
|
|
5606
|
+
session.appliedEffortLevel = pinnedEffort;
|
|
5607
|
+
}
|
|
5555
5608
|
// Emit current_mode_update only after session.modes AND
|
|
5556
5609
|
// session.configOptions have been fully reconciled. This way, a failure
|
|
5557
5610
|
// in the configOptions/effort rebuild above can't leave the client with
|
|
@@ -5574,17 +5627,21 @@ export class ClaudeAcpAgent {
|
|
|
5574
5627
|
session.currentAgent = value;
|
|
5575
5628
|
session.configOptions = session.configOptions.map((o) => o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o);
|
|
5576
5629
|
}
|
|
5630
|
+
else if (configId === EFFORT_CONFIG_ID) {
|
|
5631
|
+
// Apply first so a rejected control request cannot leave the displayed
|
|
5632
|
+
// value ahead of the SDK flag layer.
|
|
5633
|
+
await session.query.applyFlagSettings({
|
|
5634
|
+
effortLevel: toSdkEffortLevel(value),
|
|
5635
|
+
});
|
|
5636
|
+
session.configOptions = session.configOptions.map((o) => o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o);
|
|
5637
|
+
session.appliedEffortLevel = value !== "default" ? value : undefined;
|
|
5638
|
+
// "Default" clears the flag layer (toSdkEffortLevel → null), handing
|
|
5639
|
+
// effort back to the CLI's persisted per-model resolution — so it
|
|
5640
|
+
// un-pins; any other pick pins effort for the session.
|
|
5641
|
+
session.effortPinnedLevel = value !== "default" ? value : undefined;
|
|
5642
|
+
}
|
|
5577
5643
|
else {
|
|
5578
5644
|
session.configOptions = session.configOptions.map((o) => o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o);
|
|
5579
|
-
if (configId === EFFORT_CONFIG_ID) {
|
|
5580
|
-
await session.query.applyFlagSettings({
|
|
5581
|
-
effortLevel: toSdkEffortLevel(value),
|
|
5582
|
-
});
|
|
5583
|
-
// "Default" clears the flag layer (toSdkEffortLevel → null), handing
|
|
5584
|
-
// effort back to the CLI's persisted per-model resolution — so it
|
|
5585
|
-
// un-pins; any other pick pins effort for the session.
|
|
5586
|
-
session.effortPinnedByUser = value !== "default";
|
|
5587
|
-
}
|
|
5588
5645
|
}
|
|
5589
5646
|
}
|
|
5590
5647
|
/** Reconcile adapter model state after the SDK switched the session's
|
|
@@ -5927,14 +5984,15 @@ export class ClaudeAcpAgent {
|
|
|
5927
5984
|
...(modelConfig.availableModels && { availableModels: modelConfig.availableModels }),
|
|
5928
5985
|
}
|
|
5929
5986
|
: undefined);
|
|
5987
|
+
const configuredSettingsObject = typeof configuredSettings === "string"
|
|
5988
|
+
? JSON.parse(await fs.readFile(path.resolve(params.cwd, configuredSettings), "utf8"))
|
|
5989
|
+
: configuredSettings;
|
|
5930
5990
|
// Claude Code applies env from settings.json after the subprocess env. Put
|
|
5931
5991
|
// an active ACP route in the programmatic settings tier too so user/project
|
|
5932
5992
|
// settings cannot silently restore a different ANTHROPIC_BASE_URL.
|
|
5933
5993
|
let settings = configuredSettings;
|
|
5934
5994
|
if (resolvedProvider) {
|
|
5935
|
-
const baseSettings =
|
|
5936
|
-
? JSON.parse(await fs.readFile(path.resolve(params.cwd, configuredSettings), "utf8"))
|
|
5937
|
-
: configuredSettings;
|
|
5995
|
+
const baseSettings = configuredSettingsObject;
|
|
5938
5996
|
settings = {
|
|
5939
5997
|
...baseSettings,
|
|
5940
5998
|
apiKeyHelper: "",
|
|
@@ -6248,15 +6306,19 @@ export class ClaudeAcpAgent {
|
|
|
6248
6306
|
useBooleanOption: clientSupportsBooleanConfigOptions(this.clientCapabilities),
|
|
6249
6307
|
disabledReason: fastModeDisabledReason,
|
|
6250
6308
|
};
|
|
6251
|
-
//
|
|
6252
|
-
//
|
|
6253
|
-
//
|
|
6254
|
-
//
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6309
|
+
// Concrete effort must also be applied to the SDK: its automatic default
|
|
6310
|
+
// need not be our "medium" recommendation. Preserve explicit SDK options
|
|
6311
|
+
// and persisted settings; automatic values re-seed on every model switch.
|
|
6312
|
+
// Legacy clients still leave effort resolution entirely to the CLI.
|
|
6313
|
+
const useRecommendedValue = clientSupportsRecommendedConfigValue(this.clientCapabilities);
|
|
6314
|
+
const configOptions = buildConfigOptions(modes, models, modelInfos, userProvidedOptions?.effort ??
|
|
6315
|
+
settingsEffortForModel(mergeEffortSettings(settingsManager.getSettings(), configuredSettingsObject), currentModelInfo), agents, currentAgent, fastMode, {
|
|
6316
|
+
useRecommendedValue,
|
|
6317
|
+
});
|
|
6318
|
+
const initialEffort = configOptions.find((option) => option.id === EFFORT_CONFIG_ID);
|
|
6319
|
+
if (useRecommendedValue && typeof initialEffort?.currentValue === "string") {
|
|
6320
|
+
await q.applyFlagSettings({ effortLevel: toSdkEffortLevel(initialEffort.currentValue) });
|
|
6321
|
+
}
|
|
6260
6322
|
// Seed the context window without extra IPC. The cached authoritative
|
|
6261
6323
|
// window from a prior turn wins (`result.modelUsage`, cross-session),
|
|
6262
6324
|
// then the text heuristic, then the default. We deliberately do NOT issue
|
|
@@ -6287,6 +6349,7 @@ export class ClaudeAcpAgent {
|
|
|
6287
6349
|
sessionFingerprint: computeSessionFingerprint(params),
|
|
6288
6350
|
creationParams: params,
|
|
6289
6351
|
settingsManager,
|
|
6352
|
+
effortSettingsOverride: configuredSettingsObject,
|
|
6290
6353
|
titles: new SessionTitles(this, sessionId),
|
|
6291
6354
|
accumulatedUsage: {
|
|
6292
6355
|
inputTokens: 0,
|
|
@@ -6302,6 +6365,16 @@ export class ClaudeAcpAgent {
|
|
|
6302
6365
|
autoModeFallbackWarningShown: false,
|
|
6303
6366
|
autoModeFallbackWarningPending,
|
|
6304
6367
|
configOptions,
|
|
6368
|
+
effortPinnedLevel: userProvidedOptions?.effort !== undefined &&
|
|
6369
|
+
initialEffort?.currentValue === userProvidedOptions.effort
|
|
6370
|
+
? userProvidedOptions.effort
|
|
6371
|
+
: undefined,
|
|
6372
|
+
appliedEffortLevel: useRecommendedValue && typeof initialEffort?.currentValue === "string"
|
|
6373
|
+
? initialEffort.currentValue
|
|
6374
|
+
: userProvidedOptions?.effort !== undefined &&
|
|
6375
|
+
initialEffort?.currentValue === userProvidedOptions.effort
|
|
6376
|
+
? userProvidedOptions.effort
|
|
6377
|
+
: undefined,
|
|
6305
6378
|
agents,
|
|
6306
6379
|
currentAgent,
|
|
6307
6380
|
fastModeEnabled,
|
|
@@ -6696,39 +6769,6 @@ function isValidBaseUrl(baseUrl) {
|
|
|
6696
6769
|
}
|
|
6697
6770
|
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
6698
6771
|
}
|
|
6699
|
-
// Translate a UI effort value into the flag-layer payload. The SDK
|
|
6700
|
-
// shallow-merges `applyFlagSettings`, drops `undefined` during JSON transport,
|
|
6701
|
-
// and only clears a key when an explicit `null` is sent — see
|
|
6702
|
-
// `applyFlagSettings` in @anthropic-ai/claude-agent-sdk. Mapping both the
|
|
6703
|
-
// `"default"` sentinel and `undefined` (effort option absent for the model) to
|
|
6704
|
-
// `null` ensures any previously-applied flag is actually cleared. Typed as
|
|
6705
|
-
// `EffortLevel` (not `Settings["effortLevel"]`): the picker offers whatever
|
|
6706
|
-
// `supportedEffortLevels` reports, which includes the session-scoped `"max"`
|
|
6707
|
-
// that the persisted Settings shape deliberately excludes.
|
|
6708
|
-
function toSdkEffortLevel(value) {
|
|
6709
|
-
return value === undefined || value === "default" ? null : value;
|
|
6710
|
-
}
|
|
6711
|
-
/** The effort level the CLI itself resolves for a model from the persisted
|
|
6712
|
-
* settings: the per-model entry (`modelSettings`, keyed by canonical model
|
|
6713
|
-
* name — the CLI persists /effort per model since 2.1.243) wins over the
|
|
6714
|
-
* legacy top-level `effortLevel`. Alias picker rows are looked up by their
|
|
6715
|
-
* resolved model id first, then the row value, then the raw config value
|
|
6716
|
-
* for models not in the picker. Exact keys only — the CLI's canonical-form
|
|
6717
|
-
* fallback matching isn't replicated, so a miss simply falls back to the
|
|
6718
|
-
* top-level value (best-effort display; `buildConfigOptions` still
|
|
6719
|
-
* validates the result against the model's supported levels). */
|
|
6720
|
-
export function settingsEffortForModel(settings, modelInfo, modelId) {
|
|
6721
|
-
const modelSettings = settings.modelSettings;
|
|
6722
|
-
if (modelSettings) {
|
|
6723
|
-
for (const key of [modelInfo?.resolvedModel, modelInfo?.value, modelId]) {
|
|
6724
|
-
const perModel = key !== undefined ? modelSettings[key]?.effortLevel : undefined;
|
|
6725
|
-
if (typeof perModel === "string") {
|
|
6726
|
-
return perModel;
|
|
6727
|
-
}
|
|
6728
|
-
}
|
|
6729
|
-
}
|
|
6730
|
-
return settings.effortLevel;
|
|
6731
|
-
}
|
|
6732
6772
|
// `supportedAgents()` always returns Claude Code's built-in subagents — the
|
|
6733
6773
|
// ones used for Task-tool delegation (Explore, Plan, etc.) — even when the user
|
|
6734
6774
|
// has configured none of their own. Those aren't meaningful *main-thread*
|
|
@@ -6765,7 +6805,6 @@ export async function discoverCustomAgents(q) {
|
|
|
6765
6805
|
* handlers in `setSessionConfigOption`/`applyConfigOptionValue` reference the
|
|
6766
6806
|
* same identifiers and can't drift apart. */
|
|
6767
6807
|
export { MODE_CONFIG_ID };
|
|
6768
|
-
export const MODEL_CONFIG_ID = "model";
|
|
6769
6808
|
export const AGENT_CONFIG_ID = "agent";
|
|
6770
6809
|
export const FAST_MODE_CONFIG_ID = "fast";
|
|
6771
6810
|
/** Select-fallback values used when the client has not opted into boolean
|
|
@@ -6813,6 +6852,12 @@ export function normalizeFastModeDisabledReason(reason) {
|
|
|
6813
6852
|
export function clientSupportsBooleanConfigOptions(clientCapabilities) {
|
|
6814
6853
|
return clientCapabilities?.session?.configOptions?.boolean != null;
|
|
6815
6854
|
}
|
|
6855
|
+
/** Whether the Client advertised the AIR `recommendedValue` capability for
|
|
6856
|
+
* select-style model and effort options. Missing or malformed AIR metadata
|
|
6857
|
+
* retains the legacy `default` entries. */
|
|
6858
|
+
export function clientSupportsRecommendedConfigValue(clientCapabilities) {
|
|
6859
|
+
return clientSupportsAirCapability(clientCapabilities, AIR_RECOMMENDED_CONFIG_VALUE_CAPABILITY);
|
|
6860
|
+
}
|
|
6816
6861
|
/** Build the Fast mode config option. When the Client supports boolean config
|
|
6817
6862
|
* options we expose a native `type: "boolean"` toggle; otherwise we degrade to
|
|
6818
6863
|
* a two-value `select` ("on"/"off") so older Clients still get a usable
|
|
@@ -6862,61 +6907,14 @@ export function resolveFastModeEnabled(params) {
|
|
|
6862
6907
|
}
|
|
6863
6908
|
throw new Error(`Invalid value for config option ${FAST_MODE_CONFIG_ID}: ${value}`);
|
|
6864
6909
|
}
|
|
6865
|
-
export function buildConfigOptions(modes, models, modelInfos, currentEffortLevel, agents = [], currentAgent = DEFAULT_AGENT_ID, fastMode) {
|
|
6910
|
+
export function buildConfigOptions(modes, models, modelInfos, currentEffortLevel, agents = [], currentAgent = DEFAULT_AGENT_ID, fastMode, presentation) {
|
|
6866
6911
|
const options = [
|
|
6867
6912
|
SessionModeManager.configOption(modes),
|
|
6868
|
-
|
|
6869
|
-
id: MODEL_CONFIG_ID,
|
|
6870
|
-
name: "Model",
|
|
6871
|
-
description: "AI model to use",
|
|
6872
|
-
category: "model",
|
|
6873
|
-
type: "select",
|
|
6874
|
-
currentValue: models.currentModelId,
|
|
6875
|
-
options: models.availableModels.map((m) => {
|
|
6876
|
-
if (m.modelId === "default") {
|
|
6877
|
-
const defaultInfo = modelInfos.find((mi) => mi.value === "default");
|
|
6878
|
-
const resolvedModel = defaultInfo?.resolvedModel;
|
|
6879
|
-
if (resolvedModel) {
|
|
6880
|
-
const namedMatch = modelInfos.find((mi) => mi.value !== "default" && mi.resolvedModel === resolvedModel);
|
|
6881
|
-
return {
|
|
6882
|
-
value: m.modelId,
|
|
6883
|
-
name: m.name,
|
|
6884
|
-
description: namedMatch?.displayName ?? resolvedModel,
|
|
6885
|
-
};
|
|
6886
|
-
}
|
|
6887
|
-
}
|
|
6888
|
-
return { value: m.modelId, name: m.name, description: m.description ?? undefined };
|
|
6889
|
-
}),
|
|
6890
|
-
},
|
|
6913
|
+
buildModelConfigOption(models, modelInfos, presentation?.useRecommendedValue === true),
|
|
6891
6914
|
];
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
? (currentModelInfo.supportedEffortLevels ?? [])
|
|
6896
|
-
: [];
|
|
6897
|
-
if (supportedLevels.length > 0) {
|
|
6898
|
-
const effortOptions = [
|
|
6899
|
-
{ value: "default", name: "Default" },
|
|
6900
|
-
...supportedLevels.map((level) => ({
|
|
6901
|
-
value: level,
|
|
6902
|
-
name: level
|
|
6903
|
-
.split(/[_-]/)
|
|
6904
|
-
.map((part) => (part ? part.charAt(0).toUpperCase() + part.slice(1) : part))
|
|
6905
|
-
.join(" "),
|
|
6906
|
-
})),
|
|
6907
|
-
];
|
|
6908
|
-
const includes = (l) => l === "default" || supportedLevels.includes(l);
|
|
6909
|
-
const validEffort = currentEffortLevel && includes(currentEffortLevel) ? currentEffortLevel : "default";
|
|
6910
|
-
options.push({
|
|
6911
|
-
id: EFFORT_CONFIG_ID,
|
|
6912
|
-
name: "Effort",
|
|
6913
|
-
description: "Available effort levels for this model",
|
|
6914
|
-
category: "thought_level",
|
|
6915
|
-
type: "select",
|
|
6916
|
-
currentValue: validEffort,
|
|
6917
|
-
options: effortOptions,
|
|
6918
|
-
});
|
|
6919
|
-
}
|
|
6915
|
+
const effort = buildEffortConfigOption(modelInfos, models.currentModelId, currentEffortLevel, presentation?.useRecommendedValue === true);
|
|
6916
|
+
if (effort)
|
|
6917
|
+
options.push(effort);
|
|
6920
6918
|
// Surface the Fast mode toggle only when the current model supports it. The
|
|
6921
6919
|
// option renders as a native boolean toggle for Clients that opted in, and a
|
|
6922
6920
|
// two-value select otherwise.
|
|
@@ -6946,369 +6944,6 @@ export function buildConfigOptions(modes, models, modelInfos, currentEffortLevel
|
|
|
6946
6944
|
}
|
|
6947
6945
|
return options;
|
|
6948
6946
|
}
|
|
6949
|
-
// Claude Code CLI persists display strings like "opus[1m]" in settings,
|
|
6950
|
-
// but the SDK model list uses IDs like "claude-opus-4-6-1m".
|
|
6951
|
-
const MODEL_CONTEXT_HINT_PATTERN = /\[(\d+m)\]$/i;
|
|
6952
|
-
// The id-suffix spelling of a context hint ("-1m" in "claude-opus-4-6-1m");
|
|
6953
|
-
// shared by the strip and canonicalize helpers below so the two can't drift.
|
|
6954
|
-
const CONTEXT_HINT_SUFFIX_PATTERN = /-(\d+m)$/i;
|
|
6955
|
-
/** Remove context-window hints — the display form "[1m]" and the SDK id
|
|
6956
|
-
* suffix form "-1m" — from a model string. Those digits describe context
|
|
6957
|
-
* size, not model identity or generation version. */
|
|
6958
|
-
function stripContextHints(s) {
|
|
6959
|
-
return s.replace(/\[\d+m\]/gi, "").replace(CONTEXT_HINT_SUFFIX_PATTERN, "");
|
|
6960
|
-
}
|
|
6961
|
-
/** Canonicalize a model id for exact comparison: trimmed, lowercased, with
|
|
6962
|
-
* the id-suffix hint spelling unified to the bracket form ("-1m" → "[1m]").
|
|
6963
|
-
* The hint itself is kept — bare and 1M ids must stay distinct. */
|
|
6964
|
-
function canonicalizeModelId(s) {
|
|
6965
|
-
return s.trim().toLowerCase().replace(CONTEXT_HINT_SUFFIX_PATTERN, "[$1]");
|
|
6966
|
-
}
|
|
6967
|
-
/** The context hint a model string carries ("1m" for either spelling), or
|
|
6968
|
-
* null for a bare id. */
|
|
6969
|
-
function contextHintOf(s) {
|
|
6970
|
-
return canonicalizeModelId(s).match(MODEL_CONTEXT_HINT_PATTERN)?.[1] ?? null;
|
|
6971
|
-
}
|
|
6972
|
-
// Captures a model family version: `4-6`/`4.7` for dated generations, or a
|
|
6973
|
-
// bare `5` for single-number ones like "Sonnet 5". Used to keep a pinned
|
|
6974
|
-
// `claude-opus-4-6` from matching the `opus` alias once it points at 4.7.
|
|
6975
|
-
const MODEL_FAMILY_VERSION_PATTERN = /\b(\d+)(?:[-.](\d+))?\b/;
|
|
6976
|
-
function extractModelFamilyVersion(s) {
|
|
6977
|
-
const match = stripContextHints(s).match(MODEL_FAMILY_VERSION_PATTERN);
|
|
6978
|
-
if (!match)
|
|
6979
|
-
return null;
|
|
6980
|
-
return match[2] ? `${match[1]}.${match[2]}` : match[1];
|
|
6981
|
-
}
|
|
6982
|
-
function modelVersionsCompatible(preference, candidate) {
|
|
6983
|
-
const preferred = extractModelFamilyVersion(preference);
|
|
6984
|
-
if (!preferred)
|
|
6985
|
-
return true;
|
|
6986
|
-
const candidateVersion = extractModelFamilyVersion(candidate.value) ??
|
|
6987
|
-
extractModelFamilyVersion(candidate.displayName) ??
|
|
6988
|
-
extractModelFamilyVersion(candidate.description);
|
|
6989
|
-
if (!candidateVersion)
|
|
6990
|
-
return true;
|
|
6991
|
-
return preferred === candidateVersion;
|
|
6992
|
-
}
|
|
6993
|
-
function tokenizeModelPreference(model) {
|
|
6994
|
-
const lower = model.trim().toLowerCase();
|
|
6995
|
-
const contextHint = lower.match(MODEL_CONTEXT_HINT_PATTERN)?.[1]?.toLowerCase();
|
|
6996
|
-
const normalized = lower.replace(MODEL_CONTEXT_HINT_PATTERN, " $1 ");
|
|
6997
|
-
const rawTokens = normalized.split(/[^a-z0-9]+/).filter(Boolean);
|
|
6998
|
-
const tokens = rawTokens
|
|
6999
|
-
.map((token) => {
|
|
7000
|
-
if (token === "opusplan")
|
|
7001
|
-
return "opus";
|
|
7002
|
-
if (token === "best" || token === "default")
|
|
7003
|
-
return "";
|
|
7004
|
-
return token;
|
|
7005
|
-
})
|
|
7006
|
-
.filter((token) => token && token !== "claude")
|
|
7007
|
-
.filter((token) => /[a-z]/.test(token) || token.endsWith("m"));
|
|
7008
|
-
return { tokens, contextHint };
|
|
7009
|
-
}
|
|
7010
|
-
function scoreModelMatch(model, tokens, contextHint) {
|
|
7011
|
-
const haystack = `${model.value} ${model.displayName}`.toLowerCase();
|
|
7012
|
-
let score = 0;
|
|
7013
|
-
let nonHintMatched = false;
|
|
7014
|
-
for (const token of tokens) {
|
|
7015
|
-
if (haystack.includes(token)) {
|
|
7016
|
-
if (token !== contextHint)
|
|
7017
|
-
nonHintMatched = true;
|
|
7018
|
-
score += token === contextHint ? 3 : 1;
|
|
7019
|
-
}
|
|
7020
|
-
}
|
|
7021
|
-
if (contextHint && !nonHintMatched)
|
|
7022
|
-
return 0;
|
|
7023
|
-
return score;
|
|
7024
|
-
}
|
|
7025
|
-
export function resolveModelPreference(models, preference) {
|
|
7026
|
-
const trimmed = preference.trim();
|
|
7027
|
-
if (!trimmed)
|
|
7028
|
-
return null;
|
|
7029
|
-
const lower = trimmed.toLowerCase();
|
|
7030
|
-
// Exact match on value or display name. Values compare on the canonical
|
|
7031
|
-
// hint spelling so "opus-1m" hits an "opus[1m]" row (and vice versa).
|
|
7032
|
-
const canonicalPreference = canonicalizeModelId(trimmed);
|
|
7033
|
-
const directMatch = models.find((model) => model.value === trimmed ||
|
|
7034
|
-
canonicalizeModelId(model.value) === canonicalPreference ||
|
|
7035
|
-
model.displayName.toLowerCase() === lower);
|
|
7036
|
-
if (directMatch)
|
|
7037
|
-
return directMatch;
|
|
7038
|
-
// Exact match on the alias's canonical resolved id (e.g. a pinned
|
|
7039
|
-
// "claude-sonnet-5" against the "sonnet" row's `resolvedModel`). SDK-
|
|
7040
|
-
// reported and unambiguous, so it's tried before the fuzzier tiers below.
|
|
7041
|
-
// Compared on the canonical hint spelling so a "-1m"-suffix pin matches a
|
|
7042
|
-
// "[1m]"-spelled resolvedModel instead of falling into the substring tier
|
|
7043
|
-
// (which would land on the bare 200k sibling). "default" is skipped first
|
|
7044
|
-
// since it shares a resolvedModel with whichever alias the CLI currently
|
|
7045
|
-
// recommends — a specific pin should land on that named alias, not
|
|
7046
|
-
// "default".
|
|
7047
|
-
const matchesResolved = (model) => model.resolvedModel != null && canonicalizeModelId(model.resolvedModel) === canonicalPreference;
|
|
7048
|
-
const resolvedMatch = models.find((model) => model.value !== "default" && matchesResolved(model)) ??
|
|
7049
|
-
models.find(matchesResolved);
|
|
7050
|
-
if (resolvedMatch)
|
|
7051
|
-
return resolvedMatch;
|
|
7052
|
-
// Substring match. Skips candidates whose context hint disagrees with the
|
|
7053
|
-
// preference's — a bare row must not absorb a 1M-hinted preference (nor
|
|
7054
|
-
// vice versa); such pairs fall through to the tokenized tier, which
|
|
7055
|
-
// weighs hints in its scoring and still finds the best same-family row.
|
|
7056
|
-
const preferenceHint = contextHintOf(trimmed);
|
|
7057
|
-
const includesMatch = models.find((model) => {
|
|
7058
|
-
if (!modelVersionsCompatible(trimmed, model))
|
|
7059
|
-
return false;
|
|
7060
|
-
if (contextHintOf(model.value) !== preferenceHint)
|
|
7061
|
-
return false;
|
|
7062
|
-
const value = model.value.toLowerCase();
|
|
7063
|
-
const display = model.displayName.toLowerCase();
|
|
7064
|
-
return value.includes(lower) || display.includes(lower) || lower.includes(value);
|
|
7065
|
-
});
|
|
7066
|
-
if (includesMatch)
|
|
7067
|
-
return includesMatch;
|
|
7068
|
-
// Tokenized matching for aliases like "opus[1m]"
|
|
7069
|
-
const { tokens, contextHint } = tokenizeModelPreference(trimmed);
|
|
7070
|
-
if (tokens.length === 0)
|
|
7071
|
-
return null;
|
|
7072
|
-
let bestMatch = null;
|
|
7073
|
-
let bestScore = 0;
|
|
7074
|
-
for (const model of models) {
|
|
7075
|
-
if (!modelVersionsCompatible(trimmed, model))
|
|
7076
|
-
continue;
|
|
7077
|
-
const score = scoreModelMatch(model, tokens, contextHint);
|
|
7078
|
-
if (0 < score && (!bestMatch || bestScore < score)) {
|
|
7079
|
-
bestMatch = model;
|
|
7080
|
-
bestScore = score;
|
|
7081
|
-
}
|
|
7082
|
-
}
|
|
7083
|
-
return bestMatch;
|
|
7084
|
-
}
|
|
7085
|
-
/** Map the live model reported by a resumed session onto the picker's model
|
|
7086
|
-
* list. The CLI restores a resumed session's model from the transcript's
|
|
7087
|
-
* last assistant message, which records the concrete API id (e.g.
|
|
7088
|
-
* "claude-opus-4-6") with any "[1m]" context hint dropped. Tiers, in order:
|
|
7089
|
-
* 1. Exact match with the Default entry's resolution — when a named alias
|
|
7090
|
-
* shares Default's resolvedModel verbatim, the live id can't tell the
|
|
7091
|
-
* two apart, and a never-customized session should stay on Default.
|
|
7092
|
-
* 2. Exact resolvedModel match on a named row. Checked before the
|
|
7093
|
-
* hint-stripped Default comparison so a live "claude-sonnet-5[1m]" lands
|
|
7094
|
-
* on the "sonnet[1m]" row rather than a Default that resolves to the
|
|
7095
|
-
* bare "claude-sonnet-5" — the two rows differ in context window, which
|
|
7096
|
-
* drives `contextWindowSize` and capability gating downstream.
|
|
7097
|
-
* 3. Hint-stripped match with Default's resolution — a session that never
|
|
7098
|
-
* left the default resumes as the bare transcript id, and shouldn't show
|
|
7099
|
-
* a concrete picker entry.
|
|
7100
|
-
* 4. `resolveModelPreference` over the picker entries.
|
|
7101
|
-
* 5. A model with no picker counterpart (e.g. excluded by an
|
|
7102
|
-
* `availableModels` allowlist) is tracked verbatim, mirroring
|
|
7103
|
-
* `syncModelAfterExternalSwitch`: the picker shows no selection, but the
|
|
7104
|
-
* model-dependent bookkeeping stays truthful to what the SDK is running. */
|
|
7105
|
-
export function matchResumedModel(models, liveModel) {
|
|
7106
|
-
const live = canonicalizeModelId(liveModel);
|
|
7107
|
-
const defaultEntry = models.find((m) => m.value === "default");
|
|
7108
|
-
const defaultResolved = defaultEntry?.resolvedModel
|
|
7109
|
-
? canonicalizeModelId(defaultEntry.resolvedModel)
|
|
7110
|
-
: undefined;
|
|
7111
|
-
if (defaultEntry && defaultResolved === live) {
|
|
7112
|
-
return defaultEntry;
|
|
7113
|
-
}
|
|
7114
|
-
// No default-row exclusion needed: a default row matching `live` exactly
|
|
7115
|
-
// already returned at the tier above.
|
|
7116
|
-
const exactMatch = models.find((m) => m.resolvedModel && canonicalizeModelId(m.resolvedModel) === live);
|
|
7117
|
-
if (exactMatch)
|
|
7118
|
-
return exactMatch;
|
|
7119
|
-
if (defaultEntry &&
|
|
7120
|
-
defaultResolved &&
|
|
7121
|
-
stripContextHints(defaultResolved) === stripContextHints(live)) {
|
|
7122
|
-
return defaultEntry;
|
|
7123
|
-
}
|
|
7124
|
-
return (resolveModelPreference(models, liveModel) ?? {
|
|
7125
|
-
value: liveModel,
|
|
7126
|
-
displayName: liveModel,
|
|
7127
|
-
description: "",
|
|
7128
|
-
});
|
|
7129
|
-
}
|
|
7130
|
-
function resolveSettingsModel(models, settingsModel, logger) {
|
|
7131
|
-
if (settingsModel === undefined) {
|
|
7132
|
-
return null;
|
|
7133
|
-
}
|
|
7134
|
-
if (typeof settingsModel !== "string") {
|
|
7135
|
-
const typeLabel = settingsModel === null ? "null" : typeof settingsModel;
|
|
7136
|
-
logger.error(`Ignoring model from settings: expected a string, got ${typeLabel}.`);
|
|
7137
|
-
return null;
|
|
7138
|
-
}
|
|
7139
|
-
return resolveModelPreference(models, settingsModel);
|
|
7140
|
-
}
|
|
7141
|
-
/**
|
|
7142
|
-
* Restrict the SDK's model list to the user's `availableModels` allowlist
|
|
7143
|
-
* (already merged-and-deduped across settings sources by `SettingsManager`).
|
|
7144
|
-
* The user's exact entries become the model IDs surfaced via configOptions
|
|
7145
|
-
* and passed to `setModel`, which prevents Claude Code from silently
|
|
7146
|
-
* substituting a date-pinned variant (e.g. `haiku` →
|
|
7147
|
-
* `claude-haiku-4-5-20251001`) that the user may not have access to.
|
|
7148
|
-
*
|
|
7149
|
-
* Display info and capability flags are copied from the closest SDK match so
|
|
7150
|
-
* the UI still renders sensible names and effort levels.
|
|
7151
|
-
*
|
|
7152
|
-
* Semantics from https://code.claude.com/docs/en/model-config#restrict-model-selection:
|
|
7153
|
-
* - `undefined` is handled by the caller (no allowlist applied).
|
|
7154
|
-
* - The Default option is unaffected by `availableModels` — it always remains
|
|
7155
|
-
* available, even when the allowlist is `[]`.
|
|
7156
|
-
*/
|
|
7157
|
-
export function applyAvailableModelsAllowlist(sdkModels, allowlist, settingsModelOverrides) {
|
|
7158
|
-
// Default is always preserved per the docs. Synthesize one if the SDK
|
|
7159
|
-
// didn't surface it so downstream code (e.g. `getAvailableModels` picking
|
|
7160
|
-
// `models[0]` as a fallback) still has something to work with.
|
|
7161
|
-
const defaultModel = sdkModels.find((m) => m.value === "default") ?? {
|
|
7162
|
-
value: "default",
|
|
7163
|
-
displayName: "Default",
|
|
7164
|
-
description: "",
|
|
7165
|
-
};
|
|
7166
|
-
const result = [defaultModel];
|
|
7167
|
-
const seen = new Set([defaultModel.value]);
|
|
7168
|
-
const sdkModelsWithoutDefault = sdkModels.filter((m) => m.value !== "default");
|
|
7169
|
-
// Bedrock/Vertex deployments enforce short aliases (e.g. "claude-opus-4-6")
|
|
7170
|
-
// in availableModels but require provider-specific IDs at the API. We still
|
|
7171
|
-
// resolve `sdkMatch` against the alias (`trimmed`) — that's what the
|
|
7172
|
-
// matching heuristics above are built for, and override targets (ARNs,
|
|
7173
|
-
// opaque provider IDs) often won't textually resemble anything in
|
|
7174
|
-
// `sdkModelsWithoutDefault`. Only the entry's surfaced `value` becomes the
|
|
7175
|
-
// override target, so it's what `setModel` ends up passing to the API.
|
|
7176
|
-
for (const entry of allowlist) {
|
|
7177
|
-
const trimmed = entry.trim();
|
|
7178
|
-
if (!trimmed || seen.has(trimmed))
|
|
7179
|
-
continue;
|
|
7180
|
-
const overridden = settingsModelOverrides?.[trimmed];
|
|
7181
|
-
const effective = overridden ?? trimmed;
|
|
7182
|
-
if (seen.has(effective))
|
|
7183
|
-
continue;
|
|
7184
|
-
const sdkMatch = resolveModelPreference(sdkModelsWithoutDefault, trimmed);
|
|
7185
|
-
if (sdkMatch) {
|
|
7186
|
-
result.push({ ...sdkMatch, value: effective });
|
|
7187
|
-
}
|
|
7188
|
-
else {
|
|
7189
|
-
result.push({ value: effective, displayName: trimmed, description: "" });
|
|
7190
|
-
}
|
|
7191
|
-
seen.add(effective);
|
|
7192
|
-
}
|
|
7193
|
-
// The custom model option (ANTHROPIC_CUSTOM_MODEL_OPTION) is exempt from the
|
|
7194
|
-
// allowlist, the same way Default is. Per the model-config docs it adds an
|
|
7195
|
-
// entry "without replacing the built-in aliases" and "appears at the bottom of
|
|
7196
|
-
// the /model picker", so we append it last and skip the allowlist filter; this
|
|
7197
|
-
// keeps a slim alias allowlist from hiding the custom model row.
|
|
7198
|
-
// https://code.claude.com/docs/en/model-config#add-a-custom-model-option
|
|
7199
|
-
const customModelOption = process.env.ANTHROPIC_CUSTOM_MODEL_OPTION?.trim();
|
|
7200
|
-
if (customModelOption && !seen.has(customModelOption)) {
|
|
7201
|
-
const customModel = sdkModels.find((m) => m.value === customModelOption);
|
|
7202
|
-
if (customModel) {
|
|
7203
|
-
result.push(customModel);
|
|
7204
|
-
seen.add(customModel.value);
|
|
7205
|
-
}
|
|
7206
|
-
}
|
|
7207
|
-
return result;
|
|
7208
|
-
}
|
|
7209
|
-
/** Whether a rejected `setModel` was vetoed by a user-configured
|
|
7210
|
-
* PreModelSwitch hook. The CLI's rejection message is the stable,
|
|
7211
|
-
* distinguishable marker ("Model switch blocked by a PreModelSwitch hook:
|
|
7212
|
-
* <reason>"); a format change makes this stop matching and the failure
|
|
7213
|
-
* falls back to the ordinary fail-loud path, no worse than before. */
|
|
7214
|
-
function isPreModelSwitchHookBlock(error) {
|
|
7215
|
-
return error instanceof Error && error.message.includes("blocked by a PreModelSwitch hook");
|
|
7216
|
-
}
|
|
7217
|
-
async function getAvailableModels(query, models, sdkModels, settingsManager, logger, isResumedSession, sessionId, resumedModelHint) {
|
|
7218
|
-
const settings = settingsManager.getSettings();
|
|
7219
|
-
let currentModel = models[0];
|
|
7220
|
-
let resolvedFromInput;
|
|
7221
|
-
// Model priority (highest to lowest):
|
|
7222
|
-
// 1. ANTHROPIC_MODEL environment variable
|
|
7223
|
-
// 2. settings.model (user configuration)
|
|
7224
|
-
// 3. the resumed session's live model (resumed sessions only)
|
|
7225
|
-
// 4. models[0] (default first model)
|
|
7226
|
-
if (process.env.ANTHROPIC_MODEL) {
|
|
7227
|
-
const match = resolveModelPreference(models, process.env.ANTHROPIC_MODEL);
|
|
7228
|
-
if (match) {
|
|
7229
|
-
currentModel = match;
|
|
7230
|
-
resolvedFromInput = process.env.ANTHROPIC_MODEL;
|
|
7231
|
-
}
|
|
7232
|
-
}
|
|
7233
|
-
else if (typeof settings.model === "string") {
|
|
7234
|
-
const match = resolveSettingsModel(models, settings.model, logger);
|
|
7235
|
-
if (match) {
|
|
7236
|
-
currentModel = match;
|
|
7237
|
-
resolvedFromInput = settings.model;
|
|
7238
|
-
}
|
|
7239
|
-
}
|
|
7240
|
-
// A resumed session restores the model from its last real assistant
|
|
7241
|
-
// transcript record. Use that same local record instead of getContextUsage:
|
|
7242
|
-
// the latter is a control request to the live CLI process and can add tens
|
|
7243
|
-
// of seconds to session/load before its first turn. No `setModel` here: the
|
|
7244
|
-
// SDK is already running this model, and pushing a picker alias back (e.g.
|
|
7245
|
-
// "opus[1m]") could change the live model rather than describe it.
|
|
7246
|
-
if (resolvedFromInput === undefined && isResumedSession) {
|
|
7247
|
-
currentModel = resumedModelHint ? matchResumedModel(models, resumedModelHint) : currentModel;
|
|
7248
|
-
}
|
|
7249
|
-
// Skip the setModel round-trip when we can prove the SDK has already landed
|
|
7250
|
-
// on the same model. Two cases qualify:
|
|
7251
|
-
// (a) No override applied — currentModel is the SDK's own default (or, on
|
|
7252
|
-
// resume, the live model read back from the SDK above); nothing to sync.
|
|
7253
|
-
// (b) The resolver returned the user's input verbatim AND that value exists
|
|
7254
|
-
// in the SDK's original model list — meaning no fuzzy match or
|
|
7255
|
-
// allowlist rewrite was involved, and the SDK (which reads the same
|
|
7256
|
-
// ANTHROPIC_MODEL / settings.json) will have arrived at the same entry.
|
|
7257
|
-
// This only holds for fresh sessions: a resumed session lands on the
|
|
7258
|
-
// transcript's model regardless of env/settings, so the override must
|
|
7259
|
-
// be re-asserted to keep the reported model truthful.
|
|
7260
|
-
// Anything else (fuzzy match, allowlist-synthesized value, alias) gets a
|
|
7261
|
-
// setModel call so we don't drift from the user's intended pin.
|
|
7262
|
-
const sdkSawSameValue = sdkModels.some((m) => m.value === currentModel.value);
|
|
7263
|
-
const skipSetModel = resolvedFromInput === undefined ||
|
|
7264
|
-
(!isResumedSession && currentModel.value === resolvedFromInput && sdkSawSameValue);
|
|
7265
|
-
if (!skipSetModel) {
|
|
7266
|
-
const setModelStartedAt = performance.now();
|
|
7267
|
-
try {
|
|
7268
|
-
await query.setModel(currentModel.value);
|
|
7269
|
-
logger.log(`[session/models] sessionId=${sessionId} phase=set-model durationMs=${Math.round(performance.now() - setModelStartedAt)} model=${currentModel.value} outcome=success`);
|
|
7270
|
-
}
|
|
7271
|
-
catch (error) {
|
|
7272
|
-
logger.log(`[session/models] sessionId=${sessionId} phase=set-model durationMs=${Math.round(performance.now() - setModelStartedAt)} model=${currentModel.value} outcome=error`);
|
|
7273
|
-
// On a fresh session the pin is a defining option — fail loudly. A
|
|
7274
|
-
// resumed session already runs fine on the transcript's model, so
|
|
7275
|
-
// failing the whole session/load over the re-assert would be worse
|
|
7276
|
-
// than loading with the pin unapplied (mirrors the setPermissionMode
|
|
7277
|
-
// containment in createSession). The SDK then stayed on the
|
|
7278
|
-
// transcript's model, so read that back rather than reporting the
|
|
7279
|
-
// pin the session isn't running.
|
|
7280
|
-
//
|
|
7281
|
-
// One fresh-session failure is advisory, not defining: a
|
|
7282
|
-
// user-configured PreModelSwitch hook can veto the pin (CLI 2.1.251+;
|
|
7283
|
-
// 'deny', or 'ask' — which headless sessions refuse), and the SDK
|
|
7284
|
-
// rejects setModel with "Model switch blocked by a PreModelSwitch
|
|
7285
|
-
// hook: …". Terminal Claude Code never lets a hook veto its startup
|
|
7286
|
-
// model (the spawn model isn't a switch), so failing session/new here
|
|
7287
|
-
// would make the same hook config fatal only over ACP. The session
|
|
7288
|
-
// stays on the SDK's own default — report that. We can't read the
|
|
7289
|
-
// live model back on this path: getContextUsage isn't serviced on a
|
|
7290
|
-
// fresh session until the first prompt turn (issues #886/#880).
|
|
7291
|
-
if (!isResumedSession) {
|
|
7292
|
-
if (!isPreModelSwitchHookBlock(error))
|
|
7293
|
-
throw error;
|
|
7294
|
-
logger.error(`Model pin "${currentModel.value}" was vetoed by a PreModelSwitch hook; staying on the default model:`, error);
|
|
7295
|
-
currentModel = models[0];
|
|
7296
|
-
}
|
|
7297
|
-
else {
|
|
7298
|
-
logger.error(`Failed to re-assert model "${currentModel.value}" on resume:`, error);
|
|
7299
|
-
currentModel = resumedModelHint ? matchResumedModel(models, resumedModelHint) : models[0];
|
|
7300
|
-
}
|
|
7301
|
-
}
|
|
7302
|
-
}
|
|
7303
|
-
return {
|
|
7304
|
-
availableModels: models.map((model) => ({
|
|
7305
|
-
modelId: model.value,
|
|
7306
|
-
name: model.displayName,
|
|
7307
|
-
description: model.description,
|
|
7308
|
-
})),
|
|
7309
|
-
currentModelId: currentModel.value,
|
|
7310
|
-
};
|
|
7311
|
-
}
|
|
7312
6947
|
function getAvailableSlashCommands(commands,
|
|
7313
6948
|
// Names the CLI tagged terminal-bound on `system`/init (their UX lives in
|
|
7314
6949
|
// the CLI's own terminal, which ACP clients aren't) — filtered alongside
|