@oh-my-pi/pi-coding-agent 17.0.1 → 17.0.3

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 (179) hide show
  1. package/CHANGELOG.md +162 -40
  2. package/dist/cli.js +4559 -4543
  3. package/dist/types/advisor/config.d.ts +14 -6
  4. package/dist/types/advisor/runtime.d.ts +20 -11
  5. package/dist/types/autolearn/controller.d.ts +1 -0
  6. package/dist/types/config/model-discovery.d.ts +17 -0
  7. package/dist/types/config/model-resolver.d.ts +6 -2
  8. package/dist/types/config/settings-schema.d.ts +39 -10
  9. package/dist/types/config/settings.d.ts +50 -0
  10. package/dist/types/cursor.d.ts +11 -0
  11. package/dist/types/discovery/helpers.d.ts +5 -2
  12. package/dist/types/eval/agent-bridge.d.ts +7 -19
  13. package/dist/types/exec/bash-executor.d.ts +2 -0
  14. package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
  15. package/dist/types/extensibility/extensions/runner.d.ts +7 -0
  16. package/dist/types/extensibility/extensions/types.d.ts +18 -0
  17. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +55 -1
  18. package/dist/types/extensibility/shared-events.d.ts +6 -0
  19. package/dist/types/extensibility/utils.d.ts +2 -2
  20. package/dist/types/lsp/client.d.ts +2 -0
  21. package/dist/types/lsp/types.d.ts +3 -0
  22. package/dist/types/mcp/transports/stdio.d.ts +42 -1
  23. package/dist/types/modes/components/advisor-config.d.ts +8 -1
  24. package/dist/types/modes/components/agent-hub.d.ts +15 -0
  25. package/dist/types/modes/components/hook-editor.d.ts +7 -0
  26. package/dist/types/modes/components/model-hub.d.ts +5 -2
  27. package/dist/types/modes/components/session-selector.d.ts +2 -0
  28. package/dist/types/modes/controllers/command-controller.d.ts +8 -0
  29. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  30. package/dist/types/modes/print-mode.d.ts +1 -1
  31. package/dist/types/modes/warp-events.d.ts +24 -0
  32. package/dist/types/modes/warp-events.test.d.ts +1 -0
  33. package/dist/types/plan-mode/model-transition.d.ts +47 -0
  34. package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
  35. package/dist/types/registry/agent-lifecycle.d.ts +26 -1
  36. package/dist/types/registry/persisted-agents.d.ts +3 -0
  37. package/dist/types/sdk.d.ts +27 -5
  38. package/dist/types/session/agent-session.d.ts +34 -10
  39. package/dist/types/session/session-entries.d.ts +6 -1
  40. package/dist/types/session/session-history-format.d.ts +10 -0
  41. package/dist/types/session/session-manager.d.ts +19 -0
  42. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
  43. package/dist/types/task/executor.d.ts +26 -1
  44. package/dist/types/task/index.d.ts +1 -1
  45. package/dist/types/task/label.d.ts +1 -1
  46. package/dist/types/task/parallel.d.ts +14 -0
  47. package/dist/types/task/structured-subagent.d.ts +111 -0
  48. package/dist/types/task/types.d.ts +51 -0
  49. package/dist/types/telemetry-export.d.ts +34 -9
  50. package/dist/types/tools/approval.d.ts +8 -0
  51. package/dist/types/tools/bash.d.ts +2 -0
  52. package/dist/types/tools/essential-tools.d.ts +29 -0
  53. package/dist/types/tools/fetch.d.ts +4 -5
  54. package/dist/types/tools/hub/messaging.d.ts +5 -1
  55. package/dist/types/tools/image-gen.d.ts +2 -1
  56. package/dist/types/tools/index.d.ts +17 -1
  57. package/dist/types/tools/todo.d.ts +31 -0
  58. package/dist/types/tools/xdev.d.ts +11 -2
  59. package/dist/types/tui/tree-list.d.ts +7 -0
  60. package/dist/types/utils/markit.d.ts +11 -0
  61. package/dist/types/utils/title-generator.d.ts +2 -1
  62. package/dist/types/web/search/providers/kimi.d.ts +4 -1
  63. package/dist/types/web/search/types.d.ts +1 -1
  64. package/package.json +21 -16
  65. package/src/advisor/__tests__/advisor.test.ts +1304 -42
  66. package/src/advisor/__tests__/config.test.ts +58 -2
  67. package/src/advisor/config.ts +76 -24
  68. package/src/advisor/runtime.ts +445 -92
  69. package/src/autolearn/controller.ts +23 -28
  70. package/src/cli/file-processor.ts +1 -2
  71. package/src/cli.ts +5 -1
  72. package/src/config/model-discovery.ts +81 -21
  73. package/src/config/model-registry.ts +25 -6
  74. package/src/config/model-resolver.ts +14 -7
  75. package/src/config/settings-schema.ts +46 -9
  76. package/src/config/settings.ts +405 -25
  77. package/src/cursor.ts +20 -3
  78. package/src/debug/report-bundle.ts +40 -4
  79. package/src/discovery/helpers.ts +28 -5
  80. package/src/eval/__tests__/agent-bridge.test.ts +133 -47
  81. package/src/eval/__tests__/prelude-agent.test.ts +29 -0
  82. package/src/eval/agent-bridge.ts +104 -477
  83. package/src/eval/jl/prelude.jl +7 -6
  84. package/src/eval/js/shared/prelude.txt +5 -4
  85. package/src/eval/py/prelude.py +11 -39
  86. package/src/eval/rb/prelude.rb +5 -6
  87. package/src/exec/bash-executor.ts +14 -5
  88. package/src/extensibility/custom-tools/loader.ts +3 -3
  89. package/src/extensibility/custom-tools/wrapper.ts +2 -1
  90. package/src/extensibility/extensions/loader.ts +3 -3
  91. package/src/extensibility/extensions/managed-timers.ts +83 -0
  92. package/src/extensibility/extensions/runner.ts +26 -0
  93. package/src/extensibility/extensions/types.ts +18 -0
  94. package/src/extensibility/extensions/wrapper.ts +2 -1
  95. package/src/extensibility/hooks/loader.ts +3 -3
  96. package/src/extensibility/legacy-pi-coding-agent-shim.ts +149 -8
  97. package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
  98. package/src/extensibility/plugins/manager.ts +2 -2
  99. package/src/extensibility/shared-events.ts +6 -0
  100. package/src/extensibility/utils.ts +91 -25
  101. package/src/irc/bus.ts +22 -3
  102. package/src/launch/broker.ts +3 -2
  103. package/src/launch/client.ts +2 -2
  104. package/src/launch/presence.ts +2 -2
  105. package/src/lsp/client.ts +58 -1
  106. package/src/lsp/index.ts +62 -6
  107. package/src/lsp/types.ts +3 -0
  108. package/src/main.ts +11 -8
  109. package/src/mcp/manager.ts +9 -3
  110. package/src/mcp/oauth-flow.ts +20 -0
  111. package/src/mcp/transports/stdio.test.ts +269 -1
  112. package/src/mcp/transports/stdio.ts +255 -24
  113. package/src/modes/components/advisor-config.ts +65 -3
  114. package/src/modes/components/agent-hub.ts +1 -72
  115. package/src/modes/components/ask-dialog.ts +1 -1
  116. package/src/modes/components/bash-execution.ts +7 -3
  117. package/src/modes/components/eval-execution.ts +3 -1
  118. package/src/modes/components/hook-editor.ts +18 -3
  119. package/src/modes/components/model-hub.ts +138 -42
  120. package/src/modes/components/session-selector.ts +4 -0
  121. package/src/modes/components/status-line/component.test.ts +1 -0
  122. package/src/modes/components/status-line/segments.ts +21 -6
  123. package/src/modes/controllers/command-controller.ts +167 -47
  124. package/src/modes/controllers/event-controller.ts +5 -0
  125. package/src/modes/controllers/extension-ui-controller.ts +4 -22
  126. package/src/modes/controllers/input-controller.ts +12 -12
  127. package/src/modes/controllers/selector-controller.ts +191 -31
  128. package/src/modes/interactive-mode.ts +169 -62
  129. package/src/modes/print-mode.ts +3 -3
  130. package/src/modes/rpc/host-tools.ts +2 -1
  131. package/src/modes/rpc/rpc-mode.ts +19 -4
  132. package/src/modes/warp-events.test.ts +794 -0
  133. package/src/modes/warp-events.ts +232 -0
  134. package/src/plan-mode/model-transition.test.ts +60 -0
  135. package/src/plan-mode/model-transition.ts +51 -0
  136. package/src/prompts/system/system-prompt.md +1 -1
  137. package/src/prompts/tools/eval.md +5 -2
  138. package/src/prompts/tools/read.md +1 -1
  139. package/src/prompts/tools/task.md +12 -8
  140. package/src/prompts/tools/write.md +1 -1
  141. package/src/registry/agent-lifecycle.ts +133 -18
  142. package/src/registry/persisted-agents.ts +74 -0
  143. package/src/sdk.ts +343 -122
  144. package/src/session/agent-session.ts +1359 -368
  145. package/src/session/session-entries.ts +6 -1
  146. package/src/session/session-history-format.ts +20 -5
  147. package/src/session/session-manager.ts +57 -0
  148. package/src/session/streaming-output.ts +41 -1
  149. package/src/slash-commands/builtin-registry.ts +7 -0
  150. package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
  151. package/src/system-prompt.ts +7 -2
  152. package/src/task/executor.ts +100 -22
  153. package/src/task/index.ts +258 -429
  154. package/src/task/label.ts +2 -0
  155. package/src/task/parallel.ts +43 -0
  156. package/src/task/persisted-revive.ts +19 -7
  157. package/src/task/structured-subagent.ts +642 -0
  158. package/src/task/types.ts +58 -0
  159. package/src/telemetry-export.ts +453 -97
  160. package/src/tools/__tests__/eval-description.test.ts +1 -1
  161. package/src/tools/approval.ts +11 -0
  162. package/src/tools/bash.ts +71 -38
  163. package/src/tools/essential-tools.ts +45 -0
  164. package/src/tools/fetch.ts +28 -105
  165. package/src/tools/gh.ts +169 -2
  166. package/src/tools/hub/messaging.ts +16 -3
  167. package/src/tools/image-gen.ts +69 -7
  168. package/src/tools/index.ts +50 -15
  169. package/src/tools/path-utils.ts +1 -0
  170. package/src/tools/read.ts +62 -29
  171. package/src/tools/todo.ts +126 -13
  172. package/src/tools/write.ts +22 -4
  173. package/src/tools/xdev.ts +14 -3
  174. package/src/tui/tree-list.ts +39 -0
  175. package/src/utils/markit.ts +12 -0
  176. package/src/utils/title-generator.ts +15 -4
  177. package/src/utils/zip.ts +16 -1
  178. package/src/web/search/providers/kimi.ts +18 -12
  179. package/src/web/search/types.ts +6 -1
