@oh-my-pi/pi-coding-agent 13.9.15 → 13.9.16

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 CHANGED
@@ -2,6 +2,44 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [13.9.16] - 2026-03-10
6
+ ### Breaking Changes
7
+
8
+ - Web search tool no longer accepts `provider` parameter in tool calls; use internal provider resolution instead
9
+ - Removed `no_fallback` option from search parameters
10
+
11
+ ### Added
12
+
13
+ - Added `before_provider_request` extension event to intercept and modify provider request payloads before sending
14
+ - Added `emitBeforeProviderRequest()` method to ExtensionRunner for chaining payload transformations across extensions
15
+ - Added `refreshInBackground()` method to ModelRegistry for non-blocking model discovery
16
+ - Added `refreshProvider()` method to refresh models for a specific provider on demand
17
+ - Added `getDiscoverableProviders()` method to list all configured discoverable providers
18
+ - Added `getProviderDiscoveryState()` method to inspect provider discovery status, cache age, and errors
19
+ - Added provider discovery state tracking with status indicators (idle, ok, cached, unavailable, unauthenticated)
20
+ - Added model caching with 24-hour TTL to preserve discovered models across sessions
21
+ - Added provider-specific empty state messages in model selector showing cache age and discovery status
22
+ - Added live provider refresh when switching provider tabs in model selector
23
+
24
+ ### Changed
25
+
26
+ - Changed model discovery to load cached models immediately before attempting live refresh, improving startup performance
27
+ - Changed model selector to refresh offline by default when reloading config, deferring live discovery to background
28
+ - Changed model discovery timeout from 3000ms to 250ms for faster failure detection
29
+ - Changed model discovery error handling to preserve cached models when live refresh fails
30
+ - Changed `refresh()` strategy parameter to support 'offline' mode for config-only reloads
31
+ - Changed main.ts to defer model refresh until needed (--list-models or background refresh)
32
+ - Changed SDK session creation to use background refresh instead of blocking on model discovery
33
+ - Removed `provider` parameter from web search tool schema; provider selection now handled internally
34
+ - Removed `no_fallback` parameter from web search parameters; fallback behavior now automatic based on provider availability
35
+ - Renamed `SearchParams` type to `SearchToolParams` for tool execution; introduced `SearchQueryParams` for CLI queries with optional provider selection
36
+
37
+ ### Fixed
38
+
39
+ - Fixed model discovery to continue using cached models when provider is temporarily unavailable
40
+ - Fixed unauthenticated provider discovery to preserve cached models instead of discarding them
41
+ - Fixed model selector to show discovery status messages when provider has no models
42
+
5
43
  ## [13.9.15] - 2026-03-10
6
44
  ### Added
7
45
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "13.9.15",
4
+ "version": "13.9.16",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -41,12 +41,12 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@mozilla/readability": "^0.6",
44
- "@oh-my-pi/omp-stats": "13.9.15",
45
- "@oh-my-pi/pi-agent-core": "13.9.15",
46
- "@oh-my-pi/pi-ai": "13.9.15",
47
- "@oh-my-pi/pi-natives": "13.9.15",
48
- "@oh-my-pi/pi-tui": "13.9.15",
49
- "@oh-my-pi/pi-utils": "13.9.15",
44
+ "@oh-my-pi/omp-stats": "13.9.16",
45
+ "@oh-my-pi/pi-agent-core": "13.9.16",
46
+ "@oh-my-pi/pi-ai": "13.9.16",
47
+ "@oh-my-pi/pi-natives": "13.9.16",
48
+ "@oh-my-pi/pi-tui": "13.9.16",
49
+ "@oh-my-pi/pi-utils": "13.9.16",
50
50
  "@sinclair/typebox": "^0.34",
51
51
  "@xterm/headless": "^6.0",
52
52
  "ajv": "^8.18",
@@ -7,7 +7,7 @@
7
7
  import { APP_NAME } from "@oh-my-pi/pi-utils";
8
8
  import chalk from "chalk";
9
9
  import { initTheme, theme } from "../modes/theme/theme";
10
- import { runSearchQuery, type SearchParams } from "../web/search/index";
10
+ import { runSearchQuery, type SearchQueryParams } from "../web/search/index";
11
11
  import { SEARCH_PROVIDER_ORDER } from "../web/search/provider";
