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.
- package/README.md +326 -326
- package/dist/agent/index.js +1 -1
- package/dist/agent/index.js.map +1 -1
- package/dist/agent/llm.d.ts +4 -0
- package/dist/agent/llm.d.ts.map +1 -1
- package/dist/agent/project.d.ts +26 -0
- package/dist/agent/project.d.ts.map +1 -0
- package/dist/agent/project.js +303 -0
- package/dist/agent/project.js.map +1 -0
- package/dist/agent/prompt-builder.d.ts +1 -1
- package/dist/agent/prompt-builder.d.ts.map +1 -1
- package/dist/agent/prompt-builder.js +22 -13
- package/dist/agent/prompt-builder.js.map +1 -1
- package/dist/agent/providers/anthropic.d.ts.map +1 -1
- package/dist/agent/providers/anthropic.js +20 -2
- package/dist/agent/providers/anthropic.js.map +1 -1
- package/dist/agent/providers/google.d.ts.map +1 -1
- package/dist/agent/providers/google.js +1 -1
- package/dist/agent/providers/google.js.map +1 -1
- package/dist/agent/providers/ollama.d.ts.map +1 -1
- package/dist/agent/providers/ollama.js +2 -1
- package/dist/agent/providers/ollama.js.map +1 -1
- package/dist/agent/providers/openai.d.ts.map +1 -1
- package/dist/agent/providers/openai.js +63 -3
- package/dist/agent/providers/openai.js.map +1 -1
- package/dist/agent/session.d.ts +8 -2
- package/dist/agent/session.d.ts.map +1 -1
- package/dist/agent/session.js +250 -153
- 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 +58 -0
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +366 -149
- 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/read.d.ts +6 -0
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +42 -15
- package/dist/tools/read.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 -33
- 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/tools/write_plan.d.ts +1 -1
- package/dist/tools/write_plan.d.ts.map +1 -1
- package/dist/tools/write_plan.js +14 -4
- package/dist/tools/write_plan.js.map +1 -1
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +5 -4
- package/dist/tui/index.js.map +1 -1
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +30 -7
- 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/project.ts +366 -0
- package/src/agent/prompt-builder.ts +23 -13
- package/src/agent/providers/anthropic.ts +179 -172
- package/src/agent/providers/google.ts +210 -211
- package/src/agent/providers/ollama.ts +186 -182
- package/src/agent/providers/openai.ts +73 -5
- package/src/agent/session.ts +275 -153
- 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 +414 -154
- package/src/tools/git.ts +76 -76
- package/src/tools/grep.ts +57 -30
- package/src/tools/read.ts +61 -25
- package/src/tools/registry.ts +231 -265
- package/src/tools/write_plan.ts +21 -5
- package/src/tui/index.ts +786 -785
- package/src/web/index.ts +29 -7
- 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/.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,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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
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
|
|
@@ -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 =
|
|
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:
|
|
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 =
|
|
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 {
|