thatgfsj-code 1.0.3 → 3.0.0

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