@kkelly-offical/kkcode 0.1.7 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -674
- package/README.md +452 -387
- package/package.json +50 -46
- package/src/agent/agent.mjs +228 -220
- package/src/agent/custom-agent-loader.mjs +6 -3
- package/src/agent/generator.mjs +2 -2
- package/src/agent/prompt/assistant.txt +12 -0
- package/src/agent/prompt/bug-hunter.txt +89 -89
- package/src/agent/prompt/frontend-designer.txt +58 -58
- package/src/agent/prompt/guide.txt +1 -1
- package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
- package/src/agent/prompt/longagent-coding-agent.txt +37 -37
- package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
- package/src/agent/prompt/longagent-preview-agent.txt +63 -63
- package/src/command/custom-commands.mjs +2 -2
- package/src/commands/agent.mjs +1 -1
- package/src/commands/background.mjs +145 -4
- package/src/commands/chat.mjs +117 -76
- package/src/commands/config.mjs +148 -1
- package/src/commands/doctor.mjs +30 -6
- package/src/commands/init.mjs +32 -6
- package/src/commands/longagent.mjs +117 -0
- package/src/commands/mcp.mjs +275 -43
- package/src/commands/permission.mjs +1 -1
- package/src/commands/session.mjs +195 -140
- package/src/commands/skill.mjs +63 -0
- package/src/commands/theme.mjs +1 -1
- package/src/config/defaults.mjs +280 -260
- package/src/config/import-config.mjs +1 -1
- package/src/config/load-config.mjs +61 -4
- package/src/config/schema.mjs +591 -574
- package/src/context.mjs +4 -1
- package/src/core/constants.mjs +97 -91
- package/src/core/types.mjs +1 -1
- package/src/github/api.mjs +78 -78
- package/src/github/auth.mjs +294 -286
- package/src/github/flow.mjs +298 -298
- package/src/github/workspace.mjs +225 -212
- package/src/index.mjs +84 -82
- package/src/knowledge/frontend-aesthetics.txt +38 -38
- package/src/mcp/client-http.mjs +139 -141
- package/src/mcp/client-sse.mjs +297 -288
- package/src/mcp/client-stdio.mjs +534 -533
- package/src/mcp/constants.mjs +2 -2
- package/src/mcp/registry.mjs +498 -479
- package/src/mcp/stdio-framing.mjs +135 -133
- package/src/mcp/tool-result.mjs +24 -24
- package/src/observability/edit-diagnostics.mjs +449 -0
- package/src/observability/index.mjs +42 -42
- package/src/observability/metrics.mjs +165 -137
- package/src/observability/tracer.mjs +137 -137
- package/src/onboarding.mjs +209 -0
- package/src/orchestration/background-manager.mjs +567 -372
- package/src/orchestration/background-worker.mjs +419 -305
- package/src/orchestration/interruption-reason.mjs +21 -0
- package/src/orchestration/longagent-manager.mjs +197 -171
- package/src/orchestration/stage-scheduler.mjs +733 -728
- package/src/orchestration/subagent-router.mjs +7 -1
- package/src/orchestration/task-scheduler.mjs +219 -7
- package/src/permission/engine.mjs +1 -1
- package/src/permission/exec-policy.mjs +370 -370
- package/src/permission/file-edit-policy.mjs +108 -0
- package/src/permission/prompt.mjs +1 -1
- package/src/permission/rules.mjs +116 -7
- package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
- package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
- package/src/plugin/hook-bus.mjs +19 -5
- package/src/plugin/manifest-loader.mjs +222 -0
- package/src/provider/anthropic.mjs +396 -390
- package/src/provider/ollama.mjs +7 -1
- package/src/provider/openai.mjs +382 -340
- package/src/provider/retry-policy.mjs +74 -68
- package/src/provider/router.mjs +242 -241
- package/src/provider/sse.mjs +104 -104
- package/src/provider/wizard.mjs +556 -0
- package/src/repl/capability-facade.mjs +30 -0
- package/src/repl/command-surface.mjs +23 -0
- package/src/repl/controller-entry.mjs +40 -0
- package/src/repl/core-shell.mjs +208 -0
- package/src/repl/dialog-router.mjs +87 -0
- package/src/repl/input-engine.mjs +76 -0
- package/src/repl/keymap.mjs +7 -0
- package/src/repl/operator-surface.mjs +15 -0
- package/src/repl/permission-flow.mjs +49 -0
- package/src/repl/runtime-facade.mjs +36 -0
- package/src/repl/slash-router.mjs +62 -0
- package/src/repl/state-store.mjs +29 -0
- package/src/repl/turn-controller.mjs +58 -0
- package/src/repl/verification.mjs +23 -0
- package/src/repl.mjs +3368 -2981
- package/src/rules/load-rules.mjs +3 -3
- package/src/runtime.mjs +1 -1
- package/src/session/agent-transaction.mjs +86 -0
- package/src/session/checkpoint.mjs +302 -302
- package/src/session/compaction.mjs +298 -298
- package/src/session/engine.mjs +417 -232
- package/src/session/longagent-4stage.mjs +467 -460
- package/src/session/longagent-hybrid.mjs +1344 -1097
- package/src/session/longagent-plan.mjs +376 -365
- package/src/session/longagent-project-memory.mjs +53 -53
- package/src/session/longagent-scaffold.mjs +291 -291
- package/src/session/longagent-task-bus.mjs +138 -54
- package/src/session/longagent-utils.mjs +828 -472
- package/src/session/longagent.mjs +911 -900
- package/src/session/loop.mjs +1005 -930
- package/src/session/prompt/agent.txt +25 -25
- package/src/session/prompt/anthropic.txt +150 -150
- package/src/session/prompt/beast.txt +1 -1
- package/src/session/prompt/plan.txt +31 -31
- package/src/session/prompt/qwen.txt +46 -46
- package/src/session/recovery.mjs +21 -0
- package/src/session/rollback.mjs +196 -195
- package/src/session/routing-observability.mjs +72 -0
- package/src/session/runtime-state.mjs +47 -0
- package/src/session/store.mjs +523 -519
- package/src/session/system-prompt.mjs +308 -273
- package/src/session/task-validator.mjs +267 -267
- package/src/session/usability-gates.mjs +2 -2
- package/src/skill/builtin/commit.mjs +64 -64
- package/src/skill/builtin/design.mjs +76 -76
- package/src/skill/generator.mjs +18 -2
- package/src/skill/registry.mjs +642 -390
- package/src/storage/audit-store.mjs +18 -11
- package/src/storage/event-log.mjs +7 -1
- package/src/storage/ghost-commit-store.mjs +243 -245
- package/src/storage/paths.mjs +13 -0
- package/src/theme/default-theme.mjs +1 -1
- package/src/theme/markdown.mjs +4 -0
- package/src/theme/schema.mjs +1 -1
- package/src/theme/status-bar.mjs +162 -158
- package/src/tool/audit-wrapper.mjs +18 -2
- package/src/tool/edit-transaction.mjs +23 -0
- package/src/tool/executor.mjs +26 -1
- package/src/tool/file-read-state.mjs +65 -0
- package/src/tool/git-auto.mjs +526 -526
- package/src/tool/git-full-auto.mjs +487 -478
- package/src/tool/mutation-guard.mjs +54 -0
- package/src/tool/prompt/edit.txt +3 -3
- package/src/tool/prompt/multiedit.txt +1 -0
- package/src/tool/prompt/notebookedit.txt +2 -1
- package/src/tool/prompt/patch.txt +25 -24
- package/src/tool/prompt/read.txt +3 -3
- package/src/tool/prompt/sysinfo.txt +29 -0
- package/src/tool/prompt/task.txt +66 -4
- package/src/tool/prompt/write.txt +2 -2
- package/src/tool/question-prompt.mjs +99 -93
- package/src/tool/registry.mjs +1701 -1343
- package/src/tool/task-tool.mjs +14 -6
- package/src/ui/activity-renderer.mjs +667 -664
- package/src/ui/repl-background-panel.mjs +7 -0
- package/src/ui/repl-capability-panel.mjs +9 -0
- package/src/ui/repl-dashboard.mjs +54 -4
- package/src/ui/repl-help.mjs +110 -0
- package/src/ui/repl-operator-panel.mjs +12 -0
- package/src/ui/repl-route-feedback.mjs +35 -0
- package/src/ui/repl-status-view.mjs +76 -0
- package/src/ui/repl-task-panel.mjs +5 -0
- package/src/ui/repl-transcript-panel.mjs +56 -0
- package/src/ui/repl-turn-summary.mjs +135 -0
- package/src/usage/pricing.mjs +122 -121
- package/src/usage/usage-meter.mjs +1 -0
- package/src/util/git.mjs +562 -519
- package/src/util/template.mjs +6 -1
package/src/provider/openai.mjs
CHANGED
|
@@ -1,340 +1,382 @@
|
|
|
1
|
-
import { ProviderError } from "../core/errors.mjs"
|
|
2
|
-
import { requestWithRetry } from "./retry-policy.mjs"
|
|
3
|
-
import { parseSSE } from "./sse.mjs"
|
|
4
|
-
|
|
5
|
-
function sleep(ms) {
|
|
6
|
-
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function mapTools(tools) {
|
|
10
|
-
if (!tools || !tools.length) return undefined
|
|
11
|
-
|
|
12
|
-
type: "function",
|
|
13
|
-
function: {
|
|
14
|
-
name: tool.name,
|
|
15
|
-
description: tool.description,
|
|
16
|
-
parameters: tool.inputSchema
|
|
17
|
-
}
|
|
18
|
-
}))
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (!
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
if (!
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
:
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
1
|
+
import { ProviderError } from "../core/errors.mjs"
|
|
2
|
+
import { requestWithRetry } from "./retry-policy.mjs"
|
|
3
|
+
import { parseSSE } from "./sse.mjs"
|
|
4
|
+
|
|
5
|
+
function sleep(ms) {
|
|
6
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function mapTools(tools) {
|
|
10
|
+
if (!tools || !tools.length) return undefined
|
|
11
|
+
const mapped = tools.map((tool) => ({
|
|
12
|
+
type: "function",
|
|
13
|
+
function: {
|
|
14
|
+
name: tool.name,
|
|
15
|
+
description: tool.description,
|
|
16
|
+
parameters: tool.inputSchema
|
|
17
|
+
}
|
|
18
|
+
}))
|
|
19
|
+
// Cache tool definitions — they rarely change within a session
|
|
20
|
+
if (mapped.length > 0) {
|
|
21
|
+
mapped[mapped.length - 1].cache_control = { type: "ephemeral" }
|
|
22
|
+
}
|
|
23
|
+
return mapped
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function mapContentBlock(block) {
|
|
27
|
+
if (block.type === "image" && block.data) {
|
|
28
|
+
return {
|
|
29
|
+
type: "image_url",
|
|
30
|
+
image_url: {
|
|
31
|
+
url: `data:${block.mediaType || "image/png"};base64,${block.data}`
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { type: "text", text: String(block.text || block.content || "") }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function mapMessages(messages) {
|
|
39
|
+
const mapped = []
|
|
40
|
+
for (const message of messages) {
|
|
41
|
+
const content = message.content
|
|
42
|
+
if (!Array.isArray(content)) {
|
|
43
|
+
mapped.push({ role: message.role, content: String(content || "") })
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check for native tool_use blocks (assistant message with tool calls)
|
|
48
|
+
const toolUseBlocks = content.filter((b) => b.type === "tool_use")
|
|
49
|
+
if (toolUseBlocks.length > 0 && message.role === "assistant") {
|
|
50
|
+
const textParts = content.filter((b) => b.type === "text").map((b) => b.text || "").join("\n")
|
|
51
|
+
mapped.push({
|
|
52
|
+
role: "assistant",
|
|
53
|
+
content: textParts || null,
|
|
54
|
+
tool_calls: toolUseBlocks.map((b) => ({
|
|
55
|
+
id: b.id,
|
|
56
|
+
type: "function",
|
|
57
|
+
function: {
|
|
58
|
+
name: b.name,
|
|
59
|
+
arguments: JSON.stringify(b.input || {})
|
|
60
|
+
}
|
|
61
|
+
}))
|
|
62
|
+
})
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check for tool_result blocks (user message with tool results)
|
|
67
|
+
const toolResultBlocks = content.filter((b) => b.type === "tool_result")
|
|
68
|
+
if (toolResultBlocks.length > 0) {
|
|
69
|
+
for (const result of toolResultBlocks) {
|
|
70
|
+
mapped.push({
|
|
71
|
+
role: "tool",
|
|
72
|
+
tool_call_id: result.tool_use_id,
|
|
73
|
+
content: String(result.content || "")
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Regular array content (images, text)
|
|
80
|
+
mapped.push({ role: message.role, content: content.map(mapContentBlock) })
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Add cache_control to the last user message for multi-turn caching
|
|
84
|
+
for (let i = mapped.length - 1; i >= 0; i--) {
|
|
85
|
+
if (mapped[i].role === "user") {
|
|
86
|
+
const c = mapped[i].content
|
|
87
|
+
if (Array.isArray(c) && c.length) {
|
|
88
|
+
c[c.length - 1].cache_control = { type: "ephemeral" }
|
|
89
|
+
} else if (typeof c === "string") {
|
|
90
|
+
mapped[i].content = [{ type: "text", text: c, cache_control: { type: "ephemeral" } }]
|
|
91
|
+
}
|
|
92
|
+
break
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return mapped
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parseToolCalls(message) {
|
|
100
|
+
if (!Array.isArray(message?.tool_calls)) return []
|
|
101
|
+
return message.tool_calls
|
|
102
|
+
.filter((call) => call?.function?.name)
|
|
103
|
+
.map((call) => {
|
|
104
|
+
const raw = call.function.arguments || "{}"
|
|
105
|
+
let args = {}
|
|
106
|
+
try {
|
|
107
|
+
args = JSON.parse(raw)
|
|
108
|
+
} catch (parseErr) {
|
|
109
|
+
console.error(`[openai] tool_call JSON parse failed for "${call.function.name}": ${parseErr.message} (${raw.length} chars, first 200: ${raw.slice(0, 200)})`)
|
|
110
|
+
args = { __parse_error: true, __raw_length: raw.length, __error: parseErr.message }
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
id: call.id || `tc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
114
|
+
name: call.function.name,
|
|
115
|
+
args
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Build system messages from structured blocks with cache_control markers.
|
|
121
|
+
// Stable content gets cache_control for prompt caching (OpenAI auto-cache + Qwen/compatible explicit cache).
|
|
122
|
+
function buildSystemMessages(system) {
|
|
123
|
+
if (!system) return []
|
|
124
|
+
if (system.blocks && Array.isArray(system.blocks)) {
|
|
125
|
+
const stable = []
|
|
126
|
+
const dynamic = []
|
|
127
|
+
for (const block of system.blocks) {
|
|
128
|
+
if (block.cacheable) stable.push(block.text)
|
|
129
|
+
else dynamic.push(block.text)
|
|
130
|
+
}
|
|
131
|
+
const msgs = []
|
|
132
|
+
if (stable.length) {
|
|
133
|
+
msgs.push({
|
|
134
|
+
role: "system",
|
|
135
|
+
content: [{
|
|
136
|
+
type: "text",
|
|
137
|
+
text: stable.join("\n\n"),
|
|
138
|
+
cache_control: { type: "ephemeral" }
|
|
139
|
+
}]
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
if (dynamic.length) msgs.push({ role: "system", content: dynamic.join("\n\n") })
|
|
143
|
+
return msgs
|
|
144
|
+
}
|
|
145
|
+
const text = typeof system === "string" ? system : system.text || String(system)
|
|
146
|
+
if (!text) return []
|
|
147
|
+
return [{
|
|
148
|
+
role: "system",
|
|
149
|
+
content: [{ type: "text", text, cache_control: { type: "ephemeral" } }]
|
|
150
|
+
}]
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function timeoutSignal(ms, parentSignal = null) {
|
|
154
|
+
const own = AbortSignal.timeout(ms)
|
|
155
|
+
if (!parentSignal) return own
|
|
156
|
+
return AbortSignal.any([parentSignal, own])
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function countTokensOpenAI(input) {
|
|
160
|
+
const { apiKey, baseUrl, model, system, messages, tools, timeoutMs = 10000 } = input
|
|
161
|
+
if (!apiKey) return null
|
|
162
|
+
const endpoint = `${baseUrl.replace(/\/$/, "")}/chat/completions`
|
|
163
|
+
const payload = {
|
|
164
|
+
model,
|
|
165
|
+
messages: [...buildSystemMessages(system), ...mapMessages(messages)],
|
|
166
|
+
tools: mapTools(tools),
|
|
167
|
+
max_tokens: 1,
|
|
168
|
+
stream: false
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
const res = await fetch(endpoint, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
|
|
174
|
+
body: JSON.stringify(payload),
|
|
175
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
176
|
+
})
|
|
177
|
+
if (!res.ok) return null
|
|
178
|
+
const json = await res.json()
|
|
179
|
+
return json?.usage?.prompt_tokens ?? null
|
|
180
|
+
} catch {
|
|
181
|
+
return null
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function requestOpenAI(input) {
|
|
186
|
+
const { apiKey, baseUrl, model, system, messages, tools, timeoutMs = 120000, maxTokens, retry = {}, signal = null } = input
|
|
187
|
+
if (!apiKey) {
|
|
188
|
+
throw new ProviderError(`missing API key for openai provider (env: ${input.apiKeyEnv || "unknown"})`, {
|
|
189
|
+
provider: "openai"
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const payload = {
|
|
194
|
+
model,
|
|
195
|
+
messages: [...buildSystemMessages(system), ...mapMessages(messages)],
|
|
196
|
+
tools: mapTools(tools),
|
|
197
|
+
tool_choice: tools?.length ? "auto" : undefined,
|
|
198
|
+
...(maxTokens ? { max_tokens: maxTokens } : {})
|
|
199
|
+
}
|
|
200
|
+
const endpoint = `${baseUrl.replace(/\/$/, "")}/chat/completions`
|
|
201
|
+
|
|
202
|
+
return requestWithRetry({
|
|
203
|
+
attempts: Number(retry.attempts ?? 3),
|
|
204
|
+
baseDelayMs: Number(retry.baseDelayMs ?? 800),
|
|
205
|
+
signal,
|
|
206
|
+
execute: async () => {
|
|
207
|
+
const response = await fetch(endpoint, {
|
|
208
|
+
method: "POST",
|
|
209
|
+
headers: {
|
|
210
|
+
"content-type": "application/json",
|
|
211
|
+
authorization: `Bearer ${apiKey}`
|
|
212
|
+
},
|
|
213
|
+
body: JSON.stringify(payload),
|
|
214
|
+
signal: timeoutSignal(timeoutMs, signal)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
if (!response.ok) {
|
|
218
|
+
const text = await response.text().catch(() => "")
|
|
219
|
+
const error = new ProviderError(`openai request failed: ${response.status} ${text}`, {
|
|
220
|
+
provider: "openai",
|
|
221
|
+
model,
|
|
222
|
+
endpoint
|
|
223
|
+
})
|
|
224
|
+
error.httpStatus = response.status
|
|
225
|
+
throw error
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let json
|
|
229
|
+
try {
|
|
230
|
+
json = await response.json()
|
|
231
|
+
} catch (parseErr) {
|
|
232
|
+
throw new ProviderError(`openai response JSON parse failed: ${parseErr.message}`, { provider: "openai", model, endpoint })
|
|
233
|
+
}
|
|
234
|
+
const message = json?.choices?.[0]?.message ?? {}
|
|
235
|
+
const promptTokens = json?.usage?.prompt_tokens ?? 0
|
|
236
|
+
const details = json?.usage?.prompt_tokens_details || {}
|
|
237
|
+
const cachedTokens = details.cached_tokens ?? 0
|
|
238
|
+
const cacheWriteTokens = details.cache_creation_input_tokens ?? 0
|
|
239
|
+
const usage = {
|
|
240
|
+
input: promptTokens - cachedTokens,
|
|
241
|
+
output: json?.usage?.completion_tokens ?? 0,
|
|
242
|
+
cacheRead: cachedTokens,
|
|
243
|
+
cacheWrite: cacheWriteTokens
|
|
244
|
+
}
|
|
245
|
+
const toolCalls = parseToolCalls(message)
|
|
246
|
+
const text = typeof message.content === "string" ? message.content : ""
|
|
247
|
+
return { text, usage, toolCalls }
|
|
248
|
+
}
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export async function* requestOpenAIStream(input) {
|
|
253
|
+
const { apiKey, baseUrl, model, system, messages, tools, timeoutMs = 120000, streamIdleTimeoutMs = 120000, maxTokens, retry = {}, signal = null } = input
|
|
254
|
+
if (!apiKey) {
|
|
255
|
+
throw new ProviderError(`missing API key for openai provider (env: ${input.apiKeyEnv || "unknown"})`, {
|
|
256
|
+
provider: "openai"
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const payload = {
|
|
261
|
+
model,
|
|
262
|
+
messages: [...buildSystemMessages(system), ...mapMessages(messages)],
|
|
263
|
+
tools: mapTools(tools),
|
|
264
|
+
tool_choice: tools?.length ? "auto" : undefined,
|
|
265
|
+
...(maxTokens ? { max_tokens: maxTokens } : {}),
|
|
266
|
+
stream: true,
|
|
267
|
+
stream_options: { include_usage: true }
|
|
268
|
+
}
|
|
269
|
+
const endpoint = `${baseUrl.replace(/\/$/, "")}/chat/completions`
|
|
270
|
+
const attempts = Number(retry.attempts ?? 3)
|
|
271
|
+
const baseDelayMs = Number(retry.baseDelayMs ?? 800)
|
|
272
|
+
|
|
273
|
+
let response
|
|
274
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
275
|
+
let connTimer = null
|
|
276
|
+
const connController = new AbortController()
|
|
277
|
+
try {
|
|
278
|
+
// Use a connection-only timeout for the initial fetch.
|
|
279
|
+
// Once headers arrive, clear it — the SSE idle timeout handles the streaming phase.
|
|
280
|
+
connTimer = setTimeout(() => connController.abort(), timeoutMs)
|
|
281
|
+
const fetchSignal = signal
|
|
282
|
+
? AbortSignal.any([signal, connController.signal])
|
|
283
|
+
: connController.signal
|
|
284
|
+
|
|
285
|
+
response = await fetch(endpoint, {
|
|
286
|
+
method: "POST",
|
|
287
|
+
headers: {
|
|
288
|
+
"content-type": "application/json",
|
|
289
|
+
authorization: `Bearer ${apiKey}`
|
|
290
|
+
},
|
|
291
|
+
body: JSON.stringify(payload),
|
|
292
|
+
signal: fetchSignal
|
|
293
|
+
})
|
|
294
|
+
clearTimeout(connTimer)
|
|
295
|
+
|
|
296
|
+
if (!response.ok) {
|
|
297
|
+
const text = await response.text().catch(() => "")
|
|
298
|
+
const error = new ProviderError(`openai stream failed: ${response.status} ${text}`, {
|
|
299
|
+
provider: "openai", model, endpoint
|
|
300
|
+
})
|
|
301
|
+
error.httpStatus = response.status
|
|
302
|
+
throw error
|
|
303
|
+
}
|
|
304
|
+
break
|
|
305
|
+
} catch (err) {
|
|
306
|
+
clearTimeout(connTimer)
|
|
307
|
+
if (signal?.aborted) throw err
|
|
308
|
+
const isNetwork = err?.code === "ETIMEDOUT" || err?.code === "ECONNRESET" || err?.name === "AbortError"
|
|
309
|
+
if (!isNetwork || attempt >= attempts) throw err
|
|
310
|
+
await sleep(baseDelayMs * Math.pow(2, attempt - 1))
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const toolBuffers = new Map()
|
|
315
|
+
let finishReason = null
|
|
316
|
+
|
|
317
|
+
for await (const { data } of parseSSE(response.body, signal, { idleTimeoutMs: streamIdleTimeoutMs })) {
|
|
318
|
+
let json
|
|
319
|
+
try { json = JSON.parse(data) } catch { continue }
|
|
320
|
+
|
|
321
|
+
if (json.usage) {
|
|
322
|
+
const pt = json.usage.prompt_tokens ?? 0
|
|
323
|
+
const details = json.usage.prompt_tokens_details || {}
|
|
324
|
+
const ct = details.cached_tokens ?? 0
|
|
325
|
+
const cw = details.cache_creation_input_tokens ?? 0
|
|
326
|
+
yield {
|
|
327
|
+
type: "usage",
|
|
328
|
+
usage: { input: pt - ct, output: json.usage.completion_tokens ?? 0, cacheRead: ct, cacheWrite: cw }
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const choice = json.choices?.[0]
|
|
333
|
+
if (choice?.finish_reason) {
|
|
334
|
+
finishReason = choice.finish_reason
|
|
335
|
+
}
|
|
336
|
+
const delta = choice?.delta
|
|
337
|
+
if (!delta) continue
|
|
338
|
+
|
|
339
|
+
if (delta.content) {
|
|
340
|
+
yield { type: "text", content: delta.content }
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (delta.tool_calls) {
|
|
344
|
+
for (const tc of delta.tool_calls) {
|
|
345
|
+
const idx = tc.index ?? 0
|
|
346
|
+
if (!toolBuffers.has(idx)) {
|
|
347
|
+
toolBuffers.set(idx, { id: "", name: "", argsJson: "" })
|
|
348
|
+
}
|
|
349
|
+
const buf = toolBuffers.get(idx)
|
|
350
|
+
if (tc.id) buf.id = tc.id
|
|
351
|
+
if (tc.function?.name) buf.name = tc.function.name
|
|
352
|
+
if (tc.function?.arguments) buf.argsJson += tc.function.arguments
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
for (const [, buf] of toolBuffers) {
|
|
358
|
+
const raw = buf.argsJson || "{}"
|
|
359
|
+
let args = {}
|
|
360
|
+
try {
|
|
361
|
+
args = JSON.parse(raw)
|
|
362
|
+
} catch (parseErr) {
|
|
363
|
+
console.error(`[openai] tool_call JSON parse failed for "${buf.name}": ${parseErr.message} (${raw.length} chars, first 200: ${raw.slice(0, 200)})`)
|
|
364
|
+
args = { __parse_error: true, __raw_length: raw.length, __error: parseErr.message }
|
|
365
|
+
}
|
|
366
|
+
yield {
|
|
367
|
+
type: "tool_call",
|
|
368
|
+
call: {
|
|
369
|
+
id: buf.id || `tc_${Date.now()}`,
|
|
370
|
+
name: buf.name,
|
|
371
|
+
args
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Normalize: "stop" → "end_turn", "length" → "max_tokens", "tool_calls" → "tool_use"
|
|
377
|
+
const normalizedReason = finishReason === "length" ? "max_tokens"
|
|
378
|
+
: finishReason === "tool_calls" ? "tool_use"
|
|
379
|
+
: finishReason === "stop" ? "end_turn"
|
|
380
|
+
: finishReason || "end_turn"
|
|
381
|
+
yield { type: "stop", reason: normalizedReason }
|
|
382
|
+
}
|