@oh-my-pi/pi-coding-agent 15.10.0 → 15.10.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.
Files changed (238) hide show
  1. package/CHANGELOG.md +142 -1
  2. package/dist/types/cli/dry-balance-cli.d.ts +15 -1
  3. package/dist/types/cli/startup-cwd.d.ts +2 -0
  4. package/dist/types/commands/launch.d.ts +3 -0
  5. package/dist/types/commit/analysis/conventional.d.ts +2 -2
  6. package/dist/types/commit/analysis/summary.d.ts +2 -2
  7. package/dist/types/commit/changelog/generate.d.ts +2 -2
  8. package/dist/types/commit/changelog/index.d.ts +2 -2
  9. package/dist/types/commit/map-reduce/index.d.ts +3 -3
  10. package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
  11. package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
  12. package/dist/types/commit/model-selection.d.ts +10 -4
  13. package/dist/types/config/api-key-resolver.d.ts +34 -0
  14. package/dist/types/config/keybindings.d.ts +2 -2
  15. package/dist/types/config/model-provider-priority.d.ts +1 -0
  16. package/dist/types/config/model-registry.d.ts +17 -1
  17. package/dist/types/config/model-resolver.d.ts +4 -1
  18. package/dist/types/config/settings-schema.d.ts +9 -0
  19. package/dist/types/config/settings.d.ts +7 -2
  20. package/dist/types/dap/config.d.ts +14 -1
  21. package/dist/types/dap/types.d.ts +10 -0
  22. package/dist/types/debug/report-bundle.d.ts +3 -0
  23. package/dist/types/edit/file-snapshot-store.d.ts +18 -10
  24. package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
  25. package/dist/types/extensibility/extensions/types.d.ts +4 -1
  26. package/dist/types/lsp/client.d.ts +10 -0
  27. package/dist/types/lsp/utils.d.ts +3 -2
  28. package/dist/types/main.d.ts +3 -9
  29. package/dist/types/mcp/tool-bridge.d.ts +2 -0
  30. package/dist/types/modes/components/chat-block.d.ts +64 -0
  31. package/dist/types/modes/components/custom-editor.d.ts +4 -1
  32. package/dist/types/modes/components/overlay-box.d.ts +17 -0
  33. package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
  34. package/dist/types/modes/components/plan-toc.d.ts +41 -0
  35. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  36. package/dist/types/modes/components/status-line.d.ts +2 -0
  37. package/dist/types/modes/components/transcript-container.d.ts +11 -0
  38. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  39. package/dist/types/modes/controllers/event-controller.d.ts +17 -1
  40. package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
  41. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  42. package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
  43. package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
  44. package/dist/types/modes/interactive-mode.d.ts +16 -5
  45. package/dist/types/modes/magic-keywords.d.ts +1 -1
  46. package/dist/types/modes/markdown-prose.d.ts +1 -1
  47. package/dist/types/modes/theme/theme.d.ts +1 -1
  48. package/dist/types/modes/types.d.ts +21 -5
  49. package/dist/types/modes/utils/copy-targets.d.ts +21 -1
  50. package/dist/types/modes/workflow.d.ts +3 -3
  51. package/dist/types/plan-mode/approved-plan.d.ts +27 -8
  52. package/dist/types/plan-mode/plan-protection.d.ts +4 -4
  53. package/dist/types/sdk.d.ts +2 -0
  54. package/dist/types/session/agent-session.d.ts +21 -0
  55. package/dist/types/session/auth-storage.d.ts +1 -1
  56. package/dist/types/session/messages.d.ts +12 -0
  57. package/dist/types/session/session-manager.d.ts +8 -3
  58. package/dist/types/slash-commands/types.d.ts +4 -6
  59. package/dist/types/task/executor.d.ts +17 -0
  60. package/dist/types/task/index.d.ts +1 -0
  61. package/dist/types/task/render.d.ts +3 -2
  62. package/dist/types/tools/archive-reader.d.ts +5 -0
  63. package/dist/types/tools/ast-edit.d.ts +3 -0
  64. package/dist/types/tools/ast-grep.d.ts +3 -0
  65. package/dist/types/tools/bash.d.ts +1 -0
  66. package/dist/types/tools/eval.d.ts +8 -0
  67. package/dist/types/tools/find.d.ts +8 -4
  68. package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
  69. package/dist/types/tools/github-cache.d.ts +12 -0
  70. package/dist/types/tools/grouped-file-output.d.ts +95 -12
  71. package/dist/types/tools/memory-render.d.ts +4 -1
  72. package/dist/types/tools/path-utils.d.ts +8 -0
  73. package/dist/types/tools/plan-mode-guard.d.ts +8 -9
  74. package/dist/types/tools/render-utils.d.ts +5 -9
  75. package/dist/types/tools/search.d.ts +6 -2
  76. package/dist/types/tools/sqlite-reader.d.ts +1 -0
  77. package/dist/types/tools/todo.d.ts +3 -2
  78. package/dist/types/tools/write.d.ts +3 -0
  79. package/dist/types/tools/yield.d.ts +8 -0
  80. package/dist/types/tui/output-block.d.ts +16 -4
  81. package/dist/types/tui/status-line.d.ts +3 -0
  82. package/dist/types/utils/enhanced-paste.d.ts +20 -0
  83. package/dist/types/web/search/providers/kimi.d.ts +1 -1
  84. package/package.json +9 -9
  85. package/src/auto-thinking/classifier.ts +5 -1
  86. package/src/cli/args.ts +3 -1
  87. package/src/cli/dry-balance-cli.ts +54 -21
  88. package/src/cli/gallery-cli.ts +4 -1
  89. package/src/cli/gallery-fixtures/misc.ts +29 -0
  90. package/src/cli/startup-cwd.ts +68 -0
  91. package/src/commands/launch.ts +3 -0
  92. package/src/commit/analysis/conventional.ts +2 -2
  93. package/src/commit/analysis/summary.ts +2 -2
  94. package/src/commit/changelog/generate.ts +2 -2
  95. package/src/commit/changelog/index.ts +2 -2
  96. package/src/commit/map-reduce/index.ts +3 -3
  97. package/src/commit/map-reduce/map-phase.ts +2 -2
  98. package/src/commit/map-reduce/reduce-phase.ts +2 -2
  99. package/src/commit/model-selection.ts +36 -11
  100. package/src/commit/pipeline.ts +4 -4
  101. package/src/config/api-key-resolver.ts +58 -0
  102. package/src/config/model-provider-priority.ts +55 -0
  103. package/src/config/model-registry.ts +29 -24
  104. package/src/config/model-resolver.ts +39 -7
  105. package/src/config/settings-schema.ts +10 -0
  106. package/src/config/settings.ts +106 -43
  107. package/src/dap/config.ts +41 -2
  108. package/src/dap/defaults.json +1 -0
  109. package/src/dap/session.ts +1 -0
  110. package/src/dap/types.ts +10 -0
  111. package/src/debug/index.ts +47 -53
  112. package/src/debug/raw-sse-buffer.ts +7 -4
  113. package/src/debug/report-bundle.ts +9 -0
  114. package/src/edit/file-snapshot-store.ts +33 -1
  115. package/src/edit/hashline/filesystem.ts +2 -1
  116. package/src/edit/renderer.ts +82 -78
  117. package/src/eval/__tests__/llm-bridge.test.ts +110 -31
  118. package/src/eval/js/context-manager.ts +32 -15
  119. package/src/eval/llm-bridge.ts +22 -6
  120. package/src/eval/py/__tests__/prelude.test.ts +19 -0
  121. package/src/eval/py/executor.ts +23 -11
  122. package/src/eval/py/prelude.py +1 -1
  123. package/src/extensibility/extensions/types.ts +10 -1
  124. package/src/goals/tools/goal-tool.ts +36 -26
  125. package/src/internal-urls/docs-index.generated.ts +8 -8
  126. package/src/lsp/client.ts +23 -11
  127. package/src/lsp/config.ts +11 -1
  128. package/src/lsp/index.ts +61 -9
  129. package/src/lsp/utils.ts +3 -2
  130. package/src/main.ts +100 -72
  131. package/src/mcp/tool-bridge.ts +2 -0
  132. package/src/memories/index.ts +14 -7
  133. package/src/mnemopi/backend.ts +5 -1
  134. package/src/modes/acp/acp-agent.ts +33 -26
  135. package/src/modes/components/assistant-message.ts +2 -9
  136. package/src/modes/components/chat-block.ts +111 -0
  137. package/src/modes/components/copy-selector.ts +1 -44
  138. package/src/modes/components/custom-editor.ts +164 -109
  139. package/src/modes/components/custom-message.ts +1 -3
  140. package/src/modes/components/execution-shared.ts +1 -2
  141. package/src/modes/components/hook-message.ts +1 -3
  142. package/src/modes/components/model-selector.ts +59 -13
  143. package/src/modes/components/oauth-selector.ts +33 -7
  144. package/src/modes/components/overlay-box.ts +108 -0
  145. package/src/modes/components/plan-review-overlay.ts +799 -0
  146. package/src/modes/components/plan-toc.ts +138 -0
  147. package/src/modes/components/read-tool-group.ts +20 -4
  148. package/src/modes/components/skill-message.ts +0 -1
  149. package/src/modes/components/status-line.ts +19 -4
  150. package/src/modes/components/tips.txt +2 -1
  151. package/src/modes/components/todo-reminder.ts +0 -2
  152. package/src/modes/components/tool-execution.ts +68 -88
  153. package/src/modes/components/transcript-container.ts +84 -24
  154. package/src/modes/components/user-message.ts +2 -3
  155. package/src/modes/controllers/command-controller-shared.ts +7 -6
  156. package/src/modes/controllers/command-controller.ts +57 -55
  157. package/src/modes/controllers/event-controller.ts +67 -40
  158. package/src/modes/controllers/extension-ui-controller.ts +10 -73
  159. package/src/modes/controllers/input-controller.ts +170 -126
  160. package/src/modes/controllers/mcp-command-controller.ts +69 -60
  161. package/src/modes/controllers/selector-controller.ts +23 -25
  162. package/src/modes/controllers/streaming-reveal.ts +212 -0
  163. package/src/modes/controllers/tan-command-controller.ts +173 -0
  164. package/src/modes/interactive-mode.ts +274 -112
  165. package/src/modes/magic-keywords.ts +1 -1
  166. package/src/modes/markdown-prose.ts +1 -1
  167. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  168. package/src/modes/theme/shimmer.ts +20 -9
  169. package/src/modes/theme/theme-schema.json +1 -1
  170. package/src/modes/theme/theme.ts +8 -4
  171. package/src/modes/types.ts +21 -7
  172. package/src/modes/utils/copy-targets.ts +133 -27
  173. package/src/modes/utils/ui-helpers.ts +44 -46
  174. package/src/modes/workflow.ts +10 -10
  175. package/src/plan-mode/approved-plan.ts +66 -43
  176. package/src/plan-mode/plan-protection.ts +4 -4
  177. package/src/prompts/system/background-tan-dispatch.md +8 -0
  178. package/src/prompts/system/plan-mode-active.md +67 -58
  179. package/src/prompts/system/plan-mode-approved.md +1 -1
  180. package/src/prompts/system/workflow-notice.md +1 -1
  181. package/src/prompts/tools/bash.md +9 -0
  182. package/src/prompts/tools/browser.md +1 -1
  183. package/src/prompts/tools/eval.md +2 -1
  184. package/src/prompts/tools/read.md +2 -2
  185. package/src/sdk.ts +37 -46
  186. package/src/session/agent-session.ts +119 -18
  187. package/src/session/auth-storage.ts +2 -0
  188. package/src/session/messages.ts +26 -0
  189. package/src/session/session-manager.ts +109 -28
  190. package/src/slash-commands/builtin-registry.ts +36 -9
  191. package/src/slash-commands/types.ts +4 -6
  192. package/src/task/executor.ts +76 -38
  193. package/src/task/index.ts +4 -0
  194. package/src/task/render.ts +211 -147
  195. package/src/tools/archive-reader.ts +64 -0
  196. package/src/tools/ask.ts +119 -164
  197. package/src/tools/ast-edit.ts +98 -71
  198. package/src/tools/ast-grep.ts +37 -43
  199. package/src/tools/bash.ts +57 -6
  200. package/src/tools/browser/tab-supervisor.ts +13 -1
  201. package/src/tools/browser/tab-worker.ts +33 -4
  202. package/src/tools/debug.ts +20 -8
  203. package/src/tools/eval.ts +13 -2
  204. package/src/tools/fetch.ts +297 -7
  205. package/src/tools/find.ts +51 -30
  206. package/src/tools/gh-cache-invalidation.ts +200 -0
  207. package/src/tools/gh-renderer.ts +81 -42
  208. package/src/tools/github-cache.ts +25 -0
  209. package/src/tools/grouped-file-output.ts +272 -48
  210. package/src/tools/image-gen.ts +150 -103
  211. package/src/tools/inspect-image-renderer.ts +63 -41
  212. package/src/tools/inspect-image.ts +10 -3
  213. package/src/tools/job.ts +3 -4
  214. package/src/tools/memory-render.ts +4 -1
  215. package/src/tools/path-utils.ts +28 -2
  216. package/src/tools/plan-mode-guard.ts +66 -39
  217. package/src/tools/read.ts +48 -28
  218. package/src/tools/render-utils.ts +21 -37
  219. package/src/tools/resolve.ts +14 -0
  220. package/src/tools/search-tool-bm25.ts +36 -23
  221. package/src/tools/search.ts +118 -81
  222. package/src/tools/sqlite-reader.ts +9 -12
  223. package/src/tools/todo.ts +118 -52
  224. package/src/tools/write.ts +83 -64
  225. package/src/tools/yield.ts +10 -1
  226. package/src/tui/output-block.ts +60 -13
  227. package/src/tui/status-line.ts +5 -1
  228. package/src/utils/commit-message-generator.ts +11 -3
  229. package/src/utils/enhanced-paste.ts +230 -0
  230. package/src/utils/title-generator.ts +2 -1
  231. package/src/web/search/providers/anthropic.ts +25 -19
  232. package/src/web/search/providers/codex.ts +37 -8
  233. package/src/web/search/providers/exa.ts +11 -3
  234. package/src/web/search/providers/kimi.ts +28 -17
  235. package/src/web/search/providers/parallel.ts +35 -24
  236. package/src/web/search/providers/synthetic.ts +8 -6
  237. package/src/web/search/providers/tavily.ts +9 -8
  238. package/src/web/search/providers/zai.ts +8 -6
