thatgfsj-code 2.2.9 → 3.0.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.
- package/CHANGELOG.md +15 -67
- package/dist/app/index.d.ts +28 -5
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js +76 -10
- package/dist/app/index.js.map +1 -1
- package/dist/cache/fingerprint.d.ts +49 -0
- package/dist/cache/fingerprint.d.ts.map +1 -0
- package/dist/cache/fingerprint.js +100 -0
- package/dist/cache/fingerprint.js.map +1 -0
- package/dist/cache/index.d.ts +15 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +15 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/smartModel.d.ts +40 -0
- package/dist/cache/smartModel.d.ts.map +1 -0
- package/dist/cache/smartModel.js +43 -0
- package/dist/cache/smartModel.js.map +1 -0
- package/dist/cache/stats.d.ts +93 -0
- package/dist/cache/stats.d.ts.map +1 -0
- package/dist/cache/stats.js +155 -0
- package/dist/cache/stats.js.map +1 -0
- package/dist/cache/volatile.d.ts +32 -0
- package/dist/cache/volatile.d.ts.map +1 -0
- package/dist/cache/volatile.js +44 -0
- package/dist/cache/volatile.js.map +1 -0
- package/dist/cmd/index.js +49 -57
- package/dist/cmd/index.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +8 -0
- package/dist/config/index.js.map +1 -1
- package/dist/config/types.d.ts +14 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/llm/anthropic.d.ts +55 -5
- package/dist/llm/anthropic.d.ts.map +1 -1
- package/dist/llm/anthropic.js +172 -26
- package/dist/llm/anthropic.js.map +1 -1
- package/dist/llm/index.d.ts +26 -8
- package/dist/llm/index.d.ts.map +1 -1
- package/dist/llm/index.js +83 -15
- package/dist/llm/index.js.map +1 -1
- package/dist/llm/openai.d.ts +28 -8
- package/dist/llm/openai.d.ts.map +1 -1
- package/dist/llm/openai.js +58 -8
- package/dist/llm/openai.js.map +1 -1
- package/dist/llm/provider.d.ts +40 -8
- package/dist/llm/provider.d.ts.map +1 -1
- package/dist/llm/provider.js +8 -1
- package/dist/llm/provider.js.map +1 -1
- package/dist/prompts/index.d.ts +32 -0
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +33 -11
- package/dist/prompts/index.js.map +1 -1
- package/dist/session/index.d.ts +32 -3
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/index.js +51 -36
- package/dist/session/index.js.map +1 -1
- package/dist/skills/brainstorming.js +33 -33
- package/dist/skills/code-review.js +35 -35
- package/dist/skills/executing-plans.js +18 -18
- package/dist/skills/frontend-design.js +25 -25
- package/dist/skills/git-workflow.js +26 -26
- package/dist/skills/improve-architecture.js +28 -28
- package/dist/skills/neuroweave.js +37 -37
- package/dist/skills/playwright.js +62 -62
- package/dist/skills/prototype.js +20 -20
- package/dist/skills/subagent.js +19 -19
- package/dist/skills/supabase.js +34 -34
- package/dist/skills/systematic-debugging.js +34 -34
- package/dist/skills/tdd.js +29 -29
- package/dist/skills/triage.js +25 -25
- package/dist/skills/verification.js +22 -22
- package/dist/skills/writing-plans.js +32 -32
- package/dist/tools/nwt.js +16 -16
- package/dist/tui/app.d.ts.map +1 -1
- package/dist/tui/app.js +9 -2
- package/dist/tui/app.js.map +1 -1
- package/dist/tui/components/Header.d.ts +8 -0
- package/dist/tui/components/Header.d.ts.map +1 -1
- package/dist/tui/components/Header.js +17 -4
- package/dist/tui/components/Header.js.map +1 -1
- package/dist/tui/components/InitWizard.d.ts +12 -1
- package/dist/tui/components/InitWizard.d.ts.map +1 -1
- package/dist/tui/components/InitWizard.js +58 -3
- package/dist/tui/components/InitWizard.js.map +1 -1
- package/dist/tui/components/ModelSelector.d.ts +3 -1
- package/dist/tui/components/ModelSelector.d.ts.map +1 -1
- package/dist/tui/components/ModelSelector.js +24 -7
- package/dist/tui/components/ModelSelector.js.map +1 -1
- package/dist/tui/components/ToolCall.d.ts.map +1 -1
- package/dist/tui/components/ToolCall.js +1 -6
- package/dist/tui/components/ToolCall.js.map +1 -1
- package/dist/tui/components/UserInput.d.ts.map +1 -1
- package/dist/tui/components/UserInput.js +20 -0
- package/dist/tui/components/UserInput.js.map +1 -1
- package/dist/tui/hooks/useChat.d.ts +22 -0
- package/dist/tui/hooks/useChat.d.ts.map +1 -1
- package/dist/tui/hooks/useChat.js +82 -46
- package/dist/tui/hooks/useChat.js.map +1 -1
- package/dist/tui/hooks/useCommands.d.ts.map +1 -1
- package/dist/tui/hooks/useCommands.js +57 -0
- package/dist/tui/hooks/useCommands.js.map +1 -1
- package/dist/tui/welcome.d.ts.map +1 -1
- package/dist/tui/welcome.js +2 -3
- package/dist/tui/welcome.js.map +1 -1
- package/dist/types.d.ts +79 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -1
- package/dist/utils/stableStringify.d.ts +21 -0
- package/dist/utils/stableStringify.d.ts.map +1 -0
- package/dist/utils/stableStringify.js +53 -0
- package/dist/utils/stableStringify.js.map +1 -0
- package/dist/utils/thinking.d.ts.map +1 -1
- package/dist/utils/thinking.js +0 -3
- package/dist/utils/thinking.js.map +1 -1
- package/package.json +7 -6
- package/src/app/index.ts +189 -124
- package/src/cache/fingerprint.ts +101 -0
- package/src/cache/index.ts +15 -0
- package/src/cache/smartModel.ts +52 -0
- package/src/cache/stats.ts +199 -0
- package/src/cache/volatile.ts +47 -0
- package/src/cmd/index.tsx +288 -292
- package/src/config/index.ts +156 -148
- package/src/config/providers.ts +234 -234
- package/src/config/types.ts +67 -53
- package/src/hooks/index.ts +111 -111
- package/src/llm/anthropic.ts +388 -243
- package/src/llm/gemini.ts +169 -169
- package/src/llm/index.ts +265 -200
- package/src/llm/openai.ts +243 -196
- package/src/llm/provider.ts +66 -34
- package/src/prompts/index.ts +260 -220
- package/src/session/compactor.ts +103 -103
- package/src/session/index.ts +181 -168
- package/src/session/message.ts +42 -42
- package/src/skills/brainstorming.ts +43 -43
- package/src/skills/code-review.ts +45 -45
- package/src/skills/executing-plans.ts +27 -27
- package/src/skills/frontend-design.ts +35 -35
- package/src/skills/git-workflow.ts +36 -36
- package/src/skills/improve-architecture.ts +38 -38
- package/src/skills/index.ts +136 -136
- package/src/skills/neuroweave.ts +47 -47
- package/src/skills/playwright.ts +72 -72
- package/src/skills/prototype.ts +30 -30
- package/src/skills/subagent.ts +28 -28
- package/src/skills/supabase.ts +44 -44
- package/src/skills/systematic-debugging.ts +44 -44
- package/src/skills/tdd.ts +39 -39
- package/src/skills/triage.ts +35 -35
- package/src/skills/verification.ts +31 -31
- package/src/skills/writing-plans.ts +42 -42
- package/src/tools/nwt.ts +598 -598
- package/src/tools/types.ts +122 -122
- package/src/tui/app.tsx +200 -186
- package/src/tui/components/ChatList.tsx +41 -41
- package/src/tui/components/ChatMessage.tsx +54 -54
- package/src/tui/components/Header.tsx +56 -29
- package/src/tui/components/InitWizard.tsx +217 -132
- package/src/tui/components/Markdown.tsx +35 -35
- package/src/tui/components/ModelSelector.tsx +107 -87
- package/src/tui/components/StatusBar.tsx +30 -30
- package/src/tui/components/Thinking.tsx +17 -17
- package/src/tui/components/ToolCall.tsx +102 -107
- package/src/tui/components/UserInput.tsx +151 -131
- package/src/tui/hooks/useChat.ts +287 -238
- package/src/tui/hooks/useCommands.ts +234 -176
- package/src/tui/index.ts +6 -6
- package/src/tui/welcome.ts +177 -178
- package/src/types.ts +104 -42
- package/src/utils/diff.ts +71 -71
- package/src/utils/project.ts +99 -99
- package/src/utils/stableStringify.ts +47 -0
- package/src/utils/thinking.ts +118 -121
- package/tests/cache/fingerprint.test.ts +75 -0
- package/tests/cache/providerCatalog.test.ts +31 -0
- package/tests/cache/stableStringify.test.ts +43 -0
- package/tests/cache/stats.test.ts +146 -0
- package/tests/cache/volatile.test.ts +75 -0
- package/tests/smoke-pollution.mjs +78 -0
- package/tests/smoke-thinking.mjs +110 -0
- package/tests/smoke-tool-stream.mjs +69 -0
- package/tests/smoke-tool.mjs +117 -0
- package/.nwt/meta.json +0 -5
- package/dist/version.d.ts +0 -16
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js +0 -16
- package/dist/version.js.map +0 -1
- package/src/version.ts +0 -16
package/src/llm/anthropic.ts
CHANGED
|
@@ -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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
*
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
+
}
|