@oh-my-pi/pi-coding-agent 13.11.0 → 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 (74) hide show
  1. package/CHANGELOG.md +86 -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 +160 -26
  9. package/src/config/model-resolver.ts +84 -21
  10. package/src/config/settings-schema.ts +812 -647
  11. package/src/discovery/helpers.ts +11 -2
  12. package/src/exa/index.ts +1 -11
  13. package/src/exa/search.ts +1 -122
  14. package/src/exec/bash-executor.ts +62 -25
  15. package/src/extensibility/custom-tools/types.ts +2 -3
  16. package/src/extensibility/extensions/types.ts +2 -0
  17. package/src/extensibility/hooks/types.ts +2 -0
  18. package/src/index.ts +6 -6
  19. package/src/internal-urls/docs-index.generated.ts +3 -3
  20. package/src/lsp/config.ts +1 -0
  21. package/src/lsp/defaults.json +3 -3
  22. package/src/memories/index.ts +20 -7
  23. package/src/memories/storage.ts +46 -32
  24. package/src/modes/components/agent-dashboard.ts +23 -35
  25. package/src/modes/components/assistant-message.ts +25 -2
  26. package/src/modes/components/btw-panel.ts +104 -0
  27. package/src/modes/components/settings-defs.ts +5 -1
  28. package/src/modes/components/settings-selector.ts +6 -6
  29. package/src/modes/controllers/btw-controller.ts +193 -0
  30. package/src/modes/controllers/command-controller.ts +3 -1
  31. package/src/modes/controllers/event-controller.ts +4 -0
  32. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  33. package/src/modes/controllers/input-controller.ts +10 -1
  34. package/src/modes/controllers/selector-controller.ts +18 -17
  35. package/src/modes/interactive-mode.ts +22 -0
  36. package/src/modes/prompt-action-autocomplete.ts +17 -3
  37. package/src/modes/rpc/rpc-client.ts +30 -19
  38. package/src/modes/theme/theme.ts +28 -36
  39. package/src/modes/types.ts +4 -0
  40. package/src/modes/utils/ui-helpers.ts +3 -0
  41. package/src/patch/hashline.ts +120 -16
  42. package/src/prompts/system/btw-user.md +8 -0
  43. package/src/prompts/system/custom-system-prompt.md +1 -1
  44. package/src/prompts/system/system-prompt.md +1 -0
  45. package/src/prompts/tools/code-search.md +45 -0
  46. package/src/prompts/tools/hashline.md +3 -0
  47. package/src/prompts/tools/read.md +2 -2
  48. package/src/sdk.ts +36 -40
  49. package/src/session/agent-session.ts +65 -37
  50. package/src/session/blob-store.ts +32 -0
  51. package/src/session/compaction/compaction.ts +27 -6
  52. package/src/session/history-storage.ts +2 -2
  53. package/src/session/session-manager.ts +116 -44
  54. package/src/session/streaming-output.ts +17 -54
  55. package/src/slash-commands/builtin-registry.ts +11 -0
  56. package/src/system-prompt.ts +4 -17
  57. package/src/task/agents.ts +1 -1
  58. package/src/task/executor.ts +1 -1
  59. package/src/task/index.ts +9 -8
  60. package/src/tools/browser.ts +11 -0
  61. package/src/tools/exit-plan-mode.ts +6 -0
  62. package/src/tools/fetch.ts +1 -1
  63. package/src/tools/output-meta.ts +104 -9
  64. package/src/tools/read.ts +13 -26
  65. package/src/utils/title-generator.ts +70 -92
  66. package/src/utils/tools-manager.ts +1 -1
  67. package/src/web/scrapers/index.ts +7 -7
  68. package/src/web/scrapers/utils.ts +1 -0
  69. package/src/web/search/code-search.ts +385 -0
  70. package/src/web/search/index.ts +25 -280
  71. package/src/web/search/provider.ts +1 -1
  72. package/src/web/search/types.ts +28 -0
  73. package/src/exa/company.ts +0 -26
  74. package/src/exa/linkedin.ts +0 -26
