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
package/src/llm/index.ts DELETED
@@ -1,303 +0,0 @@
1
- /**
2
- * LLM Service - Factory for creating providers
3
- * Supports all providers + custom relay stations (中转站)
4
- *
5
- * v3.0.0+: streaming protocol migrated to structured StreamChunk
6
- * - chatStream yields { type: 'text' | 'tool_calls' | 'thinking' | 'usage' }
7
- * - Tool call execution is now part of the agent loop; consumers see
8
- * { type: 'tool_calls', toolCalls } for dispatch and { type: 'text', content }
9
- * for model output. The legacy @@TOOL@@ sentinel-string protocol is GONE.
10
- * - Usage / cache stats are emitted as the final { type: 'usage' } chunk,
11
- * which the TUI cache stats store consumes.
12
- */
13
-
14
- import chalk from 'chalk';
15
- import type { ChatMessage, ChatResponse, ChatOptions, ToolCall, StreamChunk } from '../types.js';
16
- import type { Tool } from '../tools/types.js';
17
- import type { LLMProvider } from './provider.js';
18
- import type { AIConfig, Config, ProviderName } from '../config/types.js';
19
- import { PROVIDERS } from '../config/providers.js';
20
- import { OpenAIProvider } from './openai.js';
21
- import { AnthropicProvider } from './anthropic.js';
22
- import { GeminiProvider } from './gemini.js';
23
- import { decideTTL } from '../cache/smartModel.js';
24
-
25
- export class LLMService {
26
- private provider: LLMProvider;
27
- private tools: Map<string, Tool> = new Map();
28
- private apiKey: string;
29
-
30
- constructor(provider: LLMProvider, apiKey: string) {
31
- this.provider = provider;
32
- this.apiKey = apiKey;
33
- }
34
-
35
- /**
36
- * v3.0.3: TTL resolved from config + smart routing. Stays null until
37
- * the first round, then never changes for the session. Anthropic
38
- * provider's setResolvedTTL is called at the same time, so the wire
39
- * prefix is consistent across rounds.
40
- */
41
- private resolvedTtl: '5m' | '1h' | null = null;
42
-
43
- /** Public accessor used by App.streamResponse to surface TTL in the UI. */
44
- getResolvedTTL(): '5m' | '1h' | null {
45
- return this.resolvedTtl;
46
- }
47
-
48
- static fromConfig(config: AIConfig & { cache?: Config['cache'] }): LLMService {
49
- const providerName = config.provider || 'siliconflow';
50
- const providerConfig = PROVIDERS[providerName];
51
-
52
- const providerCfg = {
53
- apiKey: config.apiKey || '',
54
- model: config.model || providerConfig.defaultModel,
55
- baseUrl: config.baseUrl || providerConfig.baseUrl,
56
- temperature: config.temperature ?? 0.7,
57
- maxTokens: config.maxTokens ?? 4096,
58
- // v3.0.0: forward cache policy. Anthropic reads this to decide
59
- // whether to attach cache_control markers; other providers ignore it.
60
- // v3.0.3: ttl accepts '5m' | '1h' | 'auto'. 'auto' is resolved
61
- // per-session by decideTTL() inside chatStream.
62
- cache: config.cache ?? { enabled: true, ttl: 'auto' as const, strategy: 'auto' as const },
63
- };
64
-
65
- const format = providerConfig.format;
66
- let provider: LLMProvider;
67
-
68
- switch (format) {
69
- case 'anthropic':
70
- provider = new AnthropicProvider(providerCfg);
71
- break;
72
- case 'gemini':
73
- provider = new GeminiProvider(providerCfg);
74
- break;
75
- default:
76
- provider = new OpenAIProvider(providerCfg);
77
- }
78
-
79
- return new LLMService(provider, providerCfg.apiKey);
80
- }
81
-
82
- registerTools(tools: Tool[]): void {
83
- for (const tool of tools) {
84
- this.tools.set(tool.name, tool);
85
- }
86
- }
87
-
88
- getProviderName(): string { return this.provider.name; }
89
- hasApiKey(): boolean { return !!this.apiKey; }
90
-
91
- async chat(messages: ChatMessage[], options?: ChatOptions): Promise<ChatResponse> {
92
- if (!this.hasApiKey()) throw new Error(this.getNoKeyMessage());
93
- const toolsArray = [...this.tools.values()];
94
- return this.provider.chat(messages, options, toolsArray.length > 0 ? toolsArray : undefined);
95
- }
96
-
97
- /**
98
- * Streaming chat with agent loop (tool call support).
99
- *
100
- * Structured chunk protocol (replaces the @@TOOL@@ sentinel strings):
101
- * { type: 'text', content: string } model output, accumulate + render
102
- * { type: 'tool_calls', toolCalls: ToolCall[] } dispatch tools, results will
103
- * be folded into the next request
104
- * { type: 'thinking', content: string } reasoning text (consumers may
105
- * display in debug mode)
106
- * { type: 'usage', usage: Usage } cache hit/miss + token counts;
107
- * emitted on the final round
108
- *
109
- * Returns the final ChatResponse of the agent loop when done.
110
- */
111
- async *chatStream(
112
- messages: ChatMessage[],
113
- options?: ChatOptions & { maxIterations?: number }
114
- ): AsyncGenerator<StreamChunk, ChatResponse> {
115
- if (!this.hasApiKey()) throw new Error(this.getNoKeyMessage());
116
-
117
- // v3.0.3: Resolve TTL once per session.
118
- // v3.0.4: default is 1h (long-task). 'auto' (legacy config value)
119
- // resolves via decideTTL which now always returns 1h — we cannot
120
- // predict task length at round 0, so we default to the TTL that
121
- // cannot expire mid-task. '5m'/'1h' are explicit user pins.
122
- const configTtl = (this.provider as any).config?.cache?.ttl;
123
- if (configTtl === 'auto' && this.resolvedTtl === null) {
124
- const decision = decideTTL(messages, null);
125
- this.resolvedTtl = decision.ttl;
126
- if (typeof (this.provider as any).setResolvedTTL === 'function') {
127
- (this.provider as any).setResolvedTTL(decision.ttl);
128
- }
129
- } else if (configTtl === '5m' || configTtl === '1h') {
130
- // User pinned a specific TTL — apply it once and keep it.
131
- if (this.resolvedTtl === null) {
132
- this.resolvedTtl = configTtl;
133
- if (typeof (this.provider as any).setResolvedTTL === 'function') {
134
- (this.provider as any).setResolvedTTL(configTtl);
135
- }
136
- }
137
- }
138
-
139
- const maxIterations = options?.maxIterations ?? 10;
140
- let currentMessages = [...messages];
141
- let iterations = 0;
142
- let lastUsage: ChatResponse['usage'] | undefined;
143
-
144
- while (iterations < maxIterations) {
145
- iterations++;
146
- const toolsArray = [...this.tools.values()];
147
- const hasTools = toolsArray.length > 0;
148
-
149
- let fullContent = '';
150
- let detectedToolCalls: ToolCall[] | undefined;
151
-
152
- // Forward stream chunks from the provider. We collect text internally for
153
- // tool-call persistence but always re-emit the original chunks unchanged.
154
- const stream = this.provider.chatStream(currentMessages, options, hasTools ? toolsArray : undefined);
155
-
156
- for await (const chunk of stream) {
157
- if (chunk.type === 'text' && chunk.content) {
158
- fullContent += chunk.content;
159
- yield chunk;
160
- } else if (chunk.type === 'tool_calls' && chunk.toolCalls) {
161
- detectedToolCalls = chunk.toolCalls;
162
- // Don't yield the raw tool_calls chunk here — we emit one combined
163
- // chunk after persisting the assistant message so consumers don't
164
- // double-render.
165
- } else if (chunk.type === 'thinking') {
166
- yield chunk;
167
- } else if (chunk.type === 'usage') {
168
- lastUsage = chunk.usage;
169
- yield chunk;
170
- }
171
- }
172
-
173
- // If we got tool calls, execute them and loop
174
- if (detectedToolCalls && detectedToolCalls.length > 0) {
175
- // Add assistant message with tool calls (append-only, preserves prefix cache)
176
- currentMessages.push({
177
- role: 'assistant',
178
- content: fullContent || '',
179
- tool_calls: detectedToolCalls,
180
- });
181
-
182
- // Execute each tool and emit a single structured tool_calls chunk
183
- // describing the dispatch plan. Per-tool results are appended to
184
- // currentMessages but NOT yielded as additional chunks — consumers
185
- // that want to display result text can introspect tool_calls[*].result
186
- // on a synthesized combined chunk, OR we re-emit one tool_calls chunk
187
- // at the end with results attached. We choose the simpler approach:
188
- // emit ONE tool_calls chunk per iteration with all the calls; results
189
- // are surfaced through the next assistant turn's text content.
190
- //
191
- // v3.0.0 Tool-call Repair (Reasonix P2): on tool failure we APPEND
192
- // a `[TOOL_REPAIR]` system message rather than mutating the existing
193
- // tool_call message. This preserves the upstream cache prefix —
194
- // re-writing an earlier message would shift the prefix by N bytes
195
- // and bust the cache for every subsequent round.
196
- for (const toolCall of detectedToolCalls) {
197
- const tool = this.tools.get(toolCall.function.name);
198
-
199
- if (!tool) {
200
- const errMsg = `Tool "${toolCall.function.name}" not found`;
201
- // Repair message — explain the failure and tell the model to
202
- // try a different tool. Cache-safe because we are APPENDING,
203
- // never modifying existing messages.
204
- currentMessages.push({
205
- role: 'system',
206
- content: `[TOOL_REPAIR] Previous tool_call "${toolCall.function.name}" (id=${toolCall.id}) failed: ${errMsg}. Available tools: ${[...this.tools.keys()].join(', ')}.`,
207
- });
208
- currentMessages.push({
209
- role: 'tool',
210
- content: errMsg,
211
- tool_call_id: toolCall.id,
212
- name: toolCall.function.name,
213
- });
214
- continue;
215
- }
216
-
217
- try {
218
- const params = JSON.parse(toolCall.function.arguments || '{}');
219
- const result = await tool.execute(params);
220
- const output = result.success ? (result.output || JSON.stringify(result.data)) : (result.error || 'Tool failed');
221
-
222
- currentMessages.push({
223
- role: 'tool',
224
- content: output,
225
- tool_call_id: toolCall.id,
226
- name: toolCall.function.name,
227
- });
228
-
229
- if (!result.success) {
230
- // Soft failure: tool returned success=false. Same repair pattern.
231
- currentMessages.push({
232
- role: 'system',
233
- content: `[TOOL_REPAIR] Tool "${toolCall.function.name}" returned success=false: ${output}. Consider correcting the arguments and retrying.`,
234
- });
235
- }
236
- } catch (error: any) {
237
- const errMsg = `Error: ${error.message}`;
238
- currentMessages.push({
239
- role: 'tool',
240
- content: errMsg,
241
- tool_call_id: toolCall.id,
242
- name: toolCall.function.name,
243
- });
244
- // Hard failure: tool.execute threw. Repair message so the model
245
- // can see the failure next round and adjust (e.g. fix a path
246
- // typo, retry without the optional arg).
247
- currentMessages.push({
248
- role: 'system',
249
- content: `[TOOL_REPAIR] Tool "${toolCall.function.name}" threw an exception: ${errMsg}. Inspect the arguments and retry with a corrected call.`,
250
- });
251
- }
252
- }
253
-
254
- // Emit one tool_calls chunk for this iteration. TUI renders this as a
255
- // "tool invoked" entry. Per-tool outputs are summarized on the next
256
- // assistant turn.
257
- yield { type: 'tool_calls', toolCalls: detectedToolCalls };
258
- continue;
259
- }
260
-
261
- // No tool calls - done. Return final response (with usage if we have it).
262
- return {
263
- content: fullContent,
264
- role: 'assistant',
265
- usage: lastUsage,
266
- };
267
- }
268
-
269
- return { content: '[Agent loop exceeded maximum iterations]', role: 'assistant' };
270
- }
271
-
272
- private truncateArgs(args: string): string {
273
- try {
274
- const obj = JSON.parse(args || '{}');
275
- const entries = Object.entries(obj);
276
- if (entries.length === 0) return '';
277
- return entries.map(([k, v]) => {
278
- const val = typeof v === 'string' && v.length > 50 ? v.slice(0, 50) + '...' : v;
279
- return `${k}: ${JSON.stringify(val)}`;
280
- }).join(', ');
281
- } catch {
282
- return args.length > 80 ? args.slice(0, 80) + '...' : args;
283
- }
284
- }
285
-
286
- private getNoKeyMessage(): string {
287
- return [
288
- '❌ 未配置 API Key,无法调用 AI。',
289
- '',
290
- '请先运行: gfcode init',
291
- '',
292
- '或设置环境变量:',
293
- ' export SILICONFLOW_API_KEY="sk-..."',
294
- ' export OPENAI_API_KEY="sk-..."',
295
- ' export DEEPSEEK_API_KEY="sk-..."',
296
- ].join('\n');
297
- }
298
- }
299
-
300
- export type { LLMProvider, ProviderConfig, StreamChunk } from './provider.js';
301
- export { OpenAIProvider } from './openai.js';
302
- export { AnthropicProvider } from './anthropic.js';
303
- export { GeminiProvider } from './gemini.js';
package/src/llm/openai.ts DELETED
@@ -1,243 +0,0 @@
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,71 +0,0 @@
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
- * v3.0.3: `ttl` accepts '5m' | '1h' | 'auto'. 'auto' lets the runtime
57
- * decide per-session via decideTTL() in cache/smartModel.ts. The actual
58
- * value used at request time is what gets stored on the provider's
59
- * resolvedTtl field (set by LLMService.chatStream).
60
- */
61
- cache?: {
62
- enabled: boolean;
63
- ttl?: '5m' | '1h' | 'auto';
64
- /**
65
- * 'auto' — provider default (Anthropic: explicit, OpenAI/DeepSeek: auto)
66
- * 'manual' — always emit cache_control markers regardless of provider
67
- * 'off' — never emit markers (force disable, useful for benchmarking)
68
- */
69
- strategy?: 'auto' | 'manual' | 'off';
70
- };
71
- }