gsd-pi 2.17.0 → 2.19.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 +14 -2
- package/dist/resources/extensions/gsd/auto-prompts.ts +65 -16
- package/dist/resources/extensions/gsd/auto-worktree.ts +33 -4
- package/dist/resources/extensions/gsd/auto.ts +399 -29
- package/dist/resources/extensions/gsd/captures.ts +384 -0
- package/dist/resources/extensions/gsd/commands.ts +382 -23
- package/dist/resources/extensions/gsd/complexity-classifier.ts +322 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +10 -0
- package/dist/resources/extensions/gsd/dispatch-guard.ts +7 -19
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +201 -2
- package/dist/resources/extensions/gsd/files.ts +123 -1
- package/dist/resources/extensions/gsd/guided-flow.ts +237 -4
- package/dist/resources/extensions/gsd/index.ts +47 -3
- package/dist/resources/extensions/gsd/metrics.ts +48 -0
- package/dist/resources/extensions/gsd/model-cost-table.ts +65 -0
- package/dist/resources/extensions/gsd/model-router.ts +256 -0
- package/dist/resources/extensions/gsd/paths.ts +9 -0
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/dist/resources/extensions/gsd/preferences.ts +132 -1
- package/dist/resources/extensions/gsd/prompt-loader.ts +45 -9
- package/dist/resources/extensions/gsd/prompts/execute-task.md +6 -5
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
- package/dist/resources/extensions/gsd/prompts/replan-slice.md +8 -0
- package/dist/resources/extensions/gsd/prompts/system.md +2 -0
- package/dist/resources/extensions/gsd/prompts/triage-captures.md +62 -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/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-worktree.test.ts +20 -0
- package/dist/resources/extensions/gsd/tests/captures.test.ts +438 -0
- package/dist/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
- package/dist/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -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/milestone-transition-worktree.test.ts +144 -0
- package/dist/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
- package/dist/resources/extensions/gsd/tests/model-router.test.ts +167 -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/remote-questions.test.ts +227 -1
- package/dist/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
- package/dist/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
- package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
- package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +198 -0
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +255 -0
- package/dist/resources/extensions/gsd/triage-resolution.ts +200 -0
- package/dist/resources/extensions/gsd/triage-ui.ts +175 -0
- package/dist/resources/extensions/gsd/visualizer-data.ts +154 -0
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +193 -0
- package/dist/resources/extensions/gsd/visualizer-views.ts +293 -0
- package/dist/resources/extensions/gsd/worktree-manager.ts +8 -5
- package/dist/resources/extensions/gsd/worktree.ts +22 -0
- package/dist/resources/extensions/remote-questions/discord-adapter.ts +33 -0
- package/dist/resources/extensions/remote-questions/format.ts +12 -6
- package/dist/resources/extensions/remote-questions/manager.ts +8 -0
- package/dist/resources/extensions/shared/next-action-ui.ts +16 -1
- package/package.json +1 -1
- 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/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/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/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/src/resources/extensions/gsd/activity-log.ts +37 -7
- package/src/resources/extensions/gsd/auto-dashboard.ts +14 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +65 -16
- package/src/resources/extensions/gsd/auto-worktree.ts +33 -4
- package/src/resources/extensions/gsd/auto.ts +399 -29
- package/src/resources/extensions/gsd/captures.ts +384 -0
- package/src/resources/extensions/gsd/commands.ts +382 -23
- package/src/resources/extensions/gsd/complexity-classifier.ts +322 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +10 -0
- package/src/resources/extensions/gsd/dispatch-guard.ts +7 -19
- package/src/resources/extensions/gsd/docs/preferences-reference.md +201 -2
- package/src/resources/extensions/gsd/files.ts +123 -1
- package/src/resources/extensions/gsd/guided-flow.ts +237 -4
- package/src/resources/extensions/gsd/index.ts +47 -3
- package/src/resources/extensions/gsd/metrics.ts +48 -0
- package/src/resources/extensions/gsd/model-cost-table.ts +65 -0
- package/src/resources/extensions/gsd/model-router.ts +256 -0
- package/src/resources/extensions/gsd/paths.ts +9 -0
- package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/src/resources/extensions/gsd/preferences.ts +132 -1
- package/src/resources/extensions/gsd/prompt-loader.ts +45 -9
- package/src/resources/extensions/gsd/prompts/execute-task.md +6 -5
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
- package/src/resources/extensions/gsd/prompts/replan-slice.md +8 -0
- package/src/resources/extensions/gsd/prompts/system.md +2 -0
- package/src/resources/extensions/gsd/prompts/triage-captures.md +62 -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/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-worktree.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/captures.test.ts +438 -0
- package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -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/milestone-transition-worktree.test.ts +144 -0
- package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
- package/src/resources/extensions/gsd/tests/model-router.test.ts +167 -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/remote-questions.test.ts +227 -1
- package/src/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
- package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
- package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +198 -0
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +255 -0
- package/src/resources/extensions/gsd/triage-resolution.ts +200 -0
- package/src/resources/extensions/gsd/triage-ui.ts +175 -0
- package/src/resources/extensions/gsd/visualizer-data.ts +154 -0
- package/src/resources/extensions/gsd/visualizer-overlay.ts +193 -0
- package/src/resources/extensions/gsd/visualizer-views.ts +293 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +8 -5
- package/src/resources/extensions/gsd/worktree.ts +22 -0
- package/src/resources/extensions/remote-questions/discord-adapter.ts +33 -0
- package/src/resources/extensions/remote-questions/format.ts +12 -6
- package/src/resources/extensions/remote-questions/manager.ts +8 -0
- package/src/resources/extensions/shared/next-action-ui.ts +16 -1
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { 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 { AuthStorage } from "./auth-storage.js";
|
|
7
|
+
import { ModelDiscoveryCache } from "./discovery-cache.js";
|
|
8
|
+
import { getDefaultTTL, getDiscoverableProviders, getDiscoveryAdapter } from "./model-discovery.js";
|
|
9
|
+
|
|
10
|
+
let testDir: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
testDir = join(tmpdir(), `model-registry-discovery-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
14
|
+
mkdirSync(testDir, { recursive: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
try {
|
|
19
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
20
|
+
} catch {
|
|
21
|
+
// Cleanup best-effort
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// ─── discovery cache integration ─────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
describe("ModelDiscoveryCache — integration with discovery", () => {
|
|
28
|
+
it("cache respects provider-specific TTLs", () => {
|
|
29
|
+
const cachePath = join(testDir, "cache.json");
|
|
30
|
+
const cache = new ModelDiscoveryCache(cachePath);
|
|
31
|
+
|
|
32
|
+
cache.set("ollama", [{ id: "llama2" }]);
|
|
33
|
+
const entry = cache.get("ollama");
|
|
34
|
+
assert.ok(entry);
|
|
35
|
+
assert.equal(entry.ttlMs, getDefaultTTL("ollama"));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("cache uses custom TTL when provided", () => {
|
|
39
|
+
const cachePath = join(testDir, "cache.json");
|
|
40
|
+
const cache = new ModelDiscoveryCache(cachePath);
|
|
41
|
+
|
|
42
|
+
cache.set("openai", [{ id: "gpt-4o" }], 999);
|
|
43
|
+
const entry = cache.get("openai");
|
|
44
|
+
assert.ok(entry);
|
|
45
|
+
assert.equal(entry.ttlMs, 999);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// ─── adapter resolution ─────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
describe("Discovery adapter resolution", () => {
|
|
52
|
+
it("all discoverable providers have adapters", () => {
|
|
53
|
+
const providers = getDiscoverableProviders();
|
|
54
|
+
for (const provider of providers) {
|
|
55
|
+
const adapter = getDiscoveryAdapter(provider);
|
|
56
|
+
assert.equal(adapter.supportsDiscovery, true, `${provider} should support discovery`);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("static adapters return empty model lists", async () => {
|
|
61
|
+
const staticProviders = ["anthropic", "bedrock", "azure-openai", "groq", "cerebras"];
|
|
62
|
+
for (const provider of staticProviders) {
|
|
63
|
+
const adapter = getDiscoveryAdapter(provider);
|
|
64
|
+
assert.equal(adapter.supportsDiscovery, false, `${provider} should not support discovery`);
|
|
65
|
+
const models = await adapter.fetchModels("dummy-key");
|
|
66
|
+
assert.deepEqual(models, [], `${provider} should return empty models`);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ─── AuthStorage hasAuth for discovery ───────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
describe("AuthStorage — hasAuth for discovery providers", () => {
|
|
74
|
+
it("returns false for providers without auth", () => {
|
|
75
|
+
const storage = AuthStorage.inMemory({});
|
|
76
|
+
assert.equal(storage.hasAuth("openai"), false);
|
|
77
|
+
assert.equal(storage.hasAuth("ollama"), false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("returns true for providers with stored keys", () => {
|
|
81
|
+
const storage = AuthStorage.inMemory({
|
|
82
|
+
openai: { type: "api_key" as const, key: "sk-test" },
|
|
83
|
+
});
|
|
84
|
+
assert.equal(storage.hasAuth("openai"), true);
|
|
85
|
+
assert.equal(storage.hasAuth("ollama"), false);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ─── cache persistence across instances ──────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
describe("ModelDiscoveryCache — persistence", () => {
|
|
92
|
+
it("data survives across cache instances", () => {
|
|
93
|
+
const cachePath = join(testDir, "persist.json");
|
|
94
|
+
|
|
95
|
+
const cache1 = new ModelDiscoveryCache(cachePath);
|
|
96
|
+
cache1.set("openai", [
|
|
97
|
+
{ id: "gpt-4o", name: "GPT-4o", contextWindow: 128000 },
|
|
98
|
+
{ id: "gpt-4o-mini", name: "GPT-4o Mini" },
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
const cache2 = new ModelDiscoveryCache(cachePath);
|
|
102
|
+
const entry = cache2.get("openai");
|
|
103
|
+
assert.ok(entry);
|
|
104
|
+
assert.equal(entry.models.length, 2);
|
|
105
|
+
assert.equal(entry.models[0].contextWindow, 128000);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("clear persists across instances", () => {
|
|
109
|
+
const cachePath = join(testDir, "clear.json");
|
|
110
|
+
|
|
111
|
+
const cache1 = new ModelDiscoveryCache(cachePath);
|
|
112
|
+
cache1.set("openai", [{ id: "gpt-4o" }]);
|
|
113
|
+
cache1.clear("openai");
|
|
114
|
+
|
|
115
|
+
const cache2 = new ModelDiscoveryCache(cachePath);
|
|
116
|
+
assert.equal(cache2.get("openai"), undefined);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ─── discovery TTL values ────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
describe("Discovery TTL configuration", () => {
|
|
123
|
+
it("ollama has shortest TTL (local models change often)", () => {
|
|
124
|
+
const ollamaTTL = getDefaultTTL("ollama");
|
|
125
|
+
const openaiTTL = getDefaultTTL("openai");
|
|
126
|
+
assert.ok(ollamaTTL < openaiTTL, "ollama TTL should be shorter than openai");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("unknown providers get default TTL", () => {
|
|
130
|
+
const customTTL = getDefaultTTL("my-custom-provider");
|
|
131
|
+
const defaultTTL = getDefaultTTL("default");
|
|
132
|
+
// Unknown providers should get the same TTL as the explicit "default" key
|
|
133
|
+
assert.equal(customTTL, defaultTTL);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -24,6 +24,9 @@ import { existsSync, readFileSync } from "fs";
|
|
|
24
24
|
import { join } from "path";
|
|
25
25
|
import { getAgentDir } from "../config.js";
|
|
26
26
|
import type { AuthStorage } from "./auth-storage.js";
|
|
27
|
+
import { ModelDiscoveryCache } from "./discovery-cache.js";
|
|
28
|
+
import type { DiscoveredModel, DiscoveryResult } from "./model-discovery.js";
|
|
29
|
+
import { getDefaultTTL, getDiscoverableProviders, getDiscoveryAdapter } from "./model-discovery.js";
|
|
27
30
|
import { clearConfigValueCache, resolveConfigValue, resolveHeaders } from "./resolve-config-value.js";
|
|
28
31
|
|
|
29
32
|
const Ajv = (AjvModule as any).default || AjvModule;
|
|
@@ -221,6 +224,8 @@ export const clearApiKeyCache = clearConfigValueCache;
|
|
|
221
224
|
*/
|
|
222
225
|
export class ModelRegistry {
|
|
223
226
|
private models: Model<Api>[] = [];
|
|
227
|
+
private discoveredModels: Model<Api>[] = [];
|
|
228
|
+
private discoveryCache: ModelDiscoveryCache;
|
|
224
229
|
private customProviderApiKeys: Map<string, string> = new Map();
|
|
225
230
|
private registeredProviders: Map<string, ProviderConfigInput> = new Map();
|
|
226
231
|
private loadError: string | undefined = undefined;
|
|
@@ -229,6 +234,8 @@ export class ModelRegistry {
|
|
|
229
234
|
readonly authStorage: AuthStorage,
|
|
230
235
|
private modelsJsonPath: string | undefined = join(getAgentDir(), "models.json"),
|
|
231
236
|
) {
|
|
237
|
+
this.discoveryCache = new ModelDiscoveryCache();
|
|
238
|
+
|
|
232
239
|
// Set up fallback resolver for custom provider API keys
|
|
233
240
|
this.authStorage.setFallbackResolver((provider) => {
|
|
234
241
|
const keyConfig = this.customProviderApiKeys.get(provider);
|
|
@@ -666,6 +673,106 @@ export class ModelRegistry {
|
|
|
666
673
|
});
|
|
667
674
|
}
|
|
668
675
|
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Discover models from all providers that support discovery.
|
|
679
|
+
* Results are cached and merged into the registry (never overrides existing models).
|
|
680
|
+
*/
|
|
681
|
+
async discoverModels(providers?: string[]): Promise<DiscoveryResult[]> {
|
|
682
|
+
const targetProviders = providers ?? getDiscoverableProviders();
|
|
683
|
+
const results: DiscoveryResult[] = [];
|
|
684
|
+
|
|
685
|
+
for (const providerName of targetProviders) {
|
|
686
|
+
const adapter = getDiscoveryAdapter(providerName);
|
|
687
|
+
if (!adapter.supportsDiscovery) continue;
|
|
688
|
+
|
|
689
|
+
// Skip if cache is still fresh
|
|
690
|
+
if (!this.discoveryCache.isStale(providerName)) {
|
|
691
|
+
const cached = this.discoveryCache.get(providerName);
|
|
692
|
+
if (cached) {
|
|
693
|
+
results.push({
|
|
694
|
+
provider: providerName,
|
|
695
|
+
models: cached.models,
|
|
696
|
+
fetchedAt: cached.fetchedAt,
|
|
697
|
+
});
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
try {
|
|
703
|
+
const apiKey = await this.authStorage.getApiKey(providerName);
|
|
704
|
+
if (!apiKey && providerName !== "ollama") continue;
|
|
705
|
+
|
|
706
|
+
const models = await adapter.fetchModels(apiKey ?? "", undefined);
|
|
707
|
+
this.discoveryCache.set(providerName, models);
|
|
708
|
+
results.push({
|
|
709
|
+
provider: providerName,
|
|
710
|
+
models,
|
|
711
|
+
fetchedAt: Date.now(),
|
|
712
|
+
});
|
|
713
|
+
} catch (error) {
|
|
714
|
+
results.push({
|
|
715
|
+
provider: providerName,
|
|
716
|
+
models: [],
|
|
717
|
+
fetchedAt: Date.now(),
|
|
718
|
+
error: error instanceof Error ? error.message : String(error),
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Convert and merge discovered models
|
|
724
|
+
this.discoveredModels = this.convertDiscoveredModels(results);
|
|
725
|
+
return results;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Get all models including discovered ones.
|
|
730
|
+
* Discovered models are appended but never override existing models.
|
|
731
|
+
*/
|
|
732
|
+
getAllWithDiscovered(): Model<Api>[] {
|
|
733
|
+
const existingIds = new Set(this.models.map((m) => `${m.provider}/${m.id}`));
|
|
734
|
+
const unique = this.discoveredModels.filter((m) => !existingIds.has(`${m.provider}/${m.id}`));
|
|
735
|
+
return [...this.models, ...unique];
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Check if a model was added via discovery (not built-in or custom).
|
|
740
|
+
*/
|
|
741
|
+
isDiscovered(model: Model<Api>): boolean {
|
|
742
|
+
return this.discoveredModels.some((m) => m.provider === model.provider && m.id === model.id);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Get the discovery cache instance.
|
|
747
|
+
*/
|
|
748
|
+
getDiscoveryCache(): ModelDiscoveryCache {
|
|
749
|
+
return this.discoveryCache;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Convert DiscoveryResult[] into Model<Api>[] with default values.
|
|
754
|
+
*/
|
|
755
|
+
private convertDiscoveredModels(results: DiscoveryResult[]): Model<Api>[] {
|
|
756
|
+
const converted: Model<Api>[] = [];
|
|
757
|
+
for (const result of results) {
|
|
758
|
+
if (result.error) continue;
|
|
759
|
+
for (const dm of result.models) {
|
|
760
|
+
converted.push({
|
|
761
|
+
id: dm.id,
|
|
762
|
+
name: dm.name ?? dm.id,
|
|
763
|
+
api: "openai" as Api,
|
|
764
|
+
provider: result.provider,
|
|
765
|
+
baseUrl: "",
|
|
766
|
+
reasoning: dm.reasoning ?? false,
|
|
767
|
+
input: dm.input ?? ["text"],
|
|
768
|
+
cost: dm.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
769
|
+
contextWindow: dm.contextWindow ?? 128000,
|
|
770
|
+
maxTokens: dm.maxTokens ?? 16384,
|
|
771
|
+
} as Model<Api>);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return converted;
|
|
775
|
+
}
|
|
669
776
|
}
|
|
670
777
|
|
|
671
778
|
/**
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } 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 { ModelsJsonWriter } from "./models-json-writer.js";
|
|
7
|
+
|
|
8
|
+
let testDir: string;
|
|
9
|
+
let modelsJsonPath: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
testDir = join(tmpdir(), `models-json-writer-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
13
|
+
mkdirSync(testDir, { recursive: true });
|
|
14
|
+
modelsJsonPath = join(testDir, "models.json");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
try {
|
|
19
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
20
|
+
} catch {
|
|
21
|
+
// Cleanup best-effort
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
function readModels(): Record<string, unknown> {
|
|
26
|
+
return JSON.parse(readFileSync(modelsJsonPath, "utf-8"));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── addModel ────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
describe("ModelsJsonWriter — addModel", () => {
|
|
32
|
+
it("creates file and adds model to new provider", () => {
|
|
33
|
+
const writer = new ModelsJsonWriter(modelsJsonPath);
|
|
34
|
+
writer.addModel("openai", { id: "gpt-4o", name: "GPT-4o" }, { baseUrl: "https://api.openai.com", apiKey: "env:OPENAI_API_KEY", api: "openai" });
|
|
35
|
+
|
|
36
|
+
const config = readModels() as any;
|
|
37
|
+
assert.ok(config.providers.openai);
|
|
38
|
+
assert.equal(config.providers.openai.models.length, 1);
|
|
39
|
+
assert.equal(config.providers.openai.models[0].id, "gpt-4o");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("appends model to existing provider", () => {
|
|
43
|
+
const writer = new ModelsJsonWriter(modelsJsonPath);
|
|
44
|
+
writer.addModel("openai", { id: "gpt-4o" }, { baseUrl: "https://api.openai.com", apiKey: "env:OPENAI_API_KEY", api: "openai" });
|
|
45
|
+
writer.addModel("openai", { id: "gpt-4o-mini" });
|
|
46
|
+
|
|
47
|
+
const config = readModels() as any;
|
|
48
|
+
assert.equal(config.providers.openai.models.length, 2);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("replaces model with same id", () => {
|
|
52
|
+
const writer = new ModelsJsonWriter(modelsJsonPath);
|
|
53
|
+
writer.addModel("openai", { id: "gpt-4o", name: "Old" }, { baseUrl: "https://api.openai.com", apiKey: "env:OPENAI_API_KEY", api: "openai" });
|
|
54
|
+
writer.addModel("openai", { id: "gpt-4o", name: "New" });
|
|
55
|
+
|
|
56
|
+
const config = readModels() as any;
|
|
57
|
+
assert.equal(config.providers.openai.models.length, 1);
|
|
58
|
+
assert.equal(config.providers.openai.models[0].name, "New");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ─── removeModel ─────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
describe("ModelsJsonWriter — removeModel", () => {
|
|
65
|
+
it("removes a model from provider", () => {
|
|
66
|
+
const writer = new ModelsJsonWriter(modelsJsonPath);
|
|
67
|
+
writer.addModel("openai", { id: "gpt-4o" }, { baseUrl: "https://api.openai.com", apiKey: "env:OPENAI_API_KEY", api: "openai" });
|
|
68
|
+
writer.addModel("openai", { id: "gpt-4o-mini" });
|
|
69
|
+
|
|
70
|
+
writer.removeModel("openai", "gpt-4o");
|
|
71
|
+
|
|
72
|
+
const config = readModels() as any;
|
|
73
|
+
assert.equal(config.providers.openai.models.length, 1);
|
|
74
|
+
assert.equal(config.providers.openai.models[0].id, "gpt-4o-mini");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("removes provider when last model is removed", () => {
|
|
78
|
+
const writer = new ModelsJsonWriter(modelsJsonPath);
|
|
79
|
+
writer.addModel("openai", { id: "gpt-4o" }, { baseUrl: "https://api.openai.com", apiKey: "env:OPENAI_API_KEY", api: "openai" });
|
|
80
|
+
|
|
81
|
+
writer.removeModel("openai", "gpt-4o");
|
|
82
|
+
|
|
83
|
+
const config = readModels() as any;
|
|
84
|
+
assert.equal(config.providers.openai, undefined);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("handles removing from nonexistent provider", () => {
|
|
88
|
+
const writer = new ModelsJsonWriter(modelsJsonPath);
|
|
89
|
+
// Should not throw
|
|
90
|
+
writer.removeModel("nonexistent", "model-id");
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ─── setProvider / removeProvider ────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
describe("ModelsJsonWriter — provider operations", () => {
|
|
97
|
+
it("sets a provider configuration", () => {
|
|
98
|
+
const writer = new ModelsJsonWriter(modelsJsonPath);
|
|
99
|
+
writer.setProvider("custom", {
|
|
100
|
+
baseUrl: "http://localhost:8080",
|
|
101
|
+
apiKey: "test-key",
|
|
102
|
+
api: "openai",
|
|
103
|
+
models: [{ id: "local-model" }],
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const config = readModels() as any;
|
|
107
|
+
assert.ok(config.providers.custom);
|
|
108
|
+
assert.equal(config.providers.custom.baseUrl, "http://localhost:8080");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("removes a provider", () => {
|
|
112
|
+
const writer = new ModelsJsonWriter(modelsJsonPath);
|
|
113
|
+
writer.setProvider("custom", { baseUrl: "http://localhost:8080" });
|
|
114
|
+
writer.removeProvider("custom");
|
|
115
|
+
|
|
116
|
+
const config = readModels() as any;
|
|
117
|
+
assert.equal(config.providers.custom, undefined);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("handles removing nonexistent provider", () => {
|
|
121
|
+
const writer = new ModelsJsonWriter(modelsJsonPath);
|
|
122
|
+
writer.removeProvider("nonexistent");
|
|
123
|
+
// Should not throw
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ─── listProviders ───────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
describe("ModelsJsonWriter — listProviders", () => {
|
|
130
|
+
it("returns empty config when file does not exist", () => {
|
|
131
|
+
const writer = new ModelsJsonWriter(join(testDir, "nonexistent.json"));
|
|
132
|
+
const config = writer.listProviders();
|
|
133
|
+
assert.deepEqual(config, { providers: {} });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("returns current provider config", () => {
|
|
137
|
+
const writer = new ModelsJsonWriter(modelsJsonPath);
|
|
138
|
+
writer.setProvider("openai", { baseUrl: "https://api.openai.com" });
|
|
139
|
+
writer.setProvider("ollama", { baseUrl: "http://localhost:11434" });
|
|
140
|
+
|
|
141
|
+
const config = writer.listProviders();
|
|
142
|
+
assert.ok(config.providers.openai);
|
|
143
|
+
assert.ok(config.providers.ollama);
|
|
144
|
+
});
|
|
145
|
+
});
|