gsd-pi 2.76.0-dev.4100bd590 → 2.76.0-dev.82e249f7b

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 (94) hide show
  1. package/dist/resources/extensions/gsd/auto/phases.js +4 -1
  2. package/dist/resources/extensions/gsd/auto/session.js +4 -0
  3. package/dist/resources/extensions/gsd/auto-model-selection.js +13 -2
  4. package/dist/resources/extensions/gsd/auto-start.js +12 -7
  5. package/dist/resources/extensions/gsd/auto.js +4 -1
  6. package/dist/resources/extensions/gsd/complexity-classifier.js +5 -3
  7. package/dist/resources/extensions/gsd/prompt-loader.js +22 -7
  8. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  9. package/dist/web/standalone/.next/BUILD_ID +1 -1
  10. package/dist/web/standalone/.next/app-path-routes-manifest.json +19 -19
  11. package/dist/web/standalone/.next/build-manifest.json +2 -2
  12. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  13. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  14. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  15. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  16. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  17. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  18. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  22. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/index.html +1 -1
  30. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app-paths-manifest.json +19 -19
  37. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  38. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  39. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  40. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  41. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  42. package/package.json +1 -1
  43. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  44. package/packages/pi-ai/dist/providers/openai-completions.js +60 -15
  45. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  46. package/packages/pi-ai/dist/providers/think-tag-parser.d.ts +17 -0
  47. package/packages/pi-ai/dist/providers/think-tag-parser.d.ts.map +1 -0
  48. package/packages/pi-ai/dist/providers/think-tag-parser.js +75 -0
  49. package/packages/pi-ai/dist/providers/think-tag-parser.js.map +1 -0
  50. package/packages/pi-ai/dist/providers/think-tag-parser.test.d.ts +2 -0
  51. package/packages/pi-ai/dist/providers/think-tag-parser.test.d.ts.map +1 -0
  52. package/packages/pi-ai/dist/providers/think-tag-parser.test.js +41 -0
  53. package/packages/pi-ai/dist/providers/think-tag-parser.test.js.map +1 -0
  54. package/packages/pi-ai/src/providers/openai-completions.ts +57 -16
  55. package/packages/pi-ai/src/providers/think-tag-parser.test.ts +44 -0
  56. package/packages/pi-ai/src/providers/think-tag-parser.ts +94 -0
  57. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  58. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +3 -1
  59. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -1
  60. package/packages/pi-coding-agent/dist/core/model-discovery.js +92 -12
  61. package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -1
  62. package/packages/pi-coding-agent/dist/core/model-discovery.test.js +16 -1
  63. package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -1
  64. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +61 -1
  65. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -1
  66. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +5 -0
  67. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  68. package/packages/pi-coding-agent/dist/core/model-registry.js +76 -10
  69. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  70. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -1
  71. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +13 -7
  72. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -1
  73. package/packages/pi-coding-agent/src/core/model-discovery.test.ts +19 -0
  74. package/packages/pi-coding-agent/src/core/model-discovery.ts +99 -12
  75. package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +75 -0
  76. package/packages/pi-coding-agent/src/core/model-registry.ts +86 -10
  77. package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +16 -7
  78. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  79. package/scripts/link-workspace-packages.cjs +1 -0
  80. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  81. package/src/resources/extensions/gsd/auto/phases.ts +4 -0
  82. package/src/resources/extensions/gsd/auto/session.ts +7 -1
  83. package/src/resources/extensions/gsd/auto-model-selection.ts +16 -1
  84. package/src/resources/extensions/gsd/auto-start.ts +12 -7
  85. package/src/resources/extensions/gsd/auto.ts +4 -1
  86. package/src/resources/extensions/gsd/complexity-classifier.ts +5 -3
  87. package/src/resources/extensions/gsd/prompt-loader.ts +30 -7
  88. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +12 -0
  89. package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +33 -3
  90. package/src/resources/extensions/gsd/tests/auto-thinking-restore.test.ts +38 -0
  91. package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +3 -3
  92. package/src/resources/extensions/gsd/tests/prompt-loader-extension-dir.test.ts +49 -0
  93. /package/dist/web/standalone/.next/static/{YnUwu2WWaT0_hyTLUF4nq → ecSsu49rxxcpbNmVP4mLD}/_buildManifest.js +0 -0
  94. /package/dist/web/standalone/.next/static/{YnUwu2WWaT0_hyTLUF4nq → ecSsu49rxxcpbNmVP4mLD}/_ssgManifest.js +0 -0
