@kkelly-offical/kkcode 0.1.6 → 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 +19 -2
- 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 +90 -0
- 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 -2929
- 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 +36 -14
- package/src/session/engine.mjs +417 -227
- package/src/session/longagent-4stage.mjs +467 -460
- package/src/session/longagent-hybrid.mjs +1344 -1081
- 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 -884
- package/src/session/loop.mjs +1005 -905
- package/src/session/prompt/agent.txt +25 -0
- package/src/session/prompt/anthropic.txt +150 -150
- package/src/session/prompt/beast.txt +1 -1
- package/src/session/prompt/plan.txt +28 -6
- package/src/session/prompt/qwen.txt +46 -46
- package/src/session/recovery.mjs +21 -0
- package/src/session/rollback.mjs +197 -0
- package/src/session/routing-observability.mjs +72 -0
- package/src/session/runtime-state.mjs +47 -0
- package/src/session/store.mjs +523 -510
- package/src/session/system-prompt.mjs +56 -8
- 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 +17 -4
- 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
|
@@ -1,68 +1,74 @@
|
|
|
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
|
|
9
|
-
if (status
|
|
10
|
-
if (status
|
|
11
|
-
return "
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
error
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (classification === "
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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) return "context_overflow"
|
|
9
|
+
if (status === 400) return "bad_request"
|
|
10
|
+
if (status >= 500) return "server"
|
|
11
|
+
if (status === 408 || status === 409 || status === 425) return "transient"
|
|
12
|
+
return "unknown"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isRetryable(classification) {
|
|
16
|
+
return classification === "rate_limit" || classification === "server" || classification === "transient"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function jitter(ms) {
|
|
20
|
+
return Math.round(ms * (1 + (Math.random() - 0.5) * 0.4))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function retryDelayMs(classification, baseDelayMs, attempt) {
|
|
24
|
+
if (classification === "rate_limit") {
|
|
25
|
+
return jitter(Math.min(baseDelayMs * Math.pow(3, attempt - 1), 60000))
|
|
26
|
+
}
|
|
27
|
+
return jitter(baseDelayMs * Math.pow(2, attempt - 1))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function requestWithRetry({ execute, attempts = 3, baseDelayMs = 800, signal = null }) {
|
|
31
|
+
let lastError = null
|
|
32
|
+
for (let attempt = 1; attempt <= Math.max(1, attempts); attempt++) {
|
|
33
|
+
if (signal?.aborted) {
|
|
34
|
+
const error = new Error("request aborted")
|
|
35
|
+
error.code = "ABORT_ERR"
|
|
36
|
+
throw error
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
return await execute(attempt)
|
|
40
|
+
} catch (error) {
|
|
41
|
+
lastError = error
|
|
42
|
+
const status = Number(error?.status || error?.httpStatus || 0)
|
|
43
|
+
let classification = classifyHttpError(status)
|
|
44
|
+
|
|
45
|
+
// HTTP 400 with context_length_exceeded in body → treat as context_overflow
|
|
46
|
+
if (classification === "bad_request" && /context_length_exceeded/i.test(error.message)) {
|
|
47
|
+
classification = "context_overflow"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
error.errorClass = classification
|
|
51
|
+
|
|
52
|
+
if (classification === "auth") {
|
|
53
|
+
error.message = `authentication failed (${status}): check your API key. ${error.message}`
|
|
54
|
+
throw error
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (classification === "context_overflow") {
|
|
58
|
+
error.needsCompaction = true
|
|
59
|
+
throw error
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const networkRetryable = error?.code === "ETIMEDOUT" || error?.code === "ECONNRESET" || error?.code === "ECONNREFUSED" || error?.code === "ENOTFOUND" || error?.code === "EHOSTUNREACH"
|
|
63
|
+
if ((!isRetryable(classification) && !networkRetryable) || attempt >= attempts) {
|
|
64
|
+
throw error
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const delay = networkRetryable
|
|
68
|
+
? jitter(baseDelayMs * Math.pow(2, attempt - 1))
|
|
69
|
+
: retryDelayMs(classification, baseDelayMs, attempt)
|
|
70
|
+
await sleep(delay)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
throw lastError || new Error("request failed")
|
|
74
|
+
}
|
package/src/provider/router.mjs
CHANGED
|
@@ -1,241 +1,242 @@
|
|
|
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
|
-
import { EventBus } from "../core/events.mjs"
|
|
7
|
-
import { EVENT_TYPES } from "../core/constants.mjs"
|
|
8
|
-
|
|
9
|
-
// --- Provider Registry ---
|
|
10
|
-
const registry = new Map()
|
|
11
|
-
|
|
12
|
-
export function registerProvider(name, mod) {
|
|
13
|
-
if (!mod || typeof mod.request !== "function" || typeof mod.requestStream !== "function") {
|
|
14
|
-
throw new Error(`provider "${name}" must export request() and requestStream()`)
|
|
15
|
-
}
|
|
16
|
-
registry.set(name, mod)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function listProviders() {
|
|
20
|
-
return [...registry.keys()]
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function getProvider(name) {
|
|
24
|
-
return registry.get(name) || null
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Built-in providers
|
|
28
|
-
registerProvider("openai", { request: requestOpenAI, requestStream: requestOpenAIStream, countTokens: countTokensOpenAI })
|
|
29
|
-
registerProvider("anthropic", { request: requestAnthropic, requestStream: requestAnthropicStream, countTokens: countTokensAnthropic })
|
|
30
|
-
registerProvider("openai-compatible", { request: requestOAICompat, requestStream: requestStreamOAICompat, countTokens: countTokensOpenAI })
|
|
31
|
-
registerProvider("ollama", { request: requestOllama, requestStream: requestOllamaStream })
|
|
32
|
-
|
|
33
|
-
// --- Settings Resolution ---
|
|
34
|
-
function resolveSettings(configState, providerType, overrides = {}) {
|
|
35
|
-
const llm = configState.config.provider
|
|
36
|
-
|
|
37
|
-
// Resolve registry key: direct match → config type field → fallback to openai
|
|
38
|
-
let resolvedType = providerType
|
|
39
|
-
if (!registry.has(providerType)) {
|
|
40
|
-
const providerConfig = llm[providerType]
|
|
41
|
-
if (providerConfig?.type && registry.has(providerConfig.type)) {
|
|
42
|
-
resolvedType = providerConfig.type
|
|
43
|
-
} else {
|
|
44
|
-
if (llm.strict_mode) {
|
|
45
|
-
throw new ProviderError(
|
|
46
|
-
`unknown provider "${providerType}". registered: ${listProviders().join(", ")}`,
|
|
47
|
-
{ provider: providerType, reason: "unknown_provider" }
|
|
48
|
-
)
|
|
49
|
-
}
|
|
50
|
-
console.warn(`[kkcode] unknown provider "${providerType}", falling back to openai`)
|
|
51
|
-
EventBus.emit({
|
|
52
|
-
type: EVENT_TYPES.PROVIDER_FALLBACK,
|
|
53
|
-
payload: { requested: providerType, resolved: "openai" }
|
|
54
|
-
}).catch(() => {})
|
|
55
|
-
resolvedType = "openai"
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Read config from original provider name (e.g. "deepseek"), not resolved type
|
|
60
|
-
const defaults = llm[providerType] || llm[resolvedType] || {}
|
|
61
|
-
const normalizedModel = String(overrides.model || defaults.default_model || "").includes("/")
|
|
62
|
-
? String(overrides.model || defaults.default_model).split("/").slice(1).join("/")
|
|
63
|
-
: String(overrides.model || defaults.default_model || "")
|
|
64
|
-
return {
|
|
65
|
-
providerType: resolvedType,
|
|
66
|
-
configKey: providerType,
|
|
67
|
-
model: normalizedModel,
|
|
68
|
-
baseUrl: overrides.baseUrl || defaults.base_url,
|
|
69
|
-
apiKeyEnv: overrides.apiKeyEnv || defaults.api_key_env
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (["
|
|
77
|
-
if (["
|
|
78
|
-
if (["
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (status ===
|
|
83
|
-
if (status
|
|
84
|
-
if (status >= 500) return "bad_response"
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
if (code === "
|
|
90
|
-
if (
|
|
91
|
-
return "
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
error.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
wrapped.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
yield { type: "
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
const
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
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
|
+
import { EventBus } from "../core/events.mjs"
|
|
7
|
+
import { EVENT_TYPES } from "../core/constants.mjs"
|
|
8
|
+
|
|
9
|
+
// --- Provider Registry ---
|
|
10
|
+
const registry = new Map()
|
|
11
|
+
|
|
12
|
+
export function registerProvider(name, mod) {
|
|
13
|
+
if (!mod || typeof mod.request !== "function" || typeof mod.requestStream !== "function") {
|
|
14
|
+
throw new Error(`provider "${name}" must export request() and requestStream()`)
|
|
15
|
+
}
|
|
16
|
+
registry.set(name, mod)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function listProviders() {
|
|
20
|
+
return [...registry.keys()]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getProvider(name) {
|
|
24
|
+
return registry.get(name) || null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Built-in providers
|
|
28
|
+
registerProvider("openai", { request: requestOpenAI, requestStream: requestOpenAIStream, countTokens: countTokensOpenAI })
|
|
29
|
+
registerProvider("anthropic", { request: requestAnthropic, requestStream: requestAnthropicStream, countTokens: countTokensAnthropic })
|
|
30
|
+
registerProvider("openai-compatible", { request: requestOAICompat, requestStream: requestStreamOAICompat, countTokens: countTokensOpenAI })
|
|
31
|
+
registerProvider("ollama", { request: requestOllama, requestStream: requestOllamaStream })
|
|
32
|
+
|
|
33
|
+
// --- Settings Resolution ---
|
|
34
|
+
function resolveSettings(configState, providerType, overrides = {}) {
|
|
35
|
+
const llm = configState.config.provider
|
|
36
|
+
|
|
37
|
+
// Resolve registry key: direct match → config type field → fallback to openai
|
|
38
|
+
let resolvedType = providerType
|
|
39
|
+
if (!registry.has(providerType)) {
|
|
40
|
+
const providerConfig = llm[providerType]
|
|
41
|
+
if (providerConfig?.type && registry.has(providerConfig.type)) {
|
|
42
|
+
resolvedType = providerConfig.type
|
|
43
|
+
} else {
|
|
44
|
+
if (llm.strict_mode) {
|
|
45
|
+
throw new ProviderError(
|
|
46
|
+
`unknown provider "${providerType}". registered: ${listProviders().join(", ")}`,
|
|
47
|
+
{ provider: providerType, reason: "unknown_provider" }
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
console.warn(`[kkcode] unknown provider "${providerType}", falling back to openai`)
|
|
51
|
+
EventBus.emit({
|
|
52
|
+
type: EVENT_TYPES.PROVIDER_FALLBACK,
|
|
53
|
+
payload: { requested: providerType, resolved: "openai" }
|
|
54
|
+
}).catch(() => {})
|
|
55
|
+
resolvedType = "openai"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Read config from original provider name (e.g. "deepseek"), not resolved type
|
|
60
|
+
const defaults = llm[providerType] || llm[resolvedType] || {}
|
|
61
|
+
const normalizedModel = String(overrides.model || defaults.default_model || "").includes("/")
|
|
62
|
+
? String(overrides.model || defaults.default_model).split("/").slice(1).join("/")
|
|
63
|
+
: String(overrides.model || defaults.default_model || "")
|
|
64
|
+
return {
|
|
65
|
+
providerType: resolvedType,
|
|
66
|
+
configKey: providerType,
|
|
67
|
+
model: normalizedModel,
|
|
68
|
+
baseUrl: overrides.baseUrl || defaults.base_url,
|
|
69
|
+
apiKeyEnv: overrides.apiKeyEnv || defaults.api_key_env,
|
|
70
|
+
apiKeyDirect: defaults.api_key || null
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function classifyProviderFailure(error) {
|
|
75
|
+
const cls = String(error?.errorClass || "").toLowerCase()
|
|
76
|
+
if (["auth", "authentication"].includes(cls)) return "auth"
|
|
77
|
+
if (["rate_limit"].includes(cls)) return "rate_limit"
|
|
78
|
+
if (["context_overflow", "bad_response"].includes(cls)) return "bad_response"
|
|
79
|
+
if (["server", "transient"].includes(cls)) return "bad_response"
|
|
80
|
+
|
|
81
|
+
const status = Number(error?.status || error?.httpStatus || 0)
|
|
82
|
+
if (status === 401 || status === 403) return "auth"
|
|
83
|
+
if (status === 429) return "rate_limit"
|
|
84
|
+
if (status >= 400 && status < 500) return "bad_response"
|
|
85
|
+
if (status >= 500) return "bad_response"
|
|
86
|
+
|
|
87
|
+
const code = String(error?.code || "").toUpperCase()
|
|
88
|
+
const msg = String(error?.message || "").toLowerCase()
|
|
89
|
+
if (code === "ABORT_ERR" || msg.includes("timeout") || msg.includes("timed out")) return "timeout"
|
|
90
|
+
if (code === "ETIMEDOUT" || code === "ECONNRESET") return "timeout"
|
|
91
|
+
if (msg.includes("invalid json") || msg.includes("parse")) return "bad_response"
|
|
92
|
+
return "unknown"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function normalizeProviderError(error, providerType, model) {
|
|
96
|
+
const reason = classifyProviderFailure(error)
|
|
97
|
+
if (error instanceof ProviderError) {
|
|
98
|
+
error.reason = error.reason || reason
|
|
99
|
+
error.details = {
|
|
100
|
+
...(error.details || {}),
|
|
101
|
+
provider: providerType,
|
|
102
|
+
model,
|
|
103
|
+
reason: error.reason
|
|
104
|
+
}
|
|
105
|
+
return error
|
|
106
|
+
}
|
|
107
|
+
const wrapped = new ProviderError(error?.message || "provider request failed", {
|
|
108
|
+
provider: providerType,
|
|
109
|
+
model,
|
|
110
|
+
reason
|
|
111
|
+
})
|
|
112
|
+
wrapped.reason = reason
|
|
113
|
+
wrapped.cause = error
|
|
114
|
+
return wrapped
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- Non-streaming Request ---
|
|
118
|
+
export async function requestProvider({
|
|
119
|
+
configState,
|
|
120
|
+
providerType,
|
|
121
|
+
model,
|
|
122
|
+
system,
|
|
123
|
+
messages,
|
|
124
|
+
tools,
|
|
125
|
+
baseUrl = null,
|
|
126
|
+
apiKeyEnv = null
|
|
127
|
+
}) {
|
|
128
|
+
const resolvedProviderType = providerType || configState.config.provider.default
|
|
129
|
+
const settings = resolveSettings(configState, resolvedProviderType, {
|
|
130
|
+
model,
|
|
131
|
+
baseUrl,
|
|
132
|
+
apiKeyEnv
|
|
133
|
+
})
|
|
134
|
+
const apiKey = settings.apiKeyDirect || process.env[settings.apiKeyEnv] || ""
|
|
135
|
+
const providerCfg = configState.config.provider[settings.configKey] || configState.config.provider[settings.providerType] || {}
|
|
136
|
+
|
|
137
|
+
const input = {
|
|
138
|
+
apiKey,
|
|
139
|
+
baseUrl: settings.baseUrl,
|
|
140
|
+
apiKeyEnv: settings.apiKeyEnv,
|
|
141
|
+
model: settings.model,
|
|
142
|
+
system,
|
|
143
|
+
messages,
|
|
144
|
+
tools,
|
|
145
|
+
timeoutMs: Number(providerCfg.timeout_ms || 120000),
|
|
146
|
+
maxTokens: Number(providerCfg.max_tokens || 16384),
|
|
147
|
+
retry: {
|
|
148
|
+
attempts: Number(providerCfg.retry_attempts || 3),
|
|
149
|
+
baseDelayMs: Number(providerCfg.retry_base_delay_ms || 800)
|
|
150
|
+
},
|
|
151
|
+
thinking: providerCfg.thinking || null
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const provider = registry.get(settings.providerType)
|
|
155
|
+
if (!provider) {
|
|
156
|
+
throw new Error(`unknown provider: ${settings.providerType}. registered: ${listProviders().join(", ")}`)
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
return await provider.request(input)
|
|
160
|
+
} catch (error) {
|
|
161
|
+
throw normalizeProviderError(error, settings.providerType, settings.model)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// --- Streaming Request ---
|
|
166
|
+
export async function* requestProviderStream({
|
|
167
|
+
configState,
|
|
168
|
+
providerType,
|
|
169
|
+
model,
|
|
170
|
+
system,
|
|
171
|
+
messages,
|
|
172
|
+
tools,
|
|
173
|
+
baseUrl = null,
|
|
174
|
+
apiKeyEnv = null,
|
|
175
|
+
signal = null,
|
|
176
|
+
compaction = null
|
|
177
|
+
}) {
|
|
178
|
+
const resolvedProviderType = providerType || configState.config.provider.default
|
|
179
|
+
const settings = resolveSettings(configState, resolvedProviderType, {
|
|
180
|
+
model,
|
|
181
|
+
baseUrl,
|
|
182
|
+
apiKeyEnv
|
|
183
|
+
})
|
|
184
|
+
const apiKey = settings.apiKeyDirect || process.env[settings.apiKeyEnv] || ""
|
|
185
|
+
const providerCfg = configState.config.provider[settings.configKey] || configState.config.provider[settings.providerType] || {}
|
|
186
|
+
|
|
187
|
+
if (providerCfg.stream === false) {
|
|
188
|
+
const result = await requestProvider({
|
|
189
|
+
configState, providerType, model, system, messages, tools, baseUrl, apiKeyEnv
|
|
190
|
+
})
|
|
191
|
+
if (result.text) yield { type: "text", content: result.text }
|
|
192
|
+
for (const call of result.toolCalls) yield { type: "tool_call", call }
|
|
193
|
+
yield { type: "usage", usage: result.usage }
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const input = {
|
|
198
|
+
apiKey,
|
|
199
|
+
baseUrl: settings.baseUrl,
|
|
200
|
+
apiKeyEnv: settings.apiKeyEnv,
|
|
201
|
+
model: settings.model,
|
|
202
|
+
system,
|
|
203
|
+
messages,
|
|
204
|
+
tools,
|
|
205
|
+
timeoutMs: Number(providerCfg.timeout_ms || 120000),
|
|
206
|
+
streamIdleTimeoutMs: Number(providerCfg.stream_idle_timeout_ms || 120000),
|
|
207
|
+
maxTokens: Number(providerCfg.max_tokens || 16384),
|
|
208
|
+
retry: {
|
|
209
|
+
attempts: Number(providerCfg.retry_attempts || 3),
|
|
210
|
+
baseDelayMs: Number(providerCfg.retry_base_delay_ms || 800)
|
|
211
|
+
},
|
|
212
|
+
thinking: providerCfg.thinking || null,
|
|
213
|
+
signal,
|
|
214
|
+
compaction
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const provider = registry.get(settings.providerType)
|
|
218
|
+
if (!provider) {
|
|
219
|
+
throw new Error(`unknown provider: ${settings.providerType}. registered: ${listProviders().join(", ")}`)
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
yield* provider.requestStream(input)
|
|
223
|
+
} catch (error) {
|
|
224
|
+
throw normalizeProviderError(error, settings.providerType, settings.model)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// --- Token Counting (Anthropic only, returns null for other providers) ---
|
|
229
|
+
export async function countTokensProvider({
|
|
230
|
+
configState, providerType, model, system, messages, tools,
|
|
231
|
+
baseUrl = null, apiKeyEnv = null
|
|
232
|
+
}) {
|
|
233
|
+
const resolvedProviderType = providerType || configState.config.provider.default
|
|
234
|
+
const settings = resolveSettings(configState, resolvedProviderType, { model, baseUrl, apiKeyEnv })
|
|
235
|
+
const provider = registry.get(settings.providerType)
|
|
236
|
+
if (!provider?.countTokens) return null
|
|
237
|
+
const apiKey = process.env[settings.apiKeyEnv] || ""
|
|
238
|
+
return provider.countTokens({
|
|
239
|
+
apiKey, baseUrl: settings.baseUrl, model: settings.model,
|
|
240
|
+
system, messages, tools
|
|
241
|
+
})
|
|
242
|
+
}
|