@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.
- package/CHANGELOG.md +86 -0
- package/package.json +7 -7
- package/src/capability/rule.ts +4 -0
- package/src/cli/commands/init-xdg.ts +27 -0
- package/src/cli/config-cli.ts +8 -3
- package/src/cli/shell-cli.ts +1 -1
- package/src/commands/config.ts +1 -1
- package/src/config/model-registry.ts +160 -26
- package/src/config/model-resolver.ts +84 -21
- package/src/config/settings-schema.ts +812 -647
- package/src/discovery/helpers.ts +11 -2
- package/src/exa/index.ts +1 -11
- package/src/exa/search.ts +1 -122
- package/src/exec/bash-executor.ts +62 -25
- package/src/extensibility/custom-tools/types.ts +2 -3
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/extensibility/hooks/types.ts +2 -0
- package/src/index.ts +6 -6
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/lsp/config.ts +1 -0
- package/src/lsp/defaults.json +3 -3
- package/src/memories/index.ts +20 -7
- package/src/memories/storage.ts +46 -32
- package/src/modes/components/agent-dashboard.ts +23 -35
- package/src/modes/components/assistant-message.ts +25 -2
- package/src/modes/components/btw-panel.ts +104 -0
- package/src/modes/components/settings-defs.ts +5 -1
- package/src/modes/components/settings-selector.ts +6 -6
- package/src/modes/controllers/btw-controller.ts +193 -0
- package/src/modes/controllers/command-controller.ts +3 -1
- package/src/modes/controllers/event-controller.ts +4 -0
- package/src/modes/controllers/extension-ui-controller.ts +6 -0
- package/src/modes/controllers/input-controller.ts +10 -1
- package/src/modes/controllers/selector-controller.ts +18 -17
- package/src/modes/interactive-mode.ts +22 -0
- package/src/modes/prompt-action-autocomplete.ts +17 -3
- package/src/modes/rpc/rpc-client.ts +30 -19
- package/src/modes/theme/theme.ts +28 -36
- package/src/modes/types.ts +4 -0
- package/src/modes/utils/ui-helpers.ts +3 -0
- package/src/patch/hashline.ts +120 -16
- package/src/prompts/system/btw-user.md +8 -0
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/code-search.md +45 -0
- package/src/prompts/tools/hashline.md +3 -0
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +36 -40
- package/src/session/agent-session.ts +65 -37
- package/src/session/blob-store.ts +32 -0
- package/src/session/compaction/compaction.ts +27 -6
- package/src/session/history-storage.ts +2 -2
- package/src/session/session-manager.ts +116 -44
- package/src/session/streaming-output.ts +17 -54
- package/src/slash-commands/builtin-registry.ts +11 -0
- package/src/system-prompt.ts +4 -17
- package/src/task/agents.ts +1 -1
- package/src/task/executor.ts +1 -1
- package/src/task/index.ts +9 -8
- package/src/tools/browser.ts +11 -0
- package/src/tools/exit-plan-mode.ts +6 -0
- package/src/tools/fetch.ts +1 -1
- package/src/tools/output-meta.ts +104 -9
- package/src/tools/read.ts +13 -26
- package/src/utils/title-generator.ts +70 -92
- package/src/utils/tools-manager.ts +1 -1
- package/src/web/scrapers/index.ts +7 -7
- package/src/web/scrapers/utils.ts +1 -0
- package/src/web/search/code-search.ts +385 -0
- package/src/web/search/index.ts +25 -280
- package/src/web/search/provider.ts +1 -1
- package/src/web/search/types.ts +28 -0
- package/src/exa/company.ts +0 -26
- 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.
|
|
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.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "13.
|
|
46
|
-
"@oh-my-pi/pi-ai": "13.
|
|
47
|
-
"@oh-my-pi/pi-natives": "13.
|
|
48
|
-
"@oh-my-pi/pi-tui": "13.
|
|
49
|
-
"@oh-my-pi/pi-utils": "13.
|
|
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",
|
package/src/capability/rule.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/cli/config-cli.ts
CHANGED
|
@@ -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
|
package/src/cli/shell-cli.ts
CHANGED
package/src/commands/config.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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(
|
|
887
|
-
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:
|
|
949
|
+
fetchedAt: discoveryError ? cached?.updatedAt : Date.now(),
|
|
892
950
|
models: result.models.map(model => model.id),
|
|
893
|
-
error:
|
|
951
|
+
error: discoveryError,
|
|
894
952
|
});
|
|
895
|
-
if (
|
|
896
|
-
this.#warnProviderDiscoveryFailure(providerConfig,
|
|
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;
|