@@ -41,11 +41,13 @@ export function buildAutoLearnInstructions(available: { manageSkill: boolean; le
41
41
  export interface AutoLearnControllerOptions {
42
42
  session: AgentSession;
43
43
  settings: Settings;
44
+ capture: (content: string) => Promise<void>;
44
45
  }
45
46
 
46
47
  export class AutoLearnController {
47
48
  readonly #session: AgentSession;
48
49
  readonly #settings: Settings;
50
+ readonly #capture: (content: string) => Promise<void>;
49
51
  #toolCalls = 0;
50
52
  /**
51
53
  * Whether the in-flight turn BEGAN while goal mode was active. Captured at
@@ -54,12 +56,15 @@ export class AutoLearnController {
54
56
  * would let a goal-continuation turn slip through and get nudged.
55
57
  */
56
58
  #turnStartedInGoalMode = false;
57
- /** Swallow the agent_end produced by an auto-run capture turn so it cannot re-trigger. */
58
- #suppressNext = false;
59
+ /** Prevent overlapping private capture runs while real primary turns continue. */
60
+ #captureInFlight = false;
61
+ /** One newer eligible primary stop arrived while capture was running. */
62
+ #capturePending = false;
59
63
 
60
64
  constructor(options: AutoLearnControllerOptions) {
61
65
  this.#session = options.session;
62
66
  this.#settings = options.settings;
67
+ this.#capture = options.capture;
63
68
  // The listener closure captures `this`, so the session's listener array
64
69
  // keeps the controller alive — no stored unsubscribe needed.
65
70
  this.#session.subscribe(event => this.#onEvent(event));
@@ -91,10 +96,6 @@ export class AutoLearnController {
91
96
  const startedInGoalMode = this.#turnStartedInGoalMode;
92
97
  this.#turnStartedInGoalMode = false;
93
98
 
94
- if (this.#suppressNext) {
95
- this.#suppressNext = false;
96
- return;
97
- }
98
99
  // Never nudge a turn that ended in an abort (ESC, cancel, etc.). The
99
100
  // abort flag on the session is unreliable by the time agent_end is
100
101
  // deferred to subscribers; read stopReason from the event messages.
@@ -128,30 +129,24 @@ export class AutoLearnController {
128
129
  const autoContinue = this.#settings.get("autolearn.autoContinue") === true;
129
130
  if (!autoContinue) return;
130
131
 
131
- const content = AUTOLEARN_NUDGE_AUTOCONTINUE;
132
- // Arm suppression synchronously: the synthetic capture turn's agent_end
133
- // fires inside sendCustomMessage (before it resolves), so the flag must be
134
- // set before then. Disarm when no turn actually started — a deferred/queued
135
- // dispatch or a failed send produces no agent_end, and a latched flag would
136
- // otherwise swallow the next real stop.
137
- this.#suppressNext = true;
132
+ if (this.#captureInFlight) {
133
+ this.#capturePending = true;
134
+ return;
135
+ }
136
+ this.#startCapture();
137
+ }
138
138
 
139
- this.#session
140
- .sendCustomMessage(
141
- {
142
- customType: "autolearn-nudge",
143
- content,
144
- display: false,
145
- attribution: "user",
146
- },
147
- { deliverAs: "nextTurn", triggerTurn: true, acceptTerminalEmptyStop: true },
148
- )
149
- .then(started => {
150
- if (!started) this.#suppressNext = false;
151
- })
139
+ #startCapture(): void {
140
+ this.#captureInFlight = true;
141
+ void this.#capture(AUTOLEARN_NUDGE_AUTOCONTINUE)
152
142
  .catch(err => {
153
- this.#suppressNext = false;
154
- logger.warn("auto-learn nudge delivery failed", { err });
143
+ logger.warn("auto-learn capture failed", { err });
144
+ })
145
+ .finally(() => {
146
+ this.#captureInFlight = false;
147
+ if (!this.#capturePending) return;
148
+ this.#capturePending = false;
149
+ this.#startCapture();
155
150
  });
156
151
  }
157
152
  }
@@ -9,13 +9,12 @@ import chalk from "chalk";
9
9
  import { resolveReadPath } from "../tools/path-utils";
10
10
  import { formatBytes } from "../tools/render-utils";
11
11
  import { formatDimensionNote, resizeImage } from "../utils/image-resize";
12
- import { convertFileWithMarkit } from "../utils/markit";
12
+ import { CONVERTIBLE_EXTENSIONS, convertFileWithMarkit } from "../utils/markit";
13
13
 
14
14
  // Keep CLI startup responsive and avoid OOM when users pass huge files.
15
15
  // If a file exceeds these limits, we include it as a path-only <file/> block.
16
16
  const MAX_CLI_TEXT_BYTES = 5 * 1024 * 1024; // 5MB
17
17
  const MAX_CLI_IMAGE_BYTES = 25 * 1024 * 1024; // 25MB
18
- const CONVERTIBLE_EXTENSIONS = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".rtf", ".epub"]);
19
18
 
20
19
  export interface ProcessedFiles {
21
20
  text: string;
package/src/cli.ts CHANGED
@@ -338,7 +338,11 @@ export async function runCli(argv: string[]): Promise<void> {
338
338
  // worker's parked initial messages as soon as the entry module's
339
339
  // top-level evaluation finishes.
340
340
  if (resolvedArgv[0]?.startsWith("__omp_worker_")) {
341
- await runWorkerEntrypoint(resolvedArgv[0]);
341
+ const dispatched = await runWorkerEntrypoint(resolvedArgv[0]);
342
+ if (!dispatched) {
343
+ process.stderr.write(`Error: unknown worker selector: ${resolvedArgv[0]}\n`);
344
+ process.exitCode = 1;
345
+ }
342
346
  return;
343
347
  }
344
348
 
@@ -10,6 +10,7 @@ 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
+ isQwenModelId,
13
14
  resolveModelReference,
14
15
  stripBracketedModelIdAffixes,
15
16
  } from "@oh-my-pi/pi-catalog/identity";
@@ -512,6 +513,48 @@ async function discoverLlamaCppServerMetadata(
512
513
  }
513
514
  }
514
515
 
516
+ /**
517
+ * PrismLM Ternary/1-bit Bonsai GGUFs are Qwen3.6-27B derivatives served locally
518
+ * via llama.cpp; their ids do not contain "qwen", so match them explicitly here
519
+ * rather than broadening the global `isQwenModelId` predicate.
520
+ */
521
+ function isBonsaiQwenGguf(id: string): boolean {
522
+ return /(?:ternary-)?bonsai-27b/i.test(id);
523
+ }
524
+
525
+ /**
526
+ * applyLlamaCppQwenThinking rewrites a discovered or cached llama.cpp model so a
527
+ * Qwen-family chat template (which defaults `enable_thinking: true`) can be
528
+ * turned off. Qwen ids and the Qwen3.6-based PrismLM Ternary Bonsai GGUFs are
529
+ * routed through chat-completions (the implicit llama.cpp provider defaults to
530
+ * `openai-responses`, whose disable path has no Qwen encoding) with the
531
+ * `qwen-template-false` dialect; omp emits `preserve_thinking` inside
532
+ * `chat_template_kwargs` for Qwen, so the toggle rides there too and history
533
+ * `<think>` blocks survive (`qwenPreserveThinking`). The runtime base URL gets a
534
+ * `/v1` suffix because the chat-completions request would otherwise POST to the
535
+ * native root, which does not serve it. A model with a custom transport (e.g.
536
+ * `pi-native`, whose client appends `/v1/pi/stream`) keeps its base URL so the
537
+ * suffix is not doubled. Non-Qwen models pass through unchanged. Applied on both
538
+ * fresh discovery and cache load, so an upgraded cache is corrected without
539
+ * waiting for re-discovery.
540
+ */
541
+ export function applyLlamaCppQwenThinking(model: Model<Api>): Model<Api> {
542
+ if (!isQwenModelId(model.id) && !isBonsaiQwenGguf(model.id)) return model;
543
+ return buildModel({
544
+ ...model,
545
+ api: "openai-completions",
546
+ baseUrl: model.transport ? model.baseUrl : ensureLlamaCppV1BaseUrl(normalizeLlamaCppBaseUrl(model.baseUrl)),
547
+ reasoning: true,
548
+ compat: {
549
+ ...model.compatConfig,
550
+ supportsReasoningParams: true,
551
+ thinkingFormat: "qwen-chat-template",
552
+ reasoningDisableMode: "qwen-template-false",
553
+ qwenPreserveThinking: true,
554
+ },
555
+ } as unknown as ModelSpec<Api>);
556
+ }
557
+
515
558
  export async function discoverLlamaCppModels(
516
559
  providerConfig: DiscoveryProviderConfig,
517
560
  ctx: DiscoveryContext,
@@ -553,26 +596,31 @@ export async function discoverLlamaCppModels(
553
596
  serverMetadata?.contextWindow ??
554
597
  item.trainingContextWindow ??
555
598
  DISCOVERY_DEFAULT_CONTEXT_WINDOW;
599
+ // Local llama.cpp models stamp `reasoning: false` with a minimal compat;
600
+ // applyLlamaCppQwenThinking upgrades Qwen-family ids (which cannot disable
601
+ // their default-on thinking otherwise) after the base model is built.
556
602
  discovered.push(
557
- buildModel({
558
- id,
559
- name: id,
560
- api: providerConfig.api,
561
- provider: providerConfig.provider,
562
- baseUrl,
563
- reasoning: false,
564
- input: item.input ?? serverMetadata?.input ?? ["text"],
565
- imageInputDecoder: "stb",
566
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
567
- contextWindow,
568
- maxTokens: resolveLlamaCppMaxTokens(contextWindow, serverMetadata?.maxTokens),
569
- headers,
570
- compat: {
571
- supportsStore: false,
572
- supportsDeveloperRole: false,
573
- supportsReasoningEffort: false,
574
- },
575
- } as ModelSpec<Api>),
603
+ applyLlamaCppQwenThinking(
604
+ buildModel({
605
+ id,
606
+ name: id,
607
+ api: providerConfig.api,
608
+ provider: providerConfig.provider,
609
+ baseUrl,
610
+ reasoning: false,
611
+ input: item.input ?? serverMetadata?.input ?? ["text"],
612
+ imageInputDecoder: "stb",
613
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
614
+ contextWindow,
615
+ maxTokens: resolveLlamaCppMaxTokens(contextWindow, serverMetadata?.maxTokens),
616
+ headers,
617
+ compat: {
618
+ supportsStore: false,
619
+ supportsDeveloperRole: false,
620
+ supportsReasoningEffort: false,
621
+ },
622
+ } as ModelSpec<Api>),
623
+ ),
576
624
  );
577
625
  }
578
626
  return discovered;
@@ -583,7 +631,12 @@ export async function discoverLlamaCppModelRuntimeMetadata(
583
631
  ctx: DiscoveryContext,
584
632
  ): Promise<LlamaCppDiscoveredModelRuntimeMetadata | undefined> {
585
633
  const baseUrl = normalizeLlamaCppBaseUrl(model.baseUrl);
586
- const modelsUrl = `${baseUrl}/models`;
634
+ // Probe the native `/models` endpoint (not the OpenAI-compatible `/v1/models`)
635
+ // so the runtime `meta`, `status.args`, and `architecture.input_modalities`
636
+ // fields survive; a Qwen model routed to chat-completions carries a `/v1`
637
+ // base URL, which would otherwise send this to `/v1/models`.
638
+ const nativeBaseUrl = toLlamaCppNativeBaseUrl(baseUrl);
639
+ const modelsUrl = `${nativeBaseUrl}/models`;
587
640
  const baseHeaders: Record<string, string> = { ...(model.headers ?? {}) };
588
641
  const attempt = async (headers: Record<string, string>) => {
589
642
  const [entries, serverMetadata] = await Promise.all([
@@ -597,7 +650,7 @@ export async function discoverLlamaCppModelRuntimeMetadata(
597
650
  }
598
651
  return parseLlamaCppModelList(await response.json());
599
652
  }),
600
- discoverLlamaCppServerMetadata(ctx, baseUrl, headers),
653
+ discoverLlamaCppServerMetadata(ctx, nativeBaseUrl, headers),
601
654
  ]);
602
655
  if (!entries) {
603
656
  return undefined;
@@ -898,6 +951,13 @@ function normalizeLlamaCppBaseUrl(baseUrl?: string): string {
898
951
  }
899
952
  }
900
953
 
954
+ // ensureLlamaCppV1BaseUrl appends the OpenAI-compatible `/v1` prefix a
955
+ // chat-completions request needs; native discovery keeps the bare root, which
956
+ // serves `/models` and `/props` but not `/chat/completions`.
957
+ function ensureLlamaCppV1BaseUrl(baseUrl: string): string {
958
+ return baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
959
+ }
960
+
901
961
  function toLlamaCppNativeBaseUrl(baseUrl: string): string {
902
962
  try {
903
963
  const parsed = new URL(baseUrl);
@@ -71,6 +71,7 @@ import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
71
71
  import { type ApiKeyResolverModel, type ApiKeyResolverOptions, createApiKeyResolver } from "./api-key-resolver";
72
72
  import type { ConfigError, ConfigFile } from "./config-file";
73
73
  import {
74
+ applyLlamaCppQwenThinking,
74
75
  DISCOVERY_DEFAULT_MAX_TOKENS,
75
76
  type DiscoveryContext,
76
77
  type DiscoveryProviderConfig,
@@ -1020,7 +1021,7 @@ export class ModelRegistry {
1020
1021
  // Custom/config providers bypass the model-manager merge point —
1021
1022
  // collapse effort-tier variants here so X/X-thinking twins fold.
1022
1023
  const withModelOverrides = this.#applyModelOverrides(collapseBuiltModelVariants(combined), this.#modelOverrides);
1023
- this.#models = this.#applyRuntimeProviderOverrides(withModelOverrides);
1024
+ this.#models = this.#applyLlamaCppQwenThinkingToModels(this.#applyRuntimeProviderOverrides(withModelOverrides));
1024
1025
  this.#lastStaticLoadMtime = this.#modelsConfigFile.getMtimeMs();
1025
1026
  }
1026
1027
 
@@ -1417,7 +1418,7 @@ export class ModelRegistry {
1417
1418
  // Merge runtime extension models so they survive online discovery completion
1418
1419
  const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays);
1419
1420
  const withModelOverrides = this.#applyModelOverrides(collapseBuiltModelVariants(combined), this.#modelOverrides);
1420
- this.#models = this.#applyRuntimeProviderOverrides(withModelOverrides);
1421
+ this.#models = this.#applyLlamaCppQwenThinkingToModels(this.#applyRuntimeProviderOverrides(withModelOverrides));
1421
1422
  }
1422
1423
 
1423
1424
  #configuredDiscoveryCacheProviderId(providerConfig: DiscoveryProviderConfig): string {
@@ -1779,6 +1780,22 @@ export class ModelRegistry {
1779
1780
  });
1780
1781
  }
1781
1782
 
1783
+ // #applyLlamaCppQwenThinkingToModels re-runs applyLlamaCppQwenThinking as the
1784
+ // outermost transform for llama.cpp-provider models, after discovery merges,
1785
+ // cache fallbacks, and provider/transport overrides have run. It is
1786
+ // idempotent, so it restores the routed Qwen model's chat-completions api,
1787
+ // `/v1` runtime base URL, and disable dialect even when a configured `baseUrl`
1788
+ // override (which wins in mergeDiscoveredModel) or a fallback to a pre-fix
1789
+ // cached row would otherwise leave the old spec in place.
1790
+ #applyLlamaCppQwenThinkingToModels(models: Model<Api>[]): Model<Api>[] {
1791
+ const llamaCppProviders = new Set<string>();
1792
+ for (const provider of this.#discoverableProviders) {
1793
+ if (provider.discovery.type === "llama.cpp") llamaCppProviders.add(provider.provider);
1794
+ }
1795
+ if (llamaCppProviders.size === 0) return models;
1796
+ return models.map(model => (llamaCppProviders.has(model.provider) ? applyLlamaCppQwenThinking(model) : model));
1797
+ }
1798
+
1782
1799
  #mergeProviderOverride(baseOverride: ProviderOverride | undefined, override: ProviderOverride): ProviderOverride {
1783
1800
  return {
1784
1801
  baseUrl: override.baseUrl ?? baseOverride?.baseUrl,
@@ -2306,10 +2323,12 @@ export class ModelRegistry {
2306
2323
  transportOverride,
2307
2324
  );
2308
2325
  this.#runtimeProviderOverrides.set(providerName, nextRuntimeOverride);
2309
- this.#models = this.#models.map(m => {
2310
- if (m.provider !== providerName) return m;
2311
- return this.#applyProviderTransportOverride(m, transportOverride);
2312
- });
2326
+ this.#models = this.#applyLlamaCppQwenThinkingToModels(
2327
+ this.#models.map(m => {
2328
+ if (m.provider !== providerName) return m;
2329
+ return this.#applyProviderTransportOverride(m, transportOverride);
2330
+ }),
2331
+ );
2313
2332
  }
