@mariozechner/pi-coding-agent 0.27.8 → 0.28.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 +28 -0
- package/README.md +16 -17
- package/dist/cli/list-models.d.ts +2 -1
- package/dist/cli/list-models.d.ts.map +1 -1
- package/dist/cli/list-models.js +2 -7
- package/dist/cli/list-models.js.map +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -3
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +6 -3
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +18 -20
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts +104 -0
- package/dist/core/auth-storage.d.ts.map +1 -0
- package/dist/core/auth-storage.js +232 -0
- package/dist/core/auth-storage.js.map +1 -0
- package/dist/core/model-registry.d.ts +50 -0
- package/dist/core/model-registry.d.ts.map +1 -0
- package/dist/core/model-registry.js +268 -0
- package/dist/core/model-registry.js.map +1 -0
- package/dist/core/model-resolver.d.ts +7 -4
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +12 -41
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/sdk.d.ts +13 -26
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +24 -101
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/settings-manager.d.ts +0 -5
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +0 -19
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +15 -1
- package/dist/core/skills.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -8
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +37 -21
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +3 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +4 -3
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +3 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +21 -13
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.d.ts +3 -1
- package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.js +6 -6
- package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +53 -48
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/hooks-v2.md +291 -220
- package/docs/sdk.md +86 -61
- package/examples/custom-tools/hello/index.ts +15 -15
- package/examples/custom-tools/question/index.ts +3 -3
- package/examples/custom-tools/subagent/agents.ts +1 -2
- package/examples/custom-tools/subagent/index.ts +332 -125
- package/examples/custom-tools/todo/index.ts +30 -12
- package/examples/hooks/confirm-destructive.ts +5 -7
- package/examples/hooks/custom-compaction.ts +7 -7
- package/examples/hooks/dirty-repo-guard.ts +5 -9
- package/examples/hooks/permission-gate.ts +1 -5
- package/examples/sdk/02-custom-model.ts +20 -7
- package/examples/sdk/04-skills.ts +1 -1
- package/examples/sdk/05-tools.ts +11 -14
- package/examples/sdk/06-hooks.ts +1 -1
- package/examples/sdk/07-context-files.ts +1 -1
- package/examples/sdk/08-slash-commands.ts +3 -3
- package/examples/sdk/09-api-keys-and-oauth.ts +36 -26
- package/examples/sdk/10-settings.ts +2 -2
- package/examples/sdk/12-full-control.ts +19 -20
- package/examples/sdk/README.md +26 -13
- package/package.json +4 -5
- package/dist/core/model-config.d.ts +0 -54
- package/dist/core/model-config.d.ts.map +0 -1
- package/dist/core/model-config.js +0 -376
- package/dist/core/model-config.js.map +0 -1
- package/dist/core/oauth/index.d.ts +0 -41
- package/dist/core/oauth/index.d.ts.map +0 -1
- package/dist/core/oauth/index.js +0 -84
- package/dist/core/oauth/index.js.map +0 -1
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential storage for API keys and OAuth tokens.
|
|
3
|
+
* Handles loading, saving, and refreshing credentials from auth.json.
|
|
4
|
+
*/
|
|
5
|
+
import { getEnvApiKey, getOAuthApiKey, loginAnthropic, loginAntigravity, loginGeminiCli, loginGitHubCopilot, } from "@mariozechner/pi-ai";
|
|
6
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
7
|
+
import { dirname, join } from "path";
|
|
8
|
+
/**
|
|
9
|
+
* Credential storage backed by a JSON file.
|
|
10
|
+
*/
|
|
11
|
+
export class AuthStorage {
|
|
12
|
+
authPath;
|
|
13
|
+
data = {};
|
|
14
|
+
runtimeOverrides = new Map();
|
|
15
|
+
fallbackResolver;
|
|
16
|
+
constructor(authPath) {
|
|
17
|
+
this.authPath = authPath;
|
|
18
|
+
this.reload();
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Set a runtime API key override (not persisted to disk).
|
|
22
|
+
* Used for CLI --api-key flag.
|
|
23
|
+
*/
|
|
24
|
+
setRuntimeApiKey(provider, apiKey) {
|
|
25
|
+
this.runtimeOverrides.set(provider, apiKey);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Remove a runtime API key override.
|
|
29
|
+
*/
|
|
30
|
+
removeRuntimeApiKey(provider) {
|
|
31
|
+
this.runtimeOverrides.delete(provider);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Set a fallback resolver for API keys not found in auth.json or env vars.
|
|
35
|
+
* Used for custom provider keys from models.json.
|
|
36
|
+
*/
|
|
37
|
+
setFallbackResolver(resolver) {
|
|
38
|
+
this.fallbackResolver = resolver;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Reload credentials from disk.
|
|
42
|
+
*/
|
|
43
|
+
reload() {
|
|
44
|
+
if (!existsSync(this.authPath)) {
|
|
45
|
+
this.data = {};
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
this.data = JSON.parse(readFileSync(this.authPath, "utf-8"));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
this.data = {};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Save credentials to disk.
|
|
57
|
+
*/
|
|
58
|
+
save() {
|
|
59
|
+
const dir = dirname(this.authPath);
|
|
60
|
+
if (!existsSync(dir)) {
|
|
61
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
62
|
+
}
|
|
63
|
+
writeFileSync(this.authPath, JSON.stringify(this.data, null, 2), "utf-8");
|
|
64
|
+
chmodSync(this.authPath, 0o600);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get credential for a provider.
|
|
68
|
+
*/
|
|
69
|
+
get(provider) {
|
|
70
|
+
return this.data[provider] ?? null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Set credential for a provider.
|
|
74
|
+
*/
|
|
75
|
+
set(provider, credential) {
|
|
76
|
+
this.data[provider] = credential;
|
|
77
|
+
this.save();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Remove credential for a provider.
|
|
81
|
+
*/
|
|
82
|
+
remove(provider) {
|
|
83
|
+
delete this.data[provider];
|
|
84
|
+
this.save();
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* List all providers with credentials.
|
|
88
|
+
*/
|
|
89
|
+
list() {
|
|
90
|
+
return Object.keys(this.data);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Check if credentials exist for a provider.
|
|
94
|
+
*/
|
|
95
|
+
has(provider) {
|
|
96
|
+
return provider in this.data;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Get all credentials (for passing to getOAuthApiKey).
|
|
100
|
+
*/
|
|
101
|
+
getAll() {
|
|
102
|
+
return { ...this.data };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Login to an OAuth provider.
|
|
106
|
+
*/
|
|
107
|
+
async login(provider, callbacks) {
|
|
108
|
+
let credentials;
|
|
109
|
+
switch (provider) {
|
|
110
|
+
case "anthropic":
|
|
111
|
+
credentials = await loginAnthropic((url) => callbacks.onAuth({ url }), () => callbacks.onPrompt({ message: "Paste the authorization code:" }));
|
|
112
|
+
break;
|
|
113
|
+
case "github-copilot":
|
|
114
|
+
credentials = await loginGitHubCopilot({
|
|
115
|
+
onAuth: (url, instructions) => callbacks.onAuth({ url, instructions }),
|
|
116
|
+
onPrompt: callbacks.onPrompt,
|
|
117
|
+
onProgress: callbacks.onProgress,
|
|
118
|
+
});
|
|
119
|
+
break;
|
|
120
|
+
case "google-gemini-cli":
|
|
121
|
+
credentials = await loginGeminiCli(callbacks.onAuth, callbacks.onProgress);
|
|
122
|
+
break;
|
|
123
|
+
case "google-antigravity":
|
|
124
|
+
credentials = await loginAntigravity(callbacks.onAuth, callbacks.onProgress);
|
|
125
|
+
break;
|
|
126
|
+
default:
|
|
127
|
+
throw new Error(`Unknown OAuth provider: ${provider}`);
|
|
128
|
+
}
|
|
129
|
+
this.set(provider, { type: "oauth", ...credentials });
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Logout from a provider.
|
|
133
|
+
*/
|
|
134
|
+
logout(provider) {
|
|
135
|
+
this.remove(provider);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Get API key for a provider.
|
|
139
|
+
* Priority:
|
|
140
|
+
* 1. Runtime override (CLI --api-key)
|
|
141
|
+
* 2. API key from auth.json
|
|
142
|
+
* 3. OAuth token from auth.json (auto-refreshed)
|
|
143
|
+
* 4. Environment variable
|
|
144
|
+
* 5. Fallback resolver (models.json custom providers)
|
|
145
|
+
*/
|
|
146
|
+
async getApiKey(provider) {
|
|
147
|
+
// Runtime override takes highest priority
|
|
148
|
+
const runtimeKey = this.runtimeOverrides.get(provider);
|
|
149
|
+
if (runtimeKey) {
|
|
150
|
+
return runtimeKey;
|
|
151
|
+
}
|
|
152
|
+
const cred = this.data[provider];
|
|
153
|
+
if (cred?.type === "api_key") {
|
|
154
|
+
return cred.key;
|
|
155
|
+
}
|
|
156
|
+
if (cred?.type === "oauth") {
|
|
157
|
+
// Filter to only oauth credentials for getOAuthApiKey
|
|
158
|
+
const oauthCreds = {};
|
|
159
|
+
for (const [key, value] of Object.entries(this.data)) {
|
|
160
|
+
if (value.type === "oauth") {
|
|
161
|
+
oauthCreds[key] = value;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const result = await getOAuthApiKey(provider, oauthCreds);
|
|
166
|
+
if (result) {
|
|
167
|
+
this.data[provider] = { type: "oauth", ...result.newCredentials };
|
|
168
|
+
this.save();
|
|
169
|
+
return result.apiKey;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
this.remove(provider);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// Fall back to environment variable
|
|
177
|
+
const envKey = getEnvApiKey(provider);
|
|
178
|
+
if (envKey)
|
|
179
|
+
return envKey;
|
|
180
|
+
// Fall back to custom resolver (e.g., models.json custom providers)
|
|
181
|
+
return this.fallbackResolver?.(provider) ?? null;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Migrate credentials from legacy oauth.json and settings.json apiKeys to auth.json.
|
|
185
|
+
* Only runs if auth.json doesn't exist yet. Returns list of migrated providers.
|
|
186
|
+
*/
|
|
187
|
+
static migrateLegacy(authPath, agentDir) {
|
|
188
|
+
const oauthPath = join(agentDir, "oauth.json");
|
|
189
|
+
const settingsPath = join(agentDir, "settings.json");
|
|
190
|
+
// Skip if auth.json already exists
|
|
191
|
+
if (existsSync(authPath))
|
|
192
|
+
return [];
|
|
193
|
+
const migrated = {};
|
|
194
|
+
const providers = [];
|
|
195
|
+
// Migrate oauth.json
|
|
196
|
+
if (existsSync(oauthPath)) {
|
|
197
|
+
try {
|
|
198
|
+
const oauth = JSON.parse(readFileSync(oauthPath, "utf-8"));
|
|
199
|
+
for (const [provider, cred] of Object.entries(oauth)) {
|
|
200
|
+
migrated[provider] = { type: "oauth", ...cred };
|
|
201
|
+
providers.push(provider);
|
|
202
|
+
}
|
|
203
|
+
renameSync(oauthPath, `${oauthPath}.migrated`);
|
|
204
|
+
}
|
|
205
|
+
catch { }
|
|
206
|
+
}
|
|
207
|
+
// Migrate settings.json apiKeys
|
|
208
|
+
if (existsSync(settingsPath)) {
|
|
209
|
+
try {
|
|
210
|
+
const content = readFileSync(settingsPath, "utf-8");
|
|
211
|
+
const settings = JSON.parse(content);
|
|
212
|
+
if (settings.apiKeys && typeof settings.apiKeys === "object") {
|
|
213
|
+
for (const [provider, key] of Object.entries(settings.apiKeys)) {
|
|
214
|
+
if (!migrated[provider] && typeof key === "string") {
|
|
215
|
+
migrated[provider] = { type: "api_key", key };
|
|
216
|
+
providers.push(provider);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
delete settings.apiKeys;
|
|
220
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch { }
|
|
224
|
+
}
|
|
225
|
+
if (Object.keys(migrated).length > 0) {
|
|
226
|
+
mkdirSync(dirname(authPath), { recursive: true });
|
|
227
|
+
writeFileSync(authPath, JSON.stringify(migrated, null, 2), { mode: 0o600 });
|
|
228
|
+
}
|
|
229
|
+
return providers;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=auth-storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-storage.js","sourceRoot":"","sources":["../../src/core/auth-storage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Credential storage for API keys and OAuth tokens.\n * Handles loading, saving, and refreshing credentials from auth.json.\n */\n\nimport {\n\tgetEnvApiKey,\n\tgetOAuthApiKey,\n\tloginAnthropic,\n\tloginAntigravity,\n\tloginGeminiCli,\n\tloginGitHubCopilot,\n\ttype OAuthCredentials,\n\ttype OAuthProvider,\n} from \"@mariozechner/pi-ai\";\nimport { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from \"fs\";\nimport { dirname, join } from \"path\";\n\nexport type ApiKeyCredential = {\n\ttype: \"api_key\";\n\tkey: string;\n};\n\nexport type OAuthCredential = {\n\ttype: \"oauth\";\n} & OAuthCredentials;\n\nexport type AuthCredential = ApiKeyCredential | OAuthCredential;\n\nexport type AuthStorageData = Record<string, AuthCredential>;\n\n/**\n * Credential storage backed by a JSON file.\n */\nexport class AuthStorage {\n\tprivate data: AuthStorageData = {};\n\tprivate runtimeOverrides: Map<string, string> = new Map();\n\tprivate fallbackResolver?: (provider: string) => string | undefined;\n\n\tconstructor(private authPath: string) {\n\t\tthis.reload();\n\t}\n\n\t/**\n\t * Set a runtime API key override (not persisted to disk).\n\t * Used for CLI --api-key flag.\n\t */\n\tsetRuntimeApiKey(provider: string, apiKey: string): void {\n\t\tthis.runtimeOverrides.set(provider, apiKey);\n\t}\n\n\t/**\n\t * Remove a runtime API key override.\n\t */\n\tremoveRuntimeApiKey(provider: string): void {\n\t\tthis.runtimeOverrides.delete(provider);\n\t}\n\n\t/**\n\t * Set a fallback resolver for API keys not found in auth.json or env vars.\n\t * Used for custom provider keys from models.json.\n\t */\n\tsetFallbackResolver(resolver: (provider: string) => string | undefined): void {\n\t\tthis.fallbackResolver = resolver;\n\t}\n\n\t/**\n\t * Reload credentials from disk.\n\t */\n\treload(): void {\n\t\tif (!existsSync(this.authPath)) {\n\t\t\tthis.data = {};\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tthis.data = JSON.parse(readFileSync(this.authPath, \"utf-8\"));\n\t\t} catch {\n\t\t\tthis.data = {};\n\t\t}\n\t}\n\n\t/**\n\t * Save credentials to disk.\n\t */\n\tprivate save(): void {\n\t\tconst dir = dirname(this.authPath);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true, mode: 0o700 });\n\t\t}\n\t\twriteFileSync(this.authPath, JSON.stringify(this.data, null, 2), \"utf-8\");\n\t\tchmodSync(this.authPath, 0o600);\n\t}\n\n\t/**\n\t * Get credential for a provider.\n\t */\n\tget(provider: string): AuthCredential | null {\n\t\treturn this.data[provider] ?? null;\n\t}\n\n\t/**\n\t * Set credential for a provider.\n\t */\n\tset(provider: string, credential: AuthCredential): void {\n\t\tthis.data[provider] = credential;\n\t\tthis.save();\n\t}\n\n\t/**\n\t * Remove credential for a provider.\n\t */\n\tremove(provider: string): void {\n\t\tdelete this.data[provider];\n\t\tthis.save();\n\t}\n\n\t/**\n\t * List all providers with credentials.\n\t */\n\tlist(): string[] {\n\t\treturn Object.keys(this.data);\n\t}\n\n\t/**\n\t * Check if credentials exist for a provider.\n\t */\n\thas(provider: string): boolean {\n\t\treturn provider in this.data;\n\t}\n\n\t/**\n\t * Get all credentials (for passing to getOAuthApiKey).\n\t */\n\tgetAll(): AuthStorageData {\n\t\treturn { ...this.data };\n\t}\n\n\t/**\n\t * Login to an OAuth provider.\n\t */\n\tasync login(\n\t\tprovider: OAuthProvider,\n\t\tcallbacks: {\n\t\t\tonAuth: (info: { url: string; instructions?: string }) => void;\n\t\t\tonPrompt: (prompt: { message: string; placeholder?: string }) => Promise<string>;\n\t\t\tonProgress?: (message: string) => void;\n\t\t},\n\t): Promise<void> {\n\t\tlet credentials: OAuthCredentials;\n\n\t\tswitch (provider) {\n\t\t\tcase \"anthropic\":\n\t\t\t\tcredentials = await loginAnthropic(\n\t\t\t\t\t(url) => callbacks.onAuth({ url }),\n\t\t\t\t\t() => callbacks.onPrompt({ message: \"Paste the authorization code:\" }),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"github-copilot\":\n\t\t\t\tcredentials = await loginGitHubCopilot({\n\t\t\t\t\tonAuth: (url, instructions) => callbacks.onAuth({ url, instructions }),\n\t\t\t\t\tonPrompt: callbacks.onPrompt,\n\t\t\t\t\tonProgress: callbacks.onProgress,\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\tcase \"google-gemini-cli\":\n\t\t\t\tcredentials = await loginGeminiCli(callbacks.onAuth, callbacks.onProgress);\n\t\t\t\tbreak;\n\t\t\tcase \"google-antigravity\":\n\t\t\t\tcredentials = await loginAntigravity(callbacks.onAuth, callbacks.onProgress);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tthrow new Error(`Unknown OAuth provider: ${provider}`);\n\t\t}\n\n\t\tthis.set(provider, { type: \"oauth\", ...credentials });\n\t}\n\n\t/**\n\t * Logout from a provider.\n\t */\n\tlogout(provider: string): void {\n\t\tthis.remove(provider);\n\t}\n\n\t/**\n\t * Get API key for a provider.\n\t * Priority:\n\t * 1. Runtime override (CLI --api-key)\n\t * 2. API key from auth.json\n\t * 3. OAuth token from auth.json (auto-refreshed)\n\t * 4. Environment variable\n\t * 5. Fallback resolver (models.json custom providers)\n\t */\n\tasync getApiKey(provider: string): Promise<string | null> {\n\t\t// Runtime override takes highest priority\n\t\tconst runtimeKey = this.runtimeOverrides.get(provider);\n\t\tif (runtimeKey) {\n\t\t\treturn runtimeKey;\n\t\t}\n\n\t\tconst cred = this.data[provider];\n\n\t\tif (cred?.type === \"api_key\") {\n\t\t\treturn cred.key;\n\t\t}\n\n\t\tif (cred?.type === \"oauth\") {\n\t\t\t// Filter to only oauth credentials for getOAuthApiKey\n\t\t\tconst oauthCreds: Record<string, OAuthCredentials> = {};\n\t\t\tfor (const [key, value] of Object.entries(this.data)) {\n\t\t\t\tif (value.type === \"oauth\") {\n\t\t\t\t\toauthCreds[key] = value;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst result = await getOAuthApiKey(provider as OAuthProvider, oauthCreds);\n\t\t\t\tif (result) {\n\t\t\t\t\tthis.data[provider] = { type: \"oauth\", ...result.newCredentials };\n\t\t\t\t\tthis.save();\n\t\t\t\t\treturn result.apiKey;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tthis.remove(provider);\n\t\t\t}\n\t\t}\n\n\t\t// Fall back to environment variable\n\t\tconst envKey = getEnvApiKey(provider);\n\t\tif (envKey) return envKey;\n\n\t\t// Fall back to custom resolver (e.g., models.json custom providers)\n\t\treturn this.fallbackResolver?.(provider) ?? null;\n\t}\n\n\t/**\n\t * Migrate credentials from legacy oauth.json and settings.json apiKeys to auth.json.\n\t * Only runs if auth.json doesn't exist yet. Returns list of migrated providers.\n\t */\n\tstatic migrateLegacy(authPath: string, agentDir: string): string[] {\n\t\tconst oauthPath = join(agentDir, \"oauth.json\");\n\t\tconst settingsPath = join(agentDir, \"settings.json\");\n\n\t\t// Skip if auth.json already exists\n\t\tif (existsSync(authPath)) return [];\n\n\t\tconst migrated: AuthStorageData = {};\n\t\tconst providers: string[] = [];\n\n\t\t// Migrate oauth.json\n\t\tif (existsSync(oauthPath)) {\n\t\t\ttry {\n\t\t\t\tconst oauth = JSON.parse(readFileSync(oauthPath, \"utf-8\"));\n\t\t\t\tfor (const [provider, cred] of Object.entries(oauth)) {\n\t\t\t\t\tmigrated[provider] = { type: \"oauth\", ...(cred as object) } as OAuthCredential;\n\t\t\t\t\tproviders.push(provider);\n\t\t\t\t}\n\t\t\t\trenameSync(oauthPath, `${oauthPath}.migrated`);\n\t\t\t} catch {}\n\t\t}\n\n\t\t// Migrate settings.json apiKeys\n\t\tif (existsSync(settingsPath)) {\n\t\t\ttry {\n\t\t\t\tconst content = readFileSync(settingsPath, \"utf-8\");\n\t\t\t\tconst settings = JSON.parse(content);\n\t\t\t\tif (settings.apiKeys && typeof settings.apiKeys === \"object\") {\n\t\t\t\t\tfor (const [provider, key] of Object.entries(settings.apiKeys)) {\n\t\t\t\t\t\tif (!migrated[provider] && typeof key === \"string\") {\n\t\t\t\t\t\t\tmigrated[provider] = { type: \"api_key\", key };\n\t\t\t\t\t\t\tproviders.push(provider);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tdelete settings.apiKeys;\n\t\t\t\t\twriteFileSync(settingsPath, JSON.stringify(settings, null, 2));\n\t\t\t\t}\n\t\t\t} catch {}\n\t\t}\n\n\t\tif (Object.keys(migrated).length > 0) {\n\t\t\tmkdirSync(dirname(authPath), { recursive: true });\n\t\t\twriteFileSync(authPath, JSON.stringify(migrated, null, 2), { mode: 0o600 });\n\t\t}\n\n\t\treturn providers;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model registry - manages built-in and custom models, provides API key resolution.
|
|
3
|
+
*/
|
|
4
|
+
import { type Api, type Model } from "@mariozechner/pi-ai";
|
|
5
|
+
import type { AuthStorage } from "./auth-storage.js";
|
|
6
|
+
/**
|
|
7
|
+
* Model registry - loads and manages models, resolves API keys via AuthStorage.
|
|
8
|
+
*/
|
|
9
|
+
export declare class ModelRegistry {
|
|
10
|
+
readonly authStorage: AuthStorage;
|
|
11
|
+
private modelsJsonPath;
|
|
12
|
+
private models;
|
|
13
|
+
private customProviderApiKeys;
|
|
14
|
+
private loadError;
|
|
15
|
+
constructor(authStorage: AuthStorage, modelsJsonPath?: string | null);
|
|
16
|
+
/**
|
|
17
|
+
* Reload models from disk (built-in + custom from models.json).
|
|
18
|
+
*/
|
|
19
|
+
refresh(): void;
|
|
20
|
+
/**
|
|
21
|
+
* Get any error from loading models.json (null if no error).
|
|
22
|
+
*/
|
|
23
|
+
getError(): string | null;
|
|
24
|
+
private loadModels;
|
|
25
|
+
private loadCustomModels;
|
|
26
|
+
private validateConfig;
|
|
27
|
+
private parseModels;
|
|
28
|
+
/**
|
|
29
|
+
* Get all models (built-in + custom).
|
|
30
|
+
* If models.json had errors, returns only built-in models.
|
|
31
|
+
*/
|
|
32
|
+
getAll(): Model<Api>[];
|
|
33
|
+
/**
|
|
34
|
+
* Get only models that have valid API keys available.
|
|
35
|
+
*/
|
|
36
|
+
getAvailable(): Promise<Model<Api>[]>;
|
|
37
|
+
/**
|
|
38
|
+
* Find a model by provider and ID.
|
|
39
|
+
*/
|
|
40
|
+
find(provider: string, modelId: string): Model<Api> | null;
|
|
41
|
+
/**
|
|
42
|
+
* Get API key for a model.
|
|
43
|
+
*/
|
|
44
|
+
getApiKey(model: Model<Api>): Promise<string | null>;
|
|
45
|
+
/**
|
|
46
|
+
* Check if a model is using OAuth credentials (subscription).
|
|
47
|
+
*/
|
|
48
|
+
isUsingOAuth(model: Model<Api>): boolean;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=model-registry.d.ts.map
|
|
@@ -0,0 +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;AAI7B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAsErD;;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,CAAuB;IAExC,YACU,WAAW,EAAE,WAAW,EACzB,cAAc,GAAE,MAAM,GAAG,IAAW,EAa5C;IAED;;OAEG;IACH,OAAO,IAAI,IAAI,CAId;IAED;;OAEG;IACH,QAAQ,IAAI,MAAM,GAAG,IAAI,CAExB;IAED,OAAO,CAAC,UAAU;IAmClB,OAAO,CAAC,gBAAgB;IAyCxB,OAAO,CAAC,cAAc;IAuBtB,OAAO,CAAC,WAAW;IA6CnB;;;OAGG;IACH,MAAM,IAAI,KAAK,CAAC,GAAG,CAAC,EAAE,CAErB;IAED;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAS1C;IAED;;OAEG;IACH,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAEzD;IAED;;OAEG;IACG,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAEzD;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 { 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 OpenAICompatSchema = 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\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(\"anthropic-messages\"),\n\t\t\tType.Literal(\"google-generative-ai\"),\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.String({ minLength: 1 }),\n\tapiKey: 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(\"anthropic-messages\"),\n\t\t\tType.Literal(\"google-generative-ai\"),\n\t\t]),\n\t),\n\theaders: Type.Optional(Type.Record(Type.String(), Type.String())),\n\tauthHeader: Type.Optional(Type.Boolean()),\n\tmodels: 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/**\n * Resolve an API key config value to an actual key.\n * Checks environment variable first, then treats as literal.\n */\nfunction resolveApiKeyConfig(keyConfig: string): string | undefined {\n\tconst envValue = process.env[keyConfig];\n\tif (envValue) return envValue;\n\treturn keyConfig;\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 | null = null;\n\n\tconstructor(\n\t\treadonly authStorage: AuthStorage,\n\t\tprivate modelsJsonPath: string | null = null,\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 = null;\n\t\tthis.loadModels();\n\t}\n\n\t/**\n\t * Get any error from loading models.json (null if no error).\n\t */\n\tgetError(): string | null {\n\t\treturn this.loadError;\n\t}\n\n\tprivate loadModels(): void {\n\t\t// Load built-in models\n\t\tconst builtInModels: Model<Api>[] = [];\n\t\tfor (const provider of getProviders()) {\n\t\t\tconst providerModels = getModels(provider as KnownProvider);\n\t\t\tbuiltInModels.push(...(providerModels as Model<Api>[]));\n\t\t}\n\n\t\t// Load custom models from models.json (if path provided)\n\t\tlet customModels: Model<Api>[] = [];\n\t\tif (this.modelsJsonPath) {\n\t\t\tconst result = this.loadCustomModels(this.modelsJsonPath);\n\t\t\tif (result.error) {\n\t\t\t\tthis.loadError = result.error;\n\t\t\t\t// Keep built-in models even if custom models failed to load\n\t\t\t} else {\n\t\t\t\tcustomModels = result.models;\n\t\t\t}\n\t\t}\n\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\tprivate loadCustomModels(modelsJsonPath: string): { models: Model<Api>[]; error: string | null } {\n\t\tif (!existsSync(modelsJsonPath)) {\n\t\t\treturn { models: [], error: null };\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 {\n\t\t\t\t\tmodels: [],\n\t\t\t\t\terror: `Invalid models.json schema:\\n${errors}\\n\\nFile: ${modelsJsonPath}`,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Additional validation\n\t\t\tthis.validateConfig(config);\n\n\t\t\t// Parse models\n\t\t\treturn { models: this.parseModels(config), error: null };\n\t\t} catch (error) {\n\t\t\tif (error instanceof SyntaxError) {\n\t\t\t\treturn {\n\t\t\t\t\tmodels: [],\n\t\t\t\t\terror: `Failed to parse models.json: ${error.message}\\n\\nFile: ${modelsJsonPath}`,\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tmodels: [],\n\t\t\t\terror: `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\n\t\t\tfor (const modelDef of providerConfig.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\t// Store API key config for fallback resolver\n\t\t\tthis.customProviderApiKeys.set(providerName, providerConfig.apiKey);\n\n\t\t\tfor (const modelDef of providerConfig.models) {\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) {\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\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 valid API keys available.\n\t */\n\tasync getAvailable(): Promise<Model<Api>[]> {\n\t\tconst available: Model<Api>[] = [];\n\t\tfor (const model of this.models) {\n\t\t\tconst apiKey = await this.authStorage.getApiKey(model.provider);\n\t\t\tif (apiKey) {\n\t\t\t\tavailable.push(model);\n\t\t\t}\n\t\t}\n\t\treturn available;\n\t}\n\n\t/**\n\t * Find a model by provider and ID.\n\t */\n\tfind(provider: string, modelId: string): Model<Api> | null {\n\t\treturn this.models.find((m) => m.provider === provider && m.id === modelId) ?? null;\n\t}\n\n\t/**\n\t * Get API key for a model.\n\t */\n\tasync getApiKey(model: Model<Api>): Promise<string | null> {\n\t\treturn this.authStorage.getApiKey(model.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"]}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model registry - manages built-in and custom models, provides API key resolution.
|
|
3
|
+
*/
|
|
4
|
+
import { getGitHubCopilotBaseUrl, getModels, getProviders, normalizeDomain, } from "@mariozechner/pi-ai";
|
|
5
|
+
import { Type } from "@sinclair/typebox";
|
|
6
|
+
import AjvModule from "ajv";
|
|
7
|
+
import { existsSync, readFileSync } from "fs";
|
|
8
|
+
const Ajv = AjvModule.default || AjvModule;
|
|
9
|
+
// Schema for OpenAI compatibility settings
|
|
10
|
+
const OpenAICompatSchema = Type.Object({
|
|
11
|
+
supportsStore: Type.Optional(Type.Boolean()),
|
|
12
|
+
supportsDeveloperRole: Type.Optional(Type.Boolean()),
|
|
13
|
+
supportsReasoningEffort: Type.Optional(Type.Boolean()),
|
|
14
|
+
maxTokensField: Type.Optional(Type.Union([Type.Literal("max_completion_tokens"), Type.Literal("max_tokens")])),
|
|
15
|
+
});
|
|
16
|
+
// Schema for custom model definition
|
|
17
|
+
const ModelDefinitionSchema = Type.Object({
|
|
18
|
+
id: Type.String({ minLength: 1 }),
|
|
19
|
+
name: Type.String({ minLength: 1 }),
|
|
20
|
+
api: Type.Optional(Type.Union([
|
|
21
|
+
Type.Literal("openai-completions"),
|
|
22
|
+
Type.Literal("openai-responses"),
|
|
23
|
+
Type.Literal("anthropic-messages"),
|
|
24
|
+
Type.Literal("google-generative-ai"),
|
|
25
|
+
])),
|
|
26
|
+
reasoning: Type.Boolean(),
|
|
27
|
+
input: Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")])),
|
|
28
|
+
cost: Type.Object({
|
|
29
|
+
input: Type.Number(),
|
|
30
|
+
output: Type.Number(),
|
|
31
|
+
cacheRead: Type.Number(),
|
|
32
|
+
cacheWrite: Type.Number(),
|
|
33
|
+
}),
|
|
34
|
+
contextWindow: Type.Number(),
|
|
35
|
+
maxTokens: Type.Number(),
|
|
36
|
+
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
37
|
+
compat: Type.Optional(OpenAICompatSchema),
|
|
38
|
+
});
|
|
39
|
+
const ProviderConfigSchema = Type.Object({
|
|
40
|
+
baseUrl: Type.String({ minLength: 1 }),
|
|
41
|
+
apiKey: Type.String({ minLength: 1 }),
|
|
42
|
+
api: Type.Optional(Type.Union([
|
|
43
|
+
Type.Literal("openai-completions"),
|
|
44
|
+
Type.Literal("openai-responses"),
|
|
45
|
+
Type.Literal("anthropic-messages"),
|
|
46
|
+
Type.Literal("google-generative-ai"),
|
|
47
|
+
])),
|
|
48
|
+
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
49
|
+
authHeader: Type.Optional(Type.Boolean()),
|
|
50
|
+
models: Type.Array(ModelDefinitionSchema),
|
|
51
|
+
});
|
|
52
|
+
const ModelsConfigSchema = Type.Object({
|
|
53
|
+
providers: Type.Record(Type.String(), ProviderConfigSchema),
|
|
54
|
+
});
|
|
55
|
+
/**
|
|
56
|
+
* Resolve an API key config value to an actual key.
|
|
57
|
+
* Checks environment variable first, then treats as literal.
|
|
58
|
+
*/
|
|
59
|
+
function resolveApiKeyConfig(keyConfig) {
|
|
60
|
+
const envValue = process.env[keyConfig];
|
|
61
|
+
if (envValue)
|
|
62
|
+
return envValue;
|
|
63
|
+
return keyConfig;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Model registry - loads and manages models, resolves API keys via AuthStorage.
|
|
67
|
+
*/
|
|
68
|
+
export class ModelRegistry {
|
|
69
|
+
authStorage;
|
|
70
|
+
modelsJsonPath;
|
|
71
|
+
models = [];
|
|
72
|
+
customProviderApiKeys = new Map();
|
|
73
|
+
loadError = null;
|
|
74
|
+
constructor(authStorage, modelsJsonPath = null) {
|
|
75
|
+
this.authStorage = authStorage;
|
|
76
|
+
this.modelsJsonPath = modelsJsonPath;
|
|
77
|
+
// Set up fallback resolver for custom provider API keys
|
|
78
|
+
this.authStorage.setFallbackResolver((provider) => {
|
|
79
|
+
const keyConfig = this.customProviderApiKeys.get(provider);
|
|
80
|
+
if (keyConfig) {
|
|
81
|
+
return resolveApiKeyConfig(keyConfig);
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
});
|
|
85
|
+
// Load models
|
|
86
|
+
this.loadModels();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Reload models from disk (built-in + custom from models.json).
|
|
90
|
+
*/
|
|
91
|
+
refresh() {
|
|
92
|
+
this.customProviderApiKeys.clear();
|
|
93
|
+
this.loadError = null;
|
|
94
|
+
this.loadModels();
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Get any error from loading models.json (null if no error).
|
|
98
|
+
*/
|
|
99
|
+
getError() {
|
|
100
|
+
return this.loadError;
|
|
101
|
+
}
|
|
102
|
+
loadModels() {
|
|
103
|
+
// Load built-in models
|
|
104
|
+
const builtInModels = [];
|
|
105
|
+
for (const provider of getProviders()) {
|
|
106
|
+
const providerModels = getModels(provider);
|
|
107
|
+
builtInModels.push(...providerModels);
|
|
108
|
+
}
|
|
109
|
+
// Load custom models from models.json (if path provided)
|
|
110
|
+
let customModels = [];
|
|
111
|
+
if (this.modelsJsonPath) {
|
|
112
|
+
const result = this.loadCustomModels(this.modelsJsonPath);
|
|
113
|
+
if (result.error) {
|
|
114
|
+
this.loadError = result.error;
|
|
115
|
+
// Keep built-in models even if custom models failed to load
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
customModels = result.models;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const combined = [...builtInModels, ...customModels];
|
|
122
|
+
// Update github-copilot base URL based on OAuth credentials
|
|
123
|
+
const copilotCred = this.authStorage.get("github-copilot");
|
|
124
|
+
if (copilotCred?.type === "oauth") {
|
|
125
|
+
const domain = copilotCred.enterpriseUrl
|
|
126
|
+
? (normalizeDomain(copilotCred.enterpriseUrl) ?? undefined)
|
|
127
|
+
: undefined;
|
|
128
|
+
const baseUrl = getGitHubCopilotBaseUrl(copilotCred.access, domain);
|
|
129
|
+
this.models = combined.map((m) => (m.provider === "github-copilot" ? { ...m, baseUrl } : m));
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
this.models = combined;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
loadCustomModels(modelsJsonPath) {
|
|
136
|
+
if (!existsSync(modelsJsonPath)) {
|
|
137
|
+
return { models: [], error: null };
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const content = readFileSync(modelsJsonPath, "utf-8");
|
|
141
|
+
const config = JSON.parse(content);
|
|
142
|
+
// Validate schema
|
|
143
|
+
const ajv = new Ajv();
|
|
144
|
+
const validate = ajv.compile(ModelsConfigSchema);
|
|
145
|
+
if (!validate(config)) {
|
|
146
|
+
const errors = validate.errors?.map((e) => ` - ${e.instancePath || "root"}: ${e.message}`).join("\n") ||
|
|
147
|
+
"Unknown schema error";
|
|
148
|
+
return {
|
|
149
|
+
models: [],
|
|
150
|
+
error: `Invalid models.json schema:\n${errors}\n\nFile: ${modelsJsonPath}`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
// Additional validation
|
|
154
|
+
this.validateConfig(config);
|
|
155
|
+
// Parse models
|
|
156
|
+
return { models: this.parseModels(config), error: null };
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
if (error instanceof SyntaxError) {
|
|
160
|
+
return {
|
|
161
|
+
models: [],
|
|
162
|
+
error: `Failed to parse models.json: ${error.message}\n\nFile: ${modelsJsonPath}`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
models: [],
|
|
167
|
+
error: `Failed to load models.json: ${error instanceof Error ? error.message : error}\n\nFile: ${modelsJsonPath}`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
validateConfig(config) {
|
|
172
|
+
for (const [providerName, providerConfig] of Object.entries(config.providers)) {
|
|
173
|
+
const hasProviderApi = !!providerConfig.api;
|
|
174
|
+
for (const modelDef of providerConfig.models) {
|
|
175
|
+
const hasModelApi = !!modelDef.api;
|
|
176
|
+
if (!hasProviderApi && !hasModelApi) {
|
|
177
|
+
throw new Error(`Provider ${providerName}, model ${modelDef.id}: no "api" specified. Set at provider or model level.`);
|
|
178
|
+
}
|
|
179
|
+
if (!modelDef.id)
|
|
180
|
+
throw new Error(`Provider ${providerName}: model missing "id"`);
|
|
181
|
+
if (!modelDef.name)
|
|
182
|
+
throw new Error(`Provider ${providerName}: model missing "name"`);
|
|
183
|
+
if (modelDef.contextWindow <= 0)
|
|
184
|
+
throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`);
|
|
185
|
+
if (modelDef.maxTokens <= 0)
|
|
186
|
+
throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
parseModels(config) {
|
|
191
|
+
const models = [];
|
|
192
|
+
for (const [providerName, providerConfig] of Object.entries(config.providers)) {
|
|
193
|
+
// Store API key config for fallback resolver
|
|
194
|
+
this.customProviderApiKeys.set(providerName, providerConfig.apiKey);
|
|
195
|
+
for (const modelDef of providerConfig.models) {
|
|
196
|
+
const api = modelDef.api || providerConfig.api;
|
|
197
|
+
if (!api)
|
|
198
|
+
continue;
|
|
199
|
+
// Merge headers: provider headers are base, model headers override
|
|
200
|
+
let headers = providerConfig.headers || modelDef.headers
|
|
201
|
+
? { ...providerConfig.headers, ...modelDef.headers }
|
|
202
|
+
: undefined;
|
|
203
|
+
// If authHeader is true, add Authorization header with resolved API key
|
|
204
|
+
if (providerConfig.authHeader) {
|
|
205
|
+
const resolvedKey = resolveApiKeyConfig(providerConfig.apiKey);
|
|
206
|
+
if (resolvedKey) {
|
|
207
|
+
headers = { ...headers, Authorization: `Bearer ${resolvedKey}` };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
models.push({
|
|
211
|
+
id: modelDef.id,
|
|
212
|
+
name: modelDef.name,
|
|
213
|
+
api: api,
|
|
214
|
+
provider: providerName,
|
|
215
|
+
baseUrl: providerConfig.baseUrl,
|
|
216
|
+
reasoning: modelDef.reasoning,
|
|
217
|
+
input: modelDef.input,
|
|
218
|
+
cost: modelDef.cost,
|
|
219
|
+
contextWindow: modelDef.contextWindow,
|
|
220
|
+
maxTokens: modelDef.maxTokens,
|
|
221
|
+
headers,
|
|
222
|
+
compat: modelDef.compat,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return models;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Get all models (built-in + custom).
|
|
230
|
+
* If models.json had errors, returns only built-in models.
|
|
231
|
+
*/
|
|
232
|
+
getAll() {
|
|
233
|
+
return this.models;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Get only models that have valid API keys available.
|
|
237
|
+
*/
|
|
238
|
+
async getAvailable() {
|
|
239
|
+
const available = [];
|
|
240
|
+
for (const model of this.models) {
|
|
241
|
+
const apiKey = await this.authStorage.getApiKey(model.provider);
|
|
242
|
+
if (apiKey) {
|
|
243
|
+
available.push(model);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return available;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Find a model by provider and ID.
|
|
250
|
+
*/
|
|
251
|
+
find(provider, modelId) {
|
|
252
|
+
return this.models.find((m) => m.provider === provider && m.id === modelId) ?? null;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Get API key for a model.
|
|
256
|
+
*/
|
|
257
|
+
async getApiKey(model) {
|
|
258
|
+
return this.authStorage.getApiKey(model.provider);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Check if a model is using OAuth credentials (subscription).
|
|
262
|
+
*/
|
|
263
|
+
isUsingOAuth(model) {
|
|
264
|
+
const cred = this.authStorage.get(model.provider);
|
|
265
|
+
return cred?.type === "oauth";
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
//# sourceMappingURL=model-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-registry.js","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\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 { 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 OpenAICompatSchema = 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\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(\"anthropic-messages\"),\n\t\t\tType.Literal(\"google-generative-ai\"),\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.String({ minLength: 1 }),\n\tapiKey: 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(\"anthropic-messages\"),\n\t\t\tType.Literal(\"google-generative-ai\"),\n\t\t]),\n\t),\n\theaders: Type.Optional(Type.Record(Type.String(), Type.String())),\n\tauthHeader: Type.Optional(Type.Boolean()),\n\tmodels: 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/**\n * Resolve an API key config value to an actual key.\n * Checks environment variable first, then treats as literal.\n */\nfunction resolveApiKeyConfig(keyConfig: string): string | undefined {\n\tconst envValue = process.env[keyConfig];\n\tif (envValue) return envValue;\n\treturn keyConfig;\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 | null = null;\n\n\tconstructor(\n\t\treadonly authStorage: AuthStorage,\n\t\tprivate modelsJsonPath: string | null = null,\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 = null;\n\t\tthis.loadModels();\n\t}\n\n\t/**\n\t * Get any error from loading models.json (null if no error).\n\t */\n\tgetError(): string | null {\n\t\treturn this.loadError;\n\t}\n\n\tprivate loadModels(): void {\n\t\t// Load built-in models\n\t\tconst builtInModels: Model<Api>[] = [];\n\t\tfor (const provider of getProviders()) {\n\t\t\tconst providerModels = getModels(provider as KnownProvider);\n\t\t\tbuiltInModels.push(...(providerModels as Model<Api>[]));\n\t\t}\n\n\t\t// Load custom models from models.json (if path provided)\n\t\tlet customModels: Model<Api>[] = [];\n\t\tif (this.modelsJsonPath) {\n\t\t\tconst result = this.loadCustomModels(this.modelsJsonPath);\n\t\t\tif (result.error) {\n\t\t\t\tthis.loadError = result.error;\n\t\t\t\t// Keep built-in models even if custom models failed to load\n\t\t\t} else {\n\t\t\t\tcustomModels = result.models;\n\t\t\t}\n\t\t}\n\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\tprivate loadCustomModels(modelsJsonPath: string): { models: Model<Api>[]; error: string | null } {\n\t\tif (!existsSync(modelsJsonPath)) {\n\t\t\treturn { models: [], error: null };\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 {\n\t\t\t\t\tmodels: [],\n\t\t\t\t\terror: `Invalid models.json schema:\\n${errors}\\n\\nFile: ${modelsJsonPath}`,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Additional validation\n\t\t\tthis.validateConfig(config);\n\n\t\t\t// Parse models\n\t\t\treturn { models: this.parseModels(config), error: null };\n\t\t} catch (error) {\n\t\t\tif (error instanceof SyntaxError) {\n\t\t\t\treturn {\n\t\t\t\t\tmodels: [],\n\t\t\t\t\terror: `Failed to parse models.json: ${error.message}\\n\\nFile: ${modelsJsonPath}`,\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tmodels: [],\n\t\t\t\terror: `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\n\t\t\tfor (const modelDef of providerConfig.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\t// Store API key config for fallback resolver\n\t\t\tthis.customProviderApiKeys.set(providerName, providerConfig.apiKey);\n\n\t\t\tfor (const modelDef of providerConfig.models) {\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) {\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\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 valid API keys available.\n\t */\n\tasync getAvailable(): Promise<Model<Api>[]> {\n\t\tconst available: Model<Api>[] = [];\n\t\tfor (const model of this.models) {\n\t\t\tconst apiKey = await this.authStorage.getApiKey(model.provider);\n\t\t\tif (apiKey) {\n\t\t\t\tavailable.push(model);\n\t\t\t}\n\t\t}\n\t\treturn available;\n\t}\n\n\t/**\n\t * Find a model by provider and ID.\n\t */\n\tfind(provider: string, modelId: string): Model<Api> | null {\n\t\treturn this.models.find((m) => m.provider === provider && m.id === modelId) ?? null;\n\t}\n\n\t/**\n\t * Get API key for a model.\n\t */\n\tasync getApiKey(model: Model<Api>): Promise<string | null> {\n\t\treturn this.authStorage.getApiKey(model.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"]}
|