@theia/ai-core 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 (107) hide show
  1. package/lib/browser/ai-core-frontend-module.d.ts.map +1 -1
  2. package/lib/browser/ai-core-frontend-module.js +4 -0
  3. package/lib/browser/ai-core-frontend-module.js.map +1 -1
  4. package/lib/browser/discovering-provider-contribution.d.ts +92 -0
  5. package/lib/browser/discovering-provider-contribution.d.ts.map +1 -0
  6. package/lib/browser/discovering-provider-contribution.js +189 -0
  7. package/lib/browser/discovering-provider-contribution.js.map +1 -0
  8. package/lib/browser/discovering-provider-contribution.spec.d.ts +2 -0
  9. package/lib/browser/discovering-provider-contribution.spec.d.ts.map +1 -0
  10. package/lib/browser/discovering-provider-contribution.spec.js +232 -0
  11. package/lib/browser/discovering-provider-contribution.spec.js.map +1 -0
  12. package/lib/browser/favorite-models-service.d.ts +135 -0
  13. package/lib/browser/favorite-models-service.d.ts.map +1 -0
  14. package/lib/browser/favorite-models-service.js +279 -0
  15. package/lib/browser/favorite-models-service.js.map +1 -0
  16. package/lib/browser/favorite-models-service.spec.d.ts +2 -0
  17. package/lib/browser/favorite-models-service.spec.d.ts.map +1 -0
  18. package/lib/browser/favorite-models-service.spec.js +277 -0
  19. package/lib/browser/favorite-models-service.spec.js.map +1 -0
  20. package/lib/browser/index.d.ts +3 -0
  21. package/lib/browser/index.d.ts.map +1 -1
  22. package/lib/browser/index.js +3 -0
  23. package/lib/browser/index.js.map +1 -1
  24. package/lib/browser/model-discovery-status-service.d.ts +51 -0
  25. package/lib/browser/model-discovery-status-service.d.ts.map +1 -0
  26. package/lib/browser/model-discovery-status-service.js +103 -0
  27. package/lib/browser/model-discovery-status-service.js.map +1 -0
  28. package/lib/browser/model-discovery-status-service.spec.d.ts +2 -0
  29. package/lib/browser/model-discovery-status-service.spec.d.ts.map +1 -0
  30. package/lib/browser/model-discovery-status-service.spec.js +81 -0
  31. package/lib/browser/model-discovery-status-service.spec.js.map +1 -0
  32. package/lib/common/ai-core-preferences.d.ts +11 -0
  33. package/lib/common/ai-core-preferences.d.ts.map +1 -1
  34. package/lib/common/ai-core-preferences.js +37 -1
  35. package/lib/common/ai-core-preferences.js.map +1 -1
  36. package/lib/common/index.d.ts +2 -0
  37. package/lib/common/index.d.ts.map +1 -1
  38. package/lib/common/index.js +2 -0
  39. package/lib/common/index.js.map +1 -1
  40. package/lib/common/language-model-ordering.spec.d.ts +2 -0
  41. package/lib/common/language-model-ordering.spec.d.ts.map +1 -0
  42. package/lib/common/language-model-ordering.spec.js +72 -0
  43. package/lib/common/language-model-ordering.spec.js.map +1 -0
  44. package/lib/common/language-model-util.d.ts +17 -1
  45. package/lib/common/language-model-util.d.ts.map +1 -1
  46. package/lib/common/language-model-util.js +44 -1
  47. package/lib/common/language-model-util.js.map +1 -1
  48. package/lib/common/language-model.d.ts +5 -0
  49. package/lib/common/language-model.d.ts.map +1 -1
  50. package/lib/common/language-model.js.map +1 -1
  51. package/lib/common/model-discovery-status.d.ts +112 -0
  52. package/lib/common/model-discovery-status.d.ts.map +1 -0
  53. package/lib/common/model-discovery-status.js +18 -0
  54. package/lib/common/model-discovery-status.js.map +1 -0
  55. package/lib/common/model-discovery-util.d.ts +46 -0
  56. package/lib/common/model-discovery-util.d.ts.map +1 -0
  57. package/lib/common/model-discovery-util.js +128 -0
  58. package/lib/common/model-discovery-util.js.map +1 -0
  59. package/lib/common/model-discovery-util.spec.d.ts +2 -0
  60. package/lib/common/model-discovery-util.spec.d.ts.map +1 -0
  61. package/lib/common/model-discovery-util.spec.js +95 -0
  62. package/lib/common/model-discovery-util.spec.js.map +1 -0
  63. package/lib/node/ai-core-backend-module.d.ts.map +1 -1
  64. package/lib/node/ai-core-backend-module.js +8 -0
  65. package/lib/node/ai-core-backend-module.js.map +1 -1
  66. package/lib/node/backend-language-model-registry.d.ts.map +1 -1
  67. package/lib/node/backend-language-model-registry.js +1 -0
  68. package/lib/node/backend-language-model-registry.js.map +1 -1
  69. package/lib/node/index.d.ts +2 -0
  70. package/lib/node/index.d.ts.map +1 -1
  71. package/lib/node/index.js +2 -0
  72. package/lib/node/index.js.map +1 -1
  73. package/lib/node/model-discovery-fetcher.d.ts +43 -0
  74. package/lib/node/model-discovery-fetcher.d.ts.map +1 -0
  75. package/lib/node/model-discovery-fetcher.js +83 -0
  76. package/lib/node/model-discovery-fetcher.js.map +1 -0
  77. package/lib/node/model-snapshot-store.d.ts +26 -0
  78. package/lib/node/model-snapshot-store.d.ts.map +1 -0
  79. package/lib/node/model-snapshot-store.js +73 -0
  80. package/lib/node/model-snapshot-store.js.map +1 -0
  81. package/lib/node/test/test-model-discovery-fetcher.d.ts +21 -0
  82. package/lib/node/test/test-model-discovery-fetcher.d.ts.map +1 -0
  83. package/lib/node/test/test-model-discovery-fetcher.js +54 -0
  84. package/lib/node/test/test-model-discovery-fetcher.js.map +1 -0
  85. package/package.json +9 -9
  86. package/src/browser/ai-core-frontend-module.ts +5 -0
  87. package/src/browser/discovering-provider-contribution.spec.ts +303 -0
  88. package/src/browser/discovering-provider-contribution.ts +241 -0
  89. package/src/browser/favorite-models-service.spec.ts +336 -0
  90. package/src/browser/favorite-models-service.ts +297 -0
  91. package/src/browser/index.ts +3 -0
  92. package/src/browser/model-discovery-status-service.spec.ts +95 -0
  93. package/src/browser/model-discovery-status-service.ts +118 -0
  94. package/src/common/ai-core-preferences.ts +40 -0
  95. package/src/common/index.ts +2 -0
  96. package/src/common/language-model-ordering.spec.ts +83 -0
  97. package/src/common/language-model-util.ts +43 -0
  98. package/src/common/language-model.ts +5 -0
  99. package/src/common/model-discovery-status.ts +132 -0
  100. package/src/common/model-discovery-util.spec.ts +105 -0
  101. package/src/common/model-discovery-util.ts +129 -0
  102. package/src/node/ai-core-backend-module.ts +8 -0
  103. package/src/node/backend-language-model-registry.ts +1 -0
  104. package/src/node/index.ts +2 -0
  105. package/src/node/model-discovery-fetcher.ts +103 -0
  106. package/src/node/model-snapshot-store.ts +80 -0
  107. package/src/node/test/test-model-discovery-fetcher.ts +62 -0
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ // *****************************************************************************
3
+ // Copyright (C) 2026 EclipseSource GmbH.
4
+ //
5
+ // This program and the accompanying materials are made available under the
6
+ // terms of the Eclipse Public License v. 2.0 which is available at
7
+ // http://www.eclipse.org/legal/epl-2.0.
8
+ //
9
+ // This Source Code may also be made available under the following Secondary
10
+ // Licenses when the conditions for such availability set forth in the Eclipse
11
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
12
+ // with the GNU Classpath Exception which is available at
13
+ // https://www.gnu.org/software/classpath/license.html.
14
+ //
15
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
16
+ // *****************************************************************************
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.ModelDiscoveryFetcherImpl = exports.ModelDiscoveryFetcher = void 0;
19
+ const tslib_1 = require("tslib");
20
+ const core_1 = require("@theia/core");
21
+ const promise_util_1 = require("@theia/core/lib/common/promise-util");
22
+ const inversify_1 = require("@theia/core/shared/inversify");
23
+ const model_snapshot_store_1 = require("./model-snapshot-store");
24
+ exports.ModelDiscoveryFetcher = Symbol('ModelDiscoveryFetcher');
25
+ let ModelDiscoveryFetcherImpl = class ModelDiscoveryFetcherImpl {
26
+ constructor() {
27
+ /** Number of attempts (including the first) for a discovery fetch on a retryable error. */
28
+ this.maxAttempts = 3;
29
+ /** Base linear backoff between attempts; multiplied by the number of the attempt that failed. */
30
+ this.retryDelayMs = 500;
31
+ }
32
+ async fetch(request) {
33
+ try {
34
+ const models = await this.listWithRetry(request);
35
+ await this.snapshotStore.write(request.snapshotFile, models);
36
+ return { models, fromCache: false };
37
+ }
38
+ catch (error) {
39
+ const message = error instanceof Error ? error.message : String(error);
40
+ const cached = await this.snapshotStore.read(request.snapshotFile);
41
+ if (cached) {
42
+ this.logger.warn(`${request.providerLabel}: model discovery failed (${message}); using cached snapshot.`);
43
+ return { models: cached, fromCache: true, error: message };
44
+ }
45
+ throw error;
46
+ }
47
+ }
48
+ /** Lists the models, retrying a transient failure with a linear backoff. */
49
+ async listWithRetry(request) {
50
+ let lastError;
51
+ for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {
52
+ try {
53
+ return await request.listModels();
54
+ }
55
+ catch (error) {
56
+ lastError = error;
57
+ if (attempt === this.maxAttempts || !request.isRetryable?.(error)) {
58
+ break;
59
+ }
60
+ await this.delay(this.retryDelayMs * attempt);
61
+ }
62
+ }
63
+ throw lastError;
64
+ }
65
+ /** Waits between two attempts. Overridable so a test does not have to. */
66
+ delay(ms) {
67
+ return (0, promise_util_1.timeout)(ms);
68
+ }
69
+ };
70
+ exports.ModelDiscoveryFetcherImpl = ModelDiscoveryFetcherImpl;
71
+ tslib_1.__decorate([
72
+ (0, inversify_1.inject)(model_snapshot_store_1.ModelSnapshotStore),
73
+ tslib_1.__metadata("design:type", Object)
74
+ ], ModelDiscoveryFetcherImpl.prototype, "snapshotStore", void 0);
75
+ tslib_1.__decorate([
76
+ (0, inversify_1.inject)(core_1.ILogger),
77
+ (0, inversify_1.named)('ai-core:ModelDiscoveryFetcherImpl'),
78
+ tslib_1.__metadata("design:type", Object)
79
+ ], ModelDiscoveryFetcherImpl.prototype, "logger", void 0);
80
+ exports.ModelDiscoveryFetcherImpl = ModelDiscoveryFetcherImpl = tslib_1.__decorate([
81
+ (0, inversify_1.injectable)()
82
+ ], ModelDiscoveryFetcherImpl);
83
+ //# sourceMappingURL=model-discovery-fetcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-discovery-fetcher.js","sourceRoot":"","sources":["../../src/node/model-discovery-fetcher.ts"],"names":[],"mappings":";AAAA,gFAAgF;AAChF,yCAAyC;AACzC,EAAE;AACF,2EAA2E;AAC3E,mEAAmE;AACnE,wCAAwC;AACxC,EAAE;AACF,4EAA4E;AAC5E,8EAA8E;AAC9E,6EAA6E;AAC7E,yDAAyD;AACzD,uDAAuD;AACvD,EAAE;AACF,gFAAgF;AAChF,gFAAgF;;;;AAEhF,sCAAsC;AACtC,sEAA8D;AAC9D,4DAAyE;AAEzE,iEAA4D;AAE/C,QAAA,qBAAqB,GAAG,MAAM,CAAC,uBAAuB,CAAC,CAAC;AA8B9D,IAAM,yBAAyB,GAA/B,MAAM,yBAAyB;IAA/B;QAEH,2FAA2F;QACjF,gBAAW,GAAG,CAAC,CAAC;QAC1B,iGAAiG;QACvF,iBAAY,GAAG,GAAG,CAAC;IA6CjC,CAAC;IArCG,KAAK,CAAC,KAAK,CAAC,OAA8B;QACtC,IAAI,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;YACjD,MAAM,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,OAAO,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;YAC7D,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;QACxC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACb,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACvE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;YACnE,IAAI,MAAM,EAAE,CAAC;gBACT,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,aAAa,6BAA6B,OAAO,2BAA2B,CAAC,CAAC;gBAC1G,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;YAC/D,CAAC;YACD,MAAM,KAAK,CAAC;QAChB,CAAC;IACL,CAAC;IAED,4EAA4E;IAClE,KAAK,CAAC,aAAa,CAAC,OAA8B;QACxD,IAAI,SAAkB,CAAC;QACvB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,IAAI,CAAC,WAAW,EAAE,OAAO,EAAE,EAAE,CAAC;YAC3D,IAAI,CAAC;gBACD,OAAO,MAAM,OAAO,CAAC,UAAU,EAAE,CAAC;YACtC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACb,SAAS,GAAG,KAAK,CAAC;gBAClB,IAAI,OAAO,KAAK,IAAI,CAAC,WAAW,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC;oBAChE,MAAM;gBACV,CAAC;gBACD,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,CAAC;YAClD,CAAC;QACL,CAAC;QACD,MAAM,SAAS,CAAC;IACpB,CAAC;IAED,0EAA0E;IAChE,KAAK,CAAC,EAAU;QACtB,OAAO,IAAA,sBAAO,EAAC,EAAE,CAAC,CAAC;IACvB,CAAC;CACJ,CAAA;AAlDY,8DAAyB;AAQf;IADlB,IAAA,kBAAM,EAAC,yCAAkB,CAAC;;gEAC0B;AAGlC;IADlB,IAAA,kBAAM,EAAC,cAAO,CAAC;IAAE,IAAA,iBAAK,EAAC,mCAAmC,CAAC;;yDACzB;oCAX1B,yBAAyB;IADrC,IAAA,sBAAU,GAAE;GACA,yBAAyB,CAkDrC"}
@@ -0,0 +1,26 @@
1
+ import { ILogger } from '@theia/core';
2
+ import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
3
+ import { DiscoveredModel } from '../common/model-discovery-status';
4
+ export declare const ModelSnapshotStore: unique symbol;
5
+ /** Directory below the configuration directory that holds the snapshots, one file per provider. */
6
+ export declare const MODEL_SNAPSHOT_DIRECTORY = "model-snapshots";
7
+ /**
8
+ * Persists what a provider's model discovery found, so the models stay available when a later
9
+ * fetch fails or the application starts offline.
10
+ */
11
+ export interface ModelSnapshotStore {
12
+ /** Reads a provider's snapshot, or `undefined` when there is none (or it cannot be read). */
13
+ read(fileName: string): Promise<DiscoveredModel[] | undefined>;
14
+ /** Writes a provider's snapshot. Failures are logged, not thrown: a snapshot is a convenience. */
15
+ write(fileName: string, models: DiscoveredModel[]): Promise<void>;
16
+ }
17
+ /** Stores one JSON file per provider in {@link MODEL_SNAPSHOT_DIRECTORY} below the configuration directory. */
18
+ export declare class ModelSnapshotStoreImpl implements ModelSnapshotStore {
19
+ protected readonly envVariablesServer: EnvVariablesServer;
20
+ protected readonly logger: ILogger;
21
+ read(fileName: string): Promise<DiscoveredModel[] | undefined>;
22
+ write(fileName: string, models: DiscoveredModel[]): Promise<void>;
23
+ protected isDiscoveredModel(candidate: unknown): candidate is DiscoveredModel;
24
+ protected snapshotPath(fileName: string): Promise<string>;
25
+ }
26
+ //# sourceMappingURL=model-snapshot-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-snapshot-store.d.ts","sourceRoot":"","sources":["../../src/node/model-snapshot-store.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,OAAO,EAAY,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAG1E,OAAO,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AAEnE,eAAO,MAAM,kBAAkB,eAA+B,CAAC;AAE/D,mGAAmG;AACnG,eAAO,MAAM,wBAAwB,oBAAoB,CAAC;AAE1D;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IAC/B,6FAA6F;IAC7F,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,GAAG,SAAS,CAAC,CAAC;IAC/D,kGAAkG;IAClG,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrE;AAED,+GAA+G;AAC/G,qBACa,sBAAuB,YAAW,kBAAkB;IAG7D,SAAS,CAAC,QAAQ,CAAC,kBAAkB,EAAE,kBAAkB,CAAC;IAG1D,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IAE7B,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,GAAG,SAAS,CAAC;IAW9D,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAUvE,SAAS,CAAC,iBAAiB,CAAC,SAAS,EAAE,OAAO,GAAG,SAAS,IAAI,eAAe;cAI7D,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAIlE"}
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ // *****************************************************************************
3
+ // Copyright (C) 2026 EclipseSource GmbH.
4
+ //
5
+ // This program and the accompanying materials are made available under the
6
+ // terms of the Eclipse Public License v. 2.0 which is available at
7
+ // http://www.eclipse.org/legal/epl-2.0.
8
+ //
9
+ // This Source Code may also be made available under the following Secondary
10
+ // Licenses when the conditions for such availability set forth in the Eclipse
11
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
12
+ // with the GNU Classpath Exception which is available at
13
+ // https://www.gnu.org/software/classpath/license.html.
14
+ //
15
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
16
+ // *****************************************************************************
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.ModelSnapshotStoreImpl = exports.MODEL_SNAPSHOT_DIRECTORY = exports.ModelSnapshotStore = void 0;
19
+ const tslib_1 = require("tslib");
20
+ const fs_1 = require("fs");
21
+ const path = require("path");
22
+ const core_1 = require("@theia/core");
23
+ const env_variables_1 = require("@theia/core/lib/common/env-variables");
24
+ const uri_1 = require("@theia/core/lib/common/uri");
25
+ const inversify_1 = require("@theia/core/shared/inversify");
26
+ exports.ModelSnapshotStore = Symbol('ModelSnapshotStore');
27
+ /** Directory below the configuration directory that holds the snapshots, one file per provider. */
28
+ exports.MODEL_SNAPSHOT_DIRECTORY = 'model-snapshots';
29
+ /** Stores one JSON file per provider in {@link MODEL_SNAPSHOT_DIRECTORY} below the configuration directory. */
30
+ let ModelSnapshotStoreImpl = class ModelSnapshotStoreImpl {
31
+ async read(fileName) {
32
+ try {
33
+ const content = await fs_1.promises.readFile(await this.snapshotPath(fileName), 'utf8');
34
+ const models = JSON.parse(content).models;
35
+ return Array.isArray(models) ? models.filter(model => this.isDiscoveredModel(model)) : undefined;
36
+ }
37
+ catch {
38
+ // A missing or unreadable snapshot is an expected state, not an error: discovery falls back to a live fetch.
39
+ return undefined;
40
+ }
41
+ }
42
+ async write(fileName, models) {
43
+ try {
44
+ const file = await this.snapshotPath(fileName);
45
+ await fs_1.promises.mkdir(path.dirname(file), { recursive: true });
46
+ await fs_1.promises.writeFile(file, JSON.stringify({ models }, undefined, 2));
47
+ }
48
+ catch (error) {
49
+ this.logger.warn(`Failed to persist the model snapshot '${fileName}':`, error instanceof Error ? error.message : error);
50
+ }
51
+ }
52
+ isDiscoveredModel(candidate) {
53
+ return (0, core_1.isObject)(candidate) && typeof candidate.id === 'string';
54
+ }
55
+ async snapshotPath(fileName) {
56
+ const configDirUri = await this.envVariablesServer.getConfigDirUri();
57
+ return new uri_1.default(configDirUri).resolve(exports.MODEL_SNAPSHOT_DIRECTORY).resolve(fileName).path.fsPath();
58
+ }
59
+ };
60
+ exports.ModelSnapshotStoreImpl = ModelSnapshotStoreImpl;
61
+ tslib_1.__decorate([
62
+ (0, inversify_1.inject)(env_variables_1.EnvVariablesServer),
63
+ tslib_1.__metadata("design:type", Object)
64
+ ], ModelSnapshotStoreImpl.prototype, "envVariablesServer", void 0);
65
+ tslib_1.__decorate([
66
+ (0, inversify_1.inject)(core_1.ILogger),
67
+ (0, inversify_1.named)('ai-core:ModelSnapshotStoreImpl'),
68
+ tslib_1.__metadata("design:type", Object)
69
+ ], ModelSnapshotStoreImpl.prototype, "logger", void 0);
70
+ exports.ModelSnapshotStoreImpl = ModelSnapshotStoreImpl = tslib_1.__decorate([
71
+ (0, inversify_1.injectable)()
72
+ ], ModelSnapshotStoreImpl);
73
+ //# sourceMappingURL=model-snapshot-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-snapshot-store.js","sourceRoot":"","sources":["../../src/node/model-snapshot-store.ts"],"names":[],"mappings":";AAAA,gFAAgF;AAChF,yCAAyC;AACzC,EAAE;AACF,2EAA2E;AAC3E,mEAAmE;AACnE,wCAAwC;AACxC,EAAE;AACF,4EAA4E;AAC5E,8EAA8E;AAC9E,6EAA6E;AAC7E,yDAAyD;AACzD,uDAAuD;AACvD,EAAE;AACF,gFAAgF;AAChF,gFAAgF;;;;AAEhF,2BAAoC;AACpC,6BAA6B;AAC7B,sCAAgD;AAChD,wEAA0E;AAC1E,oDAA6C;AAC7C,4DAAyE;AAG5D,QAAA,kBAAkB,GAAG,MAAM,CAAC,oBAAoB,CAAC,CAAC;AAE/D,mGAAmG;AACtF,QAAA,wBAAwB,GAAG,iBAAiB,CAAC;AAa1D,+GAA+G;AAExG,IAAM,sBAAsB,GAA5B,MAAM,sBAAsB;IAQ/B,KAAK,CAAC,IAAI,CAAC,QAAgB;QACvB,IAAI,CAAC;YACD,MAAM,OAAO,GAAG,MAAM,aAAE,CAAC,QAAQ,CAAC,MAAM,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAC;YAC7E,MAAM,MAAM,GAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAA0B,CAAC,MAAM,CAAC;YACpE,OAAO,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACrG,CAAC;QAAC,MAAM,CAAC;YACL,6GAA6G;YAC7G,OAAO,SAAS,CAAC;QACrB,CAAC;IACL,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,QAAgB,EAAE,MAAyB;QACnD,IAAI,CAAC;YACD,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;YAC/C,MAAM,aAAE,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACxD,MAAM,aAAE,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC;QACvE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACb,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,yCAAyC,QAAQ,IAAI,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QAC5H,CAAC;IACL,CAAC;IAES,iBAAiB,CAAC,SAAkB;QAC1C,OAAO,IAAA,eAAQ,EAAkB,SAAS,CAAC,IAAI,OAAO,SAAS,CAAC,EAAE,KAAK,QAAQ,CAAC;IACpF,CAAC;IAES,KAAK,CAAC,YAAY,CAAC,QAAgB;QACzC,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,eAAe,EAAE,CAAC;QACrE,OAAO,IAAI,aAAG,CAAC,YAAY,CAAC,CAAC,OAAO,CAAC,gCAAwB,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;IACnG,CAAC;CACJ,CAAA;AArCY,wDAAsB;AAGZ;IADlB,IAAA,kBAAM,EAAC,kCAAkB,CAAC;;kEAC+B;AAGvC;IADlB,IAAA,kBAAM,EAAC,cAAO,CAAC;IAAE,IAAA,iBAAK,EAAC,gCAAgC,CAAC;;sDACtB;iCAN1B,sBAAsB;IADlC,IAAA,sBAAU,GAAE;GACA,sBAAsB,CAqClC"}
@@ -0,0 +1,21 @@
1
+ import { DiscoveredModel } from '../../common/model-discovery-status';
2
+ import { ModelDiscoveryFetcherImpl } from '../model-discovery-fetcher';
3
+ import { ModelSnapshotStore } from '../model-snapshot-store';
4
+ /** Keeps what a discovery run writes in memory, so a test can seed and inspect the snapshot. */
5
+ export declare class InMemoryModelSnapshotStore implements ModelSnapshotStore {
6
+ snapshot: DiscoveredModel[] | undefined;
7
+ read(): Promise<DiscoveredModel[] | undefined>;
8
+ write(_fileName: string, models: DiscoveredModel[]): Promise<void>;
9
+ }
10
+ /**
11
+ * The real fetcher against an in-memory snapshot store and without the waiting, so that a provider's
12
+ * spec exercises the retry and fallback behaviour it actually runs with.
13
+ */
14
+ export declare class TestModelDiscoveryFetcher extends ModelDiscoveryFetcherImpl {
15
+ readonly store: InMemoryModelSnapshotStore;
16
+ get snapshot(): DiscoveredModel[] | undefined;
17
+ set snapshot(models: DiscoveredModel[] | undefined);
18
+ constructor();
19
+ protected delay(): Promise<void>;
20
+ }
21
+ //# sourceMappingURL=test-model-discovery-fetcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-model-discovery-fetcher.d.ts","sourceRoot":"","sources":["../../../src/node/test/test-model-discovery-fetcher.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAC;AACtE,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AACvE,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,gGAAgG;AAChG,qBAAa,0BAA2B,YAAW,kBAAkB;IAEjE,QAAQ,EAAE,eAAe,EAAE,GAAG,SAAS,CAAC;IAElC,IAAI,IAAI,OAAO,CAAC,eAAe,EAAE,GAAG,SAAS,CAAC;IAI9C,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CAG3E;AAED;;;GAGG;AACH,qBAAa,yBAA0B,SAAQ,yBAAyB;IAEpE,QAAQ,CAAC,KAAK,6BAAoC;IAElD,IAAI,QAAQ,IAAI,eAAe,EAAE,GAAG,SAAS,CAE5C;IAED,IAAI,QAAQ,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,SAAS,EAEjD;;cASkB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG5C"}
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ // *****************************************************************************
3
+ // Copyright (C) 2026 EclipseSource GmbH.
4
+ //
5
+ // This program and the accompanying materials are made available under the
6
+ // terms of the Eclipse Public License v. 2.0 which is available at
7
+ // http://www.eclipse.org/legal/epl-2.0.
8
+ //
9
+ // This Source Code may also be made available under the following Secondary
10
+ // Licenses when the conditions for such availability set forth in the Eclipse
11
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
12
+ // with the GNU Classpath Exception which is available at
13
+ // https://www.gnu.org/software/classpath/license.html.
14
+ //
15
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
16
+ // *****************************************************************************
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.TestModelDiscoveryFetcher = exports.InMemoryModelSnapshotStore = void 0;
19
+ const mock_logger_1 = require("@theia/core/lib/common/test/mock-logger");
20
+ const model_discovery_fetcher_1 = require("../model-discovery-fetcher");
21
+ /** Keeps what a discovery run writes in memory, so a test can seed and inspect the snapshot. */
22
+ class InMemoryModelSnapshotStore {
23
+ async read() {
24
+ return this.snapshot;
25
+ }
26
+ async write(_fileName, models) {
27
+ this.snapshot = [...models];
28
+ }
29
+ }
30
+ exports.InMemoryModelSnapshotStore = InMemoryModelSnapshotStore;
31
+ /**
32
+ * The real fetcher against an in-memory snapshot store and without the waiting, so that a provider's
33
+ * spec exercises the retry and fallback behaviour it actually runs with.
34
+ */
35
+ class TestModelDiscoveryFetcher extends model_discovery_fetcher_1.ModelDiscoveryFetcherImpl {
36
+ get snapshot() {
37
+ return this.store.snapshot;
38
+ }
39
+ set snapshot(models) {
40
+ this.store.snapshot = models;
41
+ }
42
+ constructor() {
43
+ super();
44
+ this.store = new InMemoryModelSnapshotStore();
45
+ const injected = this;
46
+ injected.snapshotStore = this.store;
47
+ injected.logger = new mock_logger_1.MockLogger();
48
+ }
49
+ delay() {
50
+ return Promise.resolve();
51
+ }
52
+ }
53
+ exports.TestModelDiscoveryFetcher = TestModelDiscoveryFetcher;
54
+ //# sourceMappingURL=test-model-discovery-fetcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-model-discovery-fetcher.js","sourceRoot":"","sources":["../../../src/node/test/test-model-discovery-fetcher.ts"],"names":[],"mappings":";AAAA,gFAAgF;AAChF,yCAAyC;AACzC,EAAE;AACF,2EAA2E;AAC3E,mEAAmE;AACnE,wCAAwC;AACxC,EAAE;AACF,4EAA4E;AAC5E,8EAA8E;AAC9E,6EAA6E;AAC7E,yDAAyD;AACzD,uDAAuD;AACvD,EAAE;AACF,gFAAgF;AAChF,gFAAgF;;;AAEhF,yEAAqE;AAErE,wEAAuE;AAGvE,gGAAgG;AAChG,MAAa,0BAA0B;IAInC,KAAK,CAAC,IAAI;QACN,OAAO,IAAI,CAAC,QAAQ,CAAC;IACzB,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,SAAiB,EAAE,MAAyB;QACpD,IAAI,CAAC,QAAQ,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC;IAChC,CAAC;CACJ;AAXD,gEAWC;AAED;;;GAGG;AACH,MAAa,yBAA0B,SAAQ,mDAAyB;IAIpE,IAAI,QAAQ;QACR,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;IAC/B,CAAC;IAED,IAAI,QAAQ,CAAC,MAAqC;QAC9C,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,MAAM,CAAC;IACjC,CAAC;IAED;QACI,KAAK,EAAE,CAAC;QAXH,UAAK,GAAG,IAAI,0BAA0B,EAAE,CAAC;QAY9C,MAAM,QAAQ,GAAG,IAA4E,CAAC;QAC9F,QAAQ,CAAC,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC;QACpC,QAAQ,CAAC,MAAM,GAAG,IAAI,wBAAU,EAAE,CAAC;IACvC,CAAC;IAEkB,KAAK;QACpB,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC7B,CAAC;CACJ;AAtBD,8DAsBC"}
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@theia/ai-core",
3
- "version": "1.76.0-next.28+98d1a268f",
3
+ "version": "1.76.0-next.34+258d11e3e",
4
4
  "description": "Theia - AI Core",
