@oh-my-pi/pi-coding-agent 17.0.9 → 17.1.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.
Files changed (285) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +1 -1
  3. package/dist/cli.js +4659 -4261
  4. package/dist/types/async/job-manager.d.ts +37 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/bench-cli.d.ts +41 -0
  7. package/dist/types/commands/bench.d.ts +15 -3
  8. package/dist/types/commands/launch.d.ts +4 -0
  9. package/dist/types/config/model-registry.d.ts +3 -1
  10. package/dist/types/config/models-config-schema.d.ts +18 -6
  11. package/dist/types/config/models-config.d.ts +14 -5
  12. package/dist/types/config/provider-globals.d.ts +2 -2
  13. package/dist/types/config/service-tier.d.ts +5 -1
  14. package/dist/types/config/settings-schema.d.ts +316 -47
  15. package/dist/types/config/settings.d.ts +7 -0
  16. package/dist/types/dap/client.d.ts +20 -0
  17. package/dist/types/exec/bash-executor.d.ts +38 -0
  18. package/dist/types/exec/direnv.d.ts +33 -0
  19. package/dist/types/export/html/args.d.ts +15 -0
  20. package/dist/types/export/html/index.d.ts +11 -26
  21. package/dist/types/export/html/web-palette.d.ts +55 -116
  22. package/dist/types/extensibility/extensions/loader.d.ts +3 -0
  23. package/dist/types/extensibility/extensions/types.d.ts +17 -2
  24. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +13 -0
  25. package/dist/types/live/controller.d.ts +40 -0
  26. package/dist/types/live/protocol.d.ts +85 -0
  27. package/dist/types/live/protocol.test.d.ts +1 -0
  28. package/dist/types/live/transport.d.ts +48 -0
  29. package/dist/types/live/visualizer.d.ts +33 -0
  30. package/dist/types/lsp/client.d.ts +16 -1
  31. package/dist/types/mcp/config-writer.test.d.ts +1 -0
  32. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  33. package/dist/types/mcp/smithery-auth.d.ts +1 -1
  34. package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
  35. package/dist/types/memory-backend/tool-names.d.ts +2 -0
  36. package/dist/types/modes/components/login-dialog.d.ts +2 -2
  37. package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
  38. package/dist/types/modes/components/settings-defs.d.ts +7 -1
  39. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
  40. package/dist/types/modes/components/tool-execution.d.ts +9 -1
  41. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  42. package/dist/types/modes/controllers/event-controller.d.ts +6 -1
  43. package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
  44. package/dist/types/modes/interactive-mode.d.ts +5 -0
  45. package/dist/types/modes/theme/theme.d.ts +1 -1
  46. package/dist/types/modes/types.d.ts +3 -1
  47. package/dist/types/modes/utils/context-usage.d.ts +12 -2
  48. package/dist/types/plan-mode/plan-files.d.ts +10 -0
  49. package/dist/types/registry/agent-lifecycle.d.ts +21 -9
  50. package/dist/types/registry/agent-registry.d.ts +11 -4
  51. package/dist/types/sdk.d.ts +15 -1
  52. package/dist/types/secrets/index.d.ts +15 -2
  53. package/dist/types/secrets/obfuscator.d.ts +113 -15
  54. package/dist/types/session/acp-permission-gate.d.ts +19 -0
  55. package/dist/types/session/agent-session-events.d.ts +84 -0
  56. package/dist/types/session/agent-session-types.d.ts +314 -0
  57. package/dist/types/session/agent-session.d.ts +118 -755
  58. package/dist/types/session/async-job-delivery.d.ts +28 -0
  59. package/dist/types/session/bash-runner.d.ts +75 -0
  60. package/dist/types/session/blob-store.d.ts +11 -1
  61. package/dist/types/session/blob-store.test.d.ts +1 -0
  62. package/dist/types/session/checkpoint-entries.d.ts +28 -0
  63. package/dist/types/session/eval-runner.d.ts +49 -0
  64. package/dist/types/session/irc-bridge.d.ts +46 -0
  65. package/dist/types/session/messages.d.ts +22 -0
  66. package/dist/types/session/model-controls.d.ts +180 -0
  67. package/dist/types/session/prewalk.d.ts +46 -0
  68. package/dist/types/session/queued-messages.d.ts +22 -0
  69. package/dist/types/session/redis-session-storage.d.ts +1 -0
  70. package/dist/types/session/retry-fallback-chains.d.ts +74 -0
  71. package/dist/types/session/role-models.d.ts +15 -0
  72. package/dist/types/session/session-advisors.d.ts +228 -0
  73. package/dist/types/session/session-entries.d.ts +8 -0
  74. package/dist/types/session/session-handoff.d.ts +70 -0
  75. package/dist/types/session/session-history-format.d.ts +6 -1
  76. package/dist/types/session/session-maintenance.d.ts +246 -0
  77. package/dist/types/session/session-manager.d.ts +41 -0
  78. package/dist/types/session/session-memory.d.ts +56 -0
  79. package/dist/types/session/session-provider-boundary.d.ts +62 -0
  80. package/dist/types/session/session-stats.d.ts +48 -0
  81. package/dist/types/session/session-tools.d.ts +143 -0
  82. package/dist/types/session/session-workspace.d.ts +32 -0
  83. package/dist/types/session/stream-guards.d.ts +44 -0
  84. package/dist/types/session/todo-tracker.d.ts +56 -0
  85. package/dist/types/session/ttsr-coordinator.d.ts +51 -0
  86. package/dist/types/session/turn-recovery.d.ts +225 -0
  87. package/dist/types/system-prompt.d.ts +2 -0
  88. package/dist/types/task/executor.d.ts +4 -1
  89. package/dist/types/task/output-manager.d.ts +3 -1
  90. package/dist/types/tools/approval.d.ts +1 -0
  91. package/dist/types/tools/browser/launch.d.ts +5 -0
  92. package/dist/types/tools/image-gen.d.ts +4 -4
  93. package/dist/types/tools/image-providers.d.ts +38 -0
  94. package/dist/types/tools/index.d.ts +5 -0
  95. package/dist/types/tools/path-utils.d.ts +8 -0
  96. package/dist/types/tools/todo.d.ts +10 -4
  97. package/dist/types/tools/xdev.d.ts +5 -1
  98. package/dist/types/tts/streaming-player.d.ts +14 -0
  99. package/dist/types/tts/vocalizer.d.ts +5 -0
  100. package/dist/types/utils/jj.d.ts +29 -0
  101. package/dist/types/utils/lang-from-path.d.ts +1 -0
  102. package/dist/types/utils/title-generator.d.ts +30 -0
  103. package/dist/types/vibe/runtime.d.ts +36 -8
  104. package/dist/types/web/search/index.d.ts +1 -1
  105. package/dist/types/web/search/provider.d.ts +15 -5
  106. package/dist/types/web/search/types.d.ts +94 -0
  107. package/package.json +12 -12
  108. package/scripts/generate-share-viewer.ts +4 -6
  109. package/src/advisor/__tests__/advisor.test.ts +643 -0
  110. package/src/advisor/runtime.ts +144 -43
  111. package/src/async/job-manager.ts +90 -2
  112. package/src/cli/args.ts +2 -0
  113. package/src/cli/auth-gateway-cli.ts +18 -3
  114. package/src/cli/bench-cli.ts +395 -29
  115. package/src/cli/flag-tables.ts +3 -0
  116. package/src/cli/gc-cli.ts +2 -2
  117. package/src/cli/usage-cli.ts +10 -1
  118. package/src/collab/host.ts +3 -2
  119. package/src/commands/bench.ts +18 -4
  120. package/src/commands/launch.ts +4 -0
  121. package/src/config/model-registry.ts +10 -2
  122. package/src/config/models-config-schema.ts +15 -4
  123. package/src/config/provider-globals.ts +9 -9
  124. package/src/config/service-tier.ts +26 -1
  125. package/src/config/settings-schema.ts +209 -51
  126. package/src/config/settings.ts +81 -2
  127. package/src/dap/client.ts +37 -4
  128. package/src/edit/index.ts +49 -12
  129. package/src/eval/py/executor.ts +103 -33
  130. package/src/exec/bash-executor.ts +89 -6
  131. package/src/exec/direnv.ts +145 -0
  132. package/src/export/html/args.ts +20 -0
  133. package/src/export/html/index.ts +45 -47
  134. package/src/export/html/template.css +19 -1
  135. package/src/export/html/template.html +6 -0
  136. package/src/export/html/template.js +21 -0
  137. package/src/export/html/tool-views.generated.js +31 -31
  138. package/src/export/html/web-palette.ts +116 -132
  139. package/src/export/share.ts +249 -49
  140. package/src/extensibility/extensions/loader.ts +29 -1
  141. package/src/extensibility/extensions/runner.ts +6 -0
  142. package/src/extensibility/extensions/types.ts +33 -2
  143. package/src/extensibility/plugins/legacy-pi-compat.ts +19 -1
  144. package/src/hindsight/state.ts +64 -6
  145. package/src/internal-urls/local-protocol.ts +2 -1
  146. package/src/internal-urls/memory-protocol.ts +2 -2
  147. package/src/internal-urls/skill-protocol.ts +2 -2
  148. package/src/internal-urls/ssh-protocol.ts +2 -1
  149. package/src/internal-urls/vault-protocol.ts +2 -1
  150. package/src/live/audio-worklet.txt +59 -0
  151. package/src/live/browser-runtime.txt +221 -0
  152. package/src/live/controller.ts +464 -0
  153. package/src/live/prompts/agent-final-message.md +3 -0
  154. package/src/live/prompts/live-instructions.md +23 -0
  155. package/src/live/protocol.test.ts +140 -0
  156. package/src/live/protocol.ts +233 -0
  157. package/src/live/transport.ts +490 -0
  158. package/src/live/visualizer.ts +234 -0
  159. package/src/lsp/client.ts +29 -9
  160. package/src/lsp/index.ts +19 -4
  161. package/src/main.ts +8 -1
  162. package/src/mcp/config-writer.test.ts +43 -0
  163. package/src/mcp/config-writer.ts +109 -82
  164. package/src/mcp/oauth-discovery.ts +10 -2
  165. package/src/mcp/smithery-auth.test.ts +29 -0
  166. package/src/mcp/smithery-auth.ts +3 -2
  167. package/src/memories/index.ts +40 -20
  168. package/src/memory-backend/tool-names.ts +2 -0
  169. package/src/mnemopi/backend.ts +24 -7
  170. package/src/modes/acp/acp-agent.ts +56 -10
  171. package/src/modes/acp/acp-event-mapper.ts +8 -1
  172. package/src/modes/components/agent-hub.ts +1 -1
  173. package/src/modes/components/login-dialog.ts +14 -4
  174. package/src/modes/components/plan-review-overlay.ts +350 -35
  175. package/src/modes/components/settings-defs.ts +23 -1
  176. package/src/modes/components/settings-selector.ts +169 -1
  177. package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
  178. package/src/modes/components/status-line/component.ts +123 -2
  179. package/src/modes/components/tool-execution.ts +11 -2
  180. package/src/modes/controllers/command-controller.ts +56 -15
  181. package/src/modes/controllers/event-controller.ts +111 -16
  182. package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
  183. package/src/modes/controllers/extension-ui-controller.ts +20 -4
  184. package/src/modes/controllers/live-command-controller.ts +178 -0
  185. package/src/modes/controllers/mcp-command-controller.ts +10 -1
  186. package/src/modes/controllers/omfg-controller.ts +44 -40
  187. package/src/modes/controllers/selector-controller.ts +18 -10
  188. package/src/modes/controllers/session-focus-controller.ts +6 -1
  189. package/src/modes/interactive-mode.ts +157 -38
  190. package/src/modes/runtime-init.ts +2 -0
  191. package/src/modes/setup-wizard/scenes/web-search.ts +17 -9
  192. package/src/modes/theme/theme.ts +1 -1
  193. package/src/modes/types.ts +3 -1
  194. package/src/modes/utils/context-usage.ts +23 -7
  195. package/src/modes/utils/ui-helpers.ts +10 -5
  196. package/src/plan-mode/plan-files.ts +40 -0
  197. package/src/prompts/bench/cache-prefix-chunk.md +1 -0
  198. package/src/prompts/bench/cache-prefix.md +3 -0
  199. package/src/prompts/bench/cache-suffix.md +1 -0
  200. package/src/prompts/system/custom-system-prompt.md +1 -1
  201. package/src/prompts/system/project-prompt.md +8 -1
  202. package/src/prompts/system/subagent-async-pending.md +6 -0
  203. package/src/prompts/system/system-prompt.md +1 -1
  204. package/src/prompts/system/workflow-notice.md +48 -40
  205. package/src/prompts/system/xdev-mount-notice.md +4 -0
  206. package/src/prompts/tools/todo.md +3 -1
  207. package/src/registry/agent-lifecycle.ts +116 -46
  208. package/src/registry/agent-registry.ts +35 -8
  209. package/src/registry/persisted-agents.ts +26 -2
  210. package/src/sdk.ts +210 -136
  211. package/src/secrets/index.ts +127 -4
  212. package/src/secrets/obfuscator.ts +2328 -105
  213. package/src/session/acp-permission-gate.ts +165 -0
  214. package/src/session/agent-session-error-log.test.ts +1 -1
  215. package/src/session/agent-session-events.ts +66 -0
  216. package/src/session/agent-session-types.ts +334 -0
  217. package/src/session/agent-session.ts +1892 -12226
  218. package/src/session/artifacts.ts +6 -5
  219. package/src/session/async-job-delivery.ts +74 -0
  220. package/src/session/bash-runner.ts +326 -0
  221. package/src/session/blob-store.test.ts +56 -0
  222. package/src/session/blob-store.ts +18 -2
  223. package/src/session/checkpoint-entries.ts +81 -0
  224. package/src/session/eval-runner.ts +212 -0
  225. package/src/session/indexed-session-storage.ts +9 -2
  226. package/src/session/irc-bridge.ts +203 -0
  227. package/src/session/messages.ts +263 -1
  228. package/src/session/model-controls.ts +714 -0
  229. package/src/session/prewalk.ts +252 -0
  230. package/src/session/queued-messages.ts +93 -0
  231. package/src/session/redis-session-storage.ts +51 -11
  232. package/src/session/retry-fallback-chains.ts +455 -0
  233. package/src/session/role-models.ts +85 -0
  234. package/src/session/session-advisors.ts +1679 -0
  235. package/src/session/session-context.ts +6 -3
  236. package/src/session/session-entries.ts +8 -0
  237. package/src/session/session-handoff.ts +308 -0
  238. package/src/session/session-history-format.ts +20 -9
  239. package/src/session/session-loader.ts +0 -46
  240. package/src/session/session-maintenance.ts +2983 -0
  241. package/src/session/session-manager.ts +390 -30
  242. package/src/session/session-memory.ts +222 -0
  243. package/src/session/session-provider-boundary.ts +307 -0
  244. package/src/session/session-stats.ts +293 -0
  245. package/src/session/session-tools.ts +943 -0
  246. package/src/session/session-workspace.ts +53 -0
  247. package/src/session/stream-guards.ts +417 -0
  248. package/src/session/todo-tracker.ts +380 -0
  249. package/src/session/ttsr-coordinator.ts +496 -0
  250. package/src/session/turn-recovery.ts +1629 -0
  251. package/src/slash-commands/builtin-registry.ts +92 -11
  252. package/src/slash-commands/helpers/usage-report.ts +22 -3
  253. package/src/system-prompt.ts +41 -17
  254. package/src/task/executor.ts +285 -70
  255. package/src/task/index.ts +98 -105
  256. package/src/task/output-manager.ts +25 -3
  257. package/src/task/persisted-revive.ts +4 -3
  258. package/src/task/structured-subagent.ts +1 -0
  259. package/src/tools/approval.ts +55 -10
  260. package/src/tools/bash-interactive.ts +26 -0
  261. package/src/tools/bash-skill-urls.ts +28 -2
  262. package/src/tools/bash.ts +339 -123
  263. package/src/tools/browser/launch.ts +9 -1
  264. package/src/tools/browser/tab-supervisor.ts +8 -5
  265. package/src/tools/browser.ts +94 -47
  266. package/src/tools/image-gen.ts +20 -28
  267. package/src/tools/image-providers.ts +50 -0
  268. package/src/tools/index.ts +9 -0
  269. package/src/tools/path-utils.ts +49 -2
  270. package/src/tools/read.ts +156 -88
  271. package/src/tools/renderers.ts +7 -1
  272. package/src/tools/todo.ts +101 -11
  273. package/src/tools/vibe.ts +1 -2
  274. package/src/tools/write.ts +51 -1
  275. package/src/tools/xdev.ts +67 -3
  276. package/src/tts/streaming-player.ts +62 -4
  277. package/src/tts/vocalizer.ts +18 -1
  278. package/src/utils/jj.ts +125 -4
  279. package/src/utils/lang-from-path.ts +7 -0
  280. package/src/utils/title-generator.ts +137 -1
  281. package/src/vibe/runtime.ts +852 -72
  282. package/src/web/search/index.ts +3 -6
  283. package/src/web/search/provider.ts +31 -16
  284. package/src/web/search/providers/xai.ts +7 -1
  285. package/src/web/search/types.ts +3 -0
