harness-auto-docs 0.3.2 → 0.3.3
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/AGENTS.md +9 -1
- package/README.md +13 -7
- package/README_ja.md +13 -7
- package/README_zh.md +13 -7
- package/dist/ai/deepseek.d.ts +7 -0
- package/dist/ai/deepseek.js +16 -0
- package/dist/ai/doubao.d.ts +7 -0
- package/dist/ai/doubao.js +16 -0
- package/dist/ai/grok.d.ts +7 -0
- package/dist/ai/grok.js +16 -0
- package/dist/ai/kimi.d.ts +7 -0
- package/dist/ai/kimi.js +16 -0
- package/dist/ai/qwen.d.ts +7 -0
- package/dist/ai/qwen.js +16 -0
- package/dist/ai/zhipu.d.ts +7 -0
- package/dist/ai/zhipu.js +16 -0
- package/dist/cli.js +24 -7
- package/docs/superpowers/plans/2026-04-03-multi-provider-models.md +802 -0
- package/docs/superpowers/specs/2026-04-03-multi-provider-models-design.md +68 -0
- package/examples/github-workflow.yml +10 -2
- package/package.json +1 -1
- package/src/ai/deepseek.ts +20 -0
- package/src/ai/doubao.ts +20 -0
- package/src/ai/grok.ts +20 -0
- package/src/ai/kimi.ts +20 -0
- package/src/ai/qwen.ts +20 -0
- package/src/ai/zhipu.ts +20 -0
- package/src/cli.ts +20 -7
- package/tests/core/deepseek.test.ts +30 -0
- package/tests/core/doubao.test.ts +30 -0
- package/tests/core/grok.test.ts +30 -0
- package/tests/core/kimi.test.ts +30 -0
- package/tests/core/qwen.test.ts +30 -0
- package/tests/core/zhipu.test.ts +30 -0
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
# Multi-Provider Model Support Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Add support for Qwen, 智谱, DeepSeek, 豆包, Kimi, and Grok AI models, each with its own provider file and dedicated API key env var.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Six new provider classes (one file each) mirror the existing `OpenAIProvider` — same OpenAI SDK, different `baseURL`. The `selectAI` function in `cli.ts` dispatches on model prefix and reads the appropriate env var. No changes to the `AIProvider` interface or any other module.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, `openai` npm SDK, Vitest
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## File Map
|
|
14
|
+
|
|
15
|
+
| Action | Path |
|
|
16
|
+
|--------|------|
|
|
17
|
+
| Create | `src/ai/qwen.ts` |
|
|
18
|
+
| Create | `src/ai/zhipu.ts` |
|
|
19
|
+
| Create | `src/ai/deepseek.ts` |
|
|
20
|
+
| Create | `src/ai/doubao.ts` |
|
|
21
|
+
| Create | `src/ai/kimi.ts` |
|
|
22
|
+
| Create | `src/ai/grok.ts` |
|
|
23
|
+
| Create | `tests/core/qwen.test.ts` |
|
|
24
|
+
| Create | `tests/core/zhipu.test.ts` |
|
|
25
|
+
| Create | `tests/core/deepseek.test.ts` |
|
|
26
|
+
| Create | `tests/core/doubao.test.ts` |
|
|
27
|
+
| Create | `tests/core/kimi.test.ts` |
|
|
28
|
+
| Create | `tests/core/grok.test.ts` |
|
|
29
|
+
| Modify | `src/cli.ts` |
|
|
30
|
+
| Modify | `README.md` |
|
|
31
|
+
| Modify | `README_zh.md` |
|
|
32
|
+
| Modify | `README_ja.md` |
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Task 1: QwenProvider
|
|
37
|
+
|
|
38
|
+
**Files:**
|
|
39
|
+
- Create: `src/ai/qwen.ts`
|
|
40
|
+
- Create: `tests/core/qwen.test.ts`
|
|
41
|
+
|
|
42
|
+
- [ ] **Step 1: Write the failing test**
|
|
43
|
+
|
|
44
|
+
Create `tests/core/qwen.test.ts`:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
48
|
+
import { QwenProvider } from '../../src/ai/qwen.js';
|
|
49
|
+
|
|
50
|
+
const mockCreate = vi.fn().mockResolvedValue({
|
|
51
|
+
choices: [{ message: { content: 'qwen generated content' } }],
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
vi.mock('openai', () => ({
|
|
55
|
+
default: vi.fn().mockImplementation(({ baseURL }: { baseURL: string }) => {
|
|
56
|
+
if (baseURL !== 'https://dashscope.aliyuncs.com/compatible-mode/v1') {
|
|
57
|
+
throw new Error(`unexpected baseURL: ${baseURL}`);
|
|
58
|
+
}
|
|
59
|
+
return { chat: { completions: { create: mockCreate } } };
|
|
60
|
+
}),
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
describe('QwenProvider', () => {
|
|
64
|
+
it('returns text content from the API response', async () => {
|
|
65
|
+
const provider = new QwenProvider('test-key', 'qwen-turbo');
|
|
66
|
+
const result = await provider.generate('write docs for this diff');
|
|
67
|
+
expect(result).toBe('qwen generated content');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('returns empty string when message content is null', async () => {
|
|
71
|
+
mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: null } }] });
|
|
72
|
+
const provider = new QwenProvider('test-key', 'qwen-turbo');
|
|
73
|
+
const result = await provider.generate('prompt');
|
|
74
|
+
expect(result).toBe('');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm test tests/core/qwen.test.ts
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Expected: FAIL — `QwenProvider` not found.
|
|
86
|
+
|
|
87
|
+
- [ ] **Step 3: Write implementation**
|
|
88
|
+
|
|
89
|
+
Create `src/ai/qwen.ts`:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import OpenAI from 'openai';
|
|
93
|
+
import type { AIProvider } from './interface.js';
|
|
94
|
+
|
|
95
|
+
export class QwenProvider implements AIProvider {
|
|
96
|
+
private client: OpenAI;
|
|
97
|
+
private model: string;
|
|
98
|
+
|
|
99
|
+
constructor(apiKey: string, model: string) {
|
|
100
|
+
this.client = new OpenAI({ apiKey, baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1' });
|
|
101
|
+
this.model = model;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async generate(prompt: string): Promise<string> {
|
|
105
|
+
const completion = await this.client.chat.completions.create({
|
|
106
|
+
model: this.model,
|
|
107
|
+
messages: [{ role: 'user', content: prompt }],
|
|
108
|
+
});
|
|
109
|
+
return completion.choices[0].message.content ?? '';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
npm test tests/core/qwen.test.ts
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Expected: PASS — 2 tests pass.
|
|
121
|
+
|
|
122
|
+
- [ ] **Step 5: Commit**
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
git add src/ai/qwen.ts tests/core/qwen.test.ts
|
|
126
|
+
git commit -m "feat: add QwenProvider"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Task 2: ZhipuProvider
|
|
132
|
+
|
|
133
|
+
**Files:**
|
|
134
|
+
- Create: `src/ai/zhipu.ts`
|
|
135
|
+
- Create: `tests/core/zhipu.test.ts`
|
|
136
|
+
|
|
137
|
+
- [ ] **Step 1: Write the failing test**
|
|
138
|
+
|
|
139
|
+
Create `tests/core/zhipu.test.ts`:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
143
|
+
import { ZhipuProvider } from '../../src/ai/zhipu.js';
|
|
144
|
+
|
|
145
|
+
const mockCreate = vi.fn().mockResolvedValue({
|
|
146
|
+
choices: [{ message: { content: 'zhipu generated content' } }],
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
vi.mock('openai', () => ({
|
|
150
|
+
default: vi.fn().mockImplementation(({ baseURL }: { baseURL: string }) => {
|
|
151
|
+
if (baseURL !== 'https://open.bigmodel.cn/api/paas/v4') {
|
|
152
|
+
throw new Error(`unexpected baseURL: ${baseURL}`);
|
|
153
|
+
}
|
|
154
|
+
return { chat: { completions: { create: mockCreate } } };
|
|
155
|
+
}),
|
|
156
|
+
}));
|
|
157
|
+
|
|
158
|
+
describe('ZhipuProvider', () => {
|
|
159
|
+
it('returns text content from the API response', async () => {
|
|
160
|
+
const provider = new ZhipuProvider('test-key', 'glm-4');
|
|
161
|
+
const result = await provider.generate('write docs for this diff');
|
|
162
|
+
expect(result).toBe('zhipu generated content');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('returns empty string when message content is null', async () => {
|
|
166
|
+
mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: null } }] });
|
|
167
|
+
const provider = new ZhipuProvider('test-key', 'glm-4');
|
|
168
|
+
const result = await provider.generate('prompt');
|
|
169
|
+
expect(result).toBe('');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
npm test tests/core/zhipu.test.ts
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Expected: FAIL — `ZhipuProvider` not found.
|
|
181
|
+
|
|
182
|
+
- [ ] **Step 3: Write implementation**
|
|
183
|
+
|
|
184
|
+
Create `src/ai/zhipu.ts`:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import OpenAI from 'openai';
|
|
188
|
+
import type { AIProvider } from './interface.js';
|
|
189
|
+
|
|
190
|
+
export class ZhipuProvider implements AIProvider {
|
|
191
|
+
private client: OpenAI;
|
|
192
|
+
private model: string;
|
|
193
|
+
|
|
194
|
+
constructor(apiKey: string, model: string) {
|
|
195
|
+
this.client = new OpenAI({ apiKey, baseURL: 'https://open.bigmodel.cn/api/paas/v4' });
|
|
196
|
+
this.model = model;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async generate(prompt: string): Promise<string> {
|
|
200
|
+
const completion = await this.client.chat.completions.create({
|
|
201
|
+
model: this.model,
|
|
202
|
+
messages: [{ role: 'user', content: prompt }],
|
|
203
|
+
});
|
|
204
|
+
return completion.choices[0].message.content ?? '';
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
npm test tests/core/zhipu.test.ts
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Expected: PASS — 2 tests pass.
|
|
216
|
+
|
|
217
|
+
- [ ] **Step 5: Commit**
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
git add src/ai/zhipu.ts tests/core/zhipu.test.ts
|
|
221
|
+
git commit -m "feat: add ZhipuProvider"
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Task 3: DeepSeekProvider
|
|
227
|
+
|
|
228
|
+
**Files:**
|
|
229
|
+
- Create: `src/ai/deepseek.ts`
|
|
230
|
+
- Create: `tests/core/deepseek.test.ts`
|
|
231
|
+
|
|
232
|
+
- [ ] **Step 1: Write the failing test**
|
|
233
|
+
|
|
234
|
+
Create `tests/core/deepseek.test.ts`:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
238
|
+
import { DeepSeekProvider } from '../../src/ai/deepseek.js';
|
|
239
|
+
|
|
240
|
+
const mockCreate = vi.fn().mockResolvedValue({
|
|
241
|
+
choices: [{ message: { content: 'deepseek generated content' } }],
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
vi.mock('openai', () => ({
|
|
245
|
+
default: vi.fn().mockImplementation(({ baseURL }: { baseURL: string }) => {
|
|
246
|
+
if (baseURL !== 'https://api.deepseek.com') {
|
|
247
|
+
throw new Error(`unexpected baseURL: ${baseURL}`);
|
|
248
|
+
}
|
|
249
|
+
return { chat: { completions: { create: mockCreate } } };
|
|
250
|
+
}),
|
|
251
|
+
}));
|
|
252
|
+
|
|
253
|
+
describe('DeepSeekProvider', () => {
|
|
254
|
+
it('returns text content from the API response', async () => {
|
|
255
|
+
const provider = new DeepSeekProvider('test-key', 'deepseek-chat');
|
|
256
|
+
const result = await provider.generate('write docs for this diff');
|
|
257
|
+
expect(result).toBe('deepseek generated content');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('returns empty string when message content is null', async () => {
|
|
261
|
+
mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: null } }] });
|
|
262
|
+
const provider = new DeepSeekProvider('test-key', 'deepseek-chat');
|
|
263
|
+
const result = await provider.generate('prompt');
|
|
264
|
+
expect(result).toBe('');
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
npm test tests/core/deepseek.test.ts
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Expected: FAIL — `DeepSeekProvider` not found.
|
|
276
|
+
|
|
277
|
+
- [ ] **Step 3: Write implementation**
|
|
278
|
+
|
|
279
|
+
Create `src/ai/deepseek.ts`:
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
import OpenAI from 'openai';
|
|
283
|
+
import type { AIProvider } from './interface.js';
|
|
284
|
+
|
|
285
|
+
export class DeepSeekProvider implements AIProvider {
|
|
286
|
+
private client: OpenAI;
|
|
287
|
+
private model: string;
|
|
288
|
+
|
|
289
|
+
constructor(apiKey: string, model: string) {
|
|
290
|
+
this.client = new OpenAI({ apiKey, baseURL: 'https://api.deepseek.com' });
|
|
291
|
+
this.model = model;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async generate(prompt: string): Promise<string> {
|
|
295
|
+
const completion = await this.client.chat.completions.create({
|
|
296
|
+
model: this.model,
|
|
297
|
+
messages: [{ role: 'user', content: prompt }],
|
|
298
|
+
});
|
|
299
|
+
return completion.choices[0].message.content ?? '';
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
npm test tests/core/deepseek.test.ts
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Expected: PASS — 2 tests pass.
|
|
311
|
+
|
|
312
|
+
- [ ] **Step 5: Commit**
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
git add src/ai/deepseek.ts tests/core/deepseek.test.ts
|
|
316
|
+
git commit -m "feat: add DeepSeekProvider"
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## Task 4: DoubaoProvider
|
|
322
|
+
|
|
323
|
+
**Files:**
|
|
324
|
+
- Create: `src/ai/doubao.ts`
|
|
325
|
+
- Create: `tests/core/doubao.test.ts`
|
|
326
|
+
|
|
327
|
+
- [ ] **Step 1: Write the failing test**
|
|
328
|
+
|
|
329
|
+
Create `tests/core/doubao.test.ts`:
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
333
|
+
import { DoubaoProvider } from '../../src/ai/doubao.js';
|
|
334
|
+
|
|
335
|
+
const mockCreate = vi.fn().mockResolvedValue({
|
|
336
|
+
choices: [{ message: { content: 'doubao generated content' } }],
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
vi.mock('openai', () => ({
|
|
340
|
+
default: vi.fn().mockImplementation(({ baseURL }: { baseURL: string }) => {
|
|
341
|
+
if (baseURL !== 'https://ark.cn-beijing.volces.com/api/v3') {
|
|
342
|
+
throw new Error(`unexpected baseURL: ${baseURL}`);
|
|
343
|
+
}
|
|
344
|
+
return { chat: { completions: { create: mockCreate } } };
|
|
345
|
+
}),
|
|
346
|
+
}));
|
|
347
|
+
|
|
348
|
+
describe('DoubaoProvider', () => {
|
|
349
|
+
it('returns text content from the API response', async () => {
|
|
350
|
+
const provider = new DoubaoProvider('test-key', 'doubao-pro-4k');
|
|
351
|
+
const result = await provider.generate('write docs for this diff');
|
|
352
|
+
expect(result).toBe('doubao generated content');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('returns empty string when message content is null', async () => {
|
|
356
|
+
mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: null } }] });
|
|
357
|
+
const provider = new DoubaoProvider('test-key', 'doubao-pro-4k');
|
|
358
|
+
const result = await provider.generate('prompt');
|
|
359
|
+
expect(result).toBe('');
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
npm test tests/core/doubao.test.ts
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Expected: FAIL — `DoubaoProvider` not found.
|
|
371
|
+
|
|
372
|
+
- [ ] **Step 3: Write implementation**
|
|
373
|
+
|
|
374
|
+
Create `src/ai/doubao.ts`:
|
|
375
|
+
|
|
376
|
+
```ts
|
|
377
|
+
import OpenAI from 'openai';
|
|
378
|
+
import type { AIProvider } from './interface.js';
|
|
379
|
+
|
|
380
|
+
export class DoubaoProvider implements AIProvider {
|
|
381
|
+
private client: OpenAI;
|
|
382
|
+
private model: string;
|
|
383
|
+
|
|
384
|
+
constructor(apiKey: string, model: string) {
|
|
385
|
+
this.client = new OpenAI({ apiKey, baseURL: 'https://ark.cn-beijing.volces.com/api/v3' });
|
|
386
|
+
this.model = model;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async generate(prompt: string): Promise<string> {
|
|
390
|
+
const completion = await this.client.chat.completions.create({
|
|
391
|
+
model: this.model,
|
|
392
|
+
messages: [{ role: 'user', content: prompt }],
|
|
393
|
+
});
|
|
394
|
+
return completion.choices[0].message.content ?? '';
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
400
|
+
|
|
401
|
+
```bash
|
|
402
|
+
npm test tests/core/doubao.test.ts
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Expected: PASS — 2 tests pass.
|
|
406
|
+
|
|
407
|
+
- [ ] **Step 5: Commit**
|
|
408
|
+
|
|
409
|
+
```bash
|
|
410
|
+
git add src/ai/doubao.ts tests/core/doubao.test.ts
|
|
411
|
+
git commit -m "feat: add DoubaoProvider"
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
## Task 5: KimiProvider
|
|
417
|
+
|
|
418
|
+
**Files:**
|
|
419
|
+
- Create: `src/ai/kimi.ts`
|
|
420
|
+
- Create: `tests/core/kimi.test.ts`
|
|
421
|
+
|
|
422
|
+
- [ ] **Step 1: Write the failing test**
|
|
423
|
+
|
|
424
|
+
Create `tests/core/kimi.test.ts`:
|
|
425
|
+
|
|
426
|
+
```ts
|
|
427
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
428
|
+
import { KimiProvider } from '../../src/ai/kimi.js';
|
|
429
|
+
|
|
430
|
+
const mockCreate = vi.fn().mockResolvedValue({
|
|
431
|
+
choices: [{ message: { content: 'kimi generated content' } }],
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
vi.mock('openai', () => ({
|
|
435
|
+
default: vi.fn().mockImplementation(({ baseURL }: { baseURL: string }) => {
|
|
436
|
+
if (baseURL !== 'https://api.moonshot.cn/v1') {
|
|
437
|
+
throw new Error(`unexpected baseURL: ${baseURL}`);
|
|
438
|
+
}
|
|
439
|
+
return { chat: { completions: { create: mockCreate } } };
|
|
440
|
+
}),
|
|
441
|
+
}));
|
|
442
|
+
|
|
443
|
+
describe('KimiProvider', () => {
|
|
444
|
+
it('returns text content from the API response', async () => {
|
|
445
|
+
const provider = new KimiProvider('test-key', 'moonshot-v1-8k');
|
|
446
|
+
const result = await provider.generate('write docs for this diff');
|
|
447
|
+
expect(result).toBe('kimi generated content');
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('returns empty string when message content is null', async () => {
|
|
451
|
+
mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: null } }] });
|
|
452
|
+
const provider = new KimiProvider('test-key', 'moonshot-v1-8k');
|
|
453
|
+
const result = await provider.generate('prompt');
|
|
454
|
+
expect(result).toBe('');
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
460
|
+
|
|
461
|
+
```bash
|
|
462
|
+
npm test tests/core/kimi.test.ts
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
Expected: FAIL — `KimiProvider` not found.
|
|
466
|
+
|
|
467
|
+
- [ ] **Step 3: Write implementation**
|
|
468
|
+
|
|
469
|
+
Create `src/ai/kimi.ts`:
|
|
470
|
+
|
|
471
|
+
```ts
|
|
472
|
+
import OpenAI from 'openai';
|
|
473
|
+
import type { AIProvider } from './interface.js';
|
|
474
|
+
|
|
475
|
+
export class KimiProvider implements AIProvider {
|
|
476
|
+
private client: OpenAI;
|
|
477
|
+
private model: string;
|
|
478
|
+
|
|
479
|
+
constructor(apiKey: string, model: string) {
|
|
480
|
+
this.client = new OpenAI({ apiKey, baseURL: 'https://api.moonshot.cn/v1' });
|
|
481
|
+
this.model = model;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async generate(prompt: string): Promise<string> {
|
|
485
|
+
const completion = await this.client.chat.completions.create({
|
|
486
|
+
model: this.model,
|
|
487
|
+
messages: [{ role: 'user', content: prompt }],
|
|
488
|
+
});
|
|
489
|
+
return completion.choices[0].message.content ?? '';
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
495
|
+
|
|
496
|
+
```bash
|
|
497
|
+
npm test tests/core/kimi.test.ts
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
Expected: PASS — 2 tests pass.
|
|
501
|
+
|
|
502
|
+
- [ ] **Step 5: Commit**
|
|
503
|
+
|
|
504
|
+
```bash
|
|
505
|
+
git add src/ai/kimi.ts tests/core/kimi.test.ts
|
|
506
|
+
git commit -m "feat: add KimiProvider"
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
---
|
|
510
|
+
|
|
511
|
+
## Task 6: GrokProvider
|
|
512
|
+
|
|
513
|
+
**Files:**
|
|
514
|
+
- Create: `src/ai/grok.ts`
|
|
515
|
+
- Create: `tests/core/grok.test.ts`
|
|
516
|
+
|
|
517
|
+
- [ ] **Step 1: Write the failing test**
|
|
518
|
+
|
|
519
|
+
Create `tests/core/grok.test.ts`:
|
|
520
|
+
|
|
521
|
+
```ts
|
|
522
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
523
|
+
import { GrokProvider } from '../../src/ai/grok.js';
|
|
524
|
+
|
|
525
|
+
const mockCreate = vi.fn().mockResolvedValue({
|
|
526
|
+
choices: [{ message: { content: 'grok generated content' } }],
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
vi.mock('openai', () => ({
|
|
530
|
+
default: vi.fn().mockImplementation(({ baseURL }: { baseURL: string }) => {
|
|
531
|
+
if (baseURL !== 'https://api.x.ai/v1') {
|
|
532
|
+
throw new Error(`unexpected baseURL: ${baseURL}`);
|
|
533
|
+
}
|
|
534
|
+
return { chat: { completions: { create: mockCreate } } };
|
|
535
|
+
}),
|
|
536
|
+
}));
|
|
537
|
+
|
|
538
|
+
describe('GrokProvider', () => {
|
|
539
|
+
it('returns text content from the API response', async () => {
|
|
540
|
+
const provider = new GrokProvider('test-key', 'grok-2');
|
|
541
|
+
const result = await provider.generate('write docs for this diff');
|
|
542
|
+
expect(result).toBe('grok generated content');
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('returns empty string when message content is null', async () => {
|
|
546
|
+
mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: null } }] });
|
|
547
|
+
const provider = new GrokProvider('test-key', 'grok-2');
|
|
548
|
+
const result = await provider.generate('prompt');
|
|
549
|
+
expect(result).toBe('');
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
555
|
+
|
|
556
|
+
```bash
|
|
557
|
+
npm test tests/core/grok.test.ts
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
Expected: FAIL — `GrokProvider` not found.
|
|
561
|
+
|
|
562
|
+
- [ ] **Step 3: Write implementation**
|
|
563
|
+
|
|
564
|
+
Create `src/ai/grok.ts`:
|
|
565
|
+
|
|
566
|
+
```ts
|
|
567
|
+
import OpenAI from 'openai';
|
|
568
|
+
import type { AIProvider } from './interface.js';
|
|
569
|
+
|
|
570
|
+
export class GrokProvider implements AIProvider {
|
|
571
|
+
private client: OpenAI;
|
|
572
|
+
private model: string;
|
|
573
|
+
|
|
574
|
+
constructor(apiKey: string, model: string) {
|
|
575
|
+
this.client = new OpenAI({ apiKey, baseURL: 'https://api.x.ai/v1' });
|
|
576
|
+
this.model = model;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async generate(prompt: string): Promise<string> {
|
|
580
|
+
const completion = await this.client.chat.completions.create({
|
|
581
|
+
model: this.model,
|
|
582
|
+
messages: [{ role: 'user', content: prompt }],
|
|
583
|
+
});
|
|
584
|
+
return completion.choices[0].message.content ?? '';
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
590
|
+
|
|
591
|
+
```bash
|
|
592
|
+
npm test tests/core/grok.test.ts
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
Expected: PASS — 2 tests pass.
|
|
596
|
+
|
|
597
|
+
- [ ] **Step 5: Commit**
|
|
598
|
+
|
|
599
|
+
```bash
|
|
600
|
+
git add src/ai/grok.ts tests/core/grok.test.ts
|
|
601
|
+
git commit -m "feat: add GrokProvider"
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
---
|
|
605
|
+
|
|
606
|
+
## Task 7: Wire providers into `selectAI`
|
|
607
|
+
|
|
608
|
+
**Files:**
|
|
609
|
+
- Modify: `src/cli.ts`
|
|
610
|
+
|
|
611
|
+
- [ ] **Step 1: Update imports and `selectAI` in `src/cli.ts`**
|
|
612
|
+
|
|
613
|
+
Find and replace the existing AI import block:
|
|
614
|
+
|
|
615
|
+
```ts
|
|
616
|
+
// FIND (replace these 4 lines):
|
|
617
|
+
import { AnthropicProvider } from './ai/anthropic.js';
|
|
618
|
+
import { OpenAIProvider } from './ai/openai.js';
|
|
619
|
+
import { MiniMaxProvider } from './ai/minimax.js';
|
|
620
|
+
import type { AIProvider } from './ai/interface.js';
|
|
621
|
+
|
|
622
|
+
// REPLACE WITH:
|
|
623
|
+
import { AnthropicProvider } from './ai/anthropic.js';
|
|
624
|
+
import { OpenAIProvider } from './ai/openai.js';
|
|
625
|
+
import { MiniMaxProvider } from './ai/minimax.js';
|
|
626
|
+
import { QwenProvider } from './ai/qwen.js';
|
|
627
|
+
import { ZhipuProvider } from './ai/zhipu.js';
|
|
628
|
+
import { DeepSeekProvider } from './ai/deepseek.js';
|
|
629
|
+
import { DoubaoProvider } from './ai/doubao.js';
|
|
630
|
+
import { KimiProvider } from './ai/kimi.js';
|
|
631
|
+
import { GrokProvider } from './ai/grok.js';
|
|
632
|
+
import type { AIProvider } from './ai/interface.js';
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
Find and replace the existing `selectAI` function:
|
|
636
|
+
|
|
637
|
+
```ts
|
|
638
|
+
// FIND (replace this entire function):
|
|
639
|
+
function selectAI(model: string, apiKey: string): AIProvider {
|
|
640
|
+
if (model.startsWith('claude-')) return new AnthropicProvider(apiKey, model);
|
|
641
|
+
if (model.startsWith('gpt-')) return new OpenAIProvider(apiKey, model);
|
|
642
|
+
if (model.startsWith('MiniMax-')) return new MiniMaxProvider(apiKey, model);
|
|
643
|
+
console.error(`Error: Unknown model "${model}". Use a claude-*, gpt-*, or MiniMax-* model.`);
|
|
644
|
+
process.exit(1);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// REPLACE WITH:
|
|
648
|
+
function selectAI(model: string, apiKey: string): AIProvider {
|
|
649
|
+
if (model.startsWith('claude-')) return new AnthropicProvider(apiKey, model);
|
|
650
|
+
if (model.startsWith('gpt-')) return new OpenAIProvider(apiKey, model);
|
|
651
|
+
if (model.startsWith('MiniMax-')) return new MiniMaxProvider(apiKey, model);
|
|
652
|
+
if (model.startsWith('qwen-')) return new QwenProvider(requireEnv('QWEN_API_KEY'), model);
|
|
653
|
+
if (model.startsWith('glm-')) return new ZhipuProvider(requireEnv('ZHIPU_API_KEY'), model);
|
|
654
|
+
if (model.startsWith('deepseek-')) return new DeepSeekProvider(requireEnv('DEEPSEEK_API_KEY'), model);
|
|
655
|
+
if (model.startsWith('doubao-')) return new DoubaoProvider(requireEnv('DOUBAO_API_KEY'), model);
|
|
656
|
+
if (model.startsWith('moonshot-')) return new KimiProvider(requireEnv('KIMI_API_KEY'), model);
|
|
657
|
+
if (model.startsWith('grok-')) return new GrokProvider(requireEnv('GROK_API_KEY'), model);
|
|
658
|
+
console.error(
|
|
659
|
+
`Error: Unknown model "${model}". Supported prefixes: claude-*, gpt-*, MiniMax-*, qwen-*, glm-*, deepseek-*, doubao-*, moonshot-*, grok-*`
|
|
660
|
+
);
|
|
661
|
+
process.exit(1);
|
|
662
|
+
}
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
- [ ] **Step 2: Build to verify no TypeScript errors**
|
|
666
|
+
|
|
667
|
+
```bash
|
|
668
|
+
npm run build
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
Expected: exits 0, `dist/` updated with no errors.
|
|
672
|
+
|
|
673
|
+
- [ ] **Step 3: Commit**
|
|
674
|
+
|
|
675
|
+
```bash
|
|
676
|
+
git add src/cli.ts
|
|
677
|
+
git commit -m "feat: wire Qwen, Zhipu, DeepSeek, Doubao, Kimi, Grok into selectAI"
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
---
|
|
681
|
+
|
|
682
|
+
## Task 8: Update README files
|
|
683
|
+
|
|
684
|
+
**Files:**
|
|
685
|
+
- Modify: `README.md`
|
|
686
|
+
- Modify: `README_zh.md`
|
|
687
|
+
- Modify: `README_ja.md`
|
|
688
|
+
|
|
689
|
+
- [ ] **Step 1: Update the "Supported models" table in `README.md`**
|
|
690
|
+
|
|
691
|
+
Find and replace the existing table block:
|
|
692
|
+
|
|
693
|
+
```markdown
|
|
694
|
+
// FIND:
|
|
695
|
+
| Prefix | Provider | Example models |
|
|
696
|
+
|--------|----------|----------------|
|
|
697
|
+
| `claude-*` | Anthropic | `claude-sonnet-4-6`, `claude-opus-4-6`, `claude-haiku-4-5-20251001` |
|
|
698
|
+
| `gpt-*` | OpenAI | `gpt-4o`, `gpt-4o-mini`, `o3` |
|
|
699
|
+
| `MiniMax-*` | MiniMax (Anthropic-compatible) | `MiniMax-Text-01` |
|
|
700
|
+
|
|
701
|
+
// REPLACE WITH:
|
|
702
|
+
| Prefix | Provider | API key env var | Example models |
|
|
703
|
+
|--------|----------|-----------------|----------------|
|
|
704
|
+
| `claude-*` | Anthropic | `AI_API_KEY` | `claude-sonnet-4-6`, `claude-opus-4-6` |
|
|
705
|
+
| `gpt-*` | OpenAI | `AI_API_KEY` | `gpt-4o`, `gpt-4o-mini`, `o3` |
|
|
706
|
+
| `MiniMax-*` | MiniMax | `AI_API_KEY` | `MiniMax-Text-01` |
|
|
707
|
+
| `qwen-*` | Qwen (阿里云) | `QWEN_API_KEY` | `qwen-turbo`, `qwen-plus`, `qwen-max` |
|
|
708
|
+
| `glm-*` | 智谱 | `ZHIPU_API_KEY` | `glm-4`, `glm-4-flash` |
|
|
709
|
+
| `deepseek-*` | DeepSeek | `DEEPSEEK_API_KEY` | `deepseek-chat`, `deepseek-coder` |
|
|
710
|
+
| `doubao-*` | 豆包 (ByteDance) | `DOUBAO_API_KEY` | `doubao-pro-4k` |
|
|
711
|
+
| `moonshot-*` | Kimi (Moonshot) | `KIMI_API_KEY` | `moonshot-v1-8k`, `moonshot-v1-32k` |
|
|
712
|
+
| `grok-*` | Grok (xAI) | `GROK_API_KEY` | `grok-2`, `grok-beta` |
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
Also find and replace the `AI_API_KEY` usage description line:
|
|
716
|
+
|
|
717
|
+
```markdown
|
|
718
|
+
// FIND:
|
|
719
|
+
- `AI_API_KEY` — your Anthropic, OpenAI, or MiniMax API key (repository secret)
|
|
720
|
+
|
|
721
|
+
// REPLACE WITH:
|
|
722
|
+
- `AI_API_KEY` — your Anthropic, OpenAI, or MiniMax API key (for Qwen/智谱/DeepSeek/豆包/Kimi/Grok use their respective `*_API_KEY` vars — see Supported models below)
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
- [ ] **Step 2: Apply the same table update to `README_zh.md`**
|
|
726
|
+
|
|
727
|
+
Find and replace in `README_zh.md`:
|
|
728
|
+
|
|
729
|
+
```markdown
|
|
730
|
+
// FIND:
|
|
731
|
+
| 前缀 | 提供商 | 示例模型 |
|
|
732
|
+
|------|--------|---------|
|
|
733
|
+
| `claude-*` | Anthropic | `claude-sonnet-4-6`、`claude-opus-4-6`、`claude-haiku-4-5-20251001` |
|
|
734
|
+
| `gpt-*` | OpenAI | `gpt-4o`、`gpt-4o-mini`、`o3` |
|
|
735
|
+
| `MiniMax-*` | MiniMax(Anthropic 兼容) | `MiniMax-Text-01` |
|
|
736
|
+
|
|
737
|
+
// REPLACE WITH:
|
|
738
|
+
| 前缀 | 供应商 | API 密钥环境变量 | 示例模型 |
|
|
739
|
+
|------|--------|-----------------|---------|
|
|
740
|
+
| `claude-*` | Anthropic | `AI_API_KEY` | `claude-sonnet-4-6`、`claude-opus-4-6` |
|
|
741
|
+
| `gpt-*` | OpenAI | `AI_API_KEY` | `gpt-4o`、`gpt-4o-mini`、`o3` |
|
|
742
|
+
| `MiniMax-*` | MiniMax | `AI_API_KEY` | `MiniMax-Text-01` |
|
|
743
|
+
| `qwen-*` | 通义千问(阿里云) | `QWEN_API_KEY` | `qwen-turbo`、`qwen-plus`、`qwen-max` |
|
|
744
|
+
| `glm-*` | 智谱 AI | `ZHIPU_API_KEY` | `glm-4`、`glm-4-flash` |
|
|
745
|
+
| `deepseek-*` | DeepSeek | `DEEPSEEK_API_KEY` | `deepseek-chat`、`deepseek-coder` |
|
|
746
|
+
| `doubao-*` | 豆包(字节跳动) | `DOUBAO_API_KEY` | `doubao-pro-4k` |
|
|
747
|
+
| `moonshot-*` | Kimi(月之暗面) | `KIMI_API_KEY` | `moonshot-v1-8k`、`moonshot-v1-32k` |
|
|
748
|
+
| `grok-*` | Grok(xAI) | `GROK_API_KEY` | `grok-2`、`grok-beta` |
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
Also find and replace the `AI_API_KEY` description line:
|
|
752
|
+
|
|
753
|
+
```markdown
|
|
754
|
+
// FIND:
|
|
755
|
+
- `AI_API_KEY` — 你的 Anthropic、OpenAI 或 MiniMax API 密钥(repository secret)
|
|
756
|
+
|
|
757
|
+
// REPLACE WITH:
|
|
758
|
+
- `AI_API_KEY` — 你的 Anthropic、OpenAI 或 MiniMax API 密钥(Qwen/智谱/DeepSeek/豆包/Kimi/Grok 请使用各自的 `*_API_KEY` 环境变量,详见下方支持的模型表格)
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
- [ ] **Step 3: Apply the same table update to `README_ja.md`**
|
|
762
|
+
|
|
763
|
+
Find and replace in `README_ja.md`:
|
|
764
|
+
|
|
765
|
+
```markdown
|
|
766
|
+
// FIND:
|
|
767
|
+
| プレフィックス | プロバイダー | 対応モデル |
|
|
768
|
+
|--------------|-------------|-----------|
|
|
769
|
+
| `claude-*` | Anthropic | `claude-sonnet-4-6`、`claude-opus-4-6`、`claude-haiku-4-5-20251001` |
|
|
770
|
+
| `gpt-*` | OpenAI | `gpt-4o`、`gpt-4o-mini`、`o3` |
|
|
771
|
+
| `MiniMax-*` | MiniMax(Anthropic 互換) | `MiniMax-Text-01` |
|
|
772
|
+
|
|
773
|
+
// REPLACE WITH:
|
|
774
|
+
| プレフィックス | プロバイダー | API キー環境変数 | モデル例 |
|
|
775
|
+
|--------------|------------|----------------|---------|
|
|
776
|
+
| `claude-*` | Anthropic | `AI_API_KEY` | `claude-sonnet-4-6`、`claude-opus-4-6` |
|
|
777
|
+
| `gpt-*` | OpenAI | `AI_API_KEY` | `gpt-4o`、`gpt-4o-mini`、`o3` |
|
|
778
|
+
| `MiniMax-*` | MiniMax | `AI_API_KEY` | `MiniMax-Text-01` |
|
|
779
|
+
| `qwen-*` | Qwen(阿里云) | `QWEN_API_KEY` | `qwen-turbo`、`qwen-plus`、`qwen-max` |
|
|
780
|
+
| `glm-*` | 智谱 AI | `ZHIPU_API_KEY` | `glm-4`、`glm-4-flash` |
|
|
781
|
+
| `deepseek-*` | DeepSeek | `DEEPSEEK_API_KEY` | `deepseek-chat`、`deepseek-coder` |
|
|
782
|
+
| `doubao-*` | 豆包(ByteDance) | `DOUBAO_API_KEY` | `doubao-pro-4k` |
|
|
783
|
+
| `moonshot-*` | Kimi(Moonshot) | `KIMI_API_KEY` | `moonshot-v1-8k`、`moonshot-v1-32k` |
|
|
784
|
+
| `grok-*` | Grok(xAI) | `GROK_API_KEY` | `grok-2`、`grok-beta` |
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
Also find and replace the `AI_API_KEY` description line:
|
|
788
|
+
|
|
789
|
+
```markdown
|
|
790
|
+
// FIND:
|
|
791
|
+
- `AI_API_KEY` — Anthropic、OpenAI、または MiniMax の API キー(repository secret)
|
|
792
|
+
|
|
793
|
+
// REPLACE WITH:
|
|
794
|
+
- `AI_API_KEY` — Anthropic、OpenAI、または MiniMax の API キー(Qwen/智谱/DeepSeek/豆包/Kimi/Grok は各自の `*_API_KEY` 環境変数を使用してください — 詳細はサポートモデル表を参照)
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
- [ ] **Step 4: Commit**
|
|
798
|
+
|
|
799
|
+
```bash
|
|
800
|
+
git add README.md README_zh.md README_ja.md
|
|
801
|
+
git commit -m "docs: add Qwen, Zhipu, DeepSeek, Doubao, Kimi, Grok to supported models"
|
|
802
|
+
```
|