thatgfsj-code 3.0.4 → 3.0.7

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 (205) hide show
  1. package/README.md +64 -21
  2. package/dist/app/index.d.ts +93 -22
  3. package/dist/app/index.d.ts.map +1 -1
  4. package/dist/app/index.js +232 -40
  5. package/dist/app/index.js.map +1 -1
  6. package/dist/cmd/index.d.ts +12 -20
  7. package/dist/cmd/index.d.ts.map +1 -1
  8. package/dist/cmd/index.js +192 -146
  9. package/dist/cmd/index.js.map +1 -1
  10. package/dist/config/index.d.ts +4 -0
  11. package/dist/config/index.d.ts.map +1 -1
  12. package/dist/config/index.js +5 -0
  13. package/dist/config/index.js.map +1 -1
  14. package/dist/config/types.d.ts +5 -0
  15. package/dist/config/types.d.ts.map +1 -1
  16. package/dist/llm/anthropic.d.ts +1 -1
  17. package/dist/llm/anthropic.d.ts.map +1 -1
  18. package/dist/llm/anthropic.js +116 -84
  19. package/dist/llm/anthropic.js.map +1 -1
  20. package/dist/llm/gemini.d.ts +27 -3
  21. package/dist/llm/gemini.d.ts.map +1 -1
  22. package/dist/llm/gemini.js +196 -73
  23. package/dist/llm/gemini.js.map +1 -1
  24. package/dist/llm/index.d.ts +20 -1
  25. package/dist/llm/index.d.ts.map +1 -1
  26. package/dist/llm/index.js +66 -6
  27. package/dist/llm/index.js.map +1 -1
  28. package/dist/llm/openai.d.ts +5 -1
  29. package/dist/llm/openai.d.ts.map +1 -1
  30. package/dist/llm/openai.js +126 -66
  31. package/dist/llm/openai.js.map +1 -1
  32. package/dist/mcp/client.d.ts +84 -34
  33. package/dist/mcp/client.d.ts.map +1 -1
  34. package/dist/mcp/client.js +291 -126
  35. package/dist/mcp/client.js.map +1 -1
  36. package/dist/session/compactor.d.ts +27 -7
  37. package/dist/session/compactor.d.ts.map +1 -1
  38. package/dist/session/compactor.js +78 -23
  39. package/dist/session/compactor.js.map +1 -1
  40. package/dist/session/index.d.ts +96 -33
  41. package/dist/session/index.d.ts.map +1 -1
  42. package/dist/session/index.js +278 -60
  43. package/dist/session/index.js.map +1 -1
  44. package/dist/tools/file.d.ts +10 -2
  45. package/dist/tools/file.d.ts.map +1 -1
  46. package/dist/tools/file.js +54 -5
  47. package/dist/tools/file.js.map +1 -1
  48. package/dist/tools/git.d.ts +16 -2
  49. package/dist/tools/git.d.ts.map +1 -1
  50. package/dist/tools/git.js +61 -18
  51. package/dist/tools/git.js.map +1 -1
  52. package/dist/tools/nwt.d.ts.map +1 -1
  53. package/dist/tools/nwt.js +8 -1
  54. package/dist/tools/nwt.js.map +1 -1
  55. package/dist/tools/search.d.ts +8 -1
  56. package/dist/tools/search.d.ts.map +1 -1
  57. package/dist/tools/search.js +105 -34
  58. package/dist/tools/search.js.map +1 -1
  59. package/dist/tools/shell.d.ts +0 -4
  60. package/dist/tools/shell.d.ts.map +1 -1
  61. package/dist/tools/shell.js +11 -26
  62. package/dist/tools/shell.js.map +1 -1
  63. package/dist/tools/types.d.ts +7 -0
  64. package/dist/tools/types.d.ts.map +1 -1
  65. package/dist/tools/types.js.map +1 -1
  66. package/dist/tui/app.d.ts.map +1 -1
  67. package/dist/tui/app.js +90 -18
  68. package/dist/tui/app.js.map +1 -1
  69. package/dist/tui/components/ChatMessage.d.ts.map +1 -1
  70. package/dist/tui/components/ChatMessage.js +9 -2
  71. package/dist/tui/components/ChatMessage.js.map +1 -1
  72. package/dist/tui/components/ConfirmPrompt.d.ts +21 -0
  73. package/dist/tui/components/ConfirmPrompt.d.ts.map +1 -0
  74. package/dist/tui/components/ConfirmPrompt.js +45 -0
  75. package/dist/tui/components/ConfirmPrompt.js.map +1 -0
  76. package/dist/tui/components/Header.d.ts +10 -5
  77. package/dist/tui/components/Header.d.ts.map +1 -1
  78. package/dist/tui/components/Header.js +13 -5
  79. package/dist/tui/components/Header.js.map +1 -1
  80. package/dist/tui/components/StatusBar.d.ts +7 -0
  81. package/dist/tui/components/StatusBar.d.ts.map +1 -1
  82. package/dist/tui/components/StatusBar.js +8 -3
  83. package/dist/tui/components/StatusBar.js.map +1 -1
  84. package/dist/tui/components/Thinking.d.ts +5 -0
  85. package/dist/tui/components/Thinking.d.ts.map +1 -1
  86. package/dist/tui/components/Thinking.js +24 -2
  87. package/dist/tui/components/Thinking.js.map +1 -1
  88. package/dist/tui/components/ToolCall.d.ts +6 -1
  89. package/dist/tui/components/ToolCall.d.ts.map +1 -1
  90. package/dist/tui/components/ToolCall.js +50 -22
  91. package/dist/tui/components/ToolCall.js.map +1 -1
  92. package/dist/tui/components/UserInput.d.ts +5 -0
  93. package/dist/tui/components/UserInput.d.ts.map +1 -1
  94. package/dist/tui/components/UserInput.js +9 -13
  95. package/dist/tui/components/UserInput.js.map +1 -1
  96. package/dist/tui/hooks/useChat.d.ts +1 -0
  97. package/dist/tui/hooks/useChat.d.ts.map +1 -1
  98. package/dist/tui/hooks/useChat.js +36 -16
  99. package/dist/tui/hooks/useChat.js.map +1 -1
  100. package/dist/tui/hooks/useCommands.d.ts +7 -1
  101. package/dist/tui/hooks/useCommands.d.ts.map +1 -1
  102. package/dist/tui/hooks/useCommands.js +77 -41
  103. package/dist/tui/hooks/useCommands.js.map +1 -1
  104. package/dist/tui/theme.d.ts +34 -0
  105. package/dist/tui/theme.d.ts.map +1 -0
  106. package/dist/tui/theme.js +34 -0
  107. package/dist/tui/theme.js.map +1 -0
  108. package/dist/tui/welcome.d.ts.map +1 -1
  109. package/dist/tui/welcome.js +9 -7
  110. package/dist/tui/welcome.js.map +1 -1
  111. package/dist/types.d.ts +16 -1
  112. package/dist/types.d.ts.map +1 -1
  113. package/dist/utils/diff.d.ts +5 -0
  114. package/dist/utils/diff.d.ts.map +1 -1
  115. package/dist/utils/diff.js +15 -0
  116. package/dist/utils/diff.js.map +1 -1
  117. package/dist/version.d.ts +12 -0
  118. package/dist/version.d.ts.map +1 -0
  119. package/dist/version.js +29 -0
  120. package/dist/version.js.map +1 -0
  121. package/package.json +19 -7
  122. package/CHANGELOG.md +0 -192
  123. package/DEVELOPMENT.md +0 -286
  124. package/ROADMAP.md +0 -91
  125. package/docs/API_KEY_GUIDE.md +0 -236
  126. package/docs/FAQ.md +0 -182
  127. package/install.bat +0 -63
  128. package/install.ps1 +0 -238
  129. package/install.sh +0 -113
  130. package/src/app/index.ts +0 -197
  131. package/src/cache/fingerprint.ts +0 -101
  132. package/src/cache/index.ts +0 -21
  133. package/src/cache/smartModel.ts +0 -133
  134. package/src/cache/stats.ts +0 -199
  135. package/src/cache/volatile.ts +0 -47
  136. package/src/cmd/index.tsx +0 -288
  137. package/src/config/index.ts +0 -156
  138. package/src/config/providers.ts +0 -234
  139. package/src/config/types.ts +0 -71
  140. package/src/hooks/index.ts +0 -111
  141. package/src/llm/anthropic.ts +0 -413
  142. package/src/llm/gemini.ts +0 -169
  143. package/src/llm/index.ts +0 -303
  144. package/src/llm/openai.ts +0 -243
  145. package/src/llm/provider.ts +0 -71
  146. package/src/mcp/client.ts +0 -330
  147. package/src/prompts/index.ts +0 -260
  148. package/src/session/compactor.ts +0 -103
  149. package/src/session/index.ts +0 -181
  150. package/src/session/message.ts +0 -42
  151. package/src/skills/brainstorming.ts +0 -43
  152. package/src/skills/code-review.ts +0 -45
  153. package/src/skills/executing-plans.ts +0 -27
  154. package/src/skills/frontend-design.ts +0 -35
  155. package/src/skills/git-workflow.ts +0 -36
  156. package/src/skills/improve-architecture.ts +0 -38
  157. package/src/skills/index.ts +0 -136
  158. package/src/skills/neuroweave.ts +0 -47
  159. package/src/skills/playwright.ts +0 -72
  160. package/src/skills/prototype.ts +0 -30
  161. package/src/skills/subagent.ts +0 -28
  162. package/src/skills/supabase.ts +0 -44
  163. package/src/skills/systematic-debugging.ts +0 -44
  164. package/src/skills/tdd.ts +0 -39
  165. package/src/skills/triage.ts +0 -35
  166. package/src/skills/verification.ts +0 -31
  167. package/src/skills/writing-plans.ts +0 -42
  168. package/src/tools/file.ts +0 -143
  169. package/src/tools/git.ts +0 -132
  170. package/src/tools/index.ts +0 -111
  171. package/src/tools/nwt.ts +0 -598
  172. package/src/tools/search.ts +0 -263
  173. package/src/tools/shell.ts +0 -136
  174. package/src/tools/types.ts +0 -122
  175. package/src/tui/app.tsx +0 -159
  176. package/src/tui/components/ChatList.tsx +0 -41
  177. package/src/tui/components/ChatMessage.tsx +0 -54
  178. package/src/tui/components/Header.tsx +0 -66
  179. package/src/tui/components/InitWizard.tsx +0 -247
  180. package/src/tui/components/Markdown.tsx +0 -35
  181. package/src/tui/components/ModelSelector.tsx +0 -107
  182. package/src/tui/components/StatusBar.tsx +0 -30
  183. package/src/tui/components/Thinking.tsx +0 -17
  184. package/src/tui/components/ToolCall.tsx +0 -102
  185. package/src/tui/components/UserInput.tsx +0 -151
  186. package/src/tui/hooks/useChat.ts +0 -287
  187. package/src/tui/hooks/useCommands.ts +0 -271
  188. package/src/tui/index.ts +0 -6
  189. package/src/tui/welcome.ts +0 -177
  190. package/src/types.ts +0 -104
  191. package/src/utils/diff.ts +0 -71
  192. package/src/utils/project.ts +0 -99
  193. package/src/utils/stableStringify.ts +0 -47
  194. package/src/utils/thinking.ts +0 -119
  195. package/tests/cache/fingerprint.test.ts +0 -75
  196. package/tests/cache/providerCatalog.test.ts +0 -31
  197. package/tests/cache/smartModel.test.ts +0 -100
  198. package/tests/cache/stableStringify.test.ts +0 -43
  199. package/tests/cache/stats.test.ts +0 -146
  200. package/tests/cache/volatile.test.ts +0 -75
  201. package/tests/smoke-pollution.mjs +0 -78
  202. package/tests/smoke-thinking.mjs +0 -110
  203. package/tests/smoke-tool-stream.mjs +0 -69
  204. package/tests/smoke-tool.mjs +0 -117
  205. package/tsconfig.json +0 -21
