@theia/ai-ide 1.76.0-next.28 → 1.76.0-next.34

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 (40) hide show
  1. package/lib/browser/ai-configuration/ai-configuration-tree-widget.d.ts +6 -0
  2. package/lib/browser/ai-configuration/ai-configuration-tree-widget.d.ts.map +1 -1
  3. package/lib/browser/ai-configuration/ai-configuration-tree-widget.js +9 -0
  4. package/lib/browser/ai-configuration/ai-configuration-tree-widget.js.map +1 -1
  5. package/lib/browser/ai-configuration/categories/agent-capabilities-settings.d.ts.map +1 -1
  6. package/lib/browser/ai-configuration/categories/agent-capabilities-settings.js +2 -1
  7. package/lib/browser/ai-configuration/categories/agent-capabilities-settings.js.map +1 -1
  8. package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.d.ts +8 -0
  9. package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.d.ts.map +1 -1
  10. package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.js +27 -8
  11. package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.js.map +1 -1
  12. package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.spec.js +38 -2
  13. package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.spec.js.map +1 -1
  14. package/lib/browser/ai-configuration/categories/models-configuration-category.d.ts +13 -2
  15. package/lib/browser/ai-configuration/categories/models-configuration-category.d.ts.map +1 -1
  16. package/lib/browser/ai-configuration/categories/models-configuration-category.js +41 -2
  17. package/lib/browser/ai-configuration/categories/models-configuration-category.js.map +1 -1
  18. package/lib/browser/ai-configuration/categories/models-configuration-category.spec.js +32 -2
  19. package/lib/browser/ai-configuration/categories/models-configuration-category.spec.js.map +1 -1
  20. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.d.ts +26 -0
  21. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.d.ts.map +1 -0
  22. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.js +212 -0
  23. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.js.map +1 -0
  24. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.spec.d.ts +2 -0
  25. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.spec.d.ts.map +1 -0
  26. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.spec.js +423 -0
  27. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.spec.js.map +1 -0
  28. package/lib/browser/ai-configuration/language-model-renderer.d.ts.map +1 -1
  29. package/lib/browser/ai-configuration/language-model-renderer.js +10 -3
  30. package/lib/browser/ai-configuration/language-model-renderer.js.map +1 -1
  31. package/package.json +25 -25
  32. package/src/browser/ai-configuration/ai-configuration-tree-widget.tsx +8 -0
  33. package/src/browser/ai-configuration/categories/agent-capabilities-settings.tsx +7 -8
  34. package/src/browser/ai-configuration/categories/model-aliases-configuration-category.spec.ts +40 -2
  35. package/src/browser/ai-configuration/categories/model-aliases-configuration-category.tsx +31 -10
  36. package/src/browser/ai-configuration/categories/models-configuration-category.spec.ts +47 -2
  37. package/src/browser/ai-configuration/categories/models-configuration-category.tsx +52 -5
  38. package/src/browser/ai-configuration/categories/provider-model-discovery-section.spec.tsx +451 -0
  39. package/src/browser/ai-configuration/categories/provider-model-discovery-section.tsx +341 -0
  40. package/src/browser/ai-configuration/language-model-renderer.tsx +11 -3
@@ -207,11 +207,19 @@ export class AiConfigurationTreeWidget extends TreeWidget {
207
207
  return match?.target;
208
208
  }
209
209
 