@@ -26,6 +26,14 @@ export interface ProviderDiscoveryAdapter {
26
26
  fetchModels(apiKey: string, baseUrl?: string): Promise<DiscoveredModel[]>;
27
27
  }
28
28
 
29
+ export const OPENAI_COMPAT_DISCOVERY_APIS = new Set([
30
+ "openai",
31
+ "openai-completions",
32
+ "openai-responses",
33
+ "openai-codex-responses",
34
+ "azure-openai-responses",
35
+ ]);
36
+
29
37
  /** Per-provider TTLs in milliseconds */
30
38
  export const DISCOVERY_TTLS: Record<string, number> = {
31
39
  ollama: 5 * 60 * 1000, // 5 minutes (local, models change often)
@@ -53,10 +61,77 @@ async function fetchWithTimeout(url: string, options: RequestInit = {}, timeoutM
53
61
 
54
62
  const OPENAI_EXCLUDED_PREFIXES = ["embedding", "tts", "dall-e", "whisper", "text-embedding", "davinci", "babbage"];
55
63
 
64
+ function asPositiveNumber(value: unknown): number | undefined {
65
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
66
+ if (typeof value === "string") {
67
+ const n = Number.parseFloat(value);
68
+ if (Number.isFinite(n) && n > 0) return n;
69
+ }
70
+ return undefined;
71
+ }
72
+
73
+ function pickFirstPositiveNumber(record: Record<string, unknown>, keys: string[]): number | undefined {
74
+ for (const key of keys) {
75
+ const value = asPositiveNumber(record[key]);
76
+ if (value !== undefined) return value;
77
+ }
78
+ return undefined;
79
+ }
80
+
81
+ function discoverInputModalities(rawModel: Record<string, unknown>, id: string): Array<"text" | "image"> {
82
+ const directModalities = rawModel.input_modalities;
83
+ const capabilitiesModalities = (rawModel.capabilities as Record<string, unknown> | undefined)?.input_modalities;
84
+ const source = Array.isArray(directModalities)
85
+ ? directModalities
86
+ : Array.isArray(capabilitiesModalities)
87
+ ? capabilitiesModalities
88
+ : [];
89
+ const supportsImage = source.some((m) => typeof m === "string" && /image|vision/i.test(m))
90
+ || /vision|image|omni|multimodal/i.test(id);
91
+ return supportsImage ? ["text", "image"] : ["text"];
92
+ }
93
+
94
+ function parseOpenAICompatibleModel(rawModel: Record<string, unknown>): DiscoveredModel | undefined {
95
+ const id = typeof rawModel.id === "string" ? rawModel.id : "";
96
+ if (!id) return undefined;
97
+ if (OPENAI_EXCLUDED_PREFIXES.some((prefix) => id.startsWith(prefix))) return undefined;
98
+
99
+ const contextWindow = pickFirstPositiveNumber(rawModel, [
100
+ "context_window",
101
+ "context_length",
102
+ "max_context_length",
103
+ "max_input_tokens",
104
+ "input_token_limit",
105
+ "max_model_len",
106
+ ]);
107
+ const maxTokens = pickFirstPositiveNumber(rawModel, [
108
+ "max_output_tokens",
109
+ "output_token_limit",
110
+ "max_completion_tokens",
111
+ "max_tokens",
112
+ ]);
113
+ const reasoning = rawModel.reasoning === true
114
+ || rawModel.supports_reasoning === true
115
+ || ((rawModel.capabilities as Record<string, unknown> | undefined)?.reasoning === true);
116
+
117
+ return {
118
+ id,
119
+ name: typeof rawModel.name === "string" && rawModel.name.length > 0 ? rawModel.name : id,
120
+ contextWindow,
121
+ maxTokens,
122
+ reasoning,
123
+ input: discoverInputModalities(rawModel, id),
124
+ };
125
+ }
126
+
56
127
  class OpenAIDiscoveryAdapter implements ProviderDiscoveryAdapter {
57
- provider = "openai";
128
+ provider: string;
58
129
  supportsDiscovery = true;
59
130
 
131
+ constructor(provider: string) {
132
+ this.provider = provider;
133
+ }
134
+
60
135
  async fetchModels(apiKey: string, baseUrl?: string): Promise<DiscoveredModel[]> {
61
136
  const url = `${baseUrl ?? "https://api.openai.com"}/v1/models`;
62
137
  const response = await fetchWithTimeout(url, {
@@ -67,14 +142,10 @@ class OpenAIDiscoveryAdapter implements ProviderDiscoveryAdapter {
67
142
  throw new Error(`OpenAI models API returned ${response.status}: ${response.statusText}`);
68
143
  }
69
144
 
70
- const data = (await response.json()) as { data: Array<{ id: string; owned_by?: string }> };
71
- return data.data
72
- .filter((m) => !OPENAI_EXCLUDED_PREFIXES.some((prefix) => m.id.startsWith(prefix)))
73
- .map((m) => ({
74
- id: m.id,
75
- name: m.id,
76
- input: ["text" as const, "image" as const],
77
- }));
145
+ const data = (await response.json()) as { data?: Array<Record<string, unknown>> };
146
+ return (data.data ?? [])
147
+ .map((m) => parseOpenAICompatibleModel(m))
148
+ .filter((m): m is DiscoveredModel => !!m);
78
149
  }
79
150
  }
80
151
 
@@ -207,7 +278,7 @@ class StaticDiscoveryAdapter implements ProviderDiscoveryAdapter {
207
278
  // ─── Registry ────────────────────────────────────────────────────────────────
208
279
 
209
280
  const adapters: Record<string, ProviderDiscoveryAdapter> = {
210
- openai: new OpenAIDiscoveryAdapter(),
281
+ openai: new OpenAIDiscoveryAdapter("openai"),
211
282
  ollama: new OllamaDiscoveryAdapter(),
212
283
  openrouter: new OpenRouterDiscoveryAdapter(),
213
284
  google: new GoogleDiscoveryAdapter(),
@@ -220,8 +291,24 @@ const adapters: Record<string, ProviderDiscoveryAdapter> = {
220
291
  mistral: new StaticDiscoveryAdapter("mistral"),
221
292
  };
222
293
 
223
- export function getDiscoveryAdapter(provider: string): ProviderDiscoveryAdapter {
224
- return adapters[provider] ?? new StaticDiscoveryAdapter(provider);
294
+ export function supportsDiscoveryForApi(api: string | undefined): boolean {
295
+ if (!api) return false;
296
+ return OPENAI_COMPAT_DISCOVERY_APIS.has(api);
297
+ }
298
+
299
+ export function getDiscoveryAdapter(provider: string, providerApis?: Iterable<string>): ProviderDiscoveryAdapter {
300
+ const known = adapters[provider];
301
+ if (known) return known;
302
+
303
+ if (providerApis) {
304
+ for (const api of providerApis) {
305
+ if (supportsDiscoveryForApi(api)) {
306
+ return new OpenAIDiscoveryAdapter(provider);
307
+ }
308
+ }
309
+ }
310
+
311
+ return new StaticDiscoveryAdapter(provider);
225
312
  }
226
313
 
227
314
  export function getDiscoverableProviders(): string[] {
@@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, it } from "node:test";
6
6
  import { AuthStorage } from "./auth-storage.js";
7
7
  import { ModelDiscoveryCache } from "./discovery-cache.js";
8
8
  import { getDefaultTTL, getDiscoverableProviders, getDiscoveryAdapter } from "./model-discovery.js";
9
+ import { ModelRegistry } from "./model-registry.js";
9
10
 
10
11
  let testDir: string;
11
12
 
@@ -133,3 +134,77 @@ describe("Discovery TTL configuration", () => {
133
134
  assert.equal(customTTL, defaultTTL);
134
135
  });
135
136
  });
137
+
138
+ describe("ModelRegistry discovery — OpenAI-compatible custom providers", () => {
139
+ it("discovers custom OpenAI-compatible providers and maps capability metadata", async () => {
140
+ const providerName = `minimax-openai-${Date.now()}-${Math.random().toString(36).slice(2)}`;
141
+ const modelsPath = join(testDir, "models.json");
142
+ writeFileSync(
143
+ modelsPath,
144
+ JSON.stringify(
145
+ {
146
+ providers: {
147
+ [providerName]: {
148
+ baseUrl: "https://api.minimax.example",
149
+ apiKey: "minimax-test-key",
150
+ api: "openai-completions",
151
+ models: [{ id: "bootstrap-model" }],
152
+ },
153
+ },
154
+ },
155
+ null,
156
+ 2,
157
+ ),
158
+ "utf-8",
159
+ );
160
+
161
+ const prevFetch = globalThis.fetch;
162
+ let requestedUrl = "";
163
+ globalThis.fetch = (async (input: string | URL | Request) => {
164
+ requestedUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
165
+ return new Response(
166
+ JSON.stringify({
167
+ data: [
168
+ {
169
+ id: "MiniMax-M2.7-highspeed",
170
+ name: "MiniMax M2.7 Highspeed",
171
+ context_window: 165000,
172
+ max_output_tokens: 32768,
173
+ supports_reasoning: true,
174
+ input_modalities: ["text", "image"],
175
+ },
176
+ ],
177
+ }),
178
+ {
179
+ status: 200,
180
+ headers: { "content-type": "application/json" },
181
+ },
182
+ );
183
+ }) as typeof globalThis.fetch;
184
+
185
+ try {
186
+ const registry = new ModelRegistry(AuthStorage.inMemory({}), modelsPath);
187
+ // Guard against global cache leakage from prior test runs.
188
+ registry.getDiscoveryCache().clear(providerName);
189
+ const results = await registry.discoverModels([providerName]);
190
+
191
+ const discovery = results.find((r) => r.provider === providerName);
192
+ assert.ok(discovery, "discovery result should include custom provider");
193
+ assert.equal(discovery?.error, undefined, "custom provider discovery should succeed");
194
+ assert.equal(requestedUrl, "https://api.minimax.example/v1/models");
195
+
196
+ const discovered = registry
197
+ .getAllWithDiscovered()
198
+ .find((m) => m.provider === providerName && m.id === "MiniMax-M2.7-highspeed");
199
+ assert.ok(discovered, "discovered model should be merged into model list");
200
+ assert.equal(discovered?.api, "openai-completions");
201
+ assert.equal(discovered?.baseUrl, "https://api.minimax.example");
202
+ assert.equal(discovered?.contextWindow, 165000);
203
+ assert.equal(discovered?.maxTokens, 32768);
204
+ assert.equal(discovered?.reasoning, true);
205
+ assert.deepEqual(discovered?.input, ["text", "image"]);
206
+ } finally {
207
+ globalThis.fetch = prevFetch;
208
+ }
209
+ });
210
+ });
@@ -29,7 +29,7 @@ import { getAgentDir } from "../config.js";
29
29
  import type { AuthStorage } from "./auth-storage.js";
30
30
  import { ModelDiscoveryCache } from "./discovery-cache.js";
31
31
  import type { DiscoveredModel, DiscoveryResult } from "./model-discovery.js";
32
- import { getDefaultTTL, getDiscoverableProviders, getDiscoveryAdapter } from "./model-discovery.js";
32
+ import { getDefaultTTL, getDiscoverableProviders, getDiscoveryAdapter, supportsDiscoveryForApi } from "./model-discovery.js";
33
33
  import { clearConfigValueCache, resolveConfigValue, resolveHeaders } from "./resolve-config-value.js";
34
34
  import { isLocalModel } from "./local-model-check.js";
35
35
 
@@ -788,11 +788,12 @@ export class ModelRegistry {
788
788
  * Results are cached and merged into the registry (never overrides existing models).
789
789
  */
790
790
  async discoverModels(providers?: string[]): Promise<DiscoveryResult[]> {
791
- const targetProviders = providers ?? getDiscoverableProviders();
791
+ const targetProviders = providers ?? this.getAutoDiscoverableProviders();
792
792
  const results: DiscoveryResult[] = [];
793
793
 
794
794
  for (const providerName of targetProviders) {
795
- const adapter = getDiscoveryAdapter(providerName);
795
+ const providerApis = this.getProviderApis(providerName);
796
+ const adapter = getDiscoveryAdapter(providerName, providerApis);
796
797
  if (!adapter.supportsDiscovery) continue;
797
798
 
798
799
  // Skip if cache is still fresh
@@ -812,8 +813,10 @@ export class ModelRegistry {
812
813
  const apiKey = await this.authStorage.getApiKey(providerName);
813
814
  if (!apiKey && !this.isProviderRequestReady(providerName)) continue;
814
815
 
815
- const models = await adapter.fetchModels(apiKey ?? "", undefined);
816
- this.discoveryCache.set(providerName, models);
816
+ const baseUrl = this.getProviderBaseUrl(providerName);
817
+ const models = await adapter.fetchModels(apiKey ?? "", baseUrl);
818
+ const ttlMs = this.getDiscoveryTtl(providerName, providerApis);
819
+ this.discoveryCache.set(providerName, models, ttlMs);
817
820
  results.push({
818
821
  provider: providerName,
819
822
  models,
@@ -865,24 +868,97 @@ export class ModelRegistry {
865
868
  const converted: Model<Api>[] = [];
866
869
  for (const result of results) {
867
870
  if (result.error) continue;
871
+ const providerDefaults = this.getDiscoveryProviderDefaults(result.provider);
868
872
  for (const dm of result.models) {
869
873
  converted.push({
870
874
  id: dm.id,
871
875
  name: dm.name ?? dm.id,
872
- api: "openai" as Api,
876
+ api: providerDefaults.api,
873
877
  provider: result.provider,
874
- baseUrl: "",
878
+ baseUrl: providerDefaults.baseUrl,
875
879
  reasoning: dm.reasoning ?? false,
876
- input: dm.input ?? ["text"],
880
+ input: dm.input ?? providerDefaults.input,
877
881
  cost: dm.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
878
- contextWindow: dm.contextWindow ?? 128000,
879
- maxTokens: dm.maxTokens ?? 16384,
882
+ contextWindow: dm.contextWindow ?? providerDefaults.contextWindow,
883
+ maxTokens: dm.maxTokens ?? providerDefaults.maxTokens,
880
884
  } as Model<Api>);
881
885
  }
882
886
  }
883
887
  return converted;
884
888
  }
885
889
 
890
+ private getProviderApis(provider: string): Set<string> {
891
+ const apis = new Set<string>();
892
+ for (const model of this.models) {
893
+ if (model.provider === provider && typeof model.api === "string" && model.api.length > 0) {
894
+ apis.add(model.api);
895
+ }
896
+ }
897
+
898
+ const providerConfig = this.registeredProviders.get(provider);
899
+ if (providerConfig?.api) apis.add(providerConfig.api);
900
+ for (const modelDef of providerConfig?.models ?? []) {
901
+ if (modelDef.api) apis.add(modelDef.api);
902
+ }
903
+ return apis;
904
+ }
905
+
906
+ private getAutoDiscoverableProviders(): string[] {
907
+ const discoverable = new Set<string>(getDiscoverableProviders());
908
+ for (const provider of new Set(this.models.map((m) => m.provider))) {
909
+ const apis = this.getProviderApis(provider);
910
+ for (const api of apis) {
911
+ if (supportsDiscoveryForApi(api)) {
912
+ discoverable.add(provider);
913
+ break;
914
+ }
915
+ }
916
+ }
917
+ return [...discoverable];
918
+ }
919
+
920
+ private getProviderBaseUrl(provider: string): string | undefined {
921
+ const fromModels = this.models.find((m) => m.provider === provider && typeof m.baseUrl === "string" && m.baseUrl.length > 0);
922
+ if (fromModels?.baseUrl) return fromModels.baseUrl;
923
+ return this.registeredProviders.get(provider)?.baseUrl;
924
+ }
925
+
926
+ private getDiscoveryProviderDefaults(provider: string): {
927
+ api: Api;
928
+ baseUrl: string;
929
+ input: ("text" | "image")[];
930
+ contextWindow: number;
931
+ maxTokens: number;
932
+ } {
933
+ const first = this.models.find((m) => m.provider === provider);
934
+ if (first) {
935
+ return {
936
+ api: first.api,
937
+ baseUrl: first.baseUrl,
938
+ input: first.input,
939
+ contextWindow: first.contextWindow,
940
+ maxTokens: first.maxTokens,
941
+ };
942
+ }
943
+
944
+ return {
945
+ api: "openai-completions",
946
+ baseUrl: this.registeredProviders.get(provider)?.baseUrl ?? "",
947
+ input: ["text"],
948
+ contextWindow: 128000,
949
+ maxTokens: 16384,
950
+ };
951
+ }
952
+
953
+ private getDiscoveryTtl(provider: string, providerApis: Set<string>): number {
954
+ for (const api of providerApis) {
955
+ if (supportsDiscoveryForApi(api)) {
956
+ return getDefaultTTL("openai");
957
+ }
958
+ }
959
+ return getDefaultTTL(provider);
960
+ }
961
+
886
962
  /**
887
963
  * Check if a model's baseUrl points to a local endpoint.
888
964
  * Delegates to standalone isLocalModel() function.
@@ -12,7 +12,7 @@ import {
12
12
  type TUI,
13
13
  } from "@gsd/pi-tui";
14
14
  import type { AuthStorage } from "../../../core/auth-storage.js";
15
- import { getDiscoverableProviders } from "../../../core/model-discovery.js";
15
+ import { getDiscoverableProviders, getDiscoveryAdapter } from "../../../core/model-discovery.js";
16
16
  import { providerDisplayName } from "./model-selector.js";
17
17
  import type { ModelRegistry } from "../../../core/model-registry.js";
18
18
  import { ModelsJsonWriter } from "../../../core/models-json-writer.js";
@@ -102,12 +102,21 @@ export class ProviderManagerComponent extends Container implements Focusable {
102
102
 
103
103
  this.providers = Array.from(providerNames)
104
104
  .sort()
105
- .map((name) => ({
106
- name,
107
- hasAuth: this.authStorage.hasAuth(name),
108
- supportsDiscovery: discoverableSet.has(name),
109
- modelCount: providerModelCounts.get(name) ?? 0,
110
- }));
105
+ .map((name) => {
106
+ const providerApis = new Set(
107
+ allModels
108
+ .filter((m) => m.provider === name)
109
+ .map((m) => m.api)
110
+ .filter((api): api is string => typeof api === "string" && api.length > 0),
111
+ );
112
+ return {
113
+ name,
114
+ hasAuth: this.authStorage.hasAuth(name),
115
+ supportsDiscovery:
116
+ discoverableSet.has(name) || getDiscoveryAdapter(name, providerApis).supportsDiscovery,
117
+ modelCount: providerModelCounts.get(name) ?? 0,
118
+ };
119
+ });
111
120
  this.clampSelectedIndex();
112
121
  }
113
122