@@ -218,4 +218,33 @@ export const miscFixtures: Record<string, GalleryFixture> = {
218
218
  },
219
219
  },
220
220
  },
221
+
222
+ // Built-in tool with no dedicated renderer — exercises the generic fallback
223
+ // (`#formatToolExecution`) path so its padded, state-tinted block is QA'd.
224
+ report_tool_issue: {
225
+ label: "Report Tool Issue",
226
+ streamingArgs: { tool: "lsp" },
227
+ args: {
228
+ tool: "lsp",
229
+ report: "Rename returned no edit for an exported symbol that has 12 references",
230
+ },
231
+ result: { content: [{ type: "text", text: "Noted, thanks!" }] },
232
+ errorResult: {
233
+ content: [{ type: "text", text: "Could not record the report: issue tracker unreachable" }],
234
+ isError: true,
235
+ },
236
+ },
237
+
238
+ // Stand-in for a custom/extension tool that ships no renderer — same generic
239
+ // fallback path most MCP/extension tools take.
240
+ custom: {
241
+ label: "Custom Tool",
242
+ streamingArgs: { query: "weather" },
243
+ args: { query: "weather in Tokyo", units: "metric" },
244
+ result: { content: [{ type: "text", text: "Tokyo: 22°C, partly cloudy, humidity 64%." }] },
245
+ errorResult: {
246
+ content: [{ type: "text", text: "Upstream provider returned 503 Service Unavailable" }],
247
+ isError: true,
248
+ },
249
+ },
221
250
  };