@@ -0,0 +1,714 @@
1
+ import { type Agent, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
+ import type { Model, ProviderSessionState, ServiceTier, ServiceTierByFamily, ServiceTierFamily } from "@oh-my-pi/pi-ai";
3
+ import {
4
+ clearAnthropicFastModeFallback,
5
+ Effort,
6
+ realizesPriorityServiceTier,
7
+ resolveModelServiceTier,
8
+ serviceTierFamily,
9
+ } from "@oh-my-pi/pi-ai";
10
+ import { isFireworksFastModelId } from "@oh-my-pi/pi-catalog/fireworks-model-id";
11
+ import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
12
+ import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
13
+ import { logger } from "@oh-my-pi/pi-utils";
14
+ import { classifyDifficulty } from "../auto-thinking/classifier";
15
+ import type { ModelRegistry } from "../config/model-registry";
16
+ import {
17
+ filterAvailableModelsByEnabledPatterns,
18
+ formatModelStringWithRouting,
19
+ getModelMatchPreferences,
20
+ type ResolvedModelRoleValue,
21
+ resolveModelRoleValue,
22
+ } from "../config/model-resolver";
23
+ import { getKnownRoleIds } from "../config/model-roles";
24
+ import type { Settings } from "../config/settings";
25
+ import { containsUltrathink } from "../modes/ultrathink";
26
+ import {
27
+ AUTO_THINKING,
28
+ type ConfiguredThinkingLevel,
29
+ clampAutoThinkingEffort,
30
+ resolveProvisionalAutoLevel,
31
+ resolveThinkingLevelForModel,
32
+ shouldDisableReasoning,
33
+ toReasoningEffort,
34
+ } from "../thinking";
35
+ import type { EditMode } from "../utils/edit-mode";
36
+ import type { AgentSessionEvent } from "./agent-session-events";
37
+ import type { ModelCycleResult, ResolvedRoleModel, RoleModelCycle, RoleModelCycleResult } from "./agent-session-types";
38
+ import { formatRoleModelValue, resolveRoleModelFull } from "./role-models";
39
+ import { EPHEMERAL_MODEL_CHANGE_ROLE } from "./session-entries";
40
+ import type { SessionManager } from "./session-manager";
41
+
42
+ /** Capabilities borrowed from the owning AgentSession. */
43
+ export interface ModelControlsHost {
44
+ agent: Agent;
45
+ settings: Settings;
46
+ modelRegistry: ModelRegistry;
47
+ sessionManager: SessionManager;
48
+ providerSessionState: Map<string, ProviderSessionState>;
49
+ model(): Model | undefined;
50
+ sessionId(): string;
51
+ promptGeneration(): number;
52
+ resolveActiveEditMode(): EditMode;
53
+ syncAfterModelChange(previousEditMode: EditMode): Promise<void>;
54
+ setModelWithProviderSessionReset(model: Model): void;
55
+ clearActiveRetryFallback(): void;
56
+ clearInheritedProviderPromptCacheKey(): void;
57
+ magicKeywordEnabled(keyword: "orchestrate" | "ultrathink" | "workflow"): boolean;
58
+ emit(event: AgentSessionEvent): void;
59
+ emitSessionEvent(event: AgentSessionEvent): Promise<void>;
60
+ emitNotice(level: "info" | "warning" | "error", message: string, source?: string): void;
61
+ }
62
+
63
+ /** Owns model selection, thinking effort, role cycling, and service tiers. */
64
+ export class ModelControls {
65
+ readonly #host: ModelControlsHost;
66
+ #scopedModels: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
67
+ #thinkingLevel: ThinkingLevel | undefined;
68
+ #autoThinking = false;
69
+ #autoResolvedLevel: Effort | undefined;
70
+ #serviceTierByFamily: ServiceTierByFamily;
71
+
72
+ constructor(
73
+ host: ModelControlsHost,
74
+ options: {
75
+ scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
76
+ thinkingLevel?: ConfiguredThinkingLevel;
77
+ serviceTierByFamily?: ServiceTierByFamily;
78
+ },
79
+ ) {
80
+ this.#host = host;
81
+ this.#scopedModels = options.scopedModels ?? [];
82
+ this.#serviceTierByFamily = options.serviceTierByFamily ?? {};
83
+ if (options.thinkingLevel === AUTO_THINKING) {
84
+ // Keep auto pending until the first turn while exposing a valid wire effort.
85
+ this.#autoThinking = true;
86
+ this.#thinkingLevel = resolveProvisionalAutoLevel(this.#model);
87
+ } else {
88
+ this.#thinkingLevel = options.thinkingLevel;
89
+ }
90
+ this.#applyThinkingLevelToAgent(this.#thinkingLevel);
91
+ }
92
+
93
+ get #model(): Model | undefined {
94
+ return this.#host.model();
95
+ }
96
+
97
+ /** Effective metadata-clamped thinking level applied to the agent. */
98
+ get thinkingLevel(): ThinkingLevel | undefined {
99
+ return this.#thinkingLevel;
100
+ }
101
+
102
+ /** Configured selector, preserving `auto` while classification is active. */
103
+ configuredThinkingLevel(): ConfiguredThinkingLevel | undefined {
104
+ return this.#autoThinking ? AUTO_THINKING : this.#thinkingLevel;
105
+ }
106
+
107
+ /** Whether per-turn automatic thinking classification is enabled. */
108
+ get isAutoThinking(): boolean {
109
+ return this.#autoThinking;
110
+ }
111
+
112
+ /** Last concrete effort selected by automatic classification. */
113
+ get autoResolvedThinkingLevel(): Effort | undefined {
114
+ return this.#autoResolvedLevel;
115
+ }
116
+
117
+ /** Models explicitly scoped to the session's cycle command. */
118
+ get scopedModels(): ReadonlyArray<{ model: Model; thinkingLevel?: ThinkingLevel }> {
119
+ return this.#scopedModels;
120
+ }
121
+
122
+ /** Live per-provider-family service-tier selection. */
123
+ get serviceTierByFamily(): ServiceTierByFamily {
124
+ return this.#serviceTierByFamily;
125
+ }
126
+
127
+ /** Restores thinking state from a transcript without persisting a new entry. */
128
+ restoreThinkingLevel(level: ConfiguredThinkingLevel | undefined): void {
129
+ this.#autoThinking = level === AUTO_THINKING;
130
+ this.#autoResolvedLevel = undefined;
131
+ this.#thinkingLevel =
132
+ level === AUTO_THINKING
133
+ ? resolveProvisionalAutoLevel(this.#model)
134
+ : resolveThinkingLevelForModel(this.#model, level);
135
+ this.#applyThinkingLevelToAgent(this.#thinkingLevel);
136
+ }
137
+
138
+ /** Restores an exact thinking snapshot after a failed session switch. */
139
+ restoreThinkingSnapshot(level: ThinkingLevel | undefined, auto: boolean, resolved: Effort | undefined): void {
140
+ this.#thinkingLevel = level;
141
+ this.#autoThinking = auto;
142
+ this.#autoResolvedLevel = resolved;
143
+ this.#applyThinkingLevelToAgent(level);
144
+ }
145
+
146
+ /** Restores service tiers without persisting a duplicate transcript entry. */
147
+ restoreServiceTiers(tiers: ServiceTierByFamily): void {
148
+ this.#serviceTierByFamily = tiers;
149
+ }
150
+ resolveRoleModel(role: string): Model | undefined {
151
+ return resolveRoleModelFull(this.#host.settings, role, this.#host.modelRegistry.getAvailable(), this.#model)
152
+ .model;
153
+ }
154
+
155
+ resolveRoleModelWithThinking(role: string): ResolvedModelRoleValue {
156
+ return resolveRoleModelFull(this.#host.settings, role, this.#host.modelRegistry.getAvailable(), this.#model);
157
+ }
158
+
159
+ resolveTemporaryModelThinkingLevel(model: Model): ConfiguredThinkingLevel | undefined {
160
+ const availableModels = this.#host.modelRegistry.getAvailable();
161
+ if (availableModels.length === 0) return undefined;
162
+
163
+ const matchPreferences = getModelMatchPreferences(this.#host.settings);
164
+ for (const role of getKnownRoleIds(this.#host.settings)) {
165
+ const roleValue = this.#host.settings.getModelRole(role);
166
+ if (!roleValue) continue;
167
+
168
+ const resolved = resolveModelRoleValue(roleValue, availableModels, {
169
+ settings: this.#host.settings,
170
+ matchPreferences,
171
+ });
172
+ if (!resolved.explicitThinkingLevel || resolved.thinkingLevel === undefined || !resolved.model) continue;
173
+ if (modelsAreEqual(resolved.model, model)) return resolved.thinkingLevel;
174
+ }
175
+
176
+ return undefined;
177
+ }
178
+
179
+ async setModel(
180
+ model: Model,
181
+ role: string = "default",
182
+ options?: {
183
+ selector?: string;
184
+ thinkingLevel?: ThinkingLevel;
185
+ persist?: boolean;
186
+ currentContextTokens?: number;
187
+ },
188
+ ): Promise<{ switched: boolean }> {
189
+ const previousEditMode = this.#host.resolveActiveEditMode();
190
+ if (!this.#host.modelRegistry.hasConfiguredAuth(model)) {
191
+ throw new Error(`No API key for ${model.provider}/${model.id}`);
192
+ }
193
+
194
+ const targetModel = await this.#host.modelRegistry.refreshSelectedModelMetadata(model);
195
+
196
+ this.#host.modelRegistry.clearSuppressedSelector(formatModelStringWithRouting(targetModel));
197
+ this.#host.clearActiveRetryFallback();
198
+ this.#host.setModelWithProviderSessionReset(targetModel);
199
+ this.#host.sessionManager.appendModelChange(`${targetModel.provider}/${targetModel.id}`, role);
200
+ if (options?.persist) {
201
+ this.#host.settings.setModelRole(
202
+ role,
203
+ formatRoleModelValue(
204
+ this.#host.settings,
205
+ this.#host.modelRegistry,
206
+ role,
207
+ targetModel,
208
+ options.selector,
209
+ options.thinkingLevel,
210
+ ),
211
+ );
212
+ }
213
+ this.#host.settings.getStorage()?.recordModelUsage(`${targetModel.provider}/${targetModel.id}`);
214
+
215
+ // Re-apply thinking for the newly selected model. Prefer the model's
216
+ // configured defaultLevel; otherwise preserve the current level (or auto).
217
+ this.#reapplyThinkingLevel(targetModel.thinking?.defaultLevel);
218
+ await this.#host.syncAfterModelChange(previousEditMode);
219
+ return { switched: true };
220
+ }
221
+
222
+ /**
223
+ * Set model temporarily (for this session only).
224
+ * Validates that a credential source is configured (synchronously, without
225
+ * refreshing OAuth or running command-backed key programs), saves to session
226
+ * log but NOT to settings.
227
+ * @throws Error if no API key available for the model
228
+ */
229
+ async setModelTemporary(
230
+ model: Model,
231
+ thinkingLevel?: ConfiguredThinkingLevel,
232
+ options?: { ephemeral?: boolean },
233
+ ): Promise<void> {
234
+ const previousEditMode = this.#host.resolveActiveEditMode();
235
+ if (!this.#host.modelRegistry.hasConfiguredAuth(model)) {
236
+ throw new Error(`No API key for ${model.provider}/${model.id}`);
237
+ }
238
+
239
+ const targetModel = await this.#host.modelRegistry.refreshSelectedModelMetadata(model);
240
+
241
+ this.#host.modelRegistry.clearSuppressedSelector(formatModelStringWithRouting(targetModel));
242
+ this.#host.clearActiveRetryFallback();
243
+ this.#host.setModelWithProviderSessionReset(targetModel);
244
+ this.#host.sessionManager.appendModelChange(
245
+ `${targetModel.provider}/${targetModel.id}`,
246
+ options?.ephemeral ? EPHEMERAL_MODEL_CHANGE_ROLE : "temporary",
247
+ );
248
+ this.#host.settings.getStorage()?.recordModelUsage(`${targetModel.provider}/${targetModel.id}`);
249
+
250
+ // Apply explicit thinking level if given; otherwise prefer the model's
251
+ // configured defaultLevel; otherwise re-clamp the current level (or auto).
252
+ if (thinkingLevel !== undefined) {
253
+ this.setThinkingLevel(thinkingLevel);
254
+ } else {
255
+ this.#reapplyThinkingLevel(targetModel.thinking?.defaultLevel);
256
+ }
257
+ await this.#host.syncAfterModelChange(previousEditMode);
258
+ }
259
+
260
+ /**
261
+ * Cycle to next/previous model.
262
+ * Uses scoped models (from --models flag) if available, otherwise all available models.
263
+ * @param direction - "forward" (default) or "backward"
264
+ * @returns The new model info, or undefined if only one model available
265
+ */
266
+ async cycleModel(direction: "forward" | "backward" = "forward"): Promise<ModelCycleResult | undefined> {
267
+ if (this.#scopedModels.length > 0) {
268
+ return this.#cycleScopedModel(direction);
269
+ }
270
+ return this.#cycleAvailableModel(direction);
271
+ }
272
+
273
+ /**
274
+ * Resolve the configured role models in the given order plus the index of
275
+ * the currently active one. Roles that have no configured model, or whose
276
+ * configured model is not currently available, are skipped. The `default`
277
+ * role falls back to the active model when no explicit assignment exists.
278
+ *
279
+ * Returns `undefined` only when there is no current model or no available
280
+ * models at all; an empty `models` array is never returned (callers should
281
+ * still guard on `models.length`).
282
+ */
283
+ getRoleModelCycle(roleOrder: readonly string[]): RoleModelCycle | undefined {
284
+ const availableModels = this.#host.modelRegistry.getAvailable();
285
+ if (availableModels.length === 0) return undefined;
286
+
287
+ const currentModel = this.#model;
288
+ if (!currentModel) return undefined;
289
+ const matchPreferences = getModelMatchPreferences(this.#host.settings);
290
+ const models: ResolvedRoleModel[] = [];
291
+
292
+ for (const role of roleOrder) {
293
+ const roleModelStr =
294
+ role === "default"
295
+ ? (this.#host.settings.getModelRole("default") ?? `${currentModel.provider}/${currentModel.id}`)
296
+ : this.#host.settings.getModelRole(role);
297
+ if (!roleModelStr) continue;
298
+
299
+ const resolved = resolveModelRoleValue(roleModelStr, availableModels, {
300
+ settings: this.#host.settings,
301
+ matchPreferences,
302
+ });
303
+ if (!resolved.model) continue;
304
+
305
+ models.push({
306
+ role,
307
+ model: resolved.model,
308
+ thinkingLevel: resolved.thinkingLevel,
309
+ explicitThinkingLevel: resolved.explicitThinkingLevel,
310
+ });
311
+ }
312
+
313
+ if (models.length === 0) return undefined;
314
+
315
+ // Trust the recorded role only while its resolved model still IS the
316
+ // active model. A model switch through another surface (alt+m, retry
317
+ // fallback, /model) or a role re-configuration leaves the recorded role
318
+ // pointing at a model the session no longer runs; cycling from that
319
+ // stale slot lands on the wrong neighbor and reads as a skipped entry.
320
+ const lastRole = this.#host.sessionManager.getLastModelChangeRole();
321
+ let currentIndex = lastRole ? models.findIndex(entry => entry.role === lastRole) : -1;
322
+ if (currentIndex !== -1 && !modelsAreEqual(models[currentIndex].model, currentModel)) {
323
+ currentIndex = -1;
324
+ }
325
+ if (currentIndex === -1) {
326
+ currentIndex = models.findIndex(entry => modelsAreEqual(entry.model, currentModel));
327
+ }
328
+ if (currentIndex === -1) currentIndex = 0;
329
+
330
+ return { models, currentIndex };
331
+ }
332
+
333
+ /**
334
+ * Apply a resolved role model as the active model without changing global
335
+ * settings. Shared with role cycling and the plan-approval model slider.
336
+ */
337
+ async applyRoleModel(entry: ResolvedRoleModel): Promise<void> {
338
+ await this.setModel(entry.model, entry.role);
339
+ if (entry.explicitThinkingLevel && entry.thinkingLevel !== undefined) {
340
+ this.setThinkingLevel(entry.thinkingLevel);
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Cycle through configured role models in a fixed order.
346
+ * Skips missing roles and changes only the active session model.
347
+ * @param roleOrder - Order of roles to cycle through (e.g., ["slow", "default", "smol"])
348
+ * @param direction - "forward" (default) or "backward"
349
+ */
350
+ async cycleRoleModels(
351
+ roleOrder: readonly string[],
352
+ direction: "forward" | "backward" = "forward",
353
+ ): Promise<RoleModelCycleResult | undefined> {
354
+ const cycle = this.getRoleModelCycle(roleOrder);
355
+ if (!cycle || cycle.models.length <= 1) return undefined;
356
+
357
+ const step = direction === "backward" ? -1 : 1;
358
+ const next = cycle.models[(cycle.currentIndex + step + cycle.models.length) % cycle.models.length];
359
+
360
+ await this.applyRoleModel(next);
361
+
362
+ return { model: next.model, thinkingLevel: this.thinkingLevel, role: next.role };
363
+ }
364
+
365
+ async #getScopedModelsWithApiKey(): Promise<Array<{ model: Model; thinkingLevel?: ThinkingLevel }>> {
366
+ const apiKeysByProvider = new Map<string, string | undefined>();
367
+ const result: Array<{ model: Model; thinkingLevel?: ThinkingLevel }> = [];
368
+
369
+ for (const scoped of this.#scopedModels) {
370
+ const provider = scoped.model.provider;
371
+ let apiKey: string | undefined;
372
+ if (apiKeysByProvider.has(provider)) {
373
+ apiKey = apiKeysByProvider.get(provider);
374
+ } else {
375
+ apiKey = await this.#host.modelRegistry.getApiKeyForProvider(provider, this.#host.sessionId());
376
+ apiKeysByProvider.set(provider, apiKey);
377
+ }
378
+
379
+ if (apiKey) {
380
+ result.push(scoped);
381
+ }
382
+ }
383
+
384
+ return result;
385
+ }
386
+
387
+ async #cycleScopedModel(direction: "forward" | "backward"): Promise<ModelCycleResult | undefined> {
388
+ const previousEditMode = this.#host.resolveActiveEditMode();
389
+ const scopedModels = await this.#getScopedModelsWithApiKey();
390
+ if (scopedModels.length <= 1) return undefined;
391
+
392
+ const currentModel = this.#model;
393
+ let currentIndex = scopedModels.findIndex(sm => modelsAreEqual(sm.model, currentModel));
394
+
395
+ if (currentIndex === -1) currentIndex = 0;
396
+ const len = scopedModels.length;
397
+ const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
398
+ const next = scopedModels[nextIndex];
399
+
400
+ // Apply model
401
+ this.#host.modelRegistry.clearSuppressedSelector(formatModelStringWithRouting(next.model));
402
+ this.#host.clearActiveRetryFallback();
403
+ this.#host.setModelWithProviderSessionReset(next.model);
404
+ this.#host.sessionManager.appendModelChange(`${next.model.provider}/${next.model.id}`);
405
+ this.#host.settings.getStorage()?.recordModelUsage(`${next.model.provider}/${next.model.id}`);
406
+
407
+ // Apply the scoped model's configured thinking level, preserving auto.
408
+ this.setThinkingLevel(this.#autoThinking ? AUTO_THINKING : next.thinkingLevel);
409
+ await this.#host.syncAfterModelChange(previousEditMode);
410
+
411
+ return { model: next.model, thinkingLevel: this.thinkingLevel, isScoped: true };
412
+ }
413
+
414
+ async #cycleAvailableModel(direction: "forward" | "backward"): Promise<ModelCycleResult | undefined> {
415
+ const previousEditMode = this.#host.resolveActiveEditMode();
416
+ const availableModels = this.#host.modelRegistry.getAvailable();
417
+ if (availableModels.length <= 1) return undefined;
418
+
419
+ const currentModel = this.#model;
420
+ let currentIndex = availableModels.findIndex(m => modelsAreEqual(m, currentModel));
421
+
422
+ if (currentIndex === -1) currentIndex = 0;
423
+ const len = availableModels.length;
424
+ const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
425
+ const nextModel = availableModels[nextIndex];
426
+
427
+ const apiKey = await this.#host.modelRegistry.getApiKey(nextModel, this.#host.sessionId());
428
+ if (!apiKey) {
429
+ throw new Error(`No API key for ${nextModel.provider}/${nextModel.id}`);
430
+ }
431
+
432
+ this.#host.modelRegistry.clearSuppressedSelector(formatModelStringWithRouting(nextModel));
433
+ this.#host.clearActiveRetryFallback();
434
+ this.#host.setModelWithProviderSessionReset(nextModel);
435
+ this.#host.sessionManager.appendModelChange(`${nextModel.provider}/${nextModel.id}`);
436
+ this.#host.settings.getStorage()?.recordModelUsage(`${nextModel.provider}/${nextModel.id}`);
437
+ // Re-apply the current thinking level (or auto) for the newly selected model
438
+ this.#reapplyThinkingLevel();
439
+ await this.#host.syncAfterModelChange(previousEditMode);
440
+
441
+ return { model: nextModel, thinkingLevel: this.thinkingLevel, isScoped: false };
442
+ }
443
+
444
+ /**
445
+ * Get all available models with valid API keys, filtered by `enabledModels` when configured.
446
+ * See {@link filterAvailableModelsByEnabledPatterns} for supported pattern forms and limitations.
447
+ */
448
+ getAvailableModels(): Model[] {
449
+ const all = this.#host.modelRegistry.getAvailable();
450
+ const patterns = this.#host.settings.get("enabledModels");
451
+ if (!patterns || patterns.length === 0) return all;
452
+ return filterAvailableModelsByEnabledPatterns(all, patterns, this.#host.settings);
453
+ }
454
+
455
+ // =========================================================================
456
+ // Thinking Level Management
457
+ // =========================================================================
458
+
459
+ #applyThinkingLevelToAgent(level: ThinkingLevel | undefined): void {
460
+ this.#host.agent.setThinkingLevel(toReasoningEffort(level));
461
+ this.#host.agent.setDisableReasoning(shouldDisableReasoning(level));
462
+ }
463
+
464
+ /**
465
+ * Set the thinking level. `auto` enables per-turn classification. Entering
466
+ * auto writes its provisional level plus `configured: "auto"` immediately,
467
+ * giving external readers an authoritative selection receipt before the next
468
+ * user turn. Later classifications persist only changed concrete resolutions.
469
+ */
470
+ setThinkingLevel(level: ConfiguredThinkingLevel | undefined, persist: boolean = false): void {
471
+ if (level === AUTO_THINKING) {
472
+ const provisional = resolveProvisionalAutoLevel(this.#model);
473
+ const wasAuto = this.#autoThinking;
474
+ const previousLevel = this.#thinkingLevel;
475
+ this.#autoThinking = true;
476
+ this.#autoResolvedLevel = undefined;
477
+ this.#thinkingLevel = provisional;
478
+ if (!wasAuto) {
479
+ this.#host.clearInheritedProviderPromptCacheKey();
480
+ }
481
+ this.#applyThinkingLevelToAgent(provisional);
482
+ if (persist) {
483
+ this.#host.settings.set("defaultThinkingLevel", AUTO_THINKING);
484
+ }
485
+ const isChanging = !wasAuto || previousLevel !== provisional;
486
+ if (isChanging) {
487
+ this.#host.sessionManager.appendThinkingLevelChange(provisional, AUTO_THINKING);
488
+ this.#host.emit({ type: "thinking_level_changed", thinkingLevel: provisional, configured: AUTO_THINKING });
489
+ }
490
+ return;
491
+ }
492
+
493
+ const wasAuto = this.#autoThinking;
494
+ this.#autoThinking = false;
495
+ this.#autoResolvedLevel = undefined;
496
+ const effectiveLevel = resolveThinkingLevelForModel(this.#model, level);
497
+ // Leaving auto must persist even when the resolved effort is unchanged (e.g.
498
+ // auto resolved to medium, then the user pins medium): otherwise the latest
499
+ // session entry keeps `configured: "auto"` and resume re-enables auto.
500
+ const isChanging = wasAuto || effectiveLevel !== this.#thinkingLevel;
501
+
502
+ this.#thinkingLevel = effectiveLevel;
503
+ this.#applyThinkingLevelToAgent(effectiveLevel);
504
+
505
+ if (isChanging) {
506
+ this.#host.clearInheritedProviderPromptCacheKey();
507
+ this.#host.sessionManager.appendThinkingLevelChange(effectiveLevel, effectiveLevel);
508
+ if (persist && effectiveLevel !== undefined && effectiveLevel !== ThinkingLevel.Off) {
509
+ this.#host.settings.set("defaultThinkingLevel", effectiveLevel);
510
+ }
511
+ this.#host.emit({ type: "thinking_level_changed", thinkingLevel: effectiveLevel });
512
+ }
513
+ }
514
+
515
+ /**
516
+ * Re-apply the active thinking selection after a model change. Preserves `auto`
517
+ * (re-clamping the provisional level to the new model); otherwise re-applies the
518
+ * preferred default or the current effective level.
519
+ */
520
+ #reapplyThinkingLevel(preferredDefault?: ThinkingLevel): void {
521
+ this.setThinkingLevel(this.#autoThinking ? AUTO_THINKING : (preferredDefault ?? this.#thinkingLevel));
522
+ }
523
+
524
+ /**
525
+ * Cycle to next thinking level: off → auto → minimal..max → off.
526
+ * @returns New selector, or undefined if model doesn't support thinking
527
+ */
528
+ cycleThinkingLevel(): ConfiguredThinkingLevel | undefined {
529
+ if (!this.#model?.reasoning) return undefined;
530
+
531
+ const levels: ConfiguredThinkingLevel[] = [
532
+ ThinkingLevel.Off,
533
+ AUTO_THINKING,
534
+ ...this.getAvailableThinkingLevels(),
535
+ ];
536
+ const configured = this.configuredThinkingLevel();
537
+ const currentLevel = configured === ThinkingLevel.Inherit ? ThinkingLevel.Off : configured;
538
+ const currentIndex = currentLevel ? levels.indexOf(currentLevel) : -1;
539
+ const nextIndex = (currentIndex + 1) % levels.length;
540
+ const nextLevel = levels[nextIndex];
541
+ if (!nextLevel) return undefined;
542
+
543
+ this.setThinkingLevel(nextLevel);
544
+ return nextLevel;
545
+ }
546
+
547
+ /** Timeout (ms) for per-turn auto-thinking classification before falling back. */
548
+ static readonly #AUTO_THINKING_TIMEOUT_MS = 4000;
549
+
550
+ /**
551
+ * Classify the current user turn and set the effective thinking level for it.
552
+ * Bounded by a timeout + abort; on any failure (no smol model, timeout, parse
553
+ * error) it falls back to the provisional concrete level and continues. Never
554
+ * throws into the turn, and never clears `#autoThinking` (auto stays active).
555
+ */
556
+ async applyAutoThinkingLevel(promptText: string, generation: number): Promise<void> {
557
+ const model = this.#model;
558
+ if (!model?.reasoning) return;
559
+ // Models with reasoning but no controllable effort surface (devin-agent
560
+ // Cascade routes effort via sibling model ids, not a wire param) have
561
+ // nothing to pick — skip classification rather than discard its result.
562
+ if (getSupportedEfforts(model).length === 0) return;
563
+
564
+ let resolved: Effort | undefined;
565
+ if (this.#host.magicKeywordEnabled("ultrathink") && containsUltrathink(promptText)) {
566
+ // The user explicitly asked for maximum thinking; bypass the classifier
567
+ // (and its xhigh auto ceiling) and jump straight to the highest
568
+ // supported level for this model.
569
+ resolved = clampAutoThinkingEffort(model, Effort.Max);
570
+ } else {
571
+ const controller = new AbortController();
572
+ const timer = setTimeout(() => controller.abort(), ModelControls.#AUTO_THINKING_TIMEOUT_MS);
573
+ try {
574
+ resolved = await classifyDifficulty(promptText, {
575
+ settings: this.#host.settings,
576
+ registry: this.#host.modelRegistry,
577
+ model,
578
+ sessionId: this.#host.sessionId(),
579
+ signal: controller.signal,
580
+ metadataResolver: provider => this.#host.agent.metadataForProvider(provider),
581
+ });
582
+ } catch (error) {
583
+ logger.debug("auto-thinking: classification failed; using fallback level", {
584
+ error: error instanceof Error ? error.message : String(error),
585
+ });
586
+ } finally {
587
+ clearTimeout(timer);
588
+ }
589
+ }
590
+
591
+ // Drop the result if the turn was aborted/superseded while classifying.
592
+ if (this.#host.promptGeneration() !== generation || !this.#autoThinking) return;
593
+
594
+ const effort = resolved ?? resolveProvisionalAutoLevel(model);
595
+ if (effort === undefined) return;
596
+ const shouldPersistResolution = this.#thinkingLevel !== effort;
597
+ this.#autoResolvedLevel = effort;
598
+ this.#thinkingLevel = effort;
599
+ this.#applyThinkingLevelToAgent(effort);
600
+ if (shouldPersistResolution) {
601
+ this.#host.sessionManager.appendThinkingLevelChange(effort, AUTO_THINKING);
602
+ }
603
+ this.#host.emit({
604
+ type: "thinking_level_changed",
605
+ thinkingLevel: effort,
606
+ configured: AUTO_THINKING,
607
+ resolved: effort,
608
+ });
609
+ }
610
+
611
+ /**
612
+ * True when the currently selected model's family is set to `priority` — the
613
+ * `/fast` on/off state for the active model. Returns false when no model is
614
+ * selected or the model exposes no service-tier family (e.g. Fireworks, which
615
+ * has its own Providers › Fireworks Tier toggle).
616
+ *
617
+ * For "is priority actually applied to the next request?" use
618
+ * {@link isFastModeActive} instead.
619
+ */
620
+ isFastModeEnabled(): boolean {
621
+ const family = this.#model ? serviceTierFamily(this.#model) : undefined;
622
+ return family ? this.#serviceTierByFamily[family] === "priority" : false;
623
+ }
624
+
625
+ /**
626
+ * True when `priority` is actually realized on the wire for the currently
627
+ * selected model (OpenAI/Google `service_tier`, direct Anthropic fast mode,
628
+ * or Fireworks priority). Returns false for tiers the active model can't
629
+ * realize and when no model is selected.
630
+ */
631
+ isFastModeActive(): boolean {
632
+ const model = this.#model;
633
+ return !!model && realizesPriorityServiceTier(this.effectiveServiceTier(model), model);
634
+ }
635
+
636
+ /**
637
+ * Effective wire service-tier for a request to `model`. Fireworks models take
638
+ * the Priority serving path only when the Providers › Fireworks Tier setting
639
+ * is `"priority"` (and never for `-fast` variants, whose Fast serving path is
640
+ * mutually exclusive with Priority). Every other model resolves the live
641
+ * per-family tier map down to the entry for its family.
642
+ */
643
+ effectiveServiceTier(model: Model | undefined = this.#model): ServiceTier | undefined {
644
+ if (model?.provider === "fireworks") {
645
+ return this.#host.settings.get("providers.fireworksTier") === "priority" && !isFireworksFastModelId(model.id)
646
+ ? "priority"
647
+ : undefined;
648
+ }
649
+ if (!model) return undefined;
650
+ return resolveModelServiceTier(this.#serviceTierByFamily, model);
651
+ }
652
+
653
+ /** The live per-family tier map, or `null` when empty (for session persistence). */
654
+ serviceTierEntry(): ServiceTierByFamily | null {
655
+ return Object.keys(this.#serviceTierByFamily).length > 0 ? this.#serviceTierByFamily : null;
656
+ }
657
+
658
+ /** Set one family's tier (or clear it with `undefined`); persists the change. */
659
+ setServiceTierFamily(family: ServiceTierFamily, tier: ServiceTier | undefined): void {
660
+ if (this.#serviceTierByFamily[family] === tier) return;
661
+ const next: ServiceTierByFamily = { ...this.#serviceTierByFamily };
662
+ if (tier) next[family] = tier;
663
+ else delete next[family];
664
+ this.#applyServiceTierByFamily(next);
665
+ }
666
+
667
+ /** Replace the whole per-family tier map; persists + re-arms Anthropic fast mode. */
668
+ #applyServiceTierByFamily(next: ServiceTierByFamily): void {
669
+ // Re-arming Anthropic priority clears the per-session fast-mode auto-disable
670
+ // so the next request actually carries `speed: "fast"` again.
671
+ if (next.anthropic === "priority" && this.#serviceTierByFamily.anthropic !== "priority") {
672
+ clearAnthropicFastModeFallback(this.#host.providerSessionState);
673
+ }
674
+ this.#serviceTierByFamily = next;
675
+ this.#host.sessionManager.appendServiceTierChange(this.serviceTierEntry());
676
+ }
677
+
678
+ /**
679
+ * `/fast on|off` targets the family of the currently selected model: it sets
680
+ * (or clears) that family's `priority` tier. Returns `false` when the model
681
+ * has no service-tier family, so callers can report that fast mode is
682
+ * unavailable instead of claiming success.
683
+ */
684
+ setFastMode(enabled: boolean): boolean {
685
+ const family = this.#model ? serviceTierFamily(this.#model) : undefined;
686
+ if (!family) {
687
+ this.#host.emitNotice(
688
+ "info",
689
+ "The current model has no service-tier control for /fast to toggle.",
690
+ "priority",
691
+ );
692
+ return false;
693
+ }
694
+ if (!enabled) {
695
+ if (this.#serviceTierByFamily[family] === "priority") this.setServiceTierFamily(family, undefined);
696
+ return true;
697
+ }
698
+ this.setServiceTierFamily(family, "priority");
699
+ return true;
700
+ }
701
+
702
+ toggleFastMode(): boolean {
703
+ if (!this.setFastMode(!this.isFastModeEnabled())) return false;
704
+ return this.isFastModeEnabled();
705
+ }
706
+
707
+ /**
708
+ * Get available thinking levels for current model.
709
+ */
710
+ getAvailableThinkingLevels(): ReadonlyArray<Effort> {
711
+ if (!this.#model) return [];
712
+ return getSupportedEfforts(this.#model);
713
+ }
714
+ }