@@ -1,413 +0,0 @@
1
- /**
2
- * Anthropic Provider
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, an array of blocks)
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")
15
- *
16
- * v3.0.0: prompt caching support
17
- * - The top-level "system" field is now an array of content blocks. The
18
- * last block carries cache_control: { type: 'ephemeral', ttl: '5m' }
19
- * by default, so Anthropic caches the entire system prefix across rounds.
20
- * The previous buildRequest silently dropped any system messages after
21
- * the first (`find` + `system` string) — that bug is fixed here: we
22
- * forward every system message and let the provider concatenate.
23
- * - The last tool definition gets a cache_control marker too, so tool
24
- * schemas are cached on subsequent rounds (Anthropic charges full price
25
- * for uncached tool descriptions, which can be the largest single block
26
- * in tool-heavy sessions).
27
- * - The stream's trailing message_delta carries a `usage` object with
28
- * cache_creation_input_tokens / cache_read_input_tokens. We yield a
29
- * structured { type: 'usage' } chunk so the cache stats layer can
30
- * record hit-rates.
31
- * - JSON serialization uses stableStringify so the byte sequence of the
32
- * request body is deterministic across rounds. Although Anthropic does
33
- * not do byte-level prefix cache (it relies on its own fingerprint),
34
- * deterministic serialization makes the on-the-wire body easy to diff
35
- * when debugging cache misses.
36
- */
37
-
38
- import type { ChatMessage, ChatResponse, ChatOptions, ToolCall, StreamChunk, Usage } from '../types.js';
39
- import type { Tool } from '../tools/types.js';
40
- import type { LLMProvider, ProviderConfig } from './provider.js';
41
- import { stableStringify } from '../utils/stableStringify.js';
42
-
43
- /** Default TTL for cache_control breakpoints. '5m' is cheaper to write; '1h' is preferred when sessions are long. */
44
- const DEFAULT_CACHE_TTL: '5m' | '1h' = '5m';
45
-
46
- export class AnthropicProvider implements LLMProvider {
47
- readonly name = 'anthropic';
48
- protected config: ProviderConfig;
49
- /**
50
- * v3.0.3: TTL resolved from the user's config + smart routing. This is
51
- * the *actual* TTL attached to every cache_control marker we send this
52
- * session. Stays stable across rounds to keep the Anthropic cache
53
- * prefix intact.
54
- *
55
- * Computed once by LLMService.chatStream when ttl='auto', then
56
- * persisted via setResolvedTTL. Subsequent rounds call chatStream
57
- * without re-deciding (ChatOptions.resolvedTtl is sticky per session).
58
- */
59
- protected resolvedTtl: '5m' | '1h' = DEFAULT_CACHE_TTL;
60
-
61
- constructor(config: ProviderConfig) {
62
- this.config = config;
63
- }
64
-
65
- /**
66
- * Called by LLMService after decideTTL(). Persists the TTL on the
67
- * provider instance so buildRequest() writes it consistently across
68
- * every round in this session.
69
- */
70
- setResolvedTTL(ttl: '5m' | '1h'): void {
71
- this.resolvedTtl = ttl;
72
- }
73
-
74
- buildTools(tools: Tool[]): any[] {
75
- return tools.map(tool => ({
76
- name: tool.name,
77
- description: tool.description,
78
- input_schema: tool.inputSchema || {
79
- type: 'object',
80
- properties: Object.fromEntries(
81
- tool.parameters.map(p => [p.name, { type: p.type, description: p.description }])
82
- ),
83
- required: tool.parameters.filter(p => p.required).map(p => p.name),
84
- },
85
- }));
86
- }
87
-
88
- async chat(messages: ChatMessage[], options?: ChatOptions, tools?: Tool[]): Promise<ChatResponse> {
89
- const body = this.buildRequest(messages, false, options, tools);
90
- const response = await this.doRequest(body);
91
-
92
- if (!response.ok) {
93
- const text = await response.text().catch(() => '');
94
- throw new Error(`Anthropic API error ${response.status}: ${text}`);
95
- }
96
-
97
- const data = await response.json();
98
-
99
- // Extract text content
100
- const textBlocks = data.content?.filter((b: any) => b.type === 'text') || [];
101
- const content = textBlocks.map((b: any) => b.text).join('');
102
-
103
- // Extract tool calls
104
- const toolUseBlocks = data.content?.filter((b: any) => b.type === 'tool_use') || [];
105
- const toolCalls = toolUseBlocks.length > 0 ? toolUseBlocks.map((b: any) => ({
106
- id: b.id,
107
- type: 'function' as const,
108
- function: {
109
- name: b.name,
110
- arguments: JSON.stringify(b.input),
111
- },
112
- })) : undefined;
113
-
114
- return {
115
- content,
116
- role: 'assistant',
117
- usage: data.usage ? this.normalizeUsage(data.usage) : undefined,
118
- tool_calls: toolCalls,
119
- };
120
- }
121
-
122
- async *chatStream(messages: ChatMessage[], options?: ChatOptions, tools?: Tool[]): AsyncGenerator<StreamChunk, ChatResponse> {
123
- const body = this.buildRequest(messages, true, options, tools);
124
- const response = await this.doRequest(body);
125
-
126
- if (!response.ok || !response.body) {
127
- const text = await response.text().catch(() => '');
128
- throw new Error(`Anthropic API error ${response.status}: ${text}`);
129
- }
130
-
131
- const reader = response.body.getReader();
132
- const decoder = new TextDecoder();
133
- let fullContent = '';
134
- let buffer = '';
135
- // Track tool use blocks
136
- const toolUseBlocks: Map<number, { id: string; name: string; input: string }> = new Map();
137
- let currentBlockIndex = -1;
138
- let currentBlockType = '';
139
- // Anthropic attaches usage info to the trailing message_delta event.
140
- let capturedUsage: Usage | undefined;
141
-
142
- try {
143
- while (true) {
144
- const { done, value } = await reader.read();
145
- if (done) break;
146
-
147
- buffer += decoder.decode(value, { stream: true });
148
- const lines = buffer.split('\n');
149
- buffer = lines.pop() || '';
150
-
151
- for (const line of lines) {
152
- const trimmed = line.trim();
153
- if (!trimmed || !trimmed.startsWith('data: ')) continue;
154
-
155
- try {
156
- const data = JSON.parse(trimmed.slice(6));
157
-
158
- // content_block_start: track block types
159
- if (data.type === 'content_block_start') {
160
- currentBlockIndex = data.index ?? 0;
161
- currentBlockType = data.content_block?.type || '';
162
- if (currentBlockType === 'tool_use') {
163
- toolUseBlocks.set(currentBlockIndex, {
164
- id: data.content_block.id || '',
165
- name: data.content_block.name || '',
166
- input: '',
167
- });
168
- } else if (currentBlockType === 'thinking') {
169
- // Anthropic extended thinking: emit as structured thinking chunks.
170
- // TUI / session layer decides whether to surface these.
171
- // (We don't push to fullContent — compressThinking strips
172
- // these blocks from persistence.)
173
- }
174
- }
175
-
176
- // content_block_delta: incremental text or tool input
177
- if (data.type === 'content_block_delta') {
178
- if (data.delta?.type === 'text_delta') {
179
- const text = data.delta.text;
180
- fullContent += text;
181
- yield { type: 'text', content: text };
182
- } else if (data.delta?.type === 'input_json_delta') {
183
- const buf = toolUseBlocks.get(currentBlockIndex);
184
- if (buf) {
185
- buf.input += data.delta.partial_json || '';
186
- }
187
- } else if (data.delta?.type === 'thinking_delta') {
188
- if (data.delta.thinking) {
189
- yield { type: 'thinking', content: data.delta.thinking };
190
- }
191
- }
192
- }
193
-
194
- // message_delta carries the final usage (cache hit/miss stats).
195
- // Per Anthropic docs: usage is only attached on the message_delta
196
- // event of the LAST chunk (after message_stop), unless the
197
- // `anthropic-beta: prompt-caching-2024-07-31` header is sent.
198
- if (data.type === 'message_delta' && data.usage) {
199
- capturedUsage = this.normalizeUsage({
200
- ...capturedUsage,
201
- ...data.usage,
202
- });
203
- }
204
-
205
- // message_start may carry the initial input_tokens + cache info
206
- // when prompt caching is active.
207
- if (data.type === 'message_start' && data.message?.usage) {
208
- capturedUsage = this.normalizeUsage(data.message.usage);
209
- }
210
- } catch {
211
- // Skip invalid JSON
212
- }
213
- }
214
- }
215
- } finally {
216
- reader.releaseLock();
217
- }
218
-
219
- // Convert tool use blocks to ToolCall[]
220
- const toolCalls: ToolCall[] = [];
221
- for (const [, buf] of toolUseBlocks) {
222
- if (buf.id && buf.name) {
223
- toolCalls.push({
224
- id: buf.id,
225
- type: 'function',
226
- function: { name: buf.name, arguments: buf.input },
227
- });
228
- }
229
- }
230
-
231
- if (toolCalls.length > 0) {
232
- yield { type: 'tool_calls', toolCalls };
233
- }
234
-
235
- if (capturedUsage) {
236
- yield { type: 'usage', usage: capturedUsage };
237
- }
238
-
239
- return {
240
- content: fullContent,
241
- role: 'assistant',
242
- usage: capturedUsage,
243
- tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
244
- };
245
- }
246
-
247
- /**
248
- * Build request body for Anthropic API.
249
- *
250
- * v3.0.0 changes:
251
- * 1. `system` is now an array of content blocks (was a single string).
252
- * 2. ALL system messages are forwarded (the previous code used `find`
253
- * and dropped every system message after the first, which broke
254
- * `[Earlier conversation...]` summaries).
255
- * 3. The LAST system block carries cache_control, marking the entire
256
- * system prefix as cacheable.
257
- * 4. The LAST tool definition carries cache_control, marking the entire
258
- * tool list as cacheable.
259
- * 5. Per-message cache_control markers are forwarded when set on
260
- * ChatMessage (rare, but supports fine-grained breakpoints).
261
- * 6. JSON.parse on tool arguments is wrapped in try/catch — interrupted
262
- * streams can leave a half-parsed JSON string that previously
263
- * crashed the entire buildRequest.
264
- */
265
- protected buildRequest(messages: ChatMessage[], stream: boolean, options?: ChatOptions, tools?: Tool[]) {
266
- // 1) Collect ALL system messages, in order, as content blocks. The
267
- // final block (and only the final block, per Anthropic convention)
268
- // carries the cache_control marker.
269
- const systemMessages = messages.filter(m => m.role === 'system');
270
- // v3.0.3: TTL is sticky per session. The provider's resolvedTtl is
271
- // set once by LLMService after decideTTL() and never changed within
272
- // a session (changing it would invalidate the Anthropic cache
273
- // prefix and cost more in cache_creation_input_tokens than it
274
- // saves).
275
- const cacheTtl = this.resolvedTtl;
276
- const cacheEnabled = this.config.cache?.enabled !== false; // default on for Anthropic
277
-
278
- const systemBlocks = systemMessages.length === 0 ? undefined : systemMessages.map((m, i, arr) => {
279
- const isLast = i === arr.length - 1;
280
- // ChatMessage.content may be string | ContentBlock[] (the latter used
281
- // for multimodal user messages). System messages are always plain
282
- // strings in our codebase, but be defensive and handle both shapes.
283
- const text = typeof m.content === 'string'
284
- ? m.content
285
- : m.content
286
- .filter(b => b.type === 'text')
287
- .map(b => (b as any).text)
288
- .join('');
289
- const block: any = { type: 'text', text };
290
- if (isLast && cacheEnabled) {
291
- block.cache_control = { type: 'ephemeral', ttl: cacheTtl };
292
- }
293
- return block;
294
- });
295
-
296
- // 2) Convert non-system messages to Anthropic wire format.
297
- const nonSystemMsgs = messages.filter(m => m.role !== 'system');
298
- const anthropicMessages = nonSystemMsgs.map(m => {
299
- if (m.role === 'tool') {
300
- // Tool result message
301
- return {
302
- role: 'user',
303
- content: [{
304
- type: 'tool_result',
305
- tool_use_id: m.tool_call_id,
306
- content: m.content,
307
- }],
308
- };
309
- }
310
- if (m.tool_calls && m.tool_calls.length > 0) {
311
- // Assistant message with tool calls
312
- const blocks: any[] = [];
313
- if (m.content) {
314
- blocks.push({ type: 'text', text: m.content });
315
- }
316
- for (const tc of m.tool_calls) {
317
- // Robust parse: aborted streams can leave a half-formed JSON
318
- // string. Fall back to {} so the request still goes through and
319
- // the model can re-request with corrected args.
320
- let parsedArgs: unknown = {};
321
- try { parsedArgs = JSON.parse(tc.function.arguments || '{}'); } catch { parsedArgs = {}; }
322
- blocks.push({
323
- type: 'tool_use',
324
- id: tc.id,
325
- name: tc.function.name,
326
- input: parsedArgs,
327
- });
328
- }
329
- return { role: 'assistant', content: blocks };
330
- }
331
- // Forward per-message cache_control if the caller attached one
332
- // (e.g. an inline breakpoint for a particular user message).
333
- const baseContent = typeof m.content === 'string' ? m.content : m.content;
334
- const msg: any = {
335
- role: m.role === 'assistant' ? 'assistant' : 'user',
336
- content: baseContent,
337
- };
338
- if (m.cache_control) {
339
- msg.cache_control = m.cache_control;
340
- }
341
- return msg;
342
- });
343
-
344
- const body: any = {
345
- model: this.config.model,
346
- max_tokens: options?.maxTokens ?? this.config.maxTokens ?? 4096,
347
- temperature: options?.temperature ?? this.config.temperature,
348
- ...(systemBlocks ? { system: systemBlocks } : {}),
349
- messages: anthropicMessages,
350
- stream,
351
- };
352
-
353
- // 3) Tools. Attach cache_control only to the LAST tool so Anthropic
354
- // caches the entire tool list as a single breakpoint.
355
- if (tools && tools.length > 0) {
356
- const toolDefs = this.buildTools(tools);
357
- if (cacheEnabled) {
358
- toolDefs[toolDefs.length - 1].cache_control = { type: 'ephemeral', ttl: cacheTtl };
359
- }
360
- body.tools = toolDefs;
361
- }
362
-
363
- return body;
364
- }
365
-
366
- /**
367
- * Execute the HTTP request.
368
- *
369
- * v3.0.0: serialization via stableStringify; also send the
370
- * `anthropic-beta: prompt-caching-2024-07-31` header so the API returns
371
- * cache_read_input_tokens / cache_creation_input_tokens in the streaming
372
- * usage fields. Some relay stations do not forward this header — that is
373
- * fine, the rest of the provider still works (just without cache stats).
374
- */
375
- protected async doRequest(body: any): Promise<Response> {
376
- const url = `${this.config.baseUrl}/messages`;
377
- const controller = new AbortController();
378
- const timeout = setTimeout(() => controller.abort(), 60000); // 60s
379
-
380
- try {
381
- return await fetch(url, {
382
- method: 'POST',
383
- headers: {
384
- 'Content-Type': 'application/json',
385
- 'x-api-key': this.config.apiKey,
386
- 'anthropic-version': '2023-06-01',
387
- 'anthropic-beta': 'prompt-caching-2024-07-31',
388
- },
389
- body: stableStringify(body),
390
- signal: controller.signal,
391
- });
392
- } finally {
393
- clearTimeout(timeout);
394
- }
395
- }
396
-
397
- /**
398
- * Map Anthropic usage JSON to the canonical Usage shape. Anthropic reports
399
- * input_tokens / output_tokens on the message_start event, and adds
400
- * cache_creation_input_tokens / cache_read_input_tokens on message_delta.
401
- * Either can arrive first depending on the streaming order, so we accept
402
- * either and merge.
403
- */
404
- protected normalizeUsage(raw: any): Usage {
405
- return {
406
- prompt_tokens: raw.input_tokens || 0,
407
- completion_tokens: raw.output_tokens || 0,
408
- total_tokens: (raw.input_tokens || 0) + (raw.output_tokens || 0),
409
- cache_creation_input_tokens: raw.cache_creation_input_tokens,
410
- cache_read_input_tokens: raw.cache_read_input_tokens,
411
- };
412
- }
413
- }
package/src/llm/gemini.ts DELETED
@@ -1,169 +0,0 @@
1
- /**
2
- * Google Gemini Provider
3
- */
4
-
5
- import type { ChatMessage, ChatResponse, ChatOptions, ToolCall } from '../types.js';
6
- import type { Tool } from '../tools/types.js';
7
- import type { LLMProvider, StreamChunk, ProviderConfig } from './provider.js';
8
-
9
- export class GeminiProvider implements LLMProvider {
10
- readonly name = 'gemini';
11
- protected config: ProviderConfig;
12
-
13
- constructor(config: ProviderConfig) {
14
- this.config = config;
15
- }
16
-
17
- buildTools(tools: Tool[]): any[] {
18
- return [{
19
- functionDeclarations: tools.map(tool => ({
20
- name: tool.name,
21
- description: tool.description,
22
- parameters: tool.inputSchema || {
23
- type: 'object',
24
- properties: Object.fromEntries(
25
- tool.parameters.map(p => [p.name, { type: p.type, description: p.description }])
26
- ),
27
- required: tool.parameters.filter(p => p.required).map(p => p.name),
28
- },
29
- })),
30
- }];
31
- }
32
-
33
- async chat(messages: ChatMessage[], options?: ChatOptions, tools?: Tool[]): Promise<ChatResponse> {
34
- const body = this.buildRequest(messages, options, tools);
35
- const url = `${this.config.baseUrl}/models/${this.config.model}:generateContent?key=${this.config.apiKey}`;
36
-
37
- const response = await fetch(url, {
38
- method: 'POST',
39
- headers: { 'Content-Type': 'application/json' },
40
- body: JSON.stringify(body),
41
- });
42
-
43
- if (!response.ok) {
44
- const text = await response.text().catch(() => '');
45
- throw new Error(`Gemini API error ${response.status}: ${text}`);
46
- }
47
-
48
- const data = await response.json();
49
- const candidate = data.candidates?.[0];
50
- const parts = candidate?.content?.parts || [];
51
- const text = parts.map((p: any) => p.text).filter(Boolean).join('');
52
-
53
- const functionCalls = parts.filter((p: any) => p.functionCall).map((p: any) => ({
54
- id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
55
- type: 'function' as const,
56
- function: {
57
- name: p.functionCall.name,
58
- arguments: JSON.stringify(p.functionCall.args || {}),
59
- },
60
- }));
61
-
62
- return {
63
- content: text,
64
- role: 'assistant',
65
- usage: data.usageMetadata ? {
66
- prompt_tokens: data.usageMetadata.promptTokenCount || 0,
67
- completion_tokens: data.usageMetadata.candidatesTokenCount || 0,
68
- total_tokens: data.usageMetadata.totalTokenCount || 0,
69
- } : undefined,
70
- tool_calls: functionCalls.length > 0 ? functionCalls : undefined,
71
- };
72
- }
73
-
74
- async *chatStream(messages: ChatMessage[], options?: ChatOptions, tools?: Tool[]): AsyncGenerator<StreamChunk, ChatResponse> {
75
- const body = this.buildRequest(messages, options, tools);
76
- const url = `${this.config.baseUrl}/models/${this.config.model}:streamGenerateContent?alt=sse&key=${this.config.apiKey}`;
77
-
78
- const response = await fetch(url, {
79
- method: 'POST',
80
- headers: { 'Content-Type': 'application/json' },
81
- body: JSON.stringify(body),
82
- });
83
-
84
- if (!response.ok || !response.body) {
85
- const text = await response.text().catch(() => '');
86
- throw new Error(`Gemini API error ${response.status}: ${text}`);
87
- }
88
-
89
- const reader = response.body.getReader();
90
- const decoder = new TextDecoder();
91
- let fullContent = '';
92
- let buffer = '';
93
- const functionCalls: ToolCall[] = [];
94
-
95
- try {
96
- while (true) {
97
- const { done, value } = await reader.read();
98
- if (done) break;
99
-
100
- buffer += decoder.decode(value, { stream: true });
101
- const lines = buffer.split('\n');
102
- buffer = lines.pop() || '';
103
-
104
- for (const line of lines) {
105
- const trimmed = line.trim();
106
- if (!trimmed || !trimmed.startsWith('data: ')) continue;
107
-
108
- try {
109
- const data = JSON.parse(trimmed.slice(6));
110
- const parts = data.candidates?.[0]?.content?.parts || [];
111
- for (const part of parts) {
112
- if (part.text) {
113
- fullContent += part.text;
114
- yield { type: 'text', content: part.text };
115
- }
116
- if (part.functionCall) {
117
- functionCalls.push({
118
- id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
119
- type: 'function',
120
- function: {
121
- name: part.functionCall.name,
122
- arguments: JSON.stringify(part.functionCall.args || {}),
123
- },
124
- });
125
- }
126
- }
127
- } catch {
128
- // Skip invalid JSON
129
- }
130
- }
131
- }
132
- } finally {
133
- reader.releaseLock();
134
- }
135
-
136
- if (functionCalls.length > 0) {
137
- yield { type: 'tool_calls', toolCalls: functionCalls };
138
- }
139
-
140
- return { content: fullContent, role: 'assistant', tool_calls: functionCalls.length > 0 ? functionCalls : undefined };
141
- }
142
-
143
- private buildRequest(messages: ChatMessage[], options?: ChatOptions, tools?: Tool[]) {
144
- const systemMsg = messages.find(m => m.role === 'system');
145
- const nonSystemMsgs = messages.filter(m => m.role !== 'system');
146
-
147
- const contents = nonSystemMsgs.map(m => ({
148
- role: m.role === 'assistant' ? 'model' : 'user',
149
- parts: [{ text: m.content }],
150
- }));
151
-
152
- const body: any = {
153
- contents,
154
- ...(systemMsg && {
155
- systemInstruction: { parts: [{ text: systemMsg.content }] },
156
- }),
157
- generationConfig: {
158
- temperature: options?.temperature ?? this.config.temperature,
159
- maxOutputTokens: options?.maxTokens ?? this.config.maxTokens,
160
- },
161
- };
162
-
163
- if (tools && tools.length > 0) {
164
- body.tools = this.buildTools(tools);
165
- }
166
-
167
- return body;
168
- }
169
- }