@oh-my-pi/pi-coding-agent 16.1.23 → 16.2.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 (254) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/cli.js +3817 -3735
  3. package/dist/types/advisor/__tests__/emission-guard.test.d.ts +1 -0
  4. package/dist/types/advisor/emission-guard.d.ts +73 -0
  5. package/dist/types/advisor/index.d.ts +1 -0
  6. package/dist/types/advisor/runtime.d.ts +7 -0
  7. package/dist/types/advisor/watchdog.d.ts +2 -0
  8. package/dist/types/cli/flag-tables.d.ts +1 -0
  9. package/dist/types/cli/gallery-fixtures/search.d.ts +1 -1
  10. package/dist/types/collab/display-name.d.ts +3 -0
  11. package/dist/types/collab/host.d.ts +0 -2
  12. package/dist/types/commands/worktree.d.ts +0 -3
  13. package/dist/types/config/inline-tool-descriptors-mode.d.ts +13 -0
  14. package/dist/types/config/model-discovery.d.ts +6 -1
  15. package/dist/types/config/model-registry.d.ts +6 -2
  16. package/dist/types/config/model-resolver.d.ts +12 -0
  17. package/dist/types/config/models-config-schema.d.ts +29 -2
  18. package/dist/types/config/models-config.d.ts +22 -1
  19. package/dist/types/config/settings-schema.d.ts +91 -21
  20. package/dist/types/config/settings.d.ts +2 -0
  21. package/dist/types/dap/config.d.ts +1 -1
  22. package/dist/types/edit/hashline/filesystem.d.ts +4 -2
  23. package/dist/types/edit/renderer.d.ts +4 -0
  24. package/dist/types/extensibility/extensions/types.d.ts +17 -17
  25. package/dist/types/extensibility/hooks/types.d.ts +10 -10
  26. package/dist/types/extensibility/plugins/marketplace/types.d.ts +1 -0
  27. package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +1 -0
  28. package/dist/types/internal-urls/index.d.ts +1 -0
  29. package/dist/types/internal-urls/registry-helpers.d.ts +2 -0
  30. package/dist/types/internal-urls/router.d.ts +1 -1
  31. package/dist/types/internal-urls/ssh-protocol.d.ts +10 -0
  32. package/dist/types/internal-urls/types.d.ts +19 -2
  33. package/dist/types/irc/bus.d.ts +6 -0
  34. package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +1 -0
  35. package/dist/types/modes/components/custom-editor.d.ts +7 -0
  36. package/dist/types/modes/components/move-overlay.d.ts +23 -0
  37. package/dist/types/modes/components/plugin-selector.d.ts +2 -1
  38. package/dist/types/modes/components/queue-mode-selector.d.ts +2 -1
  39. package/dist/types/modes/components/select-list-mouse-routing.d.ts +2 -0
  40. package/dist/types/modes/components/settings-defs.d.ts +4 -1
  41. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  42. package/dist/types/modes/components/show-images-selector.d.ts +2 -1
  43. package/dist/types/modes/components/status-line/component.d.ts +2 -0
  44. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  45. package/dist/types/modes/components/theme-selector.d.ts +2 -1
  46. package/dist/types/modes/components/thinking-selector.d.ts +2 -1
  47. package/dist/types/modes/controllers/command-controller.d.ts +10 -1
  48. package/dist/types/modes/controllers/streaming-reveal.d.ts +6 -0
  49. package/dist/types/modes/interactive-mode.d.ts +10 -1
  50. package/dist/types/modes/internal-url-autocomplete.d.ts +1 -1
  51. package/dist/types/modes/running-subagent-badge.d.ts +6 -0
  52. package/dist/types/modes/theme/theme.d.ts +1 -1
  53. package/dist/types/modes/types.d.ts +9 -1
  54. package/dist/types/sdk.d.ts +2 -2
  55. package/dist/types/session/agent-session.d.ts +2 -1
  56. package/dist/types/session/session-manager.d.ts +13 -0
  57. package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
  58. package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +1 -0
  59. package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +1 -0
  60. package/dist/types/ssh/connection-manager.d.ts +2 -0
  61. package/dist/types/ssh/file-transfer.d.ts +79 -0
  62. package/dist/types/ssh/utils.d.ts +6 -0
  63. package/dist/types/system-prompt.d.ts +3 -0
  64. package/dist/types/task/executor.d.ts +16 -0
  65. package/dist/types/tiny/text.d.ts +1 -1
  66. package/dist/types/tools/builtin-names.d.ts +5 -1
  67. package/dist/types/tools/{find.d.ts → glob.d.ts} +15 -15
  68. package/dist/types/tools/{search.d.ts → grep.d.ts} +14 -14
  69. package/dist/types/tools/index.d.ts +3 -3
  70. package/dist/types/tools/path-utils.d.ts +29 -0
  71. package/dist/types/tools/read.d.ts +2 -2
  72. package/dist/types/tools/render-utils.d.ts +8 -0
  73. package/dist/types/tools/renderers.d.ts +11 -0
  74. package/dist/types/tools/ssh.d.ts +2 -0
  75. package/dist/types/tools/todo.d.ts +0 -16
  76. package/dist/types/tools/write.d.ts +2 -2
  77. package/dist/types/utils/active-repo-context.d.ts +8 -0
  78. package/dist/types/utils/image-resize.d.ts +1 -0
  79. package/dist/types/utils/markit-cache.d.ts +23 -0
  80. package/dist/types/utils/markit.d.ts +5 -1
  81. package/dist/types/utils/prompt-path.d.ts +1 -0
  82. package/dist/types/web/search/providers/duckduckgo.d.ts +14 -0
  83. package/dist/types/web/search/providers/firecrawl.d.ts +28 -0
  84. package/dist/types/web/search/providers/tinyfish.d.ts +29 -0
  85. package/dist/types/web/search/providers/xai.d.ts +13 -0
  86. package/dist/types/web/search/types.d.ts +18 -2
  87. package/package.json +30 -15
  88. package/scripts/build-binary.ts +9 -9
  89. package/scripts/bundle-dist.ts +2 -2
  90. package/src/advisor/__tests__/advisor.test.ts +40 -4
  91. package/src/advisor/__tests__/emission-guard.test.ts +147 -0
  92. package/src/advisor/advise-tool.ts +1 -1
  93. package/src/advisor/emission-guard.ts +172 -0
  94. package/src/advisor/index.ts +1 -0
  95. package/src/advisor/runtime.ts +11 -0
  96. package/src/advisor/watchdog.ts +12 -1
  97. package/src/cli/args.ts +5 -2
  98. package/src/cli/auth-broker-cli.ts +17 -0
  99. package/src/cli/config-cli.ts +4 -0
  100. package/src/cli/flag-tables.ts +7 -4
  101. package/src/cli/gallery-cli.ts +14 -2
  102. package/src/cli/gallery-fixtures/edit.ts +60 -0
  103. package/src/cli/gallery-fixtures/fs.ts +17 -17
  104. package/src/cli/gallery-fixtures/search.ts +4 -4
  105. package/src/cli/usage-cli.ts +20 -1
  106. package/src/cli/web-search-cli.ts +1 -1
  107. package/src/collab/display-name.ts +13 -0
  108. package/src/collab/guest.ts +5 -1
  109. package/src/collab/host.ts +2 -13
  110. package/src/commands/worktree.ts +6 -0
  111. package/src/config/inline-tool-descriptors-mode.ts +27 -0
  112. package/src/config/model-discovery.ts +77 -8
  113. package/src/config/model-registry.ts +89 -11
  114. package/src/config/model-resolver.ts +39 -1
  115. package/src/config/models-config-schema.ts +41 -5
  116. package/src/config/models-config.ts +4 -1
  117. package/src/config/settings-schema.ts +85 -27
  118. package/src/config/settings.ts +172 -1
  119. package/src/cursor.ts +1 -1
  120. package/src/dap/config.ts +152 -8
  121. package/src/dap/session.ts +1 -1
  122. package/src/discovery/helpers.ts +3 -1
  123. package/src/edit/hashline/diff.ts +14 -3
  124. package/src/edit/hashline/execute.ts +42 -6
  125. package/src/edit/hashline/filesystem.ts +38 -2
  126. package/src/edit/index.ts +1 -0
  127. package/src/edit/modes/patch.ts +11 -1
  128. package/src/edit/renderer.ts +140 -13
  129. package/src/eval/__tests__/agent-bridge.test.ts +101 -0
  130. package/src/eval/agent-bridge.ts +26 -3
  131. package/src/eval/js/tool-bridge.ts +2 -2
  132. package/src/export/html/index.ts +1 -1
  133. package/src/export/html/template.js +3 -1
  134. package/src/export/html/tool-views.generated.js +20 -20
  135. package/src/extensibility/extensions/types.ts +22 -22
  136. package/src/extensibility/hooks/types.ts +11 -11
  137. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +8 -3
  138. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +25 -10
  139. package/src/extensibility/plugins/marketplace/manager.ts +22 -0
  140. package/src/extensibility/plugins/marketplace/types.ts +1 -0
  141. package/src/internal-urls/__tests__/ssh-protocol.test.ts +331 -0
  142. package/src/internal-urls/docs-index.generated.txt +2 -2
  143. package/src/internal-urls/docs-index.ts +2 -3
  144. package/src/internal-urls/index.ts +1 -0
  145. package/src/internal-urls/registry-helpers.ts +19 -4
  146. package/src/internal-urls/router.ts +5 -3
  147. package/src/internal-urls/ssh-protocol.ts +367 -0
  148. package/src/internal-urls/types.ts +19 -2
  149. package/src/irc/bus.ts +11 -3
  150. package/src/mcp/tool-bridge.ts +32 -2
  151. package/src/memories/index.ts +1 -1
  152. package/src/modes/acp/acp-event-mapper.ts +2 -2
  153. package/src/modes/components/__tests__/move-overlay.test.ts +166 -0
  154. package/src/modes/components/agent-hub.ts +3 -0
  155. package/src/modes/components/agent-transcript-viewer.ts +9 -7
  156. package/src/modes/components/custom-editor.ts +90 -5
  157. package/src/modes/components/move-overlay.ts +282 -0
  158. package/src/modes/components/plan-review-overlay.ts +35 -35
  159. package/src/modes/components/plugin-selector.ts +6 -1
  160. package/src/modes/components/queue-mode-selector.ts +6 -1
  161. package/src/modes/components/select-list-mouse-routing.ts +35 -0
  162. package/src/modes/components/session-selector.ts +11 -10
  163. package/src/modes/components/settings-defs.ts +14 -1
  164. package/src/modes/components/settings-selector.ts +196 -29
  165. package/src/modes/components/show-images-selector.ts +6 -1
  166. package/src/modes/components/status-line/component.ts +108 -28
  167. package/src/modes/components/status-line/segments.ts +5 -1
  168. package/src/modes/components/status-line/types.ts +2 -0
  169. package/src/modes/components/theme-selector.ts +6 -1
  170. package/src/modes/components/thinking-selector.ts +6 -1
  171. package/src/modes/components/tool-execution.ts +25 -9
  172. package/src/modes/components/tree-selector.ts +5 -5
  173. package/src/modes/controllers/command-controller.ts +140 -47
  174. package/src/modes/controllers/event-controller.ts +59 -3
  175. package/src/modes/controllers/input-controller.ts +68 -2
  176. package/src/modes/controllers/selector-controller.ts +5 -2
  177. package/src/modes/controllers/streaming-reveal.ts +17 -0
  178. package/src/modes/controllers/tool-args-reveal.ts +1 -1
  179. package/src/modes/interactive-mode.ts +116 -50
  180. package/src/modes/internal-url-autocomplete.ts +17 -2
  181. package/src/modes/prompt-action-autocomplete.ts +3 -1
  182. package/src/modes/running-subagent-badge.ts +13 -0
  183. package/src/modes/setup-wizard/scenes/glyph.ts +2 -13
  184. package/src/modes/setup-wizard/scenes/providers.ts +2 -1
  185. package/src/modes/setup-wizard/scenes/theme.ts +6 -12
  186. package/src/modes/setup-wizard/scenes/web-search.ts +8 -13
  187. package/src/modes/setup-wizard/wizard-overlay.ts +6 -5
  188. package/src/modes/theme/theme.ts +9 -1
  189. package/src/modes/types.ts +9 -1
  190. package/src/modes/utils/interactive-context-helpers.ts +1 -1
  191. package/src/priority.json +15 -0
  192. package/src/prompts/advisor/active-repo-watchdog.md +6 -0
  193. package/src/prompts/advisor/system.md +21 -7
  194. package/src/prompts/agents/designer.md +1 -1
  195. package/src/prompts/agents/explore.md +1 -1
  196. package/src/prompts/agents/librarian.md +2 -2
  197. package/src/prompts/agents/plan.md +2 -2
  198. package/src/prompts/agents/reviewer.md +1 -1
  199. package/src/prompts/agents/task.md +2 -2
  200. package/src/prompts/system/active-repo-context.md +4 -0
  201. package/src/prompts/system/gemini-tool-call-reminder.md +9 -0
  202. package/src/prompts/system/plan-mode-active.md +3 -3
  203. package/src/prompts/system/project-prompt.md +2 -2
  204. package/src/prompts/system/system-prompt.md +6 -6
  205. package/src/prompts/tools/bash.md +2 -2
  206. package/src/prompts/tools/checkpoint.md +1 -1
  207. package/src/prompts/tools/{find.md → glob.md} +1 -1
  208. package/src/prompts/tools/{search.md → grep.md} +3 -3
  209. package/src/prompts/tools/read.md +4 -2
  210. package/src/sdk.ts +55 -25
  211. package/src/session/agent-session.ts +508 -132
  212. package/src/session/session-history-format.ts +2 -2
  213. package/src/session/session-manager.ts +46 -0
  214. package/src/slash-commands/builtin-registry.ts +138 -20
  215. package/src/slash-commands/helpers/usage-report.ts +23 -2
  216. package/src/ssh/__tests__/connection-manager-args.test.ts +69 -0
  217. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +68 -0
  218. package/src/ssh/connection-manager.ts +12 -15
  219. package/src/ssh/file-transfer.ts +209 -0
  220. package/src/ssh/utils.ts +24 -0
  221. package/src/system-prompt.ts +56 -28
  222. package/src/task/executor.ts +96 -36
  223. package/src/task/index.ts +3 -3
  224. package/src/task/render.ts +14 -13
  225. package/src/task/worktree.ts +38 -1
  226. package/src/tiny/text.ts +49 -4
  227. package/src/tiny/worker.ts +3 -3
  228. package/src/tools/bash.ts +2 -2
  229. package/src/tools/builtin-names.ts +26 -2
  230. package/src/tools/{find.ts → glob.ts} +48 -42
  231. package/src/tools/{search.ts → grep.ts} +311 -129
  232. package/src/tools/index.ts +13 -14
  233. package/src/tools/irc.ts +6 -2
  234. package/src/tools/path-utils.ts +64 -1
  235. package/src/tools/read.ts +35 -6
  236. package/src/tools/render-utils.ts +14 -0
  237. package/src/tools/renderers.ts +15 -4
  238. package/src/tools/ssh.ts +17 -2
  239. package/src/tools/todo.ts +4 -26
  240. package/src/tools/write.ts +25 -9
  241. package/src/utils/active-repo-context.ts +143 -0
  242. package/src/utils/image-resize.ts +88 -7
  243. package/src/utils/lang-from-path.ts +3 -3
  244. package/src/utils/markit-cache.ts +166 -0
  245. package/src/utils/markit.ts +86 -18
  246. package/src/utils/prompt-path.ts +3 -0
  247. package/src/utils/title-generator.ts +1 -1
  248. package/src/web/search/provider.ts +54 -34
  249. package/src/web/search/providers/duckduckgo.ts +140 -0
  250. package/src/web/search/providers/firecrawl.ts +144 -0
  251. package/src/web/search/providers/tinyfish.ts +159 -0
  252. package/src/web/search/providers/xai.ts +292 -0
  253. package/src/web/search/providers/zai.ts +142 -56
  254. package/src/web/search/types.ts +6 -2
