lonny-agent 0.2.3 → 0.2.6

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 (129) hide show
  1. package/README.md +326 -326
  2. package/dist/agent/index.js +1 -1
  3. package/dist/agent/index.js.map +1 -1
  4. package/dist/agent/llm.d.ts +4 -0
  5. package/dist/agent/llm.d.ts.map +1 -1
  6. package/dist/agent/project.d.ts +26 -0
  7. package/dist/agent/project.d.ts.map +1 -0
  8. package/dist/agent/project.js +303 -0
  9. package/dist/agent/project.js.map +1 -0
  10. package/dist/agent/prompt-builder.d.ts +1 -1
  11. package/dist/agent/prompt-builder.d.ts.map +1 -1
  12. package/dist/agent/prompt-builder.js +22 -13
  13. package/dist/agent/prompt-builder.js.map +1 -1
  14. package/dist/agent/providers/anthropic.d.ts.map +1 -1
  15. package/dist/agent/providers/anthropic.js +20 -2
  16. package/dist/agent/providers/anthropic.js.map +1 -1
  17. package/dist/agent/providers/google.d.ts.map +1 -1
  18. package/dist/agent/providers/google.js +1 -1
  19. package/dist/agent/providers/google.js.map +1 -1
  20. package/dist/agent/providers/ollama.d.ts.map +1 -1
  21. package/dist/agent/providers/ollama.js +2 -1
  22. package/dist/agent/providers/ollama.js.map +1 -1
  23. package/dist/agent/providers/openai.d.ts.map +1 -1
  24. package/dist/agent/providers/openai.js +63 -3
  25. package/dist/agent/providers/openai.js.map +1 -1
  26. package/dist/agent/session.d.ts +8 -2
  27. package/dist/agent/session.d.ts.map +1 -1
  28. package/dist/agent/session.js +250 -153
  29. package/dist/agent/session.js.map +1 -1
  30. package/dist/config/index.d.ts.map +1 -1
  31. package/dist/config/index.js +7 -1
  32. package/dist/config/index.js.map +1 -1
  33. package/dist/tools/__tests__/bash.test.js +1 -1
  34. package/dist/tools/__tests__/bash.test.js.map +1 -1
  35. package/dist/tools/__tests__/edit.test.js +875 -371
  36. package/dist/tools/__tests__/edit.test.js.map +1 -1
  37. package/dist/tools/__tests__/grep.test.js +32 -0
  38. package/dist/tools/__tests__/grep.test.js.map +1 -1
  39. package/dist/tools/__tests__/sed.test.d.ts +2 -0
  40. package/dist/tools/__tests__/sed.test.d.ts.map +1 -0
  41. package/dist/tools/__tests__/sed.test.js +228 -0
  42. package/dist/tools/__tests__/sed.test.js.map +1 -0
  43. package/dist/tools/bash.d.ts.map +1 -1
  44. package/dist/tools/bash.js +83 -10
  45. package/dist/tools/bash.js.map +1 -1
  46. package/dist/tools/edit.d.ts +58 -0
  47. package/dist/tools/edit.d.ts.map +1 -1
  48. package/dist/tools/edit.js +366 -149
  49. package/dist/tools/edit.js.map +1 -1
  50. package/dist/tools/grep.d.ts.map +1 -1
  51. package/dist/tools/grep.js +54 -30
  52. package/dist/tools/grep.js.map +1 -1
  53. package/dist/tools/read.d.ts +6 -0
  54. package/dist/tools/read.d.ts.map +1 -1
  55. package/dist/tools/read.js +42 -15
  56. package/dist/tools/read.js.map +1 -1
  57. package/dist/tools/registry.d.ts +0 -6
  58. package/dist/tools/registry.d.ts.map +1 -1
  59. package/dist/tools/registry.js +2 -33
  60. package/dist/tools/registry.js.map +1 -1
  61. package/dist/tools/sed.d.ts +4 -0
  62. package/dist/tools/sed.d.ts.map +1 -0
  63. package/dist/tools/sed.js +121 -0
  64. package/dist/tools/sed.js.map +1 -0
  65. package/dist/tools/write_plan.d.ts +1 -1
  66. package/dist/tools/write_plan.d.ts.map +1 -1
  67. package/dist/tools/write_plan.js +14 -4
  68. package/dist/tools/write_plan.js.map +1 -1
  69. package/dist/tui/index.d.ts.map +1 -1
  70. package/dist/tui/index.js +5 -4
  71. package/dist/tui/index.js.map +1 -1
  72. package/dist/web/index.d.ts.map +1 -1
  73. package/dist/web/index.js +30 -7
  74. package/dist/web/index.js.map +1 -1
  75. package/dist/web/public/app.js +5 -855
  76. package/dist/web/public/confirm.js +70 -0
  77. package/dist/web/public/index.html +121 -120
  78. package/dist/web/public/input.js +107 -0
  79. package/dist/web/public/messages.js +395 -0
  80. package/dist/web/public/sidebar.js +82 -0
  81. package/dist/web/public/state.js +72 -0
  82. package/dist/web/public/style.css +996 -949
  83. package/dist/web/public/utils.js +37 -0
  84. package/dist/web/public/websocket.js +267 -0
  85. package/dist/web/public/ws.js +19 -0
  86. package/dist/web/session-bridge.d.ts.map +1 -1
  87. package/dist/web/session-bridge.js +3 -7
  88. package/dist/web/session-bridge.js.map +1 -1
  89. package/package.json +55 -54
  90. package/scripts/copy-native.mjs +24 -0
  91. package/scripts/copy-web.mjs +15 -0
  92. package/src/agent/index.ts +25 -25
  93. package/src/agent/llm.ts +4 -0
  94. package/src/agent/project.ts +366 -0
  95. package/src/agent/prompt-builder.ts +23 -13
  96. package/src/agent/providers/anthropic.ts +179 -172
  97. package/src/agent/providers/google.ts +210 -211
  98. package/src/agent/providers/ollama.ts +186 -182
  99. package/src/agent/providers/openai.ts +73 -5
  100. package/src/agent/session.ts +275 -153
  101. package/src/config/index.ts +342 -335
  102. package/src/tools/__tests__/bash.test.ts +1 -1
  103. package/src/tools/__tests__/edit.test.ts +981 -385
  104. package/src/tools/__tests__/grep.test.ts +35 -0
  105. package/src/tools/bash.ts +97 -11
  106. package/src/tools/edit.ts +414 -154
  107. package/src/tools/git.ts +76 -76
  108. package/src/tools/grep.ts +57 -30
  109. package/src/tools/read.ts +61 -25
  110. package/src/tools/registry.ts +231 -265
  111. package/src/tools/write_plan.ts +21 -5
  112. package/src/tui/index.ts +786 -785
  113. package/src/web/index.ts +29 -7
  114. package/src/web/public/app.js +5 -855
  115. package/src/web/public/confirm.js +70 -0
  116. package/src/web/public/index.html +121 -120
  117. package/src/web/public/input.js +107 -0
  118. package/src/web/public/messages.js +395 -0
  119. package/src/web/public/sidebar.js +82 -0
  120. package/src/web/public/state.js +72 -0
  121. package/src/web/public/style.css +996 -949
  122. package/src/web/public/utils.js +37 -0
  123. package/src/web/public/websocket.js +267 -0
  124. package/src/web/public/ws.js +19 -0
  125. package/src/web/session-bridge.ts +193 -194
  126. package/.kilo/plans/1780064105789-mighty-pixel.md +0 -171
  127. package/.kilo/plans/1780293725888-quick-wizard.md +0 -62
  128. package/.lonny/plan-web-cwd-status.md +0 -38
  129. package/src/tools/exec.ts +0 -348
