@shareai-lab/kode 1.0.69 → 1.0.71

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 (253) hide show
  1. package/README.md +205 -72
  2. package/README.zh-CN.md +246 -0
  3. package/cli.js +62 -0
  4. package/package.json +45 -25
  5. package/scripts/postinstall.js +56 -0
  6. package/src/ProjectOnboarding.tsx +180 -0
  7. package/src/Tool.ts +53 -0
  8. package/src/commands/approvedTools.ts +53 -0
  9. package/src/commands/bug.tsx +20 -0
  10. package/src/commands/clear.ts +43 -0
  11. package/src/commands/compact.ts +120 -0
  12. package/src/commands/config.tsx +19 -0
  13. package/src/commands/cost.ts +18 -0
  14. package/src/commands/ctx_viz.ts +209 -0
  15. package/src/commands/doctor.ts +24 -0
  16. package/src/commands/help.tsx +19 -0
  17. package/src/commands/init.ts +37 -0
  18. package/src/commands/listen.ts +42 -0
  19. package/src/commands/login.tsx +51 -0
  20. package/src/commands/logout.tsx +40 -0
  21. package/src/commands/mcp.ts +41 -0
  22. package/src/commands/model.tsx +40 -0
  23. package/src/commands/modelstatus.tsx +20 -0
  24. package/src/commands/onboarding.tsx +34 -0
  25. package/src/commands/pr_comments.ts +59 -0
  26. package/src/commands/refreshCommands.ts +54 -0
  27. package/src/commands/release-notes.ts +34 -0
  28. package/src/commands/resume.tsx +30 -0
  29. package/src/commands/review.ts +49 -0
  30. package/src/commands/terminalSetup.ts +221 -0
  31. package/src/commands.ts +136 -0
  32. package/src/components/ApproveApiKey.tsx +93 -0
  33. package/src/components/AsciiLogo.tsx +13 -0
  34. package/src/components/AutoUpdater.tsx +148 -0
  35. package/src/components/Bug.tsx +367 -0
  36. package/src/components/Config.tsx +289 -0
  37. package/src/components/ConsoleOAuthFlow.tsx +326 -0
  38. package/src/components/Cost.tsx +23 -0
  39. package/src/components/CostThresholdDialog.tsx +46 -0
  40. package/src/components/CustomSelect/option-map.ts +42 -0
  41. package/src/components/CustomSelect/select-option.tsx +52 -0
  42. package/src/components/CustomSelect/select.tsx +143 -0
  43. package/src/components/CustomSelect/use-select-state.ts +414 -0
  44. package/src/components/CustomSelect/use-select.ts +35 -0
  45. package/src/components/FallbackToolUseRejectedMessage.tsx +15 -0
  46. package/src/components/FileEditToolUpdatedMessage.tsx +66 -0
  47. package/src/components/Help.tsx +215 -0
  48. package/src/components/HighlightedCode.tsx +33 -0
  49. package/src/components/InvalidConfigDialog.tsx +113 -0
  50. package/src/components/Link.tsx +32 -0
  51. package/src/components/LogSelector.tsx +86 -0
  52. package/src/components/Logo.tsx +145 -0
  53. package/src/components/MCPServerApprovalDialog.tsx +100 -0
  54. package/src/components/MCPServerDialogCopy.tsx +25 -0
  55. package/src/components/MCPServerMultiselectDialog.tsx +109 -0
  56. package/src/components/Message.tsx +219 -0
  57. package/src/components/MessageResponse.tsx +15 -0
  58. package/src/components/MessageSelector.tsx +211 -0
  59. package/src/components/ModeIndicator.tsx +88 -0
  60. package/src/components/ModelConfig.tsx +301 -0
  61. package/src/components/ModelListManager.tsx +223 -0
  62. package/src/components/ModelSelector.tsx +3208 -0
  63. package/src/components/ModelStatusDisplay.tsx +228 -0
  64. package/src/components/Onboarding.tsx +274 -0
  65. package/src/components/PressEnterToContinue.tsx +11 -0
  66. package/src/components/PromptInput.tsx +710 -0
  67. package/src/components/SentryErrorBoundary.ts +33 -0
  68. package/src/components/Spinner.tsx +129 -0
  69. package/src/components/StructuredDiff.tsx +184 -0
  70. package/src/components/TextInput.tsx +246 -0
  71. package/src/components/TokenWarning.tsx +31 -0
  72. package/src/components/ToolUseLoader.tsx +40 -0
  73. package/src/components/TrustDialog.tsx +106 -0
  74. package/src/components/binary-feedback/BinaryFeedback.tsx +63 -0
  75. package/src/components/binary-feedback/BinaryFeedbackOption.tsx +111 -0
  76. package/src/components/binary-feedback/BinaryFeedbackView.tsx +172 -0
  77. package/src/components/binary-feedback/utils.ts +220 -0
  78. package/src/components/messages/AssistantBashOutputMessage.tsx +22 -0
  79. package/src/components/messages/AssistantLocalCommandOutputMessage.tsx +45 -0
  80. package/src/components/messages/AssistantRedactedThinkingMessage.tsx +19 -0
  81. package/src/components/messages/AssistantTextMessage.tsx +144 -0
  82. package/src/components/messages/AssistantThinkingMessage.tsx +40 -0
  83. package/src/components/messages/AssistantToolUseMessage.tsx +123 -0
  84. package/src/components/messages/UserBashInputMessage.tsx +28 -0
  85. package/src/components/messages/UserCommandMessage.tsx +30 -0
  86. package/src/components/messages/UserKodingInputMessage.tsx +28 -0
  87. package/src/components/messages/UserPromptMessage.tsx +35 -0
  88. package/src/components/messages/UserTextMessage.tsx +39 -0
  89. package/src/components/messages/UserToolResultMessage/UserToolCanceledMessage.tsx +12 -0
  90. package/src/components/messages/UserToolResultMessage/UserToolErrorMessage.tsx +36 -0
  91. package/src/components/messages/UserToolResultMessage/UserToolRejectMessage.tsx +31 -0
  92. package/src/components/messages/UserToolResultMessage/UserToolResultMessage.tsx +57 -0
  93. package/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx +35 -0
  94. package/src/components/messages/UserToolResultMessage/utils.tsx +56 -0
  95. package/src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx +121 -0
  96. package/src/components/permissions/FallbackPermissionRequest.tsx +155 -0
  97. package/src/components/permissions/FileEditPermissionRequest/FileEditPermissionRequest.tsx +182 -0
  98. package/src/components/permissions/FileEditPermissionRequest/FileEditToolDiff.tsx +75 -0
  99. package/src/components/permissions/FileWritePermissionRequest/FileWritePermissionRequest.tsx +164 -0
  100. package/src/components/permissions/FileWritePermissionRequest/FileWriteToolDiff.tsx +81 -0
  101. package/src/components/permissions/FilesystemPermissionRequest/FilesystemPermissionRequest.tsx +242 -0
  102. package/src/components/permissions/PermissionRequest.tsx +103 -0
  103. package/src/components/permissions/PermissionRequestTitle.tsx +69 -0
  104. package/src/components/permissions/hooks.ts +44 -0
  105. package/src/components/permissions/toolUseOptions.ts +59 -0
  106. package/src/components/permissions/utils.ts +23 -0
  107. package/src/constants/betas.ts +5 -0
  108. package/src/constants/claude-asterisk-ascii-art.tsx +238 -0
  109. package/src/constants/figures.ts +4 -0
  110. package/src/constants/keys.ts +3 -0
  111. package/src/constants/macros.ts +6 -0
  112. package/src/constants/models.ts +935 -0
  113. package/src/constants/oauth.ts +18 -0
  114. package/src/constants/product.ts +17 -0
  115. package/src/constants/prompts.ts +177 -0
  116. package/src/constants/releaseNotes.ts +7 -0
  117. package/src/context/PermissionContext.tsx +149 -0
  118. package/src/context.ts +278 -0
  119. package/src/cost-tracker.ts +84 -0
  120. package/src/entrypoints/cli.tsx +1498 -0
  121. package/src/entrypoints/mcp.ts +176 -0
  122. package/src/history.ts +25 -0
  123. package/src/hooks/useApiKeyVerification.ts +59 -0
  124. package/src/hooks/useArrowKeyHistory.ts +55 -0
  125. package/src/hooks/useCanUseTool.ts +138 -0
  126. package/src/hooks/useCancelRequest.ts +39 -0
  127. package/src/hooks/useDoublePress.ts +42 -0
  128. package/src/hooks/useExitOnCtrlCD.ts +31 -0
  129. package/src/hooks/useInterval.ts +25 -0
  130. package/src/hooks/useLogMessages.ts +16 -0
  131. package/src/hooks/useLogStartupTime.ts +12 -0
  132. package/src/hooks/useNotifyAfterTimeout.ts +65 -0
  133. package/src/hooks/usePermissionRequestLogging.ts +44 -0
  134. package/src/hooks/useSlashCommandTypeahead.ts +137 -0
  135. package/src/hooks/useTerminalSize.ts +49 -0
  136. package/src/hooks/useTextInput.ts +315 -0
  137. package/src/messages.ts +37 -0
  138. package/src/permissions.ts +268 -0
  139. package/src/query.ts +704 -0
  140. package/src/screens/ConfigureNpmPrefix.tsx +197 -0
  141. package/src/screens/Doctor.tsx +219 -0
  142. package/src/screens/LogList.tsx +68 -0
  143. package/src/screens/REPL.tsx +792 -0
  144. package/src/screens/ResumeConversation.tsx +68 -0
  145. package/src/services/browserMocks.ts +66 -0
  146. package/src/services/claude.ts +1947 -0
  147. package/src/services/customCommands.ts +683 -0
  148. package/src/services/fileFreshness.ts +377 -0
  149. package/src/services/mcpClient.ts +564 -0
  150. package/src/services/mcpServerApproval.tsx +50 -0
  151. package/src/services/notifier.ts +40 -0
  152. package/src/services/oauth.ts +357 -0
  153. package/src/services/openai.ts +796 -0
  154. package/src/services/sentry.ts +3 -0
  155. package/src/services/statsig.ts +171 -0
  156. package/src/services/statsigStorage.ts +86 -0
  157. package/src/services/systemReminder.ts +406 -0
  158. package/src/services/vcr.ts +161 -0
  159. package/src/tools/ArchitectTool/ArchitectTool.tsx +122 -0
  160. package/src/tools/ArchitectTool/prompt.ts +15 -0
  161. package/src/tools/AskExpertModelTool/AskExpertModelTool.tsx +505 -0
  162. package/src/tools/BashTool/BashTool.tsx +270 -0
  163. package/src/tools/BashTool/BashToolResultMessage.tsx +38 -0
  164. package/src/tools/BashTool/OutputLine.tsx +48 -0
  165. package/src/tools/BashTool/prompt.ts +174 -0
  166. package/src/tools/BashTool/utils.ts +56 -0
  167. package/src/tools/FileEditTool/FileEditTool.tsx +316 -0
  168. package/src/tools/FileEditTool/prompt.ts +51 -0
  169. package/src/tools/FileEditTool/utils.ts +58 -0
  170. package/src/tools/FileReadTool/FileReadTool.tsx +371 -0
  171. package/src/tools/FileReadTool/prompt.ts +7 -0
  172. package/src/tools/FileWriteTool/FileWriteTool.tsx +297 -0
  173. package/src/tools/FileWriteTool/prompt.ts +10 -0
  174. package/src/tools/GlobTool/GlobTool.tsx +119 -0
  175. package/src/tools/GlobTool/prompt.ts +8 -0
  176. package/src/tools/GrepTool/GrepTool.tsx +147 -0
  177. package/src/tools/GrepTool/prompt.ts +11 -0
  178. package/src/tools/MCPTool/MCPTool.tsx +106 -0
  179. package/src/tools/MCPTool/prompt.ts +3 -0
  180. package/src/tools/MemoryReadTool/MemoryReadTool.tsx +127 -0
  181. package/src/tools/MemoryReadTool/prompt.ts +3 -0
  182. package/src/tools/MemoryWriteTool/MemoryWriteTool.tsx +89 -0
  183. package/src/tools/MemoryWriteTool/prompt.ts +3 -0
  184. package/src/tools/MultiEditTool/MultiEditTool.tsx +366 -0
  185. package/src/tools/MultiEditTool/prompt.ts +45 -0
  186. package/src/tools/NotebookEditTool/NotebookEditTool.tsx +298 -0
  187. package/src/tools/NotebookEditTool/prompt.ts +3 -0
  188. package/src/tools/NotebookReadTool/NotebookReadTool.tsx +266 -0
  189. package/src/tools/NotebookReadTool/prompt.ts +3 -0
  190. package/src/tools/StickerRequestTool/StickerRequestTool.tsx +93 -0
  191. package/src/tools/StickerRequestTool/prompt.ts +19 -0
  192. package/src/tools/TaskTool/TaskTool.tsx +382 -0
  193. package/src/tools/TaskTool/constants.ts +1 -0
  194. package/src/tools/TaskTool/prompt.ts +56 -0
  195. package/src/tools/ThinkTool/ThinkTool.tsx +56 -0
  196. package/src/tools/ThinkTool/prompt.ts +12 -0
  197. package/src/tools/TodoWriteTool/TodoWriteTool.tsx +289 -0
  198. package/src/tools/TodoWriteTool/prompt.ts +63 -0
  199. package/src/tools/lsTool/lsTool.tsx +269 -0
  200. package/src/tools/lsTool/prompt.ts +2 -0
  201. package/src/tools.ts +63 -0
  202. package/src/types/PermissionMode.ts +120 -0
  203. package/src/types/RequestContext.ts +72 -0
  204. package/src/utils/Cursor.ts +436 -0
  205. package/src/utils/PersistentShell.ts +373 -0
  206. package/src/utils/agentStorage.ts +97 -0
  207. package/src/utils/array.ts +3 -0
  208. package/src/utils/ask.tsx +98 -0
  209. package/src/utils/auth.ts +13 -0
  210. package/src/utils/autoCompactCore.ts +223 -0
  211. package/src/utils/autoUpdater.ts +318 -0
  212. package/src/utils/betas.ts +20 -0
  213. package/src/utils/browser.ts +14 -0
  214. package/src/utils/cleanup.ts +72 -0
  215. package/src/utils/commands.ts +261 -0
  216. package/src/utils/config.ts +771 -0
  217. package/src/utils/conversationRecovery.ts +54 -0
  218. package/src/utils/debugLogger.ts +1123 -0
  219. package/src/utils/diff.ts +42 -0
  220. package/src/utils/env.ts +57 -0
  221. package/src/utils/errors.ts +21 -0
  222. package/src/utils/exampleCommands.ts +108 -0
  223. package/src/utils/execFileNoThrow.ts +51 -0
  224. package/src/utils/expertChatStorage.ts +136 -0
  225. package/src/utils/file.ts +402 -0
  226. package/src/utils/fileRecoveryCore.ts +71 -0
  227. package/src/utils/format.tsx +44 -0
  228. package/src/utils/generators.ts +62 -0
  229. package/src/utils/git.ts +92 -0
  230. package/src/utils/globalLogger.ts +77 -0
  231. package/src/utils/http.ts +10 -0
  232. package/src/utils/imagePaste.ts +38 -0
  233. package/src/utils/json.ts +13 -0
  234. package/src/utils/log.ts +382 -0
  235. package/src/utils/markdown.ts +213 -0
  236. package/src/utils/messageContextManager.ts +289 -0
  237. package/src/utils/messages.tsx +938 -0
  238. package/src/utils/model.ts +836 -0
  239. package/src/utils/permissions/filesystem.ts +118 -0
  240. package/src/utils/ripgrep.ts +167 -0
  241. package/src/utils/sessionState.ts +49 -0
  242. package/src/utils/state.ts +25 -0
  243. package/src/utils/style.ts +29 -0
  244. package/src/utils/terminal.ts +49 -0
  245. package/src/utils/theme.ts +122 -0
  246. package/src/utils/thinking.ts +144 -0
  247. package/src/utils/todoStorage.ts +431 -0
  248. package/src/utils/tokens.ts +43 -0
  249. package/src/utils/toolExecutionController.ts +163 -0
  250. package/src/utils/unaryLogging.ts +26 -0
  251. package/src/utils/user.ts +37 -0
  252. package/src/utils/validate.ts +165 -0
  253. package/cli.mjs +0 -1803