@@ -0,0 +1,68 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { getProjectDir, normalizePathForComparison, setProjectDir } from "@oh-my-pi/pi-utils";
5
+ import type { Args } from "./args";
6
+
7
+ async function maybeAutoChdir(parsed: Args): Promise<void> {
8
+ if (parsed.allowHome || parsed.cwd) {
9
+ return;
10
+ }
11
+
12
+ const home = os.homedir();
13
+ if (!home) {
14
+ return;
15
+ }
16
+
17
+ const normalizePath = normalizePathForComparison;
18
+
19
+ const cwd = normalizePath(getProjectDir());
20
+ const normalizedHome = normalizePath(home);
21
+ if (cwd !== normalizedHome) {
22
+ return;
23
+ }
24
+
25
+ const isDirectory = async (p: string) => {
26
+ try {
27
+ const s = await fs.stat(p);
28
+ return s.isDirectory();
29
+ } catch {
30
+ return false;
31
+ }
32
+ };
33
+
34
+ const candidates = [path.join(home, "tmp"), "/tmp", "/var/tmp"];
35
+ for (const candidate of candidates) {
36
+ try {
37
+ if (!(await isDirectory(candidate))) {
38
+ continue;
39
+ }
40
+ setProjectDir(candidate);
41
+ return;
42
+ } catch {
43
+ // Try next candidate.
44
+ }
45
+ }
46
+
47
+ try {
48
+ const fallback = os.tmpdir();
49
+ if (fallback && normalizePath(fallback) !== cwd && (await isDirectory(fallback))) {
50
+ setProjectDir(fallback);
51
+ }
52
+ } catch {
53
+ // Ignore fallback errors.
54
+ }
55
+ }
56
+
57
+ export async function applyStartupCwd(parsed: Args): Promise<void> {
58
+ if (parsed.cwd) {
59
+ setProjectDir(parsed.cwd);
60
+ // setProjectDir resolves the (possibly relative) target against the launch
61
+ // cwd and chdirs into it. Re-sync parsed.cwd to the resolved absolute path
62
+ // so downstream consumers (buildSessionOptions, settings/discovery, session
63
+ // persistence) don't re-resolve a relative string against the new cwd.
64
+ parsed.cwd = getProjectDir();
65
+ return;
66
+ }
67
+ await maybeAutoChdir(parsed);
68
+ }
@@ -49,6 +49,9 @@ export default class Index extends Command {
49
49
  "allow-home": Flags.boolean({
50
50
  description: "Allow starting in ~ without auto-switching to a temp dir",
51
51
  }),
52
+ cwd: Flags.string({
53
+ description: "Directory to start in (overrides the launch cwd)",
54
+ }),
52
55
  mode: Flags.string({
53
56
  description: "Output mode: text (default), json, rpc, or rpc-ui",
54
57
  options: ["text", "json", "rpc", "acp", "rpc-ui"],
@@ -1,5 +1,5 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import type { Api, Model } from "@oh-my-pi/pi-ai";
2
+ import type { Api, ApiKey, Model } from "@oh-my-pi/pi-ai";
3
3
  import { completeSimple } from "@oh-my-pi/pi-ai";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
5
  import analysisSystemPrompt from "../../commit/prompts/analysis-system.md" with { type: "text" };
@@ -14,7 +14,7 @@ const ConventionalAnalysisTool = createConventionalAnalysisTool(
14
14
 
15
15
  export interface ConventionalAnalysisInput {
16
16
  model: Model<Api>;
17
- apiKey: string;
17
+ apiKey: ApiKey;
18
18
  thinkingLevel?: ThinkingLevel;
19
19
  contextFiles?: Array<{ path: string; content: string }>;
20
20
  userContext?: string;
@@ -1,5 +1,5 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
2
+ import type { Api, ApiKey, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
3
3
  import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
5
  import * as z from "zod/v4";
@@ -19,7 +19,7 @@ const SummaryTool = {
19
19
 
20
20
  export interface SummaryInput {
21
21
  model: Model<Api>;
22
- apiKey: string;
22
+ apiKey: ApiKey;
23
23
  thinkingLevel?: ThinkingLevel;
24
24
  commitType: string;
25
25
  scope: string | null;
@@ -1,5 +1,5 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
2
+ import type { Api, ApiKey, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
3
3
  import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
5
  import * as z from "zod/v4";
@@ -25,7 +25,7 @@ export const changelogTool = {
25
25
 
26
26
  export interface ChangelogPromptInput {
27
27
  model: Model<Api>;
28
- apiKey: string;
28
+ apiKey: ApiKey;
29
29
  thinkingLevel?: ThinkingLevel;
30
30
  changelogPath: string;
31
31
  isPackageChangelog: boolean;
@@ -1,6 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
3
- import type { Api, Model } from "@oh-my-pi/pi-ai";
3
+ import type { Api, ApiKey, Model } from "@oh-my-pi/pi-ai";
4
4
  import { logger } from "@oh-my-pi/pi-utils";
5
5
  import { CHANGELOG_CATEGORIES } from "../../commit/types";
6
6
  import * as git from "../../utils/git";
@@ -15,7 +15,7 @@ const DEFAULT_MAX_DIFF_CHARS = 120_000;
15
15
  export interface ChangelogFlowInput {
16
16
  cwd: string;
17
17
  model: Model<Api>;
18
- apiKey: string;
18
+ apiKey: ApiKey;
19
19
  thinkingLevel?: ThinkingLevel;
20
20
  stagedFiles: string[];
21
21
  dryRun: boolean;
@@ -1,5 +1,5 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import type { Api, Model } from "@oh-my-pi/pi-ai";
2
+ import type { Api, ApiKey, Model } from "@oh-my-pi/pi-ai";
3
3
  import { $env } from "@oh-my-pi/pi-utils";
4
4
  import { parseFileDiffs } from "../../commit/git/diff";
5
5
  import type { ConventionalAnalysis } from "../../commit/types";
@@ -21,10 +21,10 @@ export interface MapReduceSettings {
21
21
 
22
22
  export interface MapReduceInput {
23
23
  model: Model<Api>;
24
- apiKey: string;
24
+ apiKey: ApiKey;
25
25
  thinkingLevel?: ThinkingLevel;
26
26
  smolModel: Model<Api>;
27
- smolApiKey: string;
27
+ smolApiKey: ApiKey;
28
28
  smolThinkingLevel?: ThinkingLevel;
29
29
  diff: string;
30
30
  stat: string;
@@ -1,5 +1,5 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import type { Api, AssistantMessage, Message, Model } from "@oh-my-pi/pi-ai";
2
+ import type { Api, ApiKey, AssistantMessage, Message, Model } from "@oh-my-pi/pi-ai";
3
3
  import { completeSimple } from "@oh-my-pi/pi-ai";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
5
  import fileObserverSystemPrompt from "../../commit/prompts/file-observer-system.md" with { type: "text" };
@@ -18,7 +18,7 @@ const RETRY_BACKOFF_MS = 1000;
18
18
 
19
19
  export interface MapPhaseInput {
20
20
  model: Model<Api>;
21
- apiKey: string;
21
+ apiKey: ApiKey;
22
22
  thinkingLevel?: ThinkingLevel;
23
23
  files: FileDiff[];
24
24
  config?: {
@@ -1,5 +1,5 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import type { Api, Model } from "@oh-my-pi/pi-ai";
2
+ import type { Api, ApiKey, Model } from "@oh-my-pi/pi-ai";
3
3
  import { completeSimple } from "@oh-my-pi/pi-ai";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
5
  import reduceSystemPrompt from "../../commit/prompts/reduce-system.md" with { type: "text" };
@@ -12,7 +12,7 @@ const ReduceTool = createConventionalAnalysisTool("Synthesize file observations
12
12
 
13
13
  export interface ReducePhaseInput {
14
14
  model: Model<Api>;
15
- apiKey: string;
15
+ apiKey: ApiKey;
16
16
  thinkingLevel?: ThinkingLevel;
17
17
  observations: FileObservation[];
18
18
  stat: string;
@@ -1,7 +1,9 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import type { Api, Model } from "@oh-my-pi/pi-ai";
2
+ import type { Api, ApiKey, Model } from "@oh-my-pi/pi-ai";
3
+ import type { ApiKeyResolverRegistry } from "../config/api-key-resolver";
3
4
  import { MODEL_ROLE_IDS } from "../config/model-registry";
4
5
  import {
6
+ getModelMatchPreferences,
5
7
  type ModelLookupRegistry,
6
8
  parseModelPattern,
7
9
  resolveModelRoleValue,
@@ -12,13 +14,19 @@ import MODEL_PRIO from "../priority.json" with { type: "json" };
12
14
 
13
15
  export interface ResolvedCommitModel {
14
16
  model: Model<Api>;
15
- apiKey: string;
17
+ /**
18
+ * Resolver for the model's bearer: re-resolves on 401 / usage-limit so the
19
+ * whole commit pipeline (analysis, map/reduce, changelog) inherits the
20
+ * central force-refresh + account-rotation policy.
21
+ */
22
+ apiKey: ApiKey;
16
23
  thinkingLevel?: ThinkingLevel;
17
24
  }
18
25
 
19
- type CommitModelRegistry = ModelLookupRegistry & {
20
- getApiKey: (model: Model<Api>) => Promise<string | undefined>;
21
- };
26
+ type CommitModelRegistry = ModelLookupRegistry &
27
+ ApiKeyResolverRegistry & {
28
+ getApiKey: (model: Model<Api>) => Promise<string | undefined>;
29
+ };
22
30
 
23
31
  export async function resolvePrimaryModel(
24
32
  override: string | undefined,
@@ -26,7 +34,7 @@ export async function resolvePrimaryModel(
26
34
  modelRegistry: CommitModelRegistry,
27
35
  ): Promise<ResolvedCommitModel> {
28
36
  const available = modelRegistry.getAvailable();
29
- const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
37
+ const matchPreferences = getModelMatchPreferences(settings);
30
38
  const resolved = override
31
39
  ? resolveModelRoleValue(override, available, { settings, matchPreferences, modelRegistry })
32
40
  : resolveRoleSelection(["commit", "smol", ...MODEL_ROLE_IDS], settings, available, modelRegistry);
@@ -38,28 +46,45 @@ export async function resolvePrimaryModel(
38
46
  if (!apiKey) {
39
47
  throw new Error(`No API key available for model ${model.provider}/${model.id}`);
40
48
  }
41
- return { model, apiKey, thinkingLevel: resolved?.thinkingLevel };
49
+ return {
50
+ model,
51
+ apiKey: modelRegistry.resolver(model.provider, { baseUrl: model.baseUrl }),
52
+ thinkingLevel: resolved?.thinkingLevel,
53
+ };
42
54
  }
43
55
 
44
56
  export async function resolveSmolModel(
45
57
  settings: Settings,
46
58
  modelRegistry: CommitModelRegistry,
47
59
  fallbackModel: Model<Api>,
48
- fallbackApiKey: string,
60
+ fallbackApiKey: ApiKey,
49
61
  ): Promise<ResolvedCommitModel> {
50
62
  const available = modelRegistry.getAvailable();
51
63
  const resolvedSmol = resolveRoleSelection(["smol"], settings, available, modelRegistry);
52
64
  if (resolvedSmol?.model) {
53
65
  const apiKey = await modelRegistry.getApiKey(resolvedSmol.model);
54
- if (apiKey) return { model: resolvedSmol.model, apiKey, thinkingLevel: resolvedSmol.thinkingLevel };
66
+ if (apiKey) {
67
+ return {
68
+ model: resolvedSmol.model,
69
+ apiKey: modelRegistry.resolver(resolvedSmol.model.provider, {
70
+ baseUrl: resolvedSmol.model.baseUrl,
71
+ }),
72
+ thinkingLevel: resolvedSmol.thinkingLevel,
73
+ };
74
+ }
55
75
  }
56
76
 
57
- const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
77
+ const matchPreferences = getModelMatchPreferences(settings);
58
78
  for (const pattern of MODEL_PRIO.smol) {
59
79
  const candidate = parseModelPattern(pattern, available, matchPreferences, { modelRegistry }).model;
60
80
  if (!candidate) continue;
61
81
  const apiKey = await modelRegistry.getApiKey(candidate);
62
- if (apiKey) return { model: candidate, apiKey };
82
+ if (apiKey) {
83
+ return {
84
+ model: candidate,
85
+ apiKey: modelRegistry.resolver(candidate.provider, { baseUrl: candidate.baseUrl }),
86
+ };
87
+ }
63
88
  }
64
89
 
65
90
  return { model: fallbackModel, apiKey: fallbackApiKey };
@@ -1,6 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
3
- import type { Api, Model } from "@oh-my-pi/pi-ai";
3
+ import type { Api, ApiKey, Model } from "@oh-my-pi/pi-ai";
4
4
  import { getProjectDir, logger, prompt } from "@oh-my-pi/pi-utils";
5
5
  import { ModelRegistry } from "../config/model-registry";
6
6
  import { Settings } from "../config/settings";
@@ -145,10 +145,10 @@ async function generateAnalysis(input: {
145
145
  contextFiles: Array<{ path: string; content: string }>;
146
146
  userContext?: string;
147
147
  primaryModel: Model<Api>;
148
- primaryApiKey: string;
148
+ primaryApiKey: ApiKey;
149
149
  primaryThinkingLevel?: ThinkingLevel;
150
150
  smolModel: Model<Api>;
151
- smolApiKey: string;
151
+ smolApiKey: ApiKey;
152
152
  smolThinkingLevel?: ThinkingLevel;
153
153
  commitSettings: {
154
154
  mapReduceEnabled: boolean;
@@ -206,7 +206,7 @@ async function generateSummaryWithRetry(input: {
206
206
  analysis: ConventionalAnalysis;
207
207
  stat: string;
208
208
  model: Model<Api>;
209
- apiKey: string;
209
+ apiKey: ApiKey;
210
210
  thinkingLevel?: ThinkingLevel;
211
211
  userContext?: string;
212
212
  }): Promise<{ summary: string }> {
@@ -0,0 +1,58 @@
1
+ import type { ApiKeyResolver, AuthStorage } from "@oh-my-pi/pi-ai";
2
+
3
+ export interface ApiKeyResolverOptions {
4
+ /** Session id for credential stickiness; read at resolve time by the caller. */
5
+ sessionId?: string;
6
+ /** Provider base URL hint forwarded to the auth-storage cascade. */
7
+ baseUrl?: string;
8
+ }
9
+
10
+ /**
11
+ * Minimal slice of `ModelRegistry` the resolver needs. Typed structurally so
12
+ * narrower registry shells (e.g. the commit pipeline's `CommitModelRegistry`)
13
+ * can build resolvers without depending on the full class.
14
+ */
15
+ export interface ApiKeyResolverRegistry {
16
+ getApiKeyForProvider(
17
+ provider: string,
18
+ sessionId?: string,
19
+ options?: { baseUrl?: string; forceRefresh?: boolean; signal?: AbortSignal },
20
+ ): Promise<string | undefined>;
21
+ authStorage: Pick<AuthStorage, "rotateSessionCredential">;
22
+ /**
23
+ * Build an {@link ApiKeyResolver} implementing the central a/b/c auth-retry
24
+ * policy: initial → resolve; step (b) → force-refresh same account; step (c)
25
+ * → rotate to a sibling credential, then re-resolve.
26
+ *
27
+ * The resolver is stateless (safe to reuse across requests). Callers that
28
+ * need the initial key for a guard can call `resolveApiKeyOnce(resolver)`.
29
+ */
30
+ resolver(provider: string, options?: ApiKeyResolverOptions): ApiKeyResolver;
31
+ }
32
+
33
+ /**
34
+ * Default implementation of {@link ApiKeyResolverRegistry.resolver}.
35
+ * Also usable standalone for structural registries that don't carry the method.
36
+ */
37
+ export function createApiKeyResolver(
38
+ registry: Pick<ApiKeyResolverRegistry, "getApiKeyForProvider" | "authStorage">,
39
+ provider: string,
40
+ options: ApiKeyResolverOptions = {},
41
+ ): ApiKeyResolver {
42
+ const { sessionId, baseUrl } = options;
43
+ return async ({ lastChance, error, signal }) => {
44
+ if (error === undefined) {
45
+ return registry.getApiKeyForProvider(provider, sessionId, { baseUrl });
46
+ }
47
+ if (lastChance) {
48
+ // Account constraint (401 / usage / account-rate-limit): rotate to a
49
+ // sibling credential. We do NOT honor any retry-after here — if a
50
+ // sibling exists we switch immediately; the precise no-sibling backoff
51
+ // is owned by `markUsageLimitReached` (default + server usage-report
52
+ // reset) and the outer whole-turn retry layer.
53
+ await registry.authStorage.rotateSessionCredential(provider, sessionId, { error, signal });
54
+ return registry.getApiKeyForProvider(provider, sessionId, { baseUrl });
55
+ }
56
+ return registry.getApiKeyForProvider(provider, sessionId, { baseUrl, forceRefresh: true, signal });
57
+ };
58
+ }
@@ -0,0 +1,55 @@
1
+ const DEFAULT_MODEL_PROVIDER_ORDER = [
2
+ // First-party / native account providers. Prefer these over relays when the
3
+ // same upstream model is available in more than one place.
4
+ "openai-codex",
5
+ "anthropic",
6
+ "openai",
7
+ "google-gemini-cli",
8
+ "google",
9
+ "google-vertex",
10
+ "kimi-code",
11
+ "moonshot",
12
+ "qwen-portal",
13
+ "zai",
14
+ "xai-oauth",
15
+ "xai",
16
+ "mistral",
17
+ "deepseek",
18
+ "groq",
19
+
20
+ // High-quality aggregators / hosted inference providers.
21
+ "fireworks",
22
+ "cerebras",
23
+ "openrouter",
24
+ "together",
25
+
26
+ // Generic gateways and editor/proxy providers. These are useful when picked
27
+ // explicitly, but should not win ambiguous automatic role selection.
28
+ "alibaba-coding-plan",
29
+ "google-antigravity",
30
+ "opencode-zen",
31
+ "gitlab-duo",
32
+ "opencode-go",
33
+ "kilo",
34
+ "vercel-ai-gateway",
35
+ "cloudflare-ai-gateway",
36
+ "nanogpt",
37
+ "github-copilot",
38
+ ] as const;
39
+
40
+ function addProviderRank(rank: Map<string, number>, provider: string): void {
41
+ const normalized = provider.trim().toLowerCase();
42
+ if (!normalized || rank.has(normalized)) return;
43
+ rank.set(normalized, rank.size);
44
+ }
45
+
46
+ export function buildModelProviderPriorityRank(configuredProviderOrder?: readonly string[]): Map<string, number> {
47
+ const rank = new Map<string, number>();
48
+ for (const provider of configuredProviderOrder ?? []) {
49
+ addProviderRank(rank, provider);
50
+ }
51
+ for (const provider of DEFAULT_MODEL_PROVIDER_ORDER) {
52
+ addProviderRank(rank, provider);
53
+ }
54
+ return rank;
55
+ }
@@ -95,12 +95,14 @@ const STARTUP_MODEL_CACHE_PROVIDER_IDS: readonly string[] = [
95
95
  ...SPECIAL_MODEL_MANAGER_PROVIDER_IDS,
96
96
  ];
97
97
 
98
+ import type { ApiKeyResolver } from "@oh-my-pi/pi-ai";
98
99
  import { registerOAuthProvider, unregisterOAuthProviders } from "@oh-my-pi/pi-ai/utils/oauth";
99
100
  import type { OAuthCredentials, OAuthLoginCallbacks } from "@oh-my-pi/pi-ai/utils/oauth/types";
100
101
  import { isRecord, logger } from "@oh-my-pi/pi-utils";
101
102
  import { parseModelString, resolveProviderModelReference } from "../config/model-resolver";
102
103
  import { isValidThemeColor, type ThemeColor } from "../modes/theme/theme";
103
104
  import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
105
+ import { type ApiKeyResolverOptions, createApiKeyResolver } from "./api-key-resolver";
104
106
  import { type ConfigError, ConfigFile } from "./config-file";
105
107
  import {
106
108
  buildCanonicalModelIndex,
@@ -116,6 +118,7 @@ import {
116
118
  getModelLikeIdSegments,
117
119
  stripBracketedModelIdAffixes,
118
120
  } from "./model-id-affixes";
121
+ import { buildModelProviderPriorityRank } from "./model-provider-priority";
119
122
  import {
120
123
  type ModelOverride,
121
124
  type ModelsConfig,
@@ -2206,27 +2209,8 @@ export class ModelRegistry {
2206
2209
  });
2207
2210
  }
2208
2211
 
2209
- #providerRank(models: readonly Model<Api>[]): Map<string, number> {
2210
- const configuredProviders = getConfiguredProviderOrderFromSettings();
2211
- const result = new Map<string, number>();
2212
- let nextRank = 0;
2213
- for (const provider of configuredProviders) {
2214
- const normalized = provider.trim().toLowerCase();
2215
- if (!normalized || result.has(normalized)) {
2216
- continue;
2217
- }
2218
- result.set(normalized, nextRank);
2219
- nextRank += 1;
2220
- }
2221
- for (const model of models) {
2222
- const normalized = model.provider.toLowerCase();
2223
- if (result.has(normalized)) {
2224
- continue;
2225
- }
2226
- result.set(normalized, nextRank);
2227
- nextRank += 1;
2228
- }
2229
- return result;
2212
+ #providerRank(): Map<string, number> {
2213
+ return buildModelProviderPriorityRank(getConfiguredProviderOrderFromSettings());
2230
2214
  }
2231
2215
 
2232
2216
  #resolveCanonicalVariant(
@@ -2236,7 +2220,7 @@ export class ModelRegistry {
2236
2220
  if (variants.length === 0) {
2237
2221
  return undefined;
2238
2222
  }
2239
- const providerRank = this.#providerRank(allCandidates);
2223
+ const providerRank = this.#providerRank();
2240
2224
  const modelOrder = new Map<string, number>();
2241
2225
  for (let index = 0; index < allCandidates.length; index += 1) {
2242
2226
  modelOrder.set(formatCanonicalVariantSelector(allCandidates[index]!), index);
@@ -2365,12 +2349,33 @@ export class ModelRegistry {
2365
2349
 
2366
2350
  /**
2367
2351
  * Get API key for a provider (e.g., "openai").
2352
+ *
2353
+ * `options.forceRefresh` powers step (b) of the auth-retry policy — it
2354
+ * re-mints the session-sticky OAuth token even when the cached copy still
2355
+ * looks valid. `options.signal` is threaded into any broker-bound refresh.
2368
2356
  */
2369
- async getApiKeyForProvider(provider: string, sessionId?: string, baseUrl?: string): Promise<string | undefined> {
2357
+ async getApiKeyForProvider(
2358
+ provider: string,
2359
+ sessionId?: string,
2360
+ options?: { baseUrl?: string; forceRefresh?: boolean; signal?: AbortSignal },
2361
+ ): Promise<string | undefined> {
2370
2362
  if (this.#keylessProviders.has(provider) && !this.authStorage.hasAuth(provider)) {
2371
2363
  return kNoAuth;
2372
2364
  }
2373
- return this.authStorage.getApiKey(provider, sessionId, { baseUrl });
2365
+ return this.authStorage.getApiKey(provider, sessionId, {
2366
+ baseUrl: options?.baseUrl,
2367
+ forceRefresh: options?.forceRefresh,
2368
+ signal: options?.signal,
2369
+ });
2370
+ }
2371
+
2372
+ /**
2373
+ * Build an {@link ApiKeyResolver} for this provider, implementing the
2374
+ * central a/b/c auth-retry policy. Callers that need the initial key for
2375
+ * a guard can call `resolveApiKeyOnce(resolver)`.
2376
+ */
2377
+ resolver(provider: string, options?: ApiKeyResolverOptions): ApiKeyResolver {
2378
+ return createApiKeyResolver(this, provider, options);
2374
2379
  }
2375
2380
 
2376
2381
  async #peekApiKeyForProvider(provider: string): Promise<string | undefined> {