5
5
  "dependencies": {
6
- "@theia/core": "1.76.0-next.28+98d1a268f",
7
- "@theia/editor": "1.76.0-next.28+98d1a268f",
8
- "@theia/filesystem": "1.76.0-next.28+98d1a268f",
9
- "@theia/monaco": "1.76.0-next.28+98d1a268f",
6
+ "@theia/core": "1.76.0-next.34+258d11e3e",
7
+ "@theia/editor": "1.76.0-next.34+258d11e3e",
8
+ "@theia/filesystem": "1.76.0-next.34+258d11e3e",
9
+ "@theia/monaco": "1.76.0-next.34+258d11e3e",
10
10
  "@theia/monaco-editor-core": "1.108.201",
11
- "@theia/output": "1.76.0-next.28+98d1a268f",
12
- "@theia/variable-resolver": "1.76.0-next.28+98d1a268f",
13
- "@theia/workspace": "1.76.0-next.28+98d1a268f",
11
+ "@theia/output": "1.76.0-next.34+258d11e3e",
12
+ "@theia/variable-resolver": "1.76.0-next.34+258d11e3e",
13
+ "@theia/workspace": "1.76.0-next.34+258d11e3e",
14
14
  "@types/js-yaml": "^4.0.9",
15
15
  "fast-deep-equal": "^3.1.3",
16
16
  "js-yaml": "^4.3.2",
@@ -58,5 +58,5 @@
58
58
  "nyc": {
59
59
  "extends": "../../configs/nyc.json"
60
60
  },
61
- "gitHead": "98d1a268f03bc1a6709b0fbf6842c54c2ee13ccb"
61
+ "gitHead": "258d11e3e42d4527e85bd480fce9b22f72d4ab7c"
62
62
  }