@@ -1,182 +1,186 @@
1
- import { ToolCall, type ToolDefinition } from '../../tools/types.js'
2
- import type { LLMChunk, LLMMessage, LLMProvider } from '../llm.js'
3
-
4
- interface OllamaTool {
5
- type: 'function'
6
- function: {
7
- name: string
8
- description: string
9
- parameters: Record<string, unknown>
10
- }
11
- }
12
-
13
- interface OllamaMessage {
14
- role: 'system' | 'user' | 'assistant' | 'tool'
15
- content: string
16
- tool_calls?: {
17
- function: {
18
- name: string
19
- arguments: string
20
- }
21
- }[]
22
- }
23
-
24
- interface OllamaResponse {
25
- model: string
26
- created_at: string
27
- message: OllamaMessage
28
- done: boolean
29
- done_reason?: string
30
- prompt_eval_count?: number
31
- eval_count?: number
32
- }
33
-
34
- export class OllamaProvider implements LLMProvider {
35
- private baseUrl: string
36
- private model: string
37
-
38
- constructor(_apiKey: string, baseURL?: string, model?: string) {
39
- this.baseUrl = baseURL || 'http://localhost:11434'
40
- this.model = model || 'llama3.2'
41
- }
42
-
43
- async *chat(
44
- messages: LLMMessage[],
45
- tools: ToolDefinition[],
46
- signal?: AbortSignal,
47
- ): AsyncGenerator<LLMChunk> {
48
- // Build Ollama-format messages
49
- const ollamaMessages: OllamaMessage[] = messages.map(m => {
50
- if (m.role === 'system') {
51
- return { role: 'system', content: m.content || '' }
52
- }
53
- if (m.role === 'user') {
54
- return { role: 'user', content: m.content || '' }
55
- }
56
- if (m.role === 'assistant') {
57
- if (m.tool_calls && m.tool_calls.length > 0) {
58
- return {
59
- role: 'assistant',
60
- content: m.content || '',
61
- tool_calls: m.tool_calls.map(tc => ({
62
- function: {
63
- name: tc.name,
64
- arguments: JSON.stringify(tc.input),
65
- },
66
- })),
67
- }
68
- }
69
- return { role: 'assistant', content: m.content || '' }
70
- }
71
- if (m.role === 'tool') {
72
- return { role: 'tool', content: m.content || '' }
73
- }
74
- return { role: 'user', content: '' }
75
- })
76
-
77
- // Build tools
78
- const ollamaTools: OllamaTool[] | undefined =
79
- tools.length > 0
80
- ? tools.map(t => {
81
- const properties: Record<string, unknown> = {}
82
- for (const [key, param] of Object.entries(t.parameters)) {
83
- const { required: _, ...rest } = param
84
- properties[key] = rest
85
- }
86
- return {
87
- type: 'function' as const,
88
- function: {
89
- name: t.name,
90
- description: t.description,
91
- parameters: {
92
- type: 'object',
93
- properties,
94
- required: Object.entries(t.parameters)
95
- .filter(([, v]) => v.required)
96
- .map(([k]) => k),
97
- },
98
- },
99
- }
100
- })
101
- : undefined
102
-
103
- const body: Record<string, unknown> = {
104
- model: this.model,
105
- messages: ollamaMessages,
106
- stream: true,
107
- }
108
- if (ollamaTools && ollamaTools.length > 0) {
109
- body.tools = ollamaTools
110
- }
111
-
112
- const response = await fetch(`${this.baseUrl}/api/chat`, {
113
- method: 'POST',
114
- headers: { 'Content-Type': 'application/json' },
115
- signal,
116
- body: JSON.stringify(body),
117
- })
118
-
119
- if (!response.ok) {
120
- const errText = await response.text()
121
- throw new Error(`Ollama API error (${response.status}): ${errText}`)
122
- }
123
-
124
- const reader = response.body?.getReader()
125
- if (!reader) throw new Error('No response body')
126
-
127
- const decoder = new TextDecoder()
128
- let buffer = ''
129
-
130
- while (true) {
131
- const { done, value } = await reader.read()
132
- if (done) break
133
-
134
- buffer += decoder.decode(value, { stream: true })
135
- const lines = buffer.split('\n')
136
- buffer = lines.pop() || ''
137
-
138
- for (const line of lines) {
139
- if (!line.trim()) continue
140
- try {
141
- const chunk = JSON.parse(line) as OllamaResponse
142
-
143
- if (chunk.message?.content) {
144
- yield { type: 'text', text: chunk.message.content }
145
- }
146
-
147
- if (chunk.message?.tool_calls) {
148
- for (const tc of chunk.message.tool_calls) {
149
- let input: Record<string, unknown> = {}
150
- try {
151
- input = JSON.parse(tc.function.arguments || '{}')
152
- } catch {
153
- input = {}
154
- }
155
- yield {
156
- type: 'tool_use',
157
- tool_call: {
158
- id: `ollama-${tc.function.name}-${Date.now()}`,
159
- name: tc.function.name,
160
- input,
161
- },
162
- }
163
- }
164
- }
165
-
166
- if (chunk.done) {
167
- yield {
168
- type: 'complete',
169
- finish_reason: chunk.done_reason || 'stop',
170
- usage: {
171
- input_tokens: chunk.prompt_eval_count ?? 0,
172
- output_tokens: chunk.eval_count ?? 0,
173
- },
174
- }
175
- }
176
- } catch {
177
- // skip malformed JSON
178
- }
179
- }
180
- }
181
- }
182
- }
1
+ import { ToolCall, type ToolDefinition } from '../../tools/types.js'
2
+ import type { LLMChunk, LLMMessage, LLMProvider } from '../llm.js'
3
+
4
+ interface OllamaTool {
5
+ type: 'function'
6
+ function: {
7
+ name: string
8
+ description: string
9
+ parameters: Record<string, unknown>
10
+ }
11
+ }
12
+
13
+ interface OllamaMessage {
14
+ role: 'system' | 'user' | 'assistant' | 'tool'
15
+ content: string
16
+ tool_calls?: {
17
+ function: {
18
+ name: string
19
+ arguments: string
20
+ }
21
+ }[]
22
+ }
23
+
24
+ interface OllamaResponse {
25
+ model: string
26
+ created_at: string
27
+ message: OllamaMessage
28
+ done: boolean
29
+ done_reason?: string
30
+ prompt_eval_count?: number
31
+ eval_count?: number
32
+ }
33
+
34
+ export class OllamaProvider implements LLMProvider {
35
+ private baseUrl: string
36
+ private model: string
37
+
38
+ constructor(_apiKey: string, baseURL?: string, model?: string) {
39
+ this.baseUrl = baseURL || 'http://localhost:11434'
40
+ this.model = model || 'llama3.2'
41
+ }
42
+
43
+ async *chat(
44
+ messages: LLMMessage[],
45
+ tools: ToolDefinition[],
46
+ signal?: AbortSignal,
47
+ ): AsyncGenerator<LLMChunk> {
48
+ // Build Ollama-format messages
49
+ const ollamaMessages: OllamaMessage[] = messages.map(m => {
50
+ if (m.role === 'system') {
51
+ return { role: 'system', content: m.content || '' }
52
+ }
53
+ if (m.role === 'user') {
54
+ return { role: 'user', content: m.content || '' }
55
+ }
56
+ if (m.role === 'assistant') {
57
+ if (m.tool_calls && m.tool_calls.length > 0) {
58
+ return {
59
+ role: 'assistant',
60
+ content: m.content || '',
61
+ tool_calls: m.tool_calls.map(tc => ({
62
+ function: {
63
+ name: tc.name,
64
+ arguments: JSON.stringify(tc.input),
65
+ },
66
+ })),
67
+ }
68
+ }
69
+ return { role: 'assistant', content: m.content || '' }
70
+ }
71
+ if (m.role === 'tool') {
72
+ return { role: 'tool', content: m.content || '' }
73
+ }
74
+ return { role: 'user', content: '' }
75
+ })
76
+
77
+ // Build tools
78
+ const ollamaTools: OllamaTool[] | undefined =
79
+ tools.length > 0
80
+ ? tools.map(t => {
81
+ const properties: Record<string, unknown> = {}
82
+ for (const [key, param] of Object.entries(t.parameters)) {
83
+ const { required: _, ...rest } = param
84
+ properties[key] = rest
85
+ }
86
+ return {
87
+ type: 'function' as const,
88
+ function: {
89
+ name: t.name,
90
+ description: t.description,
91
+ parameters: {
92
+ type: 'object',
93
+ properties,
94
+ required: Object.entries(t.parameters)
95
+ .filter(([, v]) => v.required)
96
+ .map(([k]) => k),
97
+ },
98
+ },
99
+ }
100
+ })
101
+ : undefined
102
+
103
+ const body: Record<string, unknown> = {
104
+ model: this.model,
105
+ messages: ollamaMessages,
106
+ stream: true,
107
+ }
108
+ if (ollamaTools && ollamaTools.length > 0) {
109
+ body.tools = ollamaTools
110
+ }
111
+
112
+ const response = await fetch(`${this.baseUrl}/api/chat`, {
113
+ method: 'POST',
114
+ headers: { 'Content-Type': 'application/json' },
115
+ signal,
116
+ body: JSON.stringify(body),
117
+ })
118
+
119
+ if (!response.ok) {
120
+ const errText = await response.text()
121
+ throw new Error(`Ollama API error (${response.status}): ${errText}`)
122
+ }
123
+
124
+ const reader = response.body?.getReader()
125
+ if (!reader) throw new Error('No response body')
126
+
127
+ const decoder = new TextDecoder()
128
+ let buffer = ''
129
+
130
+ while (true) {
131
+ const { done, value } = await reader.read()
132
+ if (done) break
133
+
134
+ buffer += decoder.decode(value, { stream: true })
135
+ const lines = buffer.split('\n')
136
+ buffer = lines.pop() || ''
137
+
138
+ for (const line of lines) {
139
+ if (!line.trim()) continue
140
+ try {
141
+ const chunk = JSON.parse(line) as OllamaResponse
142
+
143
+ if (chunk.message?.content) {
144
+ yield { type: 'text', text: chunk.message.content }
145
+ }
146
+
147
+ if (chunk.message?.tool_calls) {
148
+ for (const tc of chunk.message.tool_calls) {
149
+ let input: Record<string, unknown> = {}
150
+ try {
151
+ input = JSON.parse(tc.function.arguments || '{}')
152
+ } catch {
153
+ console.error(
154
+ '[ollama] Failed to parse tool_call arguments:',
155
+ tc.function.arguments,
156
+ )
157
+ input = {}
158
+ }
159
+ yield {
160
+ type: 'tool_use',
161
+ tool_call: {
162
+ id: `ollama-${tc.function.name}-${Date.now()}`,
163
+ name: tc.function.name,
164
+ input,
165
+ },
166
+ }
167
+ }
168
+ }
169
+
170
+ if (chunk.done) {
171
+ yield {
172
+ type: 'complete',
173
+ finish_reason: chunk.done_reason || 'stop',
174
+ usage: {
175
+ input_tokens: chunk.prompt_eval_count ?? 0,
176
+ output_tokens: chunk.eval_count ?? 0,
177
+ },
178
+ }
179
+ }
180
+ } catch {
181
+ console.error('[ollama] Failed to parse stream line:', line)
182
+ }
183
+ }
184
+ }
185
+ }
186
+ }
@@ -21,6 +21,38 @@ interface ExtendedCreateParams {
21
21
  enable_cache?: boolean
22
22
  }