@@ -0,0 +1,1947 @@
1
+ import '@anthropic-ai/sdk/shims/node'
2
+ import Anthropic, { APIConnectionError, APIError } from '@anthropic-ai/sdk'
3
+ import { AnthropicBedrock } from '@anthropic-ai/bedrock-sdk'
4
+ import { AnthropicVertex } from '@anthropic-ai/vertex-sdk'
5
+ import type { BetaUsage } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
6
+ import chalk from 'chalk'
7
+ import { createHash, randomUUID } from 'crypto'
8
+ import 'dotenv/config'
9
+
10
+ import { addToTotalCost } from '../cost-tracker'
11
+ import models from '../constants/models'
12
+ import type { AssistantMessage, UserMessage } from '../query'
13
+ import { Tool } from '../Tool'
14
+ import {
15
+ getAnthropicApiKey,
16
+ getOrCreateUserID,
17
+ getGlobalConfig,
18
+ } from '../utils/config'
19
+ import { getProjectDocs } from '../context'
20
+ import { logError, SESSION_ID } from '../utils/log'
21
+ import { USER_AGENT } from '../utils/http'
22
+ import {
23
+ createAssistantAPIErrorMessage,
24
+ normalizeContentFromAPI,
25
+ } from '../utils/messages'
26
+ import { countTokens } from '../utils/tokens'
27
+ import { logEvent } from './statsig'
28
+ import { withVCR } from './vcr'
29
+ import {
30
+ debug as debugLogger,
31
+ markPhase,
32
+ getCurrentRequest,
33
+ logLLMInteraction,
34
+ logSystemPromptConstruction,
35
+ logErrorWithDiagnosis,
36
+ } from '../utils/debugLogger'
37
+ import {
38
+ MessageContextManager,
39
+ createRetentionStrategy,
40
+ } from '../utils/messageContextManager'
41
+ import { getModelManager } from '../utils/model'
42
+ import { zodToJsonSchema } from 'zod-to-json-schema'
43
+ import type { BetaMessageStream } from '@anthropic-ai/sdk/lib/BetaMessageStream.mjs'
44
+ import type {
45
+ Message as APIMessage,
46
+ MessageParam,
47
+ TextBlockParam,
48
+ } from '@anthropic-ai/sdk/resources/index.mjs'
49
+ import { USE_BEDROCK, USE_VERTEX } from '../utils/model'
50
+ import { getCLISyspromptPrefix } from '../constants/prompts'
51
+ import { getVertexRegionForModel } from '../utils/model'
52
+ import OpenAI from 'openai'
53
+ import type { ChatCompletionStream } from 'openai/lib/ChatCompletionStream'
54
+ import { ContentBlock } from '@anthropic-ai/sdk/resources/messages/messages'
55
+ import { nanoid } from 'nanoid'
56
+ import { getCompletion, getCompletionWithProfile } from './openai'
57
+ import { getReasoningEffort } from '../utils/thinking'
58
+ import { generateSystemReminders } from './systemReminder'
59
+
60
+ // Helper function to extract model configuration for debug logging
61
+ function getModelConfigForDebug(model: string): {
62
+ modelName: string
63
+ provider: string
64
+ apiKeyStatus: 'configured' | 'missing' | 'invalid'
65
+ baseURL?: string
66
+ maxTokens?: number
67
+ reasoningEffort?: string
68
+ isStream?: boolean
69
+ temperature?: number
70
+ } {
71
+ const config = getGlobalConfig()
72
+ const modelManager = getModelManager()
73
+
74
+ // 🔧 Fix: Use ModelManager to get the actual current model profile
75
+ const modelProfile = modelManager.getModel('main')
76
+
77
+ let apiKeyStatus: 'configured' | 'missing' | 'invalid' = 'missing'
78
+ let baseURL: string | undefined
79
+ let maxTokens: number | undefined
80
+ let reasoningEffort: string | undefined
81
+
82
+ // 🔧 Fix: Use ModelProfile configuration exclusively
83
+ if (modelProfile) {
84
+ apiKeyStatus = modelProfile.apiKey ? 'configured' : 'missing'
85
+ baseURL = modelProfile.baseURL
86
+ maxTokens = modelProfile.maxTokens
87
+ reasoningEffort = modelProfile.reasoningEffort
88
+ } else {
89
+ // 🚨 No ModelProfile available - this should not happen in modern system
90
+ apiKeyStatus = 'missing'
91
+ maxTokens = undefined
92
+ reasoningEffort = undefined
93
+ }
94
+
95
+ return {
96
+ modelName: model,
97
+ provider: modelProfile?.provider || config.primaryProvider || 'anthropic',
98
+ apiKeyStatus,
99
+ baseURL,
100
+ maxTokens,
101
+ reasoningEffort,
102
+ isStream: config.stream || false,
103
+ temperature: MAIN_QUERY_TEMPERATURE,
104
+ }
105
+ }
106
+
107
+ // KodeContext管理器 - 用于项目文档的同步缓存和访问
108
+ class KodeContextManager {
109
+ private static instance: KodeContextManager
110
+ private projectDocsCache: string = ''
111
+ private cacheInitialized: boolean = false
112
+ private initPromise: Promise<void> | null = null
113
+
114
+ private constructor() {}
115
+
116
+ public static getInstance(): KodeContextManager {
117
+ if (!KodeContextManager.instance) {
118
+ KodeContextManager.instance = new KodeContextManager()
119
+ }
120
+ return KodeContextManager.instance
121
+ }
122
+
123
+ public async initialize(): Promise<void> {
124
+ if (this.cacheInitialized) return
125
+
126
+ if (this.initPromise) {
127
+ return this.initPromise
128
+ }
129
+
130
+ this.initPromise = this.loadProjectDocs()
131
+ await this.initPromise
132
+ }
133
+
134
+ private async loadProjectDocs(): Promise<void> {
135
+ try {
136
+ const projectDocs = await getProjectDocs()
137
+ this.projectDocsCache = projectDocs || ''
138
+ this.cacheInitialized = true
139
+
140
+ // 在调试模式下记录加载结果
141
+ if (process.env.NODE_ENV === 'development') {
142
+ console.log(
143
+ `[KodeContext] Loaded ${this.projectDocsCache.length} characters from project docs`,
144
+ )
145
+ }
146
+ } catch (error) {
147
+ console.warn('[KodeContext] Failed to load project docs:', error)
148
+ this.projectDocsCache = ''
149
+ this.cacheInitialized = true
150
+ }
151
+ }
152
+
153
+ public getKodeContext(): string {
154
+ if (!this.cacheInitialized) {
155
+ // 如果未初始化,异步初始化但立即返回空字符串
156
+ this.initialize().catch(console.warn)
157
+ return ''
158
+ }
159
+ return this.projectDocsCache
160
+ }
161
+
162
+ public async refreshCache(): Promise<void> {
163
+ this.cacheInitialized = false
164
+ this.initPromise = null
165
+ await this.initialize()
166
+ }
167
+ }
168
+
169
+ // 导出函数保持向后兼容
170
+ const kodeContextManager = KodeContextManager.getInstance()
171
+
172
+ // 在模块加载时异步初始化
173
+ kodeContextManager.initialize().catch(console.warn)
174
+
175
+ export const generateKodeContext = (): string => {
176
+ return kodeContextManager.getKodeContext()
177
+ }
178
+
179
+ export const refreshKodeContext = async (): Promise<void> => {
180
+ await kodeContextManager.refreshCache()
181
+ }
182
+
183
+ interface StreamResponse extends APIMessage {
184
+ ttftMs?: number
185
+ }
186
+
187
+ export const API_ERROR_MESSAGE_PREFIX = 'API Error'
188
+ export const PROMPT_TOO_LONG_ERROR_MESSAGE = 'Prompt is too long'
189
+ export const CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE = 'Credit balance is too low'
190
+ export const INVALID_API_KEY_ERROR_MESSAGE =
191
+ 'Invalid API key · Please run /login'
192
+ export const NO_CONTENT_MESSAGE = '(no content)'
193
+ const PROMPT_CACHING_ENABLED = !process.env.DISABLE_PROMPT_CACHING
194
+
195
+ // @see https://docs.anthropic.com/en/docs/about-claude/models#model-comparison-table
196
+ const HAIKU_COST_PER_MILLION_INPUT_TOKENS = 0.8
197
+ const HAIKU_COST_PER_MILLION_OUTPUT_TOKENS = 4
198
+ const HAIKU_COST_PER_MILLION_PROMPT_CACHE_WRITE_TOKENS = 1
199
+ const HAIKU_COST_PER_MILLION_PROMPT_CACHE_READ_TOKENS = 0.08
200
+
201
+ const SONNET_COST_PER_MILLION_INPUT_TOKENS = 3
202
+ const SONNET_COST_PER_MILLION_OUTPUT_TOKENS = 15
203
+ const SONNET_COST_PER_MILLION_PROMPT_CACHE_WRITE_TOKENS = 3.75
204
+ const SONNET_COST_PER_MILLION_PROMPT_CACHE_READ_TOKENS = 0.3
205
+
206
+ export const MAIN_QUERY_TEMPERATURE = 1 // to get more variation for binary feedback
207
+
208
+ function getMetadata() {
209
+ return {
210
+ user_id: `${getOrCreateUserID()}_${SESSION_ID}`,
211
+ }
212
+ }
213
+
214
+ const MAX_RETRIES = process.env.USER_TYPE === 'SWE_BENCH' ? 100 : 10
215
+ const BASE_DELAY_MS = 500
216
+
217
+ interface RetryOptions {
218
+ maxRetries?: number
219
+ signal?: AbortSignal
220
+ }
221
+
222
+ // Helper function to create an abortable delay
223
+ function abortableDelay(delayMs: number, signal?: AbortSignal): Promise<void> {
224
+ return new Promise((resolve, reject) => {
225
+ // Check if already aborted
226
+ if (signal?.aborted) {
227
+ reject(new Error('Request was aborted'))
228
+ return
229
+ }
230
+
231
+ const timeoutId = setTimeout(() => {
232
+ resolve()
233
+ }, delayMs)
234
+
235
+ // If signal is provided, listen for abort event
236
+ if (signal) {
237
+ const abortHandler = () => {
238
+ clearTimeout(timeoutId)
239
+ reject(new Error('Request was aborted'))
240
+ }
241
+ signal.addEventListener('abort', abortHandler, { once: true })
242
+ }
243
+ })
244
+ }
245
+
246
+ function getRetryDelay(
247
+ attempt: number,
248
+ retryAfterHeader?: string | null,
249
+ ): number {
250
+ if (retryAfterHeader) {
251
+ const seconds = parseInt(retryAfterHeader, 10)
252
+ if (!isNaN(seconds)) {
253
+ return seconds * 1000
254
+ }
255
+ }
256
+ return Math.min(BASE_DELAY_MS * Math.pow(2, attempt - 1), 32000) // Max 32s delay
257
+ }
258
+
259
+ function shouldRetry(error: APIError): boolean {
260
+ // Check for overloaded errors first and only retry for SWE_BENCH
261
+ if (error.message?.includes('"type":"overloaded_error"')) {
262
+ return process.env.USER_TYPE === 'SWE_BENCH'
263
+ }
264
+
265
+ // Note this is not a standard header.
266
+ const shouldRetryHeader = error.headers?.['x-should-retry']
267
+
268
+ // If the server explicitly says whether or not to retry, obey.
269
+ if (shouldRetryHeader === 'true') return true
270
+ if (shouldRetryHeader === 'false') return false
271
+
272
+ if (error instanceof APIConnectionError) {
273
+ return true
274
+ }
275
+
276
+ if (!error.status) return false
277
+
278
+ // Retry on request timeouts.
279
+ if (error.status === 408) return true
280
+
281
+ // Retry on lock timeouts.
282
+ if (error.status === 409) return true
283
+
284
+ // Retry on rate limits.
285
+ if (error.status === 429) return true
286
+
287
+ // Retry internal errors.
288
+ if (error.status && error.status >= 500) return true
289
+
290
+ return false
291
+ }
292
+
293
+ async function withRetry<T>(
294
+ operation: (attempt: number) => Promise<T>,
295
+ options: RetryOptions = {},
296
+ ): Promise<T> {
297
+ const maxRetries = options.maxRetries ?? MAX_RETRIES
298
+ let lastError: unknown
299
+
300
+ for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
301
+ try {
302
+ return await operation(attempt)
303
+ } catch (error) {
304
+ lastError = error
305
+ // Only retry if the error indicates we should
306
+ if (
307
+ attempt > maxRetries ||
308
+ !(error instanceof APIError) ||
309
+ !shouldRetry(error)
310
+ ) {
311
+ throw error
312
+ }
313
+ // 🔧 CRITICAL FIX: Check abort signal BEFORE showing retry message
314
+ if (options.signal?.aborted) {
315
+ throw new Error('Request cancelled by user')
316
+ }
317
+
318
+ // Get retry-after header if available
319
+ const retryAfter = error.headers?.['retry-after'] ?? null
320
+ const delayMs = getRetryDelay(attempt, retryAfter)
321
+
322
+ console.log(
323
+ ` ⎿ ${chalk.red(`API ${error.name} (${error.message}) · Retrying in ${Math.round(delayMs / 1000)} seconds… (attempt ${attempt}/${maxRetries})`)}`,
324
+ )
325
+
326
+ logEvent('tengu_api_retry', {
327
+ attempt: String(attempt),
328
+ delayMs: String(delayMs),
329
+ error: error.message,
330
+ status: String(error.status),
331
+ provider: USE_BEDROCK ? 'bedrock' : USE_VERTEX ? 'vertex' : '1p',
332
+ })
333
+
334
+ try {
335
+ await abortableDelay(delayMs, options.signal)
336
+ } catch (delayError) {
337
+ // If aborted during delay, throw the error to stop retrying
338
+ if (delayError.message === 'Request was aborted') {
339
+ throw new Error('Request cancelled by user')
340
+ }
341
+ throw delayError
342
+ }
343
+ }
344
+ }
345
+
346
+ throw lastError
347
+ }
348
+
349
+ /**
350
+ * Fetch available models from Anthropic API
351
+ */
352
+ export async function fetchAnthropicModels(
353
+ baseURL: string,
354
+ apiKey: string,
355
+ ): Promise<any[]> {
356
+ try {
357
+ // Use provided baseURL or default to official Anthropic API
358
+ const modelsURL = baseURL
359
+ ? `${baseURL.replace(/\/+$/, '')}/v1/models`
360
+ : 'https://api.anthropic.com/v1/models'
361
+
362
+ const response = await fetch(modelsURL, {
363
+ method: 'GET',
364
+ headers: {
365
+ 'x-api-key': apiKey,
366
+ 'anthropic-version': '2023-06-01',
367
+ 'User-Agent': USER_AGENT,
368
+ },
369
+ })
370
+
371
+ if (!response.ok) {
372
+ // Provide user-friendly error messages based on status code
373
+ if (response.status === 401) {
374
+ throw new Error(
375
+ 'Invalid API key. Please check your Anthropic API key and try again.',
376
+ )
377
+ } else if (response.status === 403) {
378
+ throw new Error(
379
+ 'API key does not have permission to access models. Please check your API key permissions.',
380
+ )
381
+ } else if (response.status === 429) {
382
+ throw new Error(
383
+ 'Too many requests. Please wait a moment and try again.',
384
+ )
385
+ } else if (response.status >= 500) {
386
+ throw new Error(
387
+ 'Anthropic service is temporarily unavailable. Please try again later.',
388
+ )
389
+ } else {
390
+ throw new Error(
391
+ `Unable to connect to Anthropic API (${response.status}). Please check your internet connection and API key.`,
392
+ )
393
+ }
394
+ }
395
+
396
+ const data = await response.json()
397
+ return data.data || []
398
+ } catch (error) {
399
+ // If it's already our custom error, pass it through
400
+ if (
401
+ (error instanceof Error && error.message.includes('API key')) ||
402
+ (error instanceof Error && error.message.includes('Anthropic'))
403
+ ) {
404
+ throw error
405
+ }
406
+
407
+ // For network errors or other issues
408
+ console.error('Failed to fetch Anthropic models:', error)
409
+ throw new Error(
410
+ 'Unable to connect to Anthropic API. Please check your internet connection and try again.',
411
+ )
412
+ }
413
+ }
414
+
415
+ export async function verifyApiKey(
416
+ apiKey: string,
417
+ baseURL?: string,
418
+ provider?: string,
419
+ ): Promise<boolean> {
420
+ if (!apiKey) {
421
+ return false
422
+ }
423
+
424
+ // For non-Anthropic providers, use OpenAI-compatible verification
425
+ if (provider && provider !== 'anthropic') {
426
+ try {
427
+ const headers: Record<string, string> = {
428
+ Authorization: `Bearer ${apiKey}`,
429
+ 'Content-Type': 'application/json',
430
+ }
431
+
432
+ // 🔧 Fix: Proper URL construction for verification
433
+ if (!baseURL) {
434
+ console.warn(
435
+ 'No baseURL provided for non-Anthropic provider verification',
436
+ )
437
+ return false
438
+ }
439
+
440
+ const modelsURL = `${baseURL.replace(/\/+$/, '')}/models`
441
+
442
+ const response = await fetch(modelsURL, {
443
+ method: 'GET',
444
+ headers,
445
+ })
446
+
447
+ return response.ok
448
+ } catch (error) {
449
+ console.warn('API verification failed for non-Anthropic provider:', error)
450
+ return false
451
+ }
452
+ }
453
+
454
+ // For Anthropic and Anthropic-compatible APIs
455
+ const clientConfig: any = {
456
+ apiKey,
457
+ dangerouslyAllowBrowser: true,
458
+ maxRetries: 3,
459
+ defaultHeaders: {
460
+ 'User-Agent': USER_AGENT,
461
+ },
462
+ }
463
+
464
+ // Only add baseURL for true Anthropic-compatible APIs
465
+ if (
466
+ baseURL &&
467
+ (provider === 'anthropic' ||
468
+ provider === 'bigdream' ||
469
+ provider === 'opendev')
470
+ ) {
471
+ clientConfig.baseURL = baseURL
472
+ }
473
+
474
+ const anthropic = new Anthropic(clientConfig)
475
+
476
+ try {
477
+ await withRetry(
478
+ async () => {
479
+ const model = 'claude-sonnet-4-20250514'
480
+ const messages: MessageParam[] = [{ role: 'user', content: 'test' }]
481
+ await anthropic.messages.create({
482
+ model,
483
+ max_tokens: 1000, // Simple test token limit for API verification
484
+ messages,
485
+ temperature: 0,
486
+ metadata: getMetadata(),
487
+ })
488
+ return true
489
+ },
490
+ { maxRetries: 2 }, // Use fewer retries for API key verification
491
+ )
492
+ return true
493
+ } catch (error) {
494
+ logError(error)
495
+ // Check for authentication error
496
+ if (
497
+ error instanceof Error &&
498
+ error.message.includes(
499
+ '{"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"}}',
500
+ )
501
+ ) {
502
+ return false
503
+ }
504
+ throw error
505
+ }
506
+ }
507
+
508
+ function convertAnthropicMessagesToOpenAIMessages(
509
+ messages: (UserMessage | AssistantMessage)[],
510
+ ): (
511
+ | OpenAI.ChatCompletionMessageParam
512
+ | OpenAI.ChatCompletionToolMessageParam
513
+ )[] {
514
+ const openaiMessages: (
515
+ | OpenAI.ChatCompletionMessageParam
516
+ | OpenAI.ChatCompletionToolMessageParam
517
+ )[] = []
518
+
519
+ const toolResults: Record<string, OpenAI.ChatCompletionToolMessageParam> = {}
520
+
521
+ for (const message of messages) {
522
+ let contentBlocks = []
523
+ if (typeof message.message.content === 'string') {
524
+ contentBlocks = [
525
+ {
526
+ type: 'text',
527
+ text: message.message.content,
528
+ },
529
+ ]
530
+ } else if (!Array.isArray(message.message.content)) {
531
+ contentBlocks = [message.message.content]
532
+ } else {
533
+ contentBlocks = message.message.content
534
+ }
535
+
536
+ for (const block of contentBlocks) {
537
+ if (block.type === 'text') {
538
+ openaiMessages.push({
539
+ role: message.message.role,
540
+ content: block.text,
541
+ })
542
+ } else if (block.type === 'tool_use') {
543
+ openaiMessages.push({
544
+ role: 'assistant',
545
+ content: undefined,
546
+ tool_calls: [
547
+ {
548
+ type: 'function',
549
+ function: {
550
+ name: block.name,
551
+ arguments: JSON.stringify(block.input),
552
+ },
553
+ id: block.id,
554
+ },
555
+ ],
556
+ })
557
+ } else if (block.type === 'tool_result') {
558
+ // Ensure content is always a string for role:tool messages
559
+ let toolContent = block.content
560
+ if (typeof toolContent !== 'string') {
561
+ // Convert content to string if it's not already
562
+ toolContent = JSON.stringify(toolContent)
563
+ }
564
+
565
+ toolResults[block.tool_use_id] = {
566
+ role: 'tool',
567
+ content: toolContent,
568
+ tool_call_id: block.tool_use_id,
569
+ }
570
+ }
571
+ }
572
+ }
573
+
574
+ const finalMessages: (
575
+ | OpenAI.ChatCompletionMessageParam
576
+ | OpenAI.ChatCompletionToolMessageParam
577
+ )[] = []
578
+
579
+ for (const message of openaiMessages) {
580
+ finalMessages.push(message)
581
+
582
+ if ('tool_calls' in message && message.tool_calls) {
583
+ for (const toolCall of message.tool_calls) {
584
+ if (toolResults[toolCall.id]) {
585
+ finalMessages.push(toolResults[toolCall.id])
586
+ }
587
+ }
588
+ }
589
+ }
590
+
591
+ return finalMessages
592
+ }
593
+
594
+ function messageReducer(
595
+ previous: OpenAI.ChatCompletionMessage,
596
+ item: OpenAI.ChatCompletionChunk,
597
+ ): OpenAI.ChatCompletionMessage {
598
+ const reduce = (acc: any, delta: OpenAI.ChatCompletionChunk.Choice.Delta) => {
599
+ acc = { ...acc }
600
+ for (const [key, value] of Object.entries(delta)) {
601
+ if (acc[key] === undefined || acc[key] === null) {
602
+ acc[key] = value
603
+ // OpenAI.Chat.Completions.ChatCompletionMessageToolCall does not have a key, .index
604
+ if (Array.isArray(acc[key])) {
605
+ for (const arr of acc[key]) {
606
+ delete arr.index
607
+ }
608
+ }
609
+ } else if (typeof acc[key] === 'string' && typeof value === 'string') {
610
+ acc[key] += value
611
+ } else if (typeof acc[key] === 'number' && typeof value === 'number') {
612
+ acc[key] = value
613
+ } else if (Array.isArray(acc[key]) && Array.isArray(value)) {
614
+ const accArray = acc[key]
615
+ for (let i = 0; i < value.length; i++) {
616
+ const { index, ...chunkTool } = value[i]
617
+ if (index - accArray.length > 1) {
618
+ throw new Error(
619
+ `Error: An array has an empty value when tool_calls are constructed. tool_calls: ${accArray}; tool: ${value}`,
620
+ )
621
+ }
622
+ accArray[index] = reduce(accArray[index], chunkTool)
623
+ }
624
+ } else if (typeof acc[key] === 'object' && typeof value === 'object') {
625
+ acc[key] = reduce(acc[key], value)
626
+ }
627
+ }
628
+ return acc
629
+ }
630
+
631
+ const choice = item.choices?.[0]
632
+ if (!choice) {
633
+ // chunk contains information about usage and token counts
634
+ return previous
635
+ }
636
+ return reduce(previous, choice.delta) as OpenAI.ChatCompletionMessage
637
+ }
638
+ async function handleMessageStream(
639
+ stream: ChatCompletionStream,
640
+ signal?: AbortSignal, // 🔧 Add AbortSignal support to stream handler
641
+ ): Promise<OpenAI.ChatCompletion> {
642
+ const streamStartTime = Date.now()
643
+ let ttftMs: number | undefined
644
+ let chunkCount = 0
645
+ let errorCount = 0
646
+
647
+ debugLogger.api('OPENAI_STREAM_START', {
648
+ streamStartTime: String(streamStartTime),
649
+ })
650
+
651
+ let message = {} as OpenAI.ChatCompletionMessage
652
+
653
+ let id, model, created, object, usage
654
+ try {
655
+ for await (const chunk of stream) {
656
+ // 🔧 CRITICAL FIX: Check abort signal in OpenAI streaming loop
657
+ if (signal?.aborted) {
658
+ debugLogger.flow('OPENAI_STREAM_ABORTED', {
659
+ chunkCount,
660
+ timestamp: Date.now()
661
+ })
662
+ throw new Error('Request was cancelled')
663
+ }
664
+
665
+ chunkCount++
666
+
667
+ try {
668
+ if (!id) {
669
+ id = chunk.id
670
+ debugLogger.api('OPENAI_STREAM_ID_RECEIVED', {
671
+ id,
672
+ chunkNumber: String(chunkCount),
673
+ })
674
+ }
675
+ if (!model) {
676
+ model = chunk.model
677
+ debugLogger.api('OPENAI_STREAM_MODEL_RECEIVED', {
678
+ model,
679
+ chunkNumber: String(chunkCount),
680
+ })
681
+ }
682
+ if (!created) {
683
+ created = chunk.created
684
+ }
685
+ if (!object) {
686
+ object = chunk.object
687
+ }
688
+ if (!usage) {
689
+ usage = chunk.usage
690
+ }
691
+
692
+ message = messageReducer(message, chunk)
693
+
694
+ if (chunk?.choices?.[0]?.delta?.content) {
695
+ if (!ttftMs) {
696
+ ttftMs = Date.now() - streamStartTime
697
+ debugLogger.api('OPENAI_STREAM_FIRST_TOKEN', {
698
+ ttftMs: String(ttftMs),
699
+ chunkNumber: String(chunkCount),
700
+ })
701
+ }
702
+ }
703
+ } catch (chunkError) {
704
+ errorCount++
705
+ debugLogger.error('OPENAI_STREAM_CHUNK_ERROR', {
706
+ chunkNumber: String(chunkCount),
707
+ errorMessage:
708
+ chunkError instanceof Error
709
+ ? chunkError.message
710
+ : String(chunkError),
711
+ errorType:
712
+ chunkError instanceof Error
713
+ ? chunkError.constructor.name
714
+ : typeof chunkError,
715
+ })
716
+ // Continue processing other chunks
717
+ }
718
+ }
719
+
720
+ debugLogger.api('OPENAI_STREAM_COMPLETE', {
721
+ totalChunks: String(chunkCount),
722
+ errorCount: String(errorCount),
723
+ totalDuration: String(Date.now() - streamStartTime),
724
+ ttftMs: String(ttftMs || 0),
725
+ finalMessageId: id || 'undefined',
726
+ })
727
+ } catch (streamError) {
728
+ debugLogger.error('OPENAI_STREAM_FATAL_ERROR', {
729
+ totalChunks: String(chunkCount),
730
+ errorCount: String(errorCount),
731
+ errorMessage:
732
+ streamError instanceof Error
733
+ ? streamError.message
734
+ : String(streamError),
735
+ errorType:
736
+ streamError instanceof Error
737
+ ? streamError.constructor.name
738
+ : typeof streamError,
739
+ })
740
+ throw streamError
741
+ }
742
+ return {
743
+ id,
744
+ created,
745
+ model,
746
+ object,
747
+ choices: [
748
+ {
749
+ index: 0,
750
+ message,
751
+ finish_reason: 'stop',
752
+ logprobs: undefined,
753
+ },
754
+ ],
755
+ usage,
756
+ }
757
+ }
758
+
759
+ function convertOpenAIResponseToAnthropic(response: OpenAI.ChatCompletion) {
760
+ let contentBlocks: ContentBlock[] = []
761
+ const message = response.choices?.[0]?.message
762
+ if (!message) {
763
+ logEvent('weird_response', {
764
+ response: JSON.stringify(response),
765
+ })
766
+ return {
767
+ role: 'assistant',
768
+ content: [],
769
+ stop_reason: response.choices?.[0]?.finish_reason,
770
+ type: 'message',
771
+ usage: response.usage,
772
+ }
773
+ }
774
+
775
+ if (message?.tool_calls) {
776
+ for (const toolCall of message.tool_calls) {
777
+ const tool = toolCall.function
778
+ const toolName = tool.name
779
+ let toolArgs = {}
780
+ try {
781
+ toolArgs = JSON.parse(tool.arguments)
782
+ } catch (e) {
783
+ // console.log(e)
784
+ }
785
+
786
+ contentBlocks.push({
787
+ type: 'tool_use',
788
+ input: toolArgs,
789
+ name: toolName,
790
+ id: toolCall.id?.length > 0 ? toolCall.id : nanoid(),
791
+ })
792
+ }
793
+ }
794
+
795
+ if ((message as any).reasoning) {
796
+ contentBlocks.push({
797
+ type: 'thinking',
798
+ thinking: (message as any).reasoning,
799
+ signature: '',
800
+ })
801
+ }
802
+
803
+ // NOTE: For deepseek api, the key for its returned reasoning process is reasoning_content
804
+ if ((message as any).reasoning_content) {
805
+ contentBlocks.push({
806
+ type: 'thinking',
807
+ thinking: (message as any).reasoning_content,
808
+ signature: '',
809
+ })
810
+ }
811
+
812
+ if (message.content) {
813
+ contentBlocks.push({
814
+ type: 'text',
815
+ text: message?.content,
816
+ citations: [],
817
+ })
818
+ }
819
+
820
+ const finalMessage = {
821
+ role: 'assistant',
822
+ content: contentBlocks,
823
+ stop_reason: response.choices?.[0]?.finish_reason,
824
+ type: 'message',
825
+ usage: response.usage,
826
+ }
827
+
828
+ return finalMessage
829
+ }
830
+
831
+ let anthropicClient: Anthropic | AnthropicBedrock | AnthropicVertex | null =
832
+ null
833
+
834
+ /**
835
+ * Get the Anthropic client, creating it if it doesn't exist
836
+ */
837
+ export function getAnthropicClient(
838
+ model?: string,
839
+ ): Anthropic | AnthropicBedrock | AnthropicVertex {
840
+ const config = getGlobalConfig()
841
+ const provider = config.primaryProvider
842
+
843
+ // Reset client if provider has changed to ensure correct configuration
844
+ if (anthropicClient && provider) {
845
+ // Always recreate client for provider-specific configurations
846
+ anthropicClient = null
847
+ }
848
+
849
+ if (anthropicClient) {
850
+ return anthropicClient
851
+ }
852
+
853
+ const region = getVertexRegionForModel(model)
854
+
855
+ const defaultHeaders: { [key: string]: string } = {
856
+ 'x-app': 'cli',
857
+ 'User-Agent': USER_AGENT,
858
+ }
859
+ if (process.env.ANTHROPIC_AUTH_TOKEN) {
860
+ defaultHeaders['Authorization'] =
861
+ `Bearer ${process.env.ANTHROPIC_AUTH_TOKEN}`
862
+ }
863
+
864
+ const ARGS = {
865
+ defaultHeaders,
866
+ maxRetries: 0, // Disabled auto-retry in favor of manual implementation
867
+ timeout: parseInt(process.env.API_TIMEOUT_MS || String(60 * 1000), 10),
868
+ }
869
+ if (USE_BEDROCK) {
870
+ const client = new AnthropicBedrock(ARGS)
871
+ anthropicClient = client
872
+ return client
873
+ }
874
+ if (USE_VERTEX) {
875
+ const vertexArgs = {
876
+ ...ARGS,
877
+ region: region || process.env.CLOUD_ML_REGION || 'us-east5',
878
+ }
879
+ const client = new AnthropicVertex(vertexArgs)
880
+ anthropicClient = client
881
+ return client
882
+ }
883
+
884
+ // Get appropriate API key and baseURL from ModelProfile
885
+ const modelManager = getModelManager()
886
+ const modelProfile = modelManager.getModel('main')
887
+
888
+ let apiKey: string
889
+ let baseURL: string | undefined
890
+
891
+ if (modelProfile) {
892
+ apiKey = modelProfile.apiKey || ''
893
+ baseURL = modelProfile.baseURL
894
+ } else {
895
+ // Fallback to default anthropic if no ModelProfile
896
+ apiKey = getAnthropicApiKey()
897
+ baseURL = undefined
898
+ }
899
+
900
+ if (process.env.USER_TYPE === 'ant' && !apiKey && provider === 'anthropic') {
901
+ console.error(
902
+ chalk.red(
903
+ '[ANT-ONLY] Please set the ANTHROPIC_API_KEY environment variable to use the CLI. To create a new key, go to https://console.anthropic.com/settings/keys.',
904
+ ),
905
+ )
906
+ }
907
+
908
+ // Create client with custom baseURL for BigDream/OpenDev
909
+ // Anthropic SDK will append the appropriate paths (like /v1/messages)
910
+ const clientConfig = {
911
+ apiKey,
912
+ dangerouslyAllowBrowser: true,
913
+ ...ARGS,
914
+ ...(baseURL && { baseURL }), // Use baseURL directly, SDK will handle API versioning
915
+ }
916
+
917
+ anthropicClient = new Anthropic(clientConfig)
918
+ return anthropicClient
919
+ }
920
+
921
+ /**
922
+ * Reset the Anthropic client to null, forcing a new client to be created on next use
923
+ */
924
+ export function resetAnthropicClient(): void {
925
+ anthropicClient = null
926
+ }
927
+
928
+ /**
929
+ * Environment variables for different client types:
930
+ *
931
+ * Direct API:
932
+ * - ANTHROPIC_API_KEY: Required for direct API access
933
+ *
934
+ * AWS Bedrock:
935
+ * - AWS credentials configured via aws-sdk defaults
936
+ *
937
+ * Vertex AI:
938
+ * - Model-specific region variables (highest priority):
939
+ * - VERTEX_REGION_CLAUDE_3_5_HAIKU: Region for Claude 3.5 Haiku model
940
+ * - VERTEX_REGION_CLAUDE_3_5_SONNET: Region for Claude 3.5 Sonnet model
941
+ * - VERTEX_REGION_CLAUDE_3_7_SONNET: Region for Claude 3.7 Sonnet model
942
+ * - CLOUD_ML_REGION: Optional. The default GCP region to use for all models
943
+ * If specific model region not specified above
944
+ * - ANTHROPIC_VERTEX_PROJECT_ID: Required. Your GCP project ID
945
+ * - Standard GCP credentials configured via google-auth-library
946
+ *
947
+ * Priority for determining region:
948
+ * 1. Hardcoded model-specific environment variables
949
+ * 2. Global CLOUD_ML_REGION variable
950
+ * 3. Default region from config
951
+ * 4. Fallback region (us-east5)
952
+ */
953
+
954
+ export function userMessageToMessageParam(
955
+ message: UserMessage,
956
+ addCache = false,
957
+ ): MessageParam {
958
+ if (addCache) {
959
+ if (typeof message.message.content === 'string') {
960
+ return {
961
+ role: 'user',
962
+ content: [
963
+ {
964
+ type: 'text',
965
+ text: message.message.content,
966
+ ...(PROMPT_CACHING_ENABLED
967
+ ? { cache_control: { type: 'ephemeral' } }
968
+ : {}),
969
+ },
970
+ ],
971
+ }
972
+ } else {
973
+ return {
974
+ role: 'user',
975
+ content: message.message.content.map((_, i) => ({
976
+ ..._,
977
+ ...(i === message.message.content.length - 1
978
+ ? PROMPT_CACHING_ENABLED
979
+ ? { cache_control: { type: 'ephemeral' } }
980
+ : {}
981
+ : {}),
982
+ })),
983
+ }
984
+ }
985
+ }
986
+ return {
987
+ role: 'user',
988
+ content: message.message.content,
989
+ }
990
+ }
991
+
992
+ export function assistantMessageToMessageParam(
993
+ message: AssistantMessage,
994
+ addCache = false,
995
+ ): MessageParam {
996
+ if (addCache) {
997
+ if (typeof message.message.content === 'string') {
998
+ return {
999
+ role: 'assistant',
1000
+ content: [
1001
+ {
1002
+ type: 'text',
1003
+ text: message.message.content,
1004
+ ...(PROMPT_CACHING_ENABLED
1005
+ ? { cache_control: { type: 'ephemeral' } }
1006
+ : {}),
1007
+ },
1008
+ ],
1009
+ }
1010
+ } else {
1011
+ return {
1012
+ role: 'assistant',
1013
+ content: message.message.content.map((_, i) => ({
1014
+ ..._,
1015
+ ...(i === message.message.content.length - 1 &&
1016
+ _.type !== 'thinking' &&
1017
+ _.type !== 'redacted_thinking'
1018
+ ? PROMPT_CACHING_ENABLED
1019
+ ? { cache_control: { type: 'ephemeral' } }
1020
+ : {}
1021
+ : {}),
1022
+ })),
1023
+ }
1024
+ }
1025
+ }
1026
+ return {
1027
+ role: 'assistant',
1028
+ content: message.message.content,
1029
+ }
1030
+ }
1031
+
1032
+ function splitSysPromptPrefix(systemPrompt: string[]): string[] {
1033
+ // split out the first block of the system prompt as the "prefix" for API
1034
+ // to match on in https://console.statsig.com/4aF3Ewatb6xPVpCwxb5nA3/dynamic_configs/claude_cli_system_prompt_prefixes
1035
+ const systemPromptFirstBlock = systemPrompt[0] || ''
1036
+ const systemPromptRest = systemPrompt.slice(1)
1037
+ return [systemPromptFirstBlock, systemPromptRest.join('\n')].filter(Boolean)
1038
+ }
1039
+
1040
+ export async function queryLLM(
1041
+ messages: (UserMessage | AssistantMessage)[],
1042
+ systemPrompt: string[],
1043
+ maxThinkingTokens: number,
1044
+ tools: Tool[],
1045
+ signal: AbortSignal,
1046
+ options: {
1047
+ safeMode: boolean
1048
+ model: string | import('../utils/config').ModelPointerType
1049
+ prependCLISysprompt: boolean
1050
+ },
1051
+ ): Promise<AssistantMessage> {
1052
+ // 🔧 统一的模型解析:支持指针、model ID 和真实模型名称
1053
+ const modelManager = getModelManager()
1054
+ const modelResolution = modelManager.resolveModelWithInfo(options.model)
1055
+
1056
+ if (!modelResolution.success || !modelResolution.profile) {
1057
+ throw new Error(
1058
+ modelResolution.error || `Failed to resolve model: ${options.model}`,
1059
+ )
1060
+ }
1061
+
1062
+ const modelProfile = modelResolution.profile
1063
+ const resolvedModel = modelProfile.modelName
1064
+
1065
+ debugLogger.api('MODEL_RESOLVED', {
1066
+ inputParam: options.model,
1067
+ resolvedModelName: modelProfile.modelName,
1068
+ resolvedModelName: resolvedModel,
1069
+ provider: modelProfile.provider,
1070
+ isPointer: ['main', 'task', 'reasoning', 'quick'].includes(options.model),
1071
+ requestId: getCurrentRequest()?.id,
1072
+ })
1073
+
1074
+ const currentRequest = getCurrentRequest()
1075
+ debugLogger.api('LLM_REQUEST_START', {
1076
+ messageCount: messages.length,
1077
+ systemPromptLength: systemPrompt.join(' ').length,
1078
+ toolCount: tools.length,
1079
+ model: resolvedModel,
1080
+ originalModelParam: options.model,
1081
+ requestId: currentRequest?.id,
1082
+ })
1083
+
1084
+ markPhase('LLM_CALL')
1085
+
1086
+ try {
1087
+ const result = await withVCR(messages, () =>
1088
+ queryLLMWithPromptCaching(
1089
+ messages,
1090
+ systemPrompt,
1091
+ maxThinkingTokens,
1092
+ tools,
1093
+ signal,
1094
+ { ...options, model: resolvedModel, modelProfile }, // Pass resolved ModelProfile
1095
+ ),
1096
+ )
1097
+
1098
+ debugLogger.api('LLM_REQUEST_SUCCESS', {
1099
+ costUSD: result.costUSD,
1100
+ durationMs: result.durationMs,
1101
+ responseLength: result.message.content?.length || 0,
1102
+ requestId: currentRequest?.id,
1103
+ })
1104
+
1105
+ return result
1106
+ } catch (error) {
1107
+ // 使用错误诊断系统记录 LLM 相关错误
1108
+ logErrorWithDiagnosis(
1109
+ error,
1110
+ {
1111
+ messageCount: messages.length,
1112
+ systemPromptLength: systemPrompt.join(' ').length,
1113
+ model: options.model,
1114
+ toolCount: tools.length,
1115
+ phase: 'LLM_CALL',
1116
+ },
1117
+ currentRequest?.id,
1118
+ )
1119
+
1120
+ throw error
1121
+ }
1122
+ }
1123
+
1124
+ export function formatSystemPromptWithContext(
1125
+ systemPrompt: string[],
1126
+ context: { [k: string]: string },
1127
+ agentId?: string,
1128
+ skipContextReminders = false, // Parameter kept for API compatibility but not used anymore
1129
+ ): { systemPrompt: string[]; reminders: string } {
1130
+ // 构建增强的系统提示 - 对齐官方 Claude Code 直接注入方式
1131
+ const enhancedPrompt = [...systemPrompt]
1132
+ let reminders = ''
1133
+
1134
+ // 只有当上下文存在时才处理
1135
+ const hasContext = Object.entries(context).length > 0
1136
+
1137
+ if (hasContext) {
1138
+ // 步骤1: 直接注入 Kode 上下文到系统提示 - 对齐官方设计
1139
+ if (!skipContextReminders) {
1140
+ const kodeContext = generateKodeContext()
1141
+ if (kodeContext) {
1142
+ // 添加分隔符和标识,使项目文档在系统提示中更清晰
1143
+ enhancedPrompt.push('\n---\n# 项目上下文\n')
1144
+ enhancedPrompt.push(kodeContext)
1145
+ enhancedPrompt.push('\n---\n')
1146
+ }
1147
+ }
1148
+
1149
+ // 步骤2: 生成其他动态提醒返回给调用方 - 保持现有动态提醒功能
1150
+ const reminderMessages = generateSystemReminders(hasContext, agentId)
1151
+ if (reminderMessages.length > 0) {
1152
+ reminders = reminderMessages.map(r => r.content).join('\n') + '\n'
1153
+ }
1154
+
1155
+ // 步骤3: 添加其他上下文到系统提示
1156
+ enhancedPrompt.push(
1157
+ `\nAs you answer the user's questions, you can use the following context:\n`,
1158
+ )
1159
+
1160
+ // 过滤掉已经由 Kode 上下文处理的项目文档(避免重复)
1161
+ const filteredContext = Object.fromEntries(
1162
+ Object.entries(context).filter(
1163
+ ([key]) => key !== 'projectDocs' && key !== 'userDocs',
1164
+ ),
1165
+ )
1166
+
1167
+ enhancedPrompt.push(
1168
+ ...Object.entries(filteredContext).map(
1169
+ ([key, value]) => `<context name="${key}">${value}</context>`,
1170
+ ),
1171
+ )
1172
+ }
1173
+
1174
+ return { systemPrompt: enhancedPrompt, reminders }
1175
+ }
1176
+
1177
+ async function queryLLMWithPromptCaching(
1178
+ messages: (UserMessage | AssistantMessage)[],
1179
+ systemPrompt: string[],
1180
+ maxThinkingTokens: number,
1181
+ tools: Tool[],
1182
+ signal: AbortSignal,
1183
+ options: {
1184
+ safeMode: boolean
1185
+ model: string
1186
+ prependCLISysprompt: boolean
1187
+ modelProfile?: ModelProfile | null
1188
+ },
1189
+ ): Promise<AssistantMessage> {
1190
+ const config = getGlobalConfig()
1191
+ const modelManager = getModelManager()
1192
+
1193
+ // 🔧 Fix: 使用传入的ModelProfile,而不是硬编码的'main'指针
1194
+ const modelProfile = options.modelProfile || modelManager.getModel('main')
1195
+ let provider: string
1196
+
1197
+ if (modelProfile) {
1198
+ provider = modelProfile.provider || config.primaryProvider || 'anthropic'
1199
+ } else {
1200
+ provider = config.primaryProvider || 'anthropic'
1201
+ }
1202
+
1203
+ // Use native Anthropic SDK for Anthropic and some Anthropic-compatible providers
1204
+ if (
1205
+ provider === 'anthropic' ||
1206
+ provider === 'bigdream' ||
1207
+ provider === 'opendev'
1208
+ ) {
1209
+ return queryAnthropicNative(
1210
+ messages,
1211
+ systemPrompt,
1212
+ maxThinkingTokens,
1213
+ tools,
1214
+ signal,
1215
+ { ...options, modelProfile },
1216
+ )
1217
+ }
1218
+
1219
+ // Use OpenAI-compatible interface for all other providers
1220
+ return queryOpenAI(messages, systemPrompt, maxThinkingTokens, tools, signal, {
1221
+ ...options,
1222
+ modelProfile,
1223
+ })
1224
+ }
1225
+
1226
+ async function queryAnthropicNative(
1227
+ messages: (UserMessage | AssistantMessage)[],
1228
+ systemPrompt: string[],
1229
+ maxThinkingTokens: number,
1230
+ tools: Tool[],
1231
+ signal: AbortSignal,
1232
+ options?: {
1233
+ safeMode: boolean
1234
+ model: string
1235
+ prependCLISysprompt: boolean
1236
+ modelProfile?: ModelProfile | null
1237
+ },
1238
+ ): Promise<AssistantMessage> {
1239
+ const config = getGlobalConfig()
1240
+ const modelManager = getModelManager()
1241
+
1242
+ // 🔧 Fix: 使用传入的ModelProfile,而不是硬编码的'main'指针
1243
+ const modelProfile = options?.modelProfile || modelManager.getModel('main')
1244
+ let anthropic: Anthropic | AnthropicBedrock | AnthropicVertex
1245
+ let model: string
1246
+ let provider: string
1247
+
1248
+ // 🔍 Debug: 记录模型配置详情
1249
+ debugLogger.api('MODEL_CONFIG_ANTHROPIC', {
1250
+ modelProfileFound: !!modelProfile,
1251
+ modelProfileId: modelProfile?.modelName,
1252
+ modelProfileName: modelProfile?.name,
1253
+ modelProfileModelName: modelProfile?.modelName,
1254
+ modelProfileProvider: modelProfile?.provider,
1255
+ modelProfileBaseURL: modelProfile?.baseURL,
1256
+ modelProfileApiKeyExists: !!modelProfile?.apiKey,
1257
+ optionsModel: options?.model,
1258
+ requestId: currentRequest?.id,
1259
+ })
1260
+
1261
+ if (modelProfile) {
1262
+ // 使用ModelProfile的完整配置
1263
+ model = modelProfile.modelName
1264
+ provider = modelProfile.provider || config.primaryProvider || 'anthropic'
1265
+
1266
+ // 基于ModelProfile创建专用的API客户端
1267
+ if (
1268
+ modelProfile.provider === 'anthropic' ||
1269
+ modelProfile.provider === 'bigdream' ||
1270
+ modelProfile.provider === 'opendev'
1271
+ ) {
1272
+ const clientConfig: any = {
1273
+ apiKey: modelProfile.apiKey,
1274
+ dangerouslyAllowBrowser: true,
1275
+ maxRetries: 0,
1276
+ timeout: parseInt(process.env.API_TIMEOUT_MS || String(60 * 1000), 10),
1277
+ defaultHeaders: {
1278
+ 'x-app': 'cli',
1279
+ 'User-Agent': USER_AGENT,
1280
+ },
1281
+ }
1282
+
1283
+ // 使用ModelProfile的baseURL而不是全局配置
1284
+ if (modelProfile.baseURL) {
1285
+ clientConfig.baseURL = modelProfile.baseURL
1286
+ }
1287
+
1288
+ anthropic = new Anthropic(clientConfig)
1289
+ } else {
1290
+ // 其他提供商的处理逻辑
1291
+ anthropic = getAnthropicClient(model)
1292
+ }
1293
+ } else {
1294
+ // 🚨 降级:没有有效的ModelProfile时,应该抛出错误
1295
+ const errorDetails = {
1296
+ modelProfileExists: !!modelProfile,
1297
+ modelProfileModelName: modelProfile?.modelName,
1298
+ requestedModel: options?.model,
1299
+ requestId: currentRequest?.id,
1300
+ }
1301
+ debugLogger.error('ANTHROPIC_FALLBACK_ERROR', errorDetails)
1302
+ throw new Error(
1303
+ `No valid ModelProfile available for Anthropic provider. Please configure model through /model command. Debug: ${JSON.stringify(errorDetails)}`,
1304
+ )
1305
+ }
1306
+
1307
+ // Prepend system prompt block for easy API identification
1308
+ if (options?.prependCLISysprompt) {
1309
+ // Log stats about first block for analyzing prefix matching config
1310
+ const [firstSyspromptBlock] = splitSysPromptPrefix(systemPrompt)
1311
+ logEvent('tengu_sysprompt_block', {
1312
+ snippet: firstSyspromptBlock?.slice(0, 20),
1313
+ length: String(firstSyspromptBlock?.length ?? 0),
1314
+ hash: firstSyspromptBlock
1315
+ ? createHash('sha256').update(firstSyspromptBlock).digest('hex')
1316
+ : '',
1317
+ })
1318
+
1319
+ systemPrompt = [getCLISyspromptPrefix(), ...systemPrompt]
1320
+ }
1321
+
1322
+ const system: TextBlockParam[] = splitSysPromptPrefix(systemPrompt).map(
1323
+ _ => ({
1324
+ ...(PROMPT_CACHING_ENABLED
1325
+ ? { cache_control: { type: 'ephemeral' } }
1326
+ : {}),
1327
+ text: _,
1328
+ type: 'text',
1329
+ }),
1330
+ )
1331
+
1332
+ const toolSchemas = tools.map(
1333
+ tool =>
1334
+ ({
1335
+ name: tool.name,
1336
+ description: tool.description,
1337
+ input_schema: zodToJsonSchema(tool.inputSchema),
1338
+ }) as Anthropic.Beta.Tools.Tool,
1339
+ )
1340
+
1341
+ const anthropicMessages = addCacheBreakpoints(messages)
1342
+ const startIncludingRetries = Date.now()
1343
+
1344
+ // 记录系统提示构建过程
1345
+ logSystemPromptConstruction({
1346
+ basePrompt: systemPrompt.join('\n'),
1347
+ kodeContext: generateKodeContext() || '',
1348
+ reminders: [], // 这里可以从 generateSystemReminders 获取
1349
+ finalPrompt: systemPrompt.join('\n'),
1350
+ })
1351
+
1352
+ let start = Date.now()
1353
+ let attemptNumber = 0
1354
+ let response
1355
+
1356
+ try {
1357
+ response = await withRetry(async attempt => {
1358
+ attemptNumber = attempt
1359
+ start = Date.now()
1360
+
1361
+ const params: Anthropic.Beta.Messages.MessageCreateParams = {
1362
+ model,
1363
+ max_tokens: getMaxTokensFromProfile(modelProfile),
1364
+ messages: anthropicMessages,
1365
+ system,
1366
+ tools: toolSchemas.length > 0 ? toolSchemas : undefined,
1367
+ tool_choice: toolSchemas.length > 0 ? { type: 'auto' } : undefined,
1368
+ }
1369
+
1370
+ if (maxThinkingTokens > 0) {
1371
+ params.extra_headers = {
1372
+ 'anthropic-beta': 'max-tokens-3-5-sonnet-2024-07-15',
1373
+ }
1374
+ ;(params as any).thinking = { max_tokens: maxThinkingTokens }
1375
+ }
1376
+
1377
+ // 🔥 REAL-TIME API CALL DEBUG - 使用全局日志系统 (Anthropic Streaming)
1378
+ debugLogger.api('ANTHROPIC_API_CALL_START_STREAMING', {
1379
+ endpoint: modelProfile?.baseURL || 'DEFAULT_ANTHROPIC',
1380
+ model,
1381
+ provider,
1382
+ apiKeyConfigured: !!modelProfile?.apiKey,
1383
+ apiKeyPrefix: modelProfile?.apiKey
1384
+ ? modelProfile.apiKey.substring(0, 8)
1385
+ : null,
1386
+ maxTokens: params.max_tokens,
1387
+ temperature: MAIN_QUERY_TEMPERATURE,
1388
+ messageCount: params.messages?.length || 0,
1389
+ streamMode: true,
1390
+ toolsCount: toolSchemas.length,
1391
+ thinkingTokens: maxThinkingTokens,
1392
+ timestamp: new Date().toISOString(),
1393
+ modelProfileId: modelProfile?.modelName,
1394
+ modelProfileName: modelProfile?.name,
1395
+ })
1396
+
1397
+ if (config.stream) {
1398
+ // 🔧 CRITICAL FIX: Connect AbortSignal to Anthropic API call
1399
+ const stream = await anthropic.beta.messages.create({
1400
+ ...params,
1401
+ stream: true,
1402
+ }, {
1403
+ signal: signal // ← CRITICAL: Connect the AbortSignal to API call
1404
+ })
1405
+
1406
+ let finalResponse: Anthropic.Beta.Messages.Message | null = null
1407
+ let messageStartEvent: any = null
1408
+ const contentBlocks: any[] = []
1409
+ let usage: any = null
1410
+ let stopReason: string | null = null
1411
+ let stopSequence: string | null = null
1412
+
1413
+ for await (const event of stream) {
1414
+ // 🔧 CRITICAL FIX: Check abort signal in streaming loop
1415
+ if (signal.aborted) {
1416
+ debugLogger.flow('STREAM_ABORTED', {
1417
+ eventType: event.type,
1418
+ timestamp: Date.now()
1419
+ })
1420
+ throw new Error('Request was cancelled')
1421
+ }
1422
+ if (event.type === 'message_start') {
1423
+ messageStartEvent = event
1424
+ finalResponse = {
1425
+ ...event.message,
1426
+ content: [], // Will be populated from content blocks
1427
+ }
1428
+ } else if (event.type === 'content_block_start') {
1429
+ contentBlocks[event.index] = { ...event.content_block }
1430
+ } else if (event.type === 'content_block_delta') {
1431
+ if (!contentBlocks[event.index]) {
1432
+ contentBlocks[event.index] = {
1433
+ type: event.delta.type === 'text_delta' ? 'text' : 'unknown',
1434
+ text: '',
1435
+ }
1436
+ }
1437
+ if (event.delta.type === 'text_delta') {
1438
+ contentBlocks[event.index].text += event.delta.text
1439
+ }
1440
+ } else if (event.type === 'message_delta') {
1441
+ if (event.delta.stop_reason) stopReason = event.delta.stop_reason
1442
+ if (event.delta.stop_sequence)
1443
+ stopSequence = event.delta.stop_sequence
1444
+ if (event.usage) usage = { ...usage, ...event.usage }
1445
+ } else if (event.type === 'message_stop') {
1446
+ break
1447
+ }
1448
+ }
1449
+
1450
+ if (!finalResponse || !messageStartEvent) {
1451
+ throw new Error('Stream ended without proper message structure')
1452
+ }
1453
+
1454
+ // Construct the final response
1455
+ finalResponse = {
1456
+ ...messageStartEvent.message,
1457
+ content: contentBlocks.filter(Boolean),
1458
+ stop_reason: stopReason,
1459
+ stop_sequence: stopSequence,
1460
+ usage: {
1461
+ ...messageStartEvent.message.usage,
1462
+ ...usage,
1463
+ },
1464
+ }
1465
+
1466
+ return finalResponse
1467
+ } else {
1468
+ // 🔥 REAL-TIME API CALL DEBUG - 使用全局日志系统 (Anthropic Non-Streaming)
1469
+ debugLogger.api('ANTHROPIC_API_CALL_START_NON_STREAMING', {
1470
+ endpoint: modelProfile?.baseURL || 'DEFAULT_ANTHROPIC',
1471
+ model,
1472
+ provider,
1473
+ apiKeyConfigured: !!modelProfile?.apiKey,
1474
+ apiKeyPrefix: modelProfile?.apiKey
1475
+ ? modelProfile.apiKey.substring(0, 8)
1476
+ : null,
1477
+ maxTokens: params.max_tokens,
1478
+ temperature: MAIN_QUERY_TEMPERATURE,
1479
+ messageCount: params.messages?.length || 0,
1480
+ streamMode: false,
1481
+ toolsCount: toolSchemas.length,
1482
+ thinkingTokens: maxThinkingTokens,
1483
+ timestamp: new Date().toISOString(),
1484
+ modelProfileId: modelProfile?.modelName,
1485
+ modelProfileName: modelProfile?.name,
1486
+ })
1487
+
1488
+ // 🔧 CRITICAL FIX: Connect AbortSignal to non-streaming API call
1489
+ return await anthropic.beta.messages.create(params, {
1490
+ signal: signal // ← CRITICAL: Connect the AbortSignal to API call
1491
+ })
1492
+ }
1493
+ }, { signal }) // 🔧 CRITICAL FIX: Pass AbortSignal to withRetry
1494
+
1495
+ const ttftMs = start - Date.now()
1496
+ const durationMs = Date.now() - startIncludingRetries
1497
+
1498
+ const content = response.content.map((block: ContentBlock) => {
1499
+ if (block.type === 'text') {
1500
+ return {
1501
+ type: 'text' as const,
1502
+ text: block.text,
1503
+ }
1504
+ } else if (block.type === 'tool_use') {
1505
+ return {
1506
+ type: 'tool_use' as const,
1507
+ id: block.id,
1508
+ name: block.name,
1509
+ input: block.input,
1510
+ }
1511
+ }
1512
+ return block
1513
+ })
1514
+
1515
+ const assistantMessage: AssistantMessage = {
1516
+ message: {
1517
+ id: response.id,
1518
+ content,
1519
+ model: response.model,
1520
+ role: 'assistant',
1521
+ stop_reason: response.stop_reason,
1522
+ stop_sequence: response.stop_sequence,
1523
+ type: 'message',
1524
+ usage: response.usage,
1525
+ },
1526
+ type: 'assistant',
1527
+ uuid: nanoid() as UUID,
1528
+ ttftMs,
1529
+ durationMs,
1530
+ costUSD: 0, // Will be calculated below
1531
+ }
1532
+
1533
+ // 记录完整的 LLM 交互调试信息 (Anthropic path)
1534
+ // 注意:Anthropic API将system prompt和messages分开,这里重构为完整的API调用视图
1535
+ const systemMessages = system.map(block => ({
1536
+ role: 'system',
1537
+ content: block.text,
1538
+ }))
1539
+
1540
+ logLLMInteraction({
1541
+ systemPrompt: systemPrompt.join('\n'),
1542
+ messages: [...systemMessages, ...anthropicMessages],
1543
+ response: response,
1544
+ usage: response.usage
1545
+ ? {
1546
+ inputTokens: response.usage.input_tokens,
1547
+ outputTokens: response.usage.output_tokens,
1548
+ }
1549
+ : undefined,
1550
+ timing: {
1551
+ start: start,
1552
+ end: Date.now(),
1553
+ },
1554
+ apiFormat: 'anthropic',
1555
+ modelConfig: getModelConfigForDebug(model),
1556
+ })
1557
+
1558
+ // Calculate cost using native Anthropic usage data
1559
+ const inputTokens = response.usage.input_tokens
1560
+ const outputTokens = response.usage.output_tokens
1561
+ const cacheCreationInputTokens =
1562
+ response.usage.cache_creation_input_tokens ?? 0
1563
+ const cacheReadInputTokens = response.usage.cache_read_input_tokens ?? 0
1564
+
1565
+ const costUSD =
1566
+ (inputTokens / 1_000_000) * getModelInputTokenCostUSD(model) +
1567
+ (outputTokens / 1_000_000) * getModelOutputTokenCostUSD(model) +
1568
+ (cacheCreationInputTokens / 1_000_000) *
1569
+ getModelInputTokenCostUSD(model) +
1570
+ (cacheReadInputTokens / 1_000_000) *
1571
+ (getModelInputTokenCostUSD(model) * 0.1) // Cache reads are 10% of input cost
1572
+
1573
+ assistantMessage.costUSD = costUSD
1574
+ addToTotalCost(costUSD)
1575
+
1576
+ logEvent('api_response_anthropic_native', {
1577
+ model,
1578
+ input_tokens: inputTokens,
1579
+ output_tokens: outputTokens,
1580
+ cache_creation_input_tokens: cacheCreationInputTokens,
1581
+ cache_read_input_tokens: cacheReadInputTokens,
1582
+ cost_usd: costUSD,
1583
+ duration_ms: durationMs,
1584
+ ttft_ms: ttftMs,
1585
+ attempt_number: attemptNumber,
1586
+ })
1587
+
1588
+ return assistantMessage
1589
+ } catch (error) {
1590
+ return getAssistantMessageFromError(error)
1591
+ }
1592
+ }
1593
+
1594
+ function getAssistantMessageFromError(error: unknown): AssistantMessage {
1595
+ if (error instanceof Error && error.message.includes('prompt is too long')) {
1596
+ return createAssistantAPIErrorMessage(PROMPT_TOO_LONG_ERROR_MESSAGE)
1597
+ }
1598
+ if (
1599
+ error instanceof Error &&
1600
+ error.message.includes('Your credit balance is too low')
1601
+ ) {
1602
+ return createAssistantAPIErrorMessage(CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE)
1603
+ }
1604
+ if (
1605
+ error instanceof Error &&
1606
+ error.message.toLowerCase().includes('x-api-key')
1607
+ ) {
1608
+ return createAssistantAPIErrorMessage(INVALID_API_KEY_ERROR_MESSAGE)
1609
+ }
1610
+ if (error instanceof Error) {
1611
+ if (process.env.NODE_ENV === 'development') {
1612
+ console.log(error)
1613
+ }
1614
+ return createAssistantAPIErrorMessage(
1615
+ `${API_ERROR_MESSAGE_PREFIX}: ${error.message}`,
1616
+ )
1617
+ }
1618
+ return createAssistantAPIErrorMessage(API_ERROR_MESSAGE_PREFIX)
1619
+ }
1620
+
1621
+ function addCacheBreakpoints(
1622
+ messages: (UserMessage | AssistantMessage)[],
1623
+ ): MessageParam[] {
1624
+ return messages.map((msg, index) => {
1625
+ return msg.type === 'user'
1626
+ ? userMessageToMessageParam(msg, index > messages.length - 3)
1627
+ : assistantMessageToMessageParam(msg, index > messages.length - 3)
1628
+ })
1629
+ }
1630
+
1631
+ async function queryOpenAI(
1632
+ messages: (UserMessage | AssistantMessage)[],
1633
+ systemPrompt: string[],
1634
+ maxThinkingTokens: number,
1635
+ tools: Tool[],
1636
+ signal: AbortSignal,
1637
+ options?: {
1638
+ safeMode: boolean
1639
+ model: string
1640
+ prependCLISysprompt: boolean
1641
+ modelProfile?: ModelProfile | null
1642
+ },
1643
+ ): Promise<AssistantMessage> {
1644
+ const config = getGlobalConfig()
1645
+ const modelManager = getModelManager()
1646
+
1647
+ // 🔧 Fix: 使用传入的ModelProfile,而不是硬编码的'main'指针
1648
+ const modelProfile = options?.modelProfile || modelManager.getModel('main')
1649
+ let model: string
1650
+
1651
+ // 🔍 Debug: 记录模型配置详情
1652
+ const currentRequest = getCurrentRequest()
1653
+ debugLogger.api('MODEL_CONFIG_OPENAI', {
1654
+ modelProfileFound: !!modelProfile,
1655
+ modelProfileId: modelProfile?.modelName,
1656
+ modelProfileName: modelProfile?.name,
1657
+ modelProfileModelName: modelProfile?.modelName,
1658
+ modelProfileProvider: modelProfile?.provider,
1659
+ modelProfileBaseURL: modelProfile?.baseURL,
1660
+ modelProfileApiKeyExists: !!modelProfile?.apiKey,
1661
+ optionsModel: options?.model,
1662
+ requestId: currentRequest?.id,
1663
+ })
1664
+
1665
+ if (modelProfile) {
1666
+ model = modelProfile.modelName
1667
+ } else {
1668
+ model = options?.model || modelProfile?.modelName || ''
1669
+ }
1670
+ // Prepend system prompt block for easy API identification
1671
+ if (options?.prependCLISysprompt) {
1672
+ // Log stats about first block for analyzing prefix matching config (see https://console.statsig.com/4aF3Ewatb6xPVpCwxb5nA3/dynamic_configs/claude_cli_system_prompt_prefixes)
1673
+ const [firstSyspromptBlock] = splitSysPromptPrefix(systemPrompt)
1674
+ logEvent('tengu_sysprompt_block', {
1675
+ snippet: firstSyspromptBlock?.slice(0, 20),
1676
+ length: String(firstSyspromptBlock?.length ?? 0),
1677
+ hash: firstSyspromptBlock
1678
+ ? createHash('sha256').update(firstSyspromptBlock).digest('hex')
1679
+ : '',
1680
+ })
1681
+
1682
+ systemPrompt = [getCLISyspromptPrefix(), ...systemPrompt]
1683
+ }
1684
+
1685
+ const system: TextBlockParam[] = splitSysPromptPrefix(systemPrompt).map(
1686
+ _ => ({
1687
+ ...(PROMPT_CACHING_ENABLED
1688
+ ? { cache_control: { type: 'ephemeral' } }
1689
+ : {}),
1690
+ text: _,
1691
+ type: 'text',
1692
+ }),
1693
+ )
1694
+
1695
+ const toolSchemas = await Promise.all(
1696
+ tools.map(
1697
+ async _ =>
1698
+ ({
1699
+ type: 'function',
1700
+ function: {
1701
+ name: _.name,
1702
+ description: await _.prompt({
1703
+ safeMode: options?.safeMode,
1704
+ }),
1705
+ // Use tool's JSON schema directly if provided, otherwise convert Zod schema
1706
+ parameters:
1707
+ 'inputJSONSchema' in _ && _.inputJSONSchema
1708
+ ? _.inputJSONSchema
1709
+ : zodToJsonSchema(_.inputSchema),
1710
+ },
1711
+ }) as OpenAI.ChatCompletionTool,
1712
+ ),
1713
+ )
1714
+
1715
+ const openaiSystem = system.map(
1716
+ s =>
1717
+ ({
1718
+ role: 'system',
1719
+ content: s.text,
1720
+ }) as OpenAI.ChatCompletionMessageParam,
1721
+ )
1722
+
1723
+ const openaiMessages = convertAnthropicMessagesToOpenAIMessages(messages)
1724
+ const startIncludingRetries = Date.now()
1725
+
1726
+ // 记录系统提示构建过程 (OpenAI path)
1727
+ logSystemPromptConstruction({
1728
+ basePrompt: systemPrompt.join('\n'),
1729
+ kodeContext: generateKodeContext() || '',
1730
+ reminders: [], // 这里可以从 generateSystemReminders 获取
1731
+ finalPrompt: systemPrompt.join('\n'),
1732
+ })
1733
+
1734
+ let start = Date.now()
1735
+ let attemptNumber = 0
1736
+ let response
1737
+
1738
+ try {
1739
+ response = await withRetry(async attempt => {
1740
+ attemptNumber = attempt
1741
+ start = Date.now()
1742
+ const opts: OpenAI.ChatCompletionCreateParams = {
1743
+ model,
1744
+ max_tokens: getMaxTokensFromProfile(modelProfile),
1745
+ messages: [...openaiSystem, ...openaiMessages],
1746
+ temperature: MAIN_QUERY_TEMPERATURE,
1747
+ }
1748
+ if (config.stream) {
1749
+ ;(opts as OpenAI.ChatCompletionCreateParams).stream = true
1750
+ opts.stream_options = {
1751
+ include_usage: true,
1752
+ }
1753
+ }
1754
+
1755
+ if (toolSchemas.length > 0) {
1756
+ opts.tools = toolSchemas
1757
+ opts.tool_choice = 'auto'
1758
+ }
1759
+ const reasoningEffort = await getReasoningEffort(modelProfile, messages)
1760
+ if (reasoningEffort) {
1761
+ logEvent('debug_reasoning_effort', {
1762
+ effort: reasoningEffort,
1763
+ })
1764
+ opts.reasoning_effort = reasoningEffort
1765
+ }
1766
+
1767
+ // 🔧 Fix: 如果有ModelProfile配置,直接使用它 (更宽松的条件)
1768
+ if (modelProfile && modelProfile.modelName) {
1769
+ debugLogger.api('USING_MODEL_PROFILE_PATH', {
1770
+ modelProfileName: modelProfile.modelName,
1771
+ modelName: modelProfile.modelName,
1772
+ provider: modelProfile.provider,
1773
+ baseURL: modelProfile.baseURL,
1774
+ apiKeyExists: !!modelProfile.apiKey,
1775
+ requestId: currentRequest?.id,
1776
+ })
1777
+
1778
+ const s = await getCompletionWithProfile(modelProfile, opts, 0, 10, signal) // 🔧 CRITICAL FIX: Pass AbortSignal to OpenAI calls
1779
+ let finalResponse
1780
+ if (opts.stream) {
1781
+ finalResponse = await handleMessageStream(s as ChatCompletionStream, signal) // 🔧 Pass AbortSignal to stream handler
1782
+ } else {
1783
+ finalResponse = s
1784
+ }
1785
+
1786
+ const r = convertOpenAIResponseToAnthropic(finalResponse)
1787
+ return r
1788
+ } else {
1789
+ // 🚨 警告:ModelProfile不可用,使用旧逻辑路径
1790
+ debugLogger.api('USING_LEGACY_PATH', {
1791
+ modelProfileExists: !!modelProfile,
1792
+ modelProfileId: modelProfile?.modelName,
1793
+ modelNameExists: !!modelProfile?.modelName,
1794
+ fallbackModel: 'main',
1795
+ actualModel: model,
1796
+ requestId: currentRequest?.id,
1797
+ })
1798
+
1799
+ // 🚨 FALLBACK: 没有有效的ModelProfile时,应该抛出错误而不是使用遗留系统
1800
+ const errorDetails = {
1801
+ modelProfileExists: !!modelProfile,
1802
+ modelProfileId: modelProfile?.modelName,
1803
+ modelNameExists: !!modelProfile?.modelName,
1804
+ requestedModel: model,
1805
+ requestId: currentRequest?.id,
1806
+ }
1807
+ debugLogger.error('NO_VALID_MODEL_PROFILE', errorDetails)
1808
+ throw new Error(
1809
+ `No valid ModelProfile available for model: ${model}. Please configure model through /model command. Debug: ${JSON.stringify(errorDetails)}`,
1810
+ )
1811
+ }
1812
+ }, { signal }) // 🔧 CRITICAL FIX: Pass AbortSignal to withRetry
1813
+ } catch (error) {
1814
+ logError(error)
1815
+ return getAssistantMessageFromError(error)
1816
+ }
1817
+ const durationMs = Date.now() - start
1818
+ const durationMsIncludingRetries = Date.now() - startIncludingRetries
1819
+
1820
+ const inputTokens = response.usage?.prompt_tokens ?? 0
1821
+ const outputTokens = response.usage?.completion_tokens ?? 0
1822
+ const cacheReadInputTokens =
1823
+ response.usage?.prompt_token_details?.cached_tokens ?? 0
1824
+ const cacheCreationInputTokens =
1825
+ response.usage?.prompt_token_details?.cached_tokens ?? 0
1826
+ const costUSD =
1827
+ (inputTokens / 1_000_000) * SONNET_COST_PER_MILLION_INPUT_TOKENS +
1828
+ (outputTokens / 1_000_000) * SONNET_COST_PER_MILLION_OUTPUT_TOKENS +
1829
+ (cacheReadInputTokens / 1_000_000) *
1830
+ SONNET_COST_PER_MILLION_PROMPT_CACHE_READ_TOKENS +
1831
+ (cacheCreationInputTokens / 1_000_000) *
1832
+ SONNET_COST_PER_MILLION_PROMPT_CACHE_WRITE_TOKENS
1833
+
1834
+ addToTotalCost(costUSD, durationMsIncludingRetries)
1835
+
1836
+ // 记录完整的 LLM 交互调试信息 (OpenAI path)
1837
+ logLLMInteraction({
1838
+ systemPrompt: systemPrompt.join('\n'),
1839
+ messages: [...openaiSystem, ...openaiMessages],
1840
+ response: response,
1841
+ usage: {
1842
+ inputTokens: inputTokens,
1843
+ outputTokens: outputTokens,
1844
+ },
1845
+ timing: {
1846
+ start: start,
1847
+ end: Date.now(),
1848
+ },
1849
+ apiFormat: 'openai',
1850
+ modelConfig: getModelConfigForDebug(model),
1851
+ })
1852
+
1853
+ return {
1854
+ message: {
1855
+ ...response,
1856
+ content: normalizeContentFromAPI(response.content),
1857
+ usage: {
1858
+ input_tokens: inputTokens,
1859
+ output_tokens: outputTokens,
1860
+ cache_read_input_tokens: cacheReadInputTokens,
1861
+ cache_creation_input_tokens: 0,
1862
+ },
1863
+ },
1864
+ costUSD,
1865
+ durationMs,
1866
+ type: 'assistant',
1867
+ uuid: randomUUID(),
1868
+ }
1869
+ }
1870
+
1871
+ function getMaxTokensFromProfile(modelProfile: any): number {
1872
+ // Use ModelProfile maxTokens or reasonable default
1873
+ return modelProfile?.maxTokens || 8000
1874
+ }
1875
+
1876
+ function getModelInputTokenCostUSD(model: string): number {
1877
+ // Find the model in the models object
1878
+ for (const providerModels of Object.values(models)) {
1879
+ const modelInfo = providerModels.find((m: any) => m.model === model)
1880
+ if (modelInfo) {
1881
+ return modelInfo.input_cost_per_token || 0
1882
+ }
1883
+ }
1884
+ // Default fallback cost for unknown models
1885
+ return 0.000003 // Default to Claude 3 Haiku cost
1886
+ }
1887
+
1888
+ function getModelOutputTokenCostUSD(model: string): number {
1889
+ // Find the model in the models object
1890
+ for (const providerModels of Object.values(models)) {
1891
+ const modelInfo = providerModels.find((m: any) => m.model === model)
1892
+ if (modelInfo) {
1893
+ return modelInfo.output_cost_per_token || 0
1894
+ }
1895
+ }
1896
+ // Default fallback cost for unknown models
1897
+ return 0.000015 // Default to Claude 3 Haiku cost
1898
+ }
1899
+
1900
+ // New unified query functions for model pointer system
1901
+ export async function queryModel(
1902
+ modelPointer: import('../utils/config').ModelPointerType,
1903
+ messages: (UserMessage | AssistantMessage)[],
1904
+ systemPrompt: string[] = [],
1905
+ signal?: AbortSignal,
1906
+ ): Promise<AssistantMessage> {
1907
+ // Use queryLLM with the pointer directly
1908
+ return queryLLM(
1909
+ messages,
1910
+ systemPrompt,
1911
+ 0, // maxThinkingTokens
1912
+ [], // tools
1913
+ signal || new AbortController().signal,
1914
+ {
1915
+ safeMode: false,
1916
+ model: modelPointer,
1917
+ prependCLISysprompt: true,
1918
+ },
1919
+ )
1920
+ }
1921
+
1922
+ // Note: Use queryModel(pointer, ...) directly instead of these convenience functions
1923
+
1924
+ // Simplified query function using quick model pointer
1925
+ export async function queryQuick({
1926
+ systemPrompt = [],
1927
+ userPrompt,
1928
+ assistantPrompt,
1929
+ enablePromptCaching = false,
1930
+ signal,
1931
+ }: {
1932
+ systemPrompt?: string[]
1933
+ userPrompt: string
1934
+ assistantPrompt?: string
1935
+ enablePromptCaching?: boolean
1936
+ signal?: AbortSignal
1937
+ }): Promise<AssistantMessage> {
1938
+ const messages = [
1939
+ {
1940
+ message: { role: 'user', content: userPrompt },
1941
+ type: 'user',
1942
+ uuid: randomUUID(),
1943
+ },
1944
+ ] as (UserMessage | AssistantMessage)[]
1945
+
1946
+ return queryModel('quick', messages, systemPrompt, 0, [], signal)
1947
+ }