@oh-my-pi/pi-coding-agent 17.0.4 → 17.0.6

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 (143) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/dist/cli.js +3616 -3676
  3. package/dist/types/advisor/advise-tool.d.ts +3 -0
  4. package/dist/types/advisor/runtime.d.ts +7 -1
  5. package/dist/types/async/job-manager.d.ts +2 -0
  6. package/dist/types/config/model-registry.d.ts +7 -0
  7. package/dist/types/config/model-resolver.d.ts +8 -0
  8. package/dist/types/config/models-config-schema.d.ts +10 -0
  9. package/dist/types/config/models-config.d.ts +6 -0
  10. package/dist/types/dap/client.d.ts +10 -0
  11. package/dist/types/dap/types.d.ts +6 -5
  12. package/dist/types/exec/bash-executor.d.ts +1 -0
  13. package/dist/types/extensibility/extensions/load-errors.d.ts +3 -0
  14. package/dist/types/extensibility/extensions/runner.d.ts +4 -2
  15. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +16 -0
  16. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +8 -0
  17. package/dist/types/launch/spawn-options.d.ts +10 -0
  18. package/dist/types/launch/spawn-options.test.d.ts +1 -0
  19. package/dist/types/modes/components/status-line/component.d.ts +10 -3
  20. package/dist/types/modes/components/status-line/types.d.ts +3 -1
  21. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  22. package/dist/types/modes/components/transcript-container.d.ts +4 -3
  23. package/dist/types/modes/components/tree-selector.d.ts +6 -2
  24. package/dist/types/modes/components/usage-row.d.ts +1 -1
  25. package/dist/types/modes/interactive-mode.d.ts +2 -0
  26. package/dist/types/modes/print-mode.d.ts +4 -0
  27. package/dist/types/modes/rpc/rpc-client.d.ts +1 -1
  28. package/dist/types/modes/rpc/rpc-frame.d.ts +9 -0
  29. package/dist/types/modes/rpc/rpc-input.d.ts +5 -0
  30. package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
  31. package/dist/types/modes/setup-wizard/index.d.ts +1 -1
  32. package/dist/types/modes/setup-wizard/scenes/model.d.ts +3 -0
  33. package/dist/types/modes/types.d.ts +2 -0
  34. package/dist/types/sdk.d.ts +2 -0
  35. package/dist/types/session/agent-session.d.ts +42 -6
  36. package/dist/types/session/messages.d.ts +6 -0
  37. package/dist/types/session/session-paths.d.ts +21 -4
  38. package/dist/types/tools/browser/aria/aria-snapshot.d.ts +17 -7
  39. package/dist/types/tools/browser/cmux/socket-client.d.ts +2 -0
  40. package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
  41. package/dist/types/tools/browser/tab-worker.d.ts +2 -0
  42. package/dist/types/tools/gh.d.ts +5 -3
  43. package/dist/types/tools/hub/index.d.ts +1 -1
  44. package/dist/types/tools/hub/jobs.d.ts +1 -0
  45. package/dist/types/tools/hub/types.d.ts +2 -0
  46. package/dist/types/tools/tool-timeouts.d.ts +1 -1
  47. package/dist/types/tools/xdev.d.ts +5 -3
  48. package/dist/types/utils/git.d.ts +33 -0
  49. package/dist/types/vibe/__tests__/token-rate.test.d.ts +1 -0
  50. package/dist/types/vibe/runtime.d.ts +23 -0
  51. package/dist/types/web/search/providers/codex.d.ts +1 -1
  52. package/package.json +12 -12
  53. package/src/advisor/__tests__/advisor.test.ts +150 -0
  54. package/src/advisor/advise-tool.ts +4 -0
  55. package/src/advisor/runtime.ts +38 -14
  56. package/src/async/job-manager.ts +3 -0
  57. package/src/cli/bench-cli.ts +8 -2
  58. package/src/cli/dry-balance-cli.ts +1 -0
  59. package/src/config/model-registry.ts +89 -8
  60. package/src/config/model-resolver.ts +78 -14
  61. package/src/config/models-config-schema.ts +1 -0
  62. package/src/config/settings.ts +3 -1
  63. package/src/dap/client.ts +168 -1
  64. package/src/dap/config.ts +51 -1
  65. package/src/dap/session.ts +575 -234
  66. package/src/dap/types.ts +6 -5
  67. package/src/discovery/agents.ts +2 -2
  68. package/src/exec/bash-executor.ts +68 -8
  69. package/src/extensibility/extensions/load-errors.ts +13 -0
  70. package/src/extensibility/extensions/runner.ts +6 -4
  71. package/src/extensibility/legacy-pi-coding-agent-shim.ts +26 -0
  72. package/src/extensibility/plugins/legacy-pi-compat.ts +41 -6
  73. package/src/launch/broker.ts +34 -31
  74. package/src/launch/client.ts +7 -1
  75. package/src/launch/spawn-options.test.ts +31 -0
  76. package/src/launch/spawn-options.ts +17 -0
  77. package/src/lsp/types.ts +5 -1
  78. package/src/main.ts +25 -4
  79. package/src/mcp/transports/stdio.test.ts +9 -1
  80. package/src/modes/components/ask-dialog.ts +137 -73
  81. package/src/modes/components/chat-transcript-builder.ts +10 -1
  82. package/src/modes/components/status-line/component.ts +57 -3
  83. package/src/modes/components/status-line/segments.ts +20 -3
  84. package/src/modes/components/status-line/types.ts +3 -1
  85. package/src/modes/components/tool-execution.ts +5 -0
  86. package/src/modes/components/transcript-container.ts +23 -122
  87. package/src/modes/components/tree-selector.ts +10 -4
  88. package/src/modes/components/usage-row.ts +17 -1
  89. package/src/modes/controllers/command-controller.ts +41 -1
  90. package/src/modes/controllers/event-controller.ts +7 -3
  91. package/src/modes/controllers/input-controller.ts +1 -1
  92. package/src/modes/controllers/selector-controller.ts +29 -8
  93. package/src/modes/interactive-mode.ts +102 -60
  94. package/src/modes/noninteractive-dispose.test.ts +14 -1
  95. package/src/modes/print-mode.ts +81 -9
  96. package/src/modes/rpc/rpc-client.ts +76 -35
  97. package/src/modes/rpc/rpc-frame.ts +156 -0
  98. package/src/modes/rpc/rpc-input.ts +38 -0
  99. package/src/modes/rpc/rpc-mode.ts +11 -4
  100. package/src/modes/setup-wizard/index.ts +2 -0
  101. package/src/modes/setup-wizard/scenes/model.ts +134 -0
  102. package/src/modes/setup-wizard/scenes/sign-in.ts +3 -1
  103. package/src/modes/types.ts +2 -0
  104. package/src/modes/utils/ui-helpers.ts +9 -3
  105. package/src/prompts/system/workflow-notice.md +89 -74
  106. package/src/prompts/tools/browser.md +3 -2
  107. package/src/prompts/tools/debug.md +2 -7
  108. package/src/prompts/tools/github.md +6 -1
  109. package/src/prompts/tools/hub.md +4 -2
  110. package/src/prompts/tools/task-async-contract.md +1 -0
  111. package/src/prompts/tools/task.md +9 -2
  112. package/src/sdk.ts +38 -6
  113. package/src/session/agent-session.ts +484 -188
  114. package/src/session/messages.test.ts +91 -0
  115. package/src/session/messages.ts +248 -110
  116. package/src/session/session-manager.ts +34 -3
  117. package/src/session/session-paths.ts +38 -9
  118. package/src/slash-commands/builtin-registry.ts +1 -0
  119. package/src/task/executor.ts +104 -33
  120. package/src/task/index.ts +46 -9
  121. package/src/task/worktree.ts +10 -0
  122. package/src/tools/browser/aria/aria-snapshot.ts +36 -8
  123. package/src/tools/browser/cmux/cmux-tab.ts +2 -1
  124. package/src/tools/browser/cmux/socket-client.ts +139 -3
  125. package/src/tools/browser/run-cancellation.ts +4 -0
  126. package/src/tools/browser/tab-protocol.ts +2 -0
  127. package/src/tools/browser/tab-supervisor.ts +21 -11
  128. package/src/tools/browser/tab-worker.ts +199 -33
  129. package/src/tools/debug.ts +3 -0
  130. package/src/tools/gh.ts +40 -2
  131. package/src/tools/hub/index.ts +4 -1
  132. package/src/tools/hub/jobs.ts +42 -1
  133. package/src/tools/hub/types.ts +2 -0
  134. package/src/tools/tool-timeouts.ts +1 -1
  135. package/src/tools/xdev.ts +11 -4
  136. package/src/tools/yield.ts +4 -1
  137. package/src/utils/git.ts +237 -0
  138. package/src/{modes/components/status-line → utils}/token-rate.ts +6 -0
  139. package/src/vibe/__tests__/token-rate.test.ts +96 -0
  140. package/src/vibe/runtime.ts +50 -0
  141. package/src/web/search/index.ts +9 -5
  142. package/src/web/search/providers/codex.ts +195 -99
  143. /package/dist/types/{modes/components/status-line → utils}/token-rate.d.ts +0 -0
