@kkelly-offical/kkcode 0.1.2

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 (196) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +445 -0
  3. package/package.json +46 -0
  4. package/src/agent/agent.mjs +170 -0
  5. package/src/agent/custom-agent-loader.mjs +158 -0
  6. package/src/agent/generator.mjs +115 -0
  7. package/src/agent/prompt/architect.txt +36 -0
  8. package/src/agent/prompt/build-fixer.txt +71 -0
  9. package/src/agent/prompt/build.txt +101 -0
  10. package/src/agent/prompt/compaction.txt +12 -0
  11. package/src/agent/prompt/explore.txt +29 -0
  12. package/src/agent/prompt/guide.txt +40 -0
  13. package/src/agent/prompt/longagent.txt +178 -0
  14. package/src/agent/prompt/plan.txt +50 -0
  15. package/src/agent/prompt/researcher.txt +23 -0
  16. package/src/agent/prompt/reviewer.txt +44 -0
  17. package/src/agent/prompt/security-reviewer.txt +62 -0
  18. package/src/agent/prompt/tdd-guide.txt +84 -0
  19. package/src/agent/prompt/title.txt +8 -0
  20. package/src/command/custom-commands.mjs +57 -0
  21. package/src/commands/agent.mjs +71 -0
  22. package/src/commands/audit.mjs +77 -0
  23. package/src/commands/background.mjs +86 -0
  24. package/src/commands/chat.mjs +114 -0
  25. package/src/commands/command.mjs +41 -0
  26. package/src/commands/config.mjs +44 -0
  27. package/src/commands/doctor.mjs +148 -0
  28. package/src/commands/hook.mjs +29 -0
  29. package/src/commands/init.mjs +141 -0
  30. package/src/commands/longagent.mjs +100 -0
  31. package/src/commands/mcp.mjs +89 -0
  32. package/src/commands/permission.mjs +36 -0
  33. package/src/commands/prompt.mjs +42 -0
  34. package/src/commands/review.mjs +266 -0
  35. package/src/commands/rule.mjs +34 -0
  36. package/src/commands/session.mjs +235 -0
  37. package/src/commands/theme.mjs +98 -0
  38. package/src/commands/usage.mjs +91 -0
  39. package/src/config/defaults.mjs +195 -0
  40. package/src/config/import-config.mjs +76 -0
  41. package/src/config/load-config.mjs +76 -0
  42. package/src/config/schema.mjs +509 -0
  43. package/src/context.mjs +40 -0
  44. package/src/core/constants.mjs +46 -0
  45. package/src/core/errors.mjs +57 -0
  46. package/src/core/events.mjs +29 -0
  47. package/src/core/types.mjs +57 -0
  48. package/src/github/api.mjs +78 -0
  49. package/src/github/auth.mjs +286 -0
  50. package/src/github/flow.mjs +298 -0
  51. package/src/github/workspace.mjs +212 -0
  52. package/src/index.mjs +82 -0
  53. package/src/knowledge/api-design.txt +9 -0
  54. package/src/knowledge/cpp.txt +10 -0
  55. package/src/knowledge/docker.txt +10 -0
  56. package/src/knowledge/dotnet.txt +9 -0
  57. package/src/knowledge/electron.txt +10 -0
  58. package/src/knowledge/flutter.txt +10 -0
  59. package/src/knowledge/go.txt +9 -0
  60. package/src/knowledge/graphql.txt +10 -0
  61. package/src/knowledge/java.txt +9 -0
  62. package/src/knowledge/kotlin.txt +10 -0
  63. package/src/knowledge/loader.mjs +125 -0
  64. package/src/knowledge/next.txt +8 -0
  65. package/src/knowledge/node.txt +8 -0
  66. package/src/knowledge/nuxt.txt +9 -0
  67. package/src/knowledge/php.txt +10 -0
  68. package/src/knowledge/python.txt +10 -0
  69. package/src/knowledge/react-native.txt +10 -0
  70. package/src/knowledge/react.txt +9 -0
  71. package/src/knowledge/ruby.txt +11 -0
  72. package/src/knowledge/rust.txt +9 -0
  73. package/src/knowledge/svelte.txt +9 -0
  74. package/src/knowledge/swift.txt +10 -0
  75. package/src/knowledge/tailwind.txt +10 -0
  76. package/src/knowledge/testing.txt +8 -0
  77. package/src/knowledge/typescript.txt +8 -0
  78. package/src/knowledge/vue.txt +9 -0
  79. package/src/mcp/client-http.mjs +157 -0
  80. package/src/mcp/client-sse.mjs +286 -0
  81. package/src/mcp/client-stdio.mjs +451 -0
  82. package/src/mcp/registry.mjs +394 -0
  83. package/src/mcp/stdio-framing.mjs +127 -0
  84. package/src/orchestration/background-manager.mjs +358 -0
  85. package/src/orchestration/background-worker.mjs +245 -0
  86. package/src/orchestration/longagent-manager.mjs +116 -0
  87. package/src/orchestration/stage-scheduler.mjs +489 -0
  88. package/src/orchestration/subagent-router.mjs +62 -0
  89. package/src/orchestration/task-scheduler.mjs +74 -0
  90. package/src/permission/engine.mjs +92 -0
  91. package/src/permission/exec-policy.mjs +372 -0
  92. package/src/permission/prompt.mjs +39 -0
  93. package/src/permission/rules.mjs +120 -0
  94. package/src/permission/workspace-trust.mjs +44 -0
  95. package/src/plugin/builtin-hooks/console-warn.mjs +41 -0
  96. package/src/plugin/builtin-hooks/extract-patterns.mjs +75 -0
  97. package/src/plugin/builtin-hooks/post-edit-format.mjs +57 -0
  98. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +61 -0
  99. package/src/plugin/builtin-hooks/strategic-compaction.mjs +38 -0
  100. package/src/plugin/hook-bus.mjs +154 -0
  101. package/src/provider/anthropic.mjs +389 -0
  102. package/src/provider/ollama.mjs +236 -0
  103. package/src/provider/openai-compatible.mjs +1 -0
  104. package/src/provider/openai.mjs +339 -0
  105. package/src/provider/retry-policy.mjs +68 -0
  106. package/src/provider/router.mjs +228 -0
  107. package/src/provider/sse.mjs +91 -0
  108. package/src/repl.mjs +2929 -0
  109. package/src/review/diff-parser.mjs +36 -0
  110. package/src/review/rejection-queue.mjs +62 -0
  111. package/src/review/review-store.mjs +21 -0
  112. package/src/review/risk-score.mjs +61 -0
  113. package/src/rules/load-rules.mjs +64 -0
  114. package/src/runtime.mjs +1 -0
  115. package/src/session/checkpoint.mjs +239 -0
  116. package/src/session/compaction.mjs +276 -0
  117. package/src/session/engine.mjs +225 -0
  118. package/src/session/instinct-manager.mjs +172 -0
  119. package/src/session/instruction-loader.mjs +25 -0
  120. package/src/session/longagent-plan.mjs +329 -0
  121. package/src/session/longagent-scaffold.mjs +100 -0
  122. package/src/session/longagent.mjs +1462 -0
  123. package/src/session/loop.mjs +905 -0
  124. package/src/session/memory-loader.mjs +75 -0
  125. package/src/session/project-context.mjs +367 -0
  126. package/src/session/prompt/anthropic.txt +151 -0
  127. package/src/session/prompt/beast.txt +37 -0
  128. package/src/session/prompt/max-steps.txt +6 -0
  129. package/src/session/prompt/plan.txt +9 -0
  130. package/src/session/prompt/qwen.txt +46 -0
  131. package/src/session/prompt-loader.mjs +18 -0
  132. package/src/session/recovery.mjs +52 -0
  133. package/src/session/store.mjs +503 -0
  134. package/src/session/system-prompt.mjs +260 -0
  135. package/src/session/task-validator.mjs +266 -0
  136. package/src/session/usability-gates.mjs +379 -0
  137. package/src/skill/builtin/backend-patterns.mjs +123 -0
  138. package/src/skill/builtin/commit.mjs +64 -0
  139. package/src/skill/builtin/debug.mjs +45 -0
  140. package/src/skill/builtin/frontend-patterns.mjs +120 -0
  141. package/src/skill/builtin/frontend.mjs +188 -0
  142. package/src/skill/builtin/init.mjs +220 -0
  143. package/src/skill/builtin/review.mjs +49 -0
  144. package/src/skill/builtin/security-checklist.mjs +80 -0
  145. package/src/skill/builtin/tdd.mjs +54 -0
  146. package/src/skill/generator.mjs +113 -0
  147. package/src/skill/registry.mjs +336 -0
  148. package/src/storage/audit-store.mjs +83 -0
  149. package/src/storage/event-log.mjs +82 -0
  150. package/src/storage/ghost-commit-store.mjs +235 -0
  151. package/src/storage/json-store.mjs +53 -0
  152. package/src/storage/paths.mjs +148 -0
  153. package/src/theme/color.mjs +64 -0
  154. package/src/theme/default-theme.mjs +29 -0
  155. package/src/theme/load-theme.mjs +71 -0
  156. package/src/theme/markdown.mjs +135 -0
  157. package/src/theme/schema.mjs +45 -0
  158. package/src/theme/status-bar.mjs +158 -0
  159. package/src/tool/audit-wrapper.mjs +38 -0
  160. package/src/tool/edit-transaction.mjs +126 -0
  161. package/src/tool/executor.mjs +109 -0
  162. package/src/tool/file-lock-manager.mjs +85 -0
  163. package/src/tool/git-auto.mjs +545 -0
  164. package/src/tool/git-full-auto.mjs +478 -0
  165. package/src/tool/image-util.mjs +276 -0
  166. package/src/tool/prompt/background_cancel.txt +1 -0
  167. package/src/tool/prompt/background_output.txt +1 -0
  168. package/src/tool/prompt/bash.txt +71 -0
  169. package/src/tool/prompt/codesearch.txt +18 -0
  170. package/src/tool/prompt/edit.txt +27 -0
  171. package/src/tool/prompt/enter_plan.txt +74 -0
  172. package/src/tool/prompt/exit_plan.txt +62 -0
  173. package/src/tool/prompt/glob.txt +33 -0
  174. package/src/tool/prompt/grep.txt +43 -0
  175. package/src/tool/prompt/list.txt +8 -0
  176. package/src/tool/prompt/multiedit.txt +20 -0
  177. package/src/tool/prompt/notebookedit.txt +21 -0
  178. package/src/tool/prompt/patch.txt +24 -0
  179. package/src/tool/prompt/question.txt +44 -0
  180. package/src/tool/prompt/read.txt +40 -0
  181. package/src/tool/prompt/task.txt +83 -0
  182. package/src/tool/prompt/todowrite.txt +117 -0
  183. package/src/tool/prompt/webfetch.txt +38 -0
  184. package/src/tool/prompt/websearch.txt +43 -0
  185. package/src/tool/prompt/write.txt +38 -0
  186. package/src/tool/prompt-loader.mjs +18 -0
  187. package/src/tool/question-prompt.mjs +86 -0
  188. package/src/tool/registry.mjs +1309 -0
  189. package/src/tool/task-tool.mjs +28 -0
  190. package/src/ui/activity-renderer.mjs +410 -0
  191. package/src/ui/repl-dashboard.mjs +357 -0
  192. package/src/usage/pricing.mjs +121 -0
  193. package/src/usage/usage-meter.mjs +113 -0
  194. package/src/util/git.mjs +496 -0
  195. package/src/util/template.mjs +10 -0
  196. package/src/util/yaml.mjs +100 -0
