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.
Files changed (190) hide show
  1. package/CHANGELOG.md +15 -67
  2. package/dist/app/index.d.ts +28 -5
  3. package/dist/app/index.d.ts.map +1 -1
  4. package/dist/app/index.js +76 -10
  5. package/dist/app/index.js.map +1 -1
  6. package/dist/cache/fingerprint.d.ts +49 -0
  7. package/dist/cache/fingerprint.d.ts.map +1 -0
  8. package/dist/cache/fingerprint.js +100 -0
  9. package/dist/cache/fingerprint.js.map +1 -0
  10. package/dist/cache/index.d.ts +15 -0
  11. package/dist/cache/index.d.ts.map +1 -0
  12. package/dist/cache/index.js +15 -0
  13. package/dist/cache/index.js.map +1 -0
  14. package/dist/cache/smartModel.d.ts +40 -0
  15. package/dist/cache/smartModel.d.ts.map +1 -0
  16. package/dist/cache/smartModel.js +43 -0
  17. package/dist/cache/smartModel.js.map +1 -0
  18. package/dist/cache/stats.d.ts +93 -0
  19. package/dist/cache/stats.d.ts.map +1 -0
  20. package/dist/cache/stats.js +155 -0
  21. package/dist/cache/stats.js.map +1 -0
  22. package/dist/cache/volatile.d.ts +32 -0
  23. package/dist/cache/volatile.d.ts.map +1 -0
  24. package/dist/cache/volatile.js +44 -0
  25. package/dist/cache/volatile.js.map +1 -0
  26. package/dist/cmd/index.js +49 -57
  27. package/dist/cmd/index.js.map +1 -1
  28. package/dist/config/index.d.ts.map +1 -1
  29. package/dist/config/index.js +8 -0
  30. package/dist/config/index.js.map +1 -1
  31. package/dist/config/types.d.ts +14 -0
  32. package/dist/config/types.d.ts.map +1 -1
  33. package/dist/llm/anthropic.d.ts +55 -5
  34. package/dist/llm/anthropic.d.ts.map +1 -1
  35. package/dist/llm/anthropic.js +172 -26
  36. package/dist/llm/anthropic.js.map +1 -1
  37. package/dist/llm/index.d.ts +26 -8
  38. package/dist/llm/index.d.ts.map +1 -1
  39. package/dist/llm/index.js +83 -15
  40. package/dist/llm/index.js.map +1 -1
  41. package/dist/llm/openai.d.ts +28 -8
  42. package/dist/llm/openai.d.ts.map +1 -1
  43. package/dist/llm/openai.js +58 -8
  44. package/dist/llm/openai.js.map +1 -1
  45. package/dist/llm/provider.d.ts +40 -8
  46. package/dist/llm/provider.d.ts.map +1 -1
  47. package/dist/llm/provider.js +8 -1
  48. package/dist/llm/provider.js.map +1 -1
  49. package/dist/prompts/index.d.ts +32 -0
  50. package/dist/prompts/index.d.ts.map +1 -1
  51. package/dist/prompts/index.js +33 -11
  52. package/dist/prompts/index.js.map +1 -1
  53. package/dist/session/index.d.ts +32 -3
  54. package/dist/session/index.d.ts.map +1 -1
  55. package/dist/session/index.js +51 -36
  56. package/dist/session/index.js.map +1 -1
  57. package/dist/skills/brainstorming.js +33 -33
  58. package/dist/skills/code-review.js +35 -35
  59. package/dist/skills/executing-plans.js +18 -18
  60. package/dist/skills/frontend-design.js +25 -25
  61. package/dist/skills/git-workflow.js +26 -26
  62. package/dist/skills/improve-architecture.js +28 -28
  63. package/dist/skills/neuroweave.js +37 -37
  64. package/dist/skills/playwright.js +62 -62
  65. package/dist/skills/prototype.js +20 -20
  66. package/dist/skills/subagent.js +19 -19
  67. package/dist/skills/supabase.js +34 -34
  68. package/dist/skills/systematic-debugging.js +34 -34
  69. package/dist/skills/tdd.js +29 -29
  70. package/dist/skills/triage.js +25 -25
  71. package/dist/skills/verification.js +22 -22
  72. package/dist/skills/writing-plans.js +32 -32
  73. package/dist/tools/nwt.js +16 -16
  74. package/dist/tui/app.d.ts.map +1 -1
  75. package/dist/tui/app.js +9 -2
  76. package/dist/tui/app.js.map +1 -1
  77. package/dist/tui/components/Header.d.ts +8 -0
  78. package/dist/tui/components/Header.d.ts.map +1 -1
  79. package/dist/tui/components/Header.js +17 -4
  80. package/dist/tui/components/Header.js.map +1 -1
  81. package/dist/tui/components/InitWizard.d.ts +12 -1
  82. package/dist/tui/components/InitWizard.d.ts.map +1 -1
  83. package/dist/tui/components/InitWizard.js +58 -3
  84. package/dist/tui/components/InitWizard.js.map +1 -1
  85. package/dist/tui/components/ModelSelector.d.ts +3 -1
  86. package/dist/tui/components/ModelSelector.d.ts.map +1 -1
  87. package/dist/tui/components/ModelSelector.js +24 -7
  88. package/dist/tui/components/ModelSelector.js.map +1 -1
  89. package/dist/tui/components/ToolCall.d.ts.map +1 -1
  90. package/dist/tui/components/ToolCall.js +1 -6
  91. package/dist/tui/components/ToolCall.js.map +1 -1
  92. package/dist/tui/components/UserInput.d.ts.map +1 -1
  93. package/dist/tui/components/UserInput.js +20 -0
  94. package/dist/tui/components/UserInput.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 +82 -46
  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 +57 -0
  101. package/dist/tui/hooks/useCommands.js.map +1 -1
  102. package/dist/tui/welcome.d.ts.map +1 -1
  103. package/dist/tui/welcome.js +2 -3
  104. package/dist/tui/welcome.js.map +1 -1
  105. package/dist/types.d.ts +79 -6
  106. package/dist/types.d.ts.map +1 -1
  107. package/dist/types.js +7 -0
  108. package/dist/types.js.map +1 -1
  109. package/dist/utils/stableStringify.d.ts +21 -0
  110. package/dist/utils/stableStringify.d.ts.map +1 -0
  111. package/dist/utils/stableStringify.js +53 -0
  112. package/dist/utils/stableStringify.js.map +1 -0
  113. package/dist/utils/thinking.d.ts.map +1 -1
  114. package/dist/utils/thinking.js +0 -3
  115. package/dist/utils/thinking.js.map +1 -1
  116. package/package.json +7 -6
  117. package/src/app/index.ts +189 -124
  118. package/src/cache/fingerprint.ts +101 -0
  119. package/src/cache/index.ts +15 -0
  120. package/src/cache/smartModel.ts +52 -0
  121. package/src/cache/stats.ts +199 -0
  122. package/src/cache/volatile.ts +47 -0
  123. package/src/cmd/index.tsx +288 -292
  124. package/src/config/index.ts +156 -148
  125. package/src/config/providers.ts +234 -234
  126. package/src/config/types.ts +67 -53
  127. package/src/hooks/index.ts +111 -111
  128. package/src/llm/anthropic.ts +388 -243
  129. package/src/llm/gemini.ts +169 -169
  130. package/src/llm/index.ts +265 -200
  131. package/src/llm/openai.ts +243 -196
  132. package/src/llm/provider.ts +66 -34
  133. package/src/prompts/index.ts +260 -220
  134. package/src/session/compactor.ts +103 -103
  135. package/src/session/index.ts +181 -168
  136. package/src/session/message.ts +42 -42
  137. package/src/skills/brainstorming.ts +43 -43
  138. package/src/skills/code-review.ts +45 -45
  139. package/src/skills/executing-plans.ts +27 -27
  140. package/src/skills/frontend-design.ts +35 -35
  141. package/src/skills/git-workflow.ts +36 -36
  142. package/src/skills/improve-architecture.ts +38 -38
  143. package/src/skills/index.ts +136 -136
  144. package/src/skills/neuroweave.ts +47 -47
  145. package/src/skills/playwright.ts +72 -72
  146. package/src/skills/prototype.ts +30 -30
  147. package/src/skills/subagent.ts +28 -28
  148. package/src/skills/supabase.ts +44 -44
  149. package/src/skills/systematic-debugging.ts +44 -44
  150. package/src/skills/tdd.ts +39 -39
  151. package/src/skills/triage.ts +35 -35
  152. package/src/skills/verification.ts +31 -31
  153. package/src/skills/writing-plans.ts +42 -42
  154. package/src/tools/nwt.ts +598 -598
  155. package/src/tools/types.ts +122 -122
  156. package/src/tui/app.tsx +200 -186
  157. package/src/tui/components/ChatList.tsx +41 -41
  158. package/src/tui/components/ChatMessage.tsx +54 -54
  159. package/src/tui/components/Header.tsx +56 -29
  160. package/src/tui/components/InitWizard.tsx +217 -132
  161. package/src/tui/components/Markdown.tsx +35 -35
  162. package/src/tui/components/ModelSelector.tsx +107 -87
  163. package/src/tui/components/StatusBar.tsx +30 -30
  164. package/src/tui/components/Thinking.tsx +17 -17
  165. package/src/tui/components/ToolCall.tsx +102 -107
  166. package/src/tui/components/UserInput.tsx +151 -131
  167. package/src/tui/hooks/useChat.ts +287 -238
  168. package/src/tui/hooks/useCommands.ts +234 -176
  169. package/src/tui/index.ts +6 -6
  170. package/src/tui/welcome.ts +177 -178
  171. package/src/types.ts +104 -42
  172. package/src/utils/diff.ts +71 -71
  173. package/src/utils/project.ts +99 -99
  174. package/src/utils/stableStringify.ts +47 -0
  175. package/src/utils/thinking.ts +118 -121
  176. package/tests/cache/fingerprint.test.ts +75 -0
  177. package/tests/cache/providerCatalog.test.ts +31 -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
  185. package/.nwt/meta.json +0 -5
  186. package/dist/version.d.ts +0 -16
  187. package/dist/version.d.ts.map +0 -1
  188. package/dist/version.js +0 -16
  189. package/dist/version.js.map +0 -1
  190. package/src/version.ts +0 -16
