@oh-my-pi/pi-coding-agent 13.11.1 → 13.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/package.json +7 -7
  3. package/src/capability/rule.ts +4 -0
  4. package/src/cli/commands/init-xdg.ts +27 -0
  5. package/src/cli/config-cli.ts +8 -3
  6. package/src/cli/shell-cli.ts +1 -1
  7. package/src/commands/config.ts +1 -1
  8. package/src/config/model-registry.ts +63 -10
  9. package/src/config/model-resolver.ts +84 -21
  10. package/src/config/settings-schema.ts +803 -637
  11. package/src/discovery/helpers.ts +8 -2
  12. package/src/exec/bash-executor.ts +62 -25
  13. package/src/extensibility/custom-tools/types.ts +2 -3
  14. package/src/extensibility/extensions/types.ts +2 -0
  15. package/src/extensibility/hooks/types.ts +2 -0
  16. package/src/index.ts +6 -6
  17. package/src/internal-urls/docs-index.generated.ts +2 -2
  18. package/src/memories/index.ts +20 -7
  19. package/src/memories/storage.ts +46 -32
  20. package/src/modes/components/agent-dashboard.ts +23 -35
  21. package/src/modes/components/assistant-message.ts +25 -2
  22. package/src/modes/components/btw-panel.ts +104 -0
  23. package/src/modes/components/settings-defs.ts +1 -1
  24. package/src/modes/components/settings-selector.ts +6 -6
  25. package/src/modes/controllers/btw-controller.ts +193 -0
  26. package/src/modes/controllers/command-controller.ts +1 -1
  27. package/src/modes/controllers/event-controller.ts +4 -0
  28. package/src/modes/controllers/input-controller.ts +10 -1
  29. package/src/modes/interactive-mode.ts +22 -0
  30. package/src/modes/prompt-action-autocomplete.ts +17 -3
  31. package/src/modes/rpc/rpc-client.ts +30 -19
  32. package/src/modes/theme/theme.ts +28 -36
  33. package/src/modes/types.ts +4 -0
  34. package/src/modes/utils/ui-helpers.ts +3 -0
  35. package/src/prompts/system/btw-user.md +8 -0
  36. package/src/prompts/system/custom-system-prompt.md +1 -1
  37. package/src/prompts/system/system-prompt.md +1 -0
  38. package/src/sdk.ts +17 -25
  39. package/src/session/agent-session.ts +65 -37
  40. package/src/session/blob-store.ts +32 -0
  41. package/src/session/compaction/compaction.ts +27 -6
  42. package/src/session/history-storage.ts +2 -2
  43. package/src/session/session-manager.ts +116 -44
  44. package/src/slash-commands/builtin-registry.ts +11 -0
  45. package/src/system-prompt.ts +4 -17
  46. package/src/task/agents.ts +1 -1
  47. package/src/task/index.ts +9 -8
  48. package/src/tools/browser.ts +11 -0
  49. package/src/tools/output-meta.ts +96 -3
  50. package/src/utils/title-generator.ts +70 -92
  51. package/src/utils/tools-manager.ts +1 -1
  52. package/src/web/scrapers/index.ts +7 -7
  53. package/src/web/scrapers/utils.ts +1 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,63 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [13.12.0] - 2026-03-14
