@kkelly-offical/kkcode 0.1.7 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/LICENSE +674 -674
  2. package/README.md +452 -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/config/defaults.mjs +280 -260
  29. package/src/config/import-config.mjs +1 -1
  30. package/src/config/load-config.mjs +61 -4
  31. package/src/config/schema.mjs +591 -574
  32. package/src/context.mjs +4 -1
  33. package/src/core/constants.mjs +97 -91
  34. package/src/core/types.mjs +1 -1
  35. package/src/github/api.mjs +78 -78
  36. package/src/github/auth.mjs +294 -286
  37. package/src/github/flow.mjs +298 -298
  38. package/src/github/workspace.mjs +225 -212
  39. package/src/index.mjs +84 -82
  40. package/src/knowledge/frontend-aesthetics.txt +38 -38
  41. package/src/mcp/client-http.mjs +139 -141
  42. package/src/mcp/client-sse.mjs +297 -288
  43. package/src/mcp/client-stdio.mjs +534 -533
  44. package/src/mcp/constants.mjs +2 -2
  45. package/src/mcp/registry.mjs +498 -479
  46. package/src/mcp/stdio-framing.mjs +135 -133
  47. package/src/mcp/tool-result.mjs +24 -24
  48. package/src/observability/edit-diagnostics.mjs +449 -0
  49. package/src/observability/index.mjs +42 -42
  50. package/src/observability/metrics.mjs +165 -137
  51. package/src/observability/tracer.mjs +137 -137
  52. package/src/onboarding.mjs +209 -0
  53. package/src/orchestration/background-manager.mjs +567 -372
  54. package/src/orchestration/background-worker.mjs +419 -305
  55. package/src/orchestration/interruption-reason.mjs +21 -0
  56. package/src/orchestration/longagent-manager.mjs +197 -171
  57. package/src/orchestration/stage-scheduler.mjs +733 -728
  58. package/src/orchestration/subagent-router.mjs +7 -1
  59. package/src/orchestration/task-scheduler.mjs +219 -7
  60. package/src/permission/engine.mjs +1 -1
  61. package/src/permission/exec-policy.mjs +370 -370
  62. package/src/permission/file-edit-policy.mjs +108 -0
  63. package/src/permission/prompt.mjs +1 -1
  64. package/src/permission/rules.mjs +116 -7
  65. package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
  66. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
  67. package/src/plugin/hook-bus.mjs +19 -5
  68. package/src/plugin/manifest-loader.mjs +222 -0
  69. package/src/provider/anthropic.mjs +396 -390
  70. package/src/provider/ollama.mjs +7 -1
  71. package/src/provider/openai.mjs +382 -340
  72. package/src/provider/retry-policy.mjs +74 -68
  73. package/src/provider/router.mjs +242 -241
  74. package/src/provider/sse.mjs +104 -104
  75. package/src/provider/wizard.mjs +556 -0
  76. package/src/repl/capability-facade.mjs +30 -0
  77. package/src/repl/command-surface.mjs +23 -0
  78. package/src/repl/controller-entry.mjs +40 -0
  79. package/src/repl/core-shell.mjs +208 -0
  80. package/src/repl/dialog-router.mjs +87 -0
  81. package/src/repl/input-engine.mjs +76 -0
  82. package/src/repl/keymap.mjs +7 -0
  83. package/src/repl/operator-surface.mjs +15 -0
  84. package/src/repl/permission-flow.mjs +49 -0
  85. package/src/repl/runtime-facade.mjs +36 -0
  86. package/src/repl/slash-router.mjs +62 -0
  87. package/src/repl/state-store.mjs +29 -0
  88. package/src/repl/turn-controller.mjs +58 -0
  89. package/src/repl/verification.mjs +23 -0
  90. package/src/repl.mjs +3368 -2981
  91. package/src/rules/load-rules.mjs +3 -3
  92. package/src/runtime.mjs +1 -1
  93. package/src/session/agent-transaction.mjs +86 -0
  94. package/src/session/checkpoint.mjs +302 -302
  95. package/src/session/compaction.mjs +298 -298
  96. package/src/session/engine.mjs +417 -232
  97. package/src/session/longagent-4stage.mjs +467 -460
  98. package/src/session/longagent-hybrid.mjs +1344 -1097
  99. package/src/session/longagent-plan.mjs +376 -365
  100. package/src/session/longagent-project-memory.mjs +53 -53
  101. package/src/session/longagent-scaffold.mjs +291 -291
  102. package/src/session/longagent-task-bus.mjs +138 -54
  103. package/src/session/longagent-utils.mjs +828 -472
  104. package/src/session/longagent.mjs +911 -900
  105. package/src/session/loop.mjs +1005 -930
  106. package/src/session/prompt/agent.txt +25 -25
  107. package/src/session/prompt/anthropic.txt +150 -150
  108. package/src/session/prompt/beast.txt +1 -1
  109. package/src/session/prompt/plan.txt +31 -31
  110. package/src/session/prompt/qwen.txt +46 -46
  111. package/src/session/recovery.mjs +21 -0
  112. package/src/session/rollback.mjs +196 -195
  113. package/src/session/routing-observability.mjs +72 -0
  114. package/src/session/runtime-state.mjs +47 -0
  115. package/src/session/store.mjs +523 -519
  116. package/src/session/system-prompt.mjs +308 -273
  117. package/src/session/task-validator.mjs +267 -267
  118. package/src/session/usability-gates.mjs +2 -2
  119. package/src/skill/builtin/commit.mjs +64 -64
  120. package/src/skill/builtin/design.mjs +76 -76
  121. package/src/skill/generator.mjs +18 -2
  122. package/src/skill/registry.mjs +642 -390
  123. package/src/storage/audit-store.mjs +18 -11
  124. package/src/storage/event-log.mjs +7 -1
  125. package/src/storage/ghost-commit-store.mjs +243 -245
  126. package/src/storage/paths.mjs +13 -0
  127. package/src/theme/default-theme.mjs +1 -1
  128. package/src/theme/markdown.mjs +4 -0
  129. package/src/theme/schema.mjs +1 -1
  130. package/src/theme/status-bar.mjs +162 -158
  131. package/src/tool/audit-wrapper.mjs +18 -2
  132. package/src/tool/edit-transaction.mjs +23 -0
  133. package/src/tool/executor.mjs +26 -1
  134. package/src/tool/file-read-state.mjs +65 -0
  135. package/src/tool/git-auto.mjs +526 -526
  136. package/src/tool/git-full-auto.mjs +487 -478
  137. package/src/tool/mutation-guard.mjs +54 -0
  138. package/src/tool/prompt/edit.txt +3 -3
  139. package/src/tool/prompt/multiedit.txt +1 -0
  140. package/src/tool/prompt/notebookedit.txt +2 -1
  141. package/src/tool/prompt/patch.txt +25 -24
  142. package/src/tool/prompt/read.txt +3 -3
  143. package/src/tool/prompt/sysinfo.txt +29 -0
  144. package/src/tool/prompt/task.txt +66 -4
  145. package/src/tool/prompt/write.txt +2 -2
  146. package/src/tool/question-prompt.mjs +99 -93
  147. package/src/tool/registry.mjs +1701 -1343
  148. package/src/tool/task-tool.mjs +14 -6
  149. package/src/ui/activity-renderer.mjs +667 -664
  150. package/src/ui/repl-background-panel.mjs +7 -0
  151. package/src/ui/repl-capability-panel.mjs +9 -0
  152. package/src/ui/repl-dashboard.mjs +54 -4
  153. package/src/ui/repl-help.mjs +110 -0
  154. package/src/ui/repl-operator-panel.mjs +12 -0
  155. package/src/ui/repl-route-feedback.mjs +35 -0
  156. package/src/ui/repl-status-view.mjs +76 -0
  157. package/src/ui/repl-task-panel.mjs +5 -0
  158. package/src/ui/repl-transcript-panel.mjs +56 -0
  159. package/src/ui/repl-turn-summary.mjs +135 -0
  160. package/src/usage/pricing.mjs +122 -121
  161. package/src/usage/usage-meter.mjs +1 -0
  162. package/src/util/git.mjs +562 -519
  163. 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 || 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
+ }