package/CHANGELOG.md CHANGED
@@ -2,6 +2,91 @@
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
+
62
+ ## [13.11.1] - 2026-03-13
63
+
64
+ ### Added
65
+
66
+ - Added `llama.cpp` as local provider
67
+ - Added `code_search` tool supporting both Exa and grep.app providers for code snippet and documentation search
68
+ - Added `providers.codeSearch` setting to configure code search provider (exa or grep)
69
+ - Added grep.app integration for public code search with result ranking by context relevance
70
+
71
+ ### Changed
72
+
73
+ - Updated compact diff preview to include line hashes for visibility and integrity verification of unchanged and added lines
74
+ - Modified compact diff preview to track line number synchronization between old and new files when processing insertions and deletions
75
+ - Simplified web search tools: removed `web_search_deep`, `web_search_crawl`, `web_search_linkedin`, and `web_search_company` tools
76
+ - Removed `exa.enableLinkedin` and `exa.enableCompany` settings; LinkedIn and company research are no longer available
77
+ - Refactored code search to use pluggable provider system instead of Exa-only implementation
78
+
79
+ ### Removed
80
+
81
+ - Removed Exa LinkedIn search tool (`exa_linkedin`)
82
+ - Removed Exa company research tool (`exa_company`)
83
+ - Removed Exa deep search tool (`exa_search_deep`)
84
+ - Removed Exa URL crawl tool (`exa_crawl`)
85
+
86
+ ### Fixed
87
+
88
+ - Fixed line number parsing in compact diff preview to handle variable-width line number fields with leading whitespace
89
+
5
90
  ## [13.11.0] - 2026-03-12
6
91
  ### Added
7
92
 
@@ -18,6 +103,7 @@
18
103
  - Added `buildNamedToolChoice` utility function to build provider-aware tool choice constraints for named tools