6
+
7
+ ### Added
8
+
9
+ - Added per-rule TTSR interrupt mode override via `interruptMode` field in rule frontmatter to allow fine-grained control over when TTSR interrupts stream processing
10
+ - Added `task` model role to allow configuring a dedicated model for subtask execution via `modelRoles.task` setting
11
+ - Added `moveCursorToMessageEnd` and `moveCursorToMessageStart` prompt actions to navigate to the beginning and end of the entire message
12
+ - Added support for provider-level `compat` configuration to apply OpenAI compatibility settings across all models from a provider
13
+ - Added `reasoningEffortMap` configuration option to map reasoning effort levels to provider-specific values
14
+ - Added support for `supportsUsageInStreaming`, `requiresToolResultName`, `requiresAssistantAfterToolResult`, `requiresThinkingAsText`, `thinkingFormat`, and `supportsStrictMode` OpenAI compatibility options
15
+ - Added support for provider-configurable `OpenAICompat.extraBody` to inject request-body fields for custom gateway/proxy routing
16
+ - Added `close()` method to SessionManager for properly closing persistent writers after flushing pending data
17
+ - Added `omp config init-xdg` command to initialize XDG Base Directory structure on Linux
18
+ - Added `getHistoryDbPath()`, `getModelDbPath()`, `getMemoriesDir()`, `getTerminalSessionsDir()` path helpers
19
+
20
+ ### Changed
21
+
22
+ - Path resolution on Linux redirects to XDG locations when `XDG_DATA_HOME` / `XDG_STATE_HOME` / `XDG_CACHE_HOME` environment variables are set
23
+
24
+ ### Changed
25
+
26
+ - Changed TTSR interrupt logic to respect per-rule `interruptMode` settings, falling back to global `ttsr.interruptMode` when rule-level override is not specified
27
+ - Reorganized settings tabs from 12 tabs (display, agent, input, tools, config, services, bash, lsp, ttsr, status) to 8 focused tabs (appearance, model, interaction, context, editing, tools, tasks, providers) for improved discoverability
28
+ - Consolidated status line settings into the Appearance tab instead of a separate Status tab
29
+ - Reorganized sampling parameters (temperature, topP, topK, minP, presencePenalty, repetitionPenalty) into the Model tab
30
+ - Moved edit tool settings (mode, fuzzyMatch, fuzzyThreshold, streamingAbort) to the Editing tab
31
+ - Moved read tool settings (readLineNumbers, readHashLines, read.defaultLimit) to the Editing tab
32
+ - Moved LSP settings (lsp.enabled, lsp.formatOnWrite, lsp.diagnosticsOnWrite, lsp.diagnosticsOnEdit) to the Editing tab
33
+ - Moved bash interceptor settings to the Editing tab
34
+ - Moved Python settings (python.toolMode, python.kernelMode, python.sharedGateway) to the Editing tab
35
+ - Moved task delegation settings (task.isolation.*, task.eager, task.maxConcurrency, task.maxRecursionDepth) to the Tasks tab
36
+ - Moved skill and command settings to the Tasks tab
37
+ - Moved provider selection settings (providers.webSearch, providers.codeSearch, providers.image, etc.) to the Providers tab
38
+ - Moved Exa settings to the Providers tab
39
+ - Moved secret handling settings to the Providers tab
40
+ - Moved speech-to-text settings to the Interaction tab
41
+ - Moved context promotion, compaction, branch summary, memories, and TTSR settings to the Context tab
42
+ - Updated tab icon symbols across unicode, nerd, and ASCII presets to match new tab structure
43
+ - Changed default agent model from `default` to `pi/task` to enable independent model configuration for subtasks
44
+ - Changed agent model resolution to support single-pattern inheritance fallback, allowing `pi/task` agents to inherit the active session model when the task role is unconfigured
45
+ - Changed system prompt to use ISO 8601 date format (YYYY-MM-DD) instead of locale-specific formatting
46
+ - Changed system prompt template to use `{{date}}` instead of `{{dateTime}}` for current date display
47
+ - Changed tool download timeout from 15 seconds to 120 seconds to accommodate slower network conditions
48
+ - Changed working directory paths in system prompt to use forward slashes for consistency across platforms
49
+ - Modified bash executor to fall back to one-shot shell execution after a persistent session hard timeout, preventing subsequent commands from hanging
50
+
51
+ ### Removed
52
+
53
+ - Removed bash executor hard timeout recovery test file (functionality already documented in existing entries)
54
+
55
+ ### Fixed
56
+
57
+ - Fixed bash execution to fall back to one-shot shell runs after a persistent session hard timeout, preventing later commands from hanging until restart
58
+ - Fixed timeout handling in RpcClient to properly clear timeouts and prevent resource leaks
59
+ - Fixed AgentSession disposal to call SessionManager's `close()` method when available, ensuring proper cleanup of persistent writers
60
+ - Removed redundant `path.join()` call wrapping `getHistoryDbPath()` in history-storage.ts
61
+
5
62
  ## [13.11.1] - 2026-03-13
6
63
 
7
64
  ### Added
@@ -46,6 +103,7 @@
46
103
  - Added `buildNamedToolChoice` utility function to build provider-aware tool choice constraints for named tools
47
104
  - Support for comma/space-separated path lists in `find`, `grep`, `ast_grep`, and `ast_edit` tools (e.g., `apps/,packages/,phases/` or `apps/ packages/ phases/`)
48
105
  - New `resolveMultiSearchPath` and `resolveMultiFindPattern` functions to handle multi-path search inputs with automatic common base path detection
106
+ - Added `display.showTokenUsage` setting to show per-turn token usage (input, output, cache) on assistant messages
49
107
 
50
108
  ### Changed