@@ -11,7 +11,6 @@
11
11
 
12
12
  import { timingSafeEqual } from "node:crypto";
13
13
  import * as fs from "node:fs/promises";
14
- import * as os from "node:os";
15
14
  import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
16
15
  import { logger } from "@oh-my-pi/pi-utils";
17
16
  import type { BusChannel, AgentEvent as WireAgentEvent, SessionEntry as WireSessionEntry } from "@oh-my-pi/pi-wire";
@@ -21,8 +20,9 @@ import { type AgentRef, AgentRegistry } from "../registry/agent-registry";
21
20
  import type { AgentSessionEvent } from "../session/agent-session";
22
21
  import { stripImagesFromMessage, USER_INTERRUPT_LABEL } from "../session/messages";
23
22
  import type { SessionEntry as StoredSessionEntry } from "../session/session-entries";
24
- import { TASK_SUBAGENT_LIFECYCLE_CHANNEL, TASK_SUBAGENT_PROGRESS_CHANNEL } from "../task";
23
+ import { TASK_SUBAGENT_LIFECYCLE_CHANNEL, TASK_SUBAGENT_PROGRESS_CHANNEL } from "../task/types";
25
24
  import { generateRoomKey, generateWriteToken, importRoomKey } from "./crypto";
25
+ import { collabDisplayName } from "./display-name";
26
26
  import {
27
27
  type AgentSnapshot,
28
28
  COLLAB_PROMPT_MESSAGE_TYPE,
@@ -102,17 +102,6 @@ const TRANSCRIPT_READ_CAP = 4 * 1024 * 1024;
102
102
  */
103
103
  const SNAPSHOT_CHUNK_BYTES = 512 * 1024;
104
104
 
105
- /** Display name for this process's user in collab sessions. */
106
- export function collabDisplayName(ctx: InteractiveModeContext): string {
107
- const configured = (ctx.settings.get("collab.displayName") ?? "").trim();
108
- if (configured) return configured;
109
- try {
110
- return os.userInfo().username;
111
- } catch {
112
- return "anonymous";
113
- }
114
- }
115
-
116
105
  export class CollabHost {
117
106
  #ctx: InteractiveModeContext;
118
107
  #socket: CollabSocket | null = null;
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * List and clean up agent-managed git worktrees under `~/.omp/wt`.
3
3
  */
4
+ import { getProjectDir } from "@oh-my-pi/pi-utils";
4
5
  import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
5
6
  import { clearWorktrees, listWorktrees } from "../cli/worktree-cli";
7
+ import { Settings } from "../config/settings";
6
8
 
7
9
  export default class Worktree extends Command {
8
10
  static description = "List or clear agent-managed git worktrees (~/.omp/wt)";
@@ -43,6 +45,10 @@ export default class Worktree extends Command {
43
45
 
44
46
  async run(): Promise<void> {
45
47
  const { args, flags } = await this.parse(Worktree);
48
+ // Load settings so the `worktree.base` override is applied before we scan
49
+ // — otherwise this command would inspect ~/.omp/wt while the agent created
50
+ // its worktrees under the configured base.
51
+ await Settings.init({ cwd: getProjectDir() });
46
52
  if (args.action === "clear") {
47
53
  await clearWorktrees({
48
54
  all: flags.all ?? false,
@@ -0,0 +1,27 @@
1
+ import { modelFamilyToken } from "@oh-my-pi/pi-catalog/identity";
2
+
3
+ /**
4
+ * Resolves whether full tool descriptors should be inlined into the system
5
+ * prompt (and stripped from provider tool schemas) for a given model and
6
+ * setting.
7
+ *
8
+ * `auto` enforces a per-model policy: inline for Gemini models, off otherwise.
9
+ * Gemini benefits from descriptors in-prompt; other providers keep them in the
10
+ * tool schemas. `on`/`off` are explicit user overrides.
11
+ *
12
+ * @param modelId Canonical model id (e.g. `gemini-3-pro`); resolve aliases via
13
+ * `ModelRegistry.getCanonicalId` before calling so `auto` classifies correctly.
14
+ */
15
+ export function shouldInlineToolDescriptors(
16
+ setting: "auto" | "on" | "off" | undefined,
17
+ modelId: string | undefined,
18
+ ): boolean {
19
+ switch (setting ?? "auto") {
20
+ case "on":
21
+ return true;
22
+ case "off":
23
+ return false;
24
+ default:
25
+ return modelId !== undefined && modelFamilyToken(modelId) === "gemini";
26
+ }
27
+ }
@@ -6,14 +6,19 @@
6
6
  * discovery lives in pi-catalog's provider-models.
7
7
  */
8
8
  import { type ApiKey, type FetchImpl, withAuth } from "@oh-my-pi/pi-ai";
9
- import type { Api, Model } from "@oh-my-pi/pi-ai/types";
9
+ import type { Api, Model, RemoteCompactionConfig } from "@oh-my-pi/pi-ai/types";
10
10
  import { buildModel } from "@oh-my-pi/pi-catalog/build";
11
11
  import {
12
12
  getBundledModelReferenceIndex,
13
13
  resolveModelReference,
14
14
  stripBracketedModelIdAffixes,
15
15
  } from "@oh-my-pi/pi-catalog/identity";
16
- import { fetchLmStudioNativeModelMetadata } from "@oh-my-pi/pi-catalog/provider-models/openai-compat";
16
+ import {
17
+ fetchLiteLLMRichModels,
18
+ fetchLmStudioNativeModelMetadata,
19
+ OPENAI_COMPAT_DISCOVERY_DEFAULT_CONTEXT_WINDOW,
20
+ OPENAI_COMPAT_DISCOVERY_DEFAULT_MAX_TOKENS,
21
+ } from "@oh-my-pi/pi-catalog/provider-models/openai-compat";
17
22
  import type { ModelSpec } from "@oh-my-pi/pi-catalog/types";
18
23
  import { isRecord } from "@oh-my-pi/pi-utils";
19
24
  import type { ProviderDiscovery } from "./models-config-schema";
@@ -27,7 +32,8 @@ import type { ProviderDiscovery } from "./models-config-schema";
27
32
  // mid-stream when models hit the cap on legitimate large tool calls (see
28
33
  // issue #1528: `write` payloads >~5KB on deepseek-v4-pro surfaced as
29
34
  // "socket connection was closed unexpectedly").
30
- export const DISCOVERY_DEFAULT_MAX_TOKENS = 32_768;
35
+ export const DISCOVERY_DEFAULT_CONTEXT_WINDOW = OPENAI_COMPAT_DISCOVERY_DEFAULT_CONTEXT_WINDOW;
36
+ export const DISCOVERY_DEFAULT_MAX_TOKENS = OPENAI_COMPAT_DISCOVERY_DEFAULT_MAX_TOKENS;
31
37
 
32
38
  const DEFAULT_OLLAMA_BASE_URL = "http://127.0.0.1:11434";
33
39
  const OLLAMA_HOST_DEFAULT_PORT = "11434";
@@ -89,6 +95,7 @@ export interface DiscoveryProviderConfig {
89
95
  baseUrl?: string;
90
96
  headers?: Record<string, string>;
91
97
  compat?: ModelSpec<Api>["compat"];
98
+ remoteCompaction?: RemoteCompactionConfig<Api>;
92
99
  discovery: ProviderDiscovery;
93
100
  optional?: boolean;
94
101
  }
@@ -209,6 +216,8 @@ export function discoverModelsByProviderType(
209
216
  return discoverOpenAIModelsList(providerConfig, ctx);
210
217
  case "proxy":
211
218
  return discoverProxyModels(providerConfig, ctx);
219
+ case "litellm":
220
+ return discoverLiteLLMModels(providerConfig, ctx);
212
221
  }
213
222
  }
214
223
 
@@ -302,7 +311,7 @@ export async function discoverOllamaModels(
302
311
  input: metadata?.input ?? ["text"],
303
312
  imageInputDecoder: "stb",
304
313
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
305
- contextWindow: metadata?.contextWindow ?? 128000,
314
+ contextWindow: metadata?.contextWindow ?? DISCOVERY_DEFAULT_CONTEXT_WINDOW,
306
315
  maxTokens: Math.min(metadata?.contextWindow ?? Number.POSITIVE_INFINITY, DISCOVERY_DEFAULT_MAX_TOKENS),
307
316
  headers: providerConfig.headers,
308
317
  } as ModelSpec<Api>);
@@ -369,7 +378,7 @@ export async function discoverLlamaCppModels(
369
378
  for (const item of models) {
370
379
  const { id } = item;
371
380
  if (!id) continue;
372
- const contextWindow = item.contextWindow ?? serverMetadata?.contextWindow ?? 128000;
381
+ const contextWindow = item.contextWindow ?? serverMetadata?.contextWindow ?? DISCOVERY_DEFAULT_CONTEXT_WINDOW;
373
382
  discovered.push(
374
383
  buildModel({
375
384
  id,
@@ -467,7 +476,7 @@ export async function discoverOpenAIModelsList(
467
476
  toPositiveNumberOrUndefined(item.max_model_len) ??
468
477
  toPositiveNumberOrUndefined(item.context_length) ??
469
478
  nativeMetadataForModel?.contextWindow ??
470
- 128000;
479
+ DISCOVERY_DEFAULT_CONTEXT_WINDOW;
471
480
  discovered.push(
472
481
  buildModel({
473
482
  id,
@@ -493,6 +502,59 @@ export async function discoverOpenAIModelsList(
493
502
  return discovered;
494
503
  }
495
504
 
505
+ export async function discoverLiteLLMModels(
506
+ providerConfig: DiscoveryProviderConfig,
507
+ ctx: DiscoveryContext,
508
+ ): Promise<Model<Api>[]> {
509
+ const baseUrl = normalizeLiteLLMDiscoveryBaseUrl(providerConfig.baseUrl);
510
+ const references = getBundledModelReferenceIndex();
511
+ const resolveReference = (id: string) => resolveModelReference(id, references) as ModelSpec<Api> | undefined;
512
+ const baseHeaders: Record<string, string> = { ...(providerConfig.headers ?? {}) };
513
+ let headers = baseHeaders;
514
+ const attempt = async (h: Record<string, string>) => {
515
+ headers = h;
516
+ let authError: (Error & { status: number }) | undefined;
517
+ const authAwareFetch: FetchImpl = async (input, init) => {
518
+ const response = await ctx.fetch(input, init);
519
+ if (response.status === 401) {
520
+ authError = new Error(`HTTP ${response.status} from ${String(input)}`) as Error & { status: number };
521
+ authError.status = response.status;
522
+ }
523
+ return response;
524
+ };
525
+ const models = await fetchLiteLLMRichModels({
526
+ api: providerConfig.api,
527
+ provider: providerConfig.provider,
528
+ baseUrl,
529
+ headers: h,
530
+ fetch: authAwareFetch,
531
+ referenceResolver: resolveReference,
532
+ signal: AbortSignal.timeout(10_000),
533
+ });
534
+ if (authError && models === null) {
535
+ throw authError;
536
+ }
537
+ return models;
538
+ };
539
+ const apiKey = await ctx.getBearerApiKeyResolver(providerConfig.provider);
540
+ let richModels: ModelSpec<Api>[] | null;
541
+ try {
542
+ richModels = apiKey
543
+ ? await withAuth(apiKey, key => attempt({ ...baseHeaders, Authorization: `Bearer ${key}` }))
544
+ : await attempt(baseHeaders);
545
+ } catch (error) {
546
+ const status = typeof error === "object" && error !== null && "status" in error ? error.status : undefined;
547
+ if (status !== 401) {
548
+ throw error;
549
+ }
550
+ richModels = null;
551
+ }
552
+ if (!richModels || richModels.length === 0) {
553
+ return discoverOpenAIModelsList({ ...providerConfig, baseUrl }, ctx);
554
+ }
555
+ return richModels.map(spec => buildModel({ ...spec, headers }));
556
+ }
557
+
496
558
  /**
497
559
  * Discover models from an Anthropic+OpenAI-compatible reseller proxy that
498
560
  * exposes both `/v1/messages` and `/v1/chat/completions`, advertising each
@@ -571,7 +633,10 @@ export async function discoverProxyModels(
571
633
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
572
634
  // Prefer the context_length the API reports for this model; fall
573
635
  // back to the bundled reference, then a sane default.
574
- contextWindow: toPositiveNumberOrUndefined(item.context_length) ?? reference?.contextWindow ?? 128000,
636
+ contextWindow:
637
+ toPositiveNumberOrUndefined(item.context_length) ??
638
+ reference?.contextWindow ??
639
+ DISCOVERY_DEFAULT_CONTEXT_WINDOW,
575
640
  maxTokens: reference?.maxTokens ?? discoveryDefaultMaxTokens(api),
576
641
  headers,
577
642
  // OpenAI-compat fields are no-ops on anthropic models; the
@@ -616,7 +681,11 @@ function toLlamaCppNativeBaseUrl(baseUrl: string): string {
616
681
  }
617
682
  }
618
683
 
619
- function normalizeOpenAIModelsListBaseUrl(baseUrl?: string): string {
684
+ export function normalizeLiteLLMDiscoveryBaseUrl(baseUrl?: string): string {
685
+ return normalizeOpenAIModelsListBaseUrl(baseUrl ?? "http://localhost:4000/v1");
686
+ }
687
+
688
+ export function normalizeOpenAIModelsListBaseUrl(baseUrl?: string): string {
620
689
  const defaultBaseUrl = "http://127.0.0.1:1234/v1";
621
690
  const raw = baseUrl || defaultBaseUrl;
622
691
  try {
@@ -1,7 +1,15 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import * as path from "node:path";
3
3
  import { registerCustomApi, unregisterCustomApis } from "@oh-my-pi/pi-ai/api-registry";
4
- import type { Api, Context, Model, ModelSpec, SimpleStreamOptions, ThinkingConfig } from "@oh-my-pi/pi-ai/types";
4
+ import type {
5
+ Api,
6
+ Context,
7
+ Model,
8
+ ModelSpec,
9
+ RemoteCompactionConfig,
10
+ SimpleStreamOptions,
11
+ ThinkingConfig,
12
+ } from "@oh-my-pi/pi-ai/types";
5
13
  import type { AssistantMessageEventStream } from "@oh-my-pi/pi-ai/utils/event-stream";
6
14
  import { buildModel } from "@oh-my-pi/pi-catalog/build";
7
15
  import { isVertexExpressOpenAIUrl } from "@oh-my-pi/pi-catalog/hosts";
@@ -71,6 +79,7 @@ import {
71
79
  discoverModelsByProviderType,
72
80
  getImplicitOllamaBaseUrl,
73
81
  getOllamaContextLengthOverride,
82
+ normalizeLiteLLMDiscoveryBaseUrl,
74
83
  } from "./model-discovery";
75
84
  import { ModelsConfigFile, type ProviderValidationModel, validateProviderConfiguration } from "./models-config";
76
85
  import type { ModelOverride, ModelsConfig, ProviderAuthMode } from "./models-config-schema";
@@ -95,6 +104,7 @@ interface ProviderOverride {
95
104
  apiKey?: string;
96
105
  authHeader?: boolean;
97
106
  compat?: ModelSpec<Api>["compat"];
107
+ remoteCompaction?: RemoteCompactionConfig<Api>;
98
108
  transport?: Model<Api>["transport"];
99
109
  }
100
110
 
@@ -127,7 +137,7 @@ interface ProviderOverride {
127
137
  export function mergeDiscoveredModel<TApi extends Api>(
128
138
  model: Model<TApi>,
129
139
  existing: Model<Api> | undefined,
130
- providerOverride?: Pick<ProviderOverride, "baseUrl" | "headers" | "transport">,
140
+ providerOverride?: Pick<ProviderOverride, "baseUrl" | "headers" | "remoteCompaction" | "transport">,
131
141
  ): Model<TApi> {
132
142
  if (existing) {
133
143
  const supportsTools = model.supportsTools ?? existing.supportsTools;
@@ -136,6 +146,10 @@ export function mergeDiscoveredModel<TApi extends Api>(
136
146
  baseUrl: providerOverride?.baseUrl ?? model.baseUrl ?? existing.baseUrl,
137
147
  headers: existing.headers ? { ...existing.headers, ...model.headers } : model.headers,
138
148
  transport: providerOverride?.transport ?? existing.transport ?? model.transport,
149
+ remoteCompaction: mergeProviderRemoteCompactionConfig(
150
+ mergeRemoteCompactionConfig(existing.remoteCompaction, model.remoteCompaction),
151
+ providerOverride?.remoteCompaction,
152
+ ),
139
153
  ...(supportsTools !== undefined ? { supportsTools } : {}),
140
154
  compat: model.compatConfig,
141
155
  } as ModelSpec<TApi>);
@@ -146,6 +160,10 @@ export function mergeDiscoveredModel<TApi extends Api>(
146
160
  baseUrl: providerOverride.baseUrl ?? model.baseUrl,
147
161
  headers: providerOverride.headers ? { ...model.headers, ...providerOverride.headers } : model.headers,
148
162
  ...(providerOverride.transport !== undefined ? { transport: providerOverride.transport } : {}),
163
+ remoteCompaction: mergeProviderRemoteCompactionConfig(
164
+ model.remoteCompaction,
165
+ providerOverride.remoteCompaction,
166
+ ),
149
167
  compat: model.compatConfig,
150
168
  } as ModelSpec<TApi>);
151
169
  }
@@ -353,6 +371,22 @@ function mergeCompat<TBase extends object, TOverride extends object>(
353
371
  return merged as TBase & TOverride;
354
372
  }
355
373
 
374
+ function mergeRemoteCompactionConfig(
375
+ baseConfig: RemoteCompactionConfig<Api> | undefined,
376
+ overrideConfig: RemoteCompactionConfig<Api> | undefined,
377
+ ): RemoteCompactionConfig<Api> | undefined {
378
+ if (!baseConfig) return overrideConfig;
379
+ if (!overrideConfig) return baseConfig;
380
+ return { ...baseConfig, ...overrideConfig };
381
+ }
382
+
383
+ function mergeProviderRemoteCompactionConfig(
384
+ modelConfig: RemoteCompactionConfig<Api> | undefined,
385
+ providerConfig: RemoteCompactionConfig<Api> | undefined,
386
+ ): RemoteCompactionConfig<Api> | undefined {
387
+ return mergeRemoteCompactionConfig(providerConfig, modelConfig);
388
+ }
389
+
356
390
  /**
357
391
  * Project a built model back to spec shape for the model-manager/cache
358
392
  * boundary: sparse compat comes from `compatConfig`, never from the resolved
@@ -380,6 +414,8 @@ interface ModelPatch {
380
414
  headers?: Record<string, string>;
381
415
  compat?: ModelSpec<Api>["compat"];
382
416
  contextPromotionTarget?: string;
417
+ compactionModel?: string;
418
+ remoteCompaction?: RemoteCompactionConfig<Api>;
383
419
  premiumMultiplier?: number;
384
420
  }
385
421
 
@@ -403,6 +439,10 @@ function applyModelPatch(base: Model<Api>, patch: ModelPatch, transport: ModelTr
403
439
  if (patch.maxTokens !== undefined) result.maxTokens = patch.maxTokens;
404
440
  if (patch.omitMaxOutputTokens !== undefined) result.omitMaxOutputTokens = patch.omitMaxOutputTokens;
405
441
  if (patch.contextPromotionTarget !== undefined) result.contextPromotionTarget = patch.contextPromotionTarget;
442
+ if (patch.compactionModel !== undefined) result.compactionModel = patch.compactionModel;
443
+ if (patch.remoteCompaction !== undefined) {
444
+ result.remoteCompaction = mergeRemoteCompactionConfig(base.remoteCompaction, patch.remoteCompaction);
445
+ }
406
446
  if (patch.premiumMultiplier !== undefined) result.premiumMultiplier = patch.premiumMultiplier;
407
447
  if (patch.cost) {
408
448
  result.cost = {
@@ -475,8 +515,6 @@ function mergeAuthHeader(
475
515
  /**
476
516
  * Decide whether a custom-yaml model should force OAuth-style request shaping.
477
517
  * - Explicit `auth: oauth` → force on.
478
- * - Explicit `auth: apiKey` / `auth: none` → leave unset (auto-detect by key prefix).
479
- * - No `auth` specified and `api: anthropic-messages` → default on. Custom Anthropic
480
518
  * endpoints are typically Claude-Code-style proxies (e.g. CLIProxyAPI) that expect
481
519
  * the cloaked request shape regardless of how the proxy itself is authenticated.
482
520
  * - Otherwise → unset.
@@ -497,6 +535,7 @@ function buildCustomModelOverlay(
497
535
  authHeader: boolean | undefined,
498
536
  providerCompat: ModelSpec<Api>["compat"] | undefined,
499
537
  providerAuth: ProviderAuthMode | undefined,
538
+ providerRemoteCompaction: RemoteCompactionConfig<Api> | undefined,
500
539
  modelDef: CustomModelDefinitionLike,
501
540
  ): CustomModelOverlay | undefined {
502
541
  const api = modelDef.api ?? providerApi;
@@ -518,6 +557,8 @@ function buildCustomModelOverlay(
518
557
  headers: mergeCustomModelHeaders(providerHeaders, modelDef.headers, authHeader, providerApiKey),
519
558
  compat: mergeCompat(providerCompat, modelDef.compat),
520
559
  contextPromotionTarget: modelDef.contextPromotionTarget,
560
+ compactionModel: modelDef.compactionModel,
561
+ remoteCompaction: mergeRemoteCompactionConfig(providerRemoteCompaction, modelDef.remoteCompaction),
521
562
  premiumMultiplier: modelDef.premiumMultiplier,
522
563
  isOAuth: resolveCustomModelIsOAuth(api, providerAuth),
523
564
  };
@@ -558,6 +599,8 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil
558
599
  omitMaxOutputTokens: resolvedModel.omitMaxOutputTokens ?? reference?.omitMaxOutputTokens,
559
600
  compat: mergeCompat(reference?.compatConfig, resolvedModel.compat),
560
601
  contextPromotionTarget: resolvedModel.contextPromotionTarget,
602
+ compactionModel: resolvedModel.compactionModel,
603
+ remoteCompaction: resolvedModel.remoteCompaction,
561
604
  premiumMultiplier: resolvedModel.premiumMultiplier,
562
605
  isOAuth: resolvedModel.isOAuth,
563
606
  } as ModelSpec<Api>);
@@ -1078,12 +1121,25 @@ export class ModelRegistry {
1078
1121
  )
1079
1122
  : models;
1080
1123
 
1124
+ const withRemoteCompaction = providerConfig.remoteCompaction
1125
+ ? withDecoderMetadata.map(model =>
1126
+ buildModel({
1127
+ ...model,
1128
+ remoteCompaction: mergeProviderRemoteCompactionConfig(
1129
+ model.remoteCompaction,
1130
+ providerConfig.remoteCompaction,
1131
+ ),
1132
+ compat: model.compatConfig,
1133
+ } as ModelSpec<Api>),
1134
+ )
1135
+ : withDecoderMetadata;
1136
+
1081
1137
  if (providerConfig.provider !== "ollama" || providerConfig.api !== "openai-responses") {
1082
- return withDecoderMetadata;
1138
+ return withRemoteCompaction;
1083
1139
  }
1084
1140
 
1085
1141
  const contextLengthOverride = getOllamaContextLengthOverride();
1086
- return withDecoderMetadata.map(model => {
1142
+ return withRemoteCompaction.map(model => {
1087
1143
  const normalized =
1088
1144
  model.api === "openai-completions"
1089
1145
  ? buildModel({
@@ -1172,7 +1228,6 @@ export class ModelRegistry {
1172
1228
  const discoverableProviders: DiscoveryProviderConfig[] = [];
1173
1229
  const providerEntries = Object.entries(value.providers ?? {});
1174
1230
  const configuredProviders = new Set(Object.keys(value.providers ?? {}));
1175
-
1176
1231
  for (const [providerName, providerConfig] of providerEntries) {
1177
1232
  const resolvedProviderHeaders = resolveConfigHeaders(providerConfig.headers);
1178
1233
  // Always set overrides when baseUrl/headers/apiKey/authHeader/compat/disableStrictTools/transport are present
@@ -1183,15 +1238,20 @@ export class ModelRegistry {
1183
1238
  providerConfig.authHeader !== undefined ||
1184
1239
  providerConfig.compat ||
1185
1240
  providerConfig.disableStrictTools ||
1241
+ providerConfig.remoteCompaction ||
1186
1242
  providerConfig.transport
1187
1243
  ) {
1188
1244
  const disableStrictCompat = providerConfig.disableStrictTools ? { disableStrictTools: true } : undefined;
1189
1245
  overrides.set(providerName, {
1190
- baseUrl: providerConfig.baseUrl,
1246
+ baseUrl:
1247
+ providerConfig.discovery?.type === "litellm"
1248
+ ? normalizeLiteLLMDiscoveryBaseUrl(providerConfig.baseUrl)
1249
+ : providerConfig.baseUrl,
1191
1250
  headers: resolvedProviderHeaders,
1192
1251
  apiKey: providerConfig.apiKey,
1193
1252
  authHeader: providerConfig.authHeader,
1194
1253
  compat: mergeCompat(providerConfig.compat, disableStrictCompat),
1254
+ remoteCompaction: providerConfig.remoteCompaction,
1195
1255
  transport: providerConfig.transport,
1196
1256
  });
1197
1257
  }
@@ -1212,6 +1272,7 @@ export class ModelRegistry {
1212
1272
  baseUrl: providerConfig.baseUrl,
1213
1273
  headers: resolvedProviderHeaders,
1214
1274
  compat: mergeCompat(providerConfig.compat, disableStrictCompat),
1275
+ remoteCompaction: providerConfig.remoteCompaction,
1215
1276
  discovery: providerConfig.discovery,
1216
1277
  optional: false,
1217
1278
  });
@@ -1302,6 +1363,9 @@ export class ModelRegistry {
1302
1363
  if (providerConfig.discovery.type === "openai-models-list") {
1303
1364
  return `${providerConfig.provider}:openai-models-list-context-v2`;
1304
1365
  }
1366
+ if (providerConfig.discovery.type === "litellm") {
1367
+ return `${providerConfig.provider}:litellm-rich-v1`;
1368
+ }
1305
1369
  return providerConfig.provider;
1306
1370
  }
1307
1371
 
@@ -1599,12 +1663,18 @@ export class ModelRegistry {
1599
1663
  authHeader: override.authHeader ?? baseOverride?.authHeader,
1600
1664
  headers: override.headers ? { ...(baseOverride?.headers ?? {}), ...override.headers } : baseOverride?.headers,
1601
1665
  compat: override.compat ? mergeCompat(baseOverride?.compat, override.compat) : baseOverride?.compat,
1666
+ remoteCompaction: mergeRemoteCompactionConfig(baseOverride?.remoteCompaction, override.remoteCompaction),
1602
1667
  transport: override.transport ?? baseOverride?.transport,
1603
1668
  };
1604
1669
  }
1605
- #applyProviderTransportOverride<T extends { baseUrl?: string; headers?: Record<string, string> }>(
1670
+ #applyProviderTransportOverride<
1671
+ T extends { baseUrl?: string; headers?: Record<string, string>; remoteCompaction?: RemoteCompactionConfig<Api> },
1672
+ >(
1606
1673
  entry: T,
1607
- override: Pick<ProviderOverride, "baseUrl" | "headers" | "authHeader" | "apiKey" | "transport">,
1674
+ override: Pick<
1675
+ ProviderOverride,
1676
+ "baseUrl" | "headers" | "authHeader" | "apiKey" | "remoteCompaction" | "transport"
1677
+ >,
1608
1678
  ): T {
1609
1679
  const headers = mergeAuthHeader(
1610
1680
  override.headers ? { ...entry.headers, ...override.headers } : entry.headers,
@@ -1618,6 +1688,7 @@ export class ModelRegistry {
1618
1688
  // Preserve the model's existing transport when the override omits one;
1619
1689
  // providers without a `transport` field keep the default per-API dispatch.
1620
1690
  ...(override.transport !== undefined ? { transport: override.transport } : {}),
1691
+ remoteCompaction: mergeProviderRemoteCompactionConfig(entry.remoteCompaction, override.remoteCompaction),
1621
1692
  };
1622
1693
  }
1623
1694
  #applyRuntimeProviderOverrides(models: Model<Api>[]): Model<Api>[] {
@@ -1719,7 +1790,6 @@ export class ModelRegistry {
1719
1790
 
1720
1791
  #parseModels(config: ModelsConfig): CustomModelOverlay[] {
1721
1792
  const models: CustomModelOverlay[] = [];
1722
-
1723
1793
  for (const [providerName, providerConfig] of Object.entries(config.providers ?? {})) {
1724
1794
  const modelDefs = providerConfig.models ?? [];
1725
1795
  if (modelDefs.length === 0) continue; // Override-only, no custom models
@@ -1740,6 +1810,7 @@ export class ModelRegistry {
1740
1810
  providerConfig.authHeader,
1741
1811
  providerCompat,
1742
1812
  (providerConfig.auth as ProviderAuthMode | undefined) ?? undefined,
1813
+ providerConfig.remoteCompaction,
1743
1814
  modelDef as CustomModelDefinitionLike,
1744
1815
  );
1745
1816
  if (!model) continue;
@@ -2147,6 +2218,7 @@ export class ModelRegistry {
2147
2218
  config.authHeader,
2148
2219
  config.compat,
2149
2220
  undefined,
2221
+ config.remoteCompaction,
2150
2222
  modelDef as CustomModelDefinitionLike,
2151
2223
  );
2152
2224
  if (!overlay) {
@@ -2214,6 +2286,7 @@ export class ModelRegistry {
2214
2286
  providerAuthHeader,
2215
2287
  providerCompat,
2216
2288
  undefined,
2289
+ config.remoteCompaction,
2217
2290
  modelDef as CustomModelDefinitionLike,
2218
2291
  );
2219
2292
  if (overlay) results.push(finalizeCustomModel(overlay, { useDefaults: true }));
@@ -2231,6 +2304,7 @@ export class ModelRegistry {
2231
2304
  config.headers ||
2232
2305
  config.apiKey ||
2233
2306
  config.authHeader !== undefined ||
2307
+ config.remoteCompaction !== undefined ||
2234
2308
  config.transport !== undefined
2235
2309
  ) {
2236
2310
  const transportOverride = {
@@ -2238,6 +2312,7 @@ export class ModelRegistry {
2238
2312
  headers: config.headers,
2239
2313
  apiKey: config.apiKey,
2240
2314
  authHeader: config.authHeader,
2315
+ remoteCompaction: config.remoteCompaction,
2241
2316
  transport: config.transport,
2242
2317
  };
2243
2318
  const nextRuntimeOverride = this.#mergeProviderOverride(
@@ -2299,6 +2374,7 @@ export interface ProviderConfigInput {
2299
2374
  streamSimple?: (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream;
2300
2375
  headers?: Record<string, string>;
2301
2376
  compat?: ModelSpec<Api>["compat"];
2377
+ remoteCompaction?: RemoteCompactionConfig<Api>;
2302
2378
  authHeader?: boolean;
2303
2379
  /** Streaming transport override — see {@link Model.transport}. */
2304
2380
  transport?: Model<Api>["transport"];
@@ -2333,6 +2409,8 @@ export interface ProviderConfigInput {
2333
2409
  headers?: Record<string, string>;
2334
2410
  compat?: ModelSpec<Api>["compat"];
2335
2411
  contextPromotionTarget?: string;
2412
+ compactionModel?: string;
2413
+ remoteCompaction?: RemoteCompactionConfig<Api>;
2336
2414
  premiumMultiplier?: number;
2337
2415
  }>;
2338
2416
  }
@@ -877,6 +877,23 @@ function shouldInheritDefaultBeforePriority(role: ModelRole): boolean {
877
877
  return role === "smol" || role === "slow" || role === "designer";
878
878
  }
879
879
 
880
+ /**
881
+ * Roles that have no priority.json chain of their own reuse another role's
882
+ * list. The advisor — a second-opinion reviewer — defaults to the `slow`
883
+ * reasoning chain, but (unlike the `slow` role, see
884
+ * {@link shouldInheritDefaultBeforePriority}) never inherits the primary's
885
+ * model, so it stays a distinct strong model out of the box.
886
+ */
887
+ const ROLE_PRIORITY_ALIAS: Partial<Record<ModelRole, keyof typeof MODEL_PRIO>> = {
888
+ advisor: "slow",
889
+ };
890
+
891
+ /** Built-in priority patterns for a role, following {@link ROLE_PRIORITY_ALIAS}. */
892
+ function rolePriorityDefaults(role: ModelRole): string[] {
893
+ const key = ROLE_PRIORITY_ALIAS[role] ?? (role as keyof typeof MODEL_PRIO);
894
+ return normalizeModelPatternList(MODEL_PRIO[key]);
895
+ }
896
+
880
897
  function resolveDefaultInheritedPatterns(
881
898
  role: ModelRole,
882
899
  configuredDefault: string | undefined,
@@ -939,7 +956,7 @@ function resolveConfiguredRolePattern(
939
956
 
940
957
  const configured = settings?.getModelRole(role)?.trim();
941
958
  const configuredDefault = settings?.getModelRole(DEFAULT_MODEL_ROLE)?.trim();
942
- const roleDefaults = normalizeModelPatternList(MODEL_PRIO[role as keyof typeof MODEL_PRIO]);
959
+ const roleDefaults = rolePriorityDefaults(role);
943
960
  const resolved = configured
944
961
  ? normalizeModelPatternList(configured)
945
962
  : resolveDefaultInheritedPatterns(role, configuredDefault, roleDefaults, settings, visited);
@@ -1245,6 +1262,27 @@ export function resolveRoleSelection(
1245
1262
  return undefined;
1246
1263
  }
1247
1264
 
1265
+ /**
1266
+ * Resolve the model for the `advisor` role. A configured `modelRoles.advisor`
1267
+ * wins outright (a bad override surfaces as no model rather than silently
1268
+ * running something else); when unset it falls back to the `slow` priority
1269
+ * chain via {@link ROLE_PRIORITY_ALIAS} — a strong reasoning model that, unlike
1270
+ * the `slow` role itself, never inherits the primary's model. Returns undefined
1271
+ * only when no candidate in the resolved chain is available.
1272
+ */
1273
+ export function resolveAdvisorRoleSelection(
1274
+ settings: Settings,
1275
+ availableModels: Model<Api>[],
1276
+ modelRegistry?: CanonicalModelRegistry,
1277
+ ): { model: Model<Api>; thinkingLevel?: ThinkingLevel } | undefined {
1278
+ const resolved = resolveModelRoleValue(`${PREFIX_MODEL_ROLE}advisor`, availableModels, {
1279
+ settings,
1280
+ matchPreferences: getModelMatchPreferences(settings),
1281
+ modelRegistry,
1282
+ });
1283
+ return resolved.model ? { model: resolved.model, thinkingLevel: resolved.thinkingLevel } : undefined;
1284
+ }
1285
+
1248
1286
  function resolveExactCanonicalScopePattern(
1249
1287
  pattern: string,
1250
1288
  modelRegistry: Pick<ModelRegistry, "getCanonicalVariants">,