@mariozechner/pi-coding-agent 0.49.3 → 0.50.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 +99 -1
- package/README.md +310 -1230
- package/dist/cli/args.d.ts +5 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +57 -23
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/config-selector.d.ts +14 -0
- package/dist/cli/config-selector.d.ts.map +1 -0
- package/dist/cli/config-selector.js +31 -0
- package/dist/cli/config-selector.js.map +1 -0
- package/dist/cli/session-picker.d.ts.map +1 -1
- package/dist/cli/session-picker.js +1 -1
- package/dist/cli/session-picker.js.map +1 -1
- package/dist/core/agent-session.d.ts +53 -34
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +262 -67
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts +8 -18
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +39 -55
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +2 -1
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/diagnostics.d.ts +15 -0
- package/dist/core/diagnostics.d.ts.map +1 -0
- package/dist/core/diagnostics.js +2 -0
- package/dist/core/diagnostics.js.map +1 -0
- package/dist/core/export-html/template.css +9 -0
- package/dist/core/export-html/template.js +6 -4
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +10 -1
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +9 -3
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +39 -12
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +112 -1
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts +9 -2
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +13 -0
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/model-registry.d.ts +42 -2
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +154 -44
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +3 -2
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/package-manager.d.ts +129 -0
- package/dist/core/package-manager.d.ts.map +1 -0
- package/dist/core/package-manager.js +1148 -0
- package/dist/core/package-manager.js.map +1 -0
- package/dist/core/prompt-templates.d.ts +6 -0
- package/dist/core/prompt-templates.d.ts.map +1 -1
- package/dist/core/prompt-templates.js +114 -54
- package/dist/core/prompt-templates.js.map +1 -1
- package/dist/core/resource-loader.d.ts +160 -0
- package/dist/core/resource-loader.d.ts.map +1 -0
- package/dist/core/resource-loader.js +604 -0
- package/dist/core/resource-loader.js.map +1 -0
- package/dist/core/sdk.d.ts +14 -105
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +52 -304
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +45 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +34 -16
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +104 -25
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts +18 -10
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +126 -93
- package/dist/core/skills.js.map +1 -1
- package/dist/core/system-prompt.d.ts +3 -27
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +16 -103
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +2 -1
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +4 -4
- package/dist/core/tools/read.js.map +1 -1
- package/dist/index.d.ts +12 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -6
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +209 -97
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/bordered-loader.d.ts +5 -1
- package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -1
- package/dist/modes/interactive/components/bordered-loader.js +29 -9
- package/dist/modes/interactive/components/bordered-loader.js.map +1 -1
- package/dist/modes/interactive/components/config-selector.d.ts +71 -0
- package/dist/modes/interactive/components/config-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/config-selector.js +468 -0
- package/dist/modes/interactive/components/config-selector.js.map +1 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +4 -0
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +1 -0
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +1 -0
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.js +3 -4
- package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts +18 -1
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +195 -87
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/skill-invocation-message.d.ts +17 -0
- package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -0
- package/dist/modes/interactive/components/skill-invocation-message.js +47 -0
- package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +5 -5
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +42 -2
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +535 -200
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/dark.json +1 -1
- package/dist/modes/interactive/theme/light.json +1 -1
- package/dist/modes/interactive/theme/theme-schema.json +8 -1
- package/dist/modes/interactive/theme/theme.d.ts +8 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +72 -25
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +25 -89
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +32 -92
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/utils/git.d.ts +2 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +6 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/shell.d.ts +1 -0
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +14 -1
- package/dist/utils/shell.js.map +1 -1
- package/dist/utils/sleep.d.ts +5 -0
- package/dist/utils/sleep.d.ts.map +1 -0
- package/dist/utils/sleep.js +17 -0
- package/dist/utils/sleep.js.map +1 -0
- package/docs/compaction.md +23 -21
- package/docs/custom-provider.md +538 -0
- package/docs/development.md +69 -0
- package/docs/extensions.md +180 -118
- package/docs/images/doom-extension.png +0 -0
- package/docs/images/interactive-mode.png +0 -0
- package/docs/images/tree-view.png +0 -0
- package/docs/json.md +79 -0
- package/docs/keybindings.md +162 -0
- package/docs/models.md +193 -0
- package/docs/packages.md +163 -0
- package/docs/prompt-templates.md +67 -0
- package/docs/providers.md +147 -0
- package/docs/sdk.md +111 -178
- package/docs/session.md +167 -16
- package/docs/settings.md +216 -0
- package/docs/shell-aliases.md +13 -0
- package/docs/skills.md +111 -202
- package/docs/terminal-setup.md +65 -0
- package/docs/themes.md +295 -0
- package/docs/tui.md +36 -5
- package/docs/windows.md +17 -0
- package/examples/README.md +1 -0
- package/examples/extensions/README.md +22 -2
- package/examples/extensions/bookmark.ts +50 -0
- package/examples/extensions/custom-provider-anthropic/index.ts +604 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +24 -0
- package/examples/extensions/custom-provider-anthropic/package.json +19 -0
- package/examples/extensions/custom-provider-gitlab-duo/index.ts +349 -0
- package/examples/extensions/custom-provider-gitlab-duo/package.json +16 -0
- package/examples/extensions/custom-provider-gitlab-duo/test.ts +82 -0
- package/examples/extensions/doom-overlay/doom/build.sh +1 -1
- package/examples/extensions/event-bus.ts +43 -0
- package/examples/extensions/message-renderer.ts +59 -0
- package/examples/extensions/session-name.ts +27 -0
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/sdk/02-custom-model.ts +3 -3
- package/examples/sdk/03-custom-prompt.ts +20 -9
- package/examples/sdk/04-skills.ts +26 -27
- package/examples/sdk/06-extensions.ts +15 -6
- package/examples/sdk/07-context-files.ts +22 -18
- package/examples/sdk/08-prompt-templates.ts +19 -14
- package/examples/sdk/09-api-keys-and-oauth.ts +5 -12
- package/examples/sdk/10-settings.ts +3 -3
- package/examples/sdk/12-full-control.ts +16 -7
- package/examples/sdk/README.md +24 -30
- package/package.json +4 -4
- package/docs/theme.md +0 -617
- package/examples/extensions/chalk-logger.ts +0 -26
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"footer-data-provider.js","sourceRoot":"","sources":["../../src/core/footer-data-provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAkB,YAAY,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,IAAI,CAAC;AAC/E,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAE9C;;;GAGG;AACH,SAAS,eAAe,GAAkB;IACzC,IAAI,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IACxB,OAAO,IAAI,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAClC,IAAI,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YACzB,IAAI,CAAC;gBACJ,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;gBAC/B,IAAI,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;oBACnB,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;oBACrD,IAAI,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;wBACpC,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;wBAChC,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;wBAC9C,IAAI,UAAU,CAAC,QAAQ,CAAC;4BAAE,OAAO,QAAQ,CAAC;oBAC3C,CAAC;gBACF,CAAC;qBAAM,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;oBAC/B,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;oBACvC,IAAI,UAAU,CAAC,QAAQ,CAAC;wBAAE,OAAO,QAAQ,CAAC;gBAC3C,CAAC;YACF,CAAC;YAAC,MAAM,CAAC;gBACR,OAAO,IAAI,CAAC;YACb,CAAC;QACF,CAAC;QACD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QAC5B,IAAI,MAAM,KAAK,GAAG;YAAE,OAAO,IAAI,CAAC;QAChC,GAAG,GAAG,MAAM,CAAC;IACd,CAAC;AAAA,CACD;AAED;;;GAGG;AACH,MAAM,OAAO,kBAAkB;IACtB,iBAAiB,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC9C,YAAY,GAA8B,SAAS,CAAC;IACpD,UAAU,GAAqB,IAAI,CAAC;IACpC,qBAAqB,GAAG,IAAI,GAAG,EAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"footer-data-provider.js","sourceRoot":"","sources":["../../src/core/footer-data-provider.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, type FSWatcher, readFileSync, statSync, watch } from \"fs\";\nimport { dirname, join, resolve } from \"path\";\n\n/**\n * Find the git HEAD path by walking up from cwd.\n * Handles both regular git repos (.git is a directory) and worktrees (.git is a file).\n */\nfunction findGitHeadPath(): string | null {\n\tlet dir = process.cwd();\n\twhile (true) {\n\t\tconst gitPath = join(dir, \".git\");\n\t\tif (existsSync(gitPath)) {\n\t\t\ttry {\n\t\t\t\tconst stat = statSync(gitPath);\n\t\t\t\tif (stat.isFile()) {\n\t\t\t\t\tconst content = readFileSync(gitPath, \"utf8\").trim();\n\t\t\t\t\tif (content.startsWith(\"gitdir: \")) {\n\t\t\t\t\t\tconst gitDir = content.slice(8);\n\t\t\t\t\t\tconst headPath = resolve(dir, gitDir, \"HEAD\");\n\t\t\t\t\t\tif (existsSync(headPath)) return headPath;\n\t\t\t\t\t}\n\t\t\t\t} else if (stat.isDirectory()) {\n\t\t\t\t\tconst headPath = join(gitPath, \"HEAD\");\n\t\t\t\t\tif (existsSync(headPath)) return headPath;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}\n\t\tconst parent = dirname(dir);\n\t\tif (parent === dir) return null;\n\t\tdir = parent;\n\t}\n}\n\n/**\n * Provides git branch and extension statuses - data not otherwise accessible to extensions.\n * Token stats, model info available via ctx.sessionManager and ctx.model.\n */\nexport class FooterDataProvider {\n\tprivate extensionStatuses = new Map<string, string>();\n\tprivate cachedBranch: string | null | undefined = undefined;\n\tprivate gitWatcher: FSWatcher | null = null;\n\tprivate branchChangeCallbacks = new Set<() => void>();\n\tprivate availableProviderCount = 0;\n\n\tconstructor() {\n\t\tthis.setupGitWatcher();\n\t}\n\n\t/** Current git branch, null if not in repo, \"detached\" if detached HEAD */\n\tgetGitBranch(): string | null {\n\t\tif (this.cachedBranch !== undefined) return this.cachedBranch;\n\n\t\ttry {\n\t\t\tconst gitHeadPath = findGitHeadPath();\n\t\t\tif (!gitHeadPath) {\n\t\t\t\tthis.cachedBranch = null;\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst content = readFileSync(gitHeadPath, \"utf8\").trim();\n\t\t\tthis.cachedBranch = content.startsWith(\"ref: refs/heads/\") ? content.slice(16) : \"detached\";\n\t\t} catch {\n\t\t\tthis.cachedBranch = null;\n\t\t}\n\t\treturn this.cachedBranch;\n\t}\n\n\t/** Extension status texts set via ctx.ui.setStatus() */\n\tgetExtensionStatuses(): ReadonlyMap<string, string> {\n\t\treturn this.extensionStatuses;\n\t}\n\n\t/** Subscribe to git branch changes. Returns unsubscribe function. */\n\tonBranchChange(callback: () => void): () => void {\n\t\tthis.branchChangeCallbacks.add(callback);\n\t\treturn () => this.branchChangeCallbacks.delete(callback);\n\t}\n\n\t/** Internal: set extension status */\n\tsetExtensionStatus(key: string, text: string | undefined): void {\n\t\tif (text === undefined) {\n\t\t\tthis.extensionStatuses.delete(key);\n\t\t} else {\n\t\t\tthis.extensionStatuses.set(key, text);\n\t\t}\n\t}\n\n\t/** Internal: clear extension statuses */\n\tclearExtensionStatuses(): void {\n\t\tthis.extensionStatuses.clear();\n\t}\n\n\t/** Number of unique providers with available models (for footer display) */\n\tgetAvailableProviderCount(): number {\n\t\treturn this.availableProviderCount;\n\t}\n\n\t/** Internal: update available provider count */\n\tsetAvailableProviderCount(count: number): void {\n\t\tthis.availableProviderCount = count;\n\t}\n\n\t/** Internal: cleanup */\n\tdispose(): void {\n\t\tif (this.gitWatcher) {\n\t\t\tthis.gitWatcher.close();\n\t\t\tthis.gitWatcher = null;\n\t\t}\n\t\tthis.branchChangeCallbacks.clear();\n\t}\n\n\tprivate setupGitWatcher(): void {\n\t\tif (this.gitWatcher) {\n\t\t\tthis.gitWatcher.close();\n\t\t\tthis.gitWatcher = null;\n\t\t}\n\n\t\tconst gitHeadPath = findGitHeadPath();\n\t\tif (!gitHeadPath) return;\n\n\t\t// Watch the directory containing HEAD, not HEAD itself.\n\t\t// Git uses atomic writes (write temp, rename over HEAD), which changes the inode.\n\t\t// fs.watch on a file stops working after the inode changes.\n\t\tconst gitDir = dirname(gitHeadPath);\n\n\t\ttry {\n\t\t\tthis.gitWatcher = watch(gitDir, (_eventType, filename) => {\n\t\t\t\tif (filename === \"HEAD\") {\n\t\t\t\t\tthis.cachedBranch = undefined;\n\t\t\t\t\tfor (const cb of this.branchChangeCallbacks) cb();\n\t\t\t\t}\n\t\t\t});\n\t\t} catch {\n\t\t\t// Silently fail if we can't watch\n\t\t}\n\t}\n}\n\n/** Read-only view for extensions - excludes setExtensionStatus, setAvailableProviderCount and dispose */\nexport type ReadonlyFooterDataProvider = Pick<\n\tFooterDataProvider,\n\t\"getGitBranch\" | \"getExtensionStatuses\" | \"getAvailableProviderCount\" | \"onBranchChange\"\n>;\n"]}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Model registry - manages built-in and custom models, provides API key resolution.
|
|
3
3
|
*/
|
|
4
|
-
import { type Api, type Model } from "@mariozechner/pi-ai";
|
|
4
|
+
import { type Api, type AssistantMessageEventStream, type Context, type Model, type OAuthProviderInterface, type SimpleStreamOptions } from "@mariozechner/pi-ai";
|
|
5
5
|
import type { AuthStorage } from "./auth-storage.js";
|
|
6
|
-
/** Clear the
|
|
6
|
+
/** Clear the config value command cache. Exported for testing. */
|
|
7
7
|
export declare function clearApiKeyCache(): void;
|
|
8
8
|
/**
|
|
9
9
|
* Model registry - loads and manages models, resolves API keys via AuthStorage.
|
|
@@ -13,6 +13,7 @@ export declare class ModelRegistry {
|
|
|
13
13
|
private modelsJsonPath;
|
|
14
14
|
private models;
|
|
15
15
|
private customProviderApiKeys;
|
|
16
|
+
private registeredProviders;
|
|
16
17
|
private loadError;
|
|
17
18
|
constructor(authStorage: AuthStorage, modelsJsonPath?: string | undefined);
|
|
18
19
|
/**
|
|
@@ -55,5 +56,44 @@ export declare class ModelRegistry {
|
|
|
55
56
|
* Check if a model is using OAuth credentials (subscription).
|
|
56
57
|
*/
|
|
57
58
|
isUsingOAuth(model: Model<Api>): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Register a provider dynamically (from extensions).
|
|
61
|
+
*
|
|
62
|
+
* If provider has models: replaces all existing models for this provider.
|
|
63
|
+
* If provider has only baseUrl/headers: overrides existing models' URLs.
|
|
64
|
+
* If provider has oauth: registers OAuth provider for /login support.
|
|
65
|
+
*/
|
|
66
|
+
registerProvider(providerName: string, config: ProviderConfigInput): void;
|
|
67
|
+
private applyProviderConfig;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Input type for registerProvider API.
|
|
71
|
+
*/
|
|
72
|
+
export interface ProviderConfigInput {
|
|
73
|
+
baseUrl?: string;
|
|
74
|
+
apiKey?: string;
|
|
75
|
+
api?: Api;
|
|
76
|
+
streamSimple?: (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream;
|
|
77
|
+
headers?: Record<string, string>;
|
|
78
|
+
authHeader?: boolean;
|
|
79
|
+
/** OAuth provider for /login support */
|
|
80
|
+
oauth?: Omit<OAuthProviderInterface, "id">;
|
|
81
|
+
models?: Array<{
|
|
82
|
+
id: string;
|
|
83
|
+
name: string;
|
|
84
|
+
api?: Api;
|
|
85
|
+
reasoning: boolean;
|
|
86
|
+
input: ("text" | "image")[];
|
|
87
|
+
cost: {
|
|
88
|
+
input: number;
|
|
89
|
+
output: number;
|
|
90
|
+
cacheRead: number;
|
|
91
|
+
cacheWrite: number;
|
|
92
|
+
};
|
|
93
|
+
contextWindow: number;
|
|
94
|
+
maxTokens: number;
|
|
95
|
+
headers?: Record<string, string>;
|
|
96
|
+
compat?: Model<Api>["compat"];
|
|
97
|
+
}>;
|
|
58
98
|
}
|
|
59
99
|
//# sourceMappingURL=model-registry.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-registry.d.ts","sourceRoot":"","sources":["../../src/core/model-registry.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACN,KAAK,GAAG,EAKR,KAAK,KAAK,EAEV,MAAM,qBAAqB,CAAC;AAK7B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAiIrD,6DAA6D;AAC7D,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AAED;;GAEG;AACH,qBAAa,aAAa;IAMxB,QAAQ,CAAC,WAAW,EAAE,WAAW;IACjC,OAAO,CAAC,cAAc;IANvB,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,qBAAqB,CAAkC;IAC/D,OAAO,CAAC,SAAS,CAAiC;IAElD,YACU,WAAW,EAAE,WAAW,EACzB,cAAc,GAAE,MAAM,GAAG,SAAqB,EAatD;IAED;;OAEG;IACH,OAAO,IAAI,IAAI,CAId;IAED;;OAEG;IACH,QAAQ,IAAI,MAAM,GAAG,SAAS,CAE7B;IAED,OAAO,CAAC,UAAU;IA8BlB,+EAA+E;IAC/E,OAAO,CAAC,iBAAiB;IAiBzB,OAAO,CAAC,gBAAgB;IAuDxB,OAAO,CAAC,cAAc;IAyCtB,OAAO,CAAC,WAAW;IAmDnB;;;OAGG;IACH,MAAM,IAAI,KAAK,CAAC,GAAG,CAAC,EAAE,CAErB;IAED;;;OAGG;IACH,YAAY,IAAI,KAAK,CAAC,GAAG,CAAC,EAAE,CAE3B;IAED;;OAEG;IACH,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,CAE9D;IAED;;OAEG;IACG,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAE9D;IAED;;OAEG;IACG,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAExE;IAED;;OAEG;IACH,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,CAGvC;CACD","sourcesContent":["/**\n * Model registry - manages built-in and custom models, provides API key resolution.\n */\n\nimport {\n\ttype Api,\n\tgetGitHubCopilotBaseUrl,\n\tgetModels,\n\tgetProviders,\n\ttype KnownProvider,\n\ttype Model,\n\tnormalizeDomain,\n} from \"@mariozechner/pi-ai\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport AjvModule from \"ajv\";\nimport { execSync } from \"child_process\";\nimport { existsSync, readFileSync } from \"fs\";\nimport type { AuthStorage } from \"./auth-storage.js\";\n\nconst Ajv = (AjvModule as any).default || AjvModule;\n\n// Schema for OpenAI compatibility settings\nconst OpenAICompletionsCompatSchema = Type.Object({\n\tsupportsStore: Type.Optional(Type.Boolean()),\n\tsupportsDeveloperRole: Type.Optional(Type.Boolean()),\n\tsupportsReasoningEffort: Type.Optional(Type.Boolean()),\n\tmaxTokensField: Type.Optional(Type.Union([Type.Literal(\"max_completion_tokens\"), Type.Literal(\"max_tokens\")])),\n});\n\nconst OpenAIResponsesCompatSchema = Type.Object({\n\t// Reserved for future use\n});\n\nconst OpenAICompatSchema = Type.Union([OpenAICompletionsCompatSchema, OpenAIResponsesCompatSchema]);\n\n// Schema for custom model definition\nconst ModelDefinitionSchema = Type.Object({\n\tid: Type.String({ minLength: 1 }),\n\tname: Type.String({ minLength: 1 }),\n\tapi: Type.Optional(\n\t\tType.Union([\n\t\t\tType.Literal(\"openai-completions\"),\n\t\t\tType.Literal(\"openai-responses\"),\n\t\t\tType.Literal(\"openai-codex-responses\"),\n\t\t\tType.Literal(\"anthropic-messages\"),\n\t\t\tType.Literal(\"google-generative-ai\"),\n\t\t\tType.Literal(\"bedrock-converse-stream\"),\n\t\t]),\n\t),\n\treasoning: Type.Boolean(),\n\tinput: Type.Array(Type.Union([Type.Literal(\"text\"), Type.Literal(\"image\")])),\n\tcost: Type.Object({\n\t\tinput: Type.Number(),\n\t\toutput: Type.Number(),\n\t\tcacheRead: Type.Number(),\n\t\tcacheWrite: Type.Number(),\n\t}),\n\tcontextWindow: Type.Number(),\n\tmaxTokens: Type.Number(),\n\theaders: Type.Optional(Type.Record(Type.String(), Type.String())),\n\tcompat: Type.Optional(OpenAICompatSchema),\n});\n\nconst ProviderConfigSchema = Type.Object({\n\tbaseUrl: Type.Optional(Type.String({ minLength: 1 })),\n\tapiKey: Type.Optional(Type.String({ minLength: 1 })),\n\tapi: Type.Optional(\n\t\tType.Union([\n\t\t\tType.Literal(\"openai-completions\"),\n\t\t\tType.Literal(\"openai-responses\"),\n\t\t\tType.Literal(\"openai-codex-responses\"),\n\t\t\tType.Literal(\"anthropic-messages\"),\n\t\t\tType.Literal(\"google-generative-ai\"),\n\t\t\tType.Literal(\"bedrock-converse-stream\"),\n\t\t]),\n\t),\n\theaders: Type.Optional(Type.Record(Type.String(), Type.String())),\n\tauthHeader: Type.Optional(Type.Boolean()),\n\tmodels: Type.Optional(Type.Array(ModelDefinitionSchema)),\n});\n\nconst ModelsConfigSchema = Type.Object({\n\tproviders: Type.Record(Type.String(), ProviderConfigSchema),\n});\n\ntype ModelsConfig = Static<typeof ModelsConfigSchema>;\n\n/** Provider override config (baseUrl, headers, apiKey) without custom models */\ninterface ProviderOverride {\n\tbaseUrl?: string;\n\theaders?: Record<string, string>;\n\tapiKey?: string;\n}\n\n/** Result of loading custom models from models.json */\ninterface CustomModelsResult {\n\tmodels: Model<Api>[];\n\t/** Providers with custom models (full replacement) */\n\treplacedProviders: Set<string>;\n\t/** Providers with only baseUrl/headers override (no custom models) */\n\toverrides: Map<string, ProviderOverride>;\n\terror: string | undefined;\n}\n\nfunction emptyCustomModelsResult(error?: string): CustomModelsResult {\n\treturn { models: [], replacedProviders: new Set(), overrides: new Map(), error };\n}\n\n// Cache for shell command results (persists for process lifetime)\nconst commandResultCache = new Map<string, string | undefined>();\n\n/**\n * Resolve an API key config value to an actual key.\n * - If starts with \"!\", executes the rest as a shell command and uses stdout (cached)\n * - Otherwise checks environment variable first, then treats as literal (not cached)\n */\nfunction resolveApiKeyConfig(keyConfig: string): string | undefined {\n\tif (keyConfig.startsWith(\"!\")) {\n\t\treturn executeApiKeyCommand(keyConfig);\n\t}\n\tconst envValue = process.env[keyConfig];\n\treturn envValue || keyConfig;\n}\n\nfunction executeApiKeyCommand(commandConfig: string): string | undefined {\n\tif (commandResultCache.has(commandConfig)) {\n\t\treturn commandResultCache.get(commandConfig);\n\t}\n\n\tconst command = commandConfig.slice(1);\n\tlet result: string | undefined;\n\ttry {\n\t\tconst output = execSync(command, {\n\t\t\tencoding: \"utf-8\",\n\t\t\ttimeout: 10000,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"ignore\"],\n\t\t});\n\t\tresult = output.trim() || undefined;\n\t} catch {\n\t\tresult = undefined;\n\t}\n\n\tcommandResultCache.set(commandConfig, result);\n\treturn result;\n}\n\n/** Clear the API key command cache. Exported for testing. */\nexport function clearApiKeyCache(): void {\n\tcommandResultCache.clear();\n}\n\n/**\n * Model registry - loads and manages models, resolves API keys via AuthStorage.\n */\nexport class ModelRegistry {\n\tprivate models: Model<Api>[] = [];\n\tprivate customProviderApiKeys: Map<string, string> = new Map();\n\tprivate loadError: string | undefined = undefined;\n\n\tconstructor(\n\t\treadonly authStorage: AuthStorage,\n\t\tprivate modelsJsonPath: string | undefined = undefined,\n\t) {\n\t\t// Set up fallback resolver for custom provider API keys\n\t\tthis.authStorage.setFallbackResolver((provider) => {\n\t\t\tconst keyConfig = this.customProviderApiKeys.get(provider);\n\t\t\tif (keyConfig) {\n\t\t\t\treturn resolveApiKeyConfig(keyConfig);\n\t\t\t}\n\t\t\treturn undefined;\n\t\t});\n\n\t\t// Load models\n\t\tthis.loadModels();\n\t}\n\n\t/**\n\t * Reload models from disk (built-in + custom from models.json).\n\t */\n\trefresh(): void {\n\t\tthis.customProviderApiKeys.clear();\n\t\tthis.loadError = undefined;\n\t\tthis.loadModels();\n\t}\n\n\t/**\n\t * Get any error from loading models.json (undefined if no error).\n\t */\n\tgetError(): string | undefined {\n\t\treturn this.loadError;\n\t}\n\n\tprivate loadModels(): void {\n\t\t// Load custom models from models.json first (to know which providers to skip/override)\n\t\tconst {\n\t\t\tmodels: customModels,\n\t\t\treplacedProviders,\n\t\t\toverrides,\n\t\t\terror,\n\t\t} = this.modelsJsonPath ? this.loadCustomModels(this.modelsJsonPath) : emptyCustomModelsResult();\n\n\t\tif (error) {\n\t\t\tthis.loadError = error;\n\t\t\t// Keep built-in models even if custom models failed to load\n\t\t}\n\n\t\tconst builtInModels = this.loadBuiltInModels(replacedProviders, overrides);\n\t\tconst combined = [...builtInModels, ...customModels];\n\n\t\t// Update github-copilot base URL based on OAuth credentials\n\t\tconst copilotCred = this.authStorage.get(\"github-copilot\");\n\t\tif (copilotCred?.type === \"oauth\") {\n\t\t\tconst domain = copilotCred.enterpriseUrl\n\t\t\t\t? (normalizeDomain(copilotCred.enterpriseUrl) ?? undefined)\n\t\t\t\t: undefined;\n\t\t\tconst baseUrl = getGitHubCopilotBaseUrl(copilotCred.access, domain);\n\t\t\tthis.models = combined.map((m) => (m.provider === \"github-copilot\" ? { ...m, baseUrl } : m));\n\t\t} else {\n\t\t\tthis.models = combined;\n\t\t}\n\t}\n\n\t/** Load built-in models, skipping replaced providers and applying overrides */\n\tprivate loadBuiltInModels(replacedProviders: Set<string>, overrides: Map<string, ProviderOverride>): Model<Api>[] {\n\t\treturn getProviders()\n\t\t\t.filter((provider) => !replacedProviders.has(provider))\n\t\t\t.flatMap((provider) => {\n\t\t\t\tconst models = getModels(provider as KnownProvider) as Model<Api>[];\n\t\t\t\tconst override = overrides.get(provider);\n\t\t\t\tif (!override) return models;\n\n\t\t\t\t// Apply baseUrl/headers override to all models of this provider\n\t\t\t\treturn models.map((m) => ({\n\t\t\t\t\t...m,\n\t\t\t\t\tbaseUrl: override.baseUrl ?? m.baseUrl,\n\t\t\t\t\theaders: override.headers ? { ...m.headers, ...override.headers } : m.headers,\n\t\t\t\t}));\n\t\t\t});\n\t}\n\n\tprivate loadCustomModels(modelsJsonPath: string): CustomModelsResult {\n\t\tif (!existsSync(modelsJsonPath)) {\n\t\t\treturn emptyCustomModelsResult();\n\t\t}\n\n\t\ttry {\n\t\t\tconst content = readFileSync(modelsJsonPath, \"utf-8\");\n\t\t\tconst config: ModelsConfig = JSON.parse(content);\n\n\t\t\t// Validate schema\n\t\t\tconst ajv = new Ajv();\n\t\t\tconst validate = ajv.compile(ModelsConfigSchema);\n\t\t\tif (!validate(config)) {\n\t\t\t\tconst errors =\n\t\t\t\t\tvalidate.errors?.map((e: any) => ` - ${e.instancePath || \"root\"}: ${e.message}`).join(\"\\n\") ||\n\t\t\t\t\t\"Unknown schema error\";\n\t\t\t\treturn emptyCustomModelsResult(`Invalid models.json schema:\\n${errors}\\n\\nFile: ${modelsJsonPath}`);\n\t\t\t}\n\n\t\t\t// Additional validation\n\t\t\tthis.validateConfig(config);\n\n\t\t\t// Separate providers into \"full replacement\" (has models) vs \"override-only\" (no models)\n\t\t\tconst replacedProviders = new Set<string>();\n\t\t\tconst overrides = new Map<string, ProviderOverride>();\n\n\t\t\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\t\t\tif (providerConfig.models && providerConfig.models.length > 0) {\n\t\t\t\t\t// Has custom models -> full replacement\n\t\t\t\t\treplacedProviders.add(providerName);\n\t\t\t\t} else {\n\t\t\t\t\t// No models -> just override baseUrl/headers on built-in\n\t\t\t\t\toverrides.set(providerName, {\n\t\t\t\t\t\tbaseUrl: providerConfig.baseUrl,\n\t\t\t\t\t\theaders: providerConfig.headers,\n\t\t\t\t\t\tapiKey: providerConfig.apiKey,\n\t\t\t\t\t});\n\t\t\t\t\t// Store API key for fallback resolver\n\t\t\t\t\tif (providerConfig.apiKey) {\n\t\t\t\t\t\tthis.customProviderApiKeys.set(providerName, providerConfig.apiKey);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn { models: this.parseModels(config), replacedProviders, overrides, error: undefined };\n\t\t} catch (error) {\n\t\t\tif (error instanceof SyntaxError) {\n\t\t\t\treturn emptyCustomModelsResult(`Failed to parse models.json: ${error.message}\\n\\nFile: ${modelsJsonPath}`);\n\t\t\t}\n\t\t\treturn emptyCustomModelsResult(\n\t\t\t\t`Failed to load models.json: ${error instanceof Error ? error.message : error}\\n\\nFile: ${modelsJsonPath}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate validateConfig(config: ModelsConfig): void {\n\t\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\t\tconst hasProviderApi = !!providerConfig.api;\n\t\t\tconst models = providerConfig.models ?? [];\n\n\t\t\tif (models.length === 0) {\n\t\t\t\t// Override-only config: just needs baseUrl (to override built-in)\n\t\t\t\tif (!providerConfig.baseUrl) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Provider ${providerName}: must specify either \"baseUrl\" (for override) or \"models\" (for replacement).`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Full replacement: needs baseUrl and apiKey\n\t\t\t\tif (!providerConfig.baseUrl) {\n\t\t\t\t\tthrow new Error(`Provider ${providerName}: \"baseUrl\" is required when defining custom models.`);\n\t\t\t\t}\n\t\t\t\tif (!providerConfig.apiKey) {\n\t\t\t\t\tthrow new Error(`Provider ${providerName}: \"apiKey\" is required when defining custom models.`);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor (const modelDef of models) {\n\t\t\t\tconst hasModelApi = !!modelDef.api;\n\n\t\t\t\tif (!hasProviderApi && !hasModelApi) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Provider ${providerName}, model ${modelDef.id}: no \"api\" specified. Set at provider or model level.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tif (!modelDef.id) throw new Error(`Provider ${providerName}: model missing \"id\"`);\n\t\t\t\tif (!modelDef.name) throw new Error(`Provider ${providerName}: model missing \"name\"`);\n\t\t\t\tif (modelDef.contextWindow <= 0)\n\t\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`);\n\t\t\t\tif (modelDef.maxTokens <= 0)\n\t\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate parseModels(config: ModelsConfig): Model<Api>[] {\n\t\tconst models: Model<Api>[] = [];\n\n\t\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\t\tconst modelDefs = providerConfig.models ?? [];\n\t\t\tif (modelDefs.length === 0) continue; // Override-only, no custom models\n\n\t\t\t// Store API key config for fallback resolver\n\t\t\tif (providerConfig.apiKey) {\n\t\t\t\tthis.customProviderApiKeys.set(providerName, providerConfig.apiKey);\n\t\t\t}\n\n\t\t\tfor (const modelDef of modelDefs) {\n\t\t\t\tconst api = modelDef.api || providerConfig.api;\n\t\t\t\tif (!api) continue;\n\n\t\t\t\t// Merge headers: provider headers are base, model headers override\n\t\t\t\tlet headers =\n\t\t\t\t\tproviderConfig.headers || modelDef.headers\n\t\t\t\t\t\t? { ...providerConfig.headers, ...modelDef.headers }\n\t\t\t\t\t\t: undefined;\n\n\t\t\t\t// If authHeader is true, add Authorization header with resolved API key\n\t\t\t\tif (providerConfig.authHeader && providerConfig.apiKey) {\n\t\t\t\t\tconst resolvedKey = resolveApiKeyConfig(providerConfig.apiKey);\n\t\t\t\t\tif (resolvedKey) {\n\t\t\t\t\t\theaders = { ...headers, Authorization: `Bearer ${resolvedKey}` };\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// baseUrl is validated to exist for providers with models\n\t\t\t\tmodels.push({\n\t\t\t\t\tid: modelDef.id,\n\t\t\t\t\tname: modelDef.name,\n\t\t\t\t\tapi: api as Api,\n\t\t\t\t\tprovider: providerName,\n\t\t\t\t\tbaseUrl: providerConfig.baseUrl!,\n\t\t\t\t\treasoning: modelDef.reasoning,\n\t\t\t\t\tinput: modelDef.input as (\"text\" | \"image\")[],\n\t\t\t\t\tcost: modelDef.cost,\n\t\t\t\t\tcontextWindow: modelDef.contextWindow,\n\t\t\t\t\tmaxTokens: modelDef.maxTokens,\n\t\t\t\t\theaders,\n\t\t\t\t\tcompat: modelDef.compat,\n\t\t\t\t} as Model<Api>);\n\t\t\t}\n\t\t}\n\n\t\treturn models;\n\t}\n\n\t/**\n\t * Get all models (built-in + custom).\n\t * If models.json had errors, returns only built-in models.\n\t */\n\tgetAll(): Model<Api>[] {\n\t\treturn this.models;\n\t}\n\n\t/**\n\t * Get only models that have auth configured.\n\t * This is a fast check that doesn't refresh OAuth tokens.\n\t */\n\tgetAvailable(): Model<Api>[] {\n\t\treturn this.models.filter((m) => this.authStorage.hasAuth(m.provider));\n\t}\n\n\t/**\n\t * Find a model by provider and ID.\n\t */\n\tfind(provider: string, modelId: string): Model<Api> | undefined {\n\t\treturn this.models.find((m) => m.provider === provider && m.id === modelId);\n\t}\n\n\t/**\n\t * Get API key for a model.\n\t */\n\tasync getApiKey(model: Model<Api>): Promise<string | undefined> {\n\t\treturn this.authStorage.getApiKey(model.provider);\n\t}\n\n\t/**\n\t * Get API key for a provider.\n\t */\n\tasync getApiKeyForProvider(provider: string): Promise<string | undefined> {\n\t\treturn this.authStorage.getApiKey(provider);\n\t}\n\n\t/**\n\t * Check if a model is using OAuth credentials (subscription).\n\t */\n\tisUsingOAuth(model: Model<Api>): boolean {\n\t\tconst cred = this.authStorage.get(model.provider);\n\t\treturn cred?.type === \"oauth\";\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"model-registry.d.ts","sourceRoot":"","sources":["../../src/core/model-registry.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Model registry - manages built-in and custom models, provides API key resolution.\n */\n\nimport {\n\ttype Api,\n\ttype AssistantMessageEventStream,\n\ttype Context,\n\tgetModels,\n\tgetProviders,\n\ttype KnownProvider,\n\ttype Model,\n\ttype OAuthProviderInterface,\n\tregisterApiProvider,\n\tregisterOAuthProvider,\n\ttype SimpleStreamOptions,\n} from \"@mariozechner/pi-ai\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport AjvModule from \"ajv\";\nimport { execSync } from \"child_process\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../config.js\";\nimport type { AuthStorage } from \"./auth-storage.js\";\n\nconst Ajv = (AjvModule as any).default || AjvModule;\n\n// Schema for OpenRouter routing preferences\nconst OpenRouterRoutingSchema = Type.Object({\n\tonly: Type.Optional(Type.Array(Type.String())),\n\torder: Type.Optional(Type.Array(Type.String())),\n});\n\n// Schema for OpenAI compatibility settings\nconst OpenAICompletionsCompatSchema = Type.Object({\n\tsupportsStore: Type.Optional(Type.Boolean()),\n\tsupportsDeveloperRole: Type.Optional(Type.Boolean()),\n\tsupportsReasoningEffort: Type.Optional(Type.Boolean()),\n\tsupportsUsageInStreaming: Type.Optional(Type.Boolean()),\n\tmaxTokensField: Type.Optional(Type.Union([Type.Literal(\"max_completion_tokens\"), Type.Literal(\"max_tokens\")])),\n\trequiresToolResultName: Type.Optional(Type.Boolean()),\n\trequiresAssistantAfterToolResult: Type.Optional(Type.Boolean()),\n\trequiresThinkingAsText: Type.Optional(Type.Boolean()),\n\trequiresMistralToolIds: Type.Optional(Type.Boolean()),\n\tthinkingFormat: Type.Optional(Type.Union([Type.Literal(\"openai\"), Type.Literal(\"zai\")])),\n\topenRouterRouting: Type.Optional(OpenRouterRoutingSchema),\n});\n\nconst OpenAIResponsesCompatSchema = Type.Object({\n\t// Reserved for future use\n});\n\nconst OpenAICompatSchema = Type.Union([OpenAICompletionsCompatSchema, OpenAIResponsesCompatSchema]);\n\n// Schema for custom model definition\nconst ModelDefinitionSchema = Type.Object({\n\tid: Type.String({ minLength: 1 }),\n\tname: Type.String({ minLength: 1 }),\n\tapi: Type.Optional(Type.String({ minLength: 1 })),\n\treasoning: Type.Boolean(),\n\tinput: Type.Array(Type.Union([Type.Literal(\"text\"), Type.Literal(\"image\")])),\n\tcost: Type.Object({\n\t\tinput: Type.Number(),\n\t\toutput: Type.Number(),\n\t\tcacheRead: Type.Number(),\n\t\tcacheWrite: Type.Number(),\n\t}),\n\tcontextWindow: Type.Number(),\n\tmaxTokens: Type.Number(),\n\theaders: Type.Optional(Type.Record(Type.String(), Type.String())),\n\tcompat: Type.Optional(OpenAICompatSchema),\n});\n\nconst ProviderConfigSchema = Type.Object({\n\tbaseUrl: Type.Optional(Type.String({ minLength: 1 })),\n\tapiKey: Type.Optional(Type.String({ minLength: 1 })),\n\tapi: Type.Optional(Type.String({ minLength: 1 })),\n\theaders: Type.Optional(Type.Record(Type.String(), Type.String())),\n\tauthHeader: Type.Optional(Type.Boolean()),\n\tmodels: Type.Optional(Type.Array(ModelDefinitionSchema)),\n});\n\nconst ModelsConfigSchema = Type.Object({\n\tproviders: Type.Record(Type.String(), ProviderConfigSchema),\n});\n\ntype ModelsConfig = Static<typeof ModelsConfigSchema>;\n\n/** Provider override config (baseUrl, headers, apiKey) without custom models */\ninterface ProviderOverride {\n\tbaseUrl?: string;\n\theaders?: Record<string, string>;\n\tapiKey?: string;\n}\n\n/** Result of loading custom models from models.json */\ninterface CustomModelsResult {\n\tmodels: Model<Api>[];\n\t/** Providers with custom models (full replacement) */\n\treplacedProviders: Set<string>;\n\t/** Providers with only baseUrl/headers override (no custom models) */\n\toverrides: Map<string, ProviderOverride>;\n\terror: string | undefined;\n}\n\nfunction emptyCustomModelsResult(error?: string): CustomModelsResult {\n\treturn { models: [], replacedProviders: new Set(), overrides: new Map(), error };\n}\n\n// Cache for shell command results (persists for process lifetime)\nconst commandResultCache = new Map<string, string | undefined>();\n\n/**\n * Resolve a config value (API key, header value, etc.) to an actual value.\n * - If starts with \"!\", executes the rest as a shell command and uses stdout (cached)\n * - Otherwise checks environment variable first, then treats as literal (not cached)\n */\nfunction resolveConfigValue(config: string): string | undefined {\n\tif (config.startsWith(\"!\")) {\n\t\treturn executeCommand(config);\n\t}\n\tconst envValue = process.env[config];\n\treturn envValue || config;\n}\n\nfunction executeCommand(commandConfig: string): string | undefined {\n\tif (commandResultCache.has(commandConfig)) {\n\t\treturn commandResultCache.get(commandConfig);\n\t}\n\n\tconst command = commandConfig.slice(1);\n\tlet result: string | undefined;\n\ttry {\n\t\tconst output = execSync(command, {\n\t\t\tencoding: \"utf-8\",\n\t\t\ttimeout: 10000,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"ignore\"],\n\t\t});\n\t\tresult = output.trim() || undefined;\n\t} catch {\n\t\tresult = undefined;\n\t}\n\n\tcommandResultCache.set(commandConfig, result);\n\treturn result;\n}\n\n/**\n * Resolve all header values using the same resolution logic as API keys.\n */\nfunction resolveHeaders(headers: Record<string, string> | undefined): Record<string, string> | undefined {\n\tif (!headers) return undefined;\n\tconst resolved: Record<string, string> = {};\n\tfor (const [key, value] of Object.entries(headers)) {\n\t\tconst resolvedValue = resolveConfigValue(value);\n\t\tif (resolvedValue) {\n\t\t\tresolved[key] = resolvedValue;\n\t\t}\n\t}\n\treturn Object.keys(resolved).length > 0 ? resolved : undefined;\n}\n\n/** Clear the config value command cache. Exported for testing. */\nexport function clearApiKeyCache(): void {\n\tcommandResultCache.clear();\n}\n\n/**\n * Model registry - loads and manages models, resolves API keys via AuthStorage.\n */\nexport class ModelRegistry {\n\tprivate models: Model<Api>[] = [];\n\tprivate customProviderApiKeys: Map<string, string> = new Map();\n\tprivate registeredProviders: Map<string, ProviderConfigInput> = new Map();\n\tprivate loadError: string | undefined = undefined;\n\n\tconstructor(\n\t\treadonly authStorage: AuthStorage,\n\t\tprivate modelsJsonPath: string | undefined = join(getAgentDir(), \"models.json\"),\n\t) {\n\t\t// Set up fallback resolver for custom provider API keys\n\t\tthis.authStorage.setFallbackResolver((provider) => {\n\t\t\tconst keyConfig = this.customProviderApiKeys.get(provider);\n\t\t\tif (keyConfig) {\n\t\t\t\treturn resolveConfigValue(keyConfig);\n\t\t\t}\n\t\t\treturn undefined;\n\t\t});\n\n\t\t// Load models\n\t\tthis.loadModels();\n\t}\n\n\t/**\n\t * Reload models from disk (built-in + custom from models.json).\n\t */\n\trefresh(): void {\n\t\tthis.customProviderApiKeys.clear();\n\t\tthis.loadError = undefined;\n\t\tthis.loadModels();\n\n\t\tfor (const [providerName, config] of this.registeredProviders.entries()) {\n\t\t\tthis.applyProviderConfig(providerName, config);\n\t\t}\n\t}\n\n\t/**\n\t * Get any error from loading models.json (undefined if no error).\n\t */\n\tgetError(): string | undefined {\n\t\treturn this.loadError;\n\t}\n\n\tprivate loadModels(): void {\n\t\t// Load custom models from models.json first (to know which providers to skip/override)\n\t\tconst {\n\t\t\tmodels: customModels,\n\t\t\treplacedProviders,\n\t\t\toverrides,\n\t\t\terror,\n\t\t} = this.modelsJsonPath ? this.loadCustomModels(this.modelsJsonPath) : emptyCustomModelsResult();\n\n\t\tif (error) {\n\t\t\tthis.loadError = error;\n\t\t\t// Keep built-in models even if custom models failed to load\n\t\t}\n\n\t\tconst builtInModels = this.loadBuiltInModels(replacedProviders, overrides);\n\t\tlet combined = [...builtInModels, ...customModels];\n\n\t\t// Let OAuth providers modify their models (e.g., update baseUrl)\n\t\tfor (const oauthProvider of this.authStorage.getOAuthProviders()) {\n\t\t\tconst cred = this.authStorage.get(oauthProvider.id);\n\t\t\tif (cred?.type === \"oauth\" && oauthProvider.modifyModels) {\n\t\t\t\tcombined = oauthProvider.modifyModels(combined, cred);\n\t\t\t}\n\t\t}\n\n\t\tthis.models = combined;\n\t}\n\n\t/** Load built-in models, skipping replaced providers and applying overrides */\n\tprivate loadBuiltInModels(replacedProviders: Set<string>, overrides: Map<string, ProviderOverride>): Model<Api>[] {\n\t\treturn getProviders()\n\t\t\t.filter((provider) => !replacedProviders.has(provider))\n\t\t\t.flatMap((provider) => {\n\t\t\t\tconst models = getModels(provider as KnownProvider) as Model<Api>[];\n\t\t\t\tconst override = overrides.get(provider);\n\t\t\t\tif (!override) return models;\n\n\t\t\t\t// Apply baseUrl/headers override to all models of this provider\n\t\t\t\tconst resolvedHeaders = resolveHeaders(override.headers);\n\t\t\t\treturn models.map((m) => ({\n\t\t\t\t\t...m,\n\t\t\t\t\tbaseUrl: override.baseUrl ?? m.baseUrl,\n\t\t\t\t\theaders: resolvedHeaders ? { ...m.headers, ...resolvedHeaders } : m.headers,\n\t\t\t\t}));\n\t\t\t});\n\t}\n\n\tprivate loadCustomModels(modelsJsonPath: string): CustomModelsResult {\n\t\tif (!existsSync(modelsJsonPath)) {\n\t\t\treturn emptyCustomModelsResult();\n\t\t}\n\n\t\ttry {\n\t\t\tconst content = readFileSync(modelsJsonPath, \"utf-8\");\n\t\t\tconst config: ModelsConfig = JSON.parse(content);\n\n\t\t\t// Validate schema\n\t\t\tconst ajv = new Ajv();\n\t\t\tconst validate = ajv.compile(ModelsConfigSchema);\n\t\t\tif (!validate(config)) {\n\t\t\t\tconst errors =\n\t\t\t\t\tvalidate.errors?.map((e: any) => ` - ${e.instancePath || \"root\"}: ${e.message}`).join(\"\\n\") ||\n\t\t\t\t\t\"Unknown schema error\";\n\t\t\t\treturn emptyCustomModelsResult(`Invalid models.json schema:\\n${errors}\\n\\nFile: ${modelsJsonPath}`);\n\t\t\t}\n\n\t\t\t// Additional validation\n\t\t\tthis.validateConfig(config);\n\n\t\t\t// Separate providers into \"full replacement\" (has models) vs \"override-only\" (no models)\n\t\t\tconst replacedProviders = new Set<string>();\n\t\t\tconst overrides = new Map<string, ProviderOverride>();\n\n\t\t\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\t\t\tif (providerConfig.models && providerConfig.models.length > 0) {\n\t\t\t\t\t// Has custom models -> full replacement\n\t\t\t\t\treplacedProviders.add(providerName);\n\t\t\t\t} else {\n\t\t\t\t\t// No models -> just override baseUrl/headers on built-in\n\t\t\t\t\toverrides.set(providerName, {\n\t\t\t\t\t\tbaseUrl: providerConfig.baseUrl,\n\t\t\t\t\t\theaders: providerConfig.headers,\n\t\t\t\t\t\tapiKey: providerConfig.apiKey,\n\t\t\t\t\t});\n\t\t\t\t\t// Store API key for fallback resolver\n\t\t\t\t\tif (providerConfig.apiKey) {\n\t\t\t\t\t\tthis.customProviderApiKeys.set(providerName, providerConfig.apiKey);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn { models: this.parseModels(config), replacedProviders, overrides, error: undefined };\n\t\t} catch (error) {\n\t\t\tif (error instanceof SyntaxError) {\n\t\t\t\treturn emptyCustomModelsResult(`Failed to parse models.json: ${error.message}\\n\\nFile: ${modelsJsonPath}`);\n\t\t\t}\n\t\t\treturn emptyCustomModelsResult(\n\t\t\t\t`Failed to load models.json: ${error instanceof Error ? error.message : error}\\n\\nFile: ${modelsJsonPath}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate validateConfig(config: ModelsConfig): void {\n\t\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\t\tconst hasProviderApi = !!providerConfig.api;\n\t\t\tconst models = providerConfig.models ?? [];\n\n\t\t\tif (models.length === 0) {\n\t\t\t\t// Override-only config: just needs baseUrl (to override built-in)\n\t\t\t\tif (!providerConfig.baseUrl) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Provider ${providerName}: must specify either \"baseUrl\" (for override) or \"models\" (for replacement).`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Full replacement: needs baseUrl and apiKey\n\t\t\t\tif (!providerConfig.baseUrl) {\n\t\t\t\t\tthrow new Error(`Provider ${providerName}: \"baseUrl\" is required when defining custom models.`);\n\t\t\t\t}\n\t\t\t\tif (!providerConfig.apiKey) {\n\t\t\t\t\tthrow new Error(`Provider ${providerName}: \"apiKey\" is required when defining custom models.`);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor (const modelDef of models) {\n\t\t\t\tconst hasModelApi = !!modelDef.api;\n\n\t\t\t\tif (!hasProviderApi && !hasModelApi) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Provider ${providerName}, model ${modelDef.id}: no \"api\" specified. Set at provider or model level.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tif (!modelDef.id) throw new Error(`Provider ${providerName}: model missing \"id\"`);\n\t\t\t\tif (!modelDef.name) throw new Error(`Provider ${providerName}: model missing \"name\"`);\n\t\t\t\tif (modelDef.contextWindow <= 0)\n\t\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`);\n\t\t\t\tif (modelDef.maxTokens <= 0)\n\t\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate parseModels(config: ModelsConfig): Model<Api>[] {\n\t\tconst models: Model<Api>[] = [];\n\n\t\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\t\tconst modelDefs = providerConfig.models ?? [];\n\t\t\tif (modelDefs.length === 0) continue; // Override-only, no custom models\n\n\t\t\t// Store API key config for fallback resolver\n\t\t\tif (providerConfig.apiKey) {\n\t\t\t\tthis.customProviderApiKeys.set(providerName, providerConfig.apiKey);\n\t\t\t}\n\n\t\t\tfor (const modelDef of modelDefs) {\n\t\t\t\tconst api = modelDef.api || providerConfig.api;\n\t\t\t\tif (!api) continue;\n\n\t\t\t\t// Merge headers: provider headers are base, model headers override\n\t\t\t\t// Resolve env vars and shell commands in header values\n\t\t\t\tconst providerHeaders = resolveHeaders(providerConfig.headers);\n\t\t\t\tconst modelHeaders = resolveHeaders(modelDef.headers);\n\t\t\t\tlet headers = providerHeaders || modelHeaders ? { ...providerHeaders, ...modelHeaders } : undefined;\n\n\t\t\t\t// If authHeader is true, add Authorization header with resolved API key\n\t\t\t\tif (providerConfig.authHeader && providerConfig.apiKey) {\n\t\t\t\t\tconst resolvedKey = resolveConfigValue(providerConfig.apiKey);\n\t\t\t\t\tif (resolvedKey) {\n\t\t\t\t\t\theaders = { ...headers, Authorization: `Bearer ${resolvedKey}` };\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// baseUrl is validated to exist for providers with models\n\t\t\t\tmodels.push({\n\t\t\t\t\tid: modelDef.id,\n\t\t\t\t\tname: modelDef.name,\n\t\t\t\t\tapi: api as Api,\n\t\t\t\t\tprovider: providerName,\n\t\t\t\t\tbaseUrl: providerConfig.baseUrl!,\n\t\t\t\t\treasoning: modelDef.reasoning,\n\t\t\t\t\tinput: modelDef.input as (\"text\" | \"image\")[],\n\t\t\t\t\tcost: modelDef.cost,\n\t\t\t\t\tcontextWindow: modelDef.contextWindow,\n\t\t\t\t\tmaxTokens: modelDef.maxTokens,\n\t\t\t\t\theaders,\n\t\t\t\t\tcompat: modelDef.compat,\n\t\t\t\t} as Model<Api>);\n\t\t\t}\n\t\t}\n\n\t\treturn models;\n\t}\n\n\t/**\n\t * Get all models (built-in + custom).\n\t * If models.json had errors, returns only built-in models.\n\t */\n\tgetAll(): Model<Api>[] {\n\t\treturn this.models;\n\t}\n\n\t/**\n\t * Get only models that have auth configured.\n\t * This is a fast check that doesn't refresh OAuth tokens.\n\t */\n\tgetAvailable(): Model<Api>[] {\n\t\treturn this.models.filter((m) => this.authStorage.hasAuth(m.provider));\n\t}\n\n\t/**\n\t * Find a model by provider and ID.\n\t */\n\tfind(provider: string, modelId: string): Model<Api> | undefined {\n\t\treturn this.models.find((m) => m.provider === provider && m.id === modelId);\n\t}\n\n\t/**\n\t * Get API key for a model.\n\t */\n\tasync getApiKey(model: Model<Api>): Promise<string | undefined> {\n\t\treturn this.authStorage.getApiKey(model.provider);\n\t}\n\n\t/**\n\t * Get API key for a provider.\n\t */\n\tasync getApiKeyForProvider(provider: string): Promise<string | undefined> {\n\t\treturn this.authStorage.getApiKey(provider);\n\t}\n\n\t/**\n\t * Check if a model is using OAuth credentials (subscription).\n\t */\n\tisUsingOAuth(model: Model<Api>): boolean {\n\t\tconst cred = this.authStorage.get(model.provider);\n\t\treturn cred?.type === \"oauth\";\n\t}\n\n\t/**\n\t * Register a provider dynamically (from extensions).\n\t *\n\t * If provider has models: replaces all existing models for this provider.\n\t * If provider has only baseUrl/headers: overrides existing models' URLs.\n\t * If provider has oauth: registers OAuth provider for /login support.\n\t */\n\tregisterProvider(providerName: string, config: ProviderConfigInput): void {\n\t\tthis.registeredProviders.set(providerName, config);\n\t\tthis.applyProviderConfig(providerName, config);\n\t}\n\n\tprivate applyProviderConfig(providerName: string, config: ProviderConfigInput): void {\n\t\t// Register OAuth provider if provided\n\t\tif (config.oauth) {\n\t\t\t// Ensure the OAuth provider ID matches the provider name\n\t\t\tconst oauthProvider: OAuthProviderInterface = {\n\t\t\t\t...config.oauth,\n\t\t\t\tid: providerName,\n\t\t\t};\n\t\t\tregisterOAuthProvider(oauthProvider);\n\t\t}\n\n\t\tif (config.streamSimple) {\n\t\t\tif (!config.api) {\n\t\t\t\tthrow new Error(`Provider ${providerName}: \"api\" is required when registering streamSimple.`);\n\t\t\t}\n\t\t\tconst streamSimple = config.streamSimple;\n\t\t\tregisterApiProvider({\n\t\t\t\tapi: config.api,\n\t\t\t\tstream: (model, context, options) => streamSimple(model, context, options as SimpleStreamOptions),\n\t\t\t\tstreamSimple,\n\t\t\t});\n\t\t}\n\n\t\t// Store API key for auth resolution\n\t\tif (config.apiKey) {\n\t\t\tthis.customProviderApiKeys.set(providerName, config.apiKey);\n\t\t}\n\n\t\tif (config.models && config.models.length > 0) {\n\t\t\t// Full replacement: remove existing models for this provider\n\t\t\tthis.models = this.models.filter((m) => m.provider !== providerName);\n\n\t\t\t// Validate required fields\n\t\t\tif (!config.baseUrl) {\n\t\t\t\tthrow new Error(`Provider ${providerName}: \"baseUrl\" is required when defining models.`);\n\t\t\t}\n\t\t\tif (!config.apiKey && !config.oauth) {\n\t\t\t\tthrow new Error(`Provider ${providerName}: \"apiKey\" or \"oauth\" is required when defining models.`);\n\t\t\t}\n\n\t\t\t// Parse and add new models\n\t\t\tfor (const modelDef of config.models) {\n\t\t\t\tconst api = modelDef.api || config.api;\n\t\t\t\tif (!api) {\n\t\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: no \"api\" specified.`);\n\t\t\t\t}\n\n\t\t\t\t// Merge headers\n\t\t\t\tconst providerHeaders = resolveHeaders(config.headers);\n\t\t\t\tconst modelHeaders = resolveHeaders(modelDef.headers);\n\t\t\t\tlet headers = providerHeaders || modelHeaders ? { ...providerHeaders, ...modelHeaders } : undefined;\n\n\t\t\t\t// If authHeader is true, add Authorization header\n\t\t\t\tif (config.authHeader && config.apiKey) {\n\t\t\t\t\tconst resolvedKey = resolveConfigValue(config.apiKey);\n\t\t\t\t\tif (resolvedKey) {\n\t\t\t\t\t\theaders = { ...headers, Authorization: `Bearer ${resolvedKey}` };\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tthis.models.push({\n\t\t\t\t\tid: modelDef.id,\n\t\t\t\t\tname: modelDef.name,\n\t\t\t\t\tapi: api as Api,\n\t\t\t\t\tprovider: providerName,\n\t\t\t\t\tbaseUrl: config.baseUrl,\n\t\t\t\t\treasoning: modelDef.reasoning,\n\t\t\t\t\tinput: modelDef.input as (\"text\" | \"image\")[],\n\t\t\t\t\tcost: modelDef.cost,\n\t\t\t\t\tcontextWindow: modelDef.contextWindow,\n\t\t\t\t\tmaxTokens: modelDef.maxTokens,\n\t\t\t\t\theaders,\n\t\t\t\t\tcompat: modelDef.compat,\n\t\t\t\t} as Model<Api>);\n\t\t\t}\n\t\t} else if (config.baseUrl) {\n\t\t\t// Override-only: update baseUrl/headers for existing models\n\t\t\tconst resolvedHeaders = resolveHeaders(config.headers);\n\t\t\tthis.models = this.models.map((m) => {\n\t\t\t\tif (m.provider !== providerName) return m;\n\t\t\t\treturn {\n\t\t\t\t\t...m,\n\t\t\t\t\tbaseUrl: config.baseUrl ?? m.baseUrl,\n\t\t\t\t\theaders: resolvedHeaders ? { ...m.headers, ...resolvedHeaders } : m.headers,\n\t\t\t\t};\n\t\t\t});\n\t\t}\n\t}\n}\n\n/**\n * Input type for registerProvider API.\n */\nexport interface ProviderConfigInput {\n\tbaseUrl?: string;\n\tapiKey?: string;\n\tapi?: Api;\n\tstreamSimple?: (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream;\n\theaders?: Record<string, string>;\n\tauthHeader?: boolean;\n\t/** OAuth provider for /login support */\n\toauth?: Omit<OAuthProviderInterface, \"id\">;\n\tmodels?: Array<{\n\t\tid: string;\n\t\tname: string;\n\t\tapi?: Api;\n\t\treasoning: boolean;\n\t\tinput: (\"text\" | \"image\")[];\n\t\tcost: { input: number; output: number; cacheRead: number; cacheWrite: number };\n\t\tcontextWindow: number;\n\t\tmaxTokens: number;\n\t\theaders?: Record<string, string>;\n\t\tcompat?: Model<Api>[\"compat\"];\n\t}>;\n}\n"]}
|
|
@@ -1,18 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Model registry - manages built-in and custom models, provides API key resolution.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { getModels, getProviders, registerApiProvider, registerOAuthProvider, } from "@mariozechner/pi-ai";
|
|
5
5
|
import { Type } from "@sinclair/typebox";
|
|
6
6
|
import AjvModule from "ajv";
|
|
7
7
|
import { execSync } from "child_process";
|
|
8
8
|
import { existsSync, readFileSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { getAgentDir } from "../config.js";
|
|
9
11
|
const Ajv = AjvModule.default || AjvModule;
|
|
12
|
+
// Schema for OpenRouter routing preferences
|
|
13
|
+
const OpenRouterRoutingSchema = Type.Object({
|
|
14
|
+
only: Type.Optional(Type.Array(Type.String())),
|
|
15
|
+
order: Type.Optional(Type.Array(Type.String())),
|
|
16
|
+
});
|
|
10
17
|
// Schema for OpenAI compatibility settings
|
|
11
18
|
const OpenAICompletionsCompatSchema = Type.Object({
|
|
12
19
|
supportsStore: Type.Optional(Type.Boolean()),
|
|
13
20
|
supportsDeveloperRole: Type.Optional(Type.Boolean()),
|
|
14
21
|
supportsReasoningEffort: Type.Optional(Type.Boolean()),
|
|
22
|
+
supportsUsageInStreaming: Type.Optional(Type.Boolean()),
|
|
15
23
|
maxTokensField: Type.Optional(Type.Union([Type.Literal("max_completion_tokens"), Type.Literal("max_tokens")])),
|
|
24
|
+
requiresToolResultName: Type.Optional(Type.Boolean()),
|
|
25
|
+
requiresAssistantAfterToolResult: Type.Optional(Type.Boolean()),
|
|
26
|
+
requiresThinkingAsText: Type.Optional(Type.Boolean()),
|
|
27
|
+
requiresMistralToolIds: Type.Optional(Type.Boolean()),
|
|
28
|
+
thinkingFormat: Type.Optional(Type.Union([Type.Literal("openai"), Type.Literal("zai")])),
|
|
29
|
+
openRouterRouting: Type.Optional(OpenRouterRoutingSchema),
|
|
16
30
|
});
|
|
17
31
|
const OpenAIResponsesCompatSchema = Type.Object({});
|
|
18
32
|
const OpenAICompatSchema = Type.Union([OpenAICompletionsCompatSchema, OpenAIResponsesCompatSchema]);
|
|
@@ -20,14 +34,7 @@ const OpenAICompatSchema = Type.Union([OpenAICompletionsCompatSchema, OpenAIResp
|
|
|
20
34
|
const ModelDefinitionSchema = Type.Object({
|
|
21
35
|
id: Type.String({ minLength: 1 }),
|
|
22
36
|
name: Type.String({ minLength: 1 }),
|
|
23
|
-
api: Type.Optional(Type.
|
|
24
|
-
Type.Literal("openai-completions"),
|
|
25
|
-
Type.Literal("openai-responses"),
|
|
26
|
-
Type.Literal("openai-codex-responses"),
|
|
27
|
-
Type.Literal("anthropic-messages"),
|
|
28
|
-
Type.Literal("google-generative-ai"),
|
|
29
|
-
Type.Literal("bedrock-converse-stream"),
|
|
30
|
-
])),
|
|
37
|
+
api: Type.Optional(Type.String({ minLength: 1 })),
|
|
31
38
|
reasoning: Type.Boolean(),
|
|
32
39
|
input: Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")])),
|
|
33
40
|
cost: Type.Object({
|
|
@@ -44,14 +51,7 @@ const ModelDefinitionSchema = Type.Object({
|
|
|
44
51
|
const ProviderConfigSchema = Type.Object({
|
|
45
52
|
baseUrl: Type.Optional(Type.String({ minLength: 1 })),
|
|
46
53
|
apiKey: Type.Optional(Type.String({ minLength: 1 })),
|
|
47
|
-
api: Type.Optional(Type.
|
|
48
|
-
Type.Literal("openai-completions"),
|
|
49
|
-
Type.Literal("openai-responses"),
|
|
50
|
-
Type.Literal("openai-codex-responses"),
|
|
51
|
-
Type.Literal("anthropic-messages"),
|
|
52
|
-
Type.Literal("google-generative-ai"),
|
|
53
|
-
Type.Literal("bedrock-converse-stream"),
|
|
54
|
-
])),
|
|
54
|
+
api: Type.Optional(Type.String({ minLength: 1 })),
|
|
55
55
|
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
56
56
|
authHeader: Type.Optional(Type.Boolean()),
|
|
57
57
|
models: Type.Optional(Type.Array(ModelDefinitionSchema)),
|
|
@@ -65,18 +65,18 @@ function emptyCustomModelsResult(error) {
|
|
|
65
65
|
// Cache for shell command results (persists for process lifetime)
|
|
66
66
|
const commandResultCache = new Map();
|
|
67
67
|
/**
|
|
68
|
-
* Resolve
|
|
68
|
+
* Resolve a config value (API key, header value, etc.) to an actual value.
|
|
69
69
|
* - If starts with "!", executes the rest as a shell command and uses stdout (cached)
|
|
70
70
|
* - Otherwise checks environment variable first, then treats as literal (not cached)
|
|
71
71
|
*/
|
|
72
|
-
function
|
|
73
|
-
if (
|
|
74
|
-
return
|
|
72
|
+
function resolveConfigValue(config) {
|
|
73
|
+
if (config.startsWith("!")) {
|
|
74
|
+
return executeCommand(config);
|
|
75
75
|
}
|
|
76
|
-
const envValue = process.env[
|
|
77
|
-
return envValue ||
|
|
76
|
+
const envValue = process.env[config];
|
|
77
|
+
return envValue || config;
|
|
78
78
|
}
|
|
79
|
-
function
|
|
79
|
+
function executeCommand(commandConfig) {
|
|
80
80
|
if (commandResultCache.has(commandConfig)) {
|
|
81
81
|
return commandResultCache.get(commandConfig);
|
|
82
82
|
}
|
|
@@ -96,7 +96,22 @@ function executeApiKeyCommand(commandConfig) {
|
|
|
96
96
|
commandResultCache.set(commandConfig, result);
|
|
97
97
|
return result;
|
|
98
98
|
}
|
|
99
|
-
/**
|
|
99
|
+
/**
|
|
100
|
+
* Resolve all header values using the same resolution logic as API keys.
|
|
101
|
+
*/
|
|
102
|
+
function resolveHeaders(headers) {
|
|
103
|
+
if (!headers)
|
|
104
|
+
return undefined;
|
|
105
|
+
const resolved = {};
|
|
106
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
107
|
+
const resolvedValue = resolveConfigValue(value);
|
|
108
|
+
if (resolvedValue) {
|
|
109
|
+
resolved[key] = resolvedValue;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return Object.keys(resolved).length > 0 ? resolved : undefined;
|
|
113
|
+
}
|
|
114
|
+
/** Clear the config value command cache. Exported for testing. */
|
|
100
115
|
export function clearApiKeyCache() {
|
|
101
116
|
commandResultCache.clear();
|
|
102
117
|
}
|
|
@@ -108,15 +123,16 @@ export class ModelRegistry {
|
|
|
108
123
|
modelsJsonPath;
|
|
109
124
|
models = [];
|
|
110
125
|
customProviderApiKeys = new Map();
|
|
126
|
+
registeredProviders = new Map();
|
|
111
127
|
loadError = undefined;
|
|
112
|
-
constructor(authStorage, modelsJsonPath =
|
|
128
|
+
constructor(authStorage, modelsJsonPath = join(getAgentDir(), "models.json")) {
|
|
113
129
|
this.authStorage = authStorage;
|
|
114
130
|
this.modelsJsonPath = modelsJsonPath;
|
|
115
131
|
// Set up fallback resolver for custom provider API keys
|
|
116
132
|
this.authStorage.setFallbackResolver((provider) => {
|
|
117
133
|
const keyConfig = this.customProviderApiKeys.get(provider);
|
|
118
134
|
if (keyConfig) {
|
|
119
|
-
return
|
|
135
|
+
return resolveConfigValue(keyConfig);
|
|
120
136
|
}
|
|
121
137
|
return undefined;
|
|
122
138
|
});
|
|
@@ -130,6 +146,9 @@ export class ModelRegistry {
|
|
|
130
146
|
this.customProviderApiKeys.clear();
|
|
131
147
|
this.loadError = undefined;
|
|
132
148
|
this.loadModels();
|
|
149
|
+
for (const [providerName, config] of this.registeredProviders.entries()) {
|
|
150
|
+
this.applyProviderConfig(providerName, config);
|
|
151
|
+
}
|
|
133
152
|
}
|
|
134
153
|
/**
|
|
135
154
|
* Get any error from loading models.json (undefined if no error).
|
|
@@ -145,19 +164,15 @@ export class ModelRegistry {
|
|
|
145
164
|
// Keep built-in models even if custom models failed to load
|
|
146
165
|
}
|
|
147
166
|
const builtInModels = this.loadBuiltInModels(replacedProviders, overrides);
|
|
148
|
-
|
|
149
|
-
//
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const baseUrl = getGitHubCopilotBaseUrl(copilotCred.access, domain);
|
|
156
|
-
this.models = combined.map((m) => (m.provider === "github-copilot" ? { ...m, baseUrl } : m));
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
this.models = combined;
|
|
167
|
+
let combined = [...builtInModels, ...customModels];
|
|
168
|
+
// Let OAuth providers modify their models (e.g., update baseUrl)
|
|
169
|
+
for (const oauthProvider of this.authStorage.getOAuthProviders()) {
|
|
170
|
+
const cred = this.authStorage.get(oauthProvider.id);
|
|
171
|
+
if (cred?.type === "oauth" && oauthProvider.modifyModels) {
|
|
172
|
+
combined = oauthProvider.modifyModels(combined, cred);
|
|
173
|
+
}
|
|
160
174
|
}
|
|
175
|
+
this.models = combined;
|
|
161
176
|
}
|
|
162
177
|
/** Load built-in models, skipping replaced providers and applying overrides */
|
|
163
178
|
loadBuiltInModels(replacedProviders, overrides) {
|
|
@@ -169,10 +184,11 @@ export class ModelRegistry {
|
|
|
169
184
|
if (!override)
|
|
170
185
|
return models;
|
|
171
186
|
// Apply baseUrl/headers override to all models of this provider
|
|
187
|
+
const resolvedHeaders = resolveHeaders(override.headers);
|
|
172
188
|
return models.map((m) => ({
|
|
173
189
|
...m,
|
|
174
190
|
baseUrl: override.baseUrl ?? m.baseUrl,
|
|
175
|
-
headers:
|
|
191
|
+
headers: resolvedHeaders ? { ...m.headers, ...resolvedHeaders } : m.headers,
|
|
176
192
|
}));
|
|
177
193
|
});
|
|
178
194
|
}
|
|
@@ -273,12 +289,13 @@ export class ModelRegistry {
|
|
|
273
289
|
if (!api)
|
|
274
290
|
continue;
|
|
275
291
|
// Merge headers: provider headers are base, model headers override
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
292
|
+
// Resolve env vars and shell commands in header values
|
|
293
|
+
const providerHeaders = resolveHeaders(providerConfig.headers);
|
|
294
|
+
const modelHeaders = resolveHeaders(modelDef.headers);
|
|
295
|
+
let headers = providerHeaders || modelHeaders ? { ...providerHeaders, ...modelHeaders } : undefined;
|
|
279
296
|
// If authHeader is true, add Authorization header with resolved API key
|
|
280
297
|
if (providerConfig.authHeader && providerConfig.apiKey) {
|
|
281
|
-
const resolvedKey =
|
|
298
|
+
const resolvedKey = resolveConfigValue(providerConfig.apiKey);
|
|
282
299
|
if (resolvedKey) {
|
|
283
300
|
headers = { ...headers, Authorization: `Bearer ${resolvedKey}` };
|
|
284
301
|
}
|
|
@@ -341,5 +358,98 @@ export class ModelRegistry {
|
|
|
341
358
|
const cred = this.authStorage.get(model.provider);
|
|
342
359
|
return cred?.type === "oauth";
|
|
343
360
|
}
|
|
361
|
+
/**
|
|
362
|
+
* Register a provider dynamically (from extensions).
|
|
363
|
+
*
|
|
364
|
+
* If provider has models: replaces all existing models for this provider.
|
|
365
|
+
* If provider has only baseUrl/headers: overrides existing models' URLs.
|
|
366
|
+
* If provider has oauth: registers OAuth provider for /login support.
|
|
367
|
+
*/
|
|
368
|
+
registerProvider(providerName, config) {
|
|
369
|
+
this.registeredProviders.set(providerName, config);
|
|
370
|
+
this.applyProviderConfig(providerName, config);
|
|
371
|
+
}
|
|
372
|
+
applyProviderConfig(providerName, config) {
|
|
373
|
+
// Register OAuth provider if provided
|
|
374
|
+
if (config.oauth) {
|
|
375
|
+
// Ensure the OAuth provider ID matches the provider name
|
|
376
|
+
const oauthProvider = {
|
|
377
|
+
...config.oauth,
|
|
378
|
+
id: providerName,
|
|
379
|
+
};
|
|
380
|
+
registerOAuthProvider(oauthProvider);
|
|
381
|
+
}
|
|
382
|
+
if (config.streamSimple) {
|
|
383
|
+
if (!config.api) {
|
|
384
|
+
throw new Error(`Provider ${providerName}: "api" is required when registering streamSimple.`);
|
|
385
|
+
}
|
|
386
|
+
const streamSimple = config.streamSimple;
|
|
387
|
+
registerApiProvider({
|
|
388
|
+
api: config.api,
|
|
389
|
+
stream: (model, context, options) => streamSimple(model, context, options),
|
|
390
|
+
streamSimple,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
// Store API key for auth resolution
|
|
394
|
+
if (config.apiKey) {
|
|
395
|
+
this.customProviderApiKeys.set(providerName, config.apiKey);
|
|
396
|
+
}
|
|
397
|
+
if (config.models && config.models.length > 0) {
|
|
398
|
+
// Full replacement: remove existing models for this provider
|
|
399
|
+
this.models = this.models.filter((m) => m.provider !== providerName);
|
|
400
|
+
// Validate required fields
|
|
401
|
+
if (!config.baseUrl) {
|
|
402
|
+
throw new Error(`Provider ${providerName}: "baseUrl" is required when defining models.`);
|
|
403
|
+
}
|
|
404
|
+
if (!config.apiKey && !config.oauth) {
|
|
405
|
+
throw new Error(`Provider ${providerName}: "apiKey" or "oauth" is required when defining models.`);
|
|
406
|
+
}
|
|
407
|
+
// Parse and add new models
|
|
408
|
+
for (const modelDef of config.models) {
|
|
409
|
+
const api = modelDef.api || config.api;
|
|
410
|
+
if (!api) {
|
|
411
|
+
throw new Error(`Provider ${providerName}, model ${modelDef.id}: no "api" specified.`);
|
|
412
|
+
}
|
|
413
|
+
// Merge headers
|
|
414
|
+
const providerHeaders = resolveHeaders(config.headers);
|
|
415
|
+
const modelHeaders = resolveHeaders(modelDef.headers);
|
|
416
|
+
let headers = providerHeaders || modelHeaders ? { ...providerHeaders, ...modelHeaders } : undefined;
|
|
417
|
+
// If authHeader is true, add Authorization header
|
|
418
|
+
if (config.authHeader && config.apiKey) {
|
|
419
|
+
const resolvedKey = resolveConfigValue(config.apiKey);
|
|
420
|
+
if (resolvedKey) {
|
|
421
|
+
headers = { ...headers, Authorization: `Bearer ${resolvedKey}` };
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
this.models.push({
|
|
425
|
+
id: modelDef.id,
|
|
426
|
+
name: modelDef.name,
|
|
427
|
+
api: api,
|
|
428
|
+
provider: providerName,
|
|
429
|
+
baseUrl: config.baseUrl,
|
|
430
|
+
reasoning: modelDef.reasoning,
|
|
431
|
+
input: modelDef.input,
|
|
432
|
+
cost: modelDef.cost,
|
|
433
|
+
contextWindow: modelDef.contextWindow,
|
|
434
|
+
maxTokens: modelDef.maxTokens,
|
|
435
|
+
headers,
|
|
436
|
+
compat: modelDef.compat,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
else if (config.baseUrl) {
|
|
441
|
+
// Override-only: update baseUrl/headers for existing models
|
|
442
|
+
const resolvedHeaders = resolveHeaders(config.headers);
|
|
443
|
+
this.models = this.models.map((m) => {
|
|
444
|
+
if (m.provider !== providerName)
|
|
445
|
+
return m;
|
|
446
|
+
return {
|
|
447
|
+
...m,
|
|
448
|
+
baseUrl: config.baseUrl ?? m.baseUrl,
|
|
449
|
+
headers: resolvedHeaders ? { ...m.headers, ...resolvedHeaders } : m.headers,
|
|
450
|
+
};
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
344
454
|
}
|
|
345
455
|
//# sourceMappingURL=model-registry.js.map
|