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.
- package/.github/workflows/update-benchmarks.yml +67 -0
- package/.pi/skills/pi-extension-dev/SKILL.md +155 -0
- package/CHANGELOG.md +59 -0
- package/LICENSE +21 -0
- package/README.md +289 -0
- package/config.ts +224 -0
- package/constants.ts +110 -0
- package/docs/free-tier-limits.md +213 -0
- package/docs/model-hopping.md +214 -0
- package/docs/plans/file-reorganization.md +172 -0
- package/docs/plans/package-json-fix.md +143 -0
- package/docs/provider-failover-plan.md +279 -0
- package/lib/json-persistence.ts +102 -0
- package/lib/logger.ts +94 -0
- package/lib/model-enhancer.ts +20 -0
- package/lib/types.ts +108 -0
- package/lib/util.ts +256 -0
- package/package.json +52 -0
- package/provider-factory.ts +221 -0
- package/provider-failover/errors.ts +275 -0
- package/provider-failover/hardcoded-benchmarks.ts +9889 -0
- package/provider-failover/index.ts +194 -0
- package/provider-helper.ts +336 -0
- package/providers/cline-auth.ts +473 -0
- package/providers/cline-models.ts +77 -0
- package/providers/cline.ts +257 -0
- package/providers/factory.ts +125 -0
- package/providers/fireworks.ts +49 -0
- package/providers/kilo-auth.ts +172 -0
- package/providers/kilo-models.ts +26 -0
- package/providers/kilo.ts +144 -0
- package/providers/mistral.ts +144 -0
- package/providers/model-fetcher.ts +138 -0
- package/providers/nvidia.ts +97 -0
- package/providers/ollama.ts +113 -0
- package/providers/openrouter.ts +175 -0
- package/providers/zen.ts +416 -0
- package/scripts/update-benchmarks.ts +255 -0
- package/tests/cline.test.ts +149 -0
- package/tests/errors.test.ts +139 -0
- package/tests/failover.test.ts +94 -0
- package/tests/fireworks.test.ts +148 -0
- package/tests/free-tier-limits.test.ts +191 -0
- package/tests/json-persistence.test.ts +105 -0
- package/tests/kilo.test.ts +186 -0
- package/tests/mistral.test.ts +138 -0
- package/tests/nvidia.test.ts +55 -0
- package/tests/ollama.test.ts +261 -0
- package/tests/openrouter.test.ts +192 -0
- package/tests/usage-tracking.test.ts +150 -0
- package/tests/util.test.ts +413 -0
- package/tests/zen.test.ts +180 -0
- package/todo.md +153 -0
- package/tsconfig.json +26 -0
- package/usage/commands.ts +17 -0
- package/usage/cumulative.ts +193 -0
- package/usage/formatters.ts +131 -0
- package/usage/index.ts +46 -0
- package/usage/limits.ts +166 -0
- package/usage/metrics.ts +222 -0
- package/usage/sessions.ts +355 -0
- package/usage/store.ts +99 -0
- package/usage/tracking.ts +329 -0
- package/usage/widget.ts +90 -0
- package/vitest.config.ts +20 -0
- package/widget/data.ts +113 -0
- package/widget/format.ts +26 -0
- package/widget/render.ts +117 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mistral Provider Tests
|
|
3
|
+
*/
|
|
4
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
let capturedConfig: any = null;
|
|
8
|
+
let capturedHook: Function | null = null;
|
|
9
|
+
|
|
10
|
+
vi.mock("../config.ts", () => ({
|
|
11
|
+
MISTRAL_API_KEY: "test-mistral-key",
|
|
12
|
+
MISTRAL_SHOW_PAID: false,
|
|
13
|
+
PROVIDER_MISTRAL: "mistral",
|
|
14
|
+
applyHidden: (models: any[]) => models,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock("../constants.ts", () => ({
|
|
18
|
+
BASE_URL_MISTRAL: "https://api.mistral.ai/v1",
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock("../provider-helper.ts", () => ({
|
|
22
|
+
createReRegister: vi.fn(() => vi.fn()),
|
|
23
|
+
setupProvider: vi.fn(),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
vi.mock("../provider-factory.ts", () => ({
|
|
27
|
+
createProvider: vi.fn(async (pi: any, def: any) => {
|
|
28
|
+
capturedConfig = def;
|
|
29
|
+
// Capture the beforeProviderRequest hook if provided
|
|
30
|
+
if (def.beforeProviderRequest) {
|
|
31
|
+
capturedHook = def.beforeProviderRequest;
|
|
32
|
+
// Also register it via pi.on
|
|
33
|
+
(pi.on as Function)("before_provider_request", (event: any) => {
|
|
34
|
+
const payload = event.payload as Record<string, unknown>;
|
|
35
|
+
return def.beforeProviderRequest(payload);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
vi.mock("../lib/logger.ts", () => ({
|
|
42
|
+
createLogger: () => ({
|
|
43
|
+
warn: vi.fn(),
|
|
44
|
+
info: vi.fn(),
|
|
45
|
+
debug: vi.fn(),
|
|
46
|
+
error: vi.fn(),
|
|
47
|
+
}),
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
import mistralProvider from "../providers/mistral.ts";
|
|
51
|
+
|
|
52
|
+
describe("Mistral Provider", () => {
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
vi.clearAllMocks();
|
|
55
|
+
capturedConfig = null;
|
|
56
|
+
capturedHook = null;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("initialization", () => {
|
|
60
|
+
it("should configure factory correctly", async () => {
|
|
61
|
+
const mockPi = {
|
|
62
|
+
registerProvider: vi.fn(),
|
|
63
|
+
on: vi.fn(),
|
|
64
|
+
} as unknown as ExtensionAPI;
|
|
65
|
+
|
|
66
|
+
await mistralProvider(mockPi);
|
|
67
|
+
|
|
68
|
+
expect(capturedConfig).toMatchObject({
|
|
69
|
+
providerId: "mistral",
|
|
70
|
+
baseUrl: "https://api.mistral.ai/v1",
|
|
71
|
+
apiKeyEnvVar: "MISTRAL_API_KEY",
|
|
72
|
+
apiKeyConfigKey: "mistral_api_key",
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should include beforeProviderRequest hook", async () => {
|
|
77
|
+
const mockPi = {
|
|
78
|
+
registerProvider: vi.fn(),
|
|
79
|
+
on: vi.fn(),
|
|
80
|
+
} as unknown as ExtensionAPI;
|
|
81
|
+
|
|
82
|
+
await mistralProvider(mockPi);
|
|
83
|
+
|
|
84
|
+
expect(capturedConfig.beforeProviderRequest).toBeDefined();
|
|
85
|
+
expect(typeof capturedConfig.beforeProviderRequest).toBe("function");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("payload filtering", () => {
|
|
90
|
+
it("should filter to only allowed fields for Mistral requests", async () => {
|
|
91
|
+
const mockPi = {
|
|
92
|
+
registerProvider: vi.fn(),
|
|
93
|
+
on: vi.fn(),
|
|
94
|
+
} as unknown as ExtensionAPI;
|
|
95
|
+
|
|
96
|
+
await mistralProvider(mockPi);
|
|
97
|
+
|
|
98
|
+
const mistralPayload = {
|
|
99
|
+
model: "mistral-large-latest",
|
|
100
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
101
|
+
temperature: 0.7,
|
|
102
|
+
max_tokens: 100,
|
|
103
|
+
unsupported_field: "should be removed",
|
|
104
|
+
another_bad_field: 123,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const result = capturedHook?.(mistralPayload);
|
|
108
|
+
|
|
109
|
+
expect(result).toEqual({
|
|
110
|
+
model: "mistral-large-latest",
|
|
111
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
112
|
+
temperature: 0.7,
|
|
113
|
+
max_tokens: 100,
|
|
114
|
+
});
|
|
115
|
+
expect(result.unsupported_field).toBeUndefined();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should not filter requests from other providers", async () => {
|
|
119
|
+
const mockPi = {
|
|
120
|
+
registerProvider: vi.fn(),
|
|
121
|
+
on: vi.fn(),
|
|
122
|
+
} as unknown as ExtensionAPI;
|
|
123
|
+
|
|
124
|
+
await mistralProvider(mockPi);
|
|
125
|
+
|
|
126
|
+
const otherPayload = {
|
|
127
|
+
model: "gpt-4",
|
|
128
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
129
|
+
temperature: 0.7,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const result = capturedHook?.(otherPayload);
|
|
133
|
+
|
|
134
|
+
// Should return undefined to let payload through unchanged
|
|
135
|
+
expect(result).toBeUndefined();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NVIDIA Provider Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
|
|
8
|
+
let capturedConfig: any = null;
|
|
9
|
+
|
|
10
|
+
vi.mock("../provider-factory.ts", () => ({
|
|
11
|
+
createProvider: vi.fn(async (_pi: any, def: any) => {
|
|
12
|
+
capturedConfig = def;
|
|
13
|
+
// Don't call fetchModels - just capture config
|
|
14
|
+
return;
|
|
15
|
+
}),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
// Minimal mocks for imports
|
|
19
|
+
vi.mock("../config.ts", () => ({
|
|
20
|
+
NVIDIA_API_KEY: "test-key",
|
|
21
|
+
NVIDIA_SHOW_PAID: true,
|
|
22
|
+
PROVIDER_NVIDIA: "nvidia",
|
|
23
|
+
applyHidden: (m: any[]) => m,
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
vi.mock("../constants.ts", () => ({
|
|
27
|
+
BASE_URL_NVIDIA: "https://integrate.api.nvidia.com/v1",
|
|
28
|
+
DEFAULT_FETCH_TIMEOUT_MS: 10000,
|
|
29
|
+
NVIDIA_MIN_SIZE_B: 70,
|
|
30
|
+
URL_MODELS_DEV: "https://models.dev/api.json",
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
import nvidiaProvider from "../providers/nvidia.ts";
|
|
34
|
+
|
|
35
|
+
describe("NVIDIA Provider", () => {
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
vi.clearAllMocks();
|
|
38
|
+
capturedConfig = null;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should configure factory correctly", async () => {
|
|
42
|
+
const mockPi = {} as ExtensionAPI;
|
|
43
|
+
await nvidiaProvider(mockPi);
|
|
44
|
+
|
|
45
|
+
expect(capturedConfig).toMatchObject({
|
|
46
|
+
providerId: "nvidia",
|
|
47
|
+
baseUrl: "https://integrate.api.nvidia.com/v1",
|
|
48
|
+
apiKeyEnvVar: "NVIDIA_API_KEY",
|
|
49
|
+
apiKeyConfigKey: "nvidia_api_key",
|
|
50
|
+
});
|
|
51
|
+
// Should NOT have showPaidFlag (NVIDIA filters internally)
|
|
52
|
+
expect(capturedConfig.showPaidFlag).toBeUndefined();
|
|
53
|
+
expect(typeof capturedConfig.fetchModels).toBe("function");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ollama 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
|
+
OLLAMA_API_KEY: "test-ollama-key",
|
|
11
|
+
OLLAMA_SHOW_PAID: true,
|
|
12
|
+
PROVIDER_OLLAMA: "ollama",
|
|
13
|
+
applyHidden: (models: unknown[]) => models,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock("../constants.ts", () => ({
|
|
17
|
+
BASE_URL_OLLAMA: "https://ollama.com/v1",
|
|
18
|
+
DEFAULT_FETCH_TIMEOUT_MS: 10000,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock("../provider-helper.ts", () => ({
|
|
22
|
+
createReRegister: vi.fn(() => vi.fn()),
|
|
23
|
+
setupProvider: vi.fn(),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
vi.mock("../lib/logger.ts", () => ({
|
|
27
|
+
createLogger: () => ({
|
|
28
|
+
warn: vi.fn(),
|
|
29
|
+
info: vi.fn(),
|
|
30
|
+
debug: vi.fn(),
|
|
31
|
+
error: vi.fn(),
|
|
32
|
+
}),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
vi.mock("../util.ts", () => ({
|
|
36
|
+
fetchWithRetry: vi.fn(),
|
|
37
|
+
logWarning: vi.fn(),
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
import { fetchWithRetry } from "../lib/util.ts";
|
|
41
|
+
import { setupProvider } from "../provider-helper.ts";
|
|
42
|
+
|
|
43
|
+
describe("Ollama Provider", () => {
|
|
44
|
+
let mockPi: ExtensionAPI;
|
|
45
|
+
let mockRegisterProvider: ReturnType<typeof vi.fn>;
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
vi.clearAllMocks();
|
|
49
|
+
mockRegisterProvider = vi.fn();
|
|
50
|
+
|
|
51
|
+
mockPi = {
|
|
52
|
+
registerProvider: mockRegisterProvider,
|
|
53
|
+
registerCommand: vi.fn(),
|
|
54
|
+
} as unknown as ExtensionAPI;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("initialization", () => {
|
|
58
|
+
it("should register provider with cloud models", async () => {
|
|
59
|
+
const mockModels = {
|
|
60
|
+
data: [
|
|
61
|
+
{
|
|
62
|
+
id: "gpt-oss:120b",
|
|
63
|
+
object: "model",
|
|
64
|
+
created: 1754352000,
|
|
65
|
+
owned_by: "ollama",
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
vi.mocked(fetchWithRetry).mockResolvedValue({
|
|
70
|
+
ok: true,
|
|
71
|
+
json: async () => mockModels,
|
|
72
|
+
} as Response);
|
|
73
|
+
|
|
74
|
+
const { default: ollamaProvider } = await import(
|
|
75
|
+
"../providers/ollama.ts"
|
|
76
|
+
);
|
|
77
|
+
await ollamaProvider(mockPi);
|
|
78
|
+
|
|
79
|
+
expect(mockRegisterProvider).toHaveBeenCalledWith(
|
|
80
|
+
"ollama",
|
|
81
|
+
expect.objectContaining({
|
|
82
|
+
baseUrl: "https://ollama.com/v1",
|
|
83
|
+
apiKey: "OLLAMA_API_KEY",
|
|
84
|
+
api: "openai-completions",
|
|
85
|
+
models: expect.any(Array),
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should set API key in environment", async () => {
|
|
91
|
+
delete process.env.OLLAMA_API_KEY;
|
|
92
|
+
|
|
93
|
+
const { default: ollamaProvider } = await import(
|
|
94
|
+
"../providers/ollama.ts"
|
|
95
|
+
);
|
|
96
|
+
await ollamaProvider(mockPi);
|
|
97
|
+
|
|
98
|
+
expect(process.env.OLLAMA_API_KEY).toBe("test-ollama-key");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should skip registration without API key", async () => {
|
|
102
|
+
const apiKeySpy = vi
|
|
103
|
+
.spyOn(await import("../config.ts"), "OLLAMA_API_KEY", "get")
|
|
104
|
+
.mockReturnValue(undefined as any);
|
|
105
|
+
|
|
106
|
+
const { default: ollamaProvider } = await import(
|
|
107
|
+
"../providers/ollama.ts"
|
|
108
|
+
);
|
|
109
|
+
await ollamaProvider(mockPi);
|
|
110
|
+
|
|
111
|
+
expect(mockRegisterProvider).not.toHaveBeenCalled();
|
|
112
|
+
apiKeySpy.mockRestore();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("should skip registration when SHOW_PAID is false", async () => {
|
|
116
|
+
const showPaidSpy = vi
|
|
117
|
+
.spyOn(await import("../config.ts"), "OLLAMA_SHOW_PAID", "get")
|
|
118
|
+
.mockReturnValue(false);
|
|
119
|
+
|
|
120
|
+
const { default: ollamaProvider } = await import(
|
|
121
|
+
"../providers/ollama.ts"
|
|
122
|
+
);
|
|
123
|
+
await ollamaProvider(mockPi);
|
|
124
|
+
|
|
125
|
+
expect(mockRegisterProvider).not.toHaveBeenCalled();
|
|
126
|
+
showPaidSpy.mockRestore();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("model fetching", () => {
|
|
131
|
+
it("should filter out small models (< 30B)", async () => {
|
|
132
|
+
const mockModels = {
|
|
133
|
+
data: [
|
|
134
|
+
{
|
|
135
|
+
id: "gpt-oss:120b",
|
|
136
|
+
object: "model",
|
|
137
|
+
created: 1754352000,
|
|
138
|
+
owned_by: "ollama",
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: "llama3.2:1b", // Should be filtered out (too small)
|
|
142
|
+
object: "model",
|
|
143
|
+
created: 1754352000,
|
|
144
|
+
owned_by: "ollama",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: "qwen3-coder:8b", // Should be kept (8b >= 3b threshold in code)
|
|
148
|
+
object: "model",
|
|
149
|
+
created: 1754352000,
|
|
150
|
+
owned_by: "ollama",
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
};
|
|
154
|
+
vi.mocked(fetchWithRetry).mockResolvedValue({
|
|
155
|
+
ok: true,
|
|
156
|
+
json: async () => mockModels,
|
|
157
|
+
} as Response);
|
|
158
|
+
|
|
159
|
+
const { default: ollamaProvider } = await import(
|
|
160
|
+
"../providers/ollama.ts"
|
|
161
|
+
);
|
|
162
|
+
await ollamaProvider(mockPi);
|
|
163
|
+
|
|
164
|
+
const registerCall = mockRegisterProvider.mock.calls[0];
|
|
165
|
+
const models = registerCall?.[1]?.models;
|
|
166
|
+
|
|
167
|
+
// Should filter out small models (< 30B), keep only 120b
|
|
168
|
+
expect(models).toHaveLength(1);
|
|
169
|
+
expect(models[0].id).toBe("gpt-oss:120b");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("should clean up model names", async () => {
|
|
173
|
+
const mockModels = {
|
|
174
|
+
data: [
|
|
175
|
+
{
|
|
176
|
+
id: "gpt-oss:120b",
|
|
177
|
+
object: "model",
|
|
178
|
+
created: 1754352000,
|
|
179
|
+
owned_by: "ollama",
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
};
|
|
183
|
+
vi.mocked(fetchWithRetry).mockResolvedValue({
|
|
184
|
+
ok: true,
|
|
185
|
+
json: async () => mockModels,
|
|
186
|
+
} as Response);
|
|
187
|
+
|
|
188
|
+
const { default: ollamaProvider } = await import(
|
|
189
|
+
"../providers/ollama.ts"
|
|
190
|
+
);
|
|
191
|
+
await ollamaProvider(mockPi);
|
|
192
|
+
|
|
193
|
+
const registerCall = mockRegisterProvider.mock.calls[0];
|
|
194
|
+
const models = registerCall?.[1]?.models;
|
|
195
|
+
|
|
196
|
+
expect(models[0].name).toBe("Gpt Oss 120b"); // Cleaned up name
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("should detect reasoning models", async () => {
|
|
200
|
+
const mockModels = {
|
|
201
|
+
data: [
|
|
202
|
+
{
|
|
203
|
+
id: "deepseek-r1:70b",
|
|
204
|
+
object: "model",
|
|
205
|
+
created: 1754352000,
|
|
206
|
+
owned_by: "ollama",
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
vi.mocked(fetchWithRetry).mockResolvedValue({
|
|
211
|
+
ok: true,
|
|
212
|
+
json: async () => mockModels,
|
|
213
|
+
} as Response);
|
|
214
|
+
|
|
215
|
+
const { default: ollamaProvider } = await import(
|
|
216
|
+
"../providers/ollama.ts"
|
|
217
|
+
);
|
|
218
|
+
await ollamaProvider(mockPi);
|
|
219
|
+
|
|
220
|
+
const registerCall = mockRegisterProvider.mock.calls[0];
|
|
221
|
+
const models = registerCall?.[1]?.models;
|
|
222
|
+
|
|
223
|
+
expect(models[0].reasoning).toBe(true);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe("setupProvider integration", () => {
|
|
228
|
+
it("should call setupProvider with stored models", async () => {
|
|
229
|
+
const mockModels = {
|
|
230
|
+
data: [
|
|
231
|
+
{
|
|
232
|
+
id: "gpt-oss:120b",
|
|
233
|
+
object: "model",
|
|
234
|
+
created: 1754352000,
|
|
235
|
+
owned_by: "ollama",
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
};
|
|
239
|
+
vi.mocked(fetchWithRetry).mockResolvedValue({
|
|
240
|
+
ok: true,
|
|
241
|
+
json: async () => mockModels,
|
|
242
|
+
} as Response);
|
|
243
|
+
|
|
244
|
+
const { default: ollamaProvider } = await import(
|
|
245
|
+
"../providers/ollama.ts"
|
|
246
|
+
);
|
|
247
|
+
await ollamaProvider(mockPi);
|
|
248
|
+
|
|
249
|
+
expect(setupProvider).toHaveBeenCalledWith(
|
|
250
|
+
mockPi,
|
|
251
|
+
expect.objectContaining({
|
|
252
|
+
providerId: "ollama",
|
|
253
|
+
}),
|
|
254
|
+
expect.objectContaining({
|
|
255
|
+
free: expect.any(Array),
|
|
256
|
+
all: expect.any(Array),
|
|
257
|
+
}),
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenRouter Provider Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
ExtensionAPI,
|
|
7
|
+
ProviderModelConfig,
|
|
8
|
+
} from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
10
|
+
|
|
11
|
+
// Mock dependencies
|
|
12
|
+
vi.mock("../config.ts", () => ({
|
|
13
|
+
OPENROUTER_API_KEY: "test-api-key",
|
|
14
|
+
OPENROUTER_SHOW_PAID: false,
|
|
15
|
+
PROVIDER_OPENROUTER: "openrouter",
|
|
16
|
+
applyHidden: (models: ProviderModelConfig[]) => models,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock("../constants.ts", () => ({
|
|
20
|
+
BASE_URL_OPENROUTER: "https://openrouter.ai/api/v1",
|
|
21
|
+
DEFAULT_FETCH_TIMEOUT_MS: 10000,
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
vi.mock("../metrics.ts", () => ({
|
|
25
|
+
fetchOpenRouterMetrics: vi.fn(),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
vi.mock("../provider-helper.ts", () => ({
|
|
29
|
+
createReRegister: vi.fn(() => vi.fn()),
|
|
30
|
+
createCtxReRegister: vi.fn(() => vi.fn()),
|
|
31
|
+
setupProvider: vi.fn(),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
vi.mock("../util.ts", () => ({
|
|
35
|
+
logWarning: vi.fn(),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
vi.mock("../providers/model-fetcher.ts", () => ({
|
|
39
|
+
fetchOpenRouterModelsWithFree: vi.fn(),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
import { setupProvider } from "../provider-helper.ts";
|
|
43
|
+
import { fetchOpenRouterModelsWithFree } from "../providers/model-fetcher.ts";
|
|
44
|
+
import openrouterProvider from "../providers/openrouter.ts";
|
|
45
|
+
|
|
46
|
+
describe("OpenRouter Provider", () => {
|
|
47
|
+
let mockPi: ExtensionAPI;
|
|
48
|
+
let mockRegisterProvider: ReturnType<typeof vi.fn>;
|
|
49
|
+
let mockOn: ReturnType<typeof vi.fn>;
|
|
50
|
+
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
vi.clearAllMocks();
|
|
53
|
+
mockRegisterProvider = vi.fn();
|
|
54
|
+
mockOn = vi.fn();
|
|
55
|
+
|
|
56
|
+
mockPi = {
|
|
57
|
+
registerProvider: mockRegisterProvider,
|
|
58
|
+
on: mockOn,
|
|
59
|
+
} as unknown as ExtensionAPI;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("initialization", () => {
|
|
63
|
+
it("should register event handlers", async () => {
|
|
64
|
+
vi.mocked(fetchOpenRouterModelsWithFree).mockResolvedValue({
|
|
65
|
+
free: [],
|
|
66
|
+
all: [],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await openrouterProvider(mockPi);
|
|
70
|
+
|
|
71
|
+
expect(mockOn).toHaveBeenCalledWith(
|
|
72
|
+
"session_start",
|
|
73
|
+
expect.any(Function),
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("session_start handling", () => {
|
|
79
|
+
it("should handle existing auth with free models", async () => {
|
|
80
|
+
const mockFreeModels: ProviderModelConfig[] = [
|
|
81
|
+
{
|
|
82
|
+
id: "gpt-3.5",
|
|
83
|
+
name: "GPT-3.5",
|
|
84
|
+
reasoning: false,
|
|
85
|
+
input: ["text"],
|
|
86
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
87
|
+
contextWindow: 16000,
|
|
88
|
+
maxTokens: 4096,
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
const mockAllModels: ProviderModelConfig[] = [
|
|
93
|
+
...mockFreeModels,
|
|
94
|
+
{
|
|
95
|
+
id: "gpt-4",
|
|
96
|
+
name: "GPT-4",
|
|
97
|
+
reasoning: true,
|
|
98
|
+
input: ["text"],
|
|
99
|
+
cost: { input: 30, output: 60, cacheRead: 0, cacheWrite: 0 },
|
|
100
|
+
contextWindow: 128000,
|
|
101
|
+
maxTokens: 4096,
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
vi.mocked(fetchOpenRouterModelsWithFree).mockResolvedValue({
|
|
106
|
+
free: mockFreeModels,
|
|
107
|
+
all: mockAllModels,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await openrouterProvider(mockPi);
|
|
111
|
+
|
|
112
|
+
// Get session_start handler
|
|
113
|
+
const sessionStartHandler = mockOn.mock.calls.find(
|
|
114
|
+
(call) => call[0] === "session_start",
|
|
115
|
+
)?.[1];
|
|
116
|
+
|
|
117
|
+
expect(sessionStartHandler).toBeDefined();
|
|
118
|
+
|
|
119
|
+
// Mock context with existing auth
|
|
120
|
+
const mockCtx = {
|
|
121
|
+
modelRegistry: {
|
|
122
|
+
getAll: vi
|
|
123
|
+
.fn()
|
|
124
|
+
.mockReturnValue(
|
|
125
|
+
mockAllModels.map((m) => ({ ...m, provider: "openrouter" })),
|
|
126
|
+
),
|
|
127
|
+
getAvailable: vi.fn().mockReturnValue([{ provider: "openrouter" }]),
|
|
128
|
+
registerProvider: vi.fn(),
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
await sessionStartHandler({}, mockCtx);
|
|
133
|
+
|
|
134
|
+
expect(mockCtx.modelRegistry.registerProvider).toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("should set API key in env when no existing auth", async () => {
|
|
138
|
+
vi.mocked(fetchOpenRouterModelsWithFree).mockResolvedValue({
|
|
139
|
+
free: [
|
|
140
|
+
{
|
|
141
|
+
id: "test-model",
|
|
142
|
+
name: "Test",
|
|
143
|
+
reasoning: false,
|
|
144
|
+
input: ["text"],
|
|
145
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
146
|
+
contextWindow: 128000,
|
|
147
|
+
maxTokens: 4096,
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
all: [],
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await openrouterProvider(mockPi);
|
|
154
|
+
|
|
155
|
+
const sessionStartHandler = mockOn.mock.calls.find(
|
|
156
|
+
(call) => call[0] === "session_start",
|
|
157
|
+
)?.[1];
|
|
158
|
+
|
|
159
|
+
const mockCtx = {
|
|
160
|
+
modelRegistry: {
|
|
161
|
+
getAll: vi.fn().mockReturnValue([]),
|
|
162
|
+
getAvailable: vi.fn().mockReturnValue([]),
|
|
163
|
+
registerProvider: vi.fn(),
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
await sessionStartHandler({}, mockCtx);
|
|
168
|
+
|
|
169
|
+
expect(process.env.OPENROUTER_API_KEY).toBe("test-api-key");
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("setupProvider integration", () => {
|
|
174
|
+
it("should call setupProvider with correct config", async () => {
|
|
175
|
+
vi.mocked(fetchOpenRouterModelsWithFree).mockResolvedValue({
|
|
176
|
+
free: [],
|
|
177
|
+
all: [],
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await openrouterProvider(mockPi);
|
|
181
|
+
|
|
182
|
+
expect(setupProvider).toHaveBeenCalledWith(
|
|
183
|
+
mockPi,
|
|
184
|
+
expect.objectContaining({
|
|
185
|
+
providerId: "openrouter",
|
|
186
|
+
reRegister: expect.any(Function),
|
|
187
|
+
}),
|
|
188
|
+
expect.objectContaining({ free: [], all: [] }),
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
});
|