@@ -0,0 +1,339 @@
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
+ return tools.map((tool) => ({
12
+ type: "function",
13
+ function: {
14
+ name: tool.name,
15
+ description: tool.description,
16
+ parameters: tool.inputSchema
17
+ }
18
+ }))
19
+ }
20
+
21
+ function mapContentBlock(block) {
22
+ if (block.type === "image" && block.data) {
23
+ return {
24
+ type: "image_url",
25
+ image_url: {
26
+ url: `data:${block.mediaType || "image/png"};base64,${block.data}`
27
+ }
28
+ }
29
+ }
30
+ return { type: "text", text: String(block.text || block.content || "") }
31
+ }
32
+
33
+ function mapMessages(messages) {
34
+ const mapped = []
35
+ for (const message of messages) {
36
+ const content = message.content
37
+ if (!Array.isArray(content)) {
38
+ mapped.push({ role: message.role, content: String(content || "") })
39
+ continue
40
+ }
41
+
42
+ // Check for native tool_use blocks (assistant message with tool calls)
43
+ const toolUseBlocks = content.filter((b) => b.type === "tool_use")
44
+ if (toolUseBlocks.length > 0 && message.role === "assistant") {
45
+ const textParts = content.filter((b) => b.type === "text").map((b) => b.text || "").join("\n")
46
+ mapped.push({
47
+ role: "assistant",
48
+ content: textParts || null,
49
+ tool_calls: toolUseBlocks.map((b) => ({
50
+ id: b.id,
51
+ type: "function",
52
+ function: {
53
+ name: b.name,
54
+ arguments: JSON.stringify(b.input || {})
55
+ }
56
+ }))
57
+ })
58
+ continue
59
+ }
60
+
61
+ // Check for tool_result blocks (user message with tool results)
62
+ const toolResultBlocks = content.filter((b) => b.type === "tool_result")
63
+ if (toolResultBlocks.length > 0) {
64
+ for (const result of toolResultBlocks) {
65
+ mapped.push({
66
+ role: "tool",
67
+ tool_call_id: result.tool_use_id,
68
+ content: String(result.content || "")
69
+ })
70
+ }
71
+ continue
72
+ }
73
+
74
+ // Regular array content (images, text)
75
+ mapped.push({ role: message.role, content: content.map(mapContentBlock) })
76
+ }
77
+ return mapped
78
+ }
79
+
80
+ function parseToolCalls(message) {
81
+ if (!Array.isArray(message?.tool_calls)) return []
82
+ return message.tool_calls
83
+ .filter((call) => call?.function?.name)
84
+ .map((call) => {
85
+ const raw = call.function.arguments || "{}"
86
+ let args = {}
87
+ try {
88
+ args = JSON.parse(raw)
89
+ } catch (parseErr) {
90
+ console.error(`[openai] tool_call JSON parse failed for "${call.function.name}": ${parseErr.message} (${raw.length} chars, first 200: ${raw.slice(0, 200)})`)
91
+ args = { __parse_error: true, __raw_length: raw.length, __error: parseErr.message }
92
+ }
93
+ return {
94
+ id: call.id || `tc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
95
+ name: call.function.name,
96
+ args
97
+ }
98
+ })
99
+ }
100
+
101
+ // Build system messages from structured blocks for optimal prefix caching.
102
+ // OpenAI auto-caches matching prefixes — stable content first, dynamic last.
103
+ function buildSystemMessages(system) {
104
+ if (!system) return []
105
+ if (system.blocks && Array.isArray(system.blocks)) {
106
+ const stable = []
107
+ const dynamic = []
108
+ for (const block of system.blocks) {
109
+ if (block.cacheable) stable.push(block.text)
110
+ else dynamic.push(block.text)
111
+ }
112
+ const msgs = []
113
+ if (stable.length) msgs.push({ role: "system", content: stable.join("\n\n") })
114
+ if (dynamic.length) msgs.push({ role: "system", content: dynamic.join("\n\n") })
115
+ return msgs
116
+ }
117
+ const text = typeof system === "string" ? system : system.text || String(system)
118
+ return text ? [{ role: "system", content: text }] : []
119
+ }
120
+
121
+ function timeoutSignal(ms, parentSignal = null) {
122
+ const own = AbortSignal.timeout(ms)
123
+ if (!parentSignal) return own
124
+ return AbortSignal.any([parentSignal, own])
125
+ }
126
+
127
+ export async function countTokensOpenAI(input) {
128
+ const { apiKey, baseUrl, model, system, messages, tools, timeoutMs = 10000 } = input
129
+ if (!apiKey) return null
130
+ const endpoint = `${baseUrl.replace(/\/$/, "")}/chat/completions`
131
+ const payload = {
132
+ model,
133
+ messages: [...buildSystemMessages(system), ...mapMessages(messages)],
134
+ tools: mapTools(tools),
135
+ max_tokens: 1,
136
+ stream: false
137
+ }
138
+ try {
139
+ const res = await fetch(endpoint, {
140
+ method: "POST",
141
+ headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
142
+ body: JSON.stringify(payload),
143
+ signal: AbortSignal.timeout(timeoutMs)
144
+ })
145
+ if (!res.ok) return null
146
+ const json = await res.json()
147
+ return json?.usage?.prompt_tokens ?? null
148
+ } catch {
149
+ return null
150
+ }
151
+ }
152
+
153
+ export async function requestOpenAI(input) {
154
+ const { apiKey, baseUrl, model, system, messages, tools, timeoutMs = 120000, maxTokens, retry = {}, signal = null } = input
155
+ if (!apiKey) {
156
+ throw new ProviderError(`missing API key for openai provider (env: ${input.apiKeyEnv || "unknown"})`, {
157
+ provider: "openai"
158
+ })
159
+ }
160
+
161
+ const payload = {
162
+ model,
163
+ messages: [...buildSystemMessages(system), ...mapMessages(messages)],
164
+ tools: mapTools(tools),
165
+ tool_choice: tools?.length ? "auto" : undefined,
166
+ ...(maxTokens ? { max_tokens: maxTokens } : {})
167
+ }
168
+ const endpoint = `${baseUrl.replace(/\/$/, "")}/chat/completions`
169
+
170
+ return requestWithRetry({
171
+ attempts: Number(retry.attempts ?? 3),
172
+ baseDelayMs: Number(retry.baseDelayMs ?? 800),
173
+ signal,
174
+ execute: async () => {
175
+ const response = await fetch(endpoint, {
176
+ method: "POST",
177
+ headers: {
178
+ "content-type": "application/json",
179
+ authorization: `Bearer ${apiKey}`
180
+ },
181
+ body: JSON.stringify(payload),
182
+ signal: timeoutSignal(timeoutMs, signal)
183
+ })
184
+
185
+ if (!response.ok) {
186
+ const text = await response.text().catch(() => "")
187
+ const error = new ProviderError(`openai request failed: ${response.status} ${text}`, {
188
+ provider: "openai",
189
+ model,
190
+ endpoint
191
+ })
192
+ error.httpStatus = response.status
193
+ throw error
194
+ }
195
+
196
+ const json = await response.json()
197
+ const message = json?.choices?.[0]?.message ?? {}
198
+ const promptTokens = json?.usage?.prompt_tokens ?? 0
199
+ const cachedTokens = json?.usage?.prompt_tokens_details?.cached_tokens ?? 0
200
+ const usage = {
201
+ input: promptTokens - cachedTokens,
202
+ output: json?.usage?.completion_tokens ?? 0,
203
+ cacheRead: cachedTokens,
204
+ cacheWrite: 0
205
+ }
206
+ const toolCalls = parseToolCalls(message)
207
+ const text = typeof message.content === "string" ? message.content : ""
208
+ return { text, usage, toolCalls }
209
+ }
210
+ })
211
+ }
212
+
213
+ export async function* requestOpenAIStream(input) {
214
+ const { apiKey, baseUrl, model, system, messages, tools, timeoutMs = 120000, streamIdleTimeoutMs = 120000, maxTokens, retry = {}, signal = null } = input
215
+ if (!apiKey) {
216
+ throw new ProviderError(`missing API key for openai provider (env: ${input.apiKeyEnv || "unknown"})`, {
217
+ provider: "openai"
218
+ })
219
+ }
220
+
221
+ const payload = {
222
+ model,
223
+ messages: [...buildSystemMessages(system), ...mapMessages(messages)],
224
+ tools: mapTools(tools),
225
+ tool_choice: tools?.length ? "auto" : undefined,
226
+ ...(maxTokens ? { max_tokens: maxTokens } : {}),
227
+ stream: true,
228
+ stream_options: { include_usage: true }
229
+ }
230
+ const endpoint = `${baseUrl.replace(/\/$/, "")}/chat/completions`
231
+ const attempts = Number(retry.attempts ?? 3)
232
+ const baseDelayMs = Number(retry.baseDelayMs ?? 800)
233
+
234
+ let response
235
+ for (let attempt = 1; attempt <= attempts; attempt++) {
236
+ try {
237
+ // Use a connection-only timeout for the initial fetch.
238
+ // Once headers arrive, clear it — the SSE idle timeout handles the streaming phase.
239
+ const connController = new AbortController()
240
+ const connTimer = setTimeout(() => connController.abort(), timeoutMs)
241
+ const fetchSignal = signal
242
+ ? AbortSignal.any([signal, connController.signal])
243
+ : connController.signal
244
+
245
+ response = await fetch(endpoint, {
246
+ method: "POST",
247
+ headers: {
248
+ "content-type": "application/json",
249
+ authorization: `Bearer ${apiKey}`
250
+ },
251
+ body: JSON.stringify(payload),
252
+ signal: fetchSignal
253
+ })
254
+ clearTimeout(connTimer)
255
+
256
+ if (!response.ok) {
257
+ const text = await response.text().catch(() => "")
258
+ const error = new ProviderError(`openai stream failed: ${response.status} ${text}`, {
259
+ provider: "openai", model, endpoint
260
+ })
261
+ error.httpStatus = response.status
262
+ throw error
263
+ }
264
+ break
265
+ } catch (err) {
266
+ if (signal?.aborted) throw err
267
+ const isNetwork = err?.code === "ETIMEDOUT" || err?.code === "ECONNRESET" || err?.name === "AbortError"
268
+ if (!isNetwork || attempt >= attempts) throw err
269
+ await sleep(baseDelayMs * Math.pow(2, attempt - 1))
270
+ }
271
+ }
272
+
273
+ const toolBuffers = new Map()
274
+ let finishReason = null
275
+
276
+ for await (const { data } of parseSSE(response.body, signal, { idleTimeoutMs: streamIdleTimeoutMs })) {
277
+ let json
278
+ try { json = JSON.parse(data) } catch { continue }
279
+
280
+ if (json.usage) {
281
+ const pt = json.usage.prompt_tokens ?? 0
282
+ const ct = json.usage.prompt_tokens_details?.cached_tokens ?? 0
283
+ yield {
284
+ type: "usage",
285
+ usage: { input: pt - ct, output: json.usage.completion_tokens ?? 0, cacheRead: ct, cacheWrite: 0 }
286
+ }
287
+ }
288
+
289
+ const choice = json.choices?.[0]
290
+ if (choice?.finish_reason) {
291
+ finishReason = choice.finish_reason
292
+ }
293
+ const delta = choice?.delta
294
+ if (!delta) continue
295
+
296
+ if (delta.content) {
297
+ yield { type: "text", content: delta.content }
298
+ }
299
+
300
+ if (delta.tool_calls) {
301
+ for (const tc of delta.tool_calls) {
302
+ const idx = tc.index ?? 0
303
+ if (!toolBuffers.has(idx)) {
304
+ toolBuffers.set(idx, { id: "", name: "", argsJson: "" })
305
+ }
306
+ const buf = toolBuffers.get(idx)
307
+ if (tc.id) buf.id = tc.id
308
+ if (tc.function?.name) buf.name = tc.function.name
309
+ if (tc.function?.arguments) buf.argsJson += tc.function.arguments
310
+ }
311
+ }
312
+ }
313
+
314
+ for (const [, buf] of toolBuffers) {
315
+ const raw = buf.argsJson || "{}"
316
+ let args = {}
317
+ try {
318
+ args = JSON.parse(raw)
319
+ } catch (parseErr) {
320
+ console.error(`[openai] tool_call JSON parse failed for "${buf.name}": ${parseErr.message} (${raw.length} chars, first 200: ${raw.slice(0, 200)})`)
321
+ args = { __parse_error: true, __raw_length: raw.length, __error: parseErr.message }
322
+ }
323
+ yield {
324
+ type: "tool_call",
325
+ call: {
326
+ id: buf.id || `tc_${Date.now()}`,
327
+ name: buf.name,
328
+ args
329
+ }
330
+ }
331
+ }
332
+
333
+ // Normalize: "stop" → "end_turn", "length" → "max_tokens", "tool_calls" → "tool_use"
334
+ const normalizedReason = finishReason === "length" ? "max_tokens"
335
+ : finishReason === "tool_calls" ? "tool_use"
336
+ : finishReason === "stop" ? "end_turn"
337
+ : finishReason || "end_turn"
338
+ yield { type: "stop", reason: normalizedReason }
339
+ }
@@ -0,0 +1,68 @@
1
+ function sleep(ms) {
2
+ return new Promise((resolve) => setTimeout(resolve, ms))
3
+ }
4
+
5
+ export function classifyHttpError(status) {
6
+ if (status === 401 || status === 403) return "auth"
7
+ if (status === 429) return "rate_limit"
8
+ if (status === 413 || status === 400) return "context_overflow"
9
+ if (status >= 500) return "server"
10
+ if (status === 408 || status === 409 || status === 425) return "transient"
11
+ return "unknown"
12
+ }
13
+
14
+ function isRetryable(classification) {
15
+ return classification === "rate_limit" || classification === "server" || classification === "transient"
16
+ }
17
+
18
+ function jitter(ms) {
19
+ return Math.round(ms * (1 + (Math.random() - 0.5) * 0.4))
20
+ }
21
+
22
+ function retryDelayMs(classification, baseDelayMs, attempt) {
23
+ if (classification === "rate_limit") {
24
+ return jitter(Math.min(baseDelayMs * Math.pow(3, attempt - 1), 60000))
25
+ }
26
+ return jitter(baseDelayMs * Math.pow(2, attempt - 1))
27
+ }
28
+
29
+ export async function requestWithRetry({ execute, attempts = 3, baseDelayMs = 800, signal = null }) {
30
+ let lastError = null
31
+ for (let attempt = 1; attempt <= Math.max(1, attempts); attempt++) {
32
+ if (signal?.aborted) {
33
+ const error = new Error("request aborted")
34
+ error.code = "ABORT_ERR"
35
+ throw error
36
+ }
37
+ try {
38
+ return await execute(attempt)
39
+ } catch (error) {
40
+ lastError = error
41
+ const status = Number(error?.status || error?.httpStatus || 0)
42
+ const classification = classifyHttpError(status)
43
+
44
+ error.errorClass = classification
45
+
46
+ if (classification === "auth") {
47
+ error.message = `authentication failed (${status}): check your API key. ${error.message}`
48
+ throw error
49
+ }
50
+
51
+ if (classification === "context_overflow") {
52
+ error.needsCompaction = true
53
+ throw error
54
+ }
55
+
56
+ const networkRetryable = error?.code === "ETIMEDOUT" || error?.code === "ECONNRESET"
57
+ if ((!isRetryable(classification) && !networkRetryable) || attempt >= attempts) {
58
+ throw error
59
+ }
60
+
61
+ const delay = networkRetryable
62
+ ? jitter(baseDelayMs * Math.pow(2, attempt - 1))
63
+ : retryDelayMs(classification, baseDelayMs, attempt)
64
+ await sleep(delay)
65
+ }
66
+ }
67
+ throw lastError || new Error("request failed")
68
+ }
@@ -0,0 +1,228 @@
1
+ import { requestAnthropic, requestAnthropicStream, countTokensAnthropic } from "./anthropic.mjs"
2
+ import { requestOpenAI, requestOpenAIStream, countTokensOpenAI } from "./openai.mjs"
3
+ import { request as requestOAICompat, requestStream as requestStreamOAICompat } from "./openai-compatible.mjs"
4
+ import { requestOllama, requestOllamaStream } from "./ollama.mjs"
5
+ import { ProviderError } from "../core/errors.mjs"
6
+
7
+ // --- Provider Registry ---
8
+ const registry = new Map()
9
+
10
+ export function registerProvider(name, mod) {
11
+ if (!mod || typeof mod.request !== "function" || typeof mod.requestStream !== "function") {
12
+ throw new Error(`provider "${name}" must export request() and requestStream()`)
13
+ }
14
+ registry.set(name, mod)
15
+ }
16
+
17
+ export function listProviders() {
18
+ return [...registry.keys()]
19
+ }
20
+
21
+ export function getProvider(name) {
22
+ return registry.get(name) || null
23
+ }
24
+
25
+ // Built-in providers
26
+ registerProvider("openai", { request: requestOpenAI, requestStream: requestOpenAIStream, countTokens: countTokensOpenAI })
27
+ registerProvider("anthropic", { request: requestAnthropic, requestStream: requestAnthropicStream, countTokens: countTokensAnthropic })
28
+ registerProvider("openai-compatible", { request: requestOAICompat, requestStream: requestStreamOAICompat, countTokens: countTokensOpenAI })
29
+ registerProvider("ollama", { request: requestOllama, requestStream: requestOllamaStream })
30
+
31
+ // --- Settings Resolution ---
32
+ function resolveSettings(configState, providerType, overrides = {}) {
33
+ const llm = configState.config.provider
34
+
35
+ // Resolve registry key: direct match → config type field → fallback to openai
36
+ let resolvedType = providerType
37
+ if (!registry.has(providerType)) {
38
+ const providerConfig = llm[providerType]
39
+ if (providerConfig?.type && registry.has(providerConfig.type)) {
40
+ resolvedType = providerConfig.type
41
+ } else {
42
+ resolvedType = "openai"
43
+ }
44
+ }
45
+
46
+ // Read config from original provider name (e.g. "deepseek"), not resolved type
47
+ const defaults = llm[providerType] || llm[resolvedType] || {}
48
+ const normalizedModel = String(overrides.model || defaults.default_model || "").includes("/")
49
+ ? String(overrides.model || defaults.default_model).split("/").slice(1).join("/")
50
+ : String(overrides.model || defaults.default_model || "")
51
+ return {
52
+ providerType: resolvedType,
53
+ configKey: providerType,
54
+ model: normalizedModel,
55
+ baseUrl: overrides.baseUrl || defaults.base_url,
56
+ apiKeyEnv: overrides.apiKeyEnv || defaults.api_key_env
57
+ }
58
+ }
59
+
60
+ function classifyProviderFailure(error) {
61
+ const cls = String(error?.errorClass || "").toLowerCase()
62
+ if (["auth", "authentication"].includes(cls)) return "auth"
63
+ if (["rate_limit"].includes(cls)) return "rate_limit"
64
+ if (["context_overflow", "bad_response"].includes(cls)) return "bad_response"
65
+ if (["server", "transient"].includes(cls)) return "bad_response"
66
+
67
+ const status = Number(error?.status || error?.httpStatus || 0)
68
+ if (status === 401 || status === 403) return "auth"
69
+ if (status === 429) return "rate_limit"
70
+ if (status >= 400 && status < 500) return "bad_response"
71
+ if (status >= 500) return "bad_response"
72
+
73
+ const code = String(error?.code || "").toUpperCase()
74
+ const msg = String(error?.message || "").toLowerCase()
75
+ if (code === "ABORT_ERR" || msg.includes("timeout") || msg.includes("timed out")) return "timeout"
76
+ if (code === "ETIMEDOUT" || code === "ECONNRESET") return "timeout"
77
+ if (msg.includes("invalid json") || msg.includes("parse")) return "bad_response"
78
+ return "unknown"
79
+ }
80
+
81
+ function normalizeProviderError(error, providerType, model) {
82
+ const reason = classifyProviderFailure(error)
83
+ if (error instanceof ProviderError) {
84
+ error.reason = error.reason || reason
85
+ error.details = {
86
+ ...(error.details || {}),
87
+ provider: providerType,
88
+ model,
89
+ reason: error.reason
90
+ }
91
+ return error
92
+ }
93
+ const wrapped = new ProviderError(error?.message || "provider request failed", {
94
+ provider: providerType,
95
+ model,
96
+ reason
97
+ })
98
+ wrapped.reason = reason
99
+ wrapped.cause = error
100
+ return wrapped
101
+ }
102
+
103
+ // --- Non-streaming Request ---
104
+ export async function requestProvider({
105
+ configState,
106
+ providerType,
107
+ model,
108
+ system,
109
+ messages,
110
+ tools,
111
+ baseUrl = null,
112
+ apiKeyEnv = null
113
+ }) {
114
+ const resolvedProviderType = providerType || configState.config.provider.default
115
+ const settings = resolveSettings(configState, resolvedProviderType, {
116
+ model,
117
+ baseUrl,
118
+ apiKeyEnv
119
+ })
120
+ const apiKey = process.env[settings.apiKeyEnv] || ""
121
+ const providerCfg = configState.config.provider[settings.configKey] || configState.config.provider[settings.providerType] || {}
122
+
123
+ const input = {
124
+ apiKey,
125
+ baseUrl: settings.baseUrl,
126
+ apiKeyEnv: settings.apiKeyEnv,
127
+ model: settings.model,
128
+ system,
129
+ messages,
130
+ tools,
131
+ timeoutMs: Number(providerCfg.timeout_ms || 120000),
132
+ maxTokens: Number(providerCfg.max_tokens || 16384),
133
+ retry: {
134
+ attempts: Number(providerCfg.retry_attempts || 3),
135
+ baseDelayMs: Number(providerCfg.retry_base_delay_ms || 800)
136
+ },
137
+ thinking: providerCfg.thinking || null
138
+ }
139
+
140
+ const provider = registry.get(settings.providerType)
141
+ if (!provider) {
142
+ throw new Error(`unknown provider: ${settings.providerType}. registered: ${listProviders().join(", ")}`)
143
+ }
144
+ try {
145
+ return await provider.request(input)
146
+ } catch (error) {
147
+ throw normalizeProviderError(error, settings.providerType, settings.model)
148
+ }
149
+ }
150
+
151
+ // --- Streaming Request ---
152
+ export async function* requestProviderStream({
153
+ configState,
154
+ providerType,
155
+ model,
156
+ system,
157
+ messages,
158
+ tools,
159
+ baseUrl = null,
160
+ apiKeyEnv = null,
161
+ signal = null,
162
+ compaction = null
163
+ }) {
164
+ const resolvedProviderType = providerType || configState.config.provider.default
165
+ const settings = resolveSettings(configState, resolvedProviderType, {
166
+ model,
167
+ baseUrl,
168
+ apiKeyEnv
169
+ })
170
+ const apiKey = process.env[settings.apiKeyEnv] || ""
171
+ const providerCfg = configState.config.provider[settings.configKey] || configState.config.provider[settings.providerType] || {}
172
+
173
+ if (providerCfg.stream === false) {
174
+ const result = await requestProvider({
175
+ configState, providerType, model, system, messages, tools, baseUrl, apiKeyEnv
176
+ })
177
+ if (result.text) yield { type: "text", content: result.text }
178
+ for (const call of result.toolCalls) yield { type: "tool_call", call }
179
+ yield { type: "usage", usage: result.usage }
180
+ return
181
+ }
182
+
183
+ const input = {
184
+ apiKey,
185
+ baseUrl: settings.baseUrl,
186
+ apiKeyEnv: settings.apiKeyEnv,
187
+ model: settings.model,
188
+ system,
189
+ messages,
190
+ tools,
191
+ timeoutMs: Number(providerCfg.timeout_ms || 120000),
192
+ streamIdleTimeoutMs: Number(providerCfg.stream_idle_timeout_ms || 120000),
193
+ maxTokens: Number(providerCfg.max_tokens || 16384),
194
+ retry: {
195
+ attempts: Number(providerCfg.retry_attempts || 3),
196
+ baseDelayMs: Number(providerCfg.retry_base_delay_ms || 800)
197
+ },
198
+ thinking: providerCfg.thinking || null,
199
+ signal,
200
+ compaction
201
+ }
202
+
203
+ const provider = registry.get(settings.providerType)
204
+ if (!provider) {
205
+ throw new Error(`unknown provider: ${settings.providerType}. registered: ${listProviders().join(", ")}`)
206
+ }
207
+ try {
208
+ yield* provider.requestStream(input)
209
+ } catch (error) {
210
+ throw normalizeProviderError(error, settings.providerType, settings.model)
211
+ }
212
+ }
213
+
214
+ // --- Token Counting (Anthropic only, returns null for other providers) ---
215
+ export async function countTokensProvider({
216
+ configState, providerType, model, system, messages, tools,
217
+ baseUrl = null, apiKeyEnv = null
218
+ }) {
219
+ const resolvedProviderType = providerType || configState.config.provider.default
220
+ const settings = resolveSettings(configState, resolvedProviderType, { model, baseUrl, apiKeyEnv })
221
+ const provider = registry.get(settings.providerType)
222
+ if (!provider?.countTokens) return null
223
+ const apiKey = process.env[settings.apiKeyEnv] || ""
224
+ return provider.countTokens({
225
+ apiKey, baseUrl: settings.baseUrl, model: settings.model,
226
+ system, messages, tools
227
+ })
228
+ }