lonny-agent 0.2.4 → 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.
- package/README.md +326 -326
- package/dist/agent/llm.d.ts +4 -0
- package/dist/agent/llm.d.ts.map +1 -1
- package/dist/agent/prompt-builder.d.ts.map +1 -1
- package/dist/agent/prompt-builder.js +13 -9
- package/dist/agent/prompt-builder.js.map +1 -1
- package/dist/agent/providers/google.d.ts.map +1 -1
- package/dist/agent/providers/google.js.map +1 -1
- package/dist/agent/providers/openai.d.ts.map +1 -1
- package/dist/agent/providers/openai.js +43 -3
- package/dist/agent/providers/openai.js.map +1 -1
- package/dist/agent/session.d.ts +4 -0
- package/dist/agent/session.d.ts.map +1 -1
- package/dist/agent/session.js +45 -7
- package/dist/agent/session.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +7 -1
- package/dist/config/index.js.map +1 -1
- package/dist/tools/__tests__/bash.test.js +1 -1
- package/dist/tools/__tests__/bash.test.js.map +1 -1
- package/dist/tools/__tests__/edit.test.js +875 -371
- package/dist/tools/__tests__/edit.test.js.map +1 -1
- package/dist/tools/__tests__/grep.test.js +32 -0
- package/dist/tools/__tests__/grep.test.js.map +1 -1
- package/dist/tools/__tests__/sed.test.d.ts +2 -0
- package/dist/tools/__tests__/sed.test.d.ts.map +1 -0
- package/dist/tools/__tests__/sed.test.js +228 -0
- package/dist/tools/__tests__/sed.test.js.map +1 -0
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +83 -10
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit.d.ts +50 -9
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +328 -158
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +54 -30
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/registry.d.ts +0 -6
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +2 -29
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/sed.d.ts +4 -0
- package/dist/tools/sed.d.ts.map +1 -0
- package/dist/tools/sed.js +121 -0
- package/dist/tools/sed.js.map +1 -0
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +26 -3
- package/dist/web/index.js.map +1 -1
- package/dist/web/public/app.js +5 -855
- package/dist/web/public/confirm.js +70 -0
- package/dist/web/public/index.html +121 -120
- package/dist/web/public/input.js +107 -0
- package/dist/web/public/messages.js +395 -0
- package/dist/web/public/sidebar.js +82 -0
- package/dist/web/public/state.js +72 -0
- package/dist/web/public/style.css +996 -949
- package/dist/web/public/utils.js +37 -0
- package/dist/web/public/websocket.js +267 -0
- package/dist/web/public/ws.js +19 -0
- package/dist/web/session-bridge.d.ts.map +1 -1
- package/dist/web/session-bridge.js +3 -7
- package/dist/web/session-bridge.js.map +1 -1
- package/package.json +55 -54
- package/scripts/copy-native.mjs +24 -0
- package/scripts/copy-web.mjs +15 -0
- package/src/agent/index.ts +25 -25
- package/src/agent/llm.ts +4 -0
- package/src/agent/prompt-builder.ts +13 -9
- package/src/agent/providers/anthropic.ts +179 -179
- package/src/agent/providers/google.ts +210 -211
- package/src/agent/providers/ollama.ts +186 -186
- package/src/agent/providers/openai.ts +61 -5
- package/src/agent/session.ts +59 -3
- package/src/config/index.ts +342 -335
- package/src/tools/__tests__/bash.test.ts +1 -1
- package/src/tools/__tests__/edit.test.ts +981 -385
- package/src/tools/__tests__/grep.test.ts +35 -0
- package/src/tools/bash.ts +97 -11
- package/src/tools/edit.ts +362 -169
- package/src/tools/git.ts +76 -76
- package/src/tools/grep.ts +57 -30
- package/src/tools/registry.ts +2 -31
- package/src/tui/index.ts +786 -786
- package/src/web/index.ts +25 -3
- package/src/web/public/app.js +5 -855
- package/src/web/public/confirm.js +70 -0
- package/src/web/public/index.html +121 -120
- package/src/web/public/input.js +107 -0
- package/src/web/public/messages.js +395 -0
- package/src/web/public/sidebar.js +82 -0
- package/src/web/public/state.js +72 -0
- package/src/web/public/style.css +996 -949
- package/src/web/public/utils.js +37 -0
- package/src/web/public/websocket.js +267 -0
- package/src/web/public/ws.js +19 -0
- package/src/web/session-bridge.ts +193 -194
- package/.claude/settings.local.json +0 -14
- package/.kilo/plans/1780064105789-mighty-pixel.md +0 -171
- package/.kilo/plans/1780293725888-quick-wizard.md +0 -62
- package/.lonny/plan-web-cwd-status.md +0 -38
- package/src/tools/exec.ts +0 -348
|
@@ -1,186 +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
|
-
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
|
-
}
|
|
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:
|
|
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
|
-
|
|
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
|
|
@@ -203,7 +252,7 @@ export class OpenAIProvider implements LLMProvider {
|
|
|
203
252
|
let input: Record<string, unknown>
|
|
204
253
|
const rawArgs = currentToolCall.arguments || ''
|
|
205
254
|
try {
|
|
206
|
-
input =
|
|
255
|
+
input = tryParseJSON(rawArgs || '{}') || {}
|
|
207
256
|
} catch {
|
|
208
257
|
console.error(
|
|
209
258
|
'[openai] Failed to parse tool_call arguments (flush on new tool):',
|
|
@@ -237,12 +286,13 @@ export class OpenAIProvider implements LLMProvider {
|
|
|
237
286
|
if (currentToolCall) {
|
|
238
287
|
const finalArgs = currentToolCall.arguments || ''
|
|
239
288
|
try {
|
|
289
|
+
const parsed = tryParseJSON(finalArgs || '{}')
|
|
240
290
|
yield {
|
|
241
291
|
type: 'tool_use',
|
|
242
292
|
tool_call: {
|
|
243
293
|
id: currentToolCall.id,
|
|
244
294
|
name: currentToolCall.name,
|
|
245
|
-
input:
|
|
295
|
+
input: parsed || {},
|
|
246
296
|
},
|
|
247
297
|
reasoning_content: reasoningContent,
|
|
248
298
|
}
|
|
@@ -274,6 +324,8 @@ export class OpenAIProvider implements LLMProvider {
|
|
|
274
324
|
usage: {
|
|
275
325
|
input_tokens: lastUsage.prompt_tokens ?? 0,
|
|
276
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,
|
|
277
329
|
},
|
|
278
330
|
}
|
|
279
331
|
reasoningContent = undefined
|
|
@@ -294,6 +346,8 @@ export class OpenAIProvider implements LLMProvider {
|
|
|
294
346
|
? {
|
|
295
347
|
input_tokens: lastUsage.prompt_tokens ?? 0,
|
|
296
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,
|
|
297
351
|
}
|
|
298
352
|
: undefined
|
|
299
353
|
yield {
|
|
@@ -310,12 +364,14 @@ export class OpenAIProvider implements LLMProvider {
|
|
|
310
364
|
? {
|
|
311
365
|
input_tokens: lastUsage.prompt_tokens ?? 0,
|
|
312
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,
|
|
313
369
|
}
|
|
314
370
|
: undefined
|
|
315
371
|
let input: Record<string, unknown>
|
|
316
372
|
const rawFinalArgs = currentToolCall.arguments || ''
|
|
317
373
|
try {
|
|
318
|
-
input =
|
|
374
|
+
input = tryParseJSON(rawFinalArgs || '{}') || {}
|
|
319
375
|
} catch {
|
|
320
376
|
console.error('[openai] Failed to parse tool_call arguments (final flush):', rawFinalArgs)
|
|
321
377
|
input = {}
|
package/src/agent/session.ts
CHANGED
|
@@ -25,6 +25,8 @@ interface SessionData {
|
|
|
25
25
|
totalInputTokens: number
|
|
26
26
|
totalOutputTokens: number
|
|
27
27
|
totalApiCalls: number
|
|
28
|
+
totalCacheHitTokens?: number
|
|
29
|
+
totalCacheMissTokens?: number
|
|
28
30
|
mode: 'code' | 'plan' | 'ask'
|
|
29
31
|
model: string
|
|
30
32
|
provider: string
|
|
@@ -222,13 +224,36 @@ function printTokenStats(
|
|
|
222
224
|
turnApi: number,
|
|
223
225
|
totalApi: number,
|
|
224
226
|
output?: SessionOutput,
|
|
227
|
+
turnCacheHit?: number,
|
|
228
|
+
turnCacheMiss?: number,
|
|
229
|
+
totalCacheHit?: number,
|
|
230
|
+
totalCacheMiss?: number,
|
|
225
231
|
): void {
|
|
226
232
|
const bus = getGlobalEventBus()
|
|
227
|
-
bus.emit(EventChannels.TOKEN_STATS, {
|
|
233
|
+
bus.emit(EventChannels.TOKEN_STATS, {
|
|
234
|
+
turnIn,
|
|
235
|
+
turnOut,
|
|
236
|
+
totalIn,
|
|
237
|
+
totalOut,
|
|
238
|
+
turnApi,
|
|
239
|
+
totalApi,
|
|
240
|
+
turnCacheHit,
|
|
241
|
+
turnCacheMiss,
|
|
242
|
+
totalCacheHit,
|
|
243
|
+
totalCacheMiss,
|
|
244
|
+
})
|
|
228
245
|
// Skip terminal output in Web UI mode
|
|
229
246
|
if (output?.suppressToolOutput) return
|
|
230
247
|
const total = totalIn + totalOut
|
|
231
|
-
|
|
248
|
+
let msg = ` ${GY}┃${RS} ${GY}${BLD}▴${RS}${GY}${turnIn}${RS} ${GY}${BLD}▾${RS}${GY}${turnOut}${RS} ${GY}total${RS} ${total} ${GY}calls${RS} ${turnApi}(${totalApi})`
|
|
249
|
+
// Show cache hit rate if available
|
|
250
|
+
const cacheHit = totalCacheHit ?? 0
|
|
251
|
+
const cacheMiss = totalCacheMiss ?? 0
|
|
252
|
+
const cacheTotal = cacheHit + cacheMiss
|
|
253
|
+
if (cacheTotal > 0) {
|
|
254
|
+
const pct = Math.round((cacheHit / cacheTotal) * 100)
|
|
255
|
+
msg += ` ${GY}cached${RS} ${pct}%`
|
|
256
|
+
}
|
|
232
257
|
writeOut(`\n${msg}\n`, output)
|
|
233
258
|
}
|
|
234
259
|
|
|
@@ -259,6 +284,10 @@ export class Session {
|
|
|
259
284
|
turnOutputTokens: number = 0
|
|
260
285
|
turnApiCalls: number = 0
|
|
261
286
|
totalApiCalls: number = 0
|
|
287
|
+
turnCacheHitTokens: number = 0
|
|
288
|
+
turnCacheMissTokens: number = 0
|
|
289
|
+
totalCacheHitTokens: number = 0
|
|
290
|
+
totalCacheMissTokens: number = 0
|
|
262
291
|
private stopped: boolean = false
|
|
263
292
|
private abortController: AbortController | null = null
|
|
264
293
|
|
|
@@ -316,6 +345,8 @@ export class Session {
|
|
|
316
345
|
totalInputTokens: this.totalInputTokens,
|
|
317
346
|
totalOutputTokens: this.totalOutputTokens,
|
|
318
347
|
totalApiCalls: this.totalApiCalls,
|
|
348
|
+
totalCacheHitTokens: this.totalCacheHitTokens || undefined,
|
|
349
|
+
totalCacheMissTokens: this.totalCacheMissTokens || undefined,
|
|
319
350
|
mode: this.config.mode,
|
|
320
351
|
model: this.config.model,
|
|
321
352
|
provider: this.config.provider,
|
|
@@ -358,6 +389,8 @@ export class Session {
|
|
|
358
389
|
session.totalInputTokens = data.totalInputTokens
|
|
359
390
|
session.totalOutputTokens = data.totalOutputTokens
|
|
360
391
|
session.totalApiCalls = data.totalApiCalls
|
|
392
|
+
session.totalCacheHitTokens = data.totalCacheHitTokens ?? 0
|
|
393
|
+
session.totalCacheMissTokens = data.totalCacheMissTokens ?? 0
|
|
361
394
|
return session
|
|
362
395
|
}
|
|
363
396
|
|
|
@@ -410,6 +443,8 @@ export class Session {
|
|
|
410
443
|
this.turnInputTokens = 0
|
|
411
444
|
this.turnOutputTokens = 0
|
|
412
445
|
this.turnApiCalls = 0
|
|
446
|
+
this.turnCacheHitTokens = 0
|
|
447
|
+
this.turnCacheMissTokens = 0
|
|
413
448
|
|
|
414
449
|
let iterations = 0
|
|
415
450
|
const maxIterations = 30
|
|
@@ -583,6 +618,15 @@ export class Session {
|
|
|
583
618
|
this.turnOutputTokens += chunk.usage.output_tokens
|
|
584
619
|
this.totalInputTokens += chunk.usage.input_tokens
|
|
585
620
|
this.totalOutputTokens += chunk.usage.output_tokens
|
|
621
|
+
// Accumulate DeepSeek cache hit/miss tokens
|
|
622
|
+
if (chunk.usage.prompt_cache_hit_tokens != null) {
|
|
623
|
+
this.turnCacheHitTokens += chunk.usage.prompt_cache_hit_tokens
|
|
624
|
+
this.totalCacheHitTokens += chunk.usage.prompt_cache_hit_tokens
|
|
625
|
+
}
|
|
626
|
+
if (chunk.usage.prompt_cache_miss_tokens != null) {
|
|
627
|
+
this.turnCacheMissTokens += chunk.usage.prompt_cache_miss_tokens
|
|
628
|
+
this.totalCacheMissTokens += chunk.usage.prompt_cache_miss_tokens
|
|
629
|
+
}
|
|
586
630
|
}
|
|
587
631
|
if (chunk.finish_reason === 'stop' || chunk.finish_reason === 'end_turn') {
|
|
588
632
|
if (toolCalls.length === 0) {
|
|
@@ -600,6 +644,10 @@ export class Session {
|
|
|
600
644
|
this.turnApiCalls,
|
|
601
645
|
this.totalApiCalls,
|
|
602
646
|
out,
|
|
647
|
+
this.turnCacheHitTokens,
|
|
648
|
+
this.turnCacheMissTokens,
|
|
649
|
+
this.totalCacheHitTokens,
|
|
650
|
+
this.totalCacheMissTokens,
|
|
603
651
|
)
|
|
604
652
|
writeOut('\n\n', out)
|
|
605
653
|
saveTokenUsage(
|
|
@@ -687,6 +735,10 @@ export class Session {
|
|
|
687
735
|
this.turnApiCalls,
|
|
688
736
|
this.totalApiCalls,
|
|
689
737
|
out,
|
|
738
|
+
this.turnCacheHitTokens,
|
|
739
|
+
this.turnCacheMissTokens,
|
|
740
|
+
this.totalCacheHitTokens,
|
|
741
|
+
this.totalCacheMissTokens,
|
|
690
742
|
)
|
|
691
743
|
writeOut('\n\n', out)
|
|
692
744
|
}
|
|
@@ -712,7 +764,7 @@ export class Session {
|
|
|
712
764
|
|
|
713
765
|
// ── User confirmation for write-type tool calls ──
|
|
714
766
|
if (!this.config.autoApprove && this.output?.confirmTool && toolCalls.length > 0) {
|
|
715
|
-
const writeTools = ['edit', 'bash', 'write_plan', '
|
|
767
|
+
const writeTools = ['edit', 'bash', 'write_plan', 'install_skill']
|
|
716
768
|
const needsConfirm = toolCalls.filter(tc => writeTools.includes(tc.name))
|
|
717
769
|
if (needsConfirm.length > 0) {
|
|
718
770
|
const approved = await this.output.confirmTool(needsConfirm)
|
|
@@ -802,6 +854,10 @@ export class Session {
|
|
|
802
854
|
this.turnApiCalls,
|
|
803
855
|
this.totalApiCalls,
|
|
804
856
|
out,
|
|
857
|
+
this.turnCacheHitTokens,
|
|
858
|
+
this.turnCacheMissTokens,
|
|
859
|
+
this.totalCacheHitTokens,
|
|
860
|
+
this.totalCacheMissTokens,
|
|
805
861
|
)
|
|
806
862
|
writeOut('\nAgent reached maximum iterations. Stopping.\n', out)
|
|
807
863
|
}
|