210
+ /**
211
+ * Selects a node on behalf of something other than a click on it — a deep link, a restored
212
+ * selection — and reveals what it holds: the category of an item, or the items of a category. A
213
+ * click needs neither, since the node it selects is already visible, and folding a category away
214
+ * only to have the next click on it unfold it again would be its own kind of rude.
215
+ */
210
216
  protected applyTreeSelection(node: SelectableTreeNode): void {
211
217
  this.updatingSelection = true;
212
218
  try {
213
219
  if (AiConfigurationItemNode.is(node) && ExpandableTreeNode.is(node.parent) && !node.parent.expanded) {
214
220
  this.model.expandNode(node.parent);
221
+ } else if (AiConfigurationCategoryNode.is(node) && ExpandableTreeNode.is(node) && !node.expanded) {
222
+ this.model.expandNode(node);
215
223
  }
216
224
  this.model.selectNode(node);
217
225
  } finally {
@@ -25,6 +25,7 @@ import { nls } from '@theia/core';
25
25
  import { codicon } from '@theia/core/lib/browser';
26
26
  import * as React from '@theia/core/shared/react';
27
27
  import { AiConfigurationItemRow } from '@theia/ai-core-ui/lib/browser/ai-configuration/components/ai-configuration-item-row';
28
+ import { AiConfigurationIconButton } from '@theia/ai-core-ui/lib/browser/ai-configuration/components/ai-configuration-primitives';
28
29
  import { AiSettingsRowService } from '@theia/ai-core-ui/lib/browser/ai-configuration/components/ai-settings-row-service';
29
30
  import { AiToggleSwitch } from '@theia/ai-core-ui/lib/browser/ai-configuration/components/ai-configuration-controls';
30
31
 
@@ -44,15 +45,13 @@ const AgentSectionHeader: React.FC<{ title: string; action?: React.ReactNode }>
44
45
  * button, so it reads as a Theia reset affordance. Its tooltip says what the section-wide reset does.
45
46
  */
46
47
  const ResetAllButton: React.FC<{ title: string; disabled?: boolean; onReset: () => void }> = ({ title, disabled, onReset }) => (
47
- <button
48
- className='ai-variant-action-button ai-agent-section-header-action'
49
- onClick={onReset}
50
- disabled={disabled}
48
+ <AiConfigurationIconButton
49
+ iconClass={codicon('discard')}
51
50
  title={title}
52
- aria-label={title}
53
- >
54
- <span aria-hidden='true' className={codicon('discard')}></span>
55
- </button>
51
+ disabled={disabled}
52
+ className='ai-agent-section-header-action'
53
+ onClick={onReset}
54
+ />
56
55
  );
57
56
 
58
57
  export interface AgentCapabilitiesSettingsProps {
@@ -32,8 +32,8 @@ function alias(id: string, description = ''): LanguageModelAlias {
32
32
  return { id, description, defaultModelIds: [] } as unknown as LanguageModelAlias;
33
33
  }
34
34
 
35
- function readyModel(id: string): LanguageModel {
36
- return { id, status: { status: 'ready' } } as unknown as LanguageModel;
35
+ function readyModel(id: string, released?: number): LanguageModel {
36
+ return { id, released, status: { status: 'ready' } } as unknown as LanguageModel;
37
37
  }
38
38
 
39
39
  function unreadyModel(id: string): LanguageModel {
@@ -77,6 +77,44 @@ describe('ModelAliasesConfigurationCategory', () => {
77
77
  expect(children[2].status?.kind).to.equal('error');
78
78
  });
79
79
 
80
+ it('divides the models per provider, newest first, after the default-list entry', () => {
81
+ const category = createCategory([], new Map());
82
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
83
+ (category as any).languageModels = [
84
+ readyModel('openai/gpt-5.5', Date.parse('2025-08-01')),
85
+ readyModel('anthropic/claude-haiku-4-5', Date.parse('2025-10-01')),
86
+ readyModel('openai/gpt-5.6-sol', Date.parse('2026-02-01')),
87
+ readyModel('anthropic/claude-opus-5', Date.parse('2026-04-01'))
88
+ ];
89
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
90
+ const options = (category as any).getModelOptions() as Array<{ value?: string; label?: string; separator?: boolean }>;
91
+ expect(options[0].value).to.equal('');
92
+ // A rule between the providers rather than a heading: the select draws separators as plain lines.
93
+ expect(options.slice(1).map(option => option.separator ? '---' : option.value)).to.deep.equal([
94
+ '---',
95
+ 'anthropic/claude-opus-5',
96
+ 'anthropic/claude-haiku-4-5',
97
+ '---',
98
+ 'openai/gpt-5.6-sol',
99
+ 'openai/gpt-5.5'
100
+ ]);
101
+ });
102
+
103
+ it('keeps the provider rules when mapping the options onto the select', () => {
104
+ const category = createCategory([], new Map());
105
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
106
+ const mapped = (category as any).toEnumOptions([
107
+ { value: 'a', label: 'A', description: 'first' },
108
+ { separator: true },
109
+ { value: 'b' }
110
+ ]) as Array<{ value: string; label: string; title?: string; separator?: boolean }>;
111
+ // A rule that loses its `separator` becomes a selectable entry with no label that clears the alias.
112
+ expect(mapped.map(option => option.separator ? '---' : option.value)).to.deep.equal(['a', '---', 'b']);
113
+ expect(mapped[0].title).to.equal('first');
114
+ // Without a label of its own, an option falls back to its value rather than rendering empty.
115
+ expect(mapped[2].label).to.equal('b');
116
+ });
117
+
80
118
  it('indexes one search item per alias, navigating to the item', () => {
81
119
  const category = createCategory([alias('default/code', 'Coding alias')], new Map());
82
120
  const items = category.getSearchItems();
@@ -14,7 +14,7 @@
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
16
 
17
- import { AgentService, AISettingsService, LANGUAGE_MODEL_ALIASES_PREFERENCE } from '@theia/ai-core';
17
+ import { AgentService, AISettingsService, groupModelsByProvider, LANGUAGE_MODEL_ALIASES_PREFERENCE } from '@theia/ai-core';
18
18
  import { FrontendLanguageModelRegistry, LanguageModel, LanguageModelRegistry, LanguageModelRequirement } from '@theia/ai-core/lib/common/language-model';
19
19
  import { LanguageModelAlias, LanguageModelAliasRegistry } from '@theia/ai-core/lib/common/language-model-alias';
20
20
  import { Emitter, Event, nls } from '@theia/core';
@@ -35,7 +35,7 @@ import {
35
35
  } from '@theia/ai-core-ui/lib/browser/ai-configuration/ai-configuration-category';
36
36
  import { CollectionCategoryRenderer } from '@theia/ai-core-ui/lib/browser/ai-configuration/renderers/collection-category-renderer';
37
37
  import { AiConfigurationSection } from '@theia/ai-core-ui/lib/browser/ai-configuration/components/ai-configuration-primitives';
38
- import { AiEnumSelect } from '@theia/ai-core-ui/lib/browser/ai-configuration/components/ai-configuration-controls';
38
+ import { AiEnumOption, AiEnumSelect } from '@theia/ai-core-ui/lib/browser/ai-configuration/components/ai-configuration-controls';
39
39
  import { AgentChips } from './agent-chips';
40
40
 
41
41
  /** Sentinel option value representing "use the alias' default priority list" (no explicit model). */
@@ -214,28 +214,49 @@ export class ModelAliasesConfigurationCategory extends CollectionCategoryRendere
214
214
  ariaLabel={nls.localize('theia/ai/core/modelAliasesConfiguration/selectedModelId', 'Selected Model')}
215
215
  value={isInvalid ? undefined : selected}
216
216
  invalid={isInvalid}
217
- options={options.map(option => ({ value: String(option.value ?? ''), label: option.label ?? String(option.value ?? ''), title: option.description }))}
217
+ options={this.toEnumOptions(options)}
218
218
  onCommit={value => this.setSelectedModel(alias, value)}
219
219
  />
220
220
  </AiConfigurationSection>;
221
221
  }
222
222
 
223
+ /**
224
+ * Maps the options onto what {@link AiEnumSelect} takes. The per-provider rules have to survive the
225
+ * mapping: dropping `separator` turns each of them into a selectable entry with no label that
226
+ * clears the alias when picked.
227
+ */
228
+ protected toEnumOptions(options: SelectOption[]): AiEnumOption[] {
229
+ return options.map(option => ({
230
+ value: String(option.value ?? ''),
231
+ label: option.label ?? String(option.value ?? ''),
232
+ title: option.description,
233
+ separator: option.separator
234
+ }));
235
+ }
236
+
223
237
  protected getModelOptions(): SelectOption[] {
224
238
  const options: SelectOption[] = [{
225
239
  value: DEFAULT_LIST_VALUE,
226
240
  label: nls.localize('theia/ai/core/modelAliasesConfiguration/defaultList', '[Default list]')
227
241
  }];
228
- for (const model of [...this.languageModels].sort((a, b) => (a.name ?? a.id).localeCompare(b.name ?? b.id))) {
229
- const ready = model.status.status === 'ready';
230
- options.push({
231
- value: model.id,
232
- label: `${model.name ?? model.id} ${ready ? '✓' : '✗'}`,
233
- description: !ready ? model.status.message : undefined
234
- });
242
+ // Divided per provider, newest first inside each group. An alias names a concrete model, which
243
+ // includes the older and release-pinned ones, so the whole catalogue is offered here.
244
+ for (const { models } of groupModelsByProvider(this.languageModels)) {
245
+ options.push({ separator: true });
246
+ options.push(...models.map(model => this.toModelOption(model)));
235
247
  }
236
248
  return options;
237
249
  }
238
250
 
251
+ protected toModelOption(model: LanguageModel): SelectOption {
252
+ const ready = model.status.status === 'ready';
253
+ return {
254
+ value: model.id,
255
+ label: `${model.name ?? model.id} ${ready ? '✓' : '✗'}`,
256
+ description: !ready ? model.status.message : undefined
257
+ };
258
+ }
259
+
239
260
  protected setSelectedModel(alias: LanguageModelAlias, value: string | undefined): void {
240
261
  const selectedModelId = value ? value : undefined;
241
262
  this.languageModelAliasRegistry.ready.then(() => this.languageModelAliasRegistry.addAlias({ ...alias, selectedModelId }));
@@ -23,6 +23,8 @@ FrontendApplicationConfigProvider.set({});
23
23
  import { expect } from 'chai';
24
24
  import { AiSettingsControl, AiSettingsRowService } from '@theia/ai-core-ui/lib/browser/ai-configuration/components/ai-settings-row-service';
25
25
  import { AiConfigurationCategoryId } from '@theia/ai-core-ui/lib/browser/ai-configuration/ai-configuration-category';
26
+ import { ModelDiscoveryStatusService } from '@theia/ai-core/lib/browser';
27
+ import { ModelDiscoveryStatus } from '@theia/ai-core/lib/common/model-discovery-status';
26
28
  import { ModelsConfigurationCategory } from './models-configuration-category';
27
29
 
28
30
  disableJSDOM();
@@ -69,7 +71,7 @@ describe('ModelsConfigurationCategory', () => {
69
71
  'ai-features.modelSettings.requestSettings',
70
72
  'ai-features.reasoning.defaults',
71
73
  'ai-features.anthropic.AnthropicApiKey',
72
- 'ai-features.anthropic.AnthropicModels',
74
+ 'ai-features.anthropic.serverSideCompaction',
73
75
  'ai-features.google.apiKey',
74
76
  // must be ignored: owned by other categories / unrelated
75
77
  'ai-features.chat.pinChatAgent',
@@ -84,7 +86,7 @@ describe('ModelsConfigurationCategory', () => {
84
86
  const sections = (category as any).getSections() as Array<{ id: string; preferenceIds: string[] }>;
85
87
  expect(sections.map(s => s.id)).to.deep.equal(['model-settings', 'anthropic', 'google']);
86
88
  expect(sections[0].preferenceIds).to.have.members(['ai-features.modelSettings.requestSettings', 'ai-features.reasoning.defaults']);
87
- expect(sections[1].preferenceIds).to.deep.equal(['ai-features.anthropic.AnthropicApiKey', 'ai-features.anthropic.AnthropicModels']);
89
+ expect(sections[1].preferenceIds).to.deep.equal(['ai-features.anthropic.AnthropicApiKey', 'ai-features.anthropic.serverSideCompaction']);
88
90
  expect(sections[2].preferenceIds).to.deep.equal(['ai-features.google.apiKey']);
89
91
  });
90
92
 
@@ -164,4 +166,47 @@ describe('ModelsConfigurationCategory', () => {
164
166
  highlight: { rowId: 'ai-features.anthropic.AnthropicApiKey' }
165
167
  });
166
168
  });
169
+
170
+ describe('model discovery section', () => {
171
+
172
+ function withDiscovery(statuses: ModelDiscoveryStatus[], modelIds: string[]): ModelsConfigurationCategory {
173
+ const category = createCategory([]);
174
+ const service: Partial<ModelDiscoveryStatusService> = {
175
+ getStatus: (providerId: string) => statuses.find(status => status.providerId === providerId)
176
+ };
177
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
178
+ (category as any).discoveryStatus = service;
179
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
180
+ (category as any).models = modelIds.map(id => ({ id }));
181
+ return category;
182
+ }
183
+
184
+ function discoveredModelIds(category: ModelsConfigurationCategory, providerId: string): string[] | undefined {
185
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
186
+ const rendered = (category as any).renderModelDiscovery(providerId) as
187
+ { props: { models: { id: string }[] } } | undefined;
188
+ return rendered && rendered.props.models.map(model => model.id);
189
+ }
190
+
191
+ it('renders nothing for a provider that does not participate in discovery', () => {
192
+ const category = withDiscovery([], ['anthropic/claude-opus-5']);
193
+ expect(discoveredModelIds(category, 'anthropic')).to.equal(undefined);
194
+ });
195
+
196
+ it('lists the provider\'s own models, keyed by the provider id', () => {
197
+ const category = withDiscovery(
198
+ [{ providerId: 'anthropic', label: 'Anthropic', state: 'ready' }],
199
+ ['anthropic/claude-opus-5', 'google/gemini-3.1-pro', 'anthropic-custom/foo']
200
+ );
201
+ expect(discoveredModelIds(category, 'anthropic')).to.deep.equal(['anthropic/claude-opus-5']);
202
+ });
203
+
204
+ it('honours a model-id prefix that differs from the provider id', () => {
205
+ const category = withDiscovery(
206
+ [{ providerId: 'openAiOfficial', label: 'OpenAI', modelIdPrefix: 'openai', state: 'ready' }],
207
+ ['openai/gpt-5.5', 'openAiOfficial/gpt-5.5', 'openai-custom/local']
208
+ );
209
+ expect(discoveredModelIds(category, 'openAiOfficial')).to.deep.equal(['openai/gpt-5.5']);
210
+ });
211
+ });
167
212
  });
@@ -14,7 +14,7 @@
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
16
 
17
- import { Emitter, Event, nls } from '@theia/core';
17
+ import { CommandService, Emitter, Event, nls } from '@theia/core';
18
18
  import { codicon } from '@theia/core/lib/browser';
19
19
  import { DisposableCollection } from '@theia/core/lib/common';
20
20
  import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
@@ -32,6 +32,9 @@ import { AiSettingsRowService } from '@theia/ai-core-ui/lib/browser/ai-configura
32
32
  import { AiConfigurationSection } from '@theia/ai-core-ui/lib/browser/ai-configuration/components/ai-configuration-primitives';
33
33
  import { CollectionCategoryRenderer } from '@theia/ai-core-ui/lib/browser/ai-configuration/renderers/collection-category-renderer';
34
34
  import { AiSettingsRow } from '@theia/ai-core-ui/lib/browser/ai-configuration/components/ai-settings-row';
35
+ import { LanguageModel, LanguageModelRegistry } from '@theia/ai-core/lib/common';
36
+ import { FavoriteModelsService, ModelDiscoveryStatusService } from '@theia/ai-core/lib/browser';
37
+ import { ProviderModelDiscoverySection } from './provider-model-discovery-section';
35
38
 
36
39
  /** Prefix shared by every AI preference. */
37
40
  const AI_FEATURES_PREFIX = 'ai-features.';
@@ -73,10 +76,25 @@ export class ModelsConfigurationCategory extends CollectionCategoryRenderer impl
73
76
  @inject(AiSettingsRowService)
74
77
  protected readonly settingsRowService: AiSettingsRowService;
75
78
 
79
+ @inject(ModelDiscoveryStatusService)
80
+ protected readonly discoveryStatus: ModelDiscoveryStatusService;
81
+
82
+ @inject(FavoriteModelsService)
83
+ protected readonly favoriteModels: FavoriteModelsService;
84
+
85
+ @inject(CommandService)
86
+ protected readonly commandService: CommandService;
87
+
88
+ @inject(LanguageModelRegistry)
89
+ protected readonly languageModelRegistry: LanguageModelRegistry;
90
+
76
91
  protected readonly onDidChangeEmitter = new Emitter<void>();
77
92
  readonly onDidChange: Event<void> = this.onDidChangeEmitter.event;
78
93
  protected readonly toDispose = new DisposableCollection(this.onDidChangeEmitter);
79
94
 
95
+ /** Registered models, kept in sync so the provider detail can list them synchronously while rendering. */
96
+ protected models: LanguageModel[] = [];
97
+
80
98
  get renderer(): this {
81
99
  return this;
82
100
  }
@@ -96,6 +114,14 @@ export class ModelsConfigurationCategory extends CollectionCategoryRenderer impl
96
114
  @postConstruct()
97
115
  protected init(): void {
98
116
  this.toDispose.push(this.settingsRowService.onPreferenceChanged(() => this.onDidChangeEmitter.fire()));
117
+ this.toDispose.push(this.discoveryStatus.onDidChange(() => this.onDidChangeEmitter.fire()));
118
+ this.toDispose.push(this.languageModelRegistry.onChange(() => this.updateModels()));
119
+ this.updateModels();
120
+ }
121
+
122
+ protected async updateModels(): Promise<void> {
123
+ this.models = await this.languageModelRegistry.getLanguageModels();
124
+ this.onDidChangeEmitter.fire();
99
125
  }
100
126
 
101
127
  dispose(): void {
@@ -130,15 +156,36 @@ export class ModelsConfigurationCategory extends CollectionCategoryRenderer impl
130
156
  </AiConfigurationSection>;
131
157
  }
132
158
 
133
- /** Provider detail (node selected): that provider's settings. */
159
+ /** Provider detail (node selected): that provider's settings, followed by its model discovery. */
134
160
  protected renderItemSections(item: AiConfigurationTreeItem, ctx: AiConfigurationRenderContext): React.ReactNode {
135
161
  const section = this.getProviderSections().find(candidate => candidate.id === item.id);
136
162
  if (!section) {
137
163
  return undefined;
138
164
  }
139
- return <AiConfigurationSection title={nls.localizeByDefault('Settings')}>
140
- {section.preferenceIds.map(preferenceId => this.renderPreferenceRow(ctx, preferenceId))}
141
- </AiConfigurationSection>;
165
+ return <>
166
+ <AiConfigurationSection title={nls.localizeByDefault('Settings')}>
167
+ {section.preferenceIds.map(preferenceId => this.renderPreferenceRow(ctx, preferenceId))}
168
+ </AiConfigurationSection>
169
+ {this.renderModelDiscovery(item.id)}
170
+ </>;
171
+ }
172
+
173
+ /** Only providers that participate in dynamic model discovery contribute this section. */
174
+ protected renderModelDiscovery(providerId: string): React.ReactNode {
175
+ const status = this.discoveryStatus.getStatus(providerId);
176
+ if (!status) {
177
+ return undefined;
178
+ }
179
+ // A provider's registered model ids may carry a prefix that differs from its preference segment
180
+ // (e.g. the `openAiOfficial` block registers `openai/…` models).
181
+ const prefix = `${status.modelIdPrefix ?? providerId}/`;
182
+ return <ProviderModelDiscoverySection
183
+ status={status}
184
+ models={this.models.filter(model => model.id.startsWith(prefix))}
185
+ favorites={this.favoriteModels}
186
+ onRefresh={() => this.discoveryStatus.refresh(providerId)}
187
+ onAction={action => this.commandService.executeCommand(action.commandId, ...(action.args ?? []))}
188
+ />;
142
189
  }
143
190
 
144
191
  protected renderPreferenceRow(ctx: AiConfigurationRenderContext, preferenceId: string): React.ReactNode {