19
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/`)
20
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
21
107
 
22
108
  ### Changed
23
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.0",
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.0",
45
- "@oh-my-pi/pi-agent-core": "13.11.0",
46
- "@oh-my-pi/pi-ai": "13.11.0",
47
- "@oh-my-pi/pi-natives": "13.11.0",
48
- "@oh-my-pi/pi-tui": "13.11.0",
49
- "@oh-my-pi/pi-utils": "13.11.0",
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([
@@ -160,7 +184,7 @@ const ModelOverrideSchema = Type.Object({
160
184
  type ModelOverride = Static<typeof ModelOverrideSchema>;
161
185
 
162
186
  const ProviderDiscoverySchema = Type.Object({
163
- type: Type.Union([Type.Literal("ollama"), Type.Literal("lm-studio")]),
187
+ type: Type.Union([Type.Literal("ollama"), Type.Literal("llama.cpp"), Type.Literal("lm-studio")]),
164
188
  });
165
189
 
166
190
  const ProviderAuthSchema = Type.Union([Type.Literal("apiKey"), Type.Literal("none")]);
@@ -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({
@@ -694,6 +739,19 @@ export class ModelRegistry {
694
739
  });
695
740
  this.#keylessProviders.add("ollama");
696
741
  }
742
+ if (!configuredProviders.has("llama.cpp")) {
743
+ this.#discoverableProviders.push({
744
+ provider: "llama.cpp",
745
+ api: "openai-responses",
746
+ baseUrl: Bun.env.LLAMA_CPP_BASE_URL || "http://127.0.0.1:8080",
747
+ discovery: { type: "llama.cpp" },
748
+ optional: true,
749
+ });
750
+ // Only mark as keyless if no API key is configured
751
+ if (!this.authStorage.hasAuth("llama.cpp")) {
752
+ this.#keylessProviders.add("llama.cpp");
753
+ }
754
+ }
697
755
  if (!configuredProviders.has("lm-studio")) {
698
756
  this.#discoverableProviders.push({
699
757
  provider: "lm-studio",
@@ -739,12 +797,13 @@ export class ModelRegistry {
739
797
  const configuredProviders = new Set(Object.keys(value.providers));
740
798
 
741
799
  for (const [providerName, providerConfig] of Object.entries(value.providers)) {
742
- // Always set overrides when baseUrl/headers present
743
- 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) {
744
802
  overrides.set(providerName, {
745
803
  baseUrl: providerConfig.baseUrl,
746
804
  headers: providerConfig.headers,
747
805
  apiKey: providerConfig.apiKey,
806
+ compat: providerConfig.compat,
748
807
  });
749
808
  }
750
809
 
@@ -759,6 +818,7 @@ export class ModelRegistry {
759
818
  api: providerConfig.api as Api,
760
819
  baseUrl: providerConfig.baseUrl,
761
820
  headers: providerConfig.headers,
821
+ compat: providerConfig.compat,
762
822
  discovery: providerConfig.discovery,
763
823
  optional: false,
764
824
  });
@@ -851,30 +911,28 @@ export class ModelRegistry {
851
911
  }
852
912
  }
853
913
 
854
- let fetchError: string | undefined;
914
+ const providerId = providerConfig.provider;
915
+ let discoveryError: string | undefined;
855
916
  const fetchDynamicModels = async (): Promise<readonly Model<Api>[] | null> => {
856
917
  try {
857
- const models =
858
- providerConfig.discovery.type === "ollama"
859
- ? await this.#discoverOllamaModels(providerConfig)
860
- : await this.#discoverLmStudioModels(providerConfig);
861
- this.#lastDiscoveryWarnings.delete(providerConfig.provider);
918
+ const models = await this.#discoverModelsByProviderType(providerConfig);
919
+ this.#lastDiscoveryWarnings.delete(providerId);
862
920
  return models;
863
921
  } catch (error) {
864
- fetchError = error instanceof Error ? error.message : String(error);
922
+ discoveryError = error instanceof Error ? error.message : String(error);
865
923
  return null;
866
924
  }
867
925
  };
868
926
 
869
927
  const manager = createModelManager<Api>({
870
- providerId: providerConfig.provider,
928
+ providerId,
871
929
  staticModels: [],
872
930
  cacheDbPath: this.#cacheDbPath,
873
931
  cacheTtlMs: 24 * 60 * 60 * 1000,
874
932
  fetchDynamicModels,
875
933
  });
876
934
  const result = await manager.refresh(strategy);
877
- const status = fetchError
935
+ const status = discoveryError
878
936
  ? result.models.length > 0
879
937
  ? "cached"
880
938
  : "unavailable"
@@ -883,19 +941,33 @@ export class ModelRegistry {
883
941
  : cached
884
942
  ? "cached"
885
943
  : "idle";
886
- this.#providerDiscoveryStates.set(providerConfig.provider, {
887
- provider: providerConfig.provider,
944
+ this.#providerDiscoveryStates.set(providerId, {
945
+ provider: providerId,
888
946
  status,
889
947
  optional: providerConfig.optional ?? false,
890
948
  stale: result.stale || status === "cached",
891
- fetchedAt: fetchError ? cached?.updatedAt : Date.now(),
949
+ fetchedAt: discoveryError ? cached?.updatedAt : Date.now(),
892
950
  models: result.models.map(model => model.id),
893
- error: fetchError,
951
+ error: discoveryError,
894
952
  });
895
- if (fetchError) {
896
- this.#warnProviderDiscoveryFailure(providerConfig, fetchError);
953
+ if (discoveryError) {
954
+ this.#warnProviderDiscoveryFailure(providerConfig, discoveryError);
955
+ }
956
+ return this.#applyProviderModelOverrides(
957
+ providerId,
958
+ this.#applyProviderCompat(providerConfig.compat, result.models),
959
+ );
960
+ }
961
+
962
+ #discoverModelsByProviderType(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
963
+ switch (providerConfig.discovery.type) {
964
+ case "ollama":
965
+ return this.#discoverOllamaModels(providerConfig);
966
+ case "llama.cpp":
967
+ return this.#discoverLlamaCppModels(providerConfig);
968
+ case "lm-studio":
969
+ return this.#discoverLmStudioModels(providerConfig);
897
970
  }
898
- return this.#applyProviderModelOverrides(providerConfig.provider, result.models);
899
971
  }
900
972
 
901
973
  #warnProviderDiscoveryFailure(providerConfig: DiscoveryProviderConfig, error: string): void {
@@ -1106,6 +1178,53 @@ export class ModelRegistry {
1106
1178
  return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
1107
1179
  }
1108
1180
 
1181
+ async #discoverLlamaCppModels(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
1182
+ const baseUrl = this.#normalizeLlamaCppBaseUrl(providerConfig.baseUrl);
1183
+ const modelsUrl = `${baseUrl}/models`;
1184
+
1185
+ const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
1186
+ const apiKey = await this.authStorage.getApiKey(providerConfig.provider);
1187
+ if (apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth) {
1188
+ headers.Authorization = `Bearer ${apiKey}`;
1189
+ }
1190
+
1191
+ const response = await fetch(modelsUrl, {
1192
+ headers,
1193
+ signal: AbortSignal.timeout(250),
1194
+ });
1195
+ if (!response.ok) {
1196
+ throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
1197
+ }
1198
+ const payload = (await response.json()) as { data?: Array<{ id: string }> };
1199
+ const models = payload.data ?? [];
1200
+ const discovered: Model<Api>[] = [];
1201
+ for (const item of models) {
1202
+ const id = item.id;
1203
+ if (!id) continue;
1204
+ discovered.push(
1205
+ enrichModelThinking({
1206
+ id,
1207
+ name: id,
1208
+ api: providerConfig.api,
1209
+ provider: providerConfig.provider,
1210
+ baseUrl,
1211
+ reasoning: false,
1212
+ input: ["text"],
1213
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
1214
+ contextWindow: 128000,
1215
+ maxTokens: 8192,
1216
+ headers,
1217
+ compat: {
1218
+ supportsStore: false,
1219
+ supportsDeveloperRole: false,
1220
+ supportsReasoningEffort: false,
1221
+ },
1222
+ }),
1223
+ );
1224
+ }
1225
+ return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
1226
+ }
1227
+
1109
1228
  async #discoverLmStudioModels(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
1110
1229
  const baseUrl = this.#normalizeLmStudioBaseUrl(providerConfig.baseUrl);
1111
1230
  const modelsUrl = `${baseUrl}/models`;
@@ -1153,6 +1272,18 @@ export class ModelRegistry {
1153
1272
  return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
1154
1273
  }
1155
1274
 
1275
+ #normalizeLlamaCppBaseUrl(baseUrl?: string): string {
1276
+ const defaultBaseUrl = "http://127.0.0.1:8080";
1277
+ const raw = baseUrl || defaultBaseUrl;
1278
+ try {
1279
+ const parsed = new URL(raw);
1280
+ const trimmedPath = parsed.pathname.replace(/\/+$/g, "");
1281
+ return `${parsed.protocol}//${parsed.host}${trimmedPath}`;
1282
+ } catch {
1283
+ return raw;
1284
+ }
1285
+ }
1286
+
1156
1287
  #normalizeLmStudioBaseUrl(baseUrl?: string): string {
1157
1288
  const defaultBaseUrl = "http://127.0.0.1:1234/v1";
1158
1289
  const raw = baseUrl || defaultBaseUrl;
@@ -1221,6 +1352,7 @@ export class ModelRegistry {
1221
1352
  providerConfig.headers,
1222
1353
  providerConfig.apiKey,
1223
1354
  providerConfig.authHeader,
1355
+ providerConfig.compat,
1224
1356
  modelDef as CustomModelDefinitionLike,
1225
1357
  { useDefaults: true },
1226
1358
  );
@@ -1382,6 +1514,7 @@ export class ModelRegistry {
1382
1514
  config.headers,
1383
1515
  config.apiKey,
1384
1516
  config.authHeader,
1517
+ config.compat,
1385
1518
  modelDef as CustomModelDefinitionLike,
1386
1519
  { useDefaults: false },
1387
1520
  );
@@ -1425,6 +1558,7 @@ export interface ProviderConfigInput {
1425
1558
  api?: Api;
1426
1559
  streamSimple?: (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream;
1427
1560
  headers?: Record<string, string>;
1561
+ compat?: Model<Api>["compat"];
1428
1562
  authHeader?: boolean;
1429
1563
  oauth?: {
1430
1564
  name: string;