package/src/llm/openai.ts CHANGED
@@ -1,196 +1,243 @@
1
- /**
2
- * OpenAI-compatible Provider
3
- * Works with: OpenAI, SiliconFlow, DeepSeek, Kimi, Zhipu, MiniMax, Baichuan, Stepfun, Doubao, Ollama, ERNIE
4
- * Also works with any OpenAI-compatible relay station (中转站)
5
- */
6
-
7
- import type { ChatMessage, ChatResponse, ChatOptions, ToolCall } from '../types.js';
8
- import type { Tool } from '../tools/types.js';
9
- import type { LLMProvider, ProviderConfig } from './provider.js';
10
-
11
- export interface StreamChunk {
12
- type: 'text' | 'tool_calls';
13
- content?: string;
14
- toolCalls?: ToolCall[];
15
- }
16
-
17
- export class OpenAIProvider implements LLMProvider {
18
- readonly name = 'openai';
19
- protected config: ProviderConfig;
20
-
21
- constructor(config: ProviderConfig) {
22
- this.config = config;
23
- }
24
-
25
- buildTools(tools: Tool[]): any[] {
26
- return tools.map(tool => ({
27
- type: 'function' as const,
28
- function: {
29
- name: tool.name,
30
- description: tool.description,
31
- parameters: tool.inputSchema || {
32
- type: 'object',
33
- properties: Object.fromEntries(
34
- tool.parameters.map(p => [p.name, { type: p.type, description: p.description }])
35
- ),
36
- required: tool.parameters.filter(p => p.required).map(p => p.name),
37
- },
38
- },
39
- }));
40
- }
41
-
42
- async chat(messages: ChatMessage[], options?: ChatOptions, tools?: Tool[]): Promise<ChatResponse> {
43
- const body = this.buildRequest(messages, false, options, tools);
44
- const response = await this.doRequest(body);
45
- const data = await response.json();
46
- const choice = data.choices?.[0];
47
-
48
- return {
49
- content: choice?.message?.content || '',
50
- role: 'assistant',
51
- usage: data.usage ? {
52
- prompt_tokens: data.usage.prompt_tokens || 0,
53
- completion_tokens: data.usage.completion_tokens || 0,
54
- total_tokens: data.usage.total_tokens || 0,
55
- } : undefined,
56
- tool_calls: choice?.message?.tool_calls,
57
- };
58
- }
59
-
60
- async *chatStream(messages: ChatMessage[], options?: ChatOptions, tools?: Tool[]): AsyncGenerator<StreamChunk, ChatResponse> {
61
- const body = this.buildRequest(messages, true, options, tools);
62
- const response = await this.doRequest(body);
63
-
64
- if (!response.ok || !response.body) {
65
- const text = await response.text().catch(() => '');
66
- throw new Error(`API error ${response.status}: ${text}`);
67
- }
68
-
69
- const reader = response.body.getReader();
70
- const decoder = new TextDecoder();
71
- let fullContent = '';
72
- let buffer = '';
73
- // Accumulate streaming tool call chunks
74
- const toolCallBuffers: Map<number, { id: string; name: string; arguments: string }> = new Map();
75
-
76
- try {
77
- while (true) {
78
- const { done, value } = await reader.read();
79
- if (done) break;
80
-
81
- buffer += decoder.decode(value, { stream: true });
82
- const lines = buffer.split('\n');
83
- buffer = lines.pop() || '';
84
-
85
- for (const line of lines) {
86
- const trimmed = line.trim();
87
- if (!trimmed) continue;
88
- if (trimmed === 'data: [DONE]') continue;
89
- if (!trimmed.startsWith('data: ')) continue;
90
-
91
- try {
92
- const data = JSON.parse(trimmed.slice(6));
93
- const delta = data.choices?.[0]?.delta;
94
-
95
- // Text content
96
- if (delta?.content) {
97
- fullContent += delta.content;
98
- yield { type: 'text', content: delta.content };
99
- }
100
-
101
- // Streaming tool calls - accumulate chunks
102
- if (delta?.tool_calls) {
103
- for (const tc of delta.tool_calls) {
104
- const idx = tc.index ?? 0;
105
- if (!toolCallBuffers.has(idx)) {
106
- toolCallBuffers.set(idx, { id: '', name: '', arguments: '' });
107
- }
108
- const buf = toolCallBuffers.get(idx)!;
109
- if (tc.id) buf.id = tc.id;
110
- if (tc.function?.name) buf.name += tc.function.name;
111
- if (tc.function?.arguments) buf.arguments += tc.function.arguments;
112
- }
113
- }
114
- } catch {
115
- // Skip invalid JSON lines
116
- }
117
- }
118
- }
119
- } finally {
120
- reader.releaseLock();
121
- }
122
-
123
- // Convert accumulated tool call buffers to ToolCall[]
124
- const toolCalls: ToolCall[] = [];
125
- for (const [, buf] of toolCallBuffers) {
126
- if (buf.id && buf.name) {
127
- toolCalls.push({
128
- id: buf.id,
129
- type: 'function',
130
- function: { name: buf.name, arguments: buf.arguments },
131
- });
132
- }
133
- }
134
-
135
- if (toolCalls.length > 0) {
136
- yield { type: 'tool_calls', toolCalls };
137
- }
138
-
139
- return {
140
- content: fullContent,
141
- role: 'assistant',
142
- tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
143
- };
144
- }
145
-
146
- /**
147
- * Build the request body for OpenAI-compatible API
148
- */
149
- protected buildRequest(messages: ChatMessage[], stream: boolean, options?: ChatOptions, tools?: Tool[]) {
150
- const body: any = {
151
- model: this.config.model,
152
- messages: messages.map(m => ({
153
- role: m.role,
154
- content: m.content,
155
- ...(m.name && { name: m.name }),
156
- ...(m.tool_call_id && { tool_call_id: m.tool_call_id }),
157
- ...(m.tool_calls && { tool_calls: m.tool_calls }),
158
- })),
159
- temperature: options?.temperature ?? this.config.temperature,
160
- max_tokens: options?.maxTokens ?? this.config.maxTokens,
161
- stream,
162
- ...(stream && { stream_options: { include_usage: true } }),
163
- };
164
-
165
- // Add tools if provided - this is critical for structured tool calling
166
- if (tools && tools.length > 0) {
167
- body.tools = this.buildTools(tools);
168
- }
169
-
170
- return body;
171
- }
172
-
173
- /**
174
- * Execute the HTTP request
175
- */
176
- protected async doRequest(body: any): Promise<Response> {
177
- const url = `${this.config.baseUrl}/chat/completions`;
178
- const controller = new AbortController();
179
- const timeout = setTimeout(() => controller.abort(), 60000); // 60s timeout
180
-
181
- try {
182
- const response = await fetch(url, {
183
- method: 'POST',
184
- headers: {
185
- 'Content-Type': 'application/json',
186
- 'Authorization': `Bearer ${this.config.apiKey}`,
187
- },
188
- body: JSON.stringify(body),
189
- signal: controller.signal,
190
- });
191
- return response;
192
- } finally {
193
- clearTimeout(timeout);
194
- }
195
- }
196
- }
1
+ /**
2
+ * OpenAI-compatible Provider
3
+ * Works with: OpenAI, SiliconFlow, DeepSeek, Kimi, Zhipu, MiniMax, Baichuan, Stepfun, Doubao, Ollama, ERNIE
4
+ * Also works with any OpenAI-compatible relay station (中转站)
5
+ *
6
+ * v3.0.0+: caching-friendly wire format
7
+ * - StreamChunk is now imported from ../types (single source of truth)
8
+ * - buildRequest uses stableStringify so that the bytes sent are byte-equal
9
+ * between requests with the same logical payload (required for DeepSeek
10
+ * automatic prefix-cache hit)
11
+ * - cache_control on individual ChatMessage is forwarded (Anthropic-style
12
+ * markers are no-ops on OpenAI-compatible APIs but harmless)
13
+ * - chatStream yields a final { type: 'usage' } chunk if the upstream
14
+ * returned usage info (stream_options.include_usage already requested)
15
+ */
16
+
17
+ import type { ChatMessage, ChatResponse, ChatOptions, ToolCall, StreamChunk } from '../types.js';
18
+ import type { Tool } from '../tools/types.js';
19
+ import type { LLMProvider, ProviderConfig } from './provider.js';
20
+ import { stableStringify } from '../utils/stableStringify.js';
21
+
22
+ export class OpenAIProvider implements LLMProvider {
23
+ readonly name = 'openai';
24
+ protected config: ProviderConfig;
25
+
26
+ constructor(config: ProviderConfig) {
27
+ this.config = config;
28
+ }
29
+
30
+ buildTools(tools: Tool[]): any[] {
31
+ return tools.map(tool => ({
32
+ type: 'function' as const,
33
+ function: {
34
+ name: tool.name,
35
+ description: tool.description,
36
+ parameters: tool.inputSchema || {
37
+ type: 'object',
38
+ properties: Object.fromEntries(
39
+ tool.parameters.map(p => [p.name, { type: p.type, description: p.description }])
40
+ ),
41
+ required: tool.parameters.filter(p => p.required).map(p => p.name),
42
+ },
43
+ },
44
+ }));
45
+ }
46
+
47
+ async chat(messages: ChatMessage[], options?: ChatOptions, tools?: Tool[]): Promise<ChatResponse> {
48
+ const body = this.buildRequest(messages, false, options, tools);
49
+ const response = await this.doRequest(body);
50
+ const data = await response.json();
51
+ const choice = data.choices?.[0];
52
+
53
+ return {
54
+ content: choice?.message?.content || '',
55
+ role: 'assistant',
56
+ usage: data.usage ? this.normalizeUsage(data.usage) : undefined,
57
+ tool_calls: choice?.message?.tool_calls,
58
+ };
59
+ }
60
+
61
+ async *chatStream(messages: ChatMessage[], options?: ChatOptions, tools?: Tool[]): AsyncGenerator<StreamChunk, ChatResponse> {
62
+ const body = this.buildRequest(messages, true, options, tools);
63
+ const response = await this.doRequest(body);
64
+
65
+ if (!response.ok || !response.body) {
66
+ const text = await response.text().catch(() => '');
67
+ throw new Error(`API error ${response.status}: ${text}`);
68
+ }
69
+
70
+ const reader = response.body.getReader();
71
+ const decoder = new TextDecoder();
72
+ let fullContent = '';
73
+ let buffer = '';
74
+ // Accumulate streaming tool call chunks
75
+ const toolCallBuffers: Map<number, { id: string; name: string; arguments: string }> = new Map();
76
+ // Some providers attach usage only on the last chunk (DeepSeek / OpenAI with
77
+ // stream_options.include_usage). We capture it here and yield at the end.
78
+ let capturedUsage: ChatResponse['usage'] | undefined;
79
+
80
+ try {
81
+ while (true) {
82
+ const { done, value } = await reader.read();
83
+ if (done) break;
84
+
85
+ buffer += decoder.decode(value, { stream: true });
86
+ const lines = buffer.split('\n');
87
+ buffer = lines.pop() || '';
88
+
89
+ for (const line of lines) {
90
+ const trimmed = line.trim();
91
+ if (!trimmed) continue;
92
+ if (trimmed === 'data: [DONE]') continue;
93
+ if (!trimmed.startsWith('data: ')) continue;
94
+
95
+ try {
96
+ const data = JSON.parse(trimmed.slice(6));
97
+ const delta = data.choices?.[0]?.delta;
98
+
99
+ // Text content
100
+ if (delta?.content) {
101
+ fullContent += delta.content;
102
+ yield { type: 'text', content: delta.content };
103
+ }
104
+
105
+ // Streaming tool calls - accumulate chunks
106
+ if (delta?.tool_calls) {
107
+ for (const tc of delta.tool_calls) {
108
+ const idx = tc.index ?? 0;
109
+ if (!toolCallBuffers.has(idx)) {
110
+ toolCallBuffers.set(idx, { id: '', name: '', arguments: '' });
111
+ }
112
+ const buf = toolCallBuffers.get(idx)!;
113
+ if (tc.id) buf.id = tc.id;
114
+ if (tc.function?.name) buf.name += tc.function.name;
115
+ if (tc.function?.arguments) buf.arguments += tc.function.arguments;
116
+ }
117
+ }
118
+
119
+ // DeepSeek/OpenAI stream-end usage (only present on the last chunk)
120
+ if (data.usage) {
121
+ capturedUsage = this.normalizeUsage(data.usage);
122
+ }
123
+ } catch {
124
+ // Skip invalid JSON lines
125
+ }
126
+ }
127
+ }
128
+ } finally {
129
+ reader.releaseLock();
130
+ }
131
+
132
+ // Convert accumulated tool call buffers to ToolCall[]
133
+ const toolCalls: ToolCall[] = [];
134
+ for (const [, buf] of toolCallBuffers) {
135
+ if (buf.id && buf.name) {
136
+ toolCalls.push({
137
+ id: buf.id,
138
+ type: 'function',
139
+ function: { name: buf.name, arguments: buf.arguments },
140
+ });
141
+ }
142
+ }
143
+
144
+ if (toolCalls.length > 0) {
145
+ yield { type: 'tool_calls', toolCalls };
146
+ }
147
+
148
+ // Yield captured usage so TUI / cache stats can observe cache hits
149
+ if (capturedUsage) {
150
+ yield { type: 'usage', usage: capturedUsage };
151
+ }
152
+
153
+ return {
154
+ content: fullContent,
155
+ role: 'assistant',
156
+ usage: capturedUsage,
157
+ tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
158
+ };
159
+ }
160
+
161
+ /**
162
+ * Build the request body for OpenAI-compatible API.
163
+ *
164
+ * Why stableStringify matters: DeepSeek (and most OpenAI-compatible APIs)
165
+ * perform automatic prefix cache lookup by byte-level hash of the request
166
+ * payload. If the JSON we send today differs from yesterday's by even one
167
+ * reordered key, the cache misses. Insertion-order stability is good enough
168
+ * when the same code path runs every time, but we now use stableStringify
169
+ * as a belt-and-suspenders guarantee against accidental key reordering from
170
+ * future refactors (spread / Object.fromEntries / map merging).
171
+ */
172
+ protected buildRequest(messages: ChatMessage[], stream: boolean, options?: ChatOptions, tools?: Tool[]) {
173
+ const body: any = {
174
+ model: this.config.model,
175
+ messages: messages.map(m => ({
176
+ role: m.role,
177
+ // content is string | ContentBlock[]. OpenAI wire format accepts both:
178
+ // - string for plain text messages
179
+ // - array of {type,text} or {type,image_url} blocks for multimodal
180
+ content: m.content,
181
+ ...(m.name && { name: m.name }),
182
+ ...(m.tool_call_id && { tool_call_id: m.tool_call_id }),
183
+ ...(m.tool_calls && { tool_calls: m.tool_calls }),
184
+ ...(m.cache_control && { cache_control: m.cache_control }),
185
+ })),
186
+ temperature: options?.temperature ?? this.config.temperature,
187
+ max_tokens: options?.maxTokens ?? this.config.maxTokens,
188
+ stream,
189
+ ...(stream && { stream_options: { include_usage: true } }),
190
+ };
191
+
192
+ // Add tools if provided - this is critical for structured tool calling
193
+ if (tools && tools.length > 0) {
194
+ body.tools = this.buildTools(tools);
195
+ }
196
+
197
+ return body;
198
+ }
199
+
200
+ /**
201
+ * Execute the HTTP request. Serializes via stableStringify so the byte
202
+ * sequence is deterministic across requests.
203
+ */
204
+ protected async doRequest(body: any): Promise<Response> {
205
+ const url = `${this.config.baseUrl}/chat/completions`;
206
+ const controller = new AbortController();
207
+ const timeout = setTimeout(() => controller.abort(), 60000); // 60s timeout
208
+
209
+ try {
210
+ const response = await fetch(url, {
211
+ method: 'POST',
212
+ headers: {
213
+ 'Content-Type': 'application/json',
214
+ 'Authorization': `Bearer ${this.config.apiKey}`,
215
+ },
216
+ body: stableStringify(body),
217
+ signal: controller.signal,
218
+ });
219
+ return response;
220
+ } finally {
221
+ clearTimeout(timeout);
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Map upstream usage JSON to the canonical Usage shape. Different providers
227
+ * attach different cache-related fields; we pass everything through so the
228
+ * cache stats layer can interpret per-provider.
229
+ */
230
+ protected normalizeUsage(raw: any): ChatResponse['usage'] {
231
+ return {
232
+ prompt_tokens: raw.prompt_tokens || 0,
233
+ completion_tokens: raw.completion_tokens || 0,
234
+ total_tokens: raw.total_tokens || 0,
235
+ // DeepSeek automatic prefix cache fields
236
+ prompt_cache_hit_tokens: raw.prompt_cache_hit_tokens,
237
+ prompt_cache_miss_tokens: raw.prompt_cache_miss_tokens,
238
+ // Anthropic prompt cache fields (passed through if relay forwards them)
239
+ cache_creation_input_tokens: raw.cache_creation_input_tokens,
240
+ cache_read_input_tokens: raw.cache_read_input_tokens,
241
+ };
242
+ }
243
+ }
@@ -1,34 +1,66 @@
1
- /**
2
- * Abstract LLM Provider interface
3
- * All providers implement this interface
4
- */
5
-
6
- import type { ChatMessage, ChatResponse, ChatOptions, ToolCall } from '../types.js';
7
- import type { Tool } from '../tools/types.js';
8
-
9
- export interface StreamChunk {
10
- type: 'text' | 'tool_calls';
11
- content?: string;
12
- toolCalls?: ToolCall[];
13
- }
14
-
15
- export interface LLMProvider {
16
- readonly name: string;
17
-
18
- /** Non-streaming chat with optional tools */
19
- chat(messages: ChatMessage[], options?: ChatOptions, tools?: Tool[]): Promise<ChatResponse>;
20
-
21
- /** Streaming chat with optional tools - yields StreamChunks */
22
- chatStream(messages: ChatMessage[], options?: ChatOptions, tools?: Tool[]): AsyncGenerator<StreamChunk, ChatResponse>;
23
-
24
- /** Convert Tool[] to provider-specific format */
25
- buildTools(tools: Tool[]): any[];
26
- }
27
-
28
- export interface ProviderConfig {
29
- apiKey: string;
30
- model: string;
31
- baseUrl: string;
32
- temperature: number;
33
- maxTokens: number;
34
- }
1
+ /**
2
+ * Abstract LLM Provider interface
3
+ * All providers implement this interface.
4
+ *
5
+ * v3.0.0+: StreamChunk is now imported from the shared types module so that
6
+ * TUI / session / cache layers can consume it uniformly. Providers no longer
7
+ * carry their own definition; see src/types.ts for the union (text/tool_calls/
8
+ * thinking/usage). The previous local 'text' | 'tool_calls' shape was kept as
9
+ * a structural type alias for backward compatibility with existing call sites
10
+ * that destructure by .type only.
11
+ */
12
+
13
+ import type { ChatMessage, ChatResponse, ChatOptions, StreamChunk as SharedStreamChunk } from '../types.js';
14
+ import type { Tool } from '../tools/types.js';
15
+
16
+ /**
17
+ * Re-export of the structured streaming chunk. Now structurally compatible
18
+ * with the old { type, content?, toolCalls? } shape — callers that previously
19
+ * destructured chunk.type and chunk.content / chunk.toolCalls still work, and
20
+ * new variants ('thinking', 'usage') are opt-in.
21
+ */
22
+ export type StreamChunk = SharedStreamChunk;
23
+
24
+ export interface LLMProvider {
25
+ readonly name: string;
26
+
27
+ /** Non-streaming chat with optional tools */
28
+ chat(messages: ChatMessage[], options?: ChatOptions, tools?: Tool[]): Promise<ChatResponse>;
29
+
30
+ /**
31
+ * Streaming chat with optional tools.
32
+ * Yields structured StreamChunks:
33
+ * { type: 'text', content: string }
34
+ * { type: 'tool_calls', toolCalls: ToolCall[] }
35
+ * { type: 'thinking', content: string }
36
+ * { type: 'usage', usage: Usage }
37
+ * Returns the final ChatResponse (with usage) when the stream completes.
38
+ */
39
+ chatStream(messages: ChatMessage[], options?: ChatOptions, tools?: Tool[]): AsyncGenerator<StreamChunk, ChatResponse>;
40
+
41
+ /** Convert Tool[] to provider-specific format */
42
+ buildTools(tools: Tool[]): any[];
43
+ }
44
+
45
+ export interface ProviderConfig {
46
+ apiKey: string;
47
+ model: string;
48
+ baseUrl: string;
49
+ temperature: number;
50
+ maxTokens: number;
51
+ /**
52
+ * Optional cache control policy. When omitted, providers fall back to their
53
+ * default behavior (Anthropic: explicit cache_control markers; OpenAI / Gemini
54
+ * / DeepSeek: automatic prefix cache, no markers needed).
55
+ */
56
+ cache?: {
57
+ enabled: boolean;
58
+ ttl?: '5m' | '1h';
59
+ /**
60
+ * 'auto' — provider default (Anthropic: explicit, OpenAI/DeepSeek: auto)
61
+ * 'manual' — always emit cache_control markers regardless of provider
62
+ * 'off' — never emit markers (force disable, useful for benchmarking)
63
+ */
64
+ strategy?: 'auto' | 'manual' | 'off';
65
+ };
66
+ }