51
109
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "13.11.1",
4
+ "version": "13.12.0",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -41,12 +41,12 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@mozilla/readability": "^0.6",
44
- "@oh-my-pi/omp-stats": "13.11.1",
45
- "@oh-my-pi/pi-agent-core": "13.11.1",
46
- "@oh-my-pi/pi-ai": "13.11.1",
47
- "@oh-my-pi/pi-natives": "13.11.1",
48
- "@oh-my-pi/pi-tui": "13.11.1",
49
- "@oh-my-pi/pi-utils": "13.11.1",
44
+ "@oh-my-pi/omp-stats": "13.12.0",
45
+ "@oh-my-pi/pi-agent-core": "13.12.0",
46
+ "@oh-my-pi/pi-ai": "13.12.0",
47
+ "@oh-my-pi/pi-natives": "13.12.0",
48
+ "@oh-my-pi/pi-tui": "13.12.0",
49
+ "@oh-my-pi/pi-utils": "13.12.0",
50
50
  "@sinclair/typebox": "^0.34",
51
51
  "@xterm/headless": "^6.0",
52
52
  "ajv": "^8.18",
@@ -20,6 +20,8 @@ export interface RuleFrontmatter {
20
20
  condition?: string | string[];
21
21
  /** New key for TTSR stream scope. */
22
22
  scope?: string | string[];
23
+ /** Per-rule TTSR interrupt mode override. */
24
+ interruptMode?: "never" | "prose-only" | "tool-only" | "always";
23
25
  [key: string]: unknown;
24
26
  }
25
27
 
@@ -43,6 +45,8 @@ export interface Rule {
43
45
  condition?: string[];
44
46
  /** Optional stream scope tokens (for example: text, thinking, tool:edit(*.ts)). */
45
47
  scope?: string[];
48
+ /** Per-rule TTSR interrupt mode override (falls back to global ttsr.interruptMode). */
49
+ interruptMode?: "never" | "prose-only" | "tool-only" | "always";
46
50
  /** Source metadata */
47
51
  _source: SourceMeta;
48
52
  }
@@ -0,0 +1,27 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+
5
+ const APP_NAME = "omp";
6
+
7
+ export async function initXdg(): Promise<void> {
8
+ if (process.platform !== "linux") {
9
+ console.error("XDG directory setup is only supported on Linux.");
10
+ process.exit(1);
11
+ }
12
+
13
+ const dataHome = process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local/share");
14
+ const stateHome = process.env.XDG_STATE_HOME || path.join(os.homedir(), ".local/state");
15
+ const cacheHome = process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache");
16
+
17
+ const dirs = [path.join(dataHome, APP_NAME), path.join(stateHome, APP_NAME), path.join(cacheHome, APP_NAME)];
18
+
19
+ for (const dir of dirs) {
20
+ await fs.mkdir(dir, { recursive: true });
21
+ console.log(`Created ${dir.replace(os.homedir(), "~")}`);
22
+ }
23
+
24
+ console.log("\nXDG directories initialized.");
25
+ console.log("Ensure XDG_DATA_HOME, XDG_STATE_HOME, and XDG_CACHE_HOME");
26
+ console.log("are set in your shell profile for omp to use them.");
27
+ }
@@ -19,12 +19,13 @@ import {
19
19
  } from "../config/settings";
20
20
  import { SETTINGS_SCHEMA } from "../config/settings-schema";
21
21
  import { theme } from "../modes/theme/theme";
22
+ import { initXdg } from "./commands/init-xdg";
22
23
 
23
24
  // =============================================================================
24
25
  // Types
25
26
  // =============================================================================
26
27
 
27
- export type ConfigAction = "list" | "get" | "set" | "reset" | "path";
28
+ export type ConfigAction = "list" | "get" | "set" | "reset" | "path" | "init-xdg";
28
29
 
29
30
  export interface ConfigCommandArgs {
30
31
  action: ConfigAction;
@@ -34,7 +35,6 @@ export interface ConfigCommandArgs {
34
35
  json?: boolean;
35
36
  };
36
37
  }
37
-
38
38
  // =============================================================================
39
39
  // Setting Filtering
40
40
  // =============================================================================
