@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.
Files changed (121) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/dist/cli.js +5349 -5328
  3. package/dist/types/cli/args.d.ts +1 -0
  4. package/dist/types/cli-commands.d.ts +12 -0
  5. package/dist/types/commands/launch.d.ts +4 -0
  6. package/dist/types/config/api-key-resolver.d.ts +3 -0
  7. package/dist/types/config/model-registry.d.ts +1 -0
  8. package/dist/types/config/model-resolver.d.ts +18 -0
  9. package/dist/types/config/settings-schema.d.ts +29 -1
  10. package/dist/types/config/settings.d.ts +7 -0
  11. package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
  12. package/dist/types/eval/py/executor.d.ts +5 -0
  13. package/dist/types/eval/py/kernel.d.ts +6 -1
  14. package/dist/types/eval/py/runtime.d.ts +9 -0
  15. package/dist/types/exec/bash-executor.d.ts +2 -0
  16. package/dist/types/extensibility/extensions/runner.d.ts +3 -2
  17. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  18. package/dist/types/memory-backend/index.d.ts +1 -0
  19. package/dist/types/memory-backend/runtime.d.ts +4 -0
  20. package/dist/types/memory-backend/types.d.ts +66 -1
  21. package/dist/types/modes/index.d.ts +3 -3
  22. package/dist/types/modes/interactive-mode.d.ts +7 -2
  23. package/dist/types/modes/oauth-manual-input.d.ts +7 -0
  24. package/dist/types/modes/rpc/rpc-client.d.ts +39 -2
  25. package/dist/types/modes/rpc/rpc-mode.d.ts +31 -2
  26. package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
  27. package/dist/types/modes/rpc/rpc-types.d.ts +75 -1
  28. package/dist/types/modes/setup-wizard/index.d.ts +5 -1
  29. package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
  30. package/dist/types/modes/types.d.ts +2 -0
  31. package/dist/types/secrets/index.d.ts +1 -1
  32. package/dist/types/secrets/obfuscator.d.ts +8 -2
  33. package/dist/types/session/agent-session.d.ts +14 -2
  34. package/dist/types/session/streaming-output.d.ts +23 -0
  35. package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
  36. package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
  37. package/dist/types/slash-commands/types.d.ts +1 -1
  38. package/dist/types/system-prompt.d.ts +2 -0
  39. package/dist/types/task/executor.d.ts +1 -0
  40. package/dist/types/task/index.d.ts +2 -2
  41. package/dist/types/task/types.d.ts +8 -0
  42. package/dist/types/thinking.d.ts +4 -0
  43. package/dist/types/tiny/title-client.d.ts +11 -0
  44. package/dist/types/tiny/title-protocol.d.ts +1 -0
  45. package/dist/types/tools/index.d.ts +6 -0
  46. package/dist/types/utils/git.d.ts +15 -2
  47. package/dist/types/utils/title-generator.d.ts +3 -2
  48. package/package.json +10 -10
  49. package/src/auto-thinking/classifier.ts +1 -0
  50. package/src/cli/args.ts +3 -0
  51. package/src/cli-commands.ts +29 -0
  52. package/src/cli.ts +8 -9
  53. package/src/commands/launch.ts +4 -0
  54. package/src/commit/model-selection.ts +3 -2
  55. package/src/config/api-key-resolver.ts +8 -6
  56. package/src/config/model-registry.ts +97 -30
  57. package/src/config/model-resolver.ts +60 -0
  58. package/src/config/settings-schema.ts +43 -15
  59. package/src/config/settings.ts +61 -3
  60. package/src/edit/hashline/execute.ts +39 -2
  61. package/src/edit/hashline/noop-loop-guard.ts +99 -0
  62. package/src/eval/completion-bridge.ts +1 -0
  63. package/src/eval/py/executor.ts +29 -7
  64. package/src/eval/py/index.ts +6 -1
  65. package/src/eval/py/kernel.ts +31 -11
  66. package/src/eval/py/runtime.ts +37 -0
  67. package/src/exec/bash-executor.ts +82 -3
  68. package/src/extensibility/extensions/get-commands-handler.ts +2 -1
  69. package/src/extensibility/extensions/runner.ts +6 -1
  70. package/src/extensibility/extensions/types.ts +3 -0
  71. package/src/hindsight/bank.ts +17 -2
  72. package/src/internal-urls/docs-index.generated.ts +3 -3
  73. package/src/main.ts +18 -6
  74. package/src/memories/index.ts +2 -0
  75. package/src/memory-backend/index.ts +1 -0
  76. package/src/memory-backend/local-backend.ts +9 -0
  77. package/src/memory-backend/off-backend.ts +9 -0
  78. package/src/memory-backend/runtime.ts +66 -0
  79. package/src/memory-backend/types.ts +81 -1
  80. package/src/mnemopi/backend.ts +151 -4
  81. package/src/modes/acp/acp-agent.ts +119 -11
  82. package/src/modes/components/assistant-message.ts +19 -21
  83. package/src/modes/components/footer.ts +3 -1
  84. package/src/modes/components/status-line/component.ts +118 -34
  85. package/src/modes/controllers/command-controller.ts +1 -1
  86. package/src/modes/controllers/input-controller.ts +1 -0
  87. package/src/modes/controllers/mcp-command-controller.ts +38 -3
  88. package/src/modes/index.ts +3 -21
  89. package/src/modes/interactive-mode.ts +39 -9
  90. package/src/modes/oauth-manual-input.ts +30 -3
  91. package/src/modes/rpc/rpc-client.ts +154 -3
  92. package/src/modes/rpc/rpc-mode.ts +97 -12
  93. package/src/modes/rpc/rpc-subagents.ts +265 -0
  94. package/src/modes/rpc/rpc-types.ts +81 -1
  95. package/src/modes/setup-wizard/index.ts +12 -2
  96. package/src/modes/setup-wizard/lazy.ts +16 -0
  97. package/src/modes/types.ts +2 -0
  98. package/src/sdk.ts +8 -1
  99. package/src/secrets/index.ts +8 -1
  100. package/src/secrets/obfuscator.ts +39 -18
  101. package/src/session/agent-session.ts +179 -54
  102. package/src/session/streaming-output.ts +166 -10
  103. package/src/slash-commands/acp-builtins.ts +24 -0
  104. package/src/slash-commands/builtin-registry.ts +20 -0
  105. package/src/slash-commands/types.ts +1 -1
  106. package/src/system-prompt.ts +14 -0
  107. package/src/task/executor.ts +13 -12
  108. package/src/task/index.ts +9 -8
  109. package/src/task/render.ts +18 -3
  110. package/src/task/types.ts +9 -0
  111. package/src/thinking.ts +7 -0
  112. package/src/tiny/title-client.ts +34 -5
  113. package/src/tiny/title-protocol.ts +1 -1
  114. package/src/tiny/worker.ts +6 -4
  115. package/src/tools/bash.ts +46 -5
  116. package/src/tools/image-gen.ts +11 -4
  117. package/src/tools/index.ts +13 -1
  118. package/src/tools/inspect-image.ts +1 -0
  119. package/src/utils/commit-message-generator.ts +1 -0
  120. package/src/utils/git.ts +267 -13
  121. 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, isSubcommand }] = await Promise.all([
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 first = argv[0];
171
- const runArgv =
172
- first === "--help" || first === "-h" || first === "--version" || first === "-v" || first === "help"
173
- ? argv
174
- : isSubcommand(first)
175
- ? argv
176
- : ["launch", ...argv];
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
@@ -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 an API key config value to an actual key.
231
- * Checks environment variable first, then treats as literal.
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 resolveApiKeyConfig(keyConfig: string): string | undefined {
234
- const envValue = Bun.env[keyConfig];
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 keyConfig;
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
- return mergeAuthHeader({ ...providerHeaders, ...modelHeaders }, authHeader, apiKeyConfig);
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 = resolveApiKeyConfig(apiKeyConfig);
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
- return resolveApiKeyConfig(keyConfig);
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.#customProviderApiKeys.set(k, v);
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
- providerConfig.headers ||
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: providerConfig.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: providerConfig.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.#customProviderApiKeys.set(providerName, providerConfig.apiKey);
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(modelId, override);
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.authStorage.getApiKey(provider);
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.#customProviderApiKeys.set(providerName, providerConfig.apiKey);
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
- providerConfig.headers,
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
- return this.#keylessProviders.has(model.provider) || this.authStorage.hasAuth(model.provider);
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.#customProviderApiKeys.set(providerName, config.apiKey);
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.authStorage.peekApiKey(providerName);
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: "string" }
3263
- ? string | undefined
3264
- : Schema[P] extends { type: "number" }
3265
- ? number
3266
- : Schema[P] extends { type: "enum"; values: infer V }
3267
- ? V extends readonly string[]
3268
- ? V[number]
3269
- : never
3270
- : Schema[P] extends { type: "array"; default: infer D }
3271
- ? D
3272
- : Schema[P] extends { type: "record"; default: infer D }
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
- : never;
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 */