@mariozechner/pi-coding-agent 0.27.9 → 0.29.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 +37 -1
- package/README.md +17 -18
- package/dist/cli/list-models.d.ts +2 -2
- 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 +23 -25
- 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/custom-tools/types.d.ts +2 -2
- package/dist/core/custom-tools/types.d.ts.map +1 -1
- package/dist/core/custom-tools/types.js.map +1 -1
- package/dist/core/hooks/types.d.ts +3 -3
- package/dist/core/hooks/types.d.ts.map +1 -1
- package/dist/core/hooks/types.js.map +1 -1
- 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 -7
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +12 -44
- 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 -22
- 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 -14
- 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 +56 -51
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/custom-tools.md +3 -3
- package/docs/hooks.md +9 -9
- 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 +6 -8
- package/examples/hooks/custom-compaction.ts +7 -7
- package/examples/hooks/dirty-repo-guard.ts +7 -15
- 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 -58
- package/dist/core/model-config.d.ts.map +0 -1
- package/dist/core/model-config.js +0 -384
- 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,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"]}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
|
|
5
5
|
import type { Api, KnownProvider, Model } from "@mariozechner/pi-ai";
|
|
6
|
-
import type {
|
|
6
|
+
import type { ModelRegistry } from "./model-registry.js";
|
|
7
7
|
/** Default model IDs for each known provider */
|
|
8
8
|
export declare const defaultModelPerProvider: Record<KnownProvider, string>;
|
|
9
9
|
export interface ScopedModel {
|
|
@@ -39,11 +39,8 @@ export declare function parseModelPattern(pattern: string, availableModels: Mode
|
|
|
39
39
|
* Supports models with colons in their IDs (e.g., OpenRouter's model:exacto).
|
|
40
40
|
* The algorithm tries to match the full pattern first, then progressively
|
|
41
41
|
* strips colon-suffixes to find a match.
|
|
42
|
-
*
|
|
43
|
-
* @param patterns - Model patterns to resolve
|
|
44
|
-
* @param settingsManager - Optional settings manager for API key fallback from settings.json
|
|
45
42
|
*/
|
|
46
|
-
export declare function resolveModelScope(patterns: string[],
|
|
43
|
+
export declare function resolveModelScope(patterns: string[], modelRegistry: ModelRegistry): Promise<ScopedModel[]>;
|
|
47
44
|
export interface InitialModelResult {
|
|
48
45
|
model: Model<Api> | null;
|
|
49
46
|
thinkingLevel: ThinkingLevel;
|
|
@@ -62,12 +59,15 @@ export declare function findInitialModel(options: {
|
|
|
62
59
|
cliModel?: string;
|
|
63
60
|
scopedModels: ScopedModel[];
|
|
64
61
|
isContinuing: boolean;
|
|
65
|
-
|
|
62
|
+
defaultProvider?: string;
|
|
63
|
+
defaultModelId?: string;
|
|
64
|
+
defaultThinkingLevel?: ThinkingLevel;
|
|
65
|
+
modelRegistry: ModelRegistry;
|
|
66
66
|
}): Promise<InitialModelResult>;
|
|
67
67
|
/**
|
|
68
68
|
* Restore model from session, with fallback to available models
|
|
69
69
|
*/
|
|
70
|
-
export declare function restoreModelFromSession(savedProvider: string, savedModelId: string, currentModel: Model<Api> | null, shouldPrintMessages: boolean,
|
|
70
|
+
export declare function restoreModelFromSession(savedProvider: string, savedModelId: string, currentModel: Model<Api> | null, shouldPrintMessages: boolean, modelRegistry: ModelRegistry): Promise<{
|
|
71
71
|
model: Model<Api> | null;
|
|
72
72
|
fallbackMessage: string | null;
|
|
73
73
|
}>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-resolver.d.ts","sourceRoot":"","sources":["../../src/core/model-resolver.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,GAAG,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAIrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D,gDAAgD;AAChD,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,CAajE,CAAC;AAEF,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,aAAa,EAAE,aAAa,CAAC;CAC7B;AAkED,MAAM,WAAW,iBAAiB;IACjC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;IACzB,aAAa,EAAE,aAAa,CAAC;IAC7B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,iBAAiB,CA0CnG;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,eAAe,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAgCrH;AAED,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;IACzB,aAAa,EAAE,aAAa,CAAC;IAC7B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,YAAY,EAAE,OAAO,CAAC;IACtB,eAAe,EAAE,eAAe,CAAC;CACjC,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA2E9B;AAED;;GAEG;AACH,wBAAsB,uBAAuB,CAC5C,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,EAC/B,mBAAmB,EAAE,OAAO,EAC5B,eAAe,CAAC,EAAE,eAAe,GAC/B,OAAO,CAAC;IAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;IAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CA2EvE","sourcesContent":["/**\n * Model resolution, scoping, and initial selection\n */\n\nimport type { ThinkingLevel } from \"@mariozechner/pi-agent-core\";\nimport type { Api, KnownProvider, Model } from \"@mariozechner/pi-ai\";\nimport chalk from \"chalk\";\nimport { isValidThinkingLevel } from \"../cli/args.js\";\nimport { findModel, getApiKeyForModel, getAvailableModels } from \"./model-config.js\";\nimport type { SettingsManager } from \"./settings-manager.js\";\n\n/** Default model IDs for each known provider */\nexport const defaultModelPerProvider: Record<KnownProvider, string> = {\n\tanthropic: \"claude-sonnet-4-5\",\n\topenai: \"gpt-5.1-codex\",\n\tgoogle: \"gemini-2.5-pro\",\n\t\"google-gemini-cli\": \"gemini-2.5-pro\",\n\t\"google-antigravity\": \"gemini-3-pro-high\",\n\t\"github-copilot\": \"gpt-4o\",\n\topenrouter: \"openai/gpt-5.1-codex\",\n\txai: \"grok-4-fast-non-reasoning\",\n\tgroq: \"openai/gpt-oss-120b\",\n\tcerebras: \"zai-glm-4.6\",\n\tzai: \"glm-4.6\",\n\tmistral: \"devstral-medium-latest\",\n};\n\nexport interface ScopedModel {\n\tmodel: Model<Api>;\n\tthinkingLevel: ThinkingLevel;\n}\n\n/**\n * Helper to check if a model ID looks like an alias (no date suffix)\n * Dates are typically in format: -20241022 or -20250929\n */\nfunction isAlias(id: string): boolean {\n\t// Check if ID ends with -latest\n\tif (id.endsWith(\"-latest\")) return true;\n\n\t// Check if ID ends with a date pattern (-YYYYMMDD)\n\tconst datePattern = /-\\d{8}$/;\n\treturn !datePattern.test(id);\n}\n\n/**\n * Try to match a pattern to a model from the available models list.\n * Returns the matched model or null if no match found.\n */\nfunction tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Model<Api> | null {\n\t// Check for provider/modelId format (provider is everything before the first /)\n\tconst slashIndex = modelPattern.indexOf(\"/\");\n\tif (slashIndex !== -1) {\n\t\tconst provider = modelPattern.substring(0, slashIndex);\n\t\tconst modelId = modelPattern.substring(slashIndex + 1);\n\t\tconst providerMatch = availableModels.find(\n\t\t\t(m) => m.provider.toLowerCase() === provider.toLowerCase() && m.id.toLowerCase() === modelId.toLowerCase(),\n\t\t);\n\t\tif (providerMatch) {\n\t\t\treturn providerMatch;\n\t\t}\n\t\t// No exact provider/model match - fall through to other matching\n\t}\n\n\t// Check for exact ID match (case-insensitive)\n\tconst exactMatch = availableModels.find((m) => m.id.toLowerCase() === modelPattern.toLowerCase());\n\tif (exactMatch) {\n\t\treturn exactMatch;\n\t}\n\n\t// No exact match - fall back to partial matching\n\tconst matches = availableModels.filter(\n\t\t(m) =>\n\t\t\tm.id.toLowerCase().includes(modelPattern.toLowerCase()) ||\n\t\t\tm.name?.toLowerCase().includes(modelPattern.toLowerCase()),\n\t);\n\n\tif (matches.length === 0) {\n\t\treturn null;\n\t}\n\n\t// Separate into aliases and dated versions\n\tconst aliases = matches.filter((m) => isAlias(m.id));\n\tconst datedVersions = matches.filter((m) => !isAlias(m.id));\n\n\tif (aliases.length > 0) {\n\t\t// Prefer alias - if multiple aliases, pick the one that sorts highest\n\t\taliases.sort((a, b) => b.id.localeCompare(a.id));\n\t\treturn aliases[0];\n\t} else {\n\t\t// No alias found, pick latest dated version\n\t\tdatedVersions.sort((a, b) => b.id.localeCompare(a.id));\n\t\treturn datedVersions[0];\n\t}\n}\n\nexport interface ParsedModelResult {\n\tmodel: Model<Api> | null;\n\tthinkingLevel: ThinkingLevel;\n\twarning: string | null;\n}\n\n/**\n * Parse a pattern to extract model and thinking level.\n * Handles models with colons in their IDs (e.g., OpenRouter's :exacto suffix).\n *\n * Algorithm:\n * 1. Try to match full pattern as a model\n * 2. If found, return it with \"off\" thinking level\n * 3. If not found and has colons, split on last colon:\n * - If suffix is valid thinking level, use it and recurse on prefix\n * - If suffix is invalid, warn and recurse on prefix with \"off\"\n *\n * @internal Exported for testing\n */\nexport function parseModelPattern(pattern: string, availableModels: Model<Api>[]): ParsedModelResult {\n\t// Try exact match first\n\tconst exactMatch = tryMatchModel(pattern, availableModels);\n\tif (exactMatch) {\n\t\treturn { model: exactMatch, thinkingLevel: \"off\", warning: null };\n\t}\n\n\t// No match - try splitting on last colon if present\n\tconst lastColonIndex = pattern.lastIndexOf(\":\");\n\tif (lastColonIndex === -1) {\n\t\t// No colons, pattern simply doesn't match any model\n\t\treturn { model: null, thinkingLevel: \"off\", warning: null };\n\t}\n\n\tconst prefix = pattern.substring(0, lastColonIndex);\n\tconst suffix = pattern.substring(lastColonIndex + 1);\n\n\tif (isValidThinkingLevel(suffix)) {\n\t\t// Valid thinking level - recurse on prefix and use this level\n\t\tconst result = parseModelPattern(prefix, availableModels);\n\t\tif (result.model) {\n\t\t\t// Only use this thinking level if no warning from inner recursion\n\t\t\t// (if there was an invalid suffix deeper, we already have \"off\")\n\t\t\treturn {\n\t\t\t\tmodel: result.model,\n\t\t\t\tthinkingLevel: result.warning ? \"off\" : suffix,\n\t\t\t\twarning: result.warning,\n\t\t\t};\n\t\t}\n\t\treturn result;\n\t} else {\n\t\t// Invalid suffix - recurse on prefix with \"off\" and warn\n\t\tconst result = parseModelPattern(prefix, availableModels);\n\t\tif (result.model) {\n\t\t\treturn {\n\t\t\t\tmodel: result.model,\n\t\t\t\tthinkingLevel: \"off\",\n\t\t\t\twarning: `Invalid thinking level \"${suffix}\" in pattern \"${pattern}\". Using \"off\" instead.`,\n\t\t\t};\n\t\t}\n\t\treturn result;\n\t}\n}\n\n/**\n * Resolve model patterns to actual Model objects with optional thinking levels\n * Format: \"pattern:level\" where :level is optional\n * For each pattern, finds all matching models and picks the best version:\n * 1. Prefer alias (e.g., claude-sonnet-4-5) over dated versions (claude-sonnet-4-5-20250929)\n * 2. If no alias, pick the latest dated version\n *\n * Supports models with colons in their IDs (e.g., OpenRouter's model:exacto).\n * The algorithm tries to match the full pattern first, then progressively\n * strips colon-suffixes to find a match.\n *\n * @param patterns - Model patterns to resolve\n * @param settingsManager - Optional settings manager for API key fallback from settings.json\n */\nexport async function resolveModelScope(patterns: string[], settingsManager?: SettingsManager): Promise<ScopedModel[]> {\n\tconst { models: availableModels, error } = await getAvailableModels(\n\t\tundefined,\n\t\tsettingsManager ? (provider) => settingsManager.getApiKey(provider) : undefined,\n\t);\n\n\tif (error) {\n\t\tconsole.warn(chalk.yellow(`Warning: Error loading models: ${error}`));\n\t\treturn [];\n\t}\n\n\tconst scopedModels: ScopedModel[] = [];\n\n\tfor (const pattern of patterns) {\n\t\tconst { model, thinkingLevel, warning } = parseModelPattern(pattern, availableModels);\n\n\t\tif (warning) {\n\t\t\tconsole.warn(chalk.yellow(`Warning: ${warning}`));\n\t\t}\n\n\t\tif (!model) {\n\t\t\tconsole.warn(chalk.yellow(`Warning: No models match pattern \"${pattern}\"`));\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Avoid duplicates\n\t\tif (!scopedModels.find((sm) => sm.model.id === model.id && sm.model.provider === model.provider)) {\n\t\t\tscopedModels.push({ model, thinkingLevel });\n\t\t}\n\t}\n\n\treturn scopedModels;\n}\n\nexport interface InitialModelResult {\n\tmodel: Model<Api> | null;\n\tthinkingLevel: ThinkingLevel;\n\tfallbackMessage: string | null;\n}\n\n/**\n * Find the initial model to use based on priority:\n * 1. CLI args (provider + model)\n * 2. First model from scoped models (if not continuing/resuming)\n * 3. Restored from session (if continuing/resuming)\n * 4. Saved default from settings\n * 5. First available model with valid API key\n */\nexport async function findInitialModel(options: {\n\tcliProvider?: string;\n\tcliModel?: string;\n\tscopedModels: ScopedModel[];\n\tisContinuing: boolean;\n\tsettingsManager: SettingsManager;\n}): Promise<InitialModelResult> {\n\tconst { cliProvider, cliModel, scopedModels, isContinuing, settingsManager } = options;\n\n\tlet model: Model<Api> | null = null;\n\tlet thinkingLevel: ThinkingLevel = \"off\";\n\n\t// 1. CLI args take priority\n\tif (cliProvider && cliModel) {\n\t\tconst { model: found, error } = findModel(cliProvider, cliModel);\n\t\tif (error) {\n\t\t\tconsole.error(chalk.red(error));\n\t\t\tprocess.exit(1);\n\t\t}\n\t\tif (!found) {\n\t\t\tconsole.error(chalk.red(`Model ${cliProvider}/${cliModel} not found`));\n\t\t\tprocess.exit(1);\n\t\t}\n\t\treturn { model: found, thinkingLevel: \"off\", fallbackMessage: null };\n\t}\n\n\t// 2. Use first model from scoped models (skip if continuing/resuming)\n\tif (scopedModels.length > 0 && !isContinuing) {\n\t\treturn {\n\t\t\tmodel: scopedModels[0].model,\n\t\t\tthinkingLevel: scopedModels[0].thinkingLevel,\n\t\t\tfallbackMessage: null,\n\t\t};\n\t}\n\n\t// 3. Try saved default from settings\n\tconst defaultProvider = settingsManager.getDefaultProvider();\n\tconst defaultModelId = settingsManager.getDefaultModel();\n\tif (defaultProvider && defaultModelId) {\n\t\tconst { model: found, error } = findModel(defaultProvider, defaultModelId);\n\t\tif (error) {\n\t\t\tconsole.error(chalk.red(error));\n\t\t\tprocess.exit(1);\n\t\t}\n\t\tif (found) {\n\t\t\tmodel = found;\n\t\t\t// Also load saved thinking level\n\t\t\tconst savedThinking = settingsManager.getDefaultThinkingLevel();\n\t\t\tif (savedThinking) {\n\t\t\t\tthinkingLevel = savedThinking;\n\t\t\t}\n\t\t\treturn { model, thinkingLevel, fallbackMessage: null };\n\t\t}\n\t}\n\n\t// 4. Try first available model with valid API key\n\tconst { models: availableModels, error } = await getAvailableModels(undefined, (provider) =>\n\t\tsettingsManager.getApiKey(provider),\n\t);\n\n\tif (error) {\n\t\tconsole.error(chalk.red(error));\n\t\tprocess.exit(1);\n\t}\n\n\tif (availableModels.length > 0) {\n\t\t// Try to find a default model from known providers\n\t\tfor (const provider of Object.keys(defaultModelPerProvider) as KnownProvider[]) {\n\t\t\tconst defaultId = defaultModelPerProvider[provider];\n\t\t\tconst match = availableModels.find((m) => m.provider === provider && m.id === defaultId);\n\t\t\tif (match) {\n\t\t\t\treturn { model: match, thinkingLevel: \"off\", fallbackMessage: null };\n\t\t\t}\n\t\t}\n\n\t\t// If no default found, use first available\n\t\treturn { model: availableModels[0], thinkingLevel: \"off\", fallbackMessage: null };\n\t}\n\n\t// 5. No model found\n\treturn { model: null, thinkingLevel: \"off\", fallbackMessage: null };\n}\n\n/**\n * Restore model from session, with fallback to available models\n */\nexport async function restoreModelFromSession(\n\tsavedProvider: string,\n\tsavedModelId: string,\n\tcurrentModel: Model<Api> | null,\n\tshouldPrintMessages: boolean,\n\tsettingsManager?: SettingsManager,\n): Promise<{ model: Model<Api> | null; fallbackMessage: string | null }> {\n\tconst { model: restoredModel, error } = findModel(savedProvider, savedModelId);\n\n\tif (error) {\n\t\tconsole.error(chalk.red(error));\n\t\tprocess.exit(1);\n\t}\n\n\t// Check if restored model exists and has a valid API key\n\tconst hasApiKey = restoredModel ? !!(await getApiKeyForModel(restoredModel)) : false;\n\n\tif (restoredModel && hasApiKey) {\n\t\tif (shouldPrintMessages) {\n\t\t\tconsole.log(chalk.dim(`Restored model: ${savedProvider}/${savedModelId}`));\n\t\t}\n\t\treturn { model: restoredModel, fallbackMessage: null };\n\t}\n\n\t// Model not found or no API key - fall back\n\tconst reason = !restoredModel ? \"model no longer exists\" : \"no API key available\";\n\n\tif (shouldPrintMessages) {\n\t\tconsole.error(chalk.yellow(`Warning: Could not restore model ${savedProvider}/${savedModelId} (${reason}).`));\n\t}\n\n\t// If we already have a model, use it as fallback\n\tif (currentModel) {\n\t\tif (shouldPrintMessages) {\n\t\t\tconsole.log(chalk.dim(`Falling back to: ${currentModel.provider}/${currentModel.id}`));\n\t\t}\n\t\treturn {\n\t\t\tmodel: currentModel,\n\t\t\tfallbackMessage: `Could not restore model ${savedProvider}/${savedModelId} (${reason}). Using ${currentModel.provider}/${currentModel.id}.`,\n\t\t};\n\t}\n\n\t// Try to find any available model\n\tconst { models: availableModels, error: availableError } = await getAvailableModels(\n\t\tundefined,\n\t\tsettingsManager ? (provider) => settingsManager.getApiKey(provider) : undefined,\n\t);\n\tif (availableError) {\n\t\tconsole.error(chalk.red(availableError));\n\t\tprocess.exit(1);\n\t}\n\n\tif (availableModels.length > 0) {\n\t\t// Try to find a default model from known providers\n\t\tlet fallbackModel: Model<Api> | null = null;\n\t\tfor (const provider of Object.keys(defaultModelPerProvider) as KnownProvider[]) {\n\t\t\tconst defaultId = defaultModelPerProvider[provider];\n\t\t\tconst match = availableModels.find((m) => m.provider === provider && m.id === defaultId);\n\t\t\tif (match) {\n\t\t\t\tfallbackModel = match;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\t// If no default found, use first available\n\t\tif (!fallbackModel) {\n\t\t\tfallbackModel = availableModels[0];\n\t\t}\n\n\t\tif (shouldPrintMessages) {\n\t\t\tconsole.log(chalk.dim(`Falling back to: ${fallbackModel.provider}/${fallbackModel.id}`));\n\t\t}\n\n\t\treturn {\n\t\t\tmodel: fallbackModel,\n\t\t\tfallbackMessage: `Could not restore model ${savedProvider}/${savedModelId} (${reason}). Using ${fallbackModel.provider}/${fallbackModel.id}.`,\n\t\t};\n\t}\n\n\t// No models available\n\treturn { model: null, fallbackMessage: null };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"model-resolver.d.ts","sourceRoot":"","sources":["../../src/core/model-resolver.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,GAAG,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAGrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,gDAAgD;AAChD,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,CAajE,CAAC;AAEF,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,aAAa,EAAE,aAAa,CAAC;CAC7B;AAkED,MAAM,WAAW,iBAAiB;IACjC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;IACzB,aAAa,EAAE,aAAa,CAAC;IAC7B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,iBAAiB,CA0CnG;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,aAAa,EAAE,aAAa,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAuBhH;AAED,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;IACzB,aAAa,EAAE,aAAa,CAAC;IAC7B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,YAAY,EAAE,OAAO,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE,aAAa,CAAC;IACrC,aAAa,EAAE,aAAa,CAAC;CAC7B,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAiE9B;AAED;;GAEG;AACH,wBAAsB,uBAAuB,CAC5C,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,EAC/B,mBAAmB,EAAE,OAAO,EAC5B,aAAa,EAAE,aAAa,GAC1B,OAAO,CAAC;IAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;IAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CA+DvE","sourcesContent":["/**\n * Model resolution, scoping, and initial selection\n */\n\nimport type { ThinkingLevel } from \"@mariozechner/pi-agent-core\";\nimport type { Api, KnownProvider, Model } from \"@mariozechner/pi-ai\";\nimport chalk from \"chalk\";\nimport { isValidThinkingLevel } from \"../cli/args.js\";\nimport type { ModelRegistry } from \"./model-registry.js\";\n\n/** Default model IDs for each known provider */\nexport const defaultModelPerProvider: Record<KnownProvider, string> = {\n\tanthropic: \"claude-sonnet-4-5\",\n\topenai: \"gpt-5.1-codex\",\n\tgoogle: \"gemini-2.5-pro\",\n\t\"google-gemini-cli\": \"gemini-2.5-pro\",\n\t\"google-antigravity\": \"gemini-3-pro-high\",\n\t\"github-copilot\": \"gpt-4o\",\n\topenrouter: \"openai/gpt-5.1-codex\",\n\txai: \"grok-4-fast-non-reasoning\",\n\tgroq: \"openai/gpt-oss-120b\",\n\tcerebras: \"zai-glm-4.6\",\n\tzai: \"glm-4.6\",\n\tmistral: \"devstral-medium-latest\",\n};\n\nexport interface ScopedModel {\n\tmodel: Model<Api>;\n\tthinkingLevel: ThinkingLevel;\n}\n\n/**\n * Helper to check if a model ID looks like an alias (no date suffix)\n * Dates are typically in format: -20241022 or -20250929\n */\nfunction isAlias(id: string): boolean {\n\t// Check if ID ends with -latest\n\tif (id.endsWith(\"-latest\")) return true;\n\n\t// Check if ID ends with a date pattern (-YYYYMMDD)\n\tconst datePattern = /-\\d{8}$/;\n\treturn !datePattern.test(id);\n}\n\n/**\n * Try to match a pattern to a model from the available models list.\n * Returns the matched model or null if no match found.\n */\nfunction tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Model<Api> | null {\n\t// Check for provider/modelId format (provider is everything before the first /)\n\tconst slashIndex = modelPattern.indexOf(\"/\");\n\tif (slashIndex !== -1) {\n\t\tconst provider = modelPattern.substring(0, slashIndex);\n\t\tconst modelId = modelPattern.substring(slashIndex + 1);\n\t\tconst providerMatch = availableModels.find(\n\t\t\t(m) => m.provider.toLowerCase() === provider.toLowerCase() && m.id.toLowerCase() === modelId.toLowerCase(),\n\t\t);\n\t\tif (providerMatch) {\n\t\t\treturn providerMatch;\n\t\t}\n\t\t// No exact provider/model match - fall through to other matching\n\t}\n\n\t// Check for exact ID match (case-insensitive)\n\tconst exactMatch = availableModels.find((m) => m.id.toLowerCase() === modelPattern.toLowerCase());\n\tif (exactMatch) {\n\t\treturn exactMatch;\n\t}\n\n\t// No exact match - fall back to partial matching\n\tconst matches = availableModels.filter(\n\t\t(m) =>\n\t\t\tm.id.toLowerCase().includes(modelPattern.toLowerCase()) ||\n\t\t\tm.name?.toLowerCase().includes(modelPattern.toLowerCase()),\n\t);\n\n\tif (matches.length === 0) {\n\t\treturn null;\n\t}\n\n\t// Separate into aliases and dated versions\n\tconst aliases = matches.filter((m) => isAlias(m.id));\n\tconst datedVersions = matches.filter((m) => !isAlias(m.id));\n\n\tif (aliases.length > 0) {\n\t\t// Prefer alias - if multiple aliases, pick the one that sorts highest\n\t\taliases.sort((a, b) => b.id.localeCompare(a.id));\n\t\treturn aliases[0];\n\t} else {\n\t\t// No alias found, pick latest dated version\n\t\tdatedVersions.sort((a, b) => b.id.localeCompare(a.id));\n\t\treturn datedVersions[0];\n\t}\n}\n\nexport interface ParsedModelResult {\n\tmodel: Model<Api> | null;\n\tthinkingLevel: ThinkingLevel;\n\twarning: string | null;\n}\n\n/**\n * Parse a pattern to extract model and thinking level.\n * Handles models with colons in their IDs (e.g., OpenRouter's :exacto suffix).\n *\n * Algorithm:\n * 1. Try to match full pattern as a model\n * 2. If found, return it with \"off\" thinking level\n * 3. If not found and has colons, split on last colon:\n * - If suffix is valid thinking level, use it and recurse on prefix\n * - If suffix is invalid, warn and recurse on prefix with \"off\"\n *\n * @internal Exported for testing\n */\nexport function parseModelPattern(pattern: string, availableModels: Model<Api>[]): ParsedModelResult {\n\t// Try exact match first\n\tconst exactMatch = tryMatchModel(pattern, availableModels);\n\tif (exactMatch) {\n\t\treturn { model: exactMatch, thinkingLevel: \"off\", warning: null };\n\t}\n\n\t// No match - try splitting on last colon if present\n\tconst lastColonIndex = pattern.lastIndexOf(\":\");\n\tif (lastColonIndex === -1) {\n\t\t// No colons, pattern simply doesn't match any model\n\t\treturn { model: null, thinkingLevel: \"off\", warning: null };\n\t}\n\n\tconst prefix = pattern.substring(0, lastColonIndex);\n\tconst suffix = pattern.substring(lastColonIndex + 1);\n\n\tif (isValidThinkingLevel(suffix)) {\n\t\t// Valid thinking level - recurse on prefix and use this level\n\t\tconst result = parseModelPattern(prefix, availableModels);\n\t\tif (result.model) {\n\t\t\t// Only use this thinking level if no warning from inner recursion\n\t\t\t// (if there was an invalid suffix deeper, we already have \"off\")\n\t\t\treturn {\n\t\t\t\tmodel: result.model,\n\t\t\t\tthinkingLevel: result.warning ? \"off\" : suffix,\n\t\t\t\twarning: result.warning,\n\t\t\t};\n\t\t}\n\t\treturn result;\n\t} else {\n\t\t// Invalid suffix - recurse on prefix with \"off\" and warn\n\t\tconst result = parseModelPattern(prefix, availableModels);\n\t\tif (result.model) {\n\t\t\treturn {\n\t\t\t\tmodel: result.model,\n\t\t\t\tthinkingLevel: \"off\",\n\t\t\t\twarning: `Invalid thinking level \"${suffix}\" in pattern \"${pattern}\". Using \"off\" instead.`,\n\t\t\t};\n\t\t}\n\t\treturn result;\n\t}\n}\n\n/**\n * Resolve model patterns to actual Model objects with optional thinking levels\n * Format: \"pattern:level\" where :level is optional\n * For each pattern, finds all matching models and picks the best version:\n * 1. Prefer alias (e.g., claude-sonnet-4-5) over dated versions (claude-sonnet-4-5-20250929)\n * 2. If no alias, pick the latest dated version\n *\n * Supports models with colons in their IDs (e.g., OpenRouter's model:exacto).\n * The algorithm tries to match the full pattern first, then progressively\n * strips colon-suffixes to find a match.\n */\nexport async function resolveModelScope(patterns: string[], modelRegistry: ModelRegistry): Promise<ScopedModel[]> {\n\tconst availableModels = await modelRegistry.getAvailable();\n\tconst scopedModels: ScopedModel[] = [];\n\n\tfor (const pattern of patterns) {\n\t\tconst { model, thinkingLevel, warning } = parseModelPattern(pattern, availableModels);\n\n\t\tif (warning) {\n\t\t\tconsole.warn(chalk.yellow(`Warning: ${warning}`));\n\t\t}\n\n\t\tif (!model) {\n\t\t\tconsole.warn(chalk.yellow(`Warning: No models match pattern \"${pattern}\"`));\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Avoid duplicates\n\t\tif (!scopedModels.find((sm) => sm.model.id === model.id && sm.model.provider === model.provider)) {\n\t\t\tscopedModels.push({ model, thinkingLevel });\n\t\t}\n\t}\n\n\treturn scopedModels;\n}\n\nexport interface InitialModelResult {\n\tmodel: Model<Api> | null;\n\tthinkingLevel: ThinkingLevel;\n\tfallbackMessage: string | null;\n}\n\n/**\n * Find the initial model to use based on priority:\n * 1. CLI args (provider + model)\n * 2. First model from scoped models (if not continuing/resuming)\n * 3. Restored from session (if continuing/resuming)\n * 4. Saved default from settings\n * 5. First available model with valid API key\n */\nexport async function findInitialModel(options: {\n\tcliProvider?: string;\n\tcliModel?: string;\n\tscopedModels: ScopedModel[];\n\tisContinuing: boolean;\n\tdefaultProvider?: string;\n\tdefaultModelId?: string;\n\tdefaultThinkingLevel?: ThinkingLevel;\n\tmodelRegistry: ModelRegistry;\n}): Promise<InitialModelResult> {\n\tconst {\n\t\tcliProvider,\n\t\tcliModel,\n\t\tscopedModels,\n\t\tisContinuing,\n\t\tdefaultProvider,\n\t\tdefaultModelId,\n\t\tdefaultThinkingLevel,\n\t\tmodelRegistry,\n\t} = options;\n\n\tlet model: Model<Api> | null = null;\n\tlet thinkingLevel: ThinkingLevel = \"off\";\n\n\t// 1. CLI args take priority\n\tif (cliProvider && cliModel) {\n\t\tconst found = modelRegistry.find(cliProvider, cliModel);\n\t\tif (!found) {\n\t\t\tconsole.error(chalk.red(`Model ${cliProvider}/${cliModel} not found`));\n\t\t\tprocess.exit(1);\n\t\t}\n\t\treturn { model: found, thinkingLevel: \"off\", fallbackMessage: null };\n\t}\n\n\t// 2. Use first model from scoped models (skip if continuing/resuming)\n\tif (scopedModels.length > 0 && !isContinuing) {\n\t\treturn {\n\t\t\tmodel: scopedModels[0].model,\n\t\t\tthinkingLevel: scopedModels[0].thinkingLevel,\n\t\t\tfallbackMessage: null,\n\t\t};\n\t}\n\n\t// 3. Try saved default from settings\n\tif (defaultProvider && defaultModelId) {\n\t\tconst found = modelRegistry.find(defaultProvider, defaultModelId);\n\t\tif (found) {\n\t\t\tmodel = found;\n\t\t\tif (defaultThinkingLevel) {\n\t\t\t\tthinkingLevel = defaultThinkingLevel;\n\t\t\t}\n\t\t\treturn { model, thinkingLevel, fallbackMessage: null };\n\t\t}\n\t}\n\n\t// 4. Try first available model with valid API key\n\tconst availableModels = await modelRegistry.getAvailable();\n\n\tif (availableModels.length > 0) {\n\t\t// Try to find a default model from known providers\n\t\tfor (const provider of Object.keys(defaultModelPerProvider) as KnownProvider[]) {\n\t\t\tconst defaultId = defaultModelPerProvider[provider];\n\t\t\tconst match = availableModels.find((m) => m.provider === provider && m.id === defaultId);\n\t\t\tif (match) {\n\t\t\t\treturn { model: match, thinkingLevel: \"off\", fallbackMessage: null };\n\t\t\t}\n\t\t}\n\n\t\t// If no default found, use first available\n\t\treturn { model: availableModels[0], thinkingLevel: \"off\", fallbackMessage: null };\n\t}\n\n\t// 5. No model found\n\treturn { model: null, thinkingLevel: \"off\", fallbackMessage: null };\n}\n\n/**\n * Restore model from session, with fallback to available models\n */\nexport async function restoreModelFromSession(\n\tsavedProvider: string,\n\tsavedModelId: string,\n\tcurrentModel: Model<Api> | null,\n\tshouldPrintMessages: boolean,\n\tmodelRegistry: ModelRegistry,\n): Promise<{ model: Model<Api> | null; fallbackMessage: string | null }> {\n\tconst restoredModel = modelRegistry.find(savedProvider, savedModelId);\n\n\t// Check if restored model exists and has a valid API key\n\tconst hasApiKey = restoredModel ? !!(await modelRegistry.getApiKey(restoredModel)) : false;\n\n\tif (restoredModel && hasApiKey) {\n\t\tif (shouldPrintMessages) {\n\t\t\tconsole.log(chalk.dim(`Restored model: ${savedProvider}/${savedModelId}`));\n\t\t}\n\t\treturn { model: restoredModel, fallbackMessage: null };\n\t}\n\n\t// Model not found or no API key - fall back\n\tconst reason = !restoredModel ? \"model no longer exists\" : \"no API key available\";\n\n\tif (shouldPrintMessages) {\n\t\tconsole.error(chalk.yellow(`Warning: Could not restore model ${savedProvider}/${savedModelId} (${reason}).`));\n\t}\n\n\t// If we already have a model, use it as fallback\n\tif (currentModel) {\n\t\tif (shouldPrintMessages) {\n\t\t\tconsole.log(chalk.dim(`Falling back to: ${currentModel.provider}/${currentModel.id}`));\n\t\t}\n\t\treturn {\n\t\t\tmodel: currentModel,\n\t\t\tfallbackMessage: `Could not restore model ${savedProvider}/${savedModelId} (${reason}). Using ${currentModel.provider}/${currentModel.id}.`,\n\t\t};\n\t}\n\n\t// Try to find any available model\n\tconst availableModels = await modelRegistry.getAvailable();\n\n\tif (availableModels.length > 0) {\n\t\t// Try to find a default model from known providers\n\t\tlet fallbackModel: Model<Api> | null = null;\n\t\tfor (const provider of Object.keys(defaultModelPerProvider) as KnownProvider[]) {\n\t\t\tconst defaultId = defaultModelPerProvider[provider];\n\t\t\tconst match = availableModels.find((m) => m.provider === provider && m.id === defaultId);\n\t\t\tif (match) {\n\t\t\t\tfallbackModel = match;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\t// If no default found, use first available\n\t\tif (!fallbackModel) {\n\t\t\tfallbackModel = availableModels[0];\n\t\t}\n\n\t\tif (shouldPrintMessages) {\n\t\t\tconsole.log(chalk.dim(`Falling back to: ${fallbackModel.provider}/${fallbackModel.id}`));\n\t\t}\n\n\t\treturn {\n\t\t\tmodel: fallbackModel,\n\t\t\tfallbackMessage: `Could not restore model ${savedProvider}/${savedModelId} (${reason}). Using ${fallbackModel.provider}/${fallbackModel.id}.`,\n\t\t};\n\t}\n\n\t// No models available\n\treturn { model: null, fallbackMessage: null };\n}\n"]}
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { isValidThinkingLevel } from "../cli/args.js";
|
|
6
|
-
import { findModel, getApiKeyForModel, getAvailableModels } from "./model-config.js";
|
|
7
6
|
/** Default model IDs for each known provider */
|
|
8
7
|
export const defaultModelPerProvider = {
|
|
9
8
|
anthropic: "claude-sonnet-4-5",
|
|
@@ -136,16 +135,9 @@ export function parseModelPattern(pattern, availableModels) {
|
|
|
136
135
|
* Supports models with colons in their IDs (e.g., OpenRouter's model:exacto).
|
|
137
136
|
* The algorithm tries to match the full pattern first, then progressively
|
|
138
137
|
* strips colon-suffixes to find a match.
|
|
139
|
-
*
|
|
140
|
-
* @param patterns - Model patterns to resolve
|
|
141
|
-
* @param settingsManager - Optional settings manager for API key fallback from settings.json
|
|
142
138
|
*/
|
|
143
|
-
export async function resolveModelScope(patterns,
|
|
144
|
-
const
|
|
145
|
-
if (error) {
|
|
146
|
-
console.warn(chalk.yellow(`Warning: Error loading models: ${error}`));
|
|
147
|
-
return [];
|
|
148
|
-
}
|
|
139
|
+
export async function resolveModelScope(patterns, modelRegistry) {
|
|
140
|
+
const availableModels = await modelRegistry.getAvailable();
|
|
149
141
|
const scopedModels = [];
|
|
150
142
|
for (const pattern of patterns) {
|
|
151
143
|
const { model, thinkingLevel, warning } = parseModelPattern(pattern, availableModels);
|
|
@@ -172,16 +164,12 @@ export async function resolveModelScope(patterns, settingsManager) {
|
|
|
172
164
|
* 5. First available model with valid API key
|
|
173
165
|
*/
|
|
174
166
|
export async function findInitialModel(options) {
|
|
175
|
-
const { cliProvider, cliModel, scopedModels, isContinuing,
|
|
167
|
+
const { cliProvider, cliModel, scopedModels, isContinuing, defaultProvider, defaultModelId, defaultThinkingLevel, modelRegistry, } = options;
|
|
176
168
|
let model = null;
|
|
177
169
|
let thinkingLevel = "off";
|
|
178
170
|
// 1. CLI args take priority
|
|
179
171
|
if (cliProvider && cliModel) {
|
|
180
|
-
const
|
|
181
|
-
if (error) {
|
|
182
|
-
console.error(chalk.red(error));
|
|
183
|
-
process.exit(1);
|
|
184
|
-
}
|
|
172
|
+
const found = modelRegistry.find(cliProvider, cliModel);
|
|
185
173
|
if (!found) {
|
|
186
174
|
console.error(chalk.red(`Model ${cliProvider}/${cliModel} not found`));
|
|
187
175
|
process.exit(1);
|
|
@@ -197,30 +185,18 @@ export async function findInitialModel(options) {
|
|
|
197
185
|
};
|
|
198
186
|
}
|
|
199
187
|
// 3. Try saved default from settings
|
|
200
|
-
const defaultProvider = settingsManager.getDefaultProvider();
|
|
201
|
-
const defaultModelId = settingsManager.getDefaultModel();
|
|
202
188
|
if (defaultProvider && defaultModelId) {
|
|
203
|
-
const
|
|
204
|
-
if (error) {
|
|
205
|
-
console.error(chalk.red(error));
|
|
206
|
-
process.exit(1);
|
|
207
|
-
}
|
|
189
|
+
const found = modelRegistry.find(defaultProvider, defaultModelId);
|
|
208
190
|
if (found) {
|
|
209
191
|
model = found;
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (savedThinking) {
|
|
213
|
-
thinkingLevel = savedThinking;
|
|
192
|
+
if (defaultThinkingLevel) {
|
|
193
|
+
thinkingLevel = defaultThinkingLevel;
|
|
214
194
|
}
|
|
215
195
|
return { model, thinkingLevel, fallbackMessage: null };
|
|
216
196
|
}
|
|
217
197
|
}
|
|
218
198
|
// 4. Try first available model with valid API key
|
|
219
|
-
const
|
|
220
|
-
if (error) {
|
|
221
|
-
console.error(chalk.red(error));
|
|
222
|
-
process.exit(1);
|
|
223
|
-
}
|
|
199
|
+
const availableModels = await modelRegistry.getAvailable();
|
|
224
200
|
if (availableModels.length > 0) {
|
|
225
201
|
// Try to find a default model from known providers
|
|
226
202
|
for (const provider of Object.keys(defaultModelPerProvider)) {
|
|
@@ -239,14 +215,10 @@ export async function findInitialModel(options) {
|
|
|
239
215
|
/**
|
|
240
216
|
* Restore model from session, with fallback to available models
|
|
241
217
|
*/
|
|
242
|
-
export async function restoreModelFromSession(savedProvider, savedModelId, currentModel, shouldPrintMessages,
|
|
243
|
-
const
|
|
244
|
-
if (error) {
|
|
245
|
-
console.error(chalk.red(error));
|
|
246
|
-
process.exit(1);
|
|
247
|
-
}
|
|
218
|
+
export async function restoreModelFromSession(savedProvider, savedModelId, currentModel, shouldPrintMessages, modelRegistry) {
|
|
219
|
+
const restoredModel = modelRegistry.find(savedProvider, savedModelId);
|
|
248
220
|
// Check if restored model exists and has a valid API key
|
|
249
|
-
const hasApiKey = restoredModel ? !!(await
|
|
221
|
+
const hasApiKey = restoredModel ? !!(await modelRegistry.getApiKey(restoredModel)) : false;
|
|
250
222
|
if (restoredModel && hasApiKey) {
|
|
251
223
|
if (shouldPrintMessages) {
|
|
252
224
|
console.log(chalk.dim(`Restored model: ${savedProvider}/${savedModelId}`));
|
|
@@ -269,11 +241,7 @@ export async function restoreModelFromSession(savedProvider, savedModelId, curre
|
|
|
269
241
|
};
|
|
270
242
|
}
|
|
271
243
|
// Try to find any available model
|
|
272
|
-
const
|
|
273
|
-
if (availableError) {
|
|
274
|
-
console.error(chalk.red(availableError));
|
|
275
|
-
process.exit(1);
|
|
276
|
-
}
|
|
244
|
+
const availableModels = await modelRegistry.getAvailable();
|
|
277
245
|
if (availableModels.length > 0) {
|
|
278
246
|
// Try to find a default model from known providers
|
|
279
247
|
let fallbackModel = null;
|