@kkelly-offical/kkcode 0.1.3 → 0.1.6

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 (58) hide show
  1. package/README.md +110 -172
  2. package/package.json +46 -46
  3. package/src/agent/agent.mjs +41 -0
  4. package/src/agent/prompt/frontend-designer.txt +58 -0
  5. package/src/agent/prompt/longagent-blueprint-agent.txt +83 -0
  6. package/src/agent/prompt/longagent-coding-agent.txt +37 -0
  7. package/src/agent/prompt/longagent-debugging-agent.txt +46 -0
  8. package/src/agent/prompt/longagent-preview-agent.txt +63 -0
  9. package/src/config/defaults.mjs +260 -195
  10. package/src/config/schema.mjs +71 -6
  11. package/src/core/constants.mjs +91 -46
  12. package/src/index.mjs +1 -1
  13. package/src/knowledge/frontend-aesthetics.txt +39 -0
  14. package/src/knowledge/loader.mjs +2 -1
  15. package/src/knowledge/tailwind.txt +12 -3
  16. package/src/mcp/client-http.mjs +141 -157
  17. package/src/mcp/client-sse.mjs +288 -286
  18. package/src/mcp/client-stdio.mjs +533 -451
  19. package/src/mcp/constants.mjs +2 -0
  20. package/src/mcp/registry.mjs +479 -394
  21. package/src/mcp/stdio-framing.mjs +133 -127
  22. package/src/mcp/tool-result.mjs +24 -0
  23. package/src/observability/index.mjs +42 -0
  24. package/src/observability/metrics.mjs +137 -0
  25. package/src/observability/tracer.mjs +137 -0
  26. package/src/orchestration/background-manager.mjs +372 -358
  27. package/src/orchestration/background-worker.mjs +305 -245
  28. package/src/orchestration/longagent-manager.mjs +171 -116
  29. package/src/orchestration/stage-scheduler.mjs +728 -489
  30. package/src/permission/exec-policy.mjs +9 -11
  31. package/src/provider/anthropic.mjs +1 -0
  32. package/src/provider/openai.mjs +340 -339
  33. package/src/provider/retry-policy.mjs +68 -68
  34. package/src/provider/router.mjs +241 -228
  35. package/src/provider/sse.mjs +104 -91
  36. package/src/repl.mjs +1 -1
  37. package/src/session/checkpoint.mjs +66 -3
  38. package/src/session/engine.mjs +227 -225
  39. package/src/session/longagent-4stage.mjs +460 -0
  40. package/src/session/longagent-hybrid.mjs +1081 -0
  41. package/src/session/longagent-plan.mjs +365 -329
  42. package/src/session/longagent-project-memory.mjs +53 -0
  43. package/src/session/longagent-scaffold.mjs +291 -100
  44. package/src/session/longagent-task-bus.mjs +54 -0
  45. package/src/session/longagent-utils.mjs +472 -0
  46. package/src/session/longagent.mjs +884 -1462
  47. package/src/session/project-context.mjs +30 -0
  48. package/src/session/store.mjs +510 -503
  49. package/src/session/task-validator.mjs +4 -3
  50. package/src/skill/builtin/design.mjs +76 -0
  51. package/src/skill/builtin/frontend.mjs +8 -0
  52. package/src/skill/registry.mjs +390 -336
  53. package/src/storage/ghost-commit-store.mjs +18 -8
  54. package/src/tool/executor.mjs +11 -0
  55. package/src/tool/git-auto.mjs +0 -19
  56. package/src/tool/registry.mjs +71 -37
  57. package/src/ui/activity-renderer.mjs +664 -410
  58. package/src/util/git.mjs +23 -0
