@theia/ai-core 1.76.0-next.6 → 1.76.0
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/lib/browser/ai-core-frontend-module.d.ts.map +1 -1
- package/lib/browser/ai-core-frontend-module.js +4 -0
- package/lib/browser/ai-core-frontend-module.js.map +1 -1
- package/lib/browser/discovering-provider-contribution.d.ts +92 -0
- package/lib/browser/discovering-provider-contribution.d.ts.map +1 -0
- package/lib/browser/discovering-provider-contribution.js +189 -0
- package/lib/browser/discovering-provider-contribution.js.map +1 -0
- package/lib/browser/discovering-provider-contribution.spec.d.ts +2 -0
- package/lib/browser/discovering-provider-contribution.spec.d.ts.map +1 -0
- package/lib/browser/discovering-provider-contribution.spec.js +233 -0
- package/lib/browser/discovering-provider-contribution.spec.js.map +1 -0
- package/lib/browser/favorite-models-service.d.ts +135 -0
- package/lib/browser/favorite-models-service.d.ts.map +1 -0
- package/lib/browser/favorite-models-service.js +279 -0
- package/lib/browser/favorite-models-service.js.map +1 -0
- package/lib/browser/favorite-models-service.spec.d.ts +2 -0
- package/lib/browser/favorite-models-service.spec.d.ts.map +1 -0
- package/lib/browser/favorite-models-service.spec.js +277 -0
- package/lib/browser/favorite-models-service.spec.js.map +1 -0
- package/lib/browser/frontend-language-model-alias-registry.js +1 -1
- package/lib/browser/frontend-language-model-service.d.ts.map +1 -1
- package/lib/browser/frontend-language-model-service.js +8 -0
- package/lib/browser/frontend-language-model-service.js.map +1 -1
- package/lib/browser/frontend-language-model-service.spec.d.ts +2 -0
- package/lib/browser/frontend-language-model-service.spec.d.ts.map +1 -0
- package/lib/browser/frontend-language-model-service.spec.js +76 -0
- package/lib/browser/frontend-language-model-service.spec.js.map +1 -0
- package/lib/browser/index.d.ts +3 -0
- package/lib/browser/index.d.ts.map +1 -1
- package/lib/browser/index.js +3 -0
- package/lib/browser/index.js.map +1 -1
- package/lib/browser/model-discovery-status-service.d.ts +52 -0
- package/lib/browser/model-discovery-status-service.d.ts.map +1 -0
- package/lib/browser/model-discovery-status-service.js +110 -0
- package/lib/browser/model-discovery-status-service.js.map +1 -0
- package/lib/browser/model-discovery-status-service.spec.d.ts +2 -0
- package/lib/browser/model-discovery-status-service.spec.d.ts.map +1 -0
- package/lib/browser/model-discovery-status-service.spec.js +86 -0
- package/lib/browser/model-discovery-status-service.spec.js.map +1 -0
- package/lib/common/ai-core-preferences.d.ts +11 -0
- package/lib/common/ai-core-preferences.d.ts.map +1 -1
- package/lib/common/ai-core-preferences.js +37 -1
- package/lib/common/ai-core-preferences.js.map +1 -1
- package/lib/common/index.d.ts +2 -0
- package/lib/common/index.d.ts.map +1 -1
- package/lib/common/index.js +2 -0
- package/lib/common/index.js.map +1 -1
- package/lib/common/language-model-ordering.spec.d.ts +2 -0
- package/lib/common/language-model-ordering.spec.d.ts.map +1 -0
- package/lib/common/language-model-ordering.spec.js +72 -0
- package/lib/common/language-model-ordering.spec.js.map +1 -0
- package/lib/common/language-model-util.d.ts +17 -1
- package/lib/common/language-model-util.d.ts.map +1 -1
- package/lib/common/language-model-util.js +44 -1
- package/lib/common/language-model-util.js.map +1 -1
- package/lib/common/language-model.d.ts +21 -0
- package/lib/common/language-model.d.ts.map +1 -1
- package/lib/common/language-model.js +50 -1
- package/lib/common/language-model.js.map +1 -1
- package/lib/common/language-model.spec.js +36 -0
- package/lib/common/language-model.spec.js.map +1 -1
- package/lib/common/model-discovery-status.d.ts +112 -0
- package/lib/common/model-discovery-status.d.ts.map +1 -0
- package/lib/common/model-discovery-status.js +18 -0
- package/lib/common/model-discovery-status.js.map +1 -0
- package/lib/common/model-discovery-util.d.ts +46 -0
- package/lib/common/model-discovery-util.d.ts.map +1 -0
- package/lib/common/model-discovery-util.js +128 -0
- package/lib/common/model-discovery-util.js.map +1 -0
- package/lib/common/model-discovery-util.spec.d.ts +2 -0
- package/lib/common/model-discovery-util.spec.d.ts.map +1 -0
- package/lib/common/model-discovery-util.spec.js +95 -0
- package/lib/common/model-discovery-util.spec.js.map +1 -0
- package/lib/common/prompt-service.js +3 -3
- package/lib/common/prompt-service.js.map +1 -1
- package/lib/common/prompt-service.spec.js +5 -0
- package/lib/common/prompt-service.spec.js.map +1 -1
- package/lib/node/ai-core-backend-module.d.ts.map +1 -1
- package/lib/node/ai-core-backend-module.js +8 -0
- package/lib/node/ai-core-backend-module.js.map +1 -1
- package/lib/node/backend-language-model-registry.d.ts.map +1 -1
- package/lib/node/backend-language-model-registry.js +1 -0
- package/lib/node/backend-language-model-registry.js.map +1 -1
- package/lib/node/index.d.ts +2 -0
- package/lib/node/index.d.ts.map +1 -1
- package/lib/node/index.js +2 -0
- package/lib/node/index.js.map +1 -1
- package/lib/node/model-discovery-fetcher.d.ts +43 -0
- package/lib/node/model-discovery-fetcher.d.ts.map +1 -0
- package/lib/node/model-discovery-fetcher.js +85 -0
- package/lib/node/model-discovery-fetcher.js.map +1 -0
- package/lib/node/model-snapshot-store.d.ts +26 -0
- package/lib/node/model-snapshot-store.d.ts.map +1 -0
- package/lib/node/model-snapshot-store.js +73 -0
- package/lib/node/model-snapshot-store.js.map +1 -0
- package/lib/node/test/test-model-discovery-fetcher.d.ts +21 -0
- package/lib/node/test/test-model-discovery-fetcher.d.ts.map +1 -0
- package/lib/node/test/test-model-discovery-fetcher.js +54 -0
- package/lib/node/test/test-model-discovery-fetcher.js.map +1 -0
- package/package.json +12 -12
- package/src/browser/ai-core-frontend-module.ts +5 -0
- package/src/browser/discovering-provider-contribution.spec.ts +304 -0
- package/src/browser/discovering-provider-contribution.ts +241 -0
- package/src/browser/favorite-models-service.spec.ts +336 -0
- package/src/browser/favorite-models-service.ts +297 -0
- package/src/browser/frontend-language-model-alias-registry.ts +1 -1
- package/src/browser/frontend-language-model-service.spec.ts +86 -0
- package/src/browser/frontend-language-model-service.ts +9 -1
- package/src/browser/index.ts +3 -0
- package/src/browser/model-discovery-status-service.spec.ts +101 -0
- package/src/browser/model-discovery-status-service.ts +123 -0
- package/src/common/ai-core-preferences.ts +40 -0
- package/src/common/index.ts +2 -0
- package/src/common/language-model-ordering.spec.ts +83 -0
- package/src/common/language-model-util.ts +43 -0
- package/src/common/language-model.spec.ts +45 -0
- package/src/common/language-model.ts +54 -0
- package/src/common/model-discovery-status.ts +132 -0
- package/src/common/model-discovery-util.spec.ts +105 -0
- package/src/common/model-discovery-util.ts +129 -0
- package/src/common/prompt-service.spec.ts +6 -0
- package/src/common/prompt-service.ts +3 -3
- package/src/node/ai-core-backend-module.ts +8 -0
- package/src/node/backend-language-model-registry.ts +1 -0
- package/src/node/index.ts +2 -0
- package/src/node/model-discovery-fetcher.ts +105 -0
- package/src/node/model-snapshot-store.ts +80 -0
- package/src/node/test/test-model-discovery-fetcher.ts +62 -0
|
@@ -41,6 +41,38 @@ export interface ReasoningSupport {
|
|
|
41
41
|
readonly supportedLevels: ReadonlyArray<ReasoningLevel>;
|
|
42
42
|
readonly defaultLevel?: ReasoningLevel;
|
|
43
43
|
}
|
|
44
|
+
export namespace ReasoningSupport {
|
|
45
|
+
/** Levels ordered by increasing effort; `'auto'` sits outside the scale. */
|
|
46
|
+
const EFFORT_SCALE: ReadonlyArray<ReasoningLevel> = ['off', 'minimal', 'low', 'medium', 'high'];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Returns `level` when `support` lists it, otherwise the nearest supported level on the effort scale,
|
|
50
|
+
* preferring the higher neighbour (an unsupported `minimal` becomes `low`, not `off`). An unsupported
|
|
51
|
+
* `'auto'` resolves to `defaultLevel`, then to the first supported level. Returns `level` unchanged
|
|
52
|
+
* when nothing suitable is supported.
|
|
53
|
+
*/
|
|
54
|
+
export function clampLevel(support: ReasoningSupport, level: ReasoningLevel): ReasoningLevel {
|
|
55
|
+
const supported = support.supportedLevels;
|
|
56
|
+
if (supported.includes(level)) {
|
|
57
|
+
return level;
|
|
58
|
+
}
|
|
59
|
+
if (level === 'auto') {
|
|
60
|
+
return support.defaultLevel && supported.includes(support.defaultLevel) ? support.defaultLevel : supported[0] ?? level;
|
|
61
|
+
}
|
|
62
|
+
const index = EFFORT_SCALE.indexOf(level);
|
|
63
|
+
for (let distance = 1; distance < EFFORT_SCALE.length; distance++) {
|
|
64
|
+
const higher = EFFORT_SCALE[index + distance];
|
|
65
|
+
if (higher && supported.includes(higher)) {
|
|
66
|
+
return higher;
|
|
67
|
+
}
|
|
68
|
+
const lower = EFFORT_SCALE[index - distance];
|
|
69
|
+
if (lower && supported.includes(lower)) {
|
|
70
|
+
return lower;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return supported.includes('auto') ? 'auto' : level;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
44
76
|
|
|
45
77
|
export type LanguageModelMessage =
|
|
46
78
|
TextMessage | ThinkingMessage | ToolUseMessage | ToolResultMessage | ServerToolUseMessage | ImageMessage | CompactionMessage;
|
|
@@ -319,6 +351,23 @@ export namespace ToolRequest {
|
|
|
319
351
|
(!('required' in obj) || (Array.isArray(obj.required) && obj.required.every(prop => typeof prop === 'string')));
|
|
320
352
|
}
|
|
321
353
|
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Resolves the `headers` attribute of a custom model preference entry. Since preferences are
|
|
357
|
+
* user-authored JSON, entries with a non-string value are dropped. Returns `undefined` when no
|
|
358
|
+
* usable header remains, so that the default request headers are left untouched. The keys are
|
|
359
|
+
* sorted, so equal header maps stringify identically regardless of their order in the preference.
|
|
360
|
+
*/
|
|
361
|
+
export function resolveCustomModelHeaders(headers: unknown): Record<string, string> | undefined {
|
|
362
|
+
if (typeof headers !== 'object' || !headers || Array.isArray(headers)) {
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
const resolved = Object.entries(headers)
|
|
366
|
+
.filter((entry): entry is [string, string] => typeof entry[1] === 'string')
|
|
367
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
368
|
+
return resolved.length > 0 ? Object.fromEntries(resolved) : undefined;
|
|
369
|
+
}
|
|
370
|
+
|
|
322
371
|
// Anthropic requires at least 50,000 tokens, so use one conservative minimum for all compaction settings.
|
|
323
372
|
export const SERVER_SIDE_COMPACTION_TOKEN_THRESHOLD_MINIMUM = 50_000;
|
|
324
373
|
|
|
@@ -650,6 +699,11 @@ export interface LanguageModelMetaData {
|
|
|
650
699
|
readonly family?: string;
|
|
651
700
|
readonly maxInputTokens?: number;
|
|
652
701
|
readonly maxOutputTokens?: number;
|
|
702
|
+
/**
|
|
703
|
+
* Release date (ms since epoch) as reported by the provider, where it reports one. Lets the model
|
|
704
|
+
* lists offer the newest models first instead of ordering them by name.
|
|
705
|
+
*/
|
|
706
|
+
readonly released?: number;
|
|
653
707
|
readonly status: LanguageModelStatus;
|
|
654
708
|
readonly reasoningSupport?: ReasoningSupport;
|
|
655
709
|
/**
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Provider-level state of dynamic model discovery.
|
|
19
|
+
*
|
|
20
|
+
* This is distinct from the per-model {@link LanguageModelStatus} (`ready`/`unavailable`):
|
|
21
|
+
* it describes whether a provider's list of models is currently being fetched, has failed,
|
|
22
|
+
* needs credentials or user consent, etc. It gives the UI a place to communicate fetching
|
|
23
|
+
* progress and error state for the whole provider.
|
|
24
|
+
*/
|
|
25
|
+
export type ModelDiscoveryState =
|
|
26
|
+
/** No credential (API key / host) is configured, so nothing can be fetched. */
|
|
27
|
+
| 'no-credentials'
|
|
28
|
+
/** A credential was found in the environment and awaits the user's one-time confirmation. */
|
|
29
|
+
| 'awaiting-consent'
|
|
30
|
+
/** Nothing in progress; no fetch has run yet. */
|
|
31
|
+
| 'idle'
|
|
32
|
+
/** A fetch is currently in flight. */
|
|
33
|
+
| 'fetching'
|
|
34
|
+
/** The last fetch (or cache load) succeeded and models are registered. */
|
|
35
|
+
| 'ready'
|
|
36
|
+
/** The last fetch failed. */
|
|
37
|
+
| 'error'
|
|
38
|
+
/** The model list is configured manually, so the provider is not asked what it offers. */
|
|
39
|
+
| 'overridden';
|
|
40
|
+
|
|
41
|
+
/** Where a provider's effective API key comes from. Used to gate env-var keys behind a one-time consent. */
|
|
42
|
+
export type ApiKeySource = 'preference' | 'environment' | 'none';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A model a provider reported from its list endpoint.
|
|
46
|
+
*
|
|
47
|
+
* Only {@link id} is guaranteed: the endpoints differ widely in what they report about a model.
|
|
48
|
+
* Anthropic gives a display name and a release date, Google additionally a description, and
|
|
49
|
+
* OpenAI nothing but the id.
|
|
50
|
+
*/
|
|
51
|
+
export interface DiscoveredModel {
|
|
52
|
+
/** Provider-native model id, without the `<provider>/` prefix. */
|
|
53
|
+
readonly id: string;
|
|
54
|
+
/** Human-readable name reported by the provider, e.g. `Claude Opus 5`. */
|
|
55
|
+
readonly label?: string;
|
|
56
|
+
/** Longer description reported by the provider. */
|
|
57
|
+
readonly description?: string;
|
|
58
|
+
/** Release date (ms since epoch) reported by the provider, used to order dated variants. */
|
|
59
|
+
readonly released?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Set by a provider that knows better than the generic ranking which of its models should be
|
|
62
|
+
* offered in the model pickers without the user asking. GitHub Copilot serves several vendors, so
|
|
63
|
+
* "the five newest" would offer five models of whichever vendor names its models with the highest
|
|
64
|
+
* numbers; it therefore nominates one per vendor itself. Left unset by the other providers.
|
|
65
|
+
*/
|
|
66
|
+
readonly featured?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Result of a provider model-discovery attempt.
|
|
71
|
+
*
|
|
72
|
+
* On a successful live fetch, `fromCache` is `false`. When the live fetch fails but a cached
|
|
73
|
+
* snapshot is available, the cached models are returned with `fromCache: true` and `error` set to
|
|
74
|
+
* the failure reason, so the UI can keep the models usable while indicating they may be stale.
|
|
75
|
+
*/
|
|
76
|
+
export interface ModelDiscoveryResult {
|
|
77
|
+
/** The discovered models, in the order they should be registered. */
|
|
78
|
+
readonly models: DiscoveredModel[];
|
|
79
|
+
/** Whether the models came from the cached snapshot because the live fetch failed. */
|
|
80
|
+
readonly fromCache: boolean;
|
|
81
|
+
/** Failure reason when the live fetch failed (even if a cached result was returned). */
|
|
82
|
+
readonly error?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* What to do about a discovery state, offered as a button next to the explanation.
|
|
87
|
+
*
|
|
88
|
+
* Most providers need none: what a missing credential wants is the API key setting, which sits on the
|
|
89
|
+
* same page right above. A provider whose credential is established by a command rather than a
|
|
90
|
+
* preference — GitHub Copilot signs in through its CLI — names that command here.
|
|
91
|
+
*/
|
|
92
|
+
export interface ModelDiscoveryAction {
|
|
93
|
+
/** Button label, already localized. */
|
|
94
|
+
readonly label: string;
|
|
95
|
+
readonly commandId: string;
|
|
96
|
+
readonly args?: unknown[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Snapshot of a provider's model-discovery state, as observed on the frontend.
|
|
101
|
+
*/
|
|
102
|
+
export interface ModelDiscoveryStatus {
|
|
103
|
+
/** Provider id, e.g. `anthropic`. Matches the provider node in the Models configuration category. */
|
|
104
|
+
readonly providerId: string;
|
|
105
|
+
/** Human-readable provider label, e.g. `Anthropic`. */
|
|
106
|
+
readonly label: string;
|
|
107
|
+
/**
|
|
108
|
+
* Prefix used for this provider's registered model ids (e.g. `openai` while the provider id is
|
|
109
|
+
* `openAiOfficial`). Defaults to {@link providerId} when omitted.
|
|
110
|
+
*/
|
|
111
|
+
readonly modelIdPrefix?: string;
|
|
112
|
+
readonly state: ModelDiscoveryState;
|
|
113
|
+
/**
|
|
114
|
+
* Overrides the badge label the state would otherwise carry, for a provider the generic wording
|
|
115
|
+
* does not fit: what GitHub Copilot lacks when it has no credential is a sign-in, not an API key.
|
|
116
|
+
*/
|
|
117
|
+
readonly stateLabel?: string;
|
|
118
|
+
/** Optional human-readable detail (error message, hint). */
|
|
119
|
+
readonly message?: string;
|
|
120
|
+
/** Timestamp (ms since epoch) of the last successful fetch, if any. */
|
|
121
|
+
readonly lastFetch?: number;
|
|
122
|
+
/** Whether the currently registered models came from a cached snapshot rather than a live fetch. */
|
|
123
|
+
readonly fromCache?: boolean;
|
|
124
|
+
/**
|
|
125
|
+
* What the provider reported about the models it discovered, keyed by id in
|
|
126
|
+
* {@link DiscoveredModel.id}. The registry stays the source of truth for which models are
|
|
127
|
+
* registered; this only carries the extra metadata (label, description) for display.
|
|
128
|
+
*/
|
|
129
|
+
readonly discovered?: readonly DiscoveredModel[];
|
|
130
|
+
/** What the user can do about the current state, where a command settles it. */
|
|
131
|
+
readonly action?: ModelDiscoveryAction;
|
|
132
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { expect } from 'chai';
|
|
18
|
+
import { DiscoveredModels } from './model-discovery-util';
|
|
19
|
+
|
|
20
|
+
describe('DiscoveredModels.undatedId', () => {
|
|
21
|
+
|
|
22
|
+
it('strips a compact date suffix, as Anthropic uses', () => {
|
|
23
|
+
expect(DiscoveredModels.undatedId('claude-opus-5-20260401')).to.equal('claude-opus-5');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('strips a dashed date suffix, as OpenAI uses', () => {
|
|
27
|
+
expect(DiscoveredModels.undatedId('gpt-4o-2024-11-20')).to.equal('gpt-4o');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('strips the year-less snapshot suffix of OpenAI\'s older models', () => {
|
|
31
|
+
expect(DiscoveredModels.undatedId('gpt-4-0613')).to.equal('gpt-4');
|
|
32
|
+
expect(DiscoveredModels.undatedId('gpt-3.5-turbo-0125')).to.equal('gpt-3.5-turbo');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('returns undefined for an id without a date suffix', () => {
|
|
36
|
+
expect(DiscoveredModels.undatedId('claude-opus-5')).to.equal(undefined);
|
|
37
|
+
expect(DiscoveredModels.undatedId('gemini-3.7-flash')).to.equal(undefined);
|
|
38
|
+
// A version suffix is not a date and must not be mistaken for one.
|
|
39
|
+
expect(DiscoveredModels.undatedId('gemini-1.5-pro-002')).to.equal(undefined);
|
|
40
|
+
// Nor is a context size, even though it is four digits like a month and day.
|
|
41
|
+
expect(DiscoveredModels.undatedId('some-model-4096')).to.equal(undefined);
|
|
42
|
+
expect(DiscoveredModels.undatedId('gpt-4-1340')).to.equal(undefined);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('DiscoveredModels.withUndatedAliases', () => {
|
|
47
|
+
|
|
48
|
+
it('adds the undated alias while keeping the release-pinned ids', () => {
|
|
49
|
+
const result = DiscoveredModels.withUndatedAliases([
|
|
50
|
+
{ id: 'claude-opus-5-20260401' },
|
|
51
|
+
{ id: 'claude-haiku-4-5-20251001' }
|
|
52
|
+
]);
|
|
53
|
+
expect(result.map(model => model.id)).to.deep.equal([
|
|
54
|
+
'claude-opus-5',
|
|
55
|
+
'claude-haiku-4-5',
|
|
56
|
+
'claude-opus-5-20260401',
|
|
57
|
+
'claude-haiku-4-5-20251001'
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('adds one alias per family and gives it the metadata of the newest release', () => {
|
|
62
|
+
const result = DiscoveredModels.withUndatedAliases([
|
|
63
|
+
{ id: 'claude-opus-5-20251120', label: 'Claude Opus 5 (November)', released: Date.parse('2025-11-20') },
|
|
64
|
+
{ id: 'claude-opus-5-20260401', label: 'Claude Opus 5', released: Date.parse('2026-04-01') }
|
|
65
|
+
]);
|
|
66
|
+
expect(result.map(model => model.id)).to.deep.equal([
|
|
67
|
+
'claude-opus-5',
|
|
68
|
+
'claude-opus-5-20251120',
|
|
69
|
+
'claude-opus-5-20260401'
|
|
70
|
+
]);
|
|
71
|
+
expect(result[0].label).to.equal('Claude Opus 5');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('orders the variants by the reported release date, which the year-less ids cannot express', () => {
|
|
75
|
+
const result = DiscoveredModels.withUndatedAliases([
|
|
76
|
+
// June 2023 sorts after January 2024 by id alone; the reported dates say otherwise.
|
|
77
|
+
{ id: 'gpt-4-0613', label: 'older', released: Date.parse('2023-06-13') },
|
|
78
|
+
{ id: 'gpt-4-0125', label: 'newer', released: Date.parse('2024-01-25') }
|
|
79
|
+
]);
|
|
80
|
+
expect(result[0].id).to.equal('gpt-4');
|
|
81
|
+
expect(result[0].label).to.equal('newer');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('falls back to the date in the id when the provider reports none', () => {
|
|
85
|
+
const result = DiscoveredModels.withUndatedAliases([
|
|
86
|
+
{ id: 'claude-opus-5-20251120', label: 'older' },
|
|
87
|
+
{ id: 'claude-opus-5-20260401', label: 'newer' }
|
|
88
|
+
]);
|
|
89
|
+
expect(result[0].label).to.equal('newer');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('leaves an alias the provider reported itself untouched', () => {
|
|
93
|
+
const result = DiscoveredModels.withUndatedAliases([
|
|
94
|
+
{ id: 'gpt-4o', label: 'GPT-4o' },
|
|
95
|
+
{ id: 'gpt-4o-2024-11-20', label: 'GPT-4o (2024-11-20)' }
|
|
96
|
+
]);
|
|
97
|
+
expect(result.map(model => model.id)).to.deep.equal(['gpt-4o', 'gpt-4o-2024-11-20']);
|
|
98
|
+
expect(result[0].label).to.equal('GPT-4o');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('passes undated models through unchanged', () => {
|
|
102
|
+
const models = [{ id: 'gemini-3.7-flash' }, { id: 'gemini-3.1-pro-preview' }];
|
|
103
|
+
expect(DiscoveredModels.withUndatedAliases(models)).to.deep.equal(models);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { DiscoveredModel } from './model-discovery-status';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Helpers for the dated model ids that provider list endpoints report. Providers call these from
|
|
21
|
+
* an overridable method of their language-models manager, so a provider (or an adopter) can opt
|
|
22
|
+
* out or refine the behaviour without reimplementing it.
|
|
23
|
+
*/
|
|
24
|
+
export namespace DiscoveredModels {
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The date suffix providers append to a release-pinned id: `-20260401` and `-2026-04-01`, plus the
|
|
28
|
+
* `-0613` (month and day only) form OpenAI used for its older snapshots. The month and day are
|
|
29
|
+
* range-checked so that an id merely ending in four digits — a context size, a version — is not
|
|
30
|
+
* mistaken for a release.
|
|
31
|
+
*/
|
|
32
|
+
const DATE_SUFFIX = /-(?:(\d{4})-?(0[1-9]|1[0-2])-?(0[1-9]|[12]\d|3[01])|(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01]))$/;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The undated form of a release-pinned model id, e.g. `claude-opus-5-20260401` → `claude-opus-5`.
|
|
36
|
+
* Returns `undefined` when the id carries no date suffix.
|
|
37
|
+
*/
|
|
38
|
+
export function undatedId(id: string): string | undefined {
|
|
39
|
+
const match = DATE_SUFFIX.exec(id);
|
|
40
|
+
return match ? id.substring(0, match.index) : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Adds the undated alias for every release-pinned model, keeping the pinned releases themselves.
|
|
45
|
+
*
|
|
46
|
+
* Providers such as Anthropic and OpenAI list one entry per release (`claude-opus-5-20260401`,
|
|
47
|
+
* `claude-opus-5-20251120`, …) while accepting the undated form (`claude-opus-5`) as a
|
|
48
|
+
* request-time alias for the newest of them. The undated id is what the built-in model aliases
|
|
49
|
+
* reference and what does not change from under a configuration when a new release ships, so it
|
|
50
|
+
* is registered even when the provider does not list it. The pinned releases stay registered too,
|
|
51
|
+
* so a specific release can be selected; what keeps them out of the way of the model pickers is
|
|
52
|
+
* that only undated ids are featured (see the favorite models service), not that they are hidden.
|
|
53
|
+
*
|
|
54
|
+
* The alias takes the metadata of the most recent variant. Aliases come first; the reported models
|
|
55
|
+
* keep their relative order after them. An alias the provider reported itself is left untouched.
|
|
56
|
+
*/
|
|
57
|
+
export function withUndatedAliases(models: DiscoveredModel[]): DiscoveredModel[] {
|
|
58
|
+
const reported = new Set(models.map(model => model.id));
|
|
59
|
+
const aliases = new Map<string, DiscoveredModel>();
|
|
60
|
+
for (const model of models) {
|
|
61
|
+
const id = undatedId(model.id);
|
|
62
|
+
if (!id || reported.has(id)) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const previous = aliases.get(id);
|
|
66
|
+
if (!previous || isNewer(model, previous)) {
|
|
67
|
+
aliases.set(id, { ...model, id });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return [...aliases.values(), ...models];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Whether the id is a pointer a provider maintains at the current model of a family, e.g.
|
|
75
|
+
* `gemini-flash-latest`. Such an id carries no version to compare, and does not need one: it
|
|
76
|
+
* resolves to whatever is newest by definition.
|
|
77
|
+
*/
|
|
78
|
+
export function isLatestPointer(id: string): boolean {
|
|
79
|
+
return id.endsWith('-latest');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The version numbers in a model id, e.g. `gemini-3.7-flash` → `[3, 7]` and `claude-sonnet-4-5`
|
|
84
|
+
* → `[4, 5]`. A heuristic, and only meaningful between the models of one provider — and one
|
|
85
|
+
* vendor at that: it says nothing about `gpt-5` against `claude-sonnet-4.5`.
|
|
86
|
+
*/
|
|
87
|
+
export function versionOf(id: string): number[] {
|
|
88
|
+
return (id.match(/\d+/g) ?? []).map(Number);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Compares two model ids by the version numbers in them, most recent first. A longer sequence wins
|
|
93
|
+
* a shared prefix, so `gemini-3.7` sorts before `gemini-3`.
|
|
94
|
+
*/
|
|
95
|
+
export function compareByVersion(left: string, right: string): number {
|
|
96
|
+
const leftVersion = versionOf(left);
|
|
97
|
+
const rightVersion = versionOf(right);
|
|
98
|
+
for (let index = 0; index < Math.max(leftVersion.length, rightVersion.length); index++) {
|
|
99
|
+
const difference = (rightVersion[index] ?? -1) - (leftVersion[index] ?? -1);
|
|
100
|
+
if (difference !== 0) {
|
|
101
|
+
return difference;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Whether `candidate` is a more recent variant than `current`. The release date the provider
|
|
109
|
+
* reported is authoritative and always comparable, so it decides whenever both carry one; the date
|
|
110
|
+
* in the id is the fallback, and it only orders ids of the same shape — a year-less `-0613` says
|
|
111
|
+
* nothing about which year it belongs to.
|
|
112
|
+
*/
|
|
113
|
+
function isNewer(candidate: DiscoveredModel, current: DiscoveredModel): boolean {
|
|
114
|
+
if (candidate.released !== undefined && current.released !== undefined) {
|
|
115
|
+
return candidate.released > current.released;
|
|
116
|
+
}
|
|
117
|
+
return idDate(candidate.id) > idDate(current.id);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The date in a release-pinned id as a comparable number, or `0` when there is none. */
|
|
121
|
+
function idDate(id: string): number {
|
|
122
|
+
const match = DATE_SUFFIX.exec(id);
|
|
123
|
+
if (!match) {
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
// Either the full `YYYYMMDD` (groups 1-3) or the year-less `MMDD` (groups 4-5).
|
|
127
|
+
return match[1] ? Number(`${match[1]}${match[2]}${match[3]}`) : Number(`${match[4]}${match[5]}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -90,6 +90,12 @@ describe('PromptService', () => {
|
|
|
90
90
|
expect(prompt?.text).to.equal('Ciao, {{invalid}}!');
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
+
it('should insert argument values literally without expanding replacement patterns', async () => {
|
|
94
|
+
const value = "echo $$ $' $& $`";
|
|
95
|
+
const prompt = await promptService.getResolvedPromptFragment('1', { name: value });
|
|
96
|
+
expect(prompt?.text).to.equal(`Hello, ${value}!`);
|
|
97
|
+
});
|
|
98
|
+
|
|
93
99
|
it('should return undefined if the prompt fragment id is not found', async () => {
|
|
94
100
|
const prompt = await promptService.getResolvedPromptFragment('4');
|
|
95
101
|
expect(prompt).to.be.undefined;
|
|
@@ -897,7 +897,7 @@ export class PromptServiceImpl implements PromptService {
|
|
|
897
897
|
let resolvedTemplate = promptFragment.template;
|
|
898
898
|
const variableAndArgResolutions = await this.resolveVariablesAndArgs(promptFragment.template, args, context);
|
|
899
899
|
variableAndArgResolutions.replacements.forEach(replacement =>
|
|
900
|
-
resolvedTemplate = resolvedTemplate.replace(replacement.placeholder, replacement.value));
|
|
900
|
+
resolvedTemplate = resolvedTemplate.replace(replacement.placeholder, () => replacement.value));
|
|
901
901
|
|
|
902
902
|
// Then resolve function references with already resolved variables and arguments
|
|
903
903
|
// This allows to resolve function references contained in resolved variables (e.g. prompt fragments)
|
|
@@ -920,7 +920,7 @@ export class PromptServiceImpl implements PromptService {
|
|
|
920
920
|
};
|
|
921
921
|
});
|
|
922
922
|
functionReplacements.forEach(replacement =>
|
|
923
|
-
resolvedTemplate = resolvedTemplate.replace(replacement.placeholder, replacement.value));
|
|
923
|
+
resolvedTemplate = resolvedTemplate.replace(replacement.placeholder, () => replacement.value));
|
|
924
924
|
|
|
925
925
|
return {
|
|
926
926
|
id: systemOrFragmentId,
|
|
@@ -945,7 +945,7 @@ export class PromptServiceImpl implements PromptService {
|
|
|
945
945
|
const resolutions = await this.resolveVariablesAndArgs(promptFragment.template, args, context, resolveVariable);
|
|
946
946
|
let resolvedTemplate = promptFragment.template;
|
|
947
947
|
resolutions.replacements.forEach(replacement =>
|
|
948
|
-
resolvedTemplate = resolvedTemplate.replace(replacement.placeholder, replacement.value));
|
|
948
|
+
resolvedTemplate = resolvedTemplate.replace(replacement.placeholder, () => replacement.value));
|
|
949
949
|
|
|
950
950
|
return {
|
|
951
951
|
id: systemOrFragmentId,
|
|
@@ -44,6 +44,8 @@ import {
|
|
|
44
44
|
} from '../common';
|
|
45
45
|
import { BackendLanguageModelRegistryImpl } from './backend-language-model-registry';
|
|
46
46
|
import { TokenUsageServiceImpl } from './token-usage-service-impl';
|
|
47
|
+
import { ModelSnapshotStore, ModelSnapshotStoreImpl } from './model-snapshot-store';
|
|
48
|
+
import { ModelDiscoveryFetcher, ModelDiscoveryFetcherImpl } from './model-discovery-fetcher';
|
|
47
49
|
import { AgentSettingsPreferenceSchema } from '../common/agent-preferences';
|
|
48
50
|
import { bindAICorePreferences } from '../common/ai-core-preferences';
|
|
49
51
|
|
|
@@ -112,5 +114,11 @@ const aiCoreConnectionModule = ConnectionContainerModule.create(({ bind, bindBac
|
|
|
112
114
|
export default new ContainerModule(bind => {
|
|
113
115
|
bind(PreferenceContribution).toConstantValue({ schema: AgentSettingsPreferenceSchema });
|
|
114
116
|
bindAICorePreferences(bind);
|
|
117
|
+
// Bound in the root container: the snapshots are per provider, not per frontend connection,
|
|
118
|
+
// and the provider managers resolving them live in connection-scoped child containers.
|
|
119
|
+
bind(ModelSnapshotStoreImpl).toSelf().inSingletonScope();
|
|
120
|
+
bind(ModelSnapshotStore).toService(ModelSnapshotStoreImpl);
|
|
121
|
+
bind(ModelDiscoveryFetcherImpl).toSelf().inSingletonScope();
|
|
122
|
+
bind(ModelDiscoveryFetcher).toService(ModelDiscoveryFetcherImpl);
|
|
115
123
|
bind(ConnectionContainerModule).toConstantValue(aiCoreConnectionModule);
|
|
116
124
|
});
|
|
@@ -62,6 +62,7 @@ export class BackendLanguageModelRegistryImpl extends DefaultLanguageModelRegist
|
|
|
62
62
|
family: model.family,
|
|
63
63
|
maxInputTokens: model.maxInputTokens,
|
|
64
64
|
maxOutputTokens: model.maxOutputTokens,
|
|
65
|
+
released: model.released,
|
|
65
66
|
reasoningSupport: model.reasoningSupport,
|
|
66
67
|
serverTools: model.serverTools,
|
|
67
68
|
serverSideCompactionSupport: model.serverSideCompactionSupport,
|
package/src/node/index.ts
CHANGED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { ILogger } from '@theia/core';
|
|
18
|
+
import { timeout } from '@theia/core/lib/common/promise-util';
|
|
19
|
+
import { inject, injectable, named } from '@theia/core/shared/inversify';
|
|
20
|
+
import { DiscoveredModel, ModelDiscoveryResult } from '../common/model-discovery-status';
|
|
21
|
+
import { ModelSnapshotStore } from './model-snapshot-store';
|
|
22
|
+
|
|
23
|
+
export const ModelDiscoveryFetcher = Symbol('ModelDiscoveryFetcher');
|
|
24
|
+
|
|
25
|
+
/** What a provider contributes to a discovery run; everything around it is the same for all of them. */
|
|
26
|
+
export interface ModelDiscoveryRequest {
|
|
27
|
+
/** Name the provider's snapshot is kept under, e.g. `anthropic-models.json`. */
|
|
28
|
+
readonly snapshotFile: string;
|
|
29
|
+
/** The provider, for the log entry a failed fetch leaves, e.g. `Anthropic`. */
|
|
30
|
+
readonly providerLabel: string;
|
|
31
|
+
/** Asks the provider what it offers. Called again while {@link isRetryable} accepts the failure. */
|
|
32
|
+
listModels(): Promise<DiscoveredModel[]>;
|
|
33
|
+
/**
|
|
34
|
+
* Whether a failure is transient enough to try again. Nothing is retried by default: a provider
|
|
35
|
+
* knows which of its SDK's errors are a connection giving out and which are an answer.
|
|
36
|
+
*/
|
|
37
|
+
isRetryable?(error: unknown): boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Runs a provider's model discovery: asks it what it offers, retries a transient failure, and keeps
|
|
42
|
+
* the answer as a snapshot so the models survive a later failure or an offline start.
|
|
43
|
+
*/
|
|
44
|
+
export interface ModelDiscoveryFetcher {
|
|
45
|
+
/**
|
|
46
|
+
* Discovers the models of one provider. Returns the snapshot with `fromCache` set when the fetch
|
|
47
|
+
* failed and there is one, and rejects with the fetch's own error when there is not.
|
|
48
|
+
*/
|
|
49
|
+
fetch(request: ModelDiscoveryRequest): Promise<ModelDiscoveryResult>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@injectable()
|
|
53
|
+
export class ModelDiscoveryFetcherImpl implements ModelDiscoveryFetcher {
|
|
54
|
+
|
|
55
|
+
/** Number of attempts (including the first) for a discovery fetch on a retryable error. */
|
|
56
|
+
protected maxAttempts = 3;
|
|
57
|
+
/** Base linear backoff between attempts; multiplied by the number of the attempt that failed. */
|
|
58
|
+
protected retryDelayMs = 500;
|
|
59
|
+
|
|
60
|
+
@inject(ModelSnapshotStore)
|
|
61
|
+
protected readonly snapshotStore: ModelSnapshotStore;
|
|
62
|
+
|
|
63
|
+
@inject(ILogger) @named('ai-core:ModelDiscoveryFetcherImpl')
|
|
64
|
+
protected readonly logger: ILogger;
|
|
65
|
+
|
|
66
|
+
async fetch(request: ModelDiscoveryRequest): Promise<ModelDiscoveryResult> {
|
|
67
|
+
try {
|
|
68
|
+
const models = await this.listWithRetry(request);
|
|
69
|
+
await this.snapshotStore.write(request.snapshotFile, models);
|
|
70
|
+
return { models, fromCache: false };
|
|
71
|
+
} catch (error) {
|
|
72
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
73
|
+
const cached = await this.snapshotStore.read(request.snapshotFile);
|
|
74
|
+
if (cached) {
|
|
75
|
+
this.logger.warn(`${request.providerLabel}: model discovery failed (${message}); using cached snapshot.`);
|
|
76
|
+
return { models: cached, fromCache: true, error: message };
|
|
77
|
+
}
|
|
78
|
+
// A run that never succeeded has no snapshot to fall back on, so record the failure here.
|
|
79
|
+
this.logger.error(`${request.providerLabel}: model discovery failed; no cached snapshot to fall back on.`, error);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Lists the models, retrying a transient failure with a linear backoff. */
|
|
85
|
+
protected async listWithRetry(request: ModelDiscoveryRequest): Promise<DiscoveredModel[]> {
|
|
86
|
+
let lastError: unknown;
|
|
87
|
+
for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {
|
|
88
|
+
try {
|
|
89
|
+
return await request.listModels();
|
|
90
|
+
} catch (error) {
|
|
91
|
+
lastError = error;
|
|
92
|
+
if (attempt === this.maxAttempts || !request.isRetryable?.(error)) {
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
await this.delay(this.retryDelayMs * attempt);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
throw lastError;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Waits between two attempts. Overridable so a test does not have to. */
|
|
102
|
+
protected delay(ms: number): Promise<void> {
|
|
103
|
+
return timeout(ms);
|
|
104
|
+
}
|
|
105
|
+
}
|