@oh-my-pi/pi-coding-agent 15.10.11 → 15.10.12
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 +44 -0
- package/dist/cli.js +5349 -5328
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli-commands.d.ts +12 -0
- package/dist/types/commands/launch.d.ts +4 -0
- package/dist/types/config/api-key-resolver.d.ts +3 -0
- package/dist/types/config/model-registry.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +18 -0
- package/dist/types/config/settings-schema.d.ts +29 -1
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
- package/dist/types/eval/py/executor.d.ts +5 -0
- package/dist/types/eval/py/kernel.d.ts +6 -1
- package/dist/types/eval/py/runtime.d.ts +9 -0
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/memory-backend/index.d.ts +1 -0
- package/dist/types/memory-backend/runtime.d.ts +4 -0
- package/dist/types/memory-backend/types.d.ts +66 -1
- package/dist/types/modes/index.d.ts +3 -3
- package/dist/types/modes/interactive-mode.d.ts +7 -2
- package/dist/types/modes/oauth-manual-input.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +39 -2
- package/dist/types/modes/rpc/rpc-mode.d.ts +31 -2
- package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +75 -1
- package/dist/types/modes/setup-wizard/index.d.ts +5 -1
- package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/secrets/index.d.ts +1 -1
- package/dist/types/secrets/obfuscator.d.ts +8 -2
- package/dist/types/session/agent-session.d.ts +14 -2
- package/dist/types/session/streaming-output.d.ts +23 -0
- package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
- package/dist/types/slash-commands/types.d.ts +1 -1
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/index.d.ts +2 -2
- package/dist/types/task/types.d.ts +8 -0
- package/dist/types/thinking.d.ts +4 -0
- package/dist/types/tiny/title-client.d.ts +11 -0
- package/dist/types/tiny/title-protocol.d.ts +1 -0
- package/dist/types/tools/index.d.ts +6 -0
- package/dist/types/utils/git.d.ts +15 -2
- package/dist/types/utils/title-generator.d.ts +3 -2
- package/package.json +10 -10
- package/src/auto-thinking/classifier.ts +1 -0
- package/src/cli/args.ts +3 -0
- package/src/cli-commands.ts +29 -0
- package/src/cli.ts +8 -9
- package/src/commands/launch.ts +4 -0
- package/src/commit/model-selection.ts +3 -2
- package/src/config/api-key-resolver.ts +8 -6
- package/src/config/model-registry.ts +97 -30
- package/src/config/model-resolver.ts +60 -0
- package/src/config/settings-schema.ts +43 -15
- package/src/config/settings.ts +61 -3
- package/src/edit/hashline/execute.ts +39 -2
- package/src/edit/hashline/noop-loop-guard.ts +99 -0
- package/src/eval/completion-bridge.ts +1 -0
- package/src/eval/py/executor.ts +29 -7
- package/src/eval/py/index.ts +6 -1
- package/src/eval/py/kernel.ts +31 -11
- package/src/eval/py/runtime.ts +37 -0
- package/src/exec/bash-executor.ts +82 -3
- package/src/extensibility/extensions/get-commands-handler.ts +2 -1
- package/src/extensibility/extensions/runner.ts +6 -1
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/hindsight/bank.ts +17 -2
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/main.ts +18 -6
- package/src/memories/index.ts +2 -0
- package/src/memory-backend/index.ts +1 -0
- package/src/memory-backend/local-backend.ts +9 -0
- package/src/memory-backend/off-backend.ts +9 -0
- package/src/memory-backend/runtime.ts +66 -0
- package/src/memory-backend/types.ts +81 -1
- package/src/mnemopi/backend.ts +151 -4
- package/src/modes/acp/acp-agent.ts +119 -11
- package/src/modes/components/assistant-message.ts +19 -21
- package/src/modes/components/footer.ts +3 -1
- package/src/modes/components/status-line/component.ts +118 -34
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +1 -0
- package/src/modes/controllers/mcp-command-controller.ts +38 -3
- package/src/modes/index.ts +3 -21
- package/src/modes/interactive-mode.ts +39 -9
- package/src/modes/oauth-manual-input.ts +30 -3
- package/src/modes/rpc/rpc-client.ts +154 -3
- package/src/modes/rpc/rpc-mode.ts +97 -12
- package/src/modes/rpc/rpc-subagents.ts +265 -0
- package/src/modes/rpc/rpc-types.ts +81 -1
- package/src/modes/setup-wizard/index.ts +12 -2
- package/src/modes/setup-wizard/lazy.ts +16 -0
- package/src/modes/types.ts +2 -0
- package/src/sdk.ts +8 -1
- package/src/secrets/index.ts +8 -1
- package/src/secrets/obfuscator.ts +39 -18
- package/src/session/agent-session.ts +179 -54
- package/src/session/streaming-output.ts +166 -10
- package/src/slash-commands/acp-builtins.ts +24 -0
- package/src/slash-commands/builtin-registry.ts +20 -0
- package/src/slash-commands/types.ts +1 -1
- package/src/system-prompt.ts +14 -0
- package/src/task/executor.ts +13 -12
- package/src/task/index.ts +9 -8
- package/src/task/render.ts +18 -3
- package/src/task/types.ts +9 -0
- package/src/thinking.ts +7 -0
- package/src/tiny/title-client.ts +34 -5
- package/src/tiny/title-protocol.ts +1 -1
- package/src/tiny/worker.ts +6 -4
- package/src/tools/bash.ts +46 -5
- package/src/tools/image-gen.ts +11 -4
- package/src/tools/index.ts +13 -1
- package/src/tools/inspect-image.ts +1 -0
- package/src/utils/commit-message-generator.ts +1 -0
- package/src/utils/git.ts +267 -13
- package/src/utils/title-generator.ts +24 -5
package/src/cli.ts
CHANGED
|
@@ -161,20 +161,19 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
161
161
|
if (await runWorkerEntrypoint(argv[0])) {
|
|
162
162
|
return;
|
|
163
163
|
}
|
|
164
|
-
const [{ run }, { commands,
|
|
164
|
+
const [{ run }, { commands, resolveCliArgv }] = await Promise.all([
|
|
165
165
|
import("@oh-my-pi/pi-utils/cli"),
|
|
166
166
|
import("./cli-commands"),
|
|
167
167
|
]);
|
|
168
168
|
// --help and --version are handled by run() directly, don't rewrite those.
|
|
169
169
|
// Everything else that isn't a known subcommand routes to "launch".
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
return run({ bin: APP_NAME, version: VERSION, argv: runArgv, commands, help: showHelp });
|
|
170
|
+
const resolved = resolveCliArgv(argv);
|
|
171
|
+
if ("error" in resolved) {
|
|
172
|
+
process.stderr.write(`error: ${resolved.error}\n`);
|
|
173
|
+
process.exitCode = 1;
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
return run({ bin: APP_NAME, version: VERSION, argv: resolved.argv, commands, help: showHelp });
|
|
178
177
|
}
|
|
179
178
|
|
|
180
179
|
// Floating call instead of top-level await: TLA forces `--bytecode` (CJS
|
package/src/commands/launch.ts
CHANGED
|
@@ -56,6 +56,10 @@ export default class Index extends Command {
|
|
|
56
56
|
description: "Output mode: text (default), json, rpc, or rpc-ui",
|
|
57
57
|
options: ["text", "json", "rpc", "acp", "rpc-ui"],
|
|
58
58
|
}),
|
|
59
|
+
config: Flags.string({
|
|
60
|
+
description: "Load an extra config.yml-style overlay for this run (repeatable)",
|
|
61
|
+
multiple: true,
|
|
62
|
+
}),
|
|
59
63
|
print: Flags.boolean({
|
|
60
64
|
char: "p",
|
|
61
65
|
description: "Non-interactive mode: process prompt and exit",
|
|
@@ -48,7 +48,7 @@ export async function resolvePrimaryModel(
|
|
|
48
48
|
}
|
|
49
49
|
return {
|
|
50
50
|
model,
|
|
51
|
-
apiKey: modelRegistry.resolver(model.provider, { baseUrl: model.baseUrl }),
|
|
51
|
+
apiKey: modelRegistry.resolver(model.provider, { baseUrl: model.baseUrl, modelId: model.id }),
|
|
52
52
|
thinkingLevel: resolved?.thinkingLevel,
|
|
53
53
|
};
|
|
54
54
|
}
|
|
@@ -68,6 +68,7 @@ export async function resolveSmolModel(
|
|
|
68
68
|
model: resolvedSmol.model,
|
|
69
69
|
apiKey: modelRegistry.resolver(resolvedSmol.model.provider, {
|
|
70
70
|
baseUrl: resolvedSmol.model.baseUrl,
|
|
71
|
+
modelId: resolvedSmol.model.id,
|
|
71
72
|
}),
|
|
72
73
|
thinkingLevel: resolvedSmol.thinkingLevel,
|
|
73
74
|
};
|
|
@@ -82,7 +83,7 @@ export async function resolveSmolModel(
|
|
|
82
83
|
if (apiKey) {
|
|
83
84
|
return {
|
|
84
85
|
model: candidate,
|
|
85
|
-
apiKey: modelRegistry.resolver(candidate.provider, { baseUrl: candidate.baseUrl }),
|
|
86
|
+
apiKey: modelRegistry.resolver(candidate.provider, { baseUrl: candidate.baseUrl, modelId: candidate.id }),
|
|
86
87
|
};
|
|
87
88
|
}
|
|
88
89
|
}
|
|
@@ -5,6 +5,8 @@ export interface ApiKeyResolverOptions {
|
|
|
5
5
|
sessionId?: string;
|
|
6
6
|
/** Provider base URL hint forwarded to the auth-storage cascade. */
|
|
7
7
|
baseUrl?: string;
|
|
8
|
+
/** Provider model id forwarded to model-scoped usage ranking/backoff. */
|
|
9
|
+
modelId?: string;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
/**
|
|
@@ -16,7 +18,7 @@ export interface ApiKeyResolverRegistry {
|
|
|
16
18
|
getApiKeyForProvider(
|
|
17
19
|
provider: string,
|
|
18
20
|
sessionId?: string,
|
|
19
|
-
options?: { baseUrl?: string; forceRefresh?: boolean; signal?: AbortSignal },
|
|
21
|
+
options?: { baseUrl?: string; modelId?: string; forceRefresh?: boolean; signal?: AbortSignal },
|
|
20
22
|
): Promise<string | undefined>;
|
|
21
23
|
authStorage: Pick<AuthStorage, "rotateSessionCredential">;
|
|
22
24
|
/**
|
|
@@ -39,10 +41,10 @@ export function createApiKeyResolver(
|
|
|
39
41
|
provider: string,
|
|
40
42
|
options: ApiKeyResolverOptions = {},
|
|
41
43
|
): ApiKeyResolver {
|
|
42
|
-
const { sessionId, baseUrl } = options;
|
|
44
|
+
const { sessionId, baseUrl, modelId } = options;
|
|
43
45
|
return async ({ lastChance, error, signal }) => {
|
|
44
46
|
if (error === undefined) {
|
|
45
|
-
return registry.getApiKeyForProvider(provider, sessionId, { baseUrl });
|
|
47
|
+
return registry.getApiKeyForProvider(provider, sessionId, { baseUrl, modelId });
|
|
46
48
|
}
|
|
47
49
|
if (lastChance) {
|
|
48
50
|
// Account constraint (401 / usage / account-rate-limit): rotate to a
|
|
@@ -50,9 +52,9 @@ export function createApiKeyResolver(
|
|
|
50
52
|
// sibling exists we switch immediately; the precise no-sibling backoff
|
|
51
53
|
// is owned by `markUsageLimitReached` (default + server usage-report
|
|
52
54
|
// reset) and the outer whole-turn retry layer.
|
|
53
|
-
await registry.authStorage.rotateSessionCredential(provider, sessionId, { error, signal });
|
|
54
|
-
return registry.getApiKeyForProvider(provider, sessionId, { baseUrl });
|
|
55
|
+
await registry.authStorage.rotateSessionCredential(provider, sessionId, { error, modelId, signal });
|
|
56
|
+
return registry.getApiKeyForProvider(provider, sessionId, { baseUrl, modelId });
|
|
55
57
|
}
|
|
56
|
-
return registry.getApiKeyForProvider(provider, sessionId, { baseUrl, forceRefresh: true, signal });
|
|
58
|
+
return registry.getApiKeyForProvider(provider, sessionId, { baseUrl, modelId, forceRefresh: true, signal });
|
|
57
59
|
};
|
|
58
60
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
1
2
|
import * as path from "node:path";
|
|
2
3
|
import { registerCustomApi, unregisterCustomApis } from "@oh-my-pi/pi-ai/api-registry";
|
|
3
4
|
import type { Api, Context, Model, ModelSpec, SimpleStreamOptions, ThinkingConfig } from "@oh-my-pi/pi-ai/types";
|
|
@@ -226,14 +227,50 @@ interface CustomModelsResult {
|
|
|
226
227
|
found: boolean;
|
|
227
228
|
}
|
|
228
229
|
|
|
230
|
+
const commandValueCache = new Map<string, string>();
|
|
231
|
+
|
|
232
|
+
function isCommandConfigValue(valueConfig: string | undefined): valueConfig is string {
|
|
233
|
+
return valueConfig?.startsWith("!") === true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function resolveCommandConfig(command: string): string | undefined {
|
|
237
|
+
const cached = commandValueCache.get(command);
|
|
238
|
+
if (cached !== undefined) return cached;
|
|
239
|
+
try {
|
|
240
|
+
const stdout = execSync(command, { encoding: "utf8", timeout: 10_000, windowsHide: true });
|
|
241
|
+
const trimmed = stdout.trim();
|
|
242
|
+
if (trimmed.length === 0) return undefined;
|
|
243
|
+
commandValueCache.set(command, trimmed);
|
|
244
|
+
return trimmed;
|
|
245
|
+
} catch {
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
interface CommandApiKeyResolution {
|
|
251
|
+
configured: boolean;
|
|
252
|
+
value?: string;
|
|
253
|
+
}
|
|
229
254
|
/**
|
|
230
|
-
* Resolve
|
|
231
|
-
*
|
|
255
|
+
* Resolve a models.yml secret/config value to an actual value.
|
|
256
|
+
* `!cmd` runs a shell command and returns trimmed stdout, otherwise env vars are
|
|
257
|
+
* checked first and the input falls back to a literal value.
|
|
232
258
|
*/
|
|
233
|
-
function
|
|
234
|
-
|
|
259
|
+
function resolveConfigValue(valueConfig: string): string | undefined {
|
|
260
|
+
if (valueConfig.startsWith("!")) return resolveCommandConfig(valueConfig.slice(1).trim());
|
|
261
|
+
const envValue = Bun.env[valueConfig];
|
|
235
262
|
if (envValue) return envValue;
|
|
236
|
-
return
|
|
263
|
+
return valueConfig;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function resolveConfigHeaders(headers: Record<string, string> | undefined): Record<string, string> | undefined {
|
|
267
|
+
if (!headers) return undefined;
|
|
268
|
+
const resolved: Record<string, string> = {};
|
|
269
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
270
|
+
const next = resolveConfigValue(value);
|
|
271
|
+
if (next) resolved[key] = next;
|
|
272
|
+
}
|
|
273
|
+
return Object.keys(resolved).length > 0 ? resolved : undefined;
|
|
237
274
|
}
|
|
238
275
|
|
|
239
276
|
function extractGoogleOAuthToken(value: string | undefined): string | undefined {
|
|
@@ -394,7 +431,8 @@ function mergeCustomModelHeaders(
|
|
|
394
431
|
authHeader: boolean | undefined,
|
|
395
432
|
apiKeyConfig: string | undefined,
|
|
396
433
|
): Record<string, string> | undefined {
|
|
397
|
-
|
|
434
|
+
const resolvedModelHeaders = resolveConfigHeaders(modelHeaders);
|
|
435
|
+
return mergeAuthHeader({ ...providerHeaders, ...resolvedModelHeaders }, authHeader, apiKeyConfig);
|
|
398
436
|
}
|
|
399
437
|
|
|
400
438
|
function mergeAuthHeader(
|
|
@@ -406,7 +444,7 @@ function mergeAuthHeader(
|
|
|
406
444
|
if (!authHeader || !apiKeyConfig) {
|
|
407
445
|
return nextHeaders;
|
|
408
446
|
}
|
|
409
|
-
const resolvedKey =
|
|
447
|
+
const resolvedKey = resolveConfigValue(apiKeyConfig);
|
|
410
448
|
return resolvedKey ? { ...nextHeaders, Authorization: `Bearer ${resolvedKey}` } : nextHeaders;
|
|
411
449
|
}
|
|
412
450
|
|
|
@@ -559,6 +597,28 @@ export class ModelRegistry {
|
|
|
559
597
|
#rebuildSuspended: number = 0;
|
|
560
598
|
#fetch: FetchImpl;
|
|
561
599
|
|
|
600
|
+
#resolveCommandBackedApiKey(provider: string): CommandApiKeyResolution {
|
|
601
|
+
const keyConfig = this.#customProviderApiKeys.get(provider);
|
|
602
|
+
if (!isCommandConfigValue(keyConfig)) return { configured: false };
|
|
603
|
+
const value = resolveConfigValue(keyConfig);
|
|
604
|
+
if (value) {
|
|
605
|
+
this.authStorage.setConfigApiKey(provider, value);
|
|
606
|
+
return { configured: true, value };
|
|
607
|
+
}
|
|
608
|
+
this.authStorage.removeConfigApiKey(provider);
|
|
609
|
+
return { configured: true };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
#installProviderApiKey(provider: string, keyConfig: string): void {
|
|
613
|
+
this.#customProviderApiKeys.set(provider, keyConfig);
|
|
614
|
+
const resolved = resolveConfigValue(keyConfig);
|
|
615
|
+
if (resolved) {
|
|
616
|
+
this.authStorage.setConfigApiKey(provider, resolved);
|
|
617
|
+
} else if (isCommandConfigValue(keyConfig)) {
|
|
618
|
+
this.authStorage.removeConfigApiKey(provider);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
562
622
|
/**
|
|
563
623
|
* @param authStorage - Auth storage for API key resolution
|
|
564
624
|
*
|
|
@@ -579,10 +639,8 @@ export class ModelRegistry {
|
|
|
579
639
|
// Set up fallback resolver for custom provider API keys
|
|
580
640
|
this.authStorage.setFallbackResolver(provider => {
|
|
581
641
|
const keyConfig = this.#customProviderApiKeys.get(provider);
|
|
582
|
-
if (keyConfig)
|
|
583
|
-
|
|
584
|
-
}
|
|
585
|
-
return undefined;
|
|
642
|
+
if (!keyConfig) return undefined;
|
|
643
|
+
return resolveConfigValue(keyConfig);
|
|
586
644
|
});
|
|
587
645
|
// Load models synchronously in constructor.
|
|
588
646
|
this.#loadModels();
|
|
@@ -673,7 +731,7 @@ export class ModelRegistry {
|
|
|
673
731
|
// Restore runtime API keys before #loadModels — survives because
|
|
674
732
|
// #loadModels only calls .set() on #customProviderApiKeys, never reassigns it.
|
|
675
733
|
for (const [k, v] of this.#runtimeProviderApiKeys) {
|
|
676
|
-
this.#
|
|
734
|
+
this.#installProviderApiKey(k, v);
|
|
677
735
|
}
|
|
678
736
|
this.#providerOverrides.clear();
|
|
679
737
|
this.#modelOverrides.clear();
|
|
@@ -975,10 +1033,11 @@ export class ModelRegistry {
|
|
|
975
1033
|
const configuredProviders = new Set(Object.keys(value.providers ?? {}));
|
|
976
1034
|
|
|
977
1035
|
for (const [providerName, providerConfig] of providerEntries) {
|
|
1036
|
+
const resolvedProviderHeaders = resolveConfigHeaders(providerConfig.headers);
|
|
978
1037
|
// Always set overrides when baseUrl/headers/apiKey/authHeader/compat/disableStrictTools/transport are present
|
|
979
1038
|
if (
|
|
980
1039
|
providerConfig.baseUrl ||
|
|
981
|
-
|
|
1040
|
+
resolvedProviderHeaders ||
|
|
982
1041
|
providerConfig.apiKey ||
|
|
983
1042
|
providerConfig.authHeader !== undefined ||
|
|
984
1043
|
providerConfig.compat ||
|
|
@@ -988,7 +1047,7 @@ export class ModelRegistry {
|
|
|
988
1047
|
const disableStrictCompat = providerConfig.disableStrictTools ? { disableStrictTools: true } : undefined;
|
|
989
1048
|
overrides.set(providerName, {
|
|
990
1049
|
baseUrl: providerConfig.baseUrl,
|
|
991
|
-
headers:
|
|
1050
|
+
headers: resolvedProviderHeaders,
|
|
992
1051
|
apiKey: providerConfig.apiKey,
|
|
993
1052
|
authHeader: providerConfig.authHeader,
|
|
994
1053
|
compat: mergeCompat(providerConfig.compat, disableStrictCompat),
|
|
@@ -1010,7 +1069,7 @@ export class ModelRegistry {
|
|
|
1010
1069
|
// fallback for entries that don't advertise one.
|
|
1011
1070
|
api: (providerConfig.api ?? "openai-completions") as Api,
|
|
1012
1071
|
baseUrl: providerConfig.baseUrl,
|
|
1013
|
-
headers:
|
|
1072
|
+
headers: resolvedProviderHeaders,
|
|
1014
1073
|
compat: mergeCompat(providerConfig.compat, disableStrictCompat),
|
|
1015
1074
|
discovery: providerConfig.discovery,
|
|
1016
1075
|
optional: false,
|
|
@@ -1022,16 +1081,17 @@ export class ModelRegistry {
|
|
|
1022
1081
|
// bearer in models.yml (e.g. for an auth-gateway baseUrl), that bearer
|
|
1023
1082
|
// must authenticate the outbound request.
|
|
1024
1083
|
if (providerConfig.apiKey) {
|
|
1025
|
-
this.#
|
|
1026
|
-
const resolved = resolveApiKeyConfig(providerConfig.apiKey);
|
|
1027
|
-
if (resolved) this.authStorage.setConfigApiKey(providerName, resolved);
|
|
1084
|
+
this.#installProviderApiKey(providerName, providerConfig.apiKey);
|
|
1028
1085
|
}
|
|
1029
1086
|
|
|
1030
1087
|
// Parse per-model overrides
|
|
1031
1088
|
if (providerConfig.modelOverrides) {
|
|
1032
1089
|
const perModel = new Map<string, ModelOverride>();
|
|
1033
1090
|
for (const [modelId, override] of Object.entries(providerConfig.modelOverrides)) {
|
|
1034
|
-
perModel.set(
|
|
1091
|
+
perModel.set(
|
|
1092
|
+
modelId,
|
|
1093
|
+
override.headers ? { ...override, headers: resolveConfigHeaders(override.headers) } : override,
|
|
1094
|
+
);
|
|
1035
1095
|
}
|
|
1036
1096
|
allModelOverrides.set(providerName, perModel);
|
|
1037
1097
|
}
|
|
@@ -1179,7 +1239,7 @@ export class ModelRegistry {
|
|
|
1179
1239
|
return {
|
|
1180
1240
|
fetch: this.#fetch,
|
|
1181
1241
|
getBearerApiKey: async provider => {
|
|
1182
|
-
const apiKey = await this.
|
|
1242
|
+
const apiKey = await this.getApiKeyForProvider(provider);
|
|
1183
1243
|
return apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth ? apiKey : undefined;
|
|
1184
1244
|
},
|
|
1185
1245
|
};
|
|
@@ -1443,10 +1503,9 @@ export class ModelRegistry {
|
|
|
1443
1503
|
for (const [providerName, providerConfig] of Object.entries(config.providers ?? {})) {
|
|
1444
1504
|
const modelDefs = providerConfig.models ?? [];
|
|
1445
1505
|
if (modelDefs.length === 0) continue; // Override-only, no custom models
|
|
1506
|
+
const resolvedProviderHeaders = resolveConfigHeaders(providerConfig.headers);
|
|
1446
1507
|
if (providerConfig.apiKey) {
|
|
1447
|
-
this.#
|
|
1448
|
-
const resolved = resolveApiKeyConfig(providerConfig.apiKey);
|
|
1449
|
-
if (resolved) this.authStorage.setConfigApiKey(providerName, resolved);
|
|
1508
|
+
this.#installProviderApiKey(providerName, providerConfig.apiKey);
|
|
1450
1509
|
}
|
|
1451
1510
|
for (const modelDef of modelDefs) {
|
|
1452
1511
|
const providerCompat = providerConfig.disableStrictTools
|
|
@@ -1456,7 +1515,7 @@ export class ModelRegistry {
|
|
|
1456
1515
|
providerName,
|
|
1457
1516
|
providerConfig.baseUrl!,
|
|
1458
1517
|
providerConfig.api as Api | undefined,
|
|
1459
|
-
|
|
1518
|
+
resolvedProviderHeaders,
|
|
1460
1519
|
providerConfig.apiKey,
|
|
1461
1520
|
providerConfig.authHeader,
|
|
1462
1521
|
providerCompat,
|
|
@@ -1626,7 +1685,10 @@ export class ModelRegistry {
|
|
|
1626
1685
|
* as providers with stored credentials. See issue #993.
|
|
1627
1686
|
*/
|
|
1628
1687
|
hasConfiguredAuth(model: Model<Api>): boolean {
|
|
1629
|
-
|
|
1688
|
+
const commandKey = this.#resolveCommandBackedApiKey(model.provider);
|
|
1689
|
+
return (
|
|
1690
|
+
commandKey.configured || this.#keylessProviders.has(model.provider) || this.authStorage.hasAuth(model.provider)
|
|
1691
|
+
);
|
|
1630
1692
|
}
|
|
1631
1693
|
|
|
1632
1694
|
getDiscoverableProviders(): string[] {
|
|
@@ -1658,6 +1720,8 @@ export class ModelRegistry {
|
|
|
1658
1720
|
* Get API key for a model.
|
|
1659
1721
|
*/
|
|
1660
1722
|
async getApiKey(model: Model<Api>, sessionId?: string): Promise<string | undefined> {
|
|
1723
|
+
const commandKey = this.#resolveCommandBackedApiKey(model.provider);
|
|
1724
|
+
if (commandKey.configured) return commandKey.value;
|
|
1661
1725
|
if (this.#keylessProviders.has(model.provider) && !this.authStorage.hasAuth(model.provider)) {
|
|
1662
1726
|
return kNoAuth;
|
|
1663
1727
|
}
|
|
@@ -1674,13 +1738,16 @@ export class ModelRegistry {
|
|
|
1674
1738
|
async getApiKeyForProvider(
|
|
1675
1739
|
provider: string,
|
|
1676
1740
|
sessionId?: string,
|
|
1677
|
-
options?: { baseUrl?: string; forceRefresh?: boolean; signal?: AbortSignal },
|
|
1741
|
+
options?: { baseUrl?: string; modelId?: string; forceRefresh?: boolean; signal?: AbortSignal },
|
|
1678
1742
|
): Promise<string | undefined> {
|
|
1743
|
+
const commandKey = this.#resolveCommandBackedApiKey(provider);
|
|
1744
|
+
if (commandKey.configured) return commandKey.value;
|
|
1679
1745
|
if (this.#keylessProviders.has(provider) && !this.authStorage.hasAuth(provider)) {
|
|
1680
1746
|
return kNoAuth;
|
|
1681
1747
|
}
|
|
1682
1748
|
return this.authStorage.getApiKey(provider, sessionId, {
|
|
1683
1749
|
baseUrl: options?.baseUrl,
|
|
1750
|
+
modelId: options?.modelId,
|
|
1684
1751
|
forceRefresh: options?.forceRefresh,
|
|
1685
1752
|
signal: options?.signal,
|
|
1686
1753
|
});
|
|
@@ -1696,6 +1763,8 @@ export class ModelRegistry {
|
|
|
1696
1763
|
}
|
|
1697
1764
|
|
|
1698
1765
|
async #peekApiKeyForProvider(provider: string): Promise<string | undefined> {
|
|
1766
|
+
const commandKey = this.#resolveCommandBackedApiKey(provider);
|
|
1767
|
+
if (commandKey.configured) return commandKey.value;
|
|
1699
1768
|
if (this.#keylessProviders.has(provider) && !this.authStorage.hasAuth(provider)) {
|
|
1700
1769
|
return kNoAuth;
|
|
1701
1770
|
}
|
|
@@ -1819,11 +1888,9 @@ export class ModelRegistry {
|
|
|
1819
1888
|
}
|
|
1820
1889
|
|
|
1821
1890
|
if (config.apiKey) {
|
|
1822
|
-
this.#
|
|
1891
|
+
this.#installProviderApiKey(providerName, config.apiKey);
|
|
1823
1892
|
// Persist runtime API keys so they survive #reloadStaticModels() cycles
|
|
1824
1893
|
this.#runtimeProviderApiKeys.set(providerName, config.apiKey);
|
|
1825
|
-
const resolved = resolveApiKeyConfig(config.apiKey);
|
|
1826
|
-
if (resolved) this.authStorage.setConfigApiKey(providerName, resolved);
|
|
1827
1894
|
}
|
|
1828
1895
|
|
|
1829
1896
|
if (config.models && config.models.length > 0) {
|
|
@@ -1892,7 +1959,7 @@ export class ModelRegistry {
|
|
|
1892
1959
|
cacheTtlMs: 24 * 60 * 60 * 1000,
|
|
1893
1960
|
dynamicModelsAuthoritative: true,
|
|
1894
1961
|
fetchDynamicModels: async () => {
|
|
1895
|
-
const apiKey = await this
|
|
1962
|
+
const apiKey = await this.#peekApiKeyForProvider(providerName);
|
|
1896
1963
|
const resolvedKey = isAuthenticated(apiKey) ? apiKey : undefined;
|
|
1897
1964
|
const modelDefs = await fetcher(resolvedKey);
|
|
1898
1965
|
const results: Model<Api>[] = [];
|
|
@@ -1058,6 +1058,66 @@ export async function resolveAllowedModels(
|
|
|
1058
1058
|
return available.filter(model => allowed.has(`${model.provider}/${model.id}`));
|
|
1059
1059
|
}
|
|
1060
1060
|
|
|
1061
|
+
/**
|
|
1062
|
+
* Synchronous subset of {@link resolveAllowedModels} for contexts where async is unavailable
|
|
1063
|
+
* (e.g. `getAvailableModels()` which is called from the ACP model-list advertisement, RPC
|
|
1064
|
+
* `get_available_models`, and the `/model` slash command). Uses the same effective
|
|
1065
|
+
* `enabledModels` scope semantics as startup resolution:
|
|
1066
|
+
*
|
|
1067
|
+
* - Glob selectors match `provider/modelId` and bare model id
|
|
1068
|
+
* - Exact canonical ids expand to all available concrete variants
|
|
1069
|
+
* - Exact `provider/modelId`, bare ids, provider-scoped fuzzy, and substring selectors
|
|
1070
|
+
* resolve through the shared model-pattern matcher
|
|
1071
|
+
* - Optional `:thinkingLevel` suffixes are stripped only when valid
|
|
1072
|
+
*
|
|
1073
|
+
* When no pattern resolves to any model (misconfiguration / typo) an empty list is returned,
|
|
1074
|
+
* consistent with the empty-list contract of {@link resolveAllowedModels}. Callers that render
|
|
1075
|
+
* a UI picker should treat an empty list as "hide the picker entry", matching how the SDK
|
|
1076
|
+
* surfaces the same misconfiguration during session initialization.
|
|
1077
|
+
*/
|
|
1078
|
+
export function filterAvailableModelsByEnabledPatterns(
|
|
1079
|
+
available: Model<Api>[],
|
|
1080
|
+
patterns: readonly string[],
|
|
1081
|
+
registry: Pick<ModelRegistry, "getCanonicalVariants">,
|
|
1082
|
+
): Model<Api>[] {
|
|
1083
|
+
if (patterns.length === 0) return available;
|
|
1084
|
+
|
|
1085
|
+
const context = buildPreferenceContext(available, undefined);
|
|
1086
|
+
const allowed = new Set<string>();
|
|
1087
|
+
const addAllowed = (model: Model<Api>) => {
|
|
1088
|
+
allowed.add(`${model.provider}/${model.id}`);
|
|
1089
|
+
};
|
|
1090
|
+
|
|
1091
|
+
for (const pattern of patterns) {
|
|
1092
|
+
if (pattern.includes("*") || pattern.includes("?") || pattern.includes("[")) {
|
|
1093
|
+
const { base: globPattern } = splitThinkingSuffix(pattern);
|
|
1094
|
+
const glob = new Bun.Glob(globPattern.toLowerCase());
|
|
1095
|
+
for (const model of available) {
|
|
1096
|
+
const fullId = `${model.provider}/${model.id}`.toLowerCase();
|
|
1097
|
+
if (glob.match(fullId) || glob.match(model.id.toLowerCase())) {
|
|
1098
|
+
addAllowed(model);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
const exactCanonical = resolveExactCanonicalScopePattern(pattern, registry, available);
|
|
1105
|
+
if (exactCanonical) {
|
|
1106
|
+
for (const model of exactCanonical.models) {
|
|
1107
|
+
addAllowed(model);
|
|
1108
|
+
}
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
const { model } = parseModelPatternWithContext(pattern, available, context, { modelRegistry: registry });
|
|
1113
|
+
if (model) {
|
|
1114
|
+
addAllowed(model);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
return allowed.size === 0 ? [] : available.filter(model => allowed.has(`${model.provider}/${model.id}`));
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1061
1121
|
export interface ResolveCliModelResult {
|
|
1062
1122
|
model: Model<Api> | undefined;
|
|
1063
1123
|
selector?: string;
|
|
@@ -151,7 +151,7 @@ export type AnyUiMetadata = UiBase & {
|
|
|
151
151
|
|
|
152
152
|
interface BooleanDef {
|
|
153
153
|
type: "boolean";
|
|
154
|
-
default: boolean;
|
|
154
|
+
default: boolean | undefined;
|
|
155
155
|
ui?: UiBoolean;
|
|
156
156
|
}
|
|
157
157
|
|
|
@@ -2067,6 +2067,20 @@ export const SETTINGS_SCHEMA = {
|
|
|
2067
2067
|
type: "number",
|
|
2068
2068
|
default: 4 * 1024 * 1024,
|
|
2069
2069
|
},
|
|
2070
|
+
"shellMinimizer.sourceOutlineLevel": {
|
|
2071
|
+
type: "enum",
|
|
2072
|
+
values: ["default", "aggressive"] as const,
|
|
2073
|
+
default: "default",
|
|
2074
|
+
ui: {
|
|
2075
|
+
tab: "editing",
|
|
2076
|
+
label: "Shell Minimizer Source Outline",
|
|
2077
|
+
description: "Source outline mode for cat/read of source files: default or aggressive",
|
|
2078
|
+
},
|
|
2079
|
+
},
|
|
2080
|
+
"shellMinimizer.legacyFilters": {
|
|
2081
|
+
type: "boolean",
|
|
2082
|
+
default: undefined,
|
|
2083
|
+
},
|
|
2070
2084
|
|
|
2071
2085
|
// Eval (per-backend toggles; add more as new backends ship, e.g. eval.ts)
|
|
2072
2086
|
"eval.py": {
|
|
@@ -2100,6 +2114,16 @@ export const SETTINGS_SCHEMA = {
|
|
|
2100
2114
|
description: "Whether to keep IPython kernel alive across calls",
|
|
2101
2115
|
},
|
|
2102
2116
|
},
|
|
2117
|
+
"python.interpreter": {
|
|
2118
|
+
type: "string",
|
|
2119
|
+
default: "",
|
|
2120
|
+
ui: {
|
|
2121
|
+
tab: "editing",
|
|
2122
|
+
label: "Python Interpreter",
|
|
2123
|
+
description:
|
|
2124
|
+
"Optional path to an exact Python executable. When set, automatic Python runtime discovery is skipped.",
|
|
2125
|
+
},
|
|
2126
|
+
},
|
|
2103
2127
|
|
|
2104
2128
|
// ────────────────────────────────────────────────────────────────────────
|
|
2105
2129
|
// Tools
|
|
@@ -3257,21 +3281,23 @@ type Schema = typeof SETTINGS_SCHEMA;
|
|
|
3257
3281
|
export type SettingPath = keyof Schema;
|
|
3258
3282
|
|
|
3259
3283
|
/** Infer the value type for a setting path */
|
|
3260
|
-
export type SettingValue<P extends SettingPath> = Schema[P] extends { type: "boolean" }
|
|
3261
|
-
? boolean
|
|
3262
|
-
: Schema[P] extends { type: "
|
|
3263
|
-
?
|
|
3264
|
-
: Schema[P] extends { type: "
|
|
3265
|
-
?
|
|
3266
|
-
: Schema[P] extends { type: "
|
|
3267
|
-
?
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
: Schema[P] extends { type: "
|
|
3284
|
+
export type SettingValue<P extends SettingPath> = Schema[P] extends { type: "boolean"; default: undefined }
|
|
3285
|
+
? boolean | undefined
|
|
3286
|
+
: Schema[P] extends { type: "boolean" }
|
|
3287
|
+
? boolean
|
|
3288
|
+
: Schema[P] extends { type: "string" }
|
|
3289
|
+
? string | undefined
|
|
3290
|
+
: Schema[P] extends { type: "number" }
|
|
3291
|
+
? number
|
|
3292
|
+
: Schema[P] extends { type: "enum"; values: infer V }
|
|
3293
|
+
? V extends readonly string[]
|
|
3294
|
+
? V[number]
|
|
3295
|
+
: never
|
|
3296
|
+
: Schema[P] extends { type: "array"; default: infer D }
|
|
3273
3297
|
? D
|
|
3274
|
-
:
|
|
3298
|
+
: Schema[P] extends { type: "record"; default: infer D }
|
|
3299
|
+
? D
|
|
3300
|
+
: never;
|
|
3275
3301
|
|
|
3276
3302
|
/** Get the default value for a setting path */
|
|
3277
3303
|
export function getDefault<P extends SettingPath>(path: P): SettingValue<P> {
|
|
@@ -3461,6 +3487,8 @@ export interface ShellMinimizerSettings {
|
|
|
3461
3487
|
only: string[];
|
|
3462
3488
|
except: string[];
|
|
3463
3489
|
maxCaptureBytes: number;
|
|
3490
|
+
sourceOutlineLevel: "default" | "aggressive";
|
|
3491
|
+
legacyFilters: boolean | undefined;
|
|
3464
3492
|
}
|
|
3465
3493
|
|
|
3466
3494
|
/** Map group prefix -> typed settings interface */
|