thatgfsj-code 0.3.0 → 0.4.1

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 (60) hide show
  1. package/dist/cmd/index.js +56 -8
  2. package/dist/cmd/index.js.map +1 -1
  3. package/dist/config/index.d.ts +5 -1
  4. package/dist/config/index.d.ts.map +1 -1
  5. package/dist/config/index.js +22 -4
  6. package/dist/config/index.js.map +1 -1
  7. package/dist/config/providers.d.ts +5 -10
  8. package/dist/config/providers.d.ts.map +1 -1
  9. package/dist/config/providers.js +152 -78
  10. package/dist/config/providers.js.map +1 -1
  11. package/dist/config/types.d.ts +1 -1
  12. package/dist/config/types.d.ts.map +1 -1
  13. package/dist/llm/anthropic.d.ts +24 -6
  14. package/dist/llm/anthropic.d.ts.map +1 -1
  15. package/dist/llm/anthropic.js +136 -46
  16. package/dist/llm/anthropic.js.map +1 -1
  17. package/dist/llm/gemini.d.ts +4 -4
  18. package/dist/llm/gemini.d.ts.map +1 -1
  19. package/dist/llm/gemini.js +67 -30
  20. package/dist/llm/gemini.js.map +1 -1
  21. package/dist/llm/index.d.ts +10 -23
  22. package/dist/llm/index.d.ts.map +1 -1
  23. package/dist/llm/index.js +73 -78
  24. package/dist/llm/index.js.map +1 -1
  25. package/dist/llm/openai.d.ts +19 -6
  26. package/dist/llm/openai.d.ts.map +1 -1
  27. package/dist/llm/openai.js +86 -35
  28. package/dist/llm/openai.js.map +1 -1
  29. package/dist/llm/provider.d.ts +11 -16
  30. package/dist/llm/provider.d.ts.map +1 -1
  31. package/dist/tui/input.d.ts +0 -1
  32. package/dist/tui/input.d.ts.map +1 -1
  33. package/dist/tui/input.js +2 -5
  34. package/dist/tui/input.js.map +1 -1
  35. package/dist/tui/output.d.ts +27 -12
  36. package/dist/tui/output.d.ts.map +1 -1
  37. package/dist/tui/output.js +176 -40
  38. package/dist/tui/output.js.map +1 -1
  39. package/dist/tui/repl.d.ts +0 -10
  40. package/dist/tui/repl.d.ts.map +1 -1
  41. package/dist/tui/repl.js +76 -42
  42. package/dist/tui/repl.js.map +1 -1
  43. package/dist/tui/welcome.d.ts +2 -8
  44. package/dist/tui/welcome.d.ts.map +1 -1
  45. package/dist/tui/welcome.js +101 -51
  46. package/dist/tui/welcome.js.map +1 -1
  47. package/package.json +1 -1
  48. package/src/cmd/index.ts +49 -8
  49. package/src/config/index.ts +21 -4
  50. package/src/config/providers.ts +153 -79
  51. package/src/config/types.ts +11 -5
  52. package/src/llm/anthropic.ts +148 -51
  53. package/src/llm/gemini.ts +81 -39
  54. package/src/llm/index.ts +75 -83
  55. package/src/llm/openai.ts +96 -41
  56. package/src/llm/provider.ts +12 -16
  57. package/src/tui/input.ts +2 -5
  58. package/src/tui/output.ts +193 -40
  59. package/src/tui/repl.ts +82 -43
  60. package/src/tui/welcome.ts +110 -53
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Provider definitions and model catalogs
3
- * Consolidated from old types.ts PROVIDERS + welcome.ts model lists
3
+ * Updated with correct API formats and more providers
4
4
  */
5
5
 
6
6
  import type { ProviderConfig, ProviderName, ModelInfo } from './types.js';
@@ -12,16 +12,9 @@ export const PROVIDERS: Record<ProviderName, ProviderConfig> = {
12
12
  name: 'SiliconFlow (硅基流动)',
13
13
  baseUrl: 'https://api.siliconflow.cn/v1',
14
14
  defaultModel: 'Qwen/Qwen2.5-7B-Instruct',
15
- envKeys: ['SILICONFLOW_API_KEY', 'OPENAI_API_KEY'],
15
+ envKeys: ['SILICONFLOW_API_KEY'],
16
16
  format: 'openai',
17
17
  },
