@shareai-lab/kode 1.0.70 → 1.0.73

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