@@ -1,68 +1,68 @@
1
- function sleep(ms) {
2
- return new Promise((resolve) => setTimeout(resolve, ms))
3
- }
4
-
5
- export function classifyHttpError(status) {
6
- if (status === 401 || status === 403) return "auth"
7
- if (status === 429) return "rate_limit"
8
- if (status === 413 || status === 400) return "context_overflow"
9
- if (status >= 500) return "server"
10
- if (status === 408 || status === 409 || status === 425) return "transient"
11
- return "unknown"
12
- }
13
-
14
- function isRetryable(classification) {
15
- return classification === "rate_limit" || classification === "server" || classification === "transient"
16
- }
17
-
18
- function jitter(ms) {
19
- return Math.round(ms * (1 + (Math.random() - 0.5) * 0.4))
20
- }
21
-
22
- function retryDelayMs(classification, baseDelayMs, attempt) {
23
- if (classification === "rate_limit") {
24
- return jitter(Math.min(baseDelayMs * Math.pow(3, attempt - 1), 60000))
25
- }
26
- return jitter(baseDelayMs * Math.pow(2, attempt - 1))
27
- }
28
-
29
- export async function requestWithRetry({ execute, attempts = 3, baseDelayMs = 800, signal = null }) {
30
- let lastError = null
31
- for (let attempt = 1; attempt <= Math.max(1, attempts); attempt++) {
32
- if (signal?.aborted) {
33
- const error = new Error("request aborted")
34
- error.code = "ABORT_ERR"
35
- throw error
36
- }
37
- try {
38
- return await execute(attempt)
39
- } catch (error) {
40
- lastError = error
41
- const status = Number(error?.status || error?.httpStatus || 0)
42
- const classification = classifyHttpError(status)
43
-
44
- error.errorClass = classification
45
-
46
- if (classification === "auth") {
47
- error.message = `authentication failed (${status}): check your API key. ${error.message}`
48
- throw error
49
- }
50
-
51
- if (classification === "context_overflow") {
52
- error.needsCompaction = true
53
- throw error
54
- }
55
-
56
- const networkRetryable = error?.code === "ETIMEDOUT" || error?.code === "ECONNRESET"
57
- if ((!isRetryable(classification) && !networkRetryable) || attempt >= attempts) {
58
- throw error
59
- }
60
-
61
- const delay = networkRetryable
62
- ? jitter(baseDelayMs * Math.pow(2, attempt - 1))
63
- : retryDelayMs(classification, baseDelayMs, attempt)
64
- await sleep(delay)
65
- }
66
- }
67
- throw lastError || new Error("request failed")
68
- }
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,228 +1,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
-
7
- // --- Provider Registry ---
8
- const registry = new Map()
9
-
10
- export function registerProvider(name, mod) {
11
- if (!mod || typeof mod.request !== "function" || typeof mod.requestStream !== "function") {
12
- throw new Error(`provider "${name}" must export request() and requestStream()`)
13
- }
14
- registry.set(name, mod)
15
- }
16
-
17
- export function listProviders() {
18
- return [...registry.keys()]
19
- }
20
-
21
- export function getProvider(name) {
22
- return registry.get(name) || null
23
- }
24
-
25
- // Built-in providers
26
- registerProvider("openai", { request: requestOpenAI, requestStream: requestOpenAIStream, countTokens: countTokensOpenAI })
27
- registerProvider("anthropic", { request: requestAnthropic, requestStream: requestAnthropicStream, countTokens: countTokensAnthropic })
28
- registerProvider("openai-compatible", { request: requestOAICompat, requestStream: requestStreamOAICompat, countTokens: countTokensOpenAI })
29
- registerProvider("ollama", { request: requestOllama, requestStream: requestOllamaStream })
30
-
31
- // --- Settings Resolution ---
32
- function resolveSettings(configState, providerType, overrides = {}) {
33
- const llm = configState.config.provider
34
-
35
- // Resolve registry key: direct match → config type field → fallback to openai
36
- let resolvedType = providerType
37
- if (!registry.has(providerType)) {
38
- const providerConfig = llm[providerType]
39
- if (providerConfig?.type && registry.has(providerConfig.type)) {
40
- resolvedType = providerConfig.type
41
- } else {
42
- resolvedType = "openai"
43
- }
44
- }
45
-
46
- // Read config from original provider name (e.g. "deepseek"), not resolved type
47
- const defaults = llm[providerType] || llm[resolvedType] || {}
48
- const normalizedModel = String(overrides.model || defaults.default_model || "").includes("/")
49
- ? String(overrides.model || defaults.default_model).split("/").slice(1).join("/")
50
- : String(overrides.model || defaults.default_model || "")
51
- return {
52
- providerType: resolvedType,
53
- configKey: providerType,
54
- model: normalizedModel,
55
- baseUrl: overrides.baseUrl || defaults.base_url,
56
- apiKeyEnv: overrides.apiKeyEnv || defaults.api_key_env
57
- }
58
- }
59
-
60
- function classifyProviderFailure(error) {
61
- const cls = String(error?.errorClass || "").toLowerCase()
62
- if (["auth", "authentication"].includes(cls)) return "auth"
63
- if (["rate_limit"].includes(cls)) return "rate_limit"
64
- if (["context_overflow", "bad_response"].includes(cls)) return "bad_response"
65
- if (["server", "transient"].includes(cls)) return "bad_response"
66
-
67
- const status = Number(error?.status || error?.httpStatus || 0)
68
- if (status === 401 || status === 403) return "auth"
69
- if (status === 429) return "rate_limit"
70
- if (status >= 400 && status < 500) return "bad_response"
71
- if (status >= 500) return "bad_response"
72
-
73
- const code = String(error?.code || "").toUpperCase()
74
- const msg = String(error?.message || "").toLowerCase()
75
- if (code === "ABORT_ERR" || msg.includes("timeout") || msg.includes("timed out")) return "timeout"
76
- if (code === "ETIMEDOUT" || code === "ECONNRESET") return "timeout"
77
- if (msg.includes("invalid json") || msg.includes("parse")) return "bad_response"
78
- return "unknown"
79
- }
80
-
81
- function normalizeProviderError(error, providerType, model) {
82
- const reason = classifyProviderFailure(error)
83
- if (error instanceof ProviderError) {
84
- error.reason = error.reason || reason
85
- error.details = {
86
- ...(error.details || {}),
87
- provider: providerType,
88
- model,
89
- reason: error.reason
90
- }
91
- return error
92
- }
93
- const wrapped = new ProviderError(error?.message || "provider request failed", {
94
- provider: providerType,
95
- model,
96
- reason
97
- })
98
- wrapped.reason = reason
99
- wrapped.cause = error
100
- return wrapped
101
- }
102
-
103
- // --- Non-streaming Request ---
104
- export async function requestProvider({
105
- configState,
106
- providerType,
107
- model,
108
- system,
109
- messages,
110
- tools,
111
- baseUrl = null,
112
- apiKeyEnv = null
113
- }) {
114
- const resolvedProviderType = providerType || configState.config.provider.default
115
- const settings = resolveSettings(configState, resolvedProviderType, {
116
- model,
117
- baseUrl,
118
- apiKeyEnv
119
- })
120
- const apiKey = process.env[settings.apiKeyEnv] || ""
121
- const providerCfg = configState.config.provider[settings.configKey] || configState.config.provider[settings.providerType] || {}
122
-
123
- const input = {
124
- apiKey,
125
- baseUrl: settings.baseUrl,
126
- apiKeyEnv: settings.apiKeyEnv,
127
- model: settings.model,
128
- system,
129
- messages,
130
- tools,
131
- timeoutMs: Number(providerCfg.timeout_ms || 120000),
132
- maxTokens: Number(providerCfg.max_tokens || 16384),
133
- retry: {
134
- attempts: Number(providerCfg.retry_attempts || 3),
135
- baseDelayMs: Number(providerCfg.retry_base_delay_ms || 800)
136
- },
137
- thinking: providerCfg.thinking || null
138
- }
139
-
140
- const provider = registry.get(settings.providerType)
141
- if (!provider) {
142
- throw new Error(`unknown provider: ${settings.providerType}. registered: ${listProviders().join(", ")}`)
143
- }
144
- try {
145
- return await provider.request(input)
146
- } catch (error) {
147
- throw normalizeProviderError(error, settings.providerType, settings.model)
148
- }
149
- }
150
-
151
- // --- Streaming Request ---
152
- export async function* requestProviderStream({
153
- configState,
154
- providerType,
155
- model,
156
- system,
157
- messages,
158
- tools,
159
- baseUrl = null,
160
- apiKeyEnv = null,
161
- signal = null,
162
- compaction = null
163
- }) {
164
- const resolvedProviderType = providerType || configState.config.provider.default
165
- const settings = resolveSettings(configState, resolvedProviderType, {
166
- model,
167
- baseUrl,
168
- apiKeyEnv
169
- })
170
- const apiKey = process.env[settings.apiKeyEnv] || ""
171
- const providerCfg = configState.config.provider[settings.configKey] || configState.config.provider[settings.providerType] || {}
172
-
173
- if (providerCfg.stream === false) {
174
- const result = await requestProvider({
175
- configState, providerType, model, system, messages, tools, baseUrl, apiKeyEnv
176
- })
177
- if (result.text) yield { type: "text", content: result.text }
178
- for (const call of result.toolCalls) yield { type: "tool_call", call }
179
- yield { type: "usage", usage: result.usage }
180
- return
181
- }
182
-
183
- const input = {
184
- apiKey,
185
- baseUrl: settings.baseUrl,
186
- apiKeyEnv: settings.apiKeyEnv,
187
- model: settings.model,
188
- system,
189
- messages,
190
- tools,
191
- timeoutMs: Number(providerCfg.timeout_ms || 120000),
192
- streamIdleTimeoutMs: Number(providerCfg.stream_idle_timeout_ms || 120000),
193
- maxTokens: Number(providerCfg.max_tokens || 16384),
194
- retry: {
195
- attempts: Number(providerCfg.retry_attempts || 3),
196
- baseDelayMs: Number(providerCfg.retry_base_delay_ms || 800)
197
- },
198
- thinking: providerCfg.thinking || null,
199
- signal,
200
- compaction
201
- }
202
-
203
- const provider = registry.get(settings.providerType)
204
- if (!provider) {
205
- throw new Error(`unknown provider: ${settings.providerType}. registered: ${listProviders().join(", ")}`)
206
- }
207
- try {
208
- yield* provider.requestStream(input)
209
- } catch (error) {
210
- throw normalizeProviderError(error, settings.providerType, settings.model)
211
- }
212
- }
213
-
214
- // --- Token Counting (Anthropic only, returns null for other providers) ---
215
- export async function countTokensProvider({
216
- configState, providerType, model, system, messages, tools,
217
- baseUrl = null, apiKeyEnv = null
218
- }) {
219
- const resolvedProviderType = providerType || configState.config.provider.default
220
- const settings = resolveSettings(configState, resolvedProviderType, { model, baseUrl, apiKeyEnv })
221
- const provider = registry.get(settings.providerType)
222
- if (!provider?.countTokens) return null
223
- const apiKey = process.env[settings.apiKeyEnv] || ""
224
- return provider.countTokens({
225
- apiKey, baseUrl: settings.baseUrl, model: settings.model,
226
- system, messages, tools
227
- })
228
- }
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
+ }