pi-free 1.0.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.
Files changed (68) hide show
  1. package/.github/workflows/update-benchmarks.yml +67 -0
  2. package/.pi/skills/pi-extension-dev/SKILL.md +155 -0
  3. package/CHANGELOG.md +59 -0
  4. package/LICENSE +21 -0
  5. package/README.md +289 -0
  6. package/config.ts +224 -0
  7. package/constants.ts +110 -0
  8. package/docs/free-tier-limits.md +213 -0
  9. package/docs/model-hopping.md +214 -0
  10. package/docs/plans/file-reorganization.md +172 -0
  11. package/docs/plans/package-json-fix.md +143 -0
  12. package/docs/provider-failover-plan.md +279 -0
  13. package/lib/json-persistence.ts +102 -0
  14. package/lib/logger.ts +94 -0
  15. package/lib/model-enhancer.ts +20 -0
  16. package/lib/types.ts +108 -0
  17. package/lib/util.ts +256 -0
  18. package/package.json +52 -0
  19. package/provider-factory.ts +221 -0
  20. package/provider-failover/errors.ts +275 -0
  21. package/provider-failover/hardcoded-benchmarks.ts +9889 -0
  22. package/provider-failover/index.ts +194 -0
  23. package/provider-helper.ts +336 -0
  24. package/providers/cline-auth.ts +473 -0
  25. package/providers/cline-models.ts +77 -0
  26. package/providers/cline.ts +257 -0
  27. package/providers/factory.ts +125 -0
  28. package/providers/fireworks.ts +49 -0
  29. package/providers/kilo-auth.ts +172 -0
  30. package/providers/kilo-models.ts +26 -0
  31. package/providers/kilo.ts +144 -0
  32. package/providers/mistral.ts +144 -0
  33. package/providers/model-fetcher.ts +138 -0
  34. package/providers/nvidia.ts +97 -0
  35. package/providers/ollama.ts +113 -0
  36. package/providers/openrouter.ts +175 -0
  37. package/providers/zen.ts +416 -0
  38. package/scripts/update-benchmarks.ts +255 -0
  39. package/tests/cline.test.ts +149 -0
  40. package/tests/errors.test.ts +139 -0
  41. package/tests/failover.test.ts +94 -0
  42. package/tests/fireworks.test.ts +148 -0
  43. package/tests/free-tier-limits.test.ts +191 -0
  44. package/tests/json-persistence.test.ts +105 -0
  45. package/tests/kilo.test.ts +186 -0
  46. package/tests/mistral.test.ts +138 -0
  47. package/tests/nvidia.test.ts +55 -0
  48. package/tests/ollama.test.ts +261 -0
  49. package/tests/openrouter.test.ts +192 -0
  50. package/tests/usage-tracking.test.ts +150 -0
  51. package/tests/util.test.ts +413 -0
  52. package/tests/zen.test.ts +180 -0
  53. package/todo.md +153 -0
  54. package/tsconfig.json +26 -0
  55. package/usage/commands.ts +17 -0
  56. package/usage/cumulative.ts +193 -0
  57. package/usage/formatters.ts +131 -0
  58. package/usage/index.ts +46 -0
  59. package/usage/limits.ts +166 -0
  60. package/usage/metrics.ts +222 -0
  61. package/usage/sessions.ts +355 -0
  62. package/usage/store.ts +99 -0
  63. package/usage/tracking.ts +329 -0
  64. package/usage/widget.ts +90 -0
  65. package/vitest.config.ts +20 -0
  66. package/widget/data.ts +113 -0
  67. package/widget/format.ts +26 -0
  68. package/widget/render.ts +117 -0
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Fireworks Provider Tests
3
+ */
4
+
5
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
6
+ import { beforeEach, describe, expect, it, vi } from "vitest";
7
+
8
+ // Mock dependencies
9
+ vi.mock("../config.ts", () => ({
10
+ FIREWORKS_API_KEY: "test-fireworks-key",
11
+ FIREWORKS_SHOW_PAID: true,
12
+ PROVIDER_FIREWORKS: "fireworks",
13
+ applyHidden: (models: unknown[]) => models,
14
+ }));
15
+
16
+ vi.mock("../constants.ts", () => ({
17
+ BASE_URL_FIREWORKS: "https://api.fireworks.ai/inference/v1",
18
+ }));
19
+
20
+ vi.mock("../provider-helper.ts", () => ({
21
+ createReRegister: vi.fn(() => vi.fn()),
22
+ setupProvider: vi.fn(),
23
+ }));
24
+
25
+ vi.mock("../lib/logger.ts", () => ({
26
+ createLogger: () => ({
27
+ warn: vi.fn(),
28
+ info: vi.fn(),
29
+ debug: vi.fn(),
30
+ error: vi.fn(),
31
+ }),
32
+ }));
33
+
34
+ import { setupProvider } from "../provider-helper.ts";
35
+
36
+ describe("Fireworks Provider", () => {
37
+ let mockPi: ExtensionAPI;
38
+ let mockRegisterProvider: ReturnType<typeof vi.fn>;
39
+
40
+ beforeEach(() => {
41
+ vi.clearAllMocks();
42
+ mockRegisterProvider = vi.fn();
43
+
44
+ mockPi = {
45
+ registerProvider: mockRegisterProvider,
46
+ registerCommand: vi.fn(),
47
+ } as unknown as ExtensionAPI;
48
+ });
49
+
50
+ describe("initialization", () => {
51
+ it("should register provider with hardcoded models", async () => {
52
+ const { default: fireworksProvider } = await import(
53
+ "../providers/fireworks.ts"
54
+ );
55
+ await fireworksProvider(mockPi);
56
+
57
+ expect(mockRegisterProvider).toHaveBeenCalledWith(
58
+ "fireworks",
59
+ expect.objectContaining({
60
+ baseUrl: "https://api.fireworks.ai/inference/v1",
61
+ apiKey: "FIREWORKS_API_KEY",
62
+ api: "openai-completions",
63
+ models: expect.any(Array),
64
+ }),
65
+ );
66
+ });
67
+
68
+ it("should set API key in environment", async () => {
69
+ delete process.env.FIREWORKS_API_KEY;
70
+
71
+ const { default: fireworksProvider } = await import(
72
+ "../providers/fireworks.ts"
73
+ );
74
+ await fireworksProvider(mockPi);
75
+
76
+ expect(process.env.FIREWORKS_API_KEY).toBe("test-fireworks-key");
77
+ });
78
+
79
+ it("should skip registration without API key", async () => {
80
+ // Mock no API key by temporarily clearing the module
81
+ const apiKeySpy = vi
82
+ .spyOn(await import("../config.ts"), "FIREWORKS_API_KEY", "get")
83
+ .mockReturnValue(undefined as any);
84
+
85
+ const { default: fireworksProvider } = await import(
86
+ "../providers/fireworks.ts"
87
+ );
88
+ await fireworksProvider(mockPi);
89
+
90
+ // Should not register provider
91
+ expect(mockRegisterProvider).not.toHaveBeenCalled();
92
+
93
+ // Restore the mock so subsequent tests have the API key
94
+ apiKeySpy.mockRestore();
95
+ });
96
+ });
97
+
98
+ describe("model configuration", () => {
99
+ it("should have hardcoded models with correct structure", async () => {
100
+ const { default: fireworksProvider } = await import(
101
+ "../providers/fireworks.ts"
102
+ );
103
+ await fireworksProvider(mockPi);
104
+
105
+ expect(mockRegisterProvider).toHaveBeenCalled();
106
+ const registerCall = mockRegisterProvider.mock.calls[0];
107
+ expect(registerCall).toBeDefined();
108
+ const models = registerCall?.[1]?.models;
109
+
110
+ expect(models).toBeInstanceOf(Array);
111
+ expect(models.length).toBeGreaterThan(0);
112
+
113
+ // Check first model has required properties
114
+ const firstModel = models[0];
115
+ expect(firstModel).toHaveProperty("id");
116
+ expect(firstModel).toHaveProperty("name");
117
+ expect(firstModel).toHaveProperty("reasoning");
118
+ expect(firstModel).toHaveProperty("input");
119
+ expect(firstModel).toHaveProperty("cost");
120
+ expect(firstModel).toHaveProperty("contextWindow");
121
+ expect(firstModel).toHaveProperty("maxTokens");
122
+
123
+ // Verify non-zero costs (paid model, not free)
124
+ expect(firstModel.cost.input).toBeGreaterThan(0);
125
+ expect(firstModel.cost.output).toBeGreaterThan(0);
126
+ });
127
+ });
128
+
129
+ describe("setupProvider integration", () => {
130
+ it("should call setupProvider with stored models", async () => {
131
+ const { default: fireworksProvider } = await import(
132
+ "../providers/fireworks.ts"
133
+ );
134
+ await fireworksProvider(mockPi);
135
+
136
+ expect(setupProvider).toHaveBeenCalledWith(
137
+ mockPi,
138
+ expect.objectContaining({
139
+ providerId: "fireworks",
140
+ }),
141
+ expect.objectContaining({
142
+ free: expect.any(Array),
143
+ all: expect.any(Array),
144
+ }),
145
+ );
146
+ });
147
+ });
148
+ });
@@ -0,0 +1,191 @@
1
+ import { beforeEach, describe, expect, it } from "vitest";
2
+ import {
3
+ FREE_TIER_LIMITS,
4
+ formatFreeTierStatus,
5
+ getFreeTierUsage,
6
+ getLimitWarning,
7
+ getModelUsage,
8
+ getProviderModelUsage,
9
+ getSessionUsage,
10
+ getTopModels,
11
+ incrementModelRequestCount,
12
+ isApproachingLimit,
13
+ resetUsageStats,
14
+ } from "../usage/limits.ts";
15
+
16
+ describe("Free Tier Limits", () => {
17
+ beforeEach(() => {
18
+ // Reset usage stats before each test
19
+ resetUsageStats();
20
+ });
21
+
22
+ describe("FREE_TIER_LIMITS", () => {
23
+ it("should have limits for kilo provider", () => {
24
+ expect(FREE_TIER_LIMITS.kilo).toBeDefined();
25
+ expect(FREE_TIER_LIMITS.kilo.requestsPerHour).toBe(200);
26
+ });
27
+
28
+ it("should have limits for openrouter provider", () => {
29
+ expect(FREE_TIER_LIMITS.openrouter).toBeDefined();
30
+ expect(FREE_TIER_LIMITS.openrouter.requestsPerDay).toBe(1000);
31
+ });
32
+
33
+ it("should have limits for nvidia provider", () => {
34
+ expect(FREE_TIER_LIMITS.nvidia).toBeDefined();
35
+ expect(FREE_TIER_LIMITS.nvidia.requestsPerMonth).toBe(1000);
36
+ });
37
+
38
+ it("should have limits for fireworks provider", () => {
39
+ expect(FREE_TIER_LIMITS.fireworks).toBeDefined();
40
+ expect(FREE_TIER_LIMITS.fireworks.requestsPerMonth).toBe(1000);
41
+ });
42
+ });
43
+
44
+ describe("Request Counting", () => {
45
+ it("should increment request count", () => {
46
+ // Just verify it doesn't throw
47
+ expect(() => {
48
+ incrementModelRequestCount("kilo", "gpt-4", 100, 50);
49
+ }).not.toThrow();
50
+ });
51
+
52
+ it("should track model usage", () => {
53
+ incrementModelRequestCount("test", "model-1", 100, 50);
54
+ incrementModelRequestCount("test", "model-1", 200, 100);
55
+
56
+ const usage = getModelUsage("test", "model-1");
57
+ expect(usage).toBeDefined();
58
+ expect(usage?.count).toBe(2);
59
+ expect(usage?.tokensIn).toBe(300);
60
+ expect(usage?.tokensOut).toBe(150);
61
+ });
62
+
63
+ it("should return undefined for unknown models", () => {
64
+ const usage = getModelUsage("unknown", "unknown-model");
65
+ expect(usage).toBeUndefined();
66
+ });
67
+
68
+ it("should track different models separately", () => {
69
+ incrementModelRequestCount("test", "model-a", 100, 50);
70
+ incrementModelRequestCount("test", "model-b", 200, 100);
71
+
72
+ expect(getModelUsage("test", "model-a")?.count).toBe(1);
73
+ expect(getModelUsage("test", "model-b")?.count).toBe(1);
74
+ });
75
+ });
76
+
77
+ describe("Provider Model Usage", () => {
78
+ it("should return all models for a provider", () => {
79
+ incrementModelRequestCount("test-provider", "model-1", 100, 50);
80
+ incrementModelRequestCount("test-provider", "model-2", 200, 100);
81
+
82
+ const models = getProviderModelUsage("test-provider");
83
+ expect(models).toHaveLength(2);
84
+ expect(models.map((m) => m.modelId)).toContain("model-1");
85
+ expect(models.map((m) => m.modelId)).toContain("model-2");
86
+ });
87
+
88
+ it("should return empty array for unknown provider", () => {
89
+ const models = getProviderModelUsage("nonexistent");
90
+ expect(models).toEqual([]);
91
+ });
92
+ });
93
+
94
+ describe("Top Models", () => {
95
+ it("should return top N models by request count", () => {
96
+ // Add many models with varying request counts
97
+ for (let i = 0; i < 5; i++) {
98
+ incrementModelRequestCount("test", "popular-model", 100, 50);
99
+ }
100
+ incrementModelRequestCount("test", "unpopular-model", 100, 50);
101
+
102
+ const top = getTopModels(2);
103
+ expect(top).toHaveLength(2);
104
+ expect(top[0].modelId).toBe("popular-model");
105
+ });
106
+ });
107
+
108
+ describe("Free Tier Usage", () => {
109
+ it("should calculate usage for kilo provider", () => {
110
+ // Simulate requests (out of 200/hour limit)
111
+ for (let i = 0; i < 100; i++) {
112
+ incrementModelRequestCount("kilo", "gpt-4", 10, 10);
113
+ }
114
+
115
+ const usage = getFreeTierUsage("kilo");
116
+ // requestsThisHour is capped at 50 as a rough estimate
117
+ expect(usage.requestsToday).toBeGreaterThanOrEqual(100);
118
+ expect(usage.requestsThisHour).toBeLessThanOrEqual(50);
119
+ });
120
+
121
+ it("should calculate usage for providers without hourly limits", () => {
122
+ // OpenRouter has daily limit, not hourly
123
+ const usage = getFreeTierUsage("openrouter");
124
+ expect(usage.requestsThisHour).toBe(0);
125
+ });
126
+ });
127
+
128
+ describe("Limit Warnings", () => {
129
+ it("should detect when approaching limit", () => {
130
+ // Use openrouter with daily limit (1000/day)
131
+ // Add 750 requests = 75% which triggers warning
132
+ for (let i = 0; i < 750; i++) {
133
+ incrementModelRequestCount("openrouter", "gpt-4", 10, 10);
134
+ }
135
+
136
+ expect(isApproachingLimit("openrouter")).toBe(true);
137
+ });
138
+
139
+ it("should not trigger warning when usage is low", () => {
140
+ incrementModelRequestCount("kilo", "gpt-4", 10, 10);
141
+ expect(isApproachingLimit("kilo")).toBe(false);
142
+ });
143
+
144
+ it("should return warning message when approaching limit", () => {
145
+ // Use openrouter with daily limit (1000/day)
146
+ // Add 750 requests = 75% which triggers warning
147
+ for (let i = 0; i < 750; i++) {
148
+ incrementModelRequestCount("openrouter", "gpt-4", 10, 10);
149
+ }
150
+
151
+ const warning = getLimitWarning("openrouter");
152
+ expect(warning).not.toBeNull();
153
+ expect(warning).toContain("%");
154
+ });
155
+
156
+ it("should return null when not approaching limit", () => {
157
+ const warning = getLimitWarning("kilo");
158
+ expect(warning).toBeNull();
159
+ });
160
+ });
161
+
162
+ describe("Session Usage", () => {
163
+ it("should generate session report", () => {
164
+ incrementModelRequestCount("kilo", "gpt-4", 1000, 500);
165
+ incrementModelRequestCount("openrouter", "mimo", 500, 250);
166
+
167
+ const report = getSessionUsage();
168
+ expect(report.totalRequests).toBe(2);
169
+ expect(report.totalTokensIn).toBe(1500);
170
+ expect(report.totalTokensOut).toBe(750);
171
+ });
172
+
173
+ it("should track providers in session report", () => {
174
+ incrementModelRequestCount("kilo", "gpt-4", 100, 50);
175
+ incrementModelRequestCount("openrouter", "mimo", 100, 50);
176
+
177
+ const report = getSessionUsage();
178
+ const providerNames = report.providers.map((p) => p.name);
179
+ expect(providerNames).toContain("kilo");
180
+ expect(providerNames).toContain("openrouter");
181
+ });
182
+ });
183
+
184
+ describe("Status Formatting", () => {
185
+ it("should format status for provider", () => {
186
+ const status = formatFreeTierStatus("kilo");
187
+ expect(typeof status).toBe("string");
188
+ expect(status).toContain("kilo");
189
+ });
190
+ });
191
+ });
@@ -0,0 +1,105 @@
1
+ /**
2
+ * JSON Persistence Tests
3
+ */
4
+
5
+ import { existsSync, rmdirSync, unlinkSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
8
+ import { createJSONLStore, createJSONStore } from "../lib/json-persistence.ts";
9
+
10
+ const TEST_DIR = join(
11
+ process.env.HOME || process.env.USERPROFILE || "",
12
+ ".pi-test",
13
+ );
14
+
15
+ describe("JSON Persistence", () => {
16
+ describe("createJSONStore", () => {
17
+ const testFile = join(TEST_DIR, "test-store.json");
18
+
19
+ beforeEach(() => {
20
+ // Clean up
21
+ try {
22
+ if (existsSync(testFile)) unlinkSync(testFile);
23
+ } catch {}
24
+ });
25
+
26
+ afterEach(() => {
27
+ try {
28
+ if (existsSync(testFile)) unlinkSync(testFile);
29
+ if (existsSync(TEST_DIR)) rmdirSync(TEST_DIR);
30
+ } catch {}
31
+ });
32
+
33
+ it("should return default value when file doesn't exist", () => {
34
+ const store = createJSONStore(testFile, { default: true });
35
+ const data = store.load();
36
+ expect(data).toEqual({ default: true });
37
+ });
38
+
39
+ it("should persist and load data", () => {
40
+ const store = createJSONStore(testFile, { count: 0 });
41
+ store.save({ count: 42 });
42
+
43
+ // Create new store instance to test loading from disk
44
+ const store2 = createJSONStore(testFile, { count: 0 });
45
+ const data = store2.load();
46
+ expect(data.count).toBe(42);
47
+ });
48
+
49
+ it("should cache data after first load", () => {
50
+ const store = createJSONStore(testFile, { value: "initial" });
51
+ store.save({ value: "updated" });
52
+
53
+ // First load reads from disk
54
+ const data1 = store.load();
55
+ expect(data1.value).toBe("updated");
56
+
57
+ // Second load should return cached value
58
+ const data2 = store.load();
59
+ expect(data2.value).toBe("updated");
60
+ });
61
+ });
62
+
63
+ describe("createJSONLStore", () => {
64
+ const testFile = join(TEST_DIR, "test-log.jsonl");
65
+
66
+ beforeEach(() => {
67
+ try {
68
+ if (existsSync(testFile)) unlinkSync(testFile);
69
+ } catch {}
70
+ });
71
+
72
+ afterEach(() => {
73
+ try {
74
+ if (existsSync(testFile)) unlinkSync(testFile);
75
+ if (existsSync(TEST_DIR)) rmdirSync(TEST_DIR);
76
+ } catch {}
77
+ });
78
+
79
+ it("should return empty array when file doesn't exist", () => {
80
+ const store = createJSONLStore<{ msg: string }>(testFile);
81
+ const data = store.load();
82
+ expect(data).toEqual([]);
83
+ });
84
+
85
+ it("should append entries", () => {
86
+ const store = createJSONLStore<{ event: string }>(testFile);
87
+ store.append({ event: "first" });
88
+ store.append({ event: "second" });
89
+
90
+ const data = store.load();
91
+ expect(data).toHaveLength(2);
92
+ expect(data[0].event).toBe("first");
93
+ expect(data[1].event).toBe("second");
94
+ });
95
+
96
+ it("should clear entries", () => {
97
+ const store = createJSONLStore<{ event: string }>(testFile);
98
+ store.append({ event: "test" });
99
+ store.clear();
100
+
101
+ const data = store.load();
102
+ expect(data).toEqual([]);
103
+ });
104
+ });
105
+ });
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Kilo Provider Tests
3
+ */
4
+
5
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
6
+ import { beforeEach, describe, expect, it, vi } from "vitest";
7
+
8
+ // Create mock functions first
9
+ const mockFetchKiloModels = vi.fn();
10
+ const mockSetupProvider = vi.fn();
11
+ const mockLoginKilo = vi.fn();
12
+
13
+ // Mock dependencies before importing the provider
14
+ vi.mock("../kilo-auth.ts", () => ({
15
+ loginKilo: (...args: unknown[]) => mockLoginKilo(...args),
16
+ refreshKiloToken: vi.fn(),
17
+ }));
18
+
19
+ vi.mock("../kilo-models.ts", () => ({
20
+ fetchKiloModels: (...args: unknown[]) => mockFetchKiloModels(...args),
21
+ KILO_GATEWAY_BASE: "https://api.kilo.ai/api/gateway",
22
+ }));
23
+
24
+ vi.mock("../provider-helper.ts", () => ({
25
+ createReRegister: vi.fn(() => vi.fn()),
26
+ setupProvider: (...args: unknown[]) => mockSetupProvider(...args),
27
+ enhanceWithCI: (models: unknown[]) => models,
28
+ }));
29
+
30
+ vi.mock("../usage-widget.ts", () => ({
31
+ registerUsageWidget: vi.fn(),
32
+ }));
33
+
34
+ vi.mock("../util.ts", () => ({
35
+ logWarning: vi.fn(),
36
+ }));
37
+
38
+ import kiloProvider from "../providers/kilo.ts";
39
+
40
+ describe("Kilo Provider", () => {
41
+ let _mockPi: ExtensionAPI;
42
+ let mockRegisterProvider: ReturnType<typeof vi.fn>;
43
+ let mockOn: ReturnType<typeof vi.fn>;
44
+
45
+ beforeEach(() => {
46
+ vi.clearAllMocks();
47
+ mockFetchKiloModels.mockReset();
48
+ mockSetupProvider.mockReset();
49
+ mockLoginKilo.mockReset();
50
+
51
+ mockRegisterProvider = vi.fn();
52
+ mockOn = vi.fn();
53
+
54
+ _mockPi = {
55
+ registerProvider: mockRegisterProvider,
56
+ on: mockOn,
57
+ registerCommand: vi.fn(),
58
+ } as unknown as ExtensionAPI;
59
+ });
60
+
61
+ describe("Kilo Provider", () => {
62
+ let mockPi: ExtensionAPI;
63
+ let mockRegisterProvider: ReturnType<typeof vi.fn>;
64
+ let mockOn: ReturnType<typeof vi.fn>;
65
+
66
+ beforeEach(() => {
67
+ vi.clearAllMocks();
68
+ mockRegisterProvider = vi.fn();
69
+ mockOn = vi.fn();
70
+
71
+ mockPi = {
72
+ registerProvider: mockRegisterProvider,
73
+ on: mockOn,
74
+ registerCommand: vi.fn(),
75
+ } as unknown as ExtensionAPI;
76
+ });
77
+
78
+ describe("initialization", () => {
79
+ it("should register provider with free models on startup", async () => {
80
+ const mockModels = [
81
+ {
82
+ id: "gpt-4",
83
+ name: "GPT-4",
84
+ reasoning: true,
85
+ input: ["text"],
86
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
87
+ contextWindow: 128000,
88
+ maxTokens: 4096,
89
+ },
90
+ ];
91
+ vi.mocked(fetchKiloModels).mockResolvedValue(mockModels);
92
+
93
+ await kiloProvider(mockPi);
94
+
95
+ expect(mockRegisterProvider).toHaveBeenCalledWith(
96
+ "kilo",
97
+ expect.objectContaining({
98
+ baseUrl: "https://api.kilo.ai/api/gateway",
99
+ apiKey: "KILO_API_KEY",
100
+ api: "openai-completions",
101
+ models: mockModels,
102
+ oauth: expect.any(Object),
103
+ }),
104
+ );
105
+ });
106
+
107
+ it("should handle model fetch failure gracefully", async () => {
108
+ vi.mocked(fetchKiloModels).mockRejectedValue(new Error("Network error"));
109
+
110
+ await kiloProvider(mockPi);
111
+
112
+ // Should still register with empty models
113
+ expect(mockRegisterProvider).toHaveBeenCalled();
114
+ });
115
+ });
116
+
117
+ describe("OAuth integration", () => {
118
+ it("should have oauth configuration", async () => {
119
+ vi.mocked(fetchKiloModels).mockResolvedValue([]);
120
+
121
+ await kiloProvider(mockPi);
122
+
123
+ const registerCall = mockRegisterProvider.mock.calls[0];
124
+ expect(registerCall[1]).toHaveProperty("oauth");
125
+ expect(registerCall[1].oauth).toHaveProperty("name", "Kilo");
126
+ expect(registerCall[1].oauth).toHaveProperty("login");
127
+ expect(registerCall[1].oauth).toHaveProperty("refreshToken");
128
+ expect(registerCall[1].oauth).toHaveProperty("getApiKey");
129
+ expect(registerCall[1].oauth).toHaveProperty("modifyModels");
130
+ });
131
+
132
+ it("should fetch all models after login", async () => {
133
+ vi.mocked(fetchKiloModels).mockResolvedValue([]);
134
+
135
+ await kiloProvider(mockPi);
136
+
137
+ const oauth = mockRegisterProvider.mock.calls[0][1].oauth;
138
+ const _mockCreds = {
139
+ access: "test-token",
140
+ refresh: "",
141
+ expires: Date.now() + 3600000,
142
+ };
143
+
144
+ vi.mocked(fetchKiloModels).mockResolvedValue([
145
+ { id: "gpt-4", name: "GPT-4" },
146
+ { id: "claude-3", name: "Claude 3" },
147
+ ]);
148
+
149
+ await oauth.login({ onProgress: vi.fn() });
150
+
151
+ expect(fetchKiloModels).toHaveBeenCalledWith({ token: "test-token" });
152
+ });
153
+ });
154
+
155
+ describe("event handlers", () => {
156
+ it("should register session_start handler", async () => {
157
+ vi.mocked(fetchKiloModels).mockResolvedValue([]);
158
+
159
+ await kiloProvider(mockPi);
160
+
161
+ expect(mockOn).toHaveBeenCalledWith(
162
+ "session_start",
163
+ expect.any(Function),
164
+ );
165
+ });
166
+ });
167
+
168
+ describe("setupProvider integration", () => {
169
+ it("should call setupProvider with correct config", async () => {
170
+ vi.mocked(fetchKiloModels).mockResolvedValue([]);
171
+
172
+ await kiloProvider(mockPi);
173
+
174
+ expect(setupProvider).toHaveBeenCalledWith(
175
+ mockPi,
176
+ expect.objectContaining({
177
+ providerId: "kilo",
178
+ tosUrl: expect.stringContaining("kilo.ai"),
179
+ reRegister: expect.any(Function),
180
+ }),
181
+ expect.objectContaining({ free: [], all: [] }),
182
+ );
183
+ });
184
+ });
185
+ });
186
+ });