2314
2333
  }
2315
2334
 
@@ -897,6 +897,10 @@ export function parseModelPattern(
897
897
  const DEFAULT_MODEL_ROLE = "default";
898
898
  const MODEL_ROLE_ALIAS_PREFIXES = [MODEL_ROLE_ALIAS_PREFIX, LEGACY_MODEL_ROLE_ALIAS_PREFIX];
899
899
 
900
+ export interface ModelRoleLookup {
901
+ getModelRole(role: ModelRole | string): string | undefined;
902
+ }
903
+
900
904
  function isModelRole(role: string): role is ModelRole {
901
905
  return (MODEL_ROLE_IDS as string[]).includes(role);
902
906
  }
@@ -913,7 +917,7 @@ function modelRoleAliasPrefixLength(value: string): number | undefined {
913
917
  return MODEL_ROLE_ALIAS_PREFIXES.find(prefix => value.startsWith(prefix))?.length;
914
918
  }
915
919
 
916
- function getModelRoleAlias(value: string, settings?: Settings): string | undefined {
920
+ function getModelRoleAlias(value: string, settings?: ModelRoleLookup): string | undefined {
917
921
  const normalized = value.trim();
918
922
  const prefixLength = modelRoleAliasPrefixLength(normalized);
919
923
  if (prefixLength === undefined) return undefined;
@@ -969,7 +973,7 @@ function resolveDefaultInheritedPatterns(
969
973
  role: ModelRole,
970
974
  configuredDefault: string | undefined,
971
975
  roleDefaults: string[],
972
- settings: Settings | undefined,
976
+ settings: ModelRoleLookup | undefined,
973
977
  visited: Set<string>,
974
978
  ): string[] {
975
979
  if (!shouldInheritDefaultBeforePriority(role) || !configuredDefault) return [];
@@ -1007,7 +1011,7 @@ function resolveDefaultInheritedPatterns(
1007
1011
 
1008
1012
  function resolveConfiguredRolePattern(
1009
1013
  value: string,
1010
- settings?: Settings,
1014
+ settings?: ModelRoleLookup,
1011
1015
  visited: Set<string> = new Set(),
1012
1016
  ): string[] | undefined {
1013
1017
  const normalized = value.trim();
@@ -1044,7 +1048,7 @@ function resolveConfiguredRolePattern(
1044
1048
  /**
1045
1049
  * Expand a role alias like "@smol" to the configured model string.
1046
1050
  */
1047
- export function expandRoleAlias(value: string, settings?: Settings): string {
1051
+ export function expandRoleAlias(value: string, settings?: ModelRoleLookup): string {
1048
1052
  const normalized = value.trim();
1049
1053
  if (normalized === DEFAULT_MODEL_ROLE) {
1050
1054
  return settings?.getModelRole("default") ?? value;
@@ -1054,7 +1058,10 @@ export function expandRoleAlias(value: string, settings?: Settings): string {
1054
1058
  return resolved ?? value;
1055
1059
  }
1056
1060
 
1057
- export function resolveConfiguredModelPatterns(value: string | string[] | undefined, settings?: Settings): string[] {
1061
+ export function resolveConfiguredModelPatterns(
1062
+ value: string | string[] | undefined,
1063
+ settings?: ModelRoleLookup,
1064
+ ): string[] {
1058
1065
  const patterns = normalizeModelPatternList(value);
1059
1066
  return patterns.flatMap(pattern => {
1060
1067
  const resolved = resolveConfiguredRolePattern(pattern, settings);
@@ -1138,7 +1145,7 @@ export interface ResolvedModelRoleValue {
1138
1145
  export function resolveModelRoleValue(
1139
1146
  roleValue: string | undefined,
1140
1147
  availableModels: Model<Api>[],
1141
- options?: { settings?: Settings; matchPreferences?: ModelMatchPreferences },
1148
+ options?: { settings?: Settings; roleLookup?: ModelRoleLookup; matchPreferences?: ModelMatchPreferences },
1142
1149
  ): ResolvedModelRoleValue {
1143
1150
  if (!roleValue) {
1144
1151
  return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning: undefined };
@@ -1149,7 +1156,7 @@ export function resolveModelRoleValue(
1149
1156
  return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning: undefined };
1150
1157
  }
1151
1158
 
1152
- const effectivePatterns = resolveConfiguredModelPatterns(normalized, options?.settings);
1159
+ const effectivePatterns = resolveConfiguredModelPatterns(normalized, options?.roleLookup ?? options?.settings);
1153
1160
  if (!effectivePatterns || effectivePatterns.length === 0) {
1154
1161
  return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning: undefined };
1155
1162
  }
@@ -67,6 +67,8 @@ import {
67
67
  // Schema Definition Types
68
68
  // ═══════════════════════════════════════════════════════════════════════════
69
69
 
70
+ export type ModelRoleStorage = "global" | "project";
71
+
70
72
  export type SettingTab =
71
73
  | "appearance"
72
74
  | "model"
@@ -509,6 +511,30 @@ export const SETTINGS_SCHEMA = {
509
511
 
510
512
  disabledExtensions: { type: "array", default: EMPTY_STRING_ARRAY },
511
513
 
514
+ modelRoleStorage: {
515
+ type: "enum",
516
+ values: ["global", "project"] as const,
517
+ default: "global",
518
+ ui: {
519
+ tab: "model",
520
+ group: "Prompt",
521
+ label: "Model Role Storage",
522
+ description: "Where model selector role assignments are saved",
523
+ options: [
524
+ {
525
+ value: "global",
526
+ label: "Global",
527
+ description: "Save role models in the active profile config (current behavior)",
528
+ },
529
+ {
530
+ value: "project",
531
+ label: "Per-project",
532
+ description: "Save project role models in .omp/config.yml; missing project roles use global defaults",
533
+ },
534
+ ],
535
+ },
536
+ },
537
+
512
538
  modelRoles: { type: "record", default: EMPTY_STRING_RECORD },
513
539
 
514
540
  modelTags: { type: "record", default: EMPTY_MODEL_TAGS_RECORD },
@@ -1439,7 +1465,7 @@ export const SETTINGS_SCHEMA = {
1439
1465
  group: "Retry & Fallback",
1440
1466
  label: "Retry Fallback Chains",
1441
1467
  description:
1442
- 'JSON object mapping model roles, model selectors ("provider/model-id"), or provider wildcards ("provider/*") to ordered fallback selectors, e.g. {"default":["openai/gpt-4o-mini"],"google-antigravity/*":["google/*","google-vertex/*"]}. Model-oriented keys apply whenever that model/provider is active, regardless of role; a "provider/*" entry keeps the failing model\'s id and swaps the provider.',
1468
+ 'JSON object mapping model roles, model selectors ("provider/model-id"), or provider wildcards ("provider/*") to ordered fallback selectors, e.g. {"default":["openai/gpt-4o-mini"],"google-antigravity/*":["google/*","google-vertex/*"]}. Model-oriented keys apply whenever that model/provider is active, regardless of role; a "provider/*" entry keeps the failing model\'s id and swaps the provider. An id-prefixed wildcard ("openrouter/google/*") re-prefixes the failing model\'s bare id (google-antigravity/gemini-x -> openrouter/google/gemini-x) and, used as a key, matches only that provider\'s ids under the prefix.',
1443
1469
  },
1444
1470
  },
1445
1471
  "retry.fallbackRevertPolicy": {
@@ -2466,7 +2492,7 @@ export const SETTINGS_SCHEMA = {
2466
2492
  group: "Auto-Learn",
2467
2493
  label: "Auto-run capture at stop",
2468
2494
  description:
2469
- "When on, auto-run one capture turn at stop (uses extra tokens). Off = passive reminder on your next turn.",
2495
+ "When on, auto-run one private capture turn at stop (uses extra tokens). When off, only standing auto-learn guidance remains.",
2470
2496
  condition: "autolearnActive",
2471
2497
  },
2472
2498
  },
@@ -3616,7 +3642,7 @@ export const SETTINGS_SCHEMA = {
3616
3642
  },
3617
3643
  "generate_image.enabled": {
3618
3644
  type: "boolean",
3619
- default: true,
3645
+ default: false,
3620
3646
  ui: {
3621
3647
  tab: "tools",
3622
3648
  group: "Available Tools",
@@ -4468,7 +4494,7 @@ export const SETTINGS_SCHEMA = {
4468
4494
  },
4469
4495
  "providers.image": {
4470
4496
  type: "enum",
4471
- values: ["auto", "openai", "antigravity", "xai", "gemini", "openrouter"] as const,
4497
+ values: ["auto", "openai", "openai-codex", "antigravity", "xai", "gemini", "openrouter"] as const,
4472
4498
  default: "auto",
4473
4499
  ui: {
4474
4500
  tab: "providers",
@@ -4480,9 +4506,19 @@ export const SETTINGS_SCHEMA = {
4480
4506
  value: "auto",
4481
4507
  label: "Auto",
4482
4508
  description:
4483
- "Priority: active session provider > GPT model image tool > Antigravity > xAI > OpenRouter > Gemini",
4509
+ "Priority: per-request provider > configured provider > active session provider > GPT model image tool > Codex subscription > Antigravity > xAI > OpenRouter > Gemini",
4510
+ },
4511
+ {
4512
+ value: "openai",
4513
+ label: "OpenAI",
4514
+ description:
4515
+ "OPENAI_API_KEY (gpt-image-2) or active GPT model; falls back to a connected Codex subscription",
4516
+ },
4517
+ {
4518
+ value: "openai-codex",
4519
+ label: "OpenAI Codex (ChatGPT)",
4520
+ description: "Uses a connected Codex / ChatGPT subscription — no OPENAI_API_KEY needed",
4484
4521
  },
4485
- { value: "openai", label: "OpenAI", description: "Uses the active GPT Responses/Codex model" },
4486
4522
  {
4487
4523
  value: "antigravity",
4488
4524
  label: "Antigravity",
@@ -4712,14 +4748,15 @@ export const SETTINGS_SCHEMA = {
4712
4748
 
4713
4749
  "providers.kimiApiFormat": {
4714
4750
  type: "enum",
4715
- values: ["openai", "anthropic"] as const,
4716
- default: "anthropic",
4751
+ values: ["auto", "openai", "anthropic"] as const,
4752
+ default: "auto",
4717
4753
  ui: {
4718
4754
  tab: "providers",
4719
4755
  group: "Protocol",
4720
4756
  label: "Kimi API Format",
4721
- description: "API format for Kimi Code provider",
4757
+ description: "API format for Kimi Code provider (auto follows live model metadata)",
4722
4758
  options: [
4759
+ { value: "auto", label: "Auto", description: "Use the model's server-declared protocol" },
4723
4760
  { value: "openai", label: "OpenAI", description: "api.kimi.com" },
4724
4761
  { value: "anthropic", label: "Anthropic", description: "api.moonshot.ai" },
4725
4762
  ],