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.
@@ -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
+ ```