@@ -73,7 +73,7 @@ function getSettingValues(def: CliSettingDef): readonly string[] | undefined {
73
73
  // Argument Parser
74
74
  // =============================================================================
75
75
 
76
- const VALID_ACTIONS: ConfigAction[] = ["list", "get", "set", "reset", "path"];
76
+ const VALID_ACTIONS: ConfigAction[] = ["list", "get", "set", "reset", "path", "init-xdg"];
77
77
 
78
78
  /**
79
79
  * Parse config subcommand arguments.
@@ -251,6 +251,9 @@ export async function runConfigCommand(cmd: ConfigCommandArgs): Promise<void> {
251
251
  case "path":
252
252
  handlePath();
253
253
  break;
254
+ case "init-xdg":
255
+ await initXdg();
256
+ break;
254
257
  }
255
258
  }
256
259
 
@@ -394,6 +397,7 @@ ${chalk.bold("Commands:")}
394
397
  set <key> <value> Set a setting value
395
398
  reset <key> Reset a setting to its default value
396
399
  path Print the config directory path
400
+ init-xdg Initialize XDG Base Directory structure (Linux only)
397
401
 
398
402
  ${chalk.bold("Options:")}
399
403
  --json Output as JSON
@@ -406,6 +410,7 @@ ${chalk.bold("Examples:")}
406
410
  ${APP_NAME} config set defaultThinkingLevel medium
407
411
  ${APP_NAME} config reset steeringMode
408
412
  ${APP_NAME} config list --json
413
+ ${APP_NAME} config init-xdg
409
414
 
410
415
  ${chalk.bold("Boolean Values:")}
411
416
  true, false, yes, no, on, off, 1, 0
@@ -85,7 +85,7 @@ export async function runShellCommand(cmd: ShellCommandArgs): Promise<void> {
85
85
 
86
86
  const interruptHandler = () => {
87
87
  if (active) {
88
- shellSession.abort();
88
+ void shellSession.abort();
89
89
  return;
90
90
  }
91
91
  rl.close();
@@ -5,7 +5,7 @@ import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
5
5
  import { type ConfigAction, type ConfigCommandArgs, runConfigCommand } from "../cli/config-cli";
6
6
  import { initTheme } from "../modes/theme/theme";
7
7
 
8
- const ACTIONS: ConfigAction[] = ["list", "get", "set", "reset", "path"];
8
+ const ACTIONS: ConfigAction[] = ["list", "get", "set", "reset", "path", "init-xdg"];
9
9
 
10
10
  export default class Config extends Command {
11
11
  static description = "Manage configuration settings";
@@ -37,7 +37,7 @@ export function isAuthenticated(apiKey: string | undefined | null): apiKey is st
37
37
  return Boolean(apiKey) && apiKey !== kNoAuth;
38
38
  }
39
39
 
40
- export type ModelRole = "default" | "smol" | "slow" | "vision" | "plan" | "commit";
40
+ export type ModelRole = "default" | "smol" | "slow" | "vision" | "plan" | "commit" | "task";
41
41
 
42
42
  export interface ModelRoleInfo {
43
43
  tag?: string;
@@ -52,9 +52,10 @@ export const MODEL_ROLES: Record<ModelRole, ModelRoleInfo> = {
52
52
  vision: { tag: "VISION", name: "Vision", color: "error" },
53
53
  plan: { tag: "PLAN", name: "Architect", color: "muted" },
54
54
  commit: { tag: "COMMIT", name: "Commit", color: "dim" },
55
+ task: { tag: "TASK", name: "Subtask", color: "muted" },
55
56
  };
56
57
 
57
- export const MODEL_ROLE_IDS: ModelRole[] = ["default", "smol", "slow", "vision", "plan", "commit"];
58
+ export const MODEL_ROLE_IDS: ModelRole[] = ["default", "smol", "slow", "vision", "plan", "commit", "task"];
58
59
 
59
60
  const OpenRouterRoutingSchema = Type.Object({
60
61
  only: Type.Optional(Type.Array(Type.String())),
@@ -68,13 +69,36 @@ const VercelGatewayRoutingSchema = Type.Object({
68
69
  });
69
70
 
70
71
  // Schema for OpenAI compatibility settings
72
+ const ReasoningEffortMapSchema = Type.Object({
73
+ minimal: Type.Optional(Type.String()),
74
+ low: Type.Optional(Type.String()),
75
+ medium: Type.Optional(Type.String()),
76
+ high: Type.Optional(Type.String()),
77
+ xhigh: Type.Optional(Type.String()),
78
+ });
79
+
71
80
  const OpenAICompatSchema = Type.Object({
72
81
  supportsStore: Type.Optional(Type.Boolean()),
73
82
  supportsDeveloperRole: Type.Optional(Type.Boolean()),
74
83
  supportsReasoningEffort: Type.Optional(Type.Boolean()),
84
+ reasoningEffortMap: Type.Optional(ReasoningEffortMapSchema),
75
85
  maxTokensField: Type.Optional(Type.Union([Type.Literal("max_completion_tokens"), Type.Literal("max_tokens")])),
86
+ supportsUsageInStreaming: Type.Optional(Type.Boolean()),
87
+ requiresToolResultName: Type.Optional(Type.Boolean()),
88
+ requiresAssistantAfterToolResult: Type.Optional(Type.Boolean()),
89
+ requiresThinkingAsText: Type.Optional(Type.Boolean()),
90
+ thinkingFormat: Type.Optional(
91
+ Type.Union([
92
+ Type.Literal("openai"),
93
+ Type.Literal("zai"),
94
+ Type.Literal("qwen"),
95
+ Type.Literal("qwen-chat-template"),
96
+ ]),
97
+ ),
76
98
  openRouterRouting: Type.Optional(OpenRouterRoutingSchema),
77
99
  vercelGatewayRouting: Type.Optional(VercelGatewayRoutingSchema),
100
+ extraBody: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
101
+ supportsStrictMode: Type.Optional(Type.Boolean()),
78
102
  });
79
103
 
80
104
  const EffortSchema = Type.Union([
@@ -180,6 +204,7 @@ const ProviderConfigSchema = Type.Object({
180
204
  ]),
181
205
  ),
182
206
  headers: Type.Optional(Type.Record(Type.String(), Type.String())),
207
+ compat: Type.Optional(OpenAICompatSchema),
183
208
  authHeader: Type.Optional(Type.Boolean()),
184
209
  auth: Type.Optional(ProviderAuthSchema),
185
210
  discovery: Type.Optional(ProviderDiscoverySchema),
@@ -212,6 +237,7 @@ interface ProviderValidationConfig {
212
237
  auth?: ProviderAuthMode;
213
238
  oauthConfigured?: boolean;
214
239
  discovery?: ProviderDiscovery;
240
+ compat?: Model<Api>["compat"];
215
241
  modelOverrides?: Record<string, unknown>;
216
242
  models: ProviderValidationModel[];
217
243
  }
@@ -227,9 +253,9 @@ function validateProviderConfiguration(
227
253
  if (models.length === 0) {
228
254
  if (mode === "models-config") {
229
255
  const hasModelOverrides = config.modelOverrides && Object.keys(config.modelOverrides).length > 0;
230
- if (!config.baseUrl && !hasModelOverrides && !config.discovery) {
256
+ if (!config.baseUrl && !config.compat && !hasModelOverrides && !config.discovery) {
231
257
  throw new Error(
232
- `Provider ${providerName}: must specify "baseUrl", "modelOverrides", "discovery", or "models".`,
258
+ `Provider ${providerName}: must specify "baseUrl", "compat", "modelOverrides", "discovery", or "models"`,
233
259
  );
234
260
  }
235
261
  }
@@ -288,6 +314,7 @@ export const ModelsConfigFile = new ConfigFile<ModelsConfig>("models", ModelsCon
288
314
  api: providerConfig.api as Api | undefined,
289
315
  auth: (providerConfig.auth ?? "apiKey") as ProviderAuthMode,
290
316
  discovery: providerConfig.discovery as ProviderDiscovery | undefined,
317
+ compat: providerConfig.compat,
291
318
  modelOverrides: providerConfig.modelOverrides,
292
319
  models: (providerConfig.models ?? []) as ProviderValidationModel[],
293
320
  },
@@ -297,11 +324,12 @@ export const ModelsConfigFile = new ConfigFile<ModelsConfig>("models", ModelsCon
297
324
  },
298
325
  );
299
326
 
300
- /** Provider override config (baseUrl, headers, apiKey) without custom models */
327
+ /** Provider override config (baseUrl, headers, apiKey, compat) without custom models */
301
328
  interface ProviderOverride {
302
329
  baseUrl?: string;
303
330
  headers?: Record<string, string>;
304
331
  apiKey?: string;
332
+ compat?: Model<Api>["compat"];
305
333
  }
306
334
 
307
335
  interface DiscoveryProviderConfig {
@@ -309,6 +337,7 @@ interface DiscoveryProviderConfig {
309
337
  api: Api;
310
338
  baseUrl?: string;
311
339
  headers?: Record<string, string>;
340
+ compat?: Model<Api>["compat"];
312
341
  discovery: ProviderDiscovery;
313
342
  optional?: boolean;
314
343
  }
@@ -397,12 +426,18 @@ function mergeCompat(
397
426
  const base = baseCompat ?? {};
398
427
  const override = overrideCompat;
399
428
  const merged: NonNullable<Model<Api>["compat"]> = { ...base, ...override };
429
+ if (baseCompat?.reasoningEffortMap || overrideCompat.reasoningEffortMap) {
430
+ merged.reasoningEffortMap = { ...baseCompat?.reasoningEffortMap, ...overrideCompat.reasoningEffortMap };
431
+ }
400
432
  if (baseCompat?.openRouterRouting || overrideCompat.openRouterRouting) {
401
433
  merged.openRouterRouting = { ...baseCompat?.openRouterRouting, ...overrideCompat.openRouterRouting };
402
434
  }
403
435
  if (baseCompat?.vercelGatewayRouting || overrideCompat.vercelGatewayRouting) {
404
436
  merged.vercelGatewayRouting = { ...baseCompat?.vercelGatewayRouting, ...overrideCompat.vercelGatewayRouting };
405
437
  }
438
+ if (baseCompat?.extraBody || overrideCompat.extraBody) {
439
+ merged.extraBody = { ...baseCompat?.extraBody, ...overrideCompat.extraBody };
440
+ }
406
441
  return merged;
407
442
  }
408
443
 
@@ -475,6 +510,7 @@ function buildCustomModel(
475
510
  providerHeaders: Record<string, string> | undefined,
476
511
  providerApiKey: string | undefined,
477
512
  authHeader: boolean | undefined,
513
+ providerCompat: Model<Api>["compat"] | undefined,
478
514
  modelDef: CustomModelDefinitionLike,
479
515
  options: CustomModelBuildOptions,
480
516
  ): Model<Api> | undefined {
@@ -496,7 +532,7 @@ function buildCustomModel(
496
532
  contextWindow: modelDef.contextWindow ?? (withDefaults ? 128000 : undefined),
497
533
  maxTokens: modelDef.maxTokens ?? (withDefaults ? 16384 : undefined),
498
534
  headers: mergeCustomModelHeaders(providerHeaders, modelDef.headers, authHeader, providerApiKey),
499
- compat: modelDef.compat,
535
+ compat: mergeCompat(providerCompat, modelDef.compat),
500
536
  contextPromotionTarget: modelDef.contextPromotionTarget,
501
537
  premiumMultiplier: modelDef.premiumMultiplier,
502
538
  } as Model<Api>);
@@ -630,6 +666,7 @@ export class ModelRegistry {
630
666
  ...model,
631
667
  baseUrl: providerOverride.baseUrl ?? model.baseUrl,
632
668
  headers: providerOverride.headers ? { ...model.headers, ...providerOverride.headers } : model.headers,
669
+ compat: mergeCompat(model.compat, providerOverride.compat),
633
670
  };
634
671
  }
635
672
  const modelOverride = perModelOverrides?.get(m.id);
@@ -669,7 +706,10 @@ export class ModelRegistry {
669
706
  });