12
12
  import { renderSearchResult } from "../web/search/render";
13
13
  import type { SearchProviderId } from "../web/search/types";
@@ -87,18 +87,16 @@ export async function runSearchCommand(cmd: SearchCommandArgs): Promise<void> {
87
87
 
88
88
  await initTheme();
89
89
 
90
- const params: SearchParams = {
90
+ const params: SearchQueryParams = {
91
91
  query: cmd.query,
92
92
  provider: cmd.provider,
93
93
  recency: cmd.recency,
94
94
  limit: cmd.limit,
95
- no_fallback: cmd.provider !== undefined && cmd.provider !== "auto",
96
95
  };
97
96
 
98
97
  const result = await runSearchQuery(params);
99
98
  const component = renderSearchResult(result, { expanded: cmd.expanded, isPartial: false }, theme, {
100
99
  query: cmd.query,
101
- provider: cmd.provider,
102
100
  allowLongAnswer: true,
103
101
  maxAnswerLines: cmd.expanded ? undefined : 6,
104
102
  });
@@ -1,3 +1,4 @@
1
+ import * as path from "node:path";
1
2
  import {
2
3
  type Api,
3
4
  type AssistantMessageEventStream,
@@ -16,6 +17,7 @@ import {
16
17
  type OAuthLoginCallbacks,
17
18
  openaiCodexModelManagerOptions,
18
19
  PROVIDER_DESCRIPTORS,
20
+ readModelCache,
19
21
  registerCustomApi,
20
22
  registerOAuthProvider,
21
23
  type SimpleStreamOptions,
@@ -308,14 +310,19 @@ interface DiscoveryProviderConfig {
308
310
  baseUrl?: string;
309
311
  headers?: Record<string, string>;
310
312
  discovery: ProviderDiscovery;
313
+ optional?: boolean;
311
314
  }
312
315
 
313
- /**
314
- * Serialized representation of ModelRegistry for passing to subagent workers.
315
- */
316
- export interface SerializedModelRegistry {
317
- models: Model<Api>[];
318
- customProviderApiKeys?: Record<string, string>;
316
+ export type ProviderDiscoveryStatus = "idle" | "ok" | "cached" | "unavailable" | "unauthenticated";
317
+
318
+ export interface ProviderDiscoveryState {
319
+ provider: string;
320
+ status: ProviderDiscoveryStatus;
321
+ optional: boolean;
322
+ stale: boolean;
323
+ fetchedAt?: number;
324
+ models: string[];
325
+ error?: string;
319
326
  }
320
327
 
321
328
  /** Result of loading custom models from models.json */
@@ -507,6 +514,10 @@ export class ModelRegistry {
507
514
  #configError: ConfigError | undefined = undefined;
508
515
  #modelsConfigFile: ConfigFile<ModelsConfig>;
509
516
  #registeredProviderSources: Set<string> = new Set();
517
+ #providerDiscoveryStates: Map<string, ProviderDiscoveryState> = new Map();
518
+ #cacheDbPath?: string;
519
+ #backgroundRefresh?: Promise<void>;
520
+ #lastDiscoveryWarnings: Map<string, string> = new Map();
510
521
 
511
522
  /**
512
523
  * @param authStorage - Auth storage for API key resolution
@@ -516,6 +527,7 @@ export class ModelRegistry {
516
527
  modelsPath?: string,
517
528
  ) {
518
529
  this.#modelsConfigFile = ModelsConfigFile.relocate(modelsPath);
530
+ this.#cacheDbPath = modelsPath ? path.join(path.dirname(modelsPath), "models.db") : undefined;
519
531
  // Set up fallback resolver for custom provider API keys
520
532
  this.authStorage.setFallbackResolver(provider => {
521
533
  const keyConfig = this.#customProviderApiKeys.get(provider);
@@ -532,14 +544,42 @@ export class ModelRegistry {
532
544
  * Reload models from disk (built-in + custom from models.json).
533
545
  */
534
546
  async refresh(strategy: ModelRefreshStrategy = "online-if-uncached"): Promise<void> {
547
+ this.#reloadStaticModels();
548
+ await this.#refreshRuntimeDiscoveries(strategy);
549
+ }
550
+
551
+ refreshInBackground(strategy: ModelRefreshStrategy = "online-if-uncached"): void {
552
+ if (this.#backgroundRefresh) {
553
+ return;
554
+ }
555
+ const refreshPromise = this.refresh(strategy)
556
+ .catch(error => {
557
+ logger.warn("background model refresh failed", {
558
+ error: error instanceof Error ? error.message : String(error),
559
+ });
560
+ })
561
+ .finally(() => {
562
+ if (this.#backgroundRefresh === refreshPromise) {
563
+ this.#backgroundRefresh = undefined;
564
+ }
565
+ });
566
+ this.#backgroundRefresh = refreshPromise;
567
+ }
568
+
569
+ async refreshProvider(providerId: string, strategy: ModelRefreshStrategy = "online"): Promise<void> {
570
+ this.#reloadStaticModels();
571
+ await this.#refreshRuntimeDiscoveries(strategy, new Set([providerId]));
572
+ }
573
+
574
+ #reloadStaticModels(): void {
535
575
  this.#modelsConfigFile.invalidate();
536
576
  this.#customProviderApiKeys.clear();
537
577
  this.#keylessProviders.clear();
538
578
  this.#discoverableProviders = [];
539
579
  this.#modelOverrides.clear();
540
580
  this.#configError = undefined;
581
+ this.#providerDiscoveryStates.clear();
541
582
  this.#loadModels();
542
- await this.#refreshRuntimeDiscoveries(strategy);
543
583
  }
544
584
 
545
585
  /**
@@ -567,7 +607,8 @@ export class ModelRegistry {
567
607
 
568
608
  this.#addImplicitDiscoverableProviders(configuredProviders);
569
609
  const builtInModels = this.#loadBuiltInModels(overrides, modelOverrides);
570
- const combined = this.#mergeCustomModels(builtInModels, customModels);
610
+ const cachedDiscoveries = this.#loadCachedDiscoverableModels();
611
+ const combined = this.#mergeCustomModels(builtInModels, [...customModels, ...cachedDiscoveries]);
571
612
 
572
613
  this.#models = this.#applyHardcodedModelPolicies(combined);
573
614
  }
@@ -614,6 +655,34 @@ export class ModelRegistry {
614
655
  return merged;
615
656
  }
616
657
 
658
+ #loadCachedDiscoverableModels(): Model<Api>[] {
659
+ const cachedModels: Model<Api>[] = [];
660
+ for (const providerConfig of this.#discoverableProviders) {
661
+ const cache = readModelCache<Api>(providerConfig.provider, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
662
+ if (!cache) {
663
+ this.#providerDiscoveryStates.set(providerConfig.provider, {
664
+ provider: providerConfig.provider,
665
+ status: "idle",
666
+ optional: providerConfig.optional ?? false,
667
+ stale: false,
668
+ models: [],
669
+ });
670
+ continue;
671
+ }
672
+ const models = this.#applyProviderModelOverrides(providerConfig.provider, cache.models);
673
+ cachedModels.push(...models);
674
+ this.#providerDiscoveryStates.set(providerConfig.provider, {
675
+ provider: providerConfig.provider,
676
+ status: "cached",
677
+ optional: providerConfig.optional ?? false,
678
+ stale: !cache.fresh || !cache.authoritative,
679
+ fetchedAt: cache.updatedAt,
680
+ models: models.map(model => model.id),
681
+ });
682
+ }
683
+ return cachedModels;
684
+ }
685
+
617
686
  #addImplicitDiscoverableProviders(configuredProviders: Set<string>): void {
618
687
  if (!configuredProviders.has("ollama")) {
619
688
  this.#discoverableProviders.push({
@@ -621,6 +690,7 @@ export class ModelRegistry {
621
690
  api: "openai-completions",
622
691
  baseUrl: Bun.env.OLLAMA_BASE_URL || "http://127.0.0.1:11434",
623
692
  discovery: { type: "ollama" },
693
+ optional: true,
624
694
  });
625
695
  this.#keylessProviders.add("ollama");
626
696
  }
@@ -630,6 +700,7 @@ export class ModelRegistry {
630
700
  api: "openai-completions",
631
701
  baseUrl: Bun.env.LM_STUDIO_BASE_URL || "http://127.0.0.1:1234/v1",
632
702
  discovery: { type: "lm-studio" },
703
+ optional: true,
633
704
  });
634
705
  this.#keylessProviders.add("lm-studio");
635
706
  }
@@ -689,6 +760,7 @@ export class ModelRegistry {
689
760
  baseUrl: providerConfig.baseUrl,
690
761
  headers: providerConfig.headers,
691
762
  discovery: providerConfig.discovery,
763
+ optional: false,
692
764
  });
693
765
  }
694
766
 
@@ -718,16 +790,22 @@ export class ModelRegistry {
718
790
  };
719
791
  }
720
792
 
721
- async #refreshRuntimeDiscoveries(strategy: ModelRefreshStrategy): Promise<void> {
793
+ async #refreshRuntimeDiscoveries(
794
+ strategy: ModelRefreshStrategy,
795
+ providerFilter?: ReadonlySet<string>,
796
+ ): Promise<void> {
797
+ const selectedDiscoverableProviders = providerFilter
798
+ ? this.#discoverableProviders.filter(provider => providerFilter.has(provider.provider))
799
+ : this.#discoverableProviders;
722
800
  const configuredDiscoveriesPromise =
723
- this.#discoverableProviders.length === 0
801
+ selectedDiscoverableProviders.length === 0
724
802
  ? Promise.resolve<Model<Api>[]>([])
725
- : Promise.all(this.#discoverableProviders.map(provider => this.#discoverProviderModels(provider))).then(
726
- results => results.flat(),
727
- );
803
+ : Promise.all(
804
+ selectedDiscoverableProviders.map(provider => this.#discoverProviderModels(provider, strategy)),
805
+ ).then(results => results.flat());
728
806
  const [configuredDiscovered, builtInDiscovered] = await Promise.all([
729
807
  configuredDiscoveriesPromise,
730
- this.#discoverBuiltInProviderModels(strategy),
808
+ this.#discoverBuiltInProviderModels(strategy, providerFilter),
731
809
  ]);
732
810
  const discovered = [...configuredDiscovered, ...builtInDiscovered];
733
811
  if (discovered.length === 0) {
@@ -751,21 +829,100 @@ export class ModelRegistry {
751
829
  this.#models = this.#applyHardcodedModelPolicies(this.#applyModelOverrides(merged, this.#modelOverrides));
752
830
  }
753
831
 
754
- async #discoverProviderModels(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
755
- switch (providerConfig.discovery.type) {
756
- case "ollama":
757
- return this.#discoverOllamaModels(providerConfig);
758
- case "lm-studio":
759
- return this.#discoverLmStudioModels(providerConfig);
832
+ async #discoverProviderModels(
833
+ providerConfig: DiscoveryProviderConfig,
834
+ strategy: ModelRefreshStrategy,
835
+ ): Promise<Model<Api>[]> {
836
+ const cached = readModelCache<Api>(providerConfig.provider, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
837
+ const requiresAuth = !this.#keylessProviders.has(providerConfig.provider);
838
+ if (requiresAuth) {
839
+ const apiKey = await this.#peekApiKeyForProvider(providerConfig.provider);
840
+ if (!isAuthenticated(apiKey)) {
841
+ this.#providerDiscoveryStates.set(providerConfig.provider, {
842
+ provider: providerConfig.provider,
843
+ status: "unauthenticated",
844
+ optional: providerConfig.optional ?? false,
845
+ stale: cached !== null,
846
+ fetchedAt: cached?.updatedAt,
847
+ models: cached?.models.map(model => model.id) ?? [],
848
+ });
849
+ this.#lastDiscoveryWarnings.delete(providerConfig.provider);
850
+ return cached?.models ?? [];
851
+ }
760
852
  }
853
+
854
+ let fetchError: string | undefined;
855
+ const fetchDynamicModels = async (): Promise<readonly Model<Api>[] | null> => {
856
+ try {
857
+ const models =
858
+ providerConfig.discovery.type === "ollama"
859
+ ? await this.#discoverOllamaModels(providerConfig)
860
+ : await this.#discoverLmStudioModels(providerConfig);
861
+ this.#lastDiscoveryWarnings.delete(providerConfig.provider);
862
+ return models;
863
+ } catch (error) {
864
+ fetchError = error instanceof Error ? error.message : String(error);
865
+ return null;
866
+ }
867
+ };
868
+
869
+ const manager = createModelManager<Api>({
870
+ providerId: providerConfig.provider,
871
+ staticModels: [],
872
+ cacheDbPath: this.#cacheDbPath,
873
+ cacheTtlMs: 24 * 60 * 60 * 1000,
874
+ fetchDynamicModels,
875
+ });
876
+ const result = await manager.refresh(strategy);
877
+ const status = fetchError
878
+ ? result.models.length > 0
879
+ ? "cached"
880
+ : "unavailable"
881
+ : result.models.length > 0 && strategy !== "offline"
882
+ ? "ok"
883
+ : cached
884
+ ? "cached"
885
+ : "idle";
886
+ this.#providerDiscoveryStates.set(providerConfig.provider, {
887
+ provider: providerConfig.provider,
888
+ status,
889
+ optional: providerConfig.optional ?? false,
890
+ stale: result.stale || status === "cached",
891
+ fetchedAt: fetchError ? cached?.updatedAt : Date.now(),
892
+ models: result.models.map(model => model.id),
893
+ error: fetchError,
894
+ });
895
+ if (fetchError) {
896
+ this.#warnProviderDiscoveryFailure(providerConfig, fetchError);
897
+ }
898
+ return this.#applyProviderModelOverrides(providerConfig.provider, result.models);
899
+ }
900
+
901
+ #warnProviderDiscoveryFailure(providerConfig: DiscoveryProviderConfig, error: string): void {
902
+ const previous = this.#lastDiscoveryWarnings.get(providerConfig.provider);
903
+ if (previous === error) {
904
+ return;
905
+ }
906
+ this.#lastDiscoveryWarnings.set(providerConfig.provider, error);
907
+ logger.warn("model discovery failed for provider", {
908
+ provider: providerConfig.provider,
909
+ url: providerConfig.baseUrl,
910
+ error,
911
+ });
761
912
  }
762
913
 
763
- async #discoverBuiltInProviderModels(strategy: ModelRefreshStrategy): Promise<Model<Api>[]> {
914
+ async #discoverBuiltInProviderModels(
915
+ strategy: ModelRefreshStrategy,
916
+ providerFilter?: ReadonlySet<string>,
917
+ ): Promise<Model<Api>[]> {
764
918
  // Skip providers already handled by configured discovery (e.g. user-configured ollama with discovery.type)
765
919
  const configuredDiscoveryProviders = new Set(this.#discoverableProviders.map(p => p.provider));
766
- const managerOptions = (await this.#collectBuiltInModelManagerOptions()).filter(
767
- opts => !configuredDiscoveryProviders.has(opts.providerId),
768
- );
920
+ const managerOptions = (await this.#collectBuiltInModelManagerOptions()).filter(opts => {
921
+ if (configuredDiscoveryProviders.has(opts.providerId)) {
922
+ return false;
923
+ }
924
+ return providerFilter ? providerFilter.has(opts.providerId) : true;
925
+ });
769
926
  if (managerOptions.length === 0) {
770
927
  return [];
771
928
  }
@@ -849,7 +1006,7 @@ export class ModelRegistry {
849
1006
  strategy: ModelRefreshStrategy,
850
1007
  ): Promise<Model<Api>[]> {
851
1008
  try {
852
- const manager = createModelManager(options);
1009
+ const manager = createModelManager({ ...options, cacheDbPath: this.#cacheDbPath });
853
1010
  const result = await manager.refresh(strategy);
854
1011
  return result.models.map(model =>
855
1012
  model.provider === options.providerId ? model : { ...model, provider: options.providerId },
@@ -874,7 +1031,7 @@ export class ModelRegistry {
874
1031
  method: "POST",
875
1032
  headers: { ...(headers ?? {}), "Content-Type": "application/json" },
876
1033
  body: JSON.stringify({ model: modelId }),
877
- signal: AbortSignal.timeout(1500),
1034
+ signal: AbortSignal.timeout(150),
878
1035
  });
879
1036
  if (!response.ok) {
880
1037
  return null;
@@ -911,57 +1068,42 @@ export class ModelRegistry {
911
1068
  const endpoint = this.#normalizeOllamaBaseUrl(providerConfig.baseUrl);
912
1069
  const tagsUrl = `${endpoint}/api/tags`;
913
1070
  const headers = { ...(providerConfig.headers ?? {}) };
914
- try {
915
- const response = await fetch(tagsUrl, {
916
- headers,
917
- signal: AbortSignal.timeout(3000),
918
- });
919
- if (!response.ok) {
920
- logger.warn("model discovery failed for provider", {
921
- provider: providerConfig.provider,
922
- status: response.status,
923
- url: tagsUrl,
924
- });
925
- return [];
926
- }
927
- const payload = (await response.json()) as { models?: Array<{ name?: string; model?: string }> };
928
- const entries = (payload.models ?? []).flatMap(item => {
929
- const id = item.model || item.name;
930
- return id ? [{ id, name: item.name || id }] : [];
931
- });
932
- const metadataById = new Map(
933
- await Promise.all(
934
- entries.map(
935
- async entry =>
936
- [entry.id, await this.#discoverOllamaModelMetadata(endpoint, entry.id, headers)] as const,
937
- ),
1071
+ const response = await fetch(tagsUrl, {
1072
+ headers,
1073
+ signal: AbortSignal.timeout(250),
1074
+ });
1075
+ if (!response.ok) {
1076
+ throw new Error(`HTTP ${response.status} from ${tagsUrl}`);
1077
+ }
1078
+ const payload = (await response.json()) as { models?: Array<{ name?: string; model?: string }> };
1079
+ const entries = (payload.models ?? []).flatMap(item => {
1080
+ const id = item.model || item.name;
1081
+ return id ? [{ id, name: item.name || id }] : [];
1082
+ });
1083
+ const metadataById = new Map(
1084
+ await Promise.all(
1085
+ entries.map(
1086
+ async entry => [entry.id, await this.#discoverOllamaModelMetadata(endpoint, entry.id, headers)] as const,
938
1087
  ),
939
- );
940
- const discovered = entries.map(entry => {
941
- const metadata = metadataById.get(entry.id);
942
- return enrichModelThinking({
943
- id: entry.id,
944
- name: entry.name,
945
- api: providerConfig.api,
946
- provider: providerConfig.provider,
947
- baseUrl: `${endpoint}/v1`,
948
- reasoning: metadata?.reasoning ?? false,
949
- input: metadata?.input ?? ["text"],
950
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
951
- contextWindow: 128000,
952
- maxTokens: 8192,
953
- headers: providerConfig.headers,
954
- });
955
- });
956
- return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
957
- } catch (error) {
958
- logger.warn("model discovery failed for provider", {
1088
+ ),
1089
+ );
1090
+ const discovered = entries.map(entry => {
1091
+ const metadata = metadataById.get(entry.id);
1092
+ return enrichModelThinking({
1093
+ id: entry.id,
1094
+ name: entry.name,
1095
+ api: providerConfig.api,
959
1096
  provider: providerConfig.provider,
960
- url: tagsUrl,
961
- error: error instanceof Error ? error.message : String(error),
1097
+ baseUrl: `${endpoint}/v1`,
1098
+ reasoning: metadata?.reasoning ?? false,
1099
+ input: metadata?.input ?? ["text"],
1100
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
1101
+ contextWindow: 128000,
1102
+ maxTokens: 8192,
1103
+ headers: providerConfig.headers,
962
1104
  });
963
- return [];
964
- }
1105
+ });
1106
+ return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
965
1107
  }
966
1108
 
967
1109
  async #discoverLmStudioModels(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
@@ -974,55 +1116,41 @@ export class ModelRegistry {
974
1116
  headers.Authorization = `Bearer ${apiKey}`;
975
1117
  }
976
1118
 
977
- try {
978
- const response = await fetch(modelsUrl, {
979
- headers,
980
- signal: AbortSignal.timeout(3000),
981
- });
982
- if (!response.ok) {
983
- logger.warn("model discovery failed for provider", {
1119
+ const response = await fetch(modelsUrl, {
1120
+ headers,
1121
+ signal: AbortSignal.timeout(250),
1122
+ });
1123
+ if (!response.ok) {
1124
+ throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
1125
+ }
1126
+ const payload = (await response.json()) as { data?: Array<{ id: string }> };
1127
+ const models = payload.data ?? [];
1128
+ const discovered: Model<Api>[] = [];
1129
+ for (const item of models) {
1130
+ const id = item.id;
1131
+ if (!id) continue;
1132
+ discovered.push(
1133
+ enrichModelThinking({
1134
+ id,
1135
+ name: id,
1136
+ api: providerConfig.api,
984
1137
  provider: providerConfig.provider,
985
- status: response.status,
986
- url: modelsUrl,
987
- });
988
- return [];
989
- }
990
- const payload = (await response.json()) as { data?: Array<{ id: string }> };
991
- const models = payload.data ?? [];
992
- const discovered: Model<Api>[] = [];
993
- for (const item of models) {
994
- const id = item.id;
995
- if (!id) continue;
996
- discovered.push(
997
- enrichModelThinking({
998
- id,
999
- name: id,
1000
- api: providerConfig.api,
1001
- provider: providerConfig.provider,
1002
- baseUrl,
1003
- reasoning: false,
1004
- input: ["text"],
1005
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
1006
- contextWindow: 128000,
1007
- maxTokens: 8192,
1008
- headers,
1009
- compat: {
1010
- supportsStore: false,
1011
- supportsDeveloperRole: false,
1012
- supportsReasoningEffort: false,
1013
- },
1014
- }),
1015
- );
1016
- }
1017
- return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
1018
- } catch (error) {
1019
- logger.warn("model discovery failed for provider", {
1020
- provider: providerConfig.provider,
1021
- url: modelsUrl,
1022
- error: error instanceof Error ? error.message : String(error),
1023
- });
1024
- return [];
1138
+ baseUrl,
1139
+ reasoning: false,
1140
+ input: ["text"],
1141
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
1142
+ contextWindow: 128000,
1143
+ maxTokens: 8192,
1144
+ headers,
1145
+ compat: {
1146
+ supportsStore: false,
1147
+ supportsDeveloperRole: false,
1148
+ supportsReasoningEffort: false,
1149
+ },
1150
+ }),
1151
+ );
1025
1152
  }
1153
+ return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
1026
1154
  }
1027
1155
 
1028
1156
  #normalizeLmStudioBaseUrl(baseUrl?: string): string {
@@ -1119,6 +1247,14 @@ export class ModelRegistry {
1119
1247
  return this.#models.filter(m => this.#keylessProviders.has(m.provider) || this.authStorage.hasAuth(m.provider));
1120
1248
  }
1121
1249
 
1250
+ getDiscoverableProviders(): string[] {
1251
+ return this.#discoverableProviders.map(provider => provider.provider);
1252
+ }
1253
+
1254
+ getProviderDiscoveryState(provider: string): ProviderDiscoveryState | undefined {
1255
+ return this.#providerDiscoveryStates.get(provider);
1256
+ }
1257
+
1122
1258
  /**
1123
1259
  * Find a model by provider and ID.
1124
1260
  */
@@ -1140,7 +1276,7 @@ export class ModelRegistry {
1140
1276
  if (this.#keylessProviders.has(model.provider)) {
1141
1277
  return kNoAuth;
1142
1278
  }
1143
- return this.authStorage.getApiKey(model.provider, sessionId, { baseUrl: model.baseUrl });
1279
+ return this.authStorage.getApiKey(model.provider, sessionId, { baseUrl: model.baseUrl, modelId: model.id });
1144
1280
  }
1145
1281
 
1146
1282
  /**
@@ -242,6 +242,16 @@ export const SETTINGS_SCHEMA = {
242
242
  description: "Action when pressing Escape twice with empty editor",
243
243
  },
244
244
  },
245
+ treeFilterMode: {
246
+ type: "enum",
247
+ values: ["default", "no-tools", "user-only", "labeled-only", "all"] as const,
248
+ default: "default",
249
+ ui: {
250
+ tab: "input",
251
+ label: "Tree filter mode",
252
+ description: "Default filter mode when opening the session tree",
253
+ },
254
+ },
245
255
  shellPath: { type: "string", default: undefined },
246
256
  collapseChangelog: {
247
257
  type: "boolean",
@@ -1310,6 +1320,9 @@ export type StatusLinePreset = SettingValue<"statusLine.preset">;
1310
1320
  /** Status line separator style - derived from schema */
1311
1321
  export type StatusLineSeparatorStyle = SettingValue<"statusLine.separator">;
1312
1322
 
1323
+ /** Tree selector filter mode - derived from schema */
1324
+ export type TreeFilterMode = SettingValue<"treeFilterMode">;
1325
+
1313
1326
  // ═══════════════════════════════════════════════════════════════════════════
1314
1327
  // Typed Group Definitions
1315
1328
  // ═══════════════════════════════════════════════════════════════════════════