@kkelly-offical/kkcode 0.1.7 → 0.2.3-preview.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.
Files changed (166) hide show
  1. package/LICENSE +674 -674
  2. package/README.md +474 -387
  3. package/package.json +50 -46
  4. package/src/agent/agent.mjs +228 -220
  5. package/src/agent/custom-agent-loader.mjs +6 -3
  6. package/src/agent/generator.mjs +2 -2
  7. package/src/agent/prompt/assistant.txt +12 -0
  8. package/src/agent/prompt/bug-hunter.txt +89 -89
  9. package/src/agent/prompt/frontend-designer.txt +58 -58
  10. package/src/agent/prompt/guide.txt +1 -1
  11. package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
  12. package/src/agent/prompt/longagent-coding-agent.txt +37 -37
  13. package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
  14. package/src/agent/prompt/longagent-preview-agent.txt +63 -63
  15. package/src/command/custom-commands.mjs +2 -2
  16. package/src/commands/agent.mjs +1 -1
  17. package/src/commands/background.mjs +145 -4
  18. package/src/commands/chat.mjs +117 -76
  19. package/src/commands/config.mjs +148 -1
  20. package/src/commands/doctor.mjs +30 -6
  21. package/src/commands/init.mjs +32 -6
  22. package/src/commands/longagent.mjs +117 -0
  23. package/src/commands/mcp.mjs +275 -43
  24. package/src/commands/permission.mjs +1 -1
  25. package/src/commands/session.mjs +195 -140
  26. package/src/commands/skill.mjs +63 -0
  27. package/src/commands/theme.mjs +1 -1
  28. package/src/commands/update.mjs +32 -0
  29. package/src/config/defaults.mjs +289 -260
  30. package/src/config/import-config.mjs +1 -1
  31. package/src/config/load-config.mjs +61 -4
  32. package/src/config/schema.mjs +604 -574
  33. package/src/context.mjs +4 -1
  34. package/src/core/constants.mjs +97 -91
  35. package/src/core/types.mjs +1 -1
  36. package/src/github/api.mjs +78 -78
  37. package/src/github/auth.mjs +294 -286
  38. package/src/github/flow.mjs +298 -298
  39. package/src/github/workspace.mjs +225 -212
  40. package/src/index.mjs +87 -82
  41. package/src/knowledge/frontend-aesthetics.txt +38 -38
  42. package/src/mcp/client-http.mjs +139 -141
  43. package/src/mcp/client-sse.mjs +297 -288
  44. package/src/mcp/client-stdio.mjs +534 -533
  45. package/src/mcp/constants.mjs +4 -2
  46. package/src/mcp/registry.mjs +498 -479
  47. package/src/mcp/stdio-framing.mjs +135 -133
  48. package/src/mcp/tool-result.mjs +24 -24
  49. package/src/observability/edit-diagnostics.mjs +449 -0
  50. package/src/observability/index.mjs +42 -42
  51. package/src/observability/metrics.mjs +165 -137
  52. package/src/observability/tracer.mjs +137 -137
  53. package/src/onboarding.mjs +209 -0
  54. package/src/orchestration/background-manager.mjs +567 -372
  55. package/src/orchestration/background-worker.mjs +419 -305
  56. package/src/orchestration/interruption-reason.mjs +21 -0
  57. package/src/orchestration/longagent-manager.mjs +197 -171
  58. package/src/orchestration/stage-scheduler.mjs +733 -728
  59. package/src/orchestration/subagent-router.mjs +7 -1
  60. package/src/orchestration/task-scheduler.mjs +219 -7
  61. package/src/permission/engine.mjs +1 -1
  62. package/src/permission/exec-policy.mjs +370 -370
  63. package/src/permission/file-edit-policy.mjs +108 -0
  64. package/src/permission/prompt.mjs +1 -1
  65. package/src/permission/rules.mjs +116 -7
  66. package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
  67. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
  68. package/src/plugin/hook-bus.mjs +19 -5
  69. package/src/plugin/manifest-loader.mjs +222 -0
  70. package/src/provider/anthropic.mjs +396 -390
  71. package/src/provider/ollama.mjs +7 -1
  72. package/src/provider/openai.mjs +382 -340
  73. package/src/provider/retry-policy.mjs +74 -68
  74. package/src/provider/router.mjs +242 -241
  75. package/src/provider/sse.mjs +104 -104
  76. package/src/provider/wizard.mjs +556 -0
  77. package/src/repl/capability-facade.mjs +30 -0
  78. package/src/repl/command-surface.mjs +23 -0
  79. package/src/repl/controller-entry.mjs +40 -0
  80. package/src/repl/core-shell.mjs +208 -0
  81. package/src/repl/dialog-router.mjs +87 -0
  82. package/src/repl/input-engine.mjs +76 -0
  83. package/src/repl/keymap.mjs +7 -0
  84. package/src/repl/operator-surface.mjs +15 -0
  85. package/src/repl/permission-flow.mjs +49 -0
  86. package/src/repl/runtime-facade.mjs +36 -0
  87. package/src/repl/slash-router.mjs +62 -0
  88. package/src/repl/state-store.mjs +29 -0
  89. package/src/repl/turn-controller.mjs +58 -0
  90. package/src/repl/verification.mjs +23 -0
  91. package/src/repl.mjs +3371 -2981
  92. package/src/rules/load-rules.mjs +3 -3
  93. package/src/runtime.mjs +1 -1
  94. package/src/session/agent-transaction.mjs +86 -0
  95. package/src/session/checkpoint.mjs +302 -302
  96. package/src/session/compaction.mjs +298 -298
  97. package/src/session/engine.mjs +417 -232
  98. package/src/session/longagent-4stage.mjs +467 -460
  99. package/src/session/longagent-hybrid.mjs +1344 -1097
  100. package/src/session/longagent-plan.mjs +376 -365
  101. package/src/session/longagent-project-memory.mjs +53 -53
  102. package/src/session/longagent-scaffold.mjs +291 -291
  103. package/src/session/longagent-task-bus.mjs +138 -54
  104. package/src/session/longagent-utils.mjs +828 -472
  105. package/src/session/longagent.mjs +911 -900
  106. package/src/session/loop.mjs +1005 -930
  107. package/src/session/prompt/agent.txt +25 -25
  108. package/src/session/prompt/anthropic.txt +150 -150
  109. package/src/session/prompt/beast.txt +1 -1
  110. package/src/session/prompt/plan.txt +31 -31
  111. package/src/session/prompt/qwen.txt +46 -46
  112. package/src/session/recovery.mjs +21 -0
  113. package/src/session/rollback.mjs +196 -195
  114. package/src/session/routing-observability.mjs +72 -0
  115. package/src/session/runtime-state.mjs +47 -0
  116. package/src/session/store.mjs +523 -519
  117. package/src/session/system-prompt.mjs +308 -273
  118. package/src/session/task-validator.mjs +267 -267
  119. package/src/session/usability-gates.mjs +2 -2
  120. package/src/skill/builtin/commit.mjs +64 -64
  121. package/src/skill/builtin/design.mjs +76 -76
  122. package/src/skill/generator.mjs +18 -2
  123. package/src/skill/registry.mjs +642 -390
  124. package/src/storage/audit-store.mjs +18 -11
  125. package/src/storage/event-log.mjs +7 -1
  126. package/src/storage/ghost-commit-store.mjs +243 -245
  127. package/src/storage/paths.mjs +17 -0
  128. package/src/theme/default-theme.mjs +1 -1
  129. package/src/theme/markdown.mjs +4 -0
  130. package/src/theme/schema.mjs +1 -1
  131. package/src/theme/status-bar.mjs +162 -158
  132. package/src/tool/audit-wrapper.mjs +18 -2
  133. package/src/tool/edit-transaction.mjs +23 -0
  134. package/src/tool/executor.mjs +26 -1
  135. package/src/tool/file-read-state.mjs +65 -0
  136. package/src/tool/git-auto.mjs +526 -526
  137. package/src/tool/git-full-auto.mjs +487 -478
  138. package/src/tool/mutation-guard.mjs +54 -0
  139. package/src/tool/prompt/edit.txt +3 -3
  140. package/src/tool/prompt/multiedit.txt +1 -0
  141. package/src/tool/prompt/notebookedit.txt +2 -1
  142. package/src/tool/prompt/patch.txt +25 -24
  143. package/src/tool/prompt/read.txt +3 -3
  144. package/src/tool/prompt/sysinfo.txt +29 -0
  145. package/src/tool/prompt/task.txt +66 -4
  146. package/src/tool/prompt/write.txt +2 -2
  147. package/src/tool/question-prompt.mjs +99 -93
  148. package/src/tool/registry.mjs +1701 -1343
  149. package/src/tool/task-tool.mjs +14 -6
  150. package/src/ui/activity-renderer.mjs +667 -664
  151. package/src/ui/repl-background-panel.mjs +7 -0
  152. package/src/ui/repl-capability-panel.mjs +9 -0
  153. package/src/ui/repl-dashboard.mjs +54 -4
  154. package/src/ui/repl-help.mjs +110 -0
  155. package/src/ui/repl-operator-panel.mjs +12 -0
  156. package/src/ui/repl-route-feedback.mjs +35 -0
  157. package/src/ui/repl-status-view.mjs +76 -0
  158. package/src/ui/repl-task-panel.mjs +5 -0
  159. package/src/ui/repl-transcript-panel.mjs +56 -0
  160. package/src/ui/repl-turn-summary.mjs +135 -0
  161. package/src/update/checker.mjs +184 -0
  162. package/src/usage/pricing.mjs +122 -121
  163. package/src/usage/usage-meter.mjs +1 -0
  164. package/src/util/git.mjs +562 -519
  165. package/src/util/template.mjs +6 -1
  166. package/src/version.mjs +3 -0