@@ -22,6 +22,8 @@ import {
22
22
  import { ContainerModule } from '@theia/core/shared/inversify';
23
23
  import { DefaultLanguageModelAliasRegistry } from './frontend-language-model-alias-registry';
24
24
  import { HideAiPreferencesContribution, aiConfigurationOpenPlaceholderSchema } from './hide-ai-preferences-contribution';
25
+ import { ModelDiscoveryStatusService } from './model-discovery-status-service';
26
+ import { FavoriteModelsService } from './favorite-models-service';
25
27
  import { TrustAwarePreferenceReader } from './trust-aware-preference-reader';
26
28
  import { AiConfigurationServiceImpl } from './ai-configuration-service-impl';
27
29
  import { LanguageModelAliasRegistry } from '../common/language-model-alias';
@@ -207,6 +209,9 @@ export default new ContainerModule(bind => {
207
209
  // ...and leave a single placeholder entry there that links to the AI Configuration view.
208
210
  bind(PreferenceContribution).toConstantValue({ schema: aiConfigurationOpenPlaceholderSchema });
209
211
 
212
+ bind(ModelDiscoveryStatusService).toSelf().inSingletonScope();
213
+ bind(FavoriteModelsService).toSelf().inSingletonScope();
214
+
210
215
  // Internal implementation detail of AiConfigurationService; consumers inject AiConfigurationService.
211
216
  bind(TrustAwarePreferenceReader).toSelf().inSingletonScope();
212
217
 
@@ -0,0 +1,303 @@
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 { ApiKeySource, DiscoveredModel, ModelDiscoveryResult } from '../common/model-discovery-status';
19
+ import { DiscoveringLanguageModelsManager, DiscoveringProviderContribution, ModelDiscoveryMessages } from './discovering-provider-contribution';
20
+ import { ModelDiscoveryStatusService } from './model-discovery-status-service';
21
+
22
+ interface TestModelDescription {
23
+ id: string;
24
+ model: string;
25
+ }
26
+
27
+ const OVERRIDES_PREF = 'test.modelOverrides';
28
+ const ALLOW_ENV_PREF = 'test.allowEnvironmentApiKey';
29
+
30
+ class TestManager implements DiscoveringLanguageModelsManager<TestModelDescription> {
31
+
32
+ result: ModelDiscoveryResult = { models: [], fromCache: false };
33
+ /** Consumed one entry per fetch, so a test can give consecutive runs different answers. */
34
+ readonly resultQueue: ModelDiscoveryResult[] = [];
35
+ failWith: Error | undefined;
36
+ apiKeySource: ApiKeySource = 'preference';
37
+ fetchCalls = 0;
38
+ allowEnvironmentApiKey: boolean | undefined;
39
+ /** Every registration and removal in the order they happened, e.g. `remove:test/a`. */
40
+ readonly calls: string[] = [];
41
+
42
+ async fetchAvailableModels(): Promise<ModelDiscoveryResult> {
43
+ this.fetchCalls++;
44
+ if (this.failWith) {
45
+ throw this.failWith;
46
+ }
47
+ return this.resultQueue.shift() ?? this.result;
48
+ }
49
+
50
+ async getApiKeySource(): Promise<ApiKeySource> {
51
+ return this.apiKeySource;
52
+ }
53
+
54
+ setAllowEnvironmentApiKey(allowed: boolean): void {
55
+ this.allowEnvironmentApiKey = allowed;
56
+ }
57
+
58
+ async createOrUpdateLanguageModels(...modelDescriptions: TestModelDescription[]): Promise<void> {
59
+ this.calls.push(`register:${modelDescriptions.map(description => description.id).join(',')}`);
60
+ }
61
+
62
+ removeLanguageModels(...modelIds: string[]): void {
63
+ this.calls.push(`remove:${modelIds.join(',')}`);
64
+ }
65
+ }
66
+
67
+ class TestContribution extends DiscoveringProviderContribution<TestModelDescription> {
68
+
69
+ protected readonly providerId = 'test';
70
+ protected readonly providerLabel = 'Test Provider';
71
+ protected readonly modelOverridesPreference = OVERRIDES_PREF;
72
+ protected readonly allowEnvironmentApiKeyPreference = ALLOW_ENV_PREF;
73
+ protected readonly manager: TestManager;
74
+
75
+ initialized = 0;
76
+
77
+ constructor(manager: TestManager) {
78
+ super();
79
+ this.manager = manager;
80
+ }
81
+
82
+ protected get discoveryMessages(): ModelDiscoveryMessages {
83
+ return {
84
+ noCredentials: 'no key',
85
+ consentRequired: 'confirm the key',
86
+ consentPrompt: 'may I use the key?',
87
+ useEnvironmentKey: 'Use key',
88
+ overridden: 'configured by hand',
89
+ cached: error => `cached: ${error}`
90
+ };
91
+ }
92
+
93
+ protected createModelDescription(model: DiscoveredModel): TestModelDescription {
94
+ return { id: this.qualifiedModelId(model.id), model: model.id };
95
+ }
96
+
97
+ protected override initializeProvider(): void {
98
+ this.initialized++;
99
+ }
100
+
101
+ /** The queued entry point, which is protected on the contribution itself. */
102
+ discover(): Promise<void> {
103
+ return this.discoverAndRegisterModels();
104
+ }
105
+ }
106
+
107
+ class PrefixedContribution extends TestContribution {
108
+ protected override get modelIdPrefix(): string {
109
+ return 'prefixed';
110
+ }
111
+ }
112
+
113
+ function createContribution(
114
+ preferences: Record<string, unknown> = {},
115
+ factory: (testManager: TestManager) => TestContribution = testManager => new TestContribution(testManager)
116
+ ): {
117
+ contribution: TestContribution;
118
+ manager: TestManager;
119
+ status: ModelDiscoveryStatusService;
120
+ written: Record<string, unknown>;
121
+ prompts: string[];
122
+ answer: (choice: string | undefined) => void;
123
+ } {
124
+ const manager = new TestManager();
125
+ const contribution = factory(manager);
126
+ const status = new ModelDiscoveryStatusService();
127
+ const written: Record<string, unknown> = {};
128
+ const prompts: string[] = [];
129
+ let choice: string | undefined;
130
+ const preferenceService = {
131
+ ready: Promise.resolve(),
132
+ get: (name: string, defaultValue: unknown) => (name in preferences ? preferences[name] : defaultValue),
133
+ set: async (name: string, value: unknown) => { written[name] = value; },
134
+ onPreferenceChanged: () => ({ dispose: () => { } })
135
+ };
136
+ const messageService = {
137
+ info: async (text: string) => {
138
+ prompts.push(text);
139
+ return choice;
140
+ }
141
+ };
142
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
143
+ const internals = contribution as any;
144
+ internals.preferenceService = preferenceService;
145
+ internals.discoveryStatus = status;
146
+ internals.messageService = messageService;
147
+ return { contribution, manager, status, written, prompts, answer: next => { choice = next; } };
148
+ }
149
+
150
+ function discovered(id: string): DiscoveredModel {
151
+ return { id };
152
+ }
153
+
154
+ describe('DiscoveringProviderContribution', () => {
155
+
156
+ it('registers what the provider offers and reports it as ready', async () => {
157
+ const { contribution, manager, status } = createContribution();
158
+ manager.result = { models: [discovered('a'), discovered('b')], fromCache: false };
159
+
160
+ await contribution.discover();
161
+
162
+ expect(manager.calls).to.deep.equal(['remove:', 'register:test/a,test/b']);
163
+ const reported = status.getStatus('test');
164
+ expect(reported?.state).to.equal('ready');
165
+ expect(reported?.discovered?.map(entry => entry.id)).to.deep.equal(['a', 'b']);
166
+ expect(reported?.lastFetch).to.be.a('number');
167
+ });
168
+
169
+ it('unregisters the models a later discovery no longer offers', async () => {
170
+ const { contribution, manager } = createContribution();
171
+ manager.result = { models: [discovered('a'), discovered('b')], fromCache: false };
172
+ await contribution.discover();
173
+ manager.calls.length = 0;
174
+
175
+ manager.result = { models: [discovered('b')], fromCache: false };
176
+ await contribution.discover();
177
+
178
+ expect(manager.calls).to.deep.equal(['remove:test/a', 'register:test/b']);
179
+ });
180
+
181
+ it('qualifies the model ids with the prefix the provider registers under', async () => {
182
+ const { contribution, manager } = createContribution({}, testManager => new PrefixedContribution(testManager));
183
+ manager.result = { models: [discovered('a')], fromCache: false };
184
+ await contribution.discover();
185
+ manager.calls.length = 0;
186
+
187
+ manager.result = { models: [], fromCache: false };
188
+ await contribution.discover();
189
+
190
+ expect(manager.calls).to.deep.equal(['remove:prefixed/a', 'register:']);
191
+ });
192
+
193
+ it('replaces discovery with a configured list without asking the provider', async () => {
194
+ const { contribution, manager, status } = createContribution({ [OVERRIDES_PREF]: ['pinned-1', 'pinned-2'] });
195
+
196
+ await contribution.discover();
197
+
198
+ expect(manager.fetchCalls).to.equal(0);
199
+ expect(manager.calls).to.deep.equal(['remove:', 'register:test/pinned-1,test/pinned-2']);
200
+ expect(status.getStatus('test')?.state).to.equal('overridden');
201
+ });
202
+
203
+ it('drops the registered models and asks for a credential when there is none', async () => {
204
+ const { contribution, manager, status } = createContribution();
205
+ manager.result = { models: [discovered('a')], fromCache: false };
206
+ await contribution.discover();
207
+ manager.calls.length = 0;
208
+
209
+ manager.apiKeySource = 'none';
210
+ await contribution.discover();
211
+
212
+ expect(manager.calls).to.deep.equal(['remove:test/a']);
213
+ expect(manager.fetchCalls).to.equal(1);
214
+ expect(status.getStatus('test')?.state).to.equal('no-credentials');
215
+ expect(status.getStatus('test')?.message).to.equal('no key');
216
+ });
217
+
218
+ it('withholds an environment key until it is confirmed, asking once', async () => {
219
+ const { contribution, manager, status, prompts } = createContribution();
220
+ manager.apiKeySource = 'environment';
221
+
222
+ await contribution.discover();
223
+ await contribution.discover();
224
+
225
+ expect(manager.fetchCalls).to.equal(0);
226
+ expect(status.getStatus('test')?.state).to.equal('awaiting-consent');
227
+ // Asked once per session, however often the state is reached again.
228
+ expect(prompts).to.deep.equal(['may I use the key?']);
229
+ });
230
+
231
+ it('persists the consent when the user grants it', async () => {
232
+ const { contribution, manager, written, answer } = createContribution();
233
+ manager.apiKeySource = 'environment';
234
+ answer('Use key');
235
+
236
+ await contribution.discover();
237
+ // The prompt is answered asynchronously, after the run itself has returned.
238
+ await Promise.resolve();
239
+
240
+ expect(written[ALLOW_ENV_PREF]).to.equal(true);
241
+ });
242
+
243
+ it('uses an environment key once the consent is on record', async () => {
244
+ const { contribution, manager, status } = createContribution({ [ALLOW_ENV_PREF]: true });
245
+ manager.apiKeySource = 'environment';
246
+ manager.result = { models: [discovered('a')], fromCache: false };
247
+
248
+ await contribution.discover();
249
+
250
+ expect(manager.fetchCalls).to.equal(1);
251
+ expect(status.getStatus('test')?.state).to.equal('ready');
252
+ });
253
+
254
+ it('keeps cached models usable and says that they may be stale', async () => {
255
+ const { contribution, status } = createContribution();
256
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
257
+ (contribution as any).manager.result = { models: [discovered('a')], fromCache: true, error: 'network down' };
258
+
259
+ await contribution.discover();
260
+
261
+ const reported = status.getStatus('test');
262
+ expect(reported?.state).to.equal('ready');
263
+ expect(reported?.fromCache).to.equal(true);
264
+ expect(reported?.message).to.equal('cached: network down');
265
+ // Nothing was fetched, so there is no successful fetch to date.
266
+ expect(reported?.lastFetch).to.equal(undefined);
267
+ });
268
+
269
+ it('reports a failed run instead of rejecting, since nobody awaits it', async () => {
270
+ const { contribution, manager, status } = createContribution();
271
+ manager.failWith = new Error('the endpoint is gone');
272
+
273
+ // Rejecting here would surface as an unhandled rejection from the unawaited startup call.
274
+ await contribution.discover();
275
+
276
+ expect(status.getStatus('test')?.state).to.equal('error');
277
+ expect(status.getStatus('test')?.message).to.equal('the endpoint is gone');
278
+ });
279
+
280
+ it('queues overlapping runs so they cannot compute what to unregister from the same list', async () => {
281
+ const { contribution, manager } = createContribution();
282
+ // The runs start before either has fetched, so what they see has to come from the queue.
283
+ manager.resultQueue.push({ models: [discovered('a')], fromCache: false }, { models: [discovered('b')], fromCache: false });
284
+
285
+ await Promise.all([contribution.discover(), contribution.discover()]);
286
+
287
+ // The second run sees what the first registered, rather than the empty list both started from.
288
+ expect(manager.calls).to.deep.equal(['remove:', 'register:test/a', 'remove:test/a', 'register:test/b']);
289
+ });
290
+
291
+ it('registers the provider, hands over the consent and discovers on startup', async () => {
292
+ const { contribution, manager, status } = createContribution();
293
+ manager.result = { models: [discovered('a')], fromCache: false };
294
+
295
+ contribution.onStart();
296
+ await contribution.discover();
297
+
298
+ expect(status.getStatus('test')?.label).to.equal('Test Provider');
299
+ expect(manager.allowEnvironmentApiKey).to.equal(false);
300
+ expect(contribution.initialized).to.equal(1);
301
+ expect(manager.fetchCalls).to.be.greaterThan(0);
302
+ });
303
+ });