18
- minimax: {
19
- name: 'MiniMax',
20
- baseUrl: 'https://api.minimax.io/anthropic/v1',
21
- defaultModel: 'MiniMax-M2.5',
22
- envKeys: ['MINIMAX_API_KEY', 'OPENAI_API_KEY'],
23
- format: 'anthropic',
24
- },
25
18
  openai: {
26
19
  name: 'OpenAI',
27
20
  baseUrl: 'https://api.openai.com/v1',
@@ -29,132 +22,213 @@ export const PROVIDERS: Record<ProviderName, ProviderConfig> = {
29
22
  envKeys: ['OPENAI_API_KEY'],
30
23
  format: 'openai',
31
24
  },
25
+ deepseek: {
26
+ name: 'DeepSeek (深度求索)',
27
+ baseUrl: 'https://api.deepseek.com',
28
+ defaultModel: 'deepseek-chat',
29
+ envKeys: ['DEEPSEEK_API_KEY'],
30
+ format: 'openai',
31
+ },
32
+ kimi: {
33
+ name: 'Kimi (月之暗面)',
34
+ baseUrl: 'https://api.moonshot.cn/v1',
35
+ defaultModel: 'kimi-k2.6',
36
+ envKeys: ['KIMI_API_KEY', 'MOONSHOT_API_KEY'],
37
+ format: 'openai',
38
+ },
39
+ zhipu: {
40
+ name: 'Zhipu GLM (智谱)',
41
+ baseUrl: 'https://open.bigmodel.cn/api/paas/v4',
42
+ defaultModel: 'glm-4-flash',
43
+ envKeys: ['ZHIPU_API_KEY', 'GLM_API_KEY'],
44
+ format: 'openai',
45
+ },
46
+ minimax: {
47
+ name: 'MiniMax',
48
+ baseUrl: 'https://api.minimax.chat/v1',
49
+ defaultModel: 'MiniMax-Text-01',
50
+ envKeys: ['MINIMAX_API_KEY'],
51
+ format: 'openai',
52
+ },
53
+ baichuan: {
54
+ name: 'Baichuan (百川)',
55
+ baseUrl: 'https://api.baichuan-ai.com/v1',
56
+ defaultModel: 'Baichuan4',
57
+ envKeys: ['BAICHUAN_API_KEY'],
58
+ format: 'openai',
59
+ },
60
+ stepfun: {
61
+ name: 'Stepfun (阶跃星辰)',
62
+ baseUrl: 'https://api.stepfun.com/v1',
63
+ defaultModel: 'step-1-flash',
64
+ envKeys: ['STEPFUN_API_KEY'],
65
+ format: 'openai',
66
+ },
67
+ doubao: {
68
+ name: 'Doubao (火山引擎豆包)',
69
+ baseUrl: 'https://ark.cn-beijing.volces.com/api/v3',
70
+ defaultModel: 'doubao-1.5-pro-32k',
71
+ envKeys: ['DOUBAO_API_KEY', 'ARK_API_KEY'],
72
+ format: 'openai',
73
+ },
32
74
  anthropic: {
33
- name: 'Anthropic',
75
+ name: 'Anthropic (Claude)',
34
76
  baseUrl: 'https://api.anthropic.com/v1',
35
- defaultModel: 'claude-3-haiku-20240307',
77
+ defaultModel: 'claude-sonnet-4-20250514',
36
78
  envKeys: ['ANTHROPIC_API_KEY'],
37
79
  format: 'anthropic',
38
80
  },
39
- ollama: {
40
- name: 'Ollama (Local)',
41
- baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434',
42
- defaultModel: 'llama2',
43
- envKeys: [],
44
- format: 'openai',
45
- },
46
81
  gemini: {
47
82
  name: 'Google Gemini',
48
83
  baseUrl: 'https://generativelanguage.googleapis.com/v1beta',
49
- defaultModel: 'gemini-1.5-flash',
84
+ defaultModel: 'gemini-2.0-flash',
50
85
  envKeys: ['GEMINI_API_KEY'],
51
86
  format: 'gemini',
52
87
  },
53
- kimi: {
54
- name: 'Kimi (Moonshot AI)',
55
- baseUrl: 'https://api.moonshot.cn/v1',
56
- defaultModel: 'moonshot-v1-8k',
57
- envKeys: ['KIMI_API_KEY', 'MOONSHOT_API_KEY'],
88
+ ollama: {
89
+ name: 'Ollama (本地)',
90
+ baseUrl: 'http://localhost:11434/v1',
91
+ defaultModel: 'llama3.1',
92
+ envKeys: [],
58
93
  format: 'openai',
59
94
  },
60
- deepseek: {
61
- name: 'DeepSeek',
62
- baseUrl: 'https://api.deepseek.com/v1',
63
- defaultModel: 'deepseek-chat',
64
- envKeys: ['DEEPSEEK_API_KEY'],
95
+ ernie: {
96
+ name: 'ERNIE (百度文心)',
97
+ baseUrl: 'https://qianfan.baidubce.com/v2',
98
+ defaultModel: 'ernie-4.5-8k',
99
+ envKeys: ['ERNIE_API_KEY', 'QIANFAN_API_KEY'],
65
100
  format: 'openai',
66
101
  },
67
- ernie: {
68
- name: 'Baidu ERNIE (文心一言)',
69
- baseUrl: 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1',
70
- defaultModel: 'ernie-4.0-8k',
71
- envKeys: ['ERNIE_API_KEY', 'BAIDU_API_KEY'],
102
+ custom_openai: {
103
+ name: '自定义 OpenAI 兼容 (中转站)',
104
+ baseUrl: '',
105
+ defaultModel: 'gpt-4o-mini',
106
+ envKeys: ['CUSTOM_API_KEY'],
72
107
  format: 'openai',
73
108
  },
109
+ custom_anthropic: {
110
+ name: '自定义 Anthropic 兼容 (中转站)',
111
+ baseUrl: '',
112
+ defaultModel: 'claude-sonnet-4-20250514',
113
+ envKeys: ['CUSTOM_API_KEY'],
114
+ format: 'anthropic',
115
+ },
74
116
  };
75
117
 
76
118
  // ==================== Model Catalogs ====================
77
119
 
78
120
  export const MODEL_CATALOGS: Record<ProviderName, ModelInfo[]> = {
79
121
  siliconflow: [
80
- { id: 'Qwen/Qwen2.5-7B-Instruct', name: 'Qwen2.5-7B (推荐)', desc: '性价比高,适合日常编程' },
81
- { id: 'Qwen/Qwen2.5-32B-Instruct', name: 'Qwen2.5-32B', desc: '更强性能,适合复杂任务' },
122
+ { id: 'Qwen/Qwen2.5-7B-Instruct', name: 'Qwen2.5-7B', desc: '免费,日常编程' },
123
+ { id: 'Qwen/Qwen2.5-32B-Instruct', name: 'Qwen2.5-32B', desc: '更强性能' },
82
124
  { id: 'Qwen/Qwen2.5-72B-Instruct', name: 'Qwen2.5-72B', desc: '旗舰模型' },
83
- { id: 'Pro/moonshotai/Kimi-K2.5', name: 'Kimi-K2.5 (私有)', desc: 'Moonshot 强力模型' },
84
- { id: 'Pro/deepseek-ai/DeepSeek-V3', name: 'DeepSeek-V3', desc: '深度求索最新模型' },
85
- { id: 'Pro/deepseek-ai/DeepSeek-R1', name: 'DeepSeek-R1', desc: '推理能力强' },
125
+ { id: 'Qwen/Qwen3-8B', name: 'Qwen3-8B', desc: '最新 Qwen3' },
126
+ { id: 'Pro/deepseek-ai/DeepSeek-V3', name: 'DeepSeek-V3', desc: '深度求索' },
127
+ { id: 'Pro/deepseek-ai/DeepSeek-R1', name: 'DeepSeek-R1', desc: '推理模型' },
128
+ { id: 'deepseek-ai/DeepSeek-V3', name: 'DeepSeek-V3 (免费)', desc: '免费版' },
86
129
  { id: 'THUDM/glm-4-9b-chat', name: 'GLM-4-9B', desc: '智谱模型' },
87
- { id: 'THUDM/glm-4-32b-chat', name: 'GLM-4-32B', desc: '智谱大模型' },
88
- { id: '01-ai/Yi-1.5-34B-Chat', name: 'Yi-1.5-34B', desc: '零一万物' },
89
- { id: 'microsoft/WizardLM-2-8x22B', name: 'WizardLM-2', desc: '微软开源' },
90
- ],
91
- minimax: [
92
- { id: 'MiniMax-M2.5', name: 'MiniMax-M2.5 (推荐)', desc: 'Agent 能力最强' },
93
- { id: 'MiniMax-M2.1', name: 'MiniMax-M2.1', desc: '稳定版本' },
130
+ { id: 'internlm/internlm2_5-7b-chat', name: 'InternLM2.5-7B', desc: '书生模型' },
94
131
  ],
95
132
  openai: [
96
- { id: 'gpt-4o-mini', name: 'GPT-4o-mini (推荐)', desc: '性价比高' },
97
- { id: 'gpt-4o', name: 'GPT-4o', desc: '最新旗舰' },
133
+ { id: 'gpt-4o-mini', name: 'GPT-4o-mini', desc: '性价比高' },
134
+ { id: 'gpt-4o', name: 'GPT-4o', desc: '旗舰多模态' },
98
135
  { id: 'gpt-4-turbo', name: 'GPT-4-Turbo', desc: '强性能' },
136
+ { id: 'o3-mini', name: 'o3-mini', desc: '推理模型' },
99
137
  ],
100
- anthropic: [
101
- { id: 'claude-3-haiku-20240307', name: 'Claude-3-Haiku (推荐)', desc: '快速响应' },
102
- { id: 'claude-3.5-sonnet-20241022', name: 'Claude-3.5-Sonnet', desc: '最新旗舰' },
103
- { id: 'claude-3-opus-20240229', name: 'Claude-3-Opus', desc: '最强性能' },
138
+ deepseek: [
139
+ { id: 'deepseek-chat', name: 'DeepSeek-Chat', desc: '通用对话' },
140
+ { id: 'deepseek-reasoner', name: 'DeepSeek-Reasoner', desc: '推理增强' },
104
141
  ],
105
- ollama: [
106
- { id: 'llama2', name: 'Llama 2', desc: 'Meta 开源模型' },
107
- { id: 'codellama', name: 'Code Llama', desc: '编程专用' },
108
- { id: 'mistral', name: 'Mistral', desc: '高性能开源' },
142
+ kimi: [
143
+ { id: 'kimi-k2.6', name: 'Kimi K2.6', desc: '最新旗舰,支持思考' },
144
+ { id: 'kimi-k2.5', name: 'Kimi K2.5', desc: '稳定版' },
145
+ { id: 'moonshot-v1-128k', name: 'Moonshot-128K', desc: '超长上下文' },
146
+ { id: 'moonshot-v1-32k', name: 'Moonshot-32K', desc: '长上下文' },
147
+ { id: 'moonshot-v1-8k', name: 'Moonshot-8K', desc: '基础版' },
109
148
  ],
110
- gemini: [
111
- { id: 'gemini-1.5-flash-8b', name: 'Gemini-1.5-Flash-8B (推荐)', desc: '免费快速' },
112
- { id: 'gemini-1.5-flash', name: 'Gemini-1.5-Flash', desc: '性价比高' },
113
- { id: 'gemini-1.5-pro', name: 'Gemini-1.5-Pro', desc: '最新旗舰' },
149
+ zhipu: [
150
+ { id: 'glm-4-plus', name: 'GLM-4-Plus', desc: '旗舰模型' },
151
+ { id: 'glm-4-flash', name: 'GLM-4-Flash', desc: '快速免费' },
152
+ { id: 'glm-4-long', name: 'GLM-4-Long', desc: '超长上下文 1M' },
153
+ { id: 'glm-4-flashx', name: 'GLM-4-FlashX', desc: '加速版' },
154
+ { id: 'glm-4-air', name: 'GLM-4-Air', desc: '轻量版' },
114
155
  ],
115
- kimi: [
116
- { id: 'moonshot-v1-8k', name: 'Moonshot-V1-8K (推荐)', desc: '日常编程' },
117
- { id: 'moonshot-v1-32k', name: 'Moonshot-V1-32K', desc: '更长上下文' },
118
- { id: 'moonshot-v1-128k', name: 'Moonshot-V1-128K', desc: '超长上下文' },
156
+ minimax: [
157
+ { id: 'MiniMax-Text-01', name: 'MiniMax-Text-01', desc: '旗舰模型' },
158
+ { id: 'abab6.5s-chat', name: 'abab6.5s', desc: '轻量版' },
119
159
  ],
120
- deepseek: [
121
- { id: 'deepseek-chat', name: 'DeepSeek-Chat (推荐)', desc: '性价比高' },
122
- { id: 'deepseek-coder', name: 'DeepSeek-Coder', desc: '编程专用' },
160
+ baichuan: [
161
+ { id: 'Baichuan4', name: 'Baichuan4', desc: '最新旗舰' },
162
+ { id: 'Baichuan3-Turbo', name: 'Baichuan3-Turbo', desc: '快速版' },
163
+ ],
164
+ stepfun: [
165
+ { id: 'step-1-flash', name: 'Step-1-Flash', desc: '快速免费' },
166
+ { id: 'step-1-8k', name: 'Step-1-8K', desc: '基础版' },
167
+ { id: 'step-2-16k', name: 'Step-2-16K', desc: '增强版' },
168
+ ],
169
+ doubao: [
170
+ { id: 'doubao-1.5-pro-32k', name: 'Doubao-1.5-Pro-32K', desc: '旗舰版' },
171
+ { id: 'doubao-1.5-lite-32k', name: 'Doubao-1.5-Lite-32K', desc: '轻量版' },
172
+ { id: 'doubao-pro-256k', name: 'Doubao-Pro-256K', desc: '超长上下文' },
173
+ ],
174
+ anthropic: [
175
+ { id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4', desc: '最新旗舰' },
176
+ { id: 'claude-3-5-haiku-20241022', name: 'Claude 3.5 Haiku', desc: '快速便宜' },
177
+ { id: 'claude-3-opus-20240229', name: 'Claude 3 Opus', desc: '最强性能' },
178
+ ],
179
+ gemini: [
180
+ { id: 'gemini-2.0-flash', name: 'Gemini 2.0 Flash', desc: '最新快速' },
181
+ { id: 'gemini-2.5-pro-preview-05-06', name: 'Gemini 2.5 Pro', desc: '最强推理' },
182
+ { id: 'gemini-1.5-flash', name: 'Gemini 1.5 Flash', desc: '经典版' },
183
+ ],
184
+ ollama: [
185
+ { id: 'llama3.1', name: 'Llama 3.1', desc: 'Meta 开源' },
186
+ { id: 'qwen2.5', name: 'Qwen 2.5', desc: '通义千问' },
187
+ { id: 'deepseek-coder-v2', name: 'DeepSeek Coder V2', desc: '编程专用' },
188
+ { id: 'codellama', name: 'Code Llama', desc: 'Meta 编程' },
189
+ { id: 'mistral', name: 'Mistral', desc: '高性能' },
123
190
  ],
124
191
  ernie: [
125
- { id: 'ernie-4.0-8k', name: 'ERNIE-4.0-8K (推荐)', desc: '百度旗舰' },
192
+ { id: 'ernie-4.5-8k', name: 'ERNIE-4.5-8K', desc: '最新旗舰' },
193
+ { id: 'ernie-4.0-8k', name: 'ERNIE-4.0-8K', desc: '强性能' },
126
194
  { id: 'ernie-3.5-8k', name: 'ERNIE-3.5-8K', desc: '性价比高' },
127
195
  ],
196
+ custom_openai: [
197
+ { id: 'gpt-4o-mini', name: 'GPT-4o-mini', desc: '默认模型,可自定义' },
198
+ { id: 'gpt-4o', name: 'GPT-4o', desc: '可自定义' },
199
+ { id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4', desc: '可自定义' },
200
+ ],
201
+ custom_anthropic: [
202
+ { id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4', desc: '默认模型,可自定义' },
203
+ { id: 'claude-3-5-haiku-20241022', name: 'Claude 3.5 Haiku', desc: '可自定义' },
204
+ ],
128
205
  };
129
206
 
130
207
  // ==================== Helpers ====================
131
208
 
132
- /**
133
- * Get models for a specific provider
134
- */
135
209
  export function getModelsForProvider(provider: ProviderName): ModelInfo[] {
136
- return MODEL_CATALOGS[provider] || MODEL_CATALOGS.siliconflow;
210
+ return MODEL_CATALOGS[provider] || [];
137
211
  }
138
212
 
139
- /**
140
- * Get API key for a provider from environment variables
141
- */
142
213
  export function getApiKeyFromEnv(provider: ProviderName): string {
143
214
  const providerConfig = PROVIDERS[provider];
144
215
  if (!providerConfig) return '';
145
-
146
216
  for (const envKey of providerConfig.envKeys) {
147
217
  if (process.env[envKey]) return process.env[envKey];
148
218
  }
149
219
  return '';
150
220
  }
151
221
 
152
- /**
153
- * List all provider names
154
- */
155
222
  export function listProviders(): Array<{ key: ProviderName; name: string }> {
156
223
  return Object.entries(PROVIDERS).map(([key, val]) => ({
157
224
  key: key as ProviderName,
158
225
  name: val.name,
159
226
  }));
160
227
  }
228
+
229
+ /**
230
+ * Check if a provider name is a custom/relay provider
231
+ */
232
+ export function isCustomProvider(provider: ProviderName): boolean {
233
+ return provider === 'custom_openai' || provider === 'custom_anthropic';
234
+ }
@@ -4,14 +4,20 @@
4
4
 
5
5
  export type ProviderName =
6
6
  | 'siliconflow'
7
- | 'minimax'
8
7
  | 'openai'
8
+ | 'deepseek'
9
+ | 'kimi'
10
+ | 'zhipu'
11
+ | 'minimax'
12
+ | 'baichuan'
13
+ | 'stepfun'
14
+ | 'doubao'
9
15
  | 'anthropic'
10
- | 'ollama'
11
16
  | 'gemini'
12
- | 'kimi'
13
- | 'deepseek'
14
- | 'ernie';
17
+ | 'ollama'
18
+ | 'ernie'
19
+ | 'custom_openai'
20
+ | 'custom_anthropic';
15
21
 
16
22
  export interface ProviderConfig {
17
23
  name: string;
@@ -1,15 +1,26 @@
1
1
  /**
2
2
  * Anthropic Provider
3
- * Works with: Anthropic Claude, MiniMax (Anthropic mode)
3
+ * Works with: Anthropic Claude API
4
+ * Also works with any Anthropic-compatible relay station (中转站)
5
+ *
6
+ * API: POST {baseUrl}/messages
7
+ * Auth: x-api-key: {apiKey}, anthropic-version: 2023-06-01
8
+ * Streaming: SSE with "event: ..." and "data: {...}" lines
9
+ *
10
+ * Key differences from OpenAI:
11
+ * - System message is separate (top-level "system" field)
12
+ * - No "system" role in messages array
13
+ * - Tool use blocks have type "tool_use" with "input" (not "arguments")
14
+ * - Tool results use role "tool_result" (not "tool")
4
15
  */
5
16
 
6
- import type { ChatMessage, ChatResponse, ChatOptions } from '../types.js';
17
+ import type { ChatMessage, ChatResponse, ChatOptions, ToolCall } from '../types.js';
7
18
  import type { Tool } from '../tools/types.js';
8
- import type { LLMProvider, ProviderConfig } from './provider.js';
19
+ import type { LLMProvider, StreamChunk, ProviderConfig } from './provider.js';
9
20
 
10
21
  export class AnthropicProvider implements LLMProvider {
11
22
  readonly name = 'anthropic';
12
- private config: ProviderConfig;
23
+ protected config: ProviderConfig;
13
24
 
14
25
  constructor(config: ProviderConfig) {
15
26
  this.config = config;
@@ -29,18 +40,9 @@ export class AnthropicProvider implements LLMProvider {
29
40
  }));
30
41
  }
31
42
 
32
- async chat(messages: ChatMessage[], options?: ChatOptions): Promise<ChatResponse> {
33
- const body = this.buildRequest(messages, false, options);
34
-
35
- const response = await fetch(`${this.config.baseUrl}/messages`, {
36
- method: 'POST',
37
- headers: {
38
- 'Content-Type': 'application/json',
39
- 'x-api-key': this.config.apiKey,
40
- 'anthropic-version': '2023-06-01',
41
- },
42
- body: JSON.stringify(body),
43
- });
43
+ async chat(messages: ChatMessage[], options?: ChatOptions, tools?: Tool[]): Promise<ChatResponse> {
44
+ const body = this.buildRequest(messages, false, options, tools);
45
+ const response = await this.doRequest(body);
44
46
 
45
47
  if (!response.ok) {
46
48
  const text = await response.text().catch(() => '');
@@ -48,41 +50,37 @@ export class AnthropicProvider implements LLMProvider {
48
50
  }
49
51
 
50
52
  const data = await response.json();
51
- const contentBlock = data.content?.find((b: any) => b.type === 'text');
53
+
54
+ // Extract text content
55
+ const textBlocks = data.content?.filter((b: any) => b.type === 'text') || [];
56
+ const content = textBlocks.map((b: any) => b.text).join('');
57
+
58
+ // Extract tool calls
52
59
  const toolUseBlocks = data.content?.filter((b: any) => b.type === 'tool_use') || [];
60
+ const toolCalls = toolUseBlocks.length > 0 ? toolUseBlocks.map((b: any) => ({
61
+ id: b.id,
62
+ type: 'function' as const,
63
+ function: {
64
+ name: b.name,
65
+ arguments: JSON.stringify(b.input),
66
+ },
67
+ })) : undefined;
53
68
 
54
69
  return {
55
- content: contentBlock?.text || '',
70
+ content,
56
71
  role: 'assistant',
57
72
  usage: data.usage ? {
58
73
  prompt_tokens: data.usage.input_tokens || 0,
59
74
  completion_tokens: data.usage.output_tokens || 0,
60
75
  total_tokens: (data.usage.input_tokens || 0) + (data.usage.output_tokens || 0),
61
76
  } : undefined,
62
- tool_calls: toolUseBlocks.length > 0 ? toolUseBlocks.map((b: any) => ({
63
- id: b.id,
64
- type: 'function' as const,
65
- function: {
66
- name: b.name,
67
- arguments: JSON.stringify(b.input),
68
- },
69
- })) : undefined,
77
+ tool_calls: toolCalls,
70
78
  };
71
79
  }
72
80
 
73
- async *chatStream(messages: ChatMessage[], options?: ChatOptions): AsyncGenerator<string, ChatResponse> {
74
- const body = this.buildRequest(messages, true, options);
75
-
76
- const response = await fetch(`${this.config.baseUrl}/messages`, {
77
- method: 'POST',
78
- headers: {
79
- 'Content-Type': 'application/json',
80
- 'x-api-key': this.config.apiKey,
81
- 'anthropic-version': '2023-06-01',
82
- 'anthropic-dangerous-direct-browser-access': 'true',
83
- },
84
- body: JSON.stringify(body),
85
- });
81
+ async *chatStream(messages: ChatMessage[], options?: ChatOptions, tools?: Tool[]): AsyncGenerator<StreamChunk, ChatResponse> {
82
+ const body = this.buildRequest(messages, true, options, tools);
83
+ const response = await this.doRequest(body);
86
84
 
87
85
  if (!response.ok || !response.body) {
88
86
  const text = await response.text().catch(() => '');
@@ -93,6 +91,10 @@ export class AnthropicProvider implements LLMProvider {
93
91
  const decoder = new TextDecoder();
94
92
  let fullContent = '';
95
93
  let buffer = '';
94
+ // Track tool use blocks
95
+ const toolUseBlocks: Map<number, { id: string; name: string; input: string }> = new Map();
96
+ let currentBlockIndex = -1;
97
+ let currentBlockType = '';
96
98
 
97
99
  try {
98
100
  while (true) {
@@ -109,10 +111,32 @@ export class AnthropicProvider implements LLMProvider {
109
111
 
110
112
  try {
111
113
  const data = JSON.parse(trimmed.slice(6));
112
- if (data.type === 'content_block_delta' && data.delta?.type === 'text_delta') {
113
- const text = data.delta.text;
114
- fullContent += text;
115
- yield text;
114
+
115
+ // content_block_start: track block types
116
+ if (data.type === 'content_block_start') {
117
+ currentBlockIndex = data.index ?? 0;
118
+ currentBlockType = data.content_block?.type || '';
119
+ if (currentBlockType === 'tool_use') {
120
+ toolUseBlocks.set(currentBlockIndex, {
121
+ id: data.content_block.id || '',
122
+ name: data.content_block.name || '',
123
+ input: '',
124
+ });
125
+ }
126
+ }
127
+
128
+ // content_block_delta: incremental text or tool input
129
+ if (data.type === 'content_block_delta') {
130
+ if (data.delta?.type === 'text_delta') {
131
+ const text = data.delta.text;
132
+ fullContent += text;
133
+ yield { type: 'text', content: text };
134
+ } else if (data.delta?.type === 'input_json_delta') {
135
+ const buf = toolUseBlocks.get(currentBlockIndex);
136
+ if (buf) {
137
+ buf.input += data.delta.partial_json || '';
138
+ }
139
+ }
116
140
  }
117
141
  } catch {
118
142
  // Skip invalid JSON
@@ -123,24 +147,97 @@ export class AnthropicProvider implements LLMProvider {
123
147
  reader.releaseLock();
124
148
  }
125
149
 
126
- return { content: fullContent, role: 'assistant' };
150
+ // Convert tool use blocks to ToolCall[]
151
+ const toolCalls: ToolCall[] = [];
152
+ for (const [, buf] of toolUseBlocks) {
153
+ if (buf.id && buf.name) {
154
+ toolCalls.push({
155
+ id: buf.id,
156
+ type: 'function',
157
+ function: { name: buf.name, arguments: buf.input },
158
+ });
159
+ }
160
+ }
161
+
162
+ if (toolCalls.length > 0) {
163
+ yield { type: 'tool_calls', toolCalls };
164
+ }
165
+
166
+ return { content: fullContent, role: 'assistant', tool_calls: toolCalls.length > 0 ? toolCalls : undefined };
127
167
  }
128
168
 
129
- private buildRequest(messages: ChatMessage[], stream: boolean, options?: ChatOptions) {
169
+ /**
170
+ * Build request body for Anthropic API
171
+ */
172
+ protected buildRequest(messages: ChatMessage[], stream: boolean, options?: ChatOptions, tools?: Tool[]) {
130
173
  // Anthropic uses separate system message
131
174
  const systemMsg = messages.find(m => m.role === 'system');
132
175
  const nonSystemMsgs = messages.filter(m => m.role !== 'system');
133
176
 
134
- return {
177
+ // Convert messages to Anthropic format
178
+ const anthropicMessages = nonSystemMsgs.map(m => {
179
+ if (m.role === 'tool') {
180
+ // Tool result message
181
+ return {
182
+ role: 'user',
183
+ content: [{
184
+ type: 'tool_result',
185
+ tool_use_id: m.tool_call_id,
186
+ content: m.content,
187
+ }],
188
+ };
189
+ }
190
+ if (m.tool_calls && m.tool_calls.length > 0) {
191
+ // Assistant message with tool calls
192
+ const blocks: any[] = [];
193
+ if (m.content) {
194
+ blocks.push({ type: 'text', text: m.content });
195
+ }
196
+ for (const tc of m.tool_calls) {
197
+ blocks.push({
198
+ type: 'tool_use',
199
+ id: tc.id,
200
+ name: tc.function.name,
201
+ input: JSON.parse(tc.function.arguments || '{}'),
202
+ });
203
+ }
204
+ return { role: 'assistant', content: blocks };
205
+ }
206
+ return {
207
+ role: m.role === 'assistant' ? 'assistant' : 'user',
208
+ content: m.content,
209
+ };
210
+ });
211
+
212
+ const body: any = {
135
213
  model: this.config.model,
136
- max_tokens: options?.maxTokens ?? this.config.maxTokens,
214
+ max_tokens: options?.maxTokens ?? this.config.maxTokens ?? 4096,
137
215
  temperature: options?.temperature ?? this.config.temperature,
138
216
  ...(systemMsg && { system: systemMsg.content }),
139
- messages: nonSystemMsgs.map(m => ({
140
- role: m.role === 'assistant' ? 'assistant' : 'user',
141
- content: m.content,
142
- })),
217
+ messages: anthropicMessages,
143
218
  stream,
144
219
  };
220
+
221
+ if (tools && tools.length > 0) {
222
+ body.tools = this.buildTools(tools);
223
+ }
224
+
225
+ return body;
226
+ }
227
+
228
+ /**
229
+ * Execute the HTTP request
230
+ */
231
+ protected async doRequest(body: any): Promise<Response> {
232
+ const url = `${this.config.baseUrl}/messages`;
233
+ return fetch(url, {
234
+ method: 'POST',
235
+ headers: {
236
+ 'Content-Type': 'application/json',
237
+ 'x-api-key': this.config.apiKey,
238
+ 'anthropic-version': '2023-06-01',
239
+ },
240
+ body: JSON.stringify(body),
241
+ });
145
242
  }
146
243
  }