@@ -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 || 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" || error?.code === "ECONNREFUSED" || error?.code === "ENOTFOUND" || error?.code === "EHOSTUNREACH"
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
- }
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
+ }
@@ -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
- function classifyProviderFailure(error) {
74
- const cls = String(error?.errorClass || "").toLowerCase()
75
- if (["auth", "authentication"].includes(cls)) return "auth"
76
- if (["rate_limit"].includes(cls)) return "rate_limit"
77
- if (["context_overflow", "bad_response"].includes(cls)) return "bad_response"
78
- if (["server", "transient"].includes(cls)) return "bad_response"
79
-
80
- const status = Number(error?.status || error?.httpStatus || 0)
81
- if (status === 401 || status === 403) return "auth"
82
- if (status === 429) return "rate_limit"
83
- if (status >= 400 && status < 500) return "bad_response"
84
- if (status >= 500) return "bad_response"
85
-
86
- const code = String(error?.code || "").toUpperCase()
87
- const msg = String(error?.message || "").toLowerCase()
88
- if (code === "ABORT_ERR" || msg.includes("timeout") || msg.includes("timed out")) return "timeout"
89
- if (code === "ETIMEDOUT" || code === "ECONNRESET") return "timeout"
90
- if (msg.includes("invalid json") || msg.includes("parse")) return "bad_response"
91
- return "unknown"
92
- }
93
-
94
- function normalizeProviderError(error, providerType, model) {
95
- const reason = classifyProviderFailure(error)
96
- if (error instanceof ProviderError) {
97
- error.reason = error.reason || reason
98
- error.details = {
99
- ...(error.details || {}),
100
- provider: providerType,
101
- model,
102
- reason: error.reason
103
- }
104
- return error
105
- }
106
- const wrapped = new ProviderError(error?.message || "provider request failed", {
107
- provider: providerType,
108
- model,
109
- reason
110
- })
111
- wrapped.reason = reason
112
- wrapped.cause = error
113
- return wrapped
114
- }
115
-
116
- // --- Non-streaming Request ---
117
- export async function requestProvider({
118
- configState,
119
- providerType,
120
- model,
121
- system,
122
- messages,
123
- tools,
124
- baseUrl = null,
125
- apiKeyEnv = null
126
- }) {
127
- const resolvedProviderType = providerType || configState.config.provider.default
128
- const settings = resolveSettings(configState, resolvedProviderType, {
129
- model,
130
- baseUrl,
131
- apiKeyEnv
132
- })
133
- const apiKey = process.env[settings.apiKeyEnv] || ""
134
- const providerCfg = configState.config.provider[settings.configKey] || configState.config.provider[settings.providerType] || {}
135
-
136
- const input = {
137
- apiKey,
138
- baseUrl: settings.baseUrl,
139
- apiKeyEnv: settings.apiKeyEnv,
140
- model: settings.model,
141
- system,
142
- messages,
143
- tools,
144
- timeoutMs: Number(providerCfg.timeout_ms || 120000),
145
- maxTokens: Number(providerCfg.max_tokens || 16384),
146
- retry: {
147
- attempts: Number(providerCfg.retry_attempts || 3),
148
- baseDelayMs: Number(providerCfg.retry_base_delay_ms || 800)
149
- },
150
- thinking: providerCfg.thinking || null
151
- }
152
-
153
- const provider = registry.get(settings.providerType)
154
- if (!provider) {
155
- throw new Error(`unknown provider: ${settings.providerType}. registered: ${listProviders().join(", ")}`)
156
- }
157
- try {
158
- return await provider.request(input)
159
- } catch (error) {
160
- throw normalizeProviderError(error, settings.providerType, settings.model)
161
- }
162
- }
163
-
164
- // --- Streaming Request ---
165
- export async function* requestProviderStream({
166
- configState,
167
- providerType,
168
- model,
169
- system,
170
- messages,
171
- tools,
172
- baseUrl = null,
173
- apiKeyEnv = null,
174
- signal = null,
175
- compaction = null
176
- }) {
177
- const resolvedProviderType = providerType || configState.config.provider.default
178
- const settings = resolveSettings(configState, resolvedProviderType, {
179
- model,
180
- baseUrl,
181
- apiKeyEnv
182
- })
183
- const apiKey = process.env[settings.apiKeyEnv] || ""
184
- const providerCfg = configState.config.provider[settings.configKey] || configState.config.provider[settings.providerType] || {}
185
-
186
- if (providerCfg.stream === false) {
187
- const result = await requestProvider({
188
- configState, providerType, model, system, messages, tools, baseUrl, apiKeyEnv
189
- })
190
- if (result.text) yield { type: "text", content: result.text }
191
- for (const call of result.toolCalls) yield { type: "tool_call", call }
192
- yield { type: "usage", usage: result.usage }
193
- return
194
- }
195
-
196
- const input = {
197
- apiKey,
198
- baseUrl: settings.baseUrl,
199
- apiKeyEnv: settings.apiKeyEnv,
200
- model: settings.model,
201
- system,
202
- messages,
203
- tools,
204
- timeoutMs: Number(providerCfg.timeout_ms || 120000),
205
- streamIdleTimeoutMs: Number(providerCfg.stream_idle_timeout_ms || 120000),
206
- maxTokens: Number(providerCfg.max_tokens || 16384),
207
- retry: {
208
- attempts: Number(providerCfg.retry_attempts || 3),
209
- baseDelayMs: Number(providerCfg.retry_base_delay_ms || 800)
210
- },
211
- thinking: providerCfg.thinking || null,
212
- signal,
213
- compaction
214
- }
215
-
216
- const provider = registry.get(settings.providerType)
217
- if (!provider) {
218
- throw new Error(`unknown provider: ${settings.providerType}. registered: ${listProviders().join(", ")}`)
219
- }
220
- try {
221
- yield* provider.requestStream(input)
222
- } catch (error) {
223
- throw normalizeProviderError(error, settings.providerType, settings.model)
224
- }
225
- }
226
-
227
- // --- Token Counting (Anthropic only, returns null for other providers) ---
228
- export async function countTokensProvider({
229
- configState, providerType, model, system, messages, tools,
230
- baseUrl = null, apiKeyEnv = null
231
- }) {
232
- const resolvedProviderType = providerType || configState.config.provider.default
233
- const settings = resolveSettings(configState, resolvedProviderType, { model, baseUrl, apiKeyEnv })
234
- const provider = registry.get(settings.providerType)
235
- if (!provider?.countTokens) return null
236
- const apiKey = process.env[settings.apiKeyEnv] || ""
237
- return provider.countTokens({
238
- apiKey, baseUrl: settings.baseUrl, model: settings.model,
239
- system, messages, tools
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
+ }