23
23
 
24
+ /** Try to parse JSON, and attempt to repair common issues like unescaped quotes */
25
+ function tryParseJSON(input: string): Record<string, unknown> | null {
26
+ try {
27
+ return JSON.parse(input)
28
+ } catch {
29
+ // Try to repair common JSON issues from LLM output
30
+ let repaired = input
31
+
32
+ // Fix unescaped quotes in string values:
33
+ // Pattern: key="unquoted text" -> key="escaped text"
34
+ // This regex finds ="<not properly escaped>" and adds escaping
35
+ // It handles: ="<content with spaces and 中文">
36
+ repaired = repaired.replace(/= "([^"\\]*(?:\\.[^"\\]*)*)"/g, (match, content) => {
37
+ // Escape any unescaped quotes within the content
38
+ const escaped = content.replace(/(?<!\\)"/g, '\\"')
39
+ return `= "${escaped}"`
40
+ })
41
+
42
+ // Also try escaping quotes after @ (for @click="...")
43
+ repaired = repaired.replace(/@(\w+)="([^"\\]*(?:\\.[^"\\]*)*)"/g, (match, attr, content) => {
44
+ const escaped = content.replace(/(?<!\\)"/g, '\\"')
45
+ return `@${attr}="${escaped}"`
46
+ })
47
+
48
+ try {
49
+ return JSON.parse(repaired)
50
+ } catch {
51
+ return null
52
+ }
53
+ }
54
+ }
55
+
24
56
  export class OpenAIProvider implements LLMProvider {
25
57
  private client: OpenAI
26
58
  private model: string
@@ -146,7 +178,14 @@ export class OpenAIProvider implements LLMProvider {
146
178
  } | null = null
147
179
  let fullText = ''
148
180
  let reasoningContent: string | undefined
149
- let lastUsage: { prompt_tokens?: number; completion_tokens?: number } | undefined
181
+ let lastUsage:
182
+ | {
183
+ prompt_tokens?: number
184
+ completion_tokens?: number
185
+ prompt_cache_hit_tokens?: number
186
+ prompt_cache_miss_tokens?: number
187
+ }
188
+ | undefined
150
189
 
151
190
  // Track a pending "complete" yield — OpenAI sends finish_reason in a content
152
191
  // chunk but sends usage in a *separate final chunk* with no choices/delta.
@@ -159,7 +198,15 @@ export class OpenAIProvider implements LLMProvider {
159
198
  for await (const chunk of stream) {
160
199
  // Capture usage info if present (may come in a chunk without choices)
161
200
  // OpenAI uses prompt_tokens/completion_tokens; map to our input_tokens/output_tokens
162
- const rawChunk = chunk as { usage?: { prompt_tokens?: number; completion_tokens?: number } }
201
+ // DeepSeek also returns prompt_cache_hit_tokens & prompt_cache_miss_tokens
202
+ const rawChunk = chunk as {
203
+ usage?: {
204
+ prompt_tokens?: number
205
+ completion_tokens?: number
206
+ prompt_cache_hit_tokens?: number
207
+ prompt_cache_miss_tokens?: number
208
+ }
209
+ }
163
210
  if (rawChunk.usage) {
164
211
  lastUsage = rawChunk.usage
165
212
  // If we have a pending complete, yield it now with usage
@@ -171,6 +218,8 @@ export class OpenAIProvider implements LLMProvider {
171
218
  usage: {
172
219
  input_tokens: rawChunk.usage.prompt_tokens ?? 0,
173
220
  output_tokens: rawChunk.usage.completion_tokens ?? 0,
221
+ prompt_cache_hit_tokens: rawChunk.usage.prompt_cache_hit_tokens,
222
+ prompt_cache_miss_tokens: rawChunk.usage.prompt_cache_miss_tokens,
174
223
  },
175
224
  }
176
225
  pendingComplete = null
@@ -201,9 +250,14 @@ export class OpenAIProvider implements LLMProvider {
201
250
  if (tc.id) {
202
251
  if (currentToolCall) {
203
252
  let input: Record<string, unknown>
253
+ const rawArgs = currentToolCall.arguments || ''
204
254
  try {
205
- input = JSON.parse(currentToolCall.arguments || '{}')
255
+ input = tryParseJSON(rawArgs || '{}') || {}
206
256
  } catch {
257
+ console.error(
258
+ '[openai] Failed to parse tool_call arguments (flush on new tool):',
259
+ rawArgs,
260
+ )
207
261
  input = {}
208
262
  }
209
263
  yield {
@@ -230,17 +284,23 @@ export class OpenAIProvider implements LLMProvider {
230
284
 
231
285
  if (chunk.choices?.[0]?.finish_reason) {
232
286
  if (currentToolCall) {
287
+ const finalArgs = currentToolCall.arguments || ''
233
288
  try {
289
+ const parsed = tryParseJSON(finalArgs || '{}')
234
290
  yield {
235
291
  type: 'tool_use',
236
292
  tool_call: {
237
293
  id: currentToolCall.id,
238
294
  name: currentToolCall.name,
239
- input: JSON.parse(currentToolCall.arguments || '{}'),
295
+ input: parsed || {},
240
296
  },
241
297
  reasoning_content: reasoningContent,
242
298
  }
243
299
  } catch {
300
+ console.error(
301
+ '[openai] Failed to parse tool_call arguments (finish_reason):',
302
+ finalArgs,
303
+ )
244
304
  yield {
245
305
  type: 'tool_use',
246
306
  tool_call: {
@@ -264,6 +324,8 @@ export class OpenAIProvider implements LLMProvider {
264
324
  usage: {
265
325
  input_tokens: lastUsage.prompt_tokens ?? 0,
266
326
  output_tokens: lastUsage.completion_tokens ?? 0,
327
+ prompt_cache_hit_tokens: lastUsage.prompt_cache_hit_tokens,
328
+ prompt_cache_miss_tokens: lastUsage.prompt_cache_miss_tokens,
267
329
  },
268
330
  }
269
331
  reasoningContent = undefined
@@ -284,6 +346,8 @@ export class OpenAIProvider implements LLMProvider {
284
346
  ? {
285
347
  input_tokens: lastUsage.prompt_tokens ?? 0,
286
348
  output_tokens: lastUsage.completion_tokens ?? 0,
349
+ prompt_cache_hit_tokens: lastUsage.prompt_cache_hit_tokens,
350
+ prompt_cache_miss_tokens: lastUsage.prompt_cache_miss_tokens,
287
351
  }
288
352
  : undefined
289
353
  yield {
@@ -300,12 +364,16 @@ export class OpenAIProvider implements LLMProvider {
300
364
  ? {
301
365
  input_tokens: lastUsage.prompt_tokens ?? 0,
302
366
  output_tokens: lastUsage.completion_tokens ?? 0,
367
+ prompt_cache_hit_tokens: lastUsage.prompt_cache_hit_tokens,
368
+ prompt_cache_miss_tokens: lastUsage.prompt_cache_miss_tokens,
303
369
  }
304
370
  : undefined
305
371
  let input: Record<string, unknown>
372
+ const rawFinalArgs = currentToolCall.arguments || ''
306
373
  try {
307
- input = JSON.parse(currentToolCall.arguments || '{}')
374
+ input = tryParseJSON(rawFinalArgs || '{}') || {}
308
375
  } catch {
376
+ console.error('[openai] Failed to parse tool_call arguments (final flush):', rawFinalArgs)
309
377
  input = {}
310
378
  }
311
379
  yield {