670
707
  continue;
671
708
  }
672
- const models = this.#applyProviderModelOverrides(providerConfig.provider, cache.models);
709
+ const models = this.#applyProviderModelOverrides(
710
+ providerConfig.provider,
711
+ this.#applyProviderCompat(providerConfig.compat, cache.models),
712
+ );
673
713
  cachedModels.push(...models);
674
714
  this.#providerDiscoveryStates.set(providerConfig.provider, {
675
715
  provider: providerConfig.provider,
@@ -683,6 +723,11 @@ export class ModelRegistry {
683
723
  return cachedModels;
684
724
  }
685
725
 
726
+ #applyProviderCompat(compat: Model<Api>["compat"] | undefined, models: Model<Api>[]): Model<Api>[] {
727
+ if (!compat) return models;
728
+ return models.map(model => ({ ...model, compat: mergeCompat(model.compat, compat) }));
729
+ }
730
+
686
731
  #addImplicitDiscoverableProviders(configuredProviders: Set<string>): void {
687
732
  if (!configuredProviders.has("ollama")) {
688
733
  this.#discoverableProviders.push({
@@ -752,12 +797,13 @@ export class ModelRegistry {
752
797
  const configuredProviders = new Set(Object.keys(value.providers));
753
798
 
754
799
  for (const [providerName, providerConfig] of Object.entries(value.providers)) {
755
- // Always set overrides when baseUrl/headers present
756
- if (providerConfig.baseUrl || providerConfig.headers || providerConfig.apiKey) {
800
+ // Always set overrides when baseUrl/headers/apiKey/compat are present
801
+ if (providerConfig.baseUrl || providerConfig.headers || providerConfig.apiKey || providerConfig.compat) {
757
802
  overrides.set(providerName, {
758
803
  baseUrl: providerConfig.baseUrl,
759
804
  headers: providerConfig.headers,
760
805
  apiKey: providerConfig.apiKey,
806
+ compat: providerConfig.compat,
761
807
  });
762
808
  }
763
809
 
@@ -772,6 +818,7 @@ export class ModelRegistry {
772
818
  api: providerConfig.api as Api,
773
819
  baseUrl: providerConfig.baseUrl,
774
820
  headers: providerConfig.headers,
821
+ compat: providerConfig.compat,
775
822
  discovery: providerConfig.discovery,
776
823
  optional: false,
777
824
  });
@@ -906,7 +953,10 @@ export class ModelRegistry {
906
953
  if (discoveryError) {
907
954
  this.#warnProviderDiscoveryFailure(providerConfig, discoveryError);
908
955
  }
909
- return this.#applyProviderModelOverrides(providerId, result.models);
956
+ return this.#applyProviderModelOverrides(
957
+ providerId,
958
+ this.#applyProviderCompat(providerConfig.compat, result.models),
959
+ );
910
960
  }
911
961
 
912
962
  #discoverModelsByProviderType(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
@@ -1302,6 +1352,7 @@ export class ModelRegistry {
1302
1352
  providerConfig.headers,
1303
1353
  providerConfig.apiKey,
1304
1354
  providerConfig.authHeader,
1355
+ providerConfig.compat,
1305
1356
  modelDef as CustomModelDefinitionLike,
1306
1357
  { useDefaults: true },
1307
1358
  );
@@ -1463,6 +1514,7 @@ export class ModelRegistry {
1463
1514
  config.headers,
1464
1515
  config.apiKey,
1465
1516
  config.authHeader,
1517
+ config.compat,
1466
1518
  modelDef as CustomModelDefinitionLike,
1467
1519
  { useDefaults: false },
1468
1520
  );
@@ -1506,6 +1558,7 @@ export interface ProviderConfigInput {
1506
1558
  api?: Api;
1507
1559
  streamSimple?: (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream;
1508
1560
  headers?: Record<string, string>;
1561
+ compat?: Model<Api>["compat"];
1509
1562
  authHeader?: boolean;
1510
1563
  oauth?: {
1511
1564
  name: string;
@@ -318,16 +318,41 @@ export function parseModelPattern(
318
318
  const PREFIX_MODEL_ROLE = "pi/";
319
319
  const DEFAULT_MODEL_ROLE = "default";
320
320
 
321
- /**
322
- * Check if a model override value is effectively the default role.
323
- */
324
- export function isDefaultModelAlias(value: string | string[] | undefined): boolean {
325
- if (!value) return true;
326
- if (Array.isArray(value)) return value.every(entry => isDefaultModelAlias(entry));
327
- if (value.startsWith(PREFIX_MODEL_ROLE)) {
328
- value = value.slice(PREFIX_MODEL_ROLE.length);
321
+ function getModelRoleAlias(value: string): ModelRole | undefined {
322
+ const normalized = value.trim();
323
+ if (!normalized.startsWith(PREFIX_MODEL_ROLE)) return undefined;
324
+
325
+ const candidate = normalized.slice(PREFIX_MODEL_ROLE.length);
326
+ for (const role of MODEL_ROLE_IDS) {
327
+ if (candidate === role) return role;
329
328
  }
330
- return value === DEFAULT_MODEL_ROLE;
329
+ return undefined;
330
+ }
331
+
332
+ function normalizeModelPatternList(value: string | string[] | undefined): string[] {
333
+ if (!value) return [];
334
+ const patterns = Array.isArray(value) ? value : value.split(",");
335
+ return patterns.map(pattern => pattern.trim()).filter(Boolean);
336
+ }
337
+
338
+ function isSessionInheritedAgentPattern(value: string): boolean {
339
+ return value === DEFAULT_MODEL_ROLE || value === `${PREFIX_MODEL_ROLE}${DEFAULT_MODEL_ROLE}` || value === "pi/task";
340
+ }
341
+
342
+ function resolveConfiguredRolePattern(value: string, settings?: Settings): string | undefined {
343
+ const normalized = value.trim();
344
+ if (!normalized) return undefined;
345
+
346
+ const lastColonIndex = normalized.lastIndexOf(":");
347
+ const thinkingLevel =
348
+ lastColonIndex > PREFIX_MODEL_ROLE.length ? parseThinkingLevel(normalized.slice(lastColonIndex + 1)) : undefined;
349
+ const aliasCandidate = thinkingLevel ? normalized.slice(0, lastColonIndex) : normalized;
350
+ const role = getModelRoleAlias(aliasCandidate);
351
+ if (!role) return normalized;
352
+
353
+ const configured = settings?.getModelRole(role)?.trim();
354
+ if (!configured) return undefined;
355
+ return thinkingLevel ? `${configured}:${thinkingLevel}` : configured;
331
356
  }
332
357
 
333
358
  /**
@@ -335,11 +360,48 @@ export function isDefaultModelAlias(value: string | string[] | undefined): boole
335
360
  */
336
361
  export function expandRoleAlias(value: string, settings?: Settings): string {
337
362
  const normalized = value.trim();
338
- if (normalized === "default") return settings?.getModelRole("default") ?? value;
339
- if (!normalized.startsWith(PREFIX_MODEL_ROLE)) return value;
340
- const role = normalized.slice(PREFIX_MODEL_ROLE.length) as ModelRole;
341
- if (!MODEL_ROLE_IDS.includes(role)) return value;
342
- return settings?.getModelRole(role) ?? value;
363
+ if (normalized === DEFAULT_MODEL_ROLE) {
364
+ return settings?.getModelRole("default") ?? value;
365
+ }
366
+
367
+ const resolved = resolveConfiguredRolePattern(value, settings);
368
+ return resolved ?? value;
369
+ }
370
+
371
+ export function resolveConfiguredModelPatterns(value: string | string[] | undefined, settings?: Settings): string[] {
372
+ const patterns = normalizeModelPatternList(value);
373
+ return patterns.flatMap(pattern => {
374
+ const resolved = resolveConfiguredRolePattern(pattern, settings);
375
+ return resolved ? [resolved] : [];
376
+ });
377
+ }
378
+
379
+ export interface AgentModelPatternResolutionOptions {
380
+ settingsOverride?: string | string[];
381
+ agentModel?: string | string[];
382
+ settings?: Settings;
383
+ activeModelPattern?: string;
384
+ fallbackModelPattern?: string;
385
+ }
386
+
387
+ export function resolveAgentModelPatterns(options: AgentModelPatternResolutionOptions): string[] {
388
+ const { settingsOverride, agentModel, settings, activeModelPattern, fallbackModelPattern } = options;
389
+
390
+ const overridePatterns = resolveConfiguredModelPatterns(settingsOverride, settings);
391
+ if (overridePatterns.length > 0) return overridePatterns;
392
+
393
+ const normalizedAgentPatterns = normalizeModelPatternList(agentModel);
394
+ const configuredAgentPatterns = resolveConfiguredModelPatterns(agentModel, settings);
395
+ const singleAgentPattern = normalizedAgentPatterns.length === 1 ? normalizedAgentPatterns[0] : undefined;
396
+ const agentInheritsSessionModel = singleAgentPattern ? isSessionInheritedAgentPattern(singleAgentPattern) : false;
397
+ if (configuredAgentPatterns.length > 0) {
398
+ if (!agentInheritsSessionModel) return configuredAgentPatterns;
399
+ if (singleAgentPattern === "pi/task") return configuredAgentPatterns;
400
+ }
401
+
402
+ const fallback =
403
+ activeModelPattern?.trim() || fallbackModelPattern?.trim() || settings?.getModelRole("default")?.trim() || "";
404
+ return resolveConfiguredModelPatterns(fallback, settings);
343
405
  }
344
406
 
345
407
  /**
@@ -367,13 +429,14 @@ export function resolveModelRoleValue(
367
429
  }
368
430
 
369
431
  const lastColonIndex = normalized.lastIndexOf(":");
370
- const hasThinkingSuffix =
371
- lastColonIndex > PREFIX_MODEL_ROLE.length && parseThinkingLevel(normalized.slice(lastColonIndex + 1));
372
- const aliasCandidate = hasThinkingSuffix ? normalized.slice(0, lastColonIndex) : normalized;
373
- const effectivePattern = expandRoleAlias(aliasCandidate, options?.settings);
374
- const patternWithSuffix = hasThinkingSuffix
375
- ? `${effectivePattern}:${normalized.slice(lastColonIndex + 1)}`
376
- : effectivePattern;
432
+ const thinkingSelector =
433
+ lastColonIndex > PREFIX_MODEL_ROLE.length ? parseThinkingLevel(normalized.slice(lastColonIndex + 1)) : undefined;
434
+ const aliasCandidate = thinkingSelector ? normalized.slice(0, lastColonIndex) : normalized;
435
+ const effectivePattern = resolveConfiguredRolePattern(aliasCandidate, options?.settings);
436
+ if (!effectivePattern) {
437
+ return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning: undefined };
438
+ }
439
+ const patternWithSuffix = thinkingSelector ? `${effectivePattern}:${thinkingSelector}` : effectivePattern;
377
440
  const { model, thinkingLevel, warning, explicitThinkingLevel } = parseModelPattern(
378
441
  patternWithSuffix,
379
442
  availableModels,