@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.
- package/LICENSE +674 -0
- package/README.md +445 -0
- package/package.json +46 -0
- package/src/agent/agent.mjs +170 -0
- package/src/agent/custom-agent-loader.mjs +158 -0
- package/src/agent/generator.mjs +115 -0
- package/src/agent/prompt/architect.txt +36 -0
- package/src/agent/prompt/build-fixer.txt +71 -0
- package/src/agent/prompt/build.txt +101 -0
- package/src/agent/prompt/compaction.txt +12 -0
- package/src/agent/prompt/explore.txt +29 -0
- package/src/agent/prompt/guide.txt +40 -0
- package/src/agent/prompt/longagent.txt +178 -0
- package/src/agent/prompt/plan.txt +50 -0
- package/src/agent/prompt/researcher.txt +23 -0
- package/src/agent/prompt/reviewer.txt +44 -0
- package/src/agent/prompt/security-reviewer.txt +62 -0
- package/src/agent/prompt/tdd-guide.txt +84 -0
- package/src/agent/prompt/title.txt +8 -0
- package/src/command/custom-commands.mjs +57 -0
- package/src/commands/agent.mjs +71 -0
- package/src/commands/audit.mjs +77 -0
- package/src/commands/background.mjs +86 -0
- package/src/commands/chat.mjs +114 -0
- package/src/commands/command.mjs +41 -0
- package/src/commands/config.mjs +44 -0
- package/src/commands/doctor.mjs +148 -0
- package/src/commands/hook.mjs +29 -0
- package/src/commands/init.mjs +141 -0
- package/src/commands/longagent.mjs +100 -0
- package/src/commands/mcp.mjs +89 -0
- package/src/commands/permission.mjs +36 -0
- package/src/commands/prompt.mjs +42 -0
- package/src/commands/review.mjs +266 -0
- package/src/commands/rule.mjs +34 -0
- package/src/commands/session.mjs +235 -0
- package/src/commands/theme.mjs +98 -0
- package/src/commands/usage.mjs +91 -0
- package/src/config/defaults.mjs +195 -0
- package/src/config/import-config.mjs +76 -0
- package/src/config/load-config.mjs +76 -0
- package/src/config/schema.mjs +509 -0
- package/src/context.mjs +40 -0
- package/src/core/constants.mjs +46 -0
- package/src/core/errors.mjs +57 -0
- package/src/core/events.mjs +29 -0
- package/src/core/types.mjs +57 -0
- package/src/github/api.mjs +78 -0
- package/src/github/auth.mjs +286 -0
- package/src/github/flow.mjs +298 -0
- package/src/github/workspace.mjs +212 -0
- package/src/index.mjs +82 -0
- package/src/knowledge/api-design.txt +9 -0
- package/src/knowledge/cpp.txt +10 -0
- package/src/knowledge/docker.txt +10 -0
- package/src/knowledge/dotnet.txt +9 -0
- package/src/knowledge/electron.txt +10 -0
- package/src/knowledge/flutter.txt +10 -0
- package/src/knowledge/go.txt +9 -0
- package/src/knowledge/graphql.txt +10 -0
- package/src/knowledge/java.txt +9 -0
- package/src/knowledge/kotlin.txt +10 -0
- package/src/knowledge/loader.mjs +125 -0
- package/src/knowledge/next.txt +8 -0
- package/src/knowledge/node.txt +8 -0
- package/src/knowledge/nuxt.txt +9 -0
- package/src/knowledge/php.txt +10 -0
- package/src/knowledge/python.txt +10 -0
- package/src/knowledge/react-native.txt +10 -0
- package/src/knowledge/react.txt +9 -0
- package/src/knowledge/ruby.txt +11 -0
- package/src/knowledge/rust.txt +9 -0
- package/src/knowledge/svelte.txt +9 -0
- package/src/knowledge/swift.txt +10 -0
- package/src/knowledge/tailwind.txt +10 -0
- package/src/knowledge/testing.txt +8 -0
- package/src/knowledge/typescript.txt +8 -0
- package/src/knowledge/vue.txt +9 -0
- package/src/mcp/client-http.mjs +157 -0
- package/src/mcp/client-sse.mjs +286 -0
- package/src/mcp/client-stdio.mjs +451 -0
- package/src/mcp/registry.mjs +394 -0
- package/src/mcp/stdio-framing.mjs +127 -0
- package/src/orchestration/background-manager.mjs +358 -0
- package/src/orchestration/background-worker.mjs +245 -0
- package/src/orchestration/longagent-manager.mjs +116 -0
- package/src/orchestration/stage-scheduler.mjs +489 -0
- package/src/orchestration/subagent-router.mjs +62 -0
- package/src/orchestration/task-scheduler.mjs +74 -0
- package/src/permission/engine.mjs +92 -0
- package/src/permission/exec-policy.mjs +372 -0
- package/src/permission/prompt.mjs +39 -0
- package/src/permission/rules.mjs +120 -0
- package/src/permission/workspace-trust.mjs +44 -0
- package/src/plugin/builtin-hooks/console-warn.mjs +41 -0
- package/src/plugin/builtin-hooks/extract-patterns.mjs +75 -0
- package/src/plugin/builtin-hooks/post-edit-format.mjs +57 -0
- package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +61 -0
- package/src/plugin/builtin-hooks/strategic-compaction.mjs +38 -0
- package/src/plugin/hook-bus.mjs +154 -0
- package/src/provider/anthropic.mjs +389 -0
- package/src/provider/ollama.mjs +236 -0
- package/src/provider/openai-compatible.mjs +1 -0
- package/src/provider/openai.mjs +339 -0
- package/src/provider/retry-policy.mjs +68 -0
- package/src/provider/router.mjs +228 -0
- package/src/provider/sse.mjs +91 -0
- package/src/repl.mjs +2929 -0
- package/src/review/diff-parser.mjs +36 -0
- package/src/review/rejection-queue.mjs +62 -0
- package/src/review/review-store.mjs +21 -0
- package/src/review/risk-score.mjs +61 -0
- package/src/rules/load-rules.mjs +64 -0
- package/src/runtime.mjs +1 -0
- package/src/session/checkpoint.mjs +239 -0
- package/src/session/compaction.mjs +276 -0
- package/src/session/engine.mjs +225 -0
- package/src/session/instinct-manager.mjs +172 -0
- package/src/session/instruction-loader.mjs +25 -0
- package/src/session/longagent-plan.mjs +329 -0
- package/src/session/longagent-scaffold.mjs +100 -0
- package/src/session/longagent.mjs +1462 -0
- package/src/session/loop.mjs +905 -0
- package/src/session/memory-loader.mjs +75 -0
- package/src/session/project-context.mjs +367 -0
- package/src/session/prompt/anthropic.txt +151 -0
- package/src/session/prompt/beast.txt +37 -0
- package/src/session/prompt/max-steps.txt +6 -0
- package/src/session/prompt/plan.txt +9 -0
- package/src/session/prompt/qwen.txt +46 -0
- package/src/session/prompt-loader.mjs +18 -0
- package/src/session/recovery.mjs +52 -0
- package/src/session/store.mjs +503 -0
- package/src/session/system-prompt.mjs +260 -0
- package/src/session/task-validator.mjs +266 -0
- package/src/session/usability-gates.mjs +379 -0
- package/src/skill/builtin/backend-patterns.mjs +123 -0
- package/src/skill/builtin/commit.mjs +64 -0
- package/src/skill/builtin/debug.mjs +45 -0
- package/src/skill/builtin/frontend-patterns.mjs +120 -0
- package/src/skill/builtin/frontend.mjs +188 -0
- package/src/skill/builtin/init.mjs +220 -0
- package/src/skill/builtin/review.mjs +49 -0
- package/src/skill/builtin/security-checklist.mjs +80 -0
- package/src/skill/builtin/tdd.mjs +54 -0
- package/src/skill/generator.mjs +113 -0
- package/src/skill/registry.mjs +336 -0
- package/src/storage/audit-store.mjs +83 -0
- package/src/storage/event-log.mjs +82 -0
- package/src/storage/ghost-commit-store.mjs +235 -0
- package/src/storage/json-store.mjs +53 -0
- package/src/storage/paths.mjs +148 -0
- package/src/theme/color.mjs +64 -0
- package/src/theme/default-theme.mjs +29 -0
- package/src/theme/load-theme.mjs +71 -0
- package/src/theme/markdown.mjs +135 -0
- package/src/theme/schema.mjs +45 -0
- package/src/theme/status-bar.mjs +158 -0
- package/src/tool/audit-wrapper.mjs +38 -0
- package/src/tool/edit-transaction.mjs +126 -0
- package/src/tool/executor.mjs +109 -0
- package/src/tool/file-lock-manager.mjs +85 -0
- package/src/tool/git-auto.mjs +545 -0
- package/src/tool/git-full-auto.mjs +478 -0
- package/src/tool/image-util.mjs +276 -0
- package/src/tool/prompt/background_cancel.txt +1 -0
- package/src/tool/prompt/background_output.txt +1 -0
- package/src/tool/prompt/bash.txt +71 -0
- package/src/tool/prompt/codesearch.txt +18 -0
- package/src/tool/prompt/edit.txt +27 -0
- package/src/tool/prompt/enter_plan.txt +74 -0
- package/src/tool/prompt/exit_plan.txt +62 -0
- package/src/tool/prompt/glob.txt +33 -0
- package/src/tool/prompt/grep.txt +43 -0
- package/src/tool/prompt/list.txt +8 -0
- package/src/tool/prompt/multiedit.txt +20 -0
- package/src/tool/prompt/notebookedit.txt +21 -0
- package/src/tool/prompt/patch.txt +24 -0
- package/src/tool/prompt/question.txt +44 -0
- package/src/tool/prompt/read.txt +40 -0
- package/src/tool/prompt/task.txt +83 -0
- package/src/tool/prompt/todowrite.txt +117 -0
- package/src/tool/prompt/webfetch.txt +38 -0
- package/src/tool/prompt/websearch.txt +43 -0
- package/src/tool/prompt/write.txt +38 -0
- package/src/tool/prompt-loader.mjs +18 -0
- package/src/tool/question-prompt.mjs +86 -0
- package/src/tool/registry.mjs +1309 -0
- package/src/tool/task-tool.mjs +28 -0
- package/src/ui/activity-renderer.mjs +410 -0
- package/src/ui/repl-dashboard.mjs +357 -0
- package/src/usage/pricing.mjs +121 -0
- package/src/usage/usage-meter.mjs +113 -0
- package/src/util/git.mjs +496 -0
- package/src/util/template.mjs +10 -0
- package/src/util/yaml.mjs +100 -0
|
@@ -0,0 +1,389 @@
|
|
|
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 []
|
|
11
|
+
const mapped = tools.map((tool) => ({
|
|
12
|
+
name: tool.name,
|
|
13
|
+
description: tool.description,
|
|
14
|
+
input_schema: tool.inputSchema
|
|
15
|
+
}))
|
|
16
|
+
// Cache the tool definitions (they rarely change within a session)
|
|
17
|
+
if (mapped.length > 0) {
|
|
18
|
+
mapped[mapped.length - 1].cache_control = { type: "ephemeral" }
|
|
19
|
+
}
|
|
20
|
+
return mapped
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function systemWithCacheControl(system) {
|
|
24
|
+
if (!system) return undefined
|
|
25
|
+
|
|
26
|
+
// Structured format from buildSystemPromptBlocks: { text, blocks }
|
|
27
|
+
// Strategy: merge all stable content into ONE block with cache_control,
|
|
28
|
+
// keeping dynamic content separate. Combined with the tool breakpoint in
|
|
29
|
+
// mapTools, this gives us 2 breakpoints total — well within the 4-max limit
|
|
30
|
+
// and ensures the cumulative prefix easily exceeds the minimum cacheable
|
|
31
|
+
// threshold (4096 tokens for Opus, 1024 for Sonnet).
|
|
32
|
+
if (system.blocks && Array.isArray(system.blocks)) {
|
|
33
|
+
const stableParts = []
|
|
34
|
+
const dynamicParts = []
|
|
35
|
+
for (const block of system.blocks) {
|
|
36
|
+
if (block.cacheable === false) {
|
|
37
|
+
dynamicParts.push(block.text)
|
|
38
|
+
} else {
|
|
39
|
+
stableParts.push(block.text)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const contentBlocks = []
|
|
44
|
+
if (stableParts.length) {
|
|
45
|
+
contentBlocks.push({
|
|
46
|
+
type: "text",
|
|
47
|
+
text: stableParts.join("\n\n"),
|
|
48
|
+
cache_control: { type: "ephemeral" }
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
if (dynamicParts.length) {
|
|
52
|
+
contentBlocks.push({ type: "text", text: dynamicParts.join("\n\n") })
|
|
53
|
+
}
|
|
54
|
+
return contentBlocks.length ? contentBlocks : undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Legacy: plain string
|
|
58
|
+
if (typeof system === "string") {
|
|
59
|
+
return [{ type: "text", text: system, cache_control: { type: "ephemeral" } }]
|
|
60
|
+
}
|
|
61
|
+
// Legacy: array of strings/blocks
|
|
62
|
+
if (Array.isArray(system)) {
|
|
63
|
+
const blocks = system.map((b) => (typeof b === "string" ? { type: "text", text: b } : { ...b }))
|
|
64
|
+
if (blocks.length > 0) {
|
|
65
|
+
blocks[blocks.length - 1].cache_control = { type: "ephemeral" }
|
|
66
|
+
}
|
|
67
|
+
return blocks
|
|
68
|
+
}
|
|
69
|
+
return system
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function mapContentBlock(block) {
|
|
73
|
+
if (block.type === "image" && block.data) {
|
|
74
|
+
return {
|
|
75
|
+
type: "image",
|
|
76
|
+
source: {
|
|
77
|
+
type: "base64",
|
|
78
|
+
media_type: block.mediaType || "image/png",
|
|
79
|
+
data: block.data
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Native Anthropic tool_use block — pass through
|
|
84
|
+
if (block.type === "tool_use") {
|
|
85
|
+
return { type: "tool_use", id: block.id, name: block.name, input: block.input || {} }
|
|
86
|
+
}
|
|
87
|
+
// Native Anthropic tool_result block — pass through
|
|
88
|
+
if (block.type === "tool_result") {
|
|
89
|
+
return {
|
|
90
|
+
type: "tool_result",
|
|
91
|
+
tool_use_id: block.tool_use_id,
|
|
92
|
+
content: String(block.content || ""),
|
|
93
|
+
...(block.is_error ? { is_error: true } : {})
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { type: "text", text: String(block.text || block.content || "") }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function mapMessages(messages) {
|
|
100
|
+
const mapped = messages.map((message) => {
|
|
101
|
+
const role = message.role === "assistant" ? "assistant" : "user"
|
|
102
|
+
const content = message.content
|
|
103
|
+
if (Array.isArray(content)) {
|
|
104
|
+
return { role, content: content.map(mapContentBlock) }
|
|
105
|
+
}
|
|
106
|
+
return { role, content: String(content || "") }
|
|
107
|
+
})
|
|
108
|
+
// Add cache_control to last user message for multi-turn caching
|
|
109
|
+
for (let i = mapped.length - 1; i >= 0; i--) {
|
|
110
|
+
if (mapped[i].role === "user") {
|
|
111
|
+
const c = mapped[i].content
|
|
112
|
+
if (Array.isArray(c) && c.length) {
|
|
113
|
+
c[c.length - 1].cache_control = { type: "ephemeral" }
|
|
114
|
+
} else if (typeof c === "string") {
|
|
115
|
+
mapped[i].content = [{ type: "text", text: c, cache_control: { type: "ephemeral" } }]
|
|
116
|
+
}
|
|
117
|
+
break
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return mapped
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function parseContentBlocks(content) {
|
|
124
|
+
const blocks = Array.isArray(content) ? content : []
|
|
125
|
+
const text = blocks.filter((block) => block.type === "text").map((block) => block.text || "").join("\n")
|
|
126
|
+
const toolCalls = blocks
|
|
127
|
+
.filter((block) => block.type === "tool_use" && block.name)
|
|
128
|
+
.map((block) => ({
|
|
129
|
+
id: block.id || `tc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
130
|
+
name: block.name,
|
|
131
|
+
args: block.input || {}
|
|
132
|
+
}))
|
|
133
|
+
return { text, toolCalls }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function timeoutSignal(ms, parentSignal = null) {
|
|
137
|
+
const own = AbortSignal.timeout(ms)
|
|
138
|
+
if (!parentSignal) return own
|
|
139
|
+
return AbortSignal.any([parentSignal, own])
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function requestAnthropic(input) {
|
|
143
|
+
const { apiKey, baseUrl, model, system, messages, tools, timeoutMs = 120000, maxTokens = 16384, retry = {}, signal = null } = input
|
|
144
|
+
if (!apiKey) {
|
|
145
|
+
throw new ProviderError(`missing API key for anthropic provider (env: ${input.apiKeyEnv || "unknown"})`, {
|
|
146
|
+
provider: "anthropic"
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const endpoint = `${baseUrl.replace(/\/$/, "")}/messages`
|
|
151
|
+
const mappedTools = mapTools(tools)
|
|
152
|
+
const payload = {
|
|
153
|
+
model,
|
|
154
|
+
max_tokens: maxTokens,
|
|
155
|
+
metadata: { user_id: "kkcode" },
|
|
156
|
+
system: systemWithCacheControl(system),
|
|
157
|
+
messages: mapMessages(messages),
|
|
158
|
+
tools: mappedTools.length ? mappedTools : undefined
|
|
159
|
+
}
|
|
160
|
+
if (input.thinking?.type) {
|
|
161
|
+
payload.thinking = { type: input.thinking.type, budget_tokens: input.thinking.budget_tokens || 10000 }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return requestWithRetry({
|
|
165
|
+
attempts: Number(retry.attempts ?? 3),
|
|
166
|
+
baseDelayMs: Number(retry.baseDelayMs ?? 800),
|
|
167
|
+
signal,
|
|
168
|
+
execute: async () => {
|
|
169
|
+
const response = await fetch(endpoint, {
|
|
170
|
+
method: "POST",
|
|
171
|
+
headers: {
|
|
172
|
+
"content-type": "application/json",
|
|
173
|
+
"x-api-key": apiKey,
|
|
174
|
+
"anthropic-version": "2023-06-01",
|
|
175
|
+
"anthropic-beta": "prompt-caching-2024-07-31"
|
|
176
|
+
},
|
|
177
|
+
body: JSON.stringify(payload),
|
|
178
|
+
signal: timeoutSignal(timeoutMs, signal)
|
|
179
|
+
})
|
|
180
|
+
if (!response.ok) {
|
|
181
|
+
const text = await response.text().catch(() => "")
|
|
182
|
+
const error = new ProviderError(`anthropic request failed: ${response.status} ${text}`, {
|
|
183
|
+
provider: "anthropic",
|
|
184
|
+
model,
|
|
185
|
+
endpoint
|
|
186
|
+
})
|
|
187
|
+
error.httpStatus = response.status
|
|
188
|
+
throw error
|
|
189
|
+
}
|
|
190
|
+
const json = await response.json()
|
|
191
|
+
const parsed = parseContentBlocks(json?.content)
|
|
192
|
+
const usage = {
|
|
193
|
+
input: json?.usage?.input_tokens ?? 0,
|
|
194
|
+
output: json?.usage?.output_tokens ?? 0,
|
|
195
|
+
cacheRead: json?.usage?.cache_read_input_tokens ?? 0,
|
|
196
|
+
cacheWrite: json?.usage?.cache_creation_input_tokens ?? 0
|
|
197
|
+
}
|
|
198
|
+
return { text: parsed.text, usage, toolCalls: parsed.toolCalls }
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function countTokensAnthropic(input) {
|
|
204
|
+
const { apiKey, baseUrl, model, system, messages, tools, timeoutMs = 10000 } = input
|
|
205
|
+
if (!apiKey) return null
|
|
206
|
+
const endpoint = `${baseUrl.replace(/\/$/, "")}/messages/count_tokens`
|
|
207
|
+
const mappedTools = mapTools(tools)
|
|
208
|
+
const payload = {
|
|
209
|
+
model,
|
|
210
|
+
system: systemWithCacheControl(system),
|
|
211
|
+
messages: mapMessages(messages),
|
|
212
|
+
tools: mappedTools.length ? mappedTools : undefined
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
const res = await fetch(endpoint, {
|
|
216
|
+
method: "POST",
|
|
217
|
+
headers: {
|
|
218
|
+
"content-type": "application/json",
|
|
219
|
+
"x-api-key": apiKey,
|
|
220
|
+
"anthropic-version": "2023-06-01"
|
|
221
|
+
},
|
|
222
|
+
body: JSON.stringify(payload),
|
|
223
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
224
|
+
})
|
|
225
|
+
if (!res.ok) return null
|
|
226
|
+
const json = await res.json()
|
|
227
|
+
return json?.input_tokens ?? null
|
|
228
|
+
} catch {
|
|
229
|
+
return null
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function* requestAnthropicStream(input) {
|
|
234
|
+
const { apiKey, baseUrl, model, system, messages, tools, timeoutMs = 120000, streamIdleTimeoutMs = 120000, maxTokens = 16384, retry = {}, signal = null, compaction = null } = input
|
|
235
|
+
if (!apiKey) {
|
|
236
|
+
throw new ProviderError(`missing API key for anthropic provider (env: ${input.apiKeyEnv || "unknown"})`, {
|
|
237
|
+
provider: "anthropic"
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const endpoint = `${baseUrl.replace(/\/$/, "")}/messages`
|
|
242
|
+
const mappedTools = mapTools(tools)
|
|
243
|
+
const payload = {
|
|
244
|
+
model,
|
|
245
|
+
max_tokens: maxTokens,
|
|
246
|
+
metadata: { user_id: "kkcode" },
|
|
247
|
+
system: systemWithCacheControl(system),
|
|
248
|
+
messages: mapMessages(messages),
|
|
249
|
+
tools: mappedTools.length ? mappedTools : undefined,
|
|
250
|
+
stream: true,
|
|
251
|
+
...(compaction ? { context_management: { edits: [{ type: "compact_20260112", trigger: { tokens: compaction.trigger || 150000 } }] } } : {})
|
|
252
|
+
}
|
|
253
|
+
if (input.thinking?.type) {
|
|
254
|
+
payload.thinking = { type: input.thinking.type, budget_tokens: input.thinking.budget_tokens || 10000 }
|
|
255
|
+
}
|
|
256
|
+
const attempts = Number(retry.attempts ?? 3)
|
|
257
|
+
const baseDelayMs = Number(retry.baseDelayMs ?? 800)
|
|
258
|
+
|
|
259
|
+
let response
|
|
260
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
261
|
+
try {
|
|
262
|
+
// Use a connection-only timeout for the initial fetch.
|
|
263
|
+
// Once headers arrive, clear it — the SSE idle timeout handles the streaming phase.
|
|
264
|
+
const connController = new AbortController()
|
|
265
|
+
const connTimer = setTimeout(() => connController.abort(), timeoutMs)
|
|
266
|
+
const fetchSignal = signal
|
|
267
|
+
? AbortSignal.any([signal, connController.signal])
|
|
268
|
+
: connController.signal
|
|
269
|
+
|
|
270
|
+
response = await fetch(endpoint, {
|
|
271
|
+
method: "POST",
|
|
272
|
+
headers: {
|
|
273
|
+
"content-type": "application/json",
|
|
274
|
+
"x-api-key": apiKey,
|
|
275
|
+
"anthropic-version": "2023-06-01",
|
|
276
|
+
"anthropic-beta": compaction ? "prompt-caching-2024-07-31,compact-2026-01-12" : "prompt-caching-2024-07-31"
|
|
277
|
+
},
|
|
278
|
+
body: JSON.stringify(payload),
|
|
279
|
+
signal: fetchSignal
|
|
280
|
+
})
|
|
281
|
+
clearTimeout(connTimer)
|
|
282
|
+
|
|
283
|
+
if (!response.ok) {
|
|
284
|
+
const text = await response.text().catch(() => "")
|
|
285
|
+
const error = new ProviderError(`anthropic stream failed: ${response.status} ${text}`, {
|
|
286
|
+
provider: "anthropic", model, endpoint
|
|
287
|
+
})
|
|
288
|
+
error.httpStatus = response.status
|
|
289
|
+
throw error
|
|
290
|
+
}
|
|
291
|
+
break
|
|
292
|
+
} catch (err) {
|
|
293
|
+
if (signal?.aborted) throw err
|
|
294
|
+
const isNetwork = err?.code === "ETIMEDOUT" || err?.code === "ECONNRESET" || err?.name === "AbortError"
|
|
295
|
+
if (!isNetwork || attempt >= attempts) throw err
|
|
296
|
+
await sleep(baseDelayMs * Math.pow(2, attempt - 1))
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
let currentBlock = null
|
|
301
|
+
let inputUsage = { input: 0, cacheRead: 0, cacheWrite: 0 }
|
|
302
|
+
let outputTokens = 0
|
|
303
|
+
let stopReason = null
|
|
304
|
+
|
|
305
|
+
for await (const { event, data } of parseSSE(response.body, signal, { idleTimeoutMs: streamIdleTimeoutMs })) {
|
|
306
|
+
let parsed
|
|
307
|
+
try { parsed = JSON.parse(data) } catch { continue }
|
|
308
|
+
|
|
309
|
+
if (event === "message_start") {
|
|
310
|
+
const u = parsed.message?.usage
|
|
311
|
+
inputUsage.input = u?.input_tokens ?? 0
|
|
312
|
+
inputUsage.cacheRead = u?.cache_read_input_tokens ?? 0
|
|
313
|
+
inputUsage.cacheWrite = u?.cache_creation_input_tokens ?? 0
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (event === "content_block_start") {
|
|
317
|
+
const block = parsed.content_block
|
|
318
|
+
currentBlock = {
|
|
319
|
+
type: block?.type,
|
|
320
|
+
id: block?.id || null,
|
|
321
|
+
name: block?.name || null,
|
|
322
|
+
jsonParts: []
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (event === "content_block_delta") {
|
|
327
|
+
if (parsed.delta?.type === "text_delta") {
|
|
328
|
+
const text = parsed.delta.text || ""
|
|
329
|
+
if (text) yield { type: "text", content: text }
|
|
330
|
+
}
|
|
331
|
+
if (parsed.delta?.type === "thinking_delta" && currentBlock?.type !== "redacted_thinking") {
|
|
332
|
+
const thinking = parsed.delta.thinking || ""
|
|
333
|
+
if (thinking) yield { type: "thinking", content: thinking }
|
|
334
|
+
}
|
|
335
|
+
if (parsed.delta?.type === "input_json_delta") {
|
|
336
|
+
if (currentBlock) currentBlock.jsonParts.push(parsed.delta.partial_json || "")
|
|
337
|
+
}
|
|
338
|
+
if (parsed.delta?.type === "compaction_delta") {
|
|
339
|
+
if (currentBlock) currentBlock.compactionContent = parsed.delta.content || ""
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (event === "content_block_stop" && currentBlock) {
|
|
344
|
+
if (currentBlock.type === "tool_use") {
|
|
345
|
+
const raw = currentBlock.jsonParts.join("") || "{}"
|
|
346
|
+
let args = {}
|
|
347
|
+
try {
|
|
348
|
+
args = JSON.parse(raw)
|
|
349
|
+
} catch (parseErr) {
|
|
350
|
+
console.error(`[anthropic] tool_call JSON parse failed for "${currentBlock.name}": ${parseErr.message} (${raw.length} chars, first 200: ${raw.slice(0, 200)})`)
|
|
351
|
+
args = { __parse_error: true, __raw_length: raw.length, __error: parseErr.message }
|
|
352
|
+
}
|
|
353
|
+
yield {
|
|
354
|
+
type: "tool_call",
|
|
355
|
+
call: {
|
|
356
|
+
id: currentBlock.id || `tc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
357
|
+
name: currentBlock.name,
|
|
358
|
+
args
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (currentBlock.type === "compaction") {
|
|
363
|
+
yield { type: "compaction", content: currentBlock.compactionContent || "" }
|
|
364
|
+
}
|
|
365
|
+
currentBlock = null
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (event === "message_delta") {
|
|
369
|
+
outputTokens = parsed.usage?.output_tokens ?? outputTokens
|
|
370
|
+
if (parsed.delta?.stop_reason) {
|
|
371
|
+
stopReason = parsed.delta.stop_reason
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (event === "message_stop") {
|
|
376
|
+
yield {
|
|
377
|
+
type: "usage",
|
|
378
|
+
usage: {
|
|
379
|
+
input: inputUsage.input,
|
|
380
|
+
output: outputTokens,
|
|
381
|
+
cacheRead: inputUsage.cacheRead,
|
|
382
|
+
cacheWrite: inputUsage.cacheWrite
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// Normalize: "end_turn" → "end_turn", "max_tokens" → "max_tokens", "tool_use" → "tool_use"
|
|
386
|
+
yield { type: "stop", reason: stopReason || "end_turn" }
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { ProviderError } from "../core/errors.mjs"
|
|
2
|
+
|
|
3
|
+
function mapTools(tools) {
|
|
4
|
+
if (!tools || !tools.length) return undefined
|
|
5
|
+
return tools.map((tool) => ({
|
|
6
|
+
type: "function",
|
|
7
|
+
function: {
|
|
8
|
+
name: tool.name,
|
|
9
|
+
description: tool.description,
|
|
10
|
+
parameters: tool.inputSchema
|
|
11
|
+
}
|
|
12
|
+
}))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function resolveSystemText(system) {
|
|
16
|
+
if (!system) return ""
|
|
17
|
+
if (typeof system === "string") return system
|
|
18
|
+
if (system.text) return system.text
|
|
19
|
+
return String(system)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function mapMessages(system, messages) {
|
|
23
|
+
const mapped = [{ role: "system", content: resolveSystemText(system) }]
|
|
24
|
+
for (const msg of messages) {
|
|
25
|
+
const content = msg.content
|
|
26
|
+
if (!Array.isArray(content)) {
|
|
27
|
+
mapped.push({ role: msg.role, content: String(content || "") })
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Assistant message with tool_use blocks → tool_calls format
|
|
32
|
+
const toolUseBlocks = content.filter((b) => b.type === "tool_use")
|
|
33
|
+
if (toolUseBlocks.length > 0 && msg.role === "assistant") {
|
|
34
|
+
const textParts = content.filter((b) => b.type === "text").map((b) => b.text || "").join("\n")
|
|
35
|
+
mapped.push({
|
|
36
|
+
role: "assistant",
|
|
37
|
+
content: textParts || "",
|
|
38
|
+
tool_calls: toolUseBlocks.map((b) => ({
|
|
39
|
+
id: b.id,
|
|
40
|
+
type: "function",
|
|
41
|
+
function: {
|
|
42
|
+
name: b.name,
|
|
43
|
+
arguments: JSON.stringify(b.input || {})
|
|
44
|
+
}
|
|
45
|
+
}))
|
|
46
|
+
})
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// User message with tool_result blocks → role:"tool" messages
|
|
51
|
+
const toolResultBlocks = content.filter((b) => b.type === "tool_result")
|
|
52
|
+
if (toolResultBlocks.length > 0) {
|
|
53
|
+
for (const result of toolResultBlocks) {
|
|
54
|
+
mapped.push({
|
|
55
|
+
role: "tool",
|
|
56
|
+
tool_call_id: result.tool_use_id,
|
|
57
|
+
content: String(result.content || "")
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Fallback: plain text extraction
|
|
64
|
+
const text = content.filter((b) => b.type === "text").map((b) => b.text || "").join("\n")
|
|
65
|
+
mapped.push({ role: msg.role, content: text || String(content) })
|
|
66
|
+
}
|
|
67
|
+
return mapped
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseToolCalls(message) {
|
|
71
|
+
if (!Array.isArray(message?.tool_calls)) return []
|
|
72
|
+
return message.tool_calls
|
|
73
|
+
.filter((call) => call?.function?.name)
|
|
74
|
+
.map((call) => {
|
|
75
|
+
let args = {}
|
|
76
|
+
if (typeof call.function.arguments === "string") {
|
|
77
|
+
try { args = JSON.parse(call.function.arguments) } catch { args = {} }
|
|
78
|
+
} else if (typeof call.function.arguments === "object" && call.function.arguments !== null) {
|
|
79
|
+
args = call.function.arguments
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
id: call.id || `tc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
83
|
+
name: call.function.name,
|
|
84
|
+
args
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function timeoutSignal(ms, parentSignal = null) {
|
|
90
|
+
const own = AbortSignal.timeout(ms)
|
|
91
|
+
if (!parentSignal) return own
|
|
92
|
+
return AbortSignal.any([parentSignal, own])
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// --- Non-streaming ---
|
|
96
|
+
export async function requestOllama(input) {
|
|
97
|
+
const { baseUrl, model, system, messages, tools, timeoutMs = 300000, signal = null } = input
|
|
98
|
+
|
|
99
|
+
const endpoint = `${baseUrl.replace(/\/$/, "")}/api/chat`
|
|
100
|
+
const payload = {
|
|
101
|
+
model,
|
|
102
|
+
messages: mapMessages(system, messages),
|
|
103
|
+
stream: false
|
|
104
|
+
}
|
|
105
|
+
const mappedTools = mapTools(tools)
|
|
106
|
+
if (mappedTools) payload.tools = mappedTools
|
|
107
|
+
|
|
108
|
+
const response = await fetch(endpoint, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: { "content-type": "application/json" },
|
|
111
|
+
body: JSON.stringify(payload),
|
|
112
|
+
signal: timeoutSignal(timeoutMs, signal)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
const text = await response.text().catch(() => "")
|
|
117
|
+
const error = new ProviderError(`ollama request failed: ${response.status} ${text}`, {
|
|
118
|
+
provider: "ollama", model, endpoint
|
|
119
|
+
})
|
|
120
|
+
error.httpStatus = response.status
|
|
121
|
+
throw error
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const json = await response.json()
|
|
125
|
+
const message = json.message || {}
|
|
126
|
+
const text = typeof message.content === "string" ? message.content : ""
|
|
127
|
+
const toolCalls = parseToolCalls(message)
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
text,
|
|
131
|
+
usage: {
|
|
132
|
+
input: json.prompt_eval_count ?? 0,
|
|
133
|
+
output: json.eval_count ?? 0,
|
|
134
|
+
cacheRead: 0,
|
|
135
|
+
cacheWrite: 0
|
|
136
|
+
},
|
|
137
|
+
toolCalls
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// --- Streaming (NDJSON) ---
|
|
142
|
+
export async function* requestOllamaStream(input) {
|
|
143
|
+
const { baseUrl, model, system, messages, tools, timeoutMs = 300000, signal = null } = input
|
|
144
|
+
|
|
145
|
+
const endpoint = `${baseUrl.replace(/\/$/, "")}/api/chat`
|
|
146
|
+
const payload = {
|
|
147
|
+
model,
|
|
148
|
+
messages: mapMessages(system, messages),
|
|
149
|
+
stream: true
|
|
150
|
+
}
|
|
151
|
+
const mappedTools = mapTools(tools)
|
|
152
|
+
if (mappedTools) payload.tools = mappedTools
|
|
153
|
+
|
|
154
|
+
const response = await fetch(endpoint, {
|
|
155
|
+
method: "POST",
|
|
156
|
+
headers: { "content-type": "application/json" },
|
|
157
|
+
body: JSON.stringify(payload),
|
|
158
|
+
signal: timeoutSignal(timeoutMs, signal)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
if (!response.ok) {
|
|
162
|
+
const text = await response.text().catch(() => "")
|
|
163
|
+
const error = new ProviderError(`ollama stream failed: ${response.status} ${text}`, {
|
|
164
|
+
provider: "ollama", model, endpoint
|
|
165
|
+
})
|
|
166
|
+
error.httpStatus = response.status
|
|
167
|
+
throw error
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const reader = response.body.getReader()
|
|
171
|
+
const decoder = new TextDecoder()
|
|
172
|
+
let buffer = ""
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
while (true) {
|
|
176
|
+
if (signal?.aborted) break
|
|
177
|
+
const { done, value } = await reader.read()
|
|
178
|
+
if (done) break
|
|
179
|
+
buffer += decoder.decode(value, { stream: true })
|
|
180
|
+
|
|
181
|
+
const lines = buffer.split("\n")
|
|
182
|
+
buffer = lines.pop()
|
|
183
|
+
|
|
184
|
+
for (const line of lines) {
|
|
185
|
+
const trimmed = line.trim()
|
|
186
|
+
if (!trimmed) continue
|
|
187
|
+
let json
|
|
188
|
+
try { json = JSON.parse(trimmed) } catch { continue }
|
|
189
|
+
|
|
190
|
+
if (json.message?.content) {
|
|
191
|
+
yield { type: "text", content: json.message.content }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (json.done) {
|
|
195
|
+
const toolCalls = parseToolCalls(json.message)
|
|
196
|
+
for (const call of toolCalls) {
|
|
197
|
+
yield { type: "tool_call", call }
|
|
198
|
+
}
|
|
199
|
+
yield {
|
|
200
|
+
type: "usage",
|
|
201
|
+
usage: {
|
|
202
|
+
input: json.prompt_eval_count ?? 0,
|
|
203
|
+
output: json.eval_count ?? 0,
|
|
204
|
+
cacheRead: 0,
|
|
205
|
+
cacheWrite: 0
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (buffer.trim()) {
|
|
213
|
+
try {
|
|
214
|
+
const json = JSON.parse(buffer.trim())
|
|
215
|
+
if (json.message?.content) yield { type: "text", content: json.message.content }
|
|
216
|
+
if (json.done) {
|
|
217
|
+
const toolCalls = parseToolCalls(json.message)
|
|
218
|
+
for (const call of toolCalls) {
|
|
219
|
+
yield { type: "tool_call", call }
|
|
220
|
+
}
|
|
221
|
+
yield {
|
|
222
|
+
type: "usage",
|
|
223
|
+
usage: {
|
|
224
|
+
input: json.prompt_eval_count ?? 0,
|
|
225
|
+
output: json.eval_count ?? 0,
|
|
226
|
+
cacheRead: 0,
|
|
227
|
+
cacheWrite: 0
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
} catch { /* ignore incomplete JSON */ }
|
|
232
|
+
}
|
|
233
|
+
} finally {
|
|
234
|
+
try { reader.releaseLock() } catch { /* reader may have pending read if generator was force-closed */ }
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { requestOpenAI as request, requestOpenAIStream as requestStream } from "./openai.mjs"
|