@@ -2,6 +2,7 @@ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
2
  import { estimateTokens } from "@oh-my-pi/pi-agent-core/compaction";
3
3
  import type { AssistantMessage, ImageContent, TextContent } from "@oh-my-pi/pi-ai";
4
4
  import * as AIError from "@oh-my-pi/pi-ai/error";
5
+ import { type CursorExecResolvedCarrier, kCursorExecResolved } from "@oh-my-pi/pi-ai/utils/block-symbols";
5
6
  import { logger } from "@oh-my-pi/pi-utils";
6
7
  import { obfuscateToolArguments, type SecretObfuscator } from "../secrets/obfuscator";
7
8
  import { formatSessionHistoryMarkdown, PRIMARY_CONTEXT_CUSTOM_TYPES } from "../session/session-history-format";
@@ -129,7 +130,20 @@ export function quarantineAdvisorUnsafeOutput(
129
130
  const unavailableToolNames = new Set<string>();
130
131
  const generatedParts: string[] = [];
131
132
  for (const block of message.content) {
132
- if (block.type === "toolCall" && !availableToolNames.has(block.name)) unavailableToolNames.add(block.name);
133
+ // Cursor exec-channel native blocks (bash/read/grep/...) are stamped
134
+ // kCursorExecResolved: they already ran server-side through the
135
+ // advisor-scoped CursorExecHandlers bridge, which rejects ungranted
136
+ // tools in-band ("Tool not available") and lets the model self-correct.
137
+ // Quarantining them would discard the legitimate advise emitted in the
138
+ // same turn (issue #5900). The scoped bridge is the grant gate here, not
139
+ // this pre-dispatch check.
140
+ if (
141
+ block.type === "toolCall" &&
142
+ !availableToolNames.has(block.name) &&
143
+ (block as CursorExecResolvedCarrier)[kCursorExecResolved] !== true
144
+ ) {
145
+ unavailableToolNames.add(block.name);
146
+ }
133
147
  if (block.type === "toolCall" && block.name === "advise" && typeof block.arguments.note === "string") {
134
148
  generatedParts.push(block.arguments.note);
135
149
  }
@@ -219,8 +233,7 @@ interface PendingDelta {
219
233
 
220
234
  interface CatchupWaiter {
221
235
  threshold: number;
222
- resolve: () => void;
223
- finish: () => void;
236
+ finish: (caughtUp: boolean) => void;
224
237
  timer?: NodeJS.Timeout;
225
238
  }
226
239
 
@@ -366,7 +379,13 @@ export class AdvisorRuntime {
366
379
  }
367
380
  }
368
381
 
369
- waitForCatchup(maxMs: number, threshold: number, signal?: AbortSignal): Promise<void> {
382
+ /**
383
+ * Wait until the advisor backlog falls below `threshold`.
384
+ *
385
+ * Returns `false` when the deadline, abort signal, or a runtime failure releases
386
+ * the waiter before the requested backlog was drained.
387
+ */
388
+ waitForCatchup(maxMs: number, threshold: number, signal?: AbortSignal): Promise<boolean> {
370
389
  if (
371
390
  this.disposed ||
372
391
  signal?.aborted ||
@@ -378,21 +397,26 @@ export class AdvisorRuntime {
378
397
  // primary would otherwise park for the full catch-up budget.
379
398
  this.#failing
380
399
  )
381
- return Promise.resolve();
382
- const { promise, resolve } = Promise.withResolvers<void>();
400
+ return Promise.resolve(this.#backlog < threshold);
401
+ const { promise, resolve } = Promise.withResolvers<boolean>();
383
402
  let waiter!: CatchupWaiter;
384
- const finish = (): void => {
403
+ const finish = (caughtUp: boolean): void => {
385
404
  const idx = this.#waiters.indexOf(waiter);
386
405
  if (idx >= 0) this.#waiters.splice(idx, 1);
387
406
  clearTimeout(waiter.timer);
388
- signal?.removeEventListener("abort", finish);
389
- resolve();
407
+ signal?.removeEventListener("abort", abort);
408
+ resolve(caughtUp);
409
+ };
410
+ const abort = (): void => finish(false);
411
+ waiter = {
412
+ threshold,
413
+ finish,
414
+ timer: setTimeout(abort, maxMs),
390
415
  };
391
- waiter = { threshold, resolve, finish, timer: setTimeout(finish, maxMs) };
392
416
  this.#waiters.push(waiter);
393
- signal?.addEventListener("abort", finish, { once: true });
417
+ signal?.addEventListener("abort", abort, { once: true });
394
418
  if (signal?.aborted) {
395
- finish();
419
+ abort();
396
420
  }
397
421
  return promise;
398
422
  }
@@ -571,14 +595,14 @@ export class AdvisorRuntime {
571
595
  for (let i = this.#waiters.length - 1; i >= 0; i--) {
572
596
  const w = this.#waiters[i];
573
597
  if (this.#backlog < w.threshold) {
574
- w.finish();
598
+ w.finish(true);
575
599
  }
576
600
  }
577
601
  }
578
602
 
579
603
  #wakeAllWaiters(): void {
580
604
  for (const w of [...this.#waiters]) {
581
- w.finish();
605
+ w.finish(false);
582
606
  }
583
607
  }
584
608
 
@@ -37,6 +37,8 @@ export interface AsyncJob {
37
37
  promise: Promise<void>;
38
38
  resultText?: string;
39
39
  errorText?: string;
40
+ /** Latest tool-render details reported by the running job. */
41
+ latestDetails?: Record<string, unknown>;
40
42
  /**
41
43
  * Registry id of the agent that registered the job (e.g. "Main",
42
44
  * "AuthLoader"). Used by scoped cancel/list APIs so a subagent's teardown
@@ -205,6 +207,7 @@ export class AsyncJobManager {
205
207
  };
206
208
 
207
209
  const reportProgress = async (text: string, details?: Record<string, unknown>): Promise<void> => {
210
+ if (details) job.latestDetails = details;
208
211
  if (!options?.onProgress) return;
209
212
  try {
210
213
  await options.onProgress(text, details);
@@ -443,7 +443,7 @@ function resolveBenchModels(
443
443
  const resolved: BenchTarget[] = [];
444
444
  const errors: string[] = [];
445
445
  for (const selector of selectors) {
446
- const result = resolveCliModel({ cliModel: selector, modelRegistry, preferences });
446
+ const result = resolveCliModel({ cliModel: selector, modelRegistry, settings, preferences });
447
447
  if (result.error) {
448
448
  errors.push(`${selector}: ${result.error}`);
449
449
  continue;
@@ -454,7 +454,13 @@ function resolveBenchModels(
454
454
  }
455
455
  if (result.warning) writeStderr(`${chalk.yellow(`Warning: ${result.warning}`)}\n`);
456
456
  let model = result.model;
457
- const authenticated = resolveAuthenticatedAlternative(selector, model, modelRegistry, preferences.providerOrder);
457
+ const authSelector = result.configuredPatterns?.[result.configuredPatternIndex ?? 0] ?? selector;
458
+ const authenticated = resolveAuthenticatedAlternative(
459
+ authSelector,
460
+ model,
461
+ modelRegistry,
462
+ preferences.providerOrder,
463
+ );
458
464
  if (authenticated) {
459
465
  writeStderr(
460
466
  `${chalk.yellow(
@@ -555,6 +555,7 @@ async function resolveDryBalanceModel(
555
555
  const resolved = resolveCliModel({
556
556
  cliModel: modelSelector,
557
557
  modelRegistry,
558
+ settings,
558
559
  preferences,
559
560
  });
560
561
  if (resolved.error) throw new Error(resolved.error);
@@ -857,6 +857,18 @@ export class ModelRegistry {
857
857
  }
858
858
  }
859
859
  await this.#refreshRuntimeDiscoveries(strategy, new Set([providerId]));
860
+ // #reloadStaticModels above may have rebuilt #models from static sources,
861
+ // dropping models previously discovered by OTHER runtime providers (their
862
+ // fetchDynamicModels results live only in #models + the SQLite cache, not
863
+ // in #loadModels' static inputs). Restore them from cache with the default
864
+ // online-if-uncached strategy: no network while their cached row is
865
+ // fresh, so the scoped refresh above stays the only forced fetch.
866
+ const otherRuntimeProviderIds = new Set(
867
+ [...this.#runtimeModelManagers.keys()].filter(runtimeId => runtimeId !== providerId),
868
+ );
869
+ if (otherRuntimeProviderIds.size > 0) {
870
+ await this.#refreshRuntimeDiscoveries("online-if-uncached", otherRuntimeProviderIds);
871
+ }
860
872
  }
861
873
 
862
874
  /**
@@ -1105,9 +1117,33 @@ export class ModelRegistry {
1105
1117
  if (cache.fresh && cache.authoritative) {
1106
1118
  authoritativeFreshProviders.add(providerId);
1107
1119
  }
1108
- const models = cache.models.map(model =>
1109
- model.provider === providerId ? model : { ...model, provider: providerId },
1110
- );
1120
+ // The v10 model cache never persists request headers (#5780): restore
1121
+ // them from the bundled static catalog, and drop cached rows whose
1122
+ // headers cannot be rebuilt so the bundled fallback (which still
1123
+ // carries its headers) wins the startup merge instead of a cached
1124
+ // model with required transport headers missing.
1125
+ const omittedHeaderIds = new Set(cache.headerOmittedModelIds);
1126
+ const unrestorableHeaderIds = new Set(cache.unrestorableHeaderModelIds);
1127
+ const bundledById =
1128
+ omittedHeaderIds.size > 0
1129
+ ? new Map(
1130
+ (getBundledModels(providerId as Parameters<typeof getBundledModels>[0]) as Model<Api>[]).map(
1131
+ bundledModel => [bundledModel.id, bundledModel],
1132
+ ),
1133
+ )
1134
+ : undefined;
1135
+ const models: ModelSpec<Api>[] = [];
1136
+ for (const cachedModel of cache.models) {
1137
+ const spec = cachedModel.provider === providerId ? cachedModel : { ...cachedModel, provider: providerId };
1138
+ if (!omittedHeaderIds.has(spec.id)) {
1139
+ models.push(spec);
1140
+ continue;
1141
+ }
1142
+ if (unrestorableHeaderIds.has(spec.id)) continue;
1143
+ const bundledHeaders = bundledById?.get(spec.id)?.headers;
1144
+ if (!bundledHeaders) continue;
1145
+ models.push({ ...spec, headers: bundledHeaders });
1146
+ }
1111
1147
  const providerOverride = this.#providerOverrides.get(providerId);
1112
1148
  const withTransport = providerOverride
1113
1149
  ? models.map(model => this.#applyProviderTransportOverride(model, providerOverride))
@@ -1145,13 +1181,20 @@ export class ModelRegistry {
1145
1181
  continue;
1146
1182
  }
1147
1183
  const configStale = this.#isDiscoveryCacheOlderThanModelsConfig(cache.updatedAt);
1184
+ // Cached rows never persist headers (#5780); models that had live
1185
+ // headers cannot be rebuilt here, so exclude them and mark the
1186
+ // discovery stale to force a refetch instead of returning models
1187
+ // missing required transport headers.
1188
+ const omittedHeaderIds = new Set(cache.headerOmittedModelIds);
1189
+ const usableCacheModels =
1190
+ omittedHeaderIds.size > 0 ? cache.models.filter(model => !omittedHeaderIds.has(model.id)) : cache.models;
1148
1191
  const models = this.#applyProviderModelOverrides(
1149
1192
  providerConfig.provider,
1150
1193
  this.#normalizeDiscoverableModels(
1151
1194
  providerConfig,
1152
1195
  this.#applyProviderCompat(
1153
1196
  providerConfig.compat,
1154
- cache.models.map(model => buildModel(model)),
1197
+ usableCacheModels.map(model => buildModel(model)),
1155
1198
  ),
1156
1199
  ),
1157
1200
  );
@@ -1160,7 +1203,12 @@ export class ModelRegistry {
1160
1203
  provider: providerConfig.provider,
1161
1204
  status: "cached",
1162
1205
  optional: providerConfig.optional ?? false,
1163
- stale: providerConfig.discovery.type === "llama.cpp" || !cache.fresh || !cache.authoritative || configStale,
1206
+ stale:
1207
+ providerConfig.discovery.type === "llama.cpp" ||
1208
+ !cache.fresh ||
1209
+ !cache.authoritative ||
1210
+ configStale ||
1211
+ omittedHeaderIds.size > 0,
1164
1212
  fetchedAt: cache.updatedAt,
1165
1213
  models: models.map(model => model.id),
1166
1214
  });
@@ -1593,6 +1641,7 @@ export class ModelRegistry {
1593
1641
  providerId: string,
1594
1642
  strategy: ModelRefreshStrategy,
1595
1643
  cacheProviderId: string,
1644
+ authoritative: boolean,
1596
1645
  ): Promise<string | undefined> {
1597
1646
  const peekedKey = await this.#peekApiKeyForProvider(providerId);
1598
1647
  if (isAuthenticated(peekedKey) || strategy === "offline") {
@@ -1602,7 +1651,13 @@ export class ModelRegistry {
1602
1651
  if (oauthCredentials.length === 0) {
1603
1652
  return peekedKey;
1604
1653
  }
1605
- if (strategy === "online-if-uncached") {
1654
+ // Authoritative providers prune bundled models only when their manager is
1655
+ // actually constructed, which needs an authenticated key. A fresh cache does
1656
+ // not let us skip the refresh here: with an expired OAuth token peekedKey is
1657
+ // undefined, the manager is never added, and stale bundled models survive the
1658
+ // full cache TTL. So only take the no-refresh shortcut for non-authoritative
1659
+ // providers, whose bundled models stay visible regardless.
1660
+ if (strategy === "online-if-uncached" && !authoritative) {
1606
1661
  // Mirror shouldFetchRemoteSources: built-in managers use the catalog's
1607
1662
  // default TTL, so only refresh when the manager will actually fetch.
1608
1663
  const cache = readModelCache<Api>(
@@ -1634,11 +1689,13 @@ export class ModelRegistry {
1634
1689
  ): Promise<ModelManagerOptions<Api>[]> {
1635
1690
  const specialProviderDescriptors: Array<{
1636
1691
  providerId: string;
1692
+ authoritative: boolean;
1637
1693
  resolveKey: (value: string | undefined) => string | undefined;
1638
1694
  createOptions: (key: string) => ModelManagerOptions<Api>;
1639
1695
  }> = [
1640
1696
  {
1641
1697
  providerId: "google-antigravity",
1698
+ authoritative: false,
1642
1699
  resolveKey: extractGoogleOAuthToken,
1643
1700
  createOptions: oauthToken =>
1644
1701
  googleAntigravityModelManagerOptions({
@@ -1649,6 +1706,7 @@ export class ModelRegistry {
1649
1706
  },
1650
1707
  {
1651
1708
  providerId: "google-gemini-cli",
1709
+ authoritative: false,
1652
1710
  resolveKey: extractGoogleOAuthToken,
1653
1711
  createOptions: oauthToken =>
1654
1712
  googleGeminiCliModelManagerOptions({
@@ -1659,12 +1717,14 @@ export class ModelRegistry {
1659
1717
  },
1660
1718
  {
1661
1719
  providerId: "openai-codex",
1720
+ authoritative: true,
1662
1721
  resolveKey: value => value,
1663
1722
  createOptions: accessToken => {
1664
1723
  const accountId = resolveOAuthAccountIdForAccessToken(this.authStorage, "openai-codex", accessToken);
1665
1724
  return openaiCodexModelManagerOptions({
1666
1725
  accessToken,
1667
1726
  accountId,
1727
+ fetch: this.#fetch,
1668
1728
  });
1669
1729
  },
1670
1730
  },
@@ -1689,12 +1749,22 @@ export class ModelRegistry {
1689
1749
  const cacheProviderId =
1690
1750
  descriptor.createModelManagerOptions({ baseUrl: discoveryBaseUrl, fetch: this.#fetch })
1691
1751
  .cacheProviderId ?? descriptor.providerId;
1692
- return this.#resolveBuiltInDiscoveryApiKey(descriptor.providerId, strategy, cacheProviderId);
1752
+ return this.#resolveBuiltInDiscoveryApiKey(
1753
+ descriptor.providerId,
1754
+ strategy,
1755
+ cacheProviderId,
1756
+ descriptor.dynamicModelsAuthoritative ?? false,
1757
+ );
1693
1758
  }),
1694
1759
  );
1695
1760
  const specialKeys = await Promise.all(
1696
1761
  enabledSpecialProviderDescriptors.map(descriptor =>
1697
- this.#resolveBuiltInDiscoveryApiKey(descriptor.providerId, strategy, descriptor.providerId),
1762
+ this.#resolveBuiltInDiscoveryApiKey(
1763
+ descriptor.providerId,
1764
+ strategy,
1765
+ descriptor.providerId,
1766
+ descriptor.authoritative,
1767
+ ),
1698
1768
  ),
1699
1769
  );
1700
1770
  const options: ModelManagerOptions<Api>[] = [];
@@ -1986,6 +2056,17 @@ export class ModelRegistry {
1986
2056
  );
1987
2057
  }
1988
2058
 
2059
+ /**
2060
+ * Whether the provider's configured API key is resolved from a command.
2061
+ *
2062
+ * Callers use this to distinguish the registry's command-first resolver
2063
+ * path from lower-priority credentials in {@link authStorage}.
2064
+ */
2065
+ hasCommandBackedApiKey(provider: string): boolean {
2066
+ const keyConfig = this.#customProviderApiKeys.get(provider);
2067
+ return isCommandConfigValue(keyConfig);
2068
+ }
2069
+
1989
2070
  getDiscoverableProviders(): string[] {
1990
2071
  const disabledProviders = getDisabledProviderIdsFromSettings();
1991
2072
  return this.#discoverableProviders
@@ -1138,6 +1138,8 @@ export function resolveAgentPrewalkPattern(options: AgentPrewalkResolutionOption
1138
1138
  export interface ResolvedModelRoleValue {
1139
1139
  model: Model<Api> | undefined;
1140
1140
  thinkingLevel?: ConfiguredThinkingLevel;
1141
+ /** matchedPatternIndex identifies the first configured pattern that matched an available model. */
1142
+ matchedPatternIndex?: number;
1141
1143
  explicitThinkingLevel: boolean;
1142
1144
  warning: string | undefined;
1143
1145
  }
@@ -1167,11 +1169,12 @@ export function resolveModelRoleValue(
1167
1169
  // models) once and reuse it across every fallback pattern instead of
1168
1170
  // rebuilding it per pattern inside parseModelPattern.
1169
1171
  const preferenceContext = buildPreferenceContext(availableModels, matchPreferences);
1170
- for (const effectivePattern of effectivePatterns) {
1172
+ for (const [patternIndex, effectivePattern] of effectivePatterns.entries()) {
1171
1173
  const resolved = matchPatternWithContext(effectivePattern, availableModels, preferenceContext);
1172
1174
  if (resolved.model) {
1173
1175
  return {
1174
1176
  model: resolved.model,
1177
+ matchedPatternIndex: patternIndex,
1175
1178
  thinkingLevel: resolved.explicitThinkingLevel
1176
1179
  ? resolved.thinkingLevel === AUTO_THINKING
1177
1180
  ? AUTO_THINKING
@@ -1598,6 +1601,10 @@ export function filterAvailableModelsByEnabledPatterns(
1598
1601
 
1599
1602
  export interface ResolveCliModelResult {
1600
1603
  model: Model<Api> | undefined;
1604
+ /** configuredPatterns is the full configured fallback chain when the selector resolves through a role. */
1605
+ configuredPatterns?: string[];
1606
+ /** configuredPatternIndex identifies the configured role pattern that matched an available model. */
1607
+ configuredPatternIndex?: number;
1601
1608
  selector?: string;
1602
1609
  thinkingLevel?: ConfiguredThinkingLevel;
1603
1610
  warning: string | undefined;
@@ -1606,6 +1613,8 @@ export interface ResolveCliModelResult {
1606
1613
 
1607
1614
  /**
1608
1615
  * Resolve a single model from CLI flags.
1616
+ *
1617
+ * Exact model names take precedence over configured role names.
1609
1618
  */
1610
1619
  export function resolveCliModel(options: {
1611
1620
  cliProvider?: string;
@@ -1630,19 +1639,6 @@ export function resolveCliModel(options: {
1630
1639
  };
1631
1640
  }
1632
1641
 
1633
- if (!cliProvider && modelRoleAliasPrefixLength(cliModel) !== undefined) {
1634
- const resolved = resolveModelRoleValue(cliModel, availableModels, { settings, matchPreferences: preferences });
1635
- if (resolved.model) {
1636
- return {
1637
- model: resolved.model,
1638
- selector: formatModelString(resolved.model),
1639
- thinkingLevel: resolved.thinkingLevel,
1640
- warning: resolved.warning,
1641
- error: undefined,
1642
- };
1643
- }
1644
- }
1645
-
1646
1642
  const providerMap = new Map<string, string>();
1647
1643
  for (const model of availableModels) {
1648
1644
  providerMap.set(model.provider.toLowerCase(), model.provider);
@@ -1682,6 +1678,73 @@ export function resolveCliModel(options: {
1682
1678
  error: undefined,
1683
1679
  };
1684
1680
  }
1681
+ const { base: exactBase, level: exactThinkingLevel } = splitThinkingSuffix(
1682
+ trimmedModel,
1683
+ -1,
1684
+ MAX_THINKING_SUFFIX_OPTIONS,
1685
+ );
1686
+ if (exactThinkingLevel) {
1687
+ let exactSuffixed = findExactModelReferenceMatch(exactBase, availableModels);
1688
+ if (!exactSuffixed) {
1689
+ const lowerExactBase = exactBase.toLowerCase();
1690
+ exactSuffixed = availableModels.find(
1691
+ model =>
1692
+ model.id.toLowerCase() === lowerExactBase ||
1693
+ `${model.provider}/${model.id}`.toLowerCase() === lowerExactBase,
1694
+ );
1695
+ }
1696
+ if (exactSuffixed) {
1697
+ return {
1698
+ model: exactSuffixed,
1699
+ selector: formatModelString(exactSuffixed),
1700
+ warning: undefined,
1701
+ thinkingLevel: exactThinkingLevel,
1702
+ error: undefined,
1703
+ };
1704
+ }
1705
+ }
1706
+ }
1707
+ let configuredPatterns: string[] | undefined;
1708
+ if (!cliProvider) {
1709
+ const { base: bareRoleName, level: bareRoleThinkingLevel } = splitThinkingSuffix(
1710
+ trimmedModel,
1711
+ -1,
1712
+ MAX_THINKING_SUFFIX_OPTIONS,
1713
+ );
1714
+ const roleSelector =
1715
+ modelRoleAliasPrefixLength(trimmedModel) !== undefined
1716
+ ? trimmedModel
1717
+ : settings?.getModelRole(bareRoleName) !== undefined
1718
+ ? `${formatModelRoleAlias(bareRoleName)}${bareRoleThinkingLevel ? `:${bareRoleThinkingLevel}` : ""}`
1719
+ : undefined;
1720
+ if (roleSelector) {
1721
+ configuredPatterns = resolveConfiguredModelPatterns([roleSelector], settings);
1722
+ const resolved = resolveModelRoleValue(roleSelector, availableModels, {
1723
+ settings,
1724
+ matchPreferences: preferences,
1725
+ });
1726
+ if (resolved.model) {
1727
+ return {
1728
+ model: resolved.model,
1729
+ selector: formatModelString(resolved.model),
1730
+ configuredPatterns,
1731
+ configuredPatternIndex: resolved.matchedPatternIndex,
1732
+ thinkingLevel: resolved.thinkingLevel,
1733
+ warning: resolved.warning,
1734
+ error: undefined,
1735
+ };
1736
+ }
1737
+ if (configuredPatterns && configuredPatterns.length > 0) {
1738
+ return {
1739
+ model: undefined,
1740
+ configuredPatterns,
1741
+ selector: undefined,
1742
+ thinkingLevel: undefined,
1743
+ warning: resolved.warning,
1744
+ error: `Model "${trimmedModel}" not found. Run "omp models" to see available models.`,
1745
+ };
1746
+ }
1747
+ }
1685
1748
  }
1686
1749
 
1687
1750
  let pattern = trimmedModel;
@@ -1725,6 +1788,7 @@ export function resolveCliModel(options: {
1725
1788
  const display = provider ? `${provider}/${pattern}` : cliModel;
1726
1789
  return {
1727
1790
  model: undefined,
1791
+ configuredPatterns,
1728
1792
  selector: undefined,
1729
1793
  thinkingLevel: undefined,
1730
1794
  warning,
@@ -61,6 +61,7 @@ const OpenAICompatFields = {
61
61
  "supportsImageDetailOriginal?": "boolean",
62
62
  // anthropic-messages compat flags (same `compat` slot, per-api interpretation)
63
63
  "supportsEagerToolInputStreaming?": "boolean",
64
+ "allowAnthropicHeaderOverrides?": "boolean",
64
65
  "requiresToolResultId?": "boolean",
65
66
  "replayUnsignedThinking?": "boolean",
66
67
  } as const;
@@ -355,7 +355,9 @@ export class Settings {
355
355
  this.#cwd = path.normalize(options.cwd ?? getProjectDir());
356
356
  this.#agentDir = path.normalize(options.agentDir ?? getAgentDir());
357
357
  this.#configPath = options.inMemory ? null : path.join(this.#agentDir, MAIN_CONFIG_FILENAMES[0]);
358
- this.#configFiles = options.configFiles?.map(file => path.resolve(this.#cwd, expandTilde(file))) ?? [];
358
+ const configFiles = process.env.PI_CONFIG_FILES?.split(path.delimiter).filter(Boolean) ?? [];
359
+ if (options.configFiles) configFiles.push(...options.configFiles);
360
+ this.#configFiles = configFiles.map(file => path.resolve(this.#cwd, expandTilde(file)));
359
361
  this.#persist = !options.inMemory && options.readOnly !== true;
360
362
 
361
363
  if (options.overrides) {