gsd-pi 2.16.0 → 2.18.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/README.md +39 -0
- package/dist/onboarding.js +2 -2
- package/dist/remote-questions-config.d.ts +10 -0
- package/dist/remote-questions-config.js +36 -0
- package/dist/resources/extensions/gsd/activity-log.ts +37 -7
- package/dist/resources/extensions/gsd/auto-dashboard.ts +4 -0
- package/dist/resources/extensions/gsd/auto-dispatch.ts +9 -3
- package/dist/resources/extensions/gsd/auto-prompts.ts +91 -42
- package/dist/resources/extensions/gsd/auto-recovery.ts +7 -2
- package/dist/resources/extensions/gsd/auto-worktree.ts +33 -4
- package/dist/resources/extensions/gsd/auto.ts +177 -25
- package/dist/resources/extensions/gsd/commands.ts +264 -23
- package/dist/resources/extensions/gsd/complexity.ts +236 -0
- package/dist/resources/extensions/gsd/dispatch-guard.ts +7 -19
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +202 -2
- package/dist/resources/extensions/gsd/files.ts +129 -3
- package/dist/resources/extensions/gsd/git-service.ts +19 -8
- package/dist/resources/extensions/gsd/gitignore.ts +41 -2
- package/dist/resources/extensions/gsd/guided-flow.ts +247 -10
- package/dist/resources/extensions/gsd/index.ts +47 -3
- package/dist/resources/extensions/gsd/metrics.ts +44 -0
- package/dist/resources/extensions/gsd/native-git-bridge.ts +5 -0
- package/dist/resources/extensions/gsd/native-parser-bridge.ts +5 -0
- package/dist/resources/extensions/gsd/paths.ts +9 -0
- package/dist/resources/extensions/gsd/preferences.ts +181 -2
- package/dist/resources/extensions/gsd/prompts/execute-task.md +6 -5
- package/dist/resources/extensions/gsd/prompts/system.md +2 -0
- package/dist/resources/extensions/gsd/queue-order.ts +231 -0
- package/dist/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
- package/dist/resources/extensions/gsd/routing-history.ts +290 -0
- package/dist/resources/extensions/gsd/state.ts +15 -3
- package/dist/resources/extensions/gsd/templates/knowledge.md +19 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +14 -0
- package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +50 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
- package/dist/resources/extensions/gsd/tests/budget-prediction.test.ts +220 -0
- package/dist/resources/extensions/gsd/tests/complexity-routing.test.ts +294 -0
- package/dist/resources/extensions/gsd/tests/context-compression.test.ts +180 -0
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
- package/dist/resources/extensions/gsd/tests/git-service.test.ts +132 -0
- package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
- package/dist/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
- package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
- package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +28 -0
- package/dist/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
- package/dist/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
- package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
- package/dist/resources/extensions/gsd/tests/routing-history.test.ts +87 -0
- package/dist/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
- package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +130 -0
- package/dist/resources/extensions/gsd/tests/token-profile.test.ts +263 -0
- package/dist/resources/extensions/gsd/types.ts +28 -0
- package/dist/resources/extensions/gsd/worktree-manager.ts +8 -5
- package/dist/resources/extensions/gsd/worktree.ts +24 -2
- package/dist/resources/extensions/shared/next-action-ui.ts +16 -1
- package/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +493 -13
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +422 -62
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-shared.d.ts +12 -0
- package/packages/pi-ai/dist/providers/google-shared.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/google-shared.js +9 -22
- package/packages/pi-ai/dist/providers/google-shared.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-shared.test.d.ts +2 -0
- package/packages/pi-ai/dist/providers/google-shared.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/google-shared.test.js +125 -0
- package/packages/pi-ai/dist/providers/google-shared.test.js.map +1 -0
- package/packages/pi-ai/src/models.generated.ts +422 -62
- package/packages/pi-ai/src/providers/google-shared.test.ts +137 -0
- package/packages/pi-ai/src/providers/google-shared.ts +10 -19
- package/packages/pi-coding-agent/dist/cli/args.d.ts +5 -0
- package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/args.js +21 -0
- package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
- package/packages/pi-coding-agent/dist/cli/list-models.d.ts +14 -3
- package/packages/pi-coding-agent/dist/cli/list-models.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/list-models.js +52 -17
- package/packages/pi-coding-agent/dist/cli/list-models.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts +27 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.js +79 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +140 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +35 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.js +162 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js +100 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +113 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts +26 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +98 -0
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts +62 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.js +145 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.js +118 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
- package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts +7 -7
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.js +209 -13
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js +67 -0
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/index.d.ts +5 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +4 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/main.js +17 -2
- package/packages/pi-coding-agent/dist/main.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.js +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +25 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +121 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +32 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js +10 -0
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js.map +1 -1
- package/packages/pi-coding-agent/src/cli/args.ts +21 -0
- package/packages/pi-coding-agent/src/cli/list-models.ts +70 -17
- package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +170 -0
- package/packages/pi-coding-agent/src/core/discovery-cache.ts +97 -0
- package/packages/pi-coding-agent/src/core/model-discovery.test.ts +125 -0
- package/packages/pi-coding-agent/src/core/model-discovery.ts +231 -0
- package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +135 -0
- package/packages/pi-coding-agent/src/core/model-registry.ts +107 -0
- package/packages/pi-coding-agent/src/core/models-json-writer.test.ts +145 -0
- package/packages/pi-coding-agent/src/core/models-json-writer.ts +188 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +21 -0
- package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
- package/packages/pi-coding-agent/src/core/tools/edit-diff.test.ts +85 -0
- package/packages/pi-coding-agent/src/core/tools/edit-diff.ts +245 -17
- package/packages/pi-coding-agent/src/index.ts +5 -0
- package/packages/pi-coding-agent/src/main.ts +19 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/index.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +163 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +37 -0
- package/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts +13 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/pkg/dist/modes/interactive/theme/theme.js +10 -0
- package/pkg/dist/modes/interactive/theme/theme.js.map +1 -1
- package/src/resources/extensions/gsd/activity-log.ts +37 -7
- package/src/resources/extensions/gsd/auto-dashboard.ts +4 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +9 -3
- package/src/resources/extensions/gsd/auto-prompts.ts +91 -42
- package/src/resources/extensions/gsd/auto-recovery.ts +7 -2
- package/src/resources/extensions/gsd/auto-worktree.ts +33 -4
- package/src/resources/extensions/gsd/auto.ts +177 -25
- package/src/resources/extensions/gsd/commands.ts +264 -23
- package/src/resources/extensions/gsd/complexity.ts +236 -0
- package/src/resources/extensions/gsd/dispatch-guard.ts +7 -19
- package/src/resources/extensions/gsd/docs/preferences-reference.md +202 -2
- package/src/resources/extensions/gsd/files.ts +129 -3
- package/src/resources/extensions/gsd/git-service.ts +19 -8
- package/src/resources/extensions/gsd/gitignore.ts +41 -2
- package/src/resources/extensions/gsd/guided-flow.ts +247 -10
- package/src/resources/extensions/gsd/index.ts +47 -3
- package/src/resources/extensions/gsd/metrics.ts +44 -0
- package/src/resources/extensions/gsd/native-git-bridge.ts +5 -0
- package/src/resources/extensions/gsd/native-parser-bridge.ts +5 -0
- package/src/resources/extensions/gsd/paths.ts +9 -0
- package/src/resources/extensions/gsd/preferences.ts +181 -2
- package/src/resources/extensions/gsd/prompts/execute-task.md +6 -5
- package/src/resources/extensions/gsd/prompts/system.md +2 -0
- package/src/resources/extensions/gsd/queue-order.ts +231 -0
- package/src/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
- package/src/resources/extensions/gsd/routing-history.ts +290 -0
- package/src/resources/extensions/gsd/state.ts +15 -3
- package/src/resources/extensions/gsd/templates/knowledge.md +19 -0
- package/src/resources/extensions/gsd/templates/preferences.md +14 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/budget-prediction.test.ts +220 -0
- package/src/resources/extensions/gsd/tests/complexity-routing.test.ts +294 -0
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +180 -0
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +132 -0
- package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
- package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/preferences-git.test.ts +28 -0
- package/src/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
- package/src/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
- package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
- package/src/resources/extensions/gsd/tests/routing-history.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +130 -0
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +263 -0
- package/src/resources/extensions/gsd/types.ts +28 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +8 -5
- package/src/resources/extensions/gsd/worktree.ts +24 -2
- package/src/resources/extensions/shared/next-action-ui.ts +16 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
6
|
+
import { ModelDiscoveryCache } from "./discovery-cache.js";
|
|
7
|
+
|
|
8
|
+
let testDir: string;
|
|
9
|
+
let cachePath: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
testDir = join(tmpdir(), `discovery-cache-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
13
|
+
mkdirSync(testDir, { recursive: true });
|
|
14
|
+
cachePath = join(testDir, "discovery-cache.json");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
try {
|
|
19
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
20
|
+
} catch {
|
|
21
|
+
// Cleanup best-effort
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// ─── basic operations ────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
describe("ModelDiscoveryCache — basic operations", () => {
|
|
28
|
+
it("starts with no entries", () => {
|
|
29
|
+
const cache = new ModelDiscoveryCache(cachePath);
|
|
30
|
+
assert.equal(cache.get("openai"), undefined);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("stores and retrieves models", () => {
|
|
34
|
+
const cache = new ModelDiscoveryCache(cachePath);
|
|
35
|
+
const models = [{ id: "gpt-4o", name: "GPT-4o" }];
|
|
36
|
+
cache.set("openai", models);
|
|
37
|
+
|
|
38
|
+
const entry = cache.get("openai");
|
|
39
|
+
assert.ok(entry);
|
|
40
|
+
assert.deepEqual(entry.models, models);
|
|
41
|
+
assert.ok(entry.fetchedAt > 0);
|
|
42
|
+
assert.ok(entry.ttlMs > 0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("persists to disk and reloads", () => {
|
|
46
|
+
const cache1 = new ModelDiscoveryCache(cachePath);
|
|
47
|
+
cache1.set("openai", [{ id: "gpt-4o" }]);
|
|
48
|
+
|
|
49
|
+
const cache2 = new ModelDiscoveryCache(cachePath);
|
|
50
|
+
const entry = cache2.get("openai");
|
|
51
|
+
assert.ok(entry);
|
|
52
|
+
assert.equal(entry.models[0].id, "gpt-4o");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("clear removes a specific provider", () => {
|
|
56
|
+
const cache = new ModelDiscoveryCache(cachePath);
|
|
57
|
+
cache.set("openai", [{ id: "gpt-4o" }]);
|
|
58
|
+
cache.set("google", [{ id: "gemini-pro" }]);
|
|
59
|
+
|
|
60
|
+
cache.clear("openai");
|
|
61
|
+
assert.equal(cache.get("openai"), undefined);
|
|
62
|
+
assert.ok(cache.get("google"));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("clear without provider removes all entries", () => {
|
|
66
|
+
const cache = new ModelDiscoveryCache(cachePath);
|
|
67
|
+
cache.set("openai", [{ id: "gpt-4o" }]);
|
|
68
|
+
cache.set("google", [{ id: "gemini-pro" }]);
|
|
69
|
+
|
|
70
|
+
cache.clear();
|
|
71
|
+
assert.equal(cache.get("openai"), undefined);
|
|
72
|
+
assert.equal(cache.get("google"), undefined);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ─── staleness ───────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
describe("ModelDiscoveryCache — staleness", () => {
|
|
79
|
+
it("newly set entries are not stale", () => {
|
|
80
|
+
const cache = new ModelDiscoveryCache(cachePath);
|
|
81
|
+
cache.set("openai", [{ id: "gpt-4o" }]);
|
|
82
|
+
assert.equal(cache.isStale("openai"), false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("missing providers are stale", () => {
|
|
86
|
+
const cache = new ModelDiscoveryCache(cachePath);
|
|
87
|
+
assert.equal(cache.isStale("unknown"), true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("entries with expired TTL are stale", () => {
|
|
91
|
+
const cache = new ModelDiscoveryCache(cachePath);
|
|
92
|
+
cache.set("openai", [{ id: "gpt-4o" }], 1); // 1ms TTL
|
|
93
|
+
|
|
94
|
+
// Wait for TTL to expire
|
|
95
|
+
const start = Date.now();
|
|
96
|
+
while (Date.now() - start < 5) {
|
|
97
|
+
// busy wait
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
assert.equal(cache.isStale("openai"), true);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ─── getAll ──────────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
describe("ModelDiscoveryCache — getAll", () => {
|
|
107
|
+
it("returns non-stale entries by default", () => {
|
|
108
|
+
const cache = new ModelDiscoveryCache(cachePath);
|
|
109
|
+
cache.set("openai", [{ id: "gpt-4o" }]);
|
|
110
|
+
cache.set("stale", [{ id: "old" }], 1);
|
|
111
|
+
|
|
112
|
+
// Wait for stale TTL
|
|
113
|
+
const start = Date.now();
|
|
114
|
+
while (Date.now() - start < 5) {
|
|
115
|
+
// busy wait
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const all = cache.getAll();
|
|
119
|
+
assert.ok(all.has("openai"));
|
|
120
|
+
assert.ok(!all.has("stale"));
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("returns all entries when includeStale is true", () => {
|
|
124
|
+
const cache = new ModelDiscoveryCache(cachePath);
|
|
125
|
+
cache.set("openai", [{ id: "gpt-4o" }]);
|
|
126
|
+
cache.set("stale", [{ id: "old" }], 1);
|
|
127
|
+
|
|
128
|
+
// Wait for stale TTL
|
|
129
|
+
const start = Date.now();
|
|
130
|
+
while (Date.now() - start < 5) {
|
|
131
|
+
// busy wait
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const all = cache.getAll(true);
|
|
135
|
+
assert.ok(all.has("openai"));
|
|
136
|
+
assert.ok(all.has("stale"));
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ─── edge cases ──────────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
describe("ModelDiscoveryCache — edge cases", () => {
|
|
143
|
+
it("handles corrupted cache file gracefully", () => {
|
|
144
|
+
writeFileSync(cachePath, "not valid json", "utf-8");
|
|
145
|
+
const cache = new ModelDiscoveryCache(cachePath);
|
|
146
|
+
assert.equal(cache.get("openai"), undefined);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("handles wrong version gracefully", () => {
|
|
150
|
+
writeFileSync(cachePath, JSON.stringify({ version: 99, entries: {} }), "utf-8");
|
|
151
|
+
const cache = new ModelDiscoveryCache(cachePath);
|
|
152
|
+
assert.equal(cache.get("openai"), undefined);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("handles missing cache file", () => {
|
|
156
|
+
const cache = new ModelDiscoveryCache(join(testDir, "nonexistent", "cache.json"));
|
|
157
|
+
assert.equal(cache.get("openai"), undefined);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("overwrites existing entry for same provider", () => {
|
|
161
|
+
const cache = new ModelDiscoveryCache(cachePath);
|
|
162
|
+
cache.set("openai", [{ id: "gpt-4o" }]);
|
|
163
|
+
cache.set("openai", [{ id: "gpt-4o-mini" }]);
|
|
164
|
+
|
|
165
|
+
const entry = cache.get("openai");
|
|
166
|
+
assert.ok(entry);
|
|
167
|
+
assert.equal(entry.models.length, 1);
|
|
168
|
+
assert.equal(entry.models[0].id, "gpt-4o-mini");
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disk-based cache for discovered models.
|
|
3
|
+
* Stores results at {agentDir}/discovery-cache.json with per-provider TTLs.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
7
|
+
import { dirname, join } from "path";
|
|
8
|
+
import { getAgentDir } from "../config.js";
|
|
9
|
+
import { type DiscoveredModel, getDefaultTTL } from "./model-discovery.js";
|
|
10
|
+
|
|
11
|
+
export interface DiscoveryCacheEntry {
|
|
12
|
+
models: DiscoveredModel[];
|
|
13
|
+
fetchedAt: number;
|
|
14
|
+
ttlMs: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DiscoveryCacheData {
|
|
18
|
+
version: 1;
|
|
19
|
+
entries: Record<string, DiscoveryCacheEntry>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class ModelDiscoveryCache {
|
|
23
|
+
private data: DiscoveryCacheData;
|
|
24
|
+
private cachePath: string;
|
|
25
|
+
|
|
26
|
+
constructor(cachePath?: string) {
|
|
27
|
+
this.cachePath = cachePath ?? join(getAgentDir(), "discovery-cache.json");
|
|
28
|
+
this.data = { version: 1, entries: {} };
|
|
29
|
+
this.load();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get(provider: string): DiscoveryCacheEntry | undefined {
|
|
33
|
+
const entry = this.data.entries[provider];
|
|
34
|
+
return entry;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
set(provider: string, models: DiscoveredModel[], ttlMs?: number): void {
|
|
38
|
+
this.data.entries[provider] = {
|
|
39
|
+
models,
|
|
40
|
+
fetchedAt: Date.now(),
|
|
41
|
+
ttlMs: ttlMs ?? getDefaultTTL(provider),
|
|
42
|
+
};
|
|
43
|
+
this.save();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
isStale(provider: string): boolean {
|
|
47
|
+
const entry = this.data.entries[provider];
|
|
48
|
+
if (!entry) return true;
|
|
49
|
+
return Date.now() - entry.fetchedAt > entry.ttlMs;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
clear(provider?: string): void {
|
|
53
|
+
if (provider) {
|
|
54
|
+
delete this.data.entries[provider];
|
|
55
|
+
} else {
|
|
56
|
+
this.data.entries = {};
|
|
57
|
+
}
|
|
58
|
+
this.save();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getAll(includeStale = false): Map<string, DiscoveryCacheEntry> {
|
|
62
|
+
const result = new Map<string, DiscoveryCacheEntry>();
|
|
63
|
+
for (const [provider, entry] of Object.entries(this.data.entries)) {
|
|
64
|
+
if (includeStale || !this.isStale(provider)) {
|
|
65
|
+
result.set(provider, entry);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
load(): void {
|
|
72
|
+
try {
|
|
73
|
+
if (existsSync(this.cachePath)) {
|
|
74
|
+
const content = readFileSync(this.cachePath, "utf-8");
|
|
75
|
+
const parsed = JSON.parse(content) as DiscoveryCacheData;
|
|
76
|
+
if (parsed.version === 1 && parsed.entries) {
|
|
77
|
+
this.data = parsed;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// Corrupted or unreadable cache — start fresh
|
|
82
|
+
this.data = { version: 1, entries: {} };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
save(): void {
|
|
87
|
+
try {
|
|
88
|
+
const dir = dirname(this.cachePath);
|
|
89
|
+
if (!existsSync(dir)) {
|
|
90
|
+
mkdirSync(dir, { recursive: true });
|
|
91
|
+
}
|
|
92
|
+
writeFileSync(this.cachePath, JSON.stringify(this.data, null, 2), "utf-8");
|
|
93
|
+
} catch {
|
|
94
|
+
// Silently ignore write failures (read-only FS, permissions, etc.)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
DISCOVERY_TTLS,
|
|
5
|
+
getDefaultTTL,
|
|
6
|
+
getDiscoverableProviders,
|
|
7
|
+
getDiscoveryAdapter,
|
|
8
|
+
} from "./model-discovery.js";
|
|
9
|
+
|
|
10
|
+
// ─── getDiscoveryAdapter ─────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
describe("getDiscoveryAdapter", () => {
|
|
13
|
+
it("returns an adapter for openai", () => {
|
|
14
|
+
const adapter = getDiscoveryAdapter("openai");
|
|
15
|
+
assert.equal(adapter.provider, "openai");
|
|
16
|
+
assert.equal(adapter.supportsDiscovery, true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("returns an adapter for ollama", () => {
|
|
20
|
+
const adapter = getDiscoveryAdapter("ollama");
|
|
21
|
+
assert.equal(adapter.provider, "ollama");
|
|
22
|
+
assert.equal(adapter.supportsDiscovery, true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("returns an adapter for openrouter", () => {
|
|
26
|
+
const adapter = getDiscoveryAdapter("openrouter");
|
|
27
|
+
assert.equal(adapter.provider, "openrouter");
|
|
28
|
+
assert.equal(adapter.supportsDiscovery, true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("returns an adapter for google", () => {
|
|
32
|
+
const adapter = getDiscoveryAdapter("google");
|
|
33
|
+
assert.equal(adapter.provider, "google");
|
|
34
|
+
assert.equal(adapter.supportsDiscovery, true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns a static adapter for anthropic", () => {
|
|
38
|
+
const adapter = getDiscoveryAdapter("anthropic");
|
|
39
|
+
assert.equal(adapter.provider, "anthropic");
|
|
40
|
+
assert.equal(adapter.supportsDiscovery, false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("returns a static adapter for bedrock", () => {
|
|
44
|
+
const adapter = getDiscoveryAdapter("bedrock");
|
|
45
|
+
assert.equal(adapter.provider, "bedrock");
|
|
46
|
+
assert.equal(adapter.supportsDiscovery, false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("returns a static adapter for unknown providers", () => {
|
|
50
|
+
const adapter = getDiscoveryAdapter("unknown-provider");
|
|
51
|
+
assert.equal(adapter.provider, "unknown-provider");
|
|
52
|
+
assert.equal(adapter.supportsDiscovery, false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("static adapter fetchModels returns empty array", async () => {
|
|
56
|
+
const adapter = getDiscoveryAdapter("anthropic");
|
|
57
|
+
const models = await adapter.fetchModels("key");
|
|
58
|
+
assert.deepEqual(models, []);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ─── getDiscoverableProviders ────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
describe("getDiscoverableProviders", () => {
|
|
65
|
+
it("returns only providers that support discovery", () => {
|
|
66
|
+
const providers = getDiscoverableProviders();
|
|
67
|
+
assert.ok(providers.includes("openai"));
|
|
68
|
+
assert.ok(providers.includes("ollama"));
|
|
69
|
+
assert.ok(providers.includes("openrouter"));
|
|
70
|
+
assert.ok(providers.includes("google"));
|
|
71
|
+
assert.ok(!providers.includes("anthropic"));
|
|
72
|
+
assert.ok(!providers.includes("bedrock"));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("returns an array of strings", () => {
|
|
76
|
+
const providers = getDiscoverableProviders();
|
|
77
|
+
assert.ok(Array.isArray(providers));
|
|
78
|
+
for (const p of providers) {
|
|
79
|
+
assert.equal(typeof p, "string");
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ─── getDefaultTTL ───────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
describe("getDefaultTTL", () => {
|
|
87
|
+
it("returns 5 minutes for ollama", () => {
|
|
88
|
+
assert.equal(getDefaultTTL("ollama"), 5 * 60 * 1000);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("returns 1 hour for openai", () => {
|
|
92
|
+
assert.equal(getDefaultTTL("openai"), 60 * 60 * 1000);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("returns 1 hour for google", () => {
|
|
96
|
+
assert.equal(getDefaultTTL("google"), 60 * 60 * 1000);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("returns 1 hour for openrouter", () => {
|
|
100
|
+
assert.equal(getDefaultTTL("openrouter"), 60 * 60 * 1000);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("returns 24 hours for unknown providers", () => {
|
|
104
|
+
assert.equal(getDefaultTTL("some-custom"), 24 * 60 * 60 * 1000);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ─── DISCOVERY_TTLS ──────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
describe("DISCOVERY_TTLS", () => {
|
|
111
|
+
it("has expected keys", () => {
|
|
112
|
+
assert.ok("ollama" in DISCOVERY_TTLS);
|
|
113
|
+
assert.ok("openai" in DISCOVERY_TTLS);
|
|
114
|
+
assert.ok("google" in DISCOVERY_TTLS);
|
|
115
|
+
assert.ok("openrouter" in DISCOVERY_TTLS);
|
|
116
|
+
assert.ok("default" in DISCOVERY_TTLS);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("all values are positive numbers", () => {
|
|
120
|
+
for (const [, value] of Object.entries(DISCOVERY_TTLS)) {
|
|
121
|
+
assert.equal(typeof value, "number");
|
|
122
|
+
assert.ok(value > 0);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider discovery adapters for runtime model enumeration.
|
|
3
|
+
* Each adapter implements ProviderDiscoveryAdapter to fetch models from provider APIs.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface DiscoveredModel {
|
|
7
|
+
id: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
contextWindow?: number;
|
|
10
|
+
maxTokens?: number;
|
|
11
|
+
reasoning?: boolean;
|
|
12
|
+
input?: ("text" | "image")[];
|
|
13
|
+
cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface DiscoveryResult {
|
|
17
|
+
provider: string;
|
|
18
|
+
models: DiscoveredModel[];
|
|
19
|
+
fetchedAt: number;
|
|
20
|
+
error?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ProviderDiscoveryAdapter {
|
|
24
|
+
provider: string;
|
|
25
|
+
supportsDiscovery: boolean;
|
|
26
|
+
fetchModels(apiKey: string, baseUrl?: string): Promise<DiscoveredModel[]>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Per-provider TTLs in milliseconds */
|
|
30
|
+
export const DISCOVERY_TTLS: Record<string, number> = {
|
|
31
|
+
ollama: 5 * 60 * 1000, // 5 minutes (local, models change often)
|
|
32
|
+
openai: 60 * 60 * 1000, // 1 hour
|
|
33
|
+
google: 60 * 60 * 1000, // 1 hour
|
|
34
|
+
openrouter: 60 * 60 * 1000, // 1 hour
|
|
35
|
+
default: 24 * 60 * 60 * 1000, // 24 hours
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function getDefaultTTL(provider: string): number {
|
|
39
|
+
return DISCOVERY_TTLS[provider] ?? DISCOVERY_TTLS.default;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function fetchWithTimeout(url: string, options: RequestInit = {}, timeoutMs = 5000): Promise<Response> {
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
45
|
+
try {
|
|
46
|
+
return await fetch(url, { ...options, signal: controller.signal });
|
|
47
|
+
} finally {
|
|
48
|
+
clearTimeout(timeout);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── OpenAI Adapter ──────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
const OPENAI_EXCLUDED_PREFIXES = ["embedding", "tts", "dall-e", "whisper", "text-embedding", "davinci", "babbage"];
|
|
55
|
+
|
|
56
|
+
class OpenAIDiscoveryAdapter implements ProviderDiscoveryAdapter {
|
|
57
|
+
provider = "openai";
|
|
58
|
+
supportsDiscovery = true;
|
|
59
|
+
|
|
60
|
+
async fetchModels(apiKey: string, baseUrl?: string): Promise<DiscoveredModel[]> {
|
|
61
|
+
const url = `${baseUrl ?? "https://api.openai.com"}/v1/models`;
|
|
62
|
+
const response = await fetchWithTimeout(url, {
|
|
63
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new Error(`OpenAI models API returned ${response.status}: ${response.statusText}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const data = (await response.json()) as { data: Array<{ id: string; owned_by?: string }> };
|
|
71
|
+
return data.data
|
|
72
|
+
.filter((m) => !OPENAI_EXCLUDED_PREFIXES.some((prefix) => m.id.startsWith(prefix)))
|
|
73
|
+
.map((m) => ({
|
|
74
|
+
id: m.id,
|
|
75
|
+
name: m.id,
|
|
76
|
+
input: ["text" as const, "image" as const],
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ─── Ollama Adapter ──────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
class OllamaDiscoveryAdapter implements ProviderDiscoveryAdapter {
|
|
84
|
+
provider = "ollama";
|
|
85
|
+
supportsDiscovery = true;
|
|
86
|
+
|
|
87
|
+
async fetchModels(_apiKey: string, baseUrl?: string): Promise<DiscoveredModel[]> {
|
|
88
|
+
const url = `${baseUrl ?? "http://localhost:11434"}/api/tags`;
|
|
89
|
+
const response = await fetchWithTimeout(url);
|
|
90
|
+
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
throw new Error(`Ollama tags API returned ${response.status}: ${response.statusText}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const data = (await response.json()) as {
|
|
96
|
+
models: Array<{ name: string; size: number; details?: { parameter_size?: string } }>;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return (data.models ?? []).map((m) => ({
|
|
100
|
+
id: m.name,
|
|
101
|
+
name: m.name,
|
|
102
|
+
input: ["text" as const],
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─── OpenRouter Adapter ──────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
class OpenRouterDiscoveryAdapter implements ProviderDiscoveryAdapter {
|
|
110
|
+
provider = "openrouter";
|
|
111
|
+
supportsDiscovery = true;
|
|
112
|
+
|
|
113
|
+
async fetchModels(apiKey: string, baseUrl?: string): Promise<DiscoveredModel[]> {
|
|
114
|
+
const url = `${baseUrl ?? "https://openrouter.ai"}/api/v1/models`;
|
|
115
|
+
const response = await fetchWithTimeout(url, {
|
|
116
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
throw new Error(`OpenRouter models API returned ${response.status}: ${response.statusText}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const data = (await response.json()) as {
|
|
124
|
+
data: Array<{
|
|
125
|
+
id: string;
|
|
126
|
+
name: string;
|
|
127
|
+
context_length?: number;
|
|
128
|
+
top_provider?: { max_completion_tokens?: number };
|
|
129
|
+
pricing?: { prompt: string; completion: string };
|
|
130
|
+
}>;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return (data.data ?? []).map((m) => {
|
|
134
|
+
const cost =
|
|
135
|
+
m.pricing?.prompt !== undefined && m.pricing?.completion !== undefined
|
|
136
|
+
? {
|
|
137
|
+
input: parseFloat(m.pricing.prompt) * 1_000_000,
|
|
138
|
+
output: parseFloat(m.pricing.completion) * 1_000_000,
|
|
139
|
+
cacheRead: 0,
|
|
140
|
+
cacheWrite: 0,
|
|
141
|
+
}
|
|
142
|
+
: undefined;
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
id: m.id,
|
|
146
|
+
name: m.name,
|
|
147
|
+
contextWindow: m.context_length,
|
|
148
|
+
maxTokens: m.top_provider?.max_completion_tokens,
|
|
149
|
+
cost,
|
|
150
|
+
input: ["text" as const, "image" as const],
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─── Google/Gemini Adapter ───────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
class GoogleDiscoveryAdapter implements ProviderDiscoveryAdapter {
|
|
159
|
+
provider = "google";
|
|
160
|
+
supportsDiscovery = true;
|
|
161
|
+
|
|
162
|
+
async fetchModels(apiKey: string, baseUrl?: string): Promise<DiscoveredModel[]> {
|
|
163
|
+
const url = `${baseUrl ?? "https://generativelanguage.googleapis.com"}/v1beta/models?key=${apiKey}`;
|
|
164
|
+
const response = await fetchWithTimeout(url);
|
|
165
|
+
|
|
166
|
+
if (!response.ok) {
|
|
167
|
+
throw new Error(`Google models API returned ${response.status}: ${response.statusText}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const data = (await response.json()) as {
|
|
171
|
+
models: Array<{
|
|
172
|
+
name: string;
|
|
173
|
+
displayName: string;
|
|
174
|
+
supportedGenerationMethods?: string[];
|
|
175
|
+
inputTokenLimit?: number;
|
|
176
|
+
outputTokenLimit?: number;
|
|
177
|
+
}>;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
return (data.models ?? [])
|
|
181
|
+
.filter((m) => m.supportedGenerationMethods?.includes("generateContent"))
|
|
182
|
+
.map((m) => ({
|
|
183
|
+
id: m.name.replace("models/", ""),
|
|
184
|
+
name: m.displayName,
|
|
185
|
+
contextWindow: m.inputTokenLimit,
|
|
186
|
+
maxTokens: m.outputTokenLimit,
|
|
187
|
+
input: ["text" as const, "image" as const],
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ─── Static Adapter (no discovery) ───────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
class StaticDiscoveryAdapter implements ProviderDiscoveryAdapter {
|
|
195
|
+
provider: string;
|
|
196
|
+
supportsDiscovery = false;
|
|
197
|
+
|
|
198
|
+
constructor(provider: string) {
|
|
199
|
+
this.provider = provider;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async fetchModels(): Promise<DiscoveredModel[]> {
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ─── Registry ────────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
const adapters: Record<string, ProviderDiscoveryAdapter> = {
|
|
210
|
+
openai: new OpenAIDiscoveryAdapter(),
|
|
211
|
+
ollama: new OllamaDiscoveryAdapter(),
|
|
212
|
+
openrouter: new OpenRouterDiscoveryAdapter(),
|
|
213
|
+
google: new GoogleDiscoveryAdapter(),
|
|
214
|
+
anthropic: new StaticDiscoveryAdapter("anthropic"),
|
|
215
|
+
bedrock: new StaticDiscoveryAdapter("bedrock"),
|
|
216
|
+
"azure-openai": new StaticDiscoveryAdapter("azure-openai"),
|
|
217
|
+
groq: new StaticDiscoveryAdapter("groq"),
|
|
218
|
+
cerebras: new StaticDiscoveryAdapter("cerebras"),
|
|
219
|
+
xai: new StaticDiscoveryAdapter("xai"),
|
|
220
|
+
mistral: new StaticDiscoveryAdapter("mistral"),
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
export function getDiscoveryAdapter(provider: string): ProviderDiscoveryAdapter {
|
|
224
|
+
return adapters[provider] ?? new StaticDiscoveryAdapter(provider);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function getDiscoverableProviders(): string[] {
|
|
228
|
+
return Object.entries(adapters)
|
|
229
|
+
.filter(([, adapter]) => adapter.supportsDiscovery)
|
|
230
|
+
.map(([name]) => name);
|
|
231
|
+
}
|