@shareai-lab/kode 1.0.9

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 (286) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +426 -0
  3. package/README.zh-CN.md +326 -0
  4. package/cli.js +79 -0
  5. package/package.json +119 -0
  6. package/scripts/postinstall.js +18 -0
  7. package/src/ProjectOnboarding.tsx +198 -0
  8. package/src/Tool.ts +82 -0
  9. package/src/commands/agents.tsx +3410 -0
  10. package/src/commands/approvedTools.ts +53 -0
  11. package/src/commands/bug.tsx +20 -0
  12. package/src/commands/clear.ts +43 -0
  13. package/src/commands/compact.ts +120 -0
  14. package/src/commands/config.tsx +19 -0
  15. package/src/commands/cost.ts +18 -0
  16. package/src/commands/ctx_viz.ts +209 -0
  17. package/src/commands/doctor.ts +24 -0
  18. package/src/commands/help.tsx +19 -0
  19. package/src/commands/init.ts +37 -0
  20. package/src/commands/listen.ts +42 -0
  21. package/src/commands/login.tsx +51 -0
  22. package/src/commands/logout.tsx +40 -0
  23. package/src/commands/mcp.ts +41 -0
  24. package/src/commands/model.tsx +40 -0
  25. package/src/commands/modelstatus.tsx +20 -0
  26. package/src/commands/onboarding.tsx +34 -0
  27. package/src/commands/pr_comments.ts +59 -0
  28. package/src/commands/refreshCommands.ts +54 -0
  29. package/src/commands/release-notes.ts +34 -0
  30. package/src/commands/resume.tsx +31 -0
  31. package/src/commands/review.ts +49 -0
  32. package/src/commands/terminalSetup.ts +221 -0
  33. package/src/commands.ts +139 -0
  34. package/src/components/ApproveApiKey.tsx +93 -0
  35. package/src/components/AsciiLogo.tsx +13 -0
  36. package/src/components/AutoUpdater.tsx +148 -0
  37. package/src/components/Bug.tsx +367 -0
  38. package/src/components/Config.tsx +293 -0
  39. package/src/components/ConsoleOAuthFlow.tsx +327 -0
  40. package/src/components/Cost.tsx +23 -0
  41. package/src/components/CostThresholdDialog.tsx +46 -0
  42. package/src/components/CustomSelect/option-map.ts +42 -0
  43. package/src/components/CustomSelect/select-option.tsx +78 -0
  44. package/src/components/CustomSelect/select.tsx +152 -0
  45. package/src/components/CustomSelect/theme.ts +45 -0
  46. package/src/components/CustomSelect/use-select-state.ts +414 -0
  47. package/src/components/CustomSelect/use-select.ts +35 -0
  48. package/src/components/FallbackToolUseRejectedMessage.tsx +15 -0
  49. package/src/components/FileEditToolUpdatedMessage.tsx +66 -0
  50. package/src/components/Help.tsx +215 -0
  51. package/src/components/HighlightedCode.tsx +33 -0
  52. package/src/components/InvalidConfigDialog.tsx +113 -0
  53. package/src/components/Link.tsx +32 -0
  54. package/src/components/LogSelector.tsx +86 -0
  55. package/src/components/Logo.tsx +170 -0
  56. package/src/components/MCPServerApprovalDialog.tsx +100 -0
  57. package/src/components/MCPServerDialogCopy.tsx +25 -0
  58. package/src/components/MCPServerMultiselectDialog.tsx +109 -0
  59. package/src/components/Message.tsx +221 -0
  60. package/src/components/MessageResponse.tsx +15 -0
  61. package/src/components/MessageSelector.tsx +211 -0
  62. package/src/components/ModeIndicator.tsx +88 -0
  63. package/src/components/ModelConfig.tsx +301 -0
  64. package/src/components/ModelListManager.tsx +227 -0
  65. package/src/components/ModelSelector.tsx +3387 -0
  66. package/src/components/ModelStatusDisplay.tsx +230 -0
  67. package/src/components/Onboarding.tsx +274 -0
  68. package/src/components/PressEnterToContinue.tsx +11 -0
  69. package/src/components/PromptInput.tsx +760 -0
  70. package/src/components/SentryErrorBoundary.ts +39 -0
  71. package/src/components/Spinner.tsx +129 -0
  72. package/src/components/StickerRequestForm.tsx +16 -0
  73. package/src/components/StructuredDiff.tsx +191 -0
  74. package/src/components/TextInput.tsx +259 -0
  75. package/src/components/TodoItem.tsx +47 -0
  76. package/src/components/TokenWarning.tsx +31 -0
  77. package/src/components/ToolUseLoader.tsx +40 -0
  78. package/src/components/TrustDialog.tsx +106 -0
  79. package/src/components/binary-feedback/BinaryFeedback.tsx +63 -0
  80. package/src/components/binary-feedback/BinaryFeedbackOption.tsx +111 -0
  81. package/src/components/binary-feedback/BinaryFeedbackView.tsx +172 -0
  82. package/src/components/binary-feedback/utils.ts +220 -0
  83. package/src/components/messages/AssistantBashOutputMessage.tsx +22 -0
  84. package/src/components/messages/AssistantLocalCommandOutputMessage.tsx +49 -0
  85. package/src/components/messages/AssistantRedactedThinkingMessage.tsx +19 -0
  86. package/src/components/messages/AssistantTextMessage.tsx +144 -0
  87. package/src/components/messages/AssistantThinkingMessage.tsx +40 -0
  88. package/src/components/messages/AssistantToolUseMessage.tsx +133 -0
  89. package/src/components/messages/TaskProgressMessage.tsx +32 -0
  90. package/src/components/messages/TaskToolMessage.tsx +58 -0
  91. package/src/components/messages/UserBashInputMessage.tsx +28 -0
  92. package/src/components/messages/UserCommandMessage.tsx +30 -0
  93. package/src/components/messages/UserKodingInputMessage.tsx +28 -0
  94. package/src/components/messages/UserPromptMessage.tsx +35 -0
  95. package/src/components/messages/UserTextMessage.tsx +39 -0
  96. package/src/components/messages/UserToolResultMessage/UserToolCanceledMessage.tsx +12 -0
  97. package/src/components/messages/UserToolResultMessage/UserToolErrorMessage.tsx +36 -0
  98. package/src/components/messages/UserToolResultMessage/UserToolRejectMessage.tsx +31 -0
  99. package/src/components/messages/UserToolResultMessage/UserToolResultMessage.tsx +57 -0
  100. package/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx +35 -0
  101. package/src/components/messages/UserToolResultMessage/utils.tsx +56 -0
  102. package/src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx +121 -0
  103. package/src/components/permissions/FallbackPermissionRequest.tsx +153 -0
  104. package/src/components/permissions/FileEditPermissionRequest/FileEditPermissionRequest.tsx +182 -0
  105. package/src/components/permissions/FileEditPermissionRequest/FileEditToolDiff.tsx +77 -0
  106. package/src/components/permissions/FileWritePermissionRequest/FileWritePermissionRequest.tsx +164 -0
  107. package/src/components/permissions/FileWritePermissionRequest/FileWriteToolDiff.tsx +83 -0
  108. package/src/components/permissions/FilesystemPermissionRequest/FilesystemPermissionRequest.tsx +240 -0
  109. package/src/components/permissions/PermissionRequest.tsx +101 -0
  110. package/src/components/permissions/PermissionRequestTitle.tsx +69 -0
  111. package/src/components/permissions/hooks.ts +44 -0
  112. package/src/components/permissions/toolUseOptions.ts +59 -0
  113. package/src/components/permissions/utils.ts +23 -0
  114. package/src/constants/betas.ts +5 -0
  115. package/src/constants/claude-asterisk-ascii-art.tsx +238 -0
  116. package/src/constants/figures.ts +4 -0
  117. package/src/constants/keys.ts +3 -0
  118. package/src/constants/macros.ts +8 -0
  119. package/src/constants/modelCapabilities.ts +179 -0
  120. package/src/constants/models.ts +1025 -0
  121. package/src/constants/oauth.ts +18 -0
  122. package/src/constants/product.ts +17 -0
  123. package/src/constants/prompts.ts +168 -0
  124. package/src/constants/releaseNotes.ts +7 -0
  125. package/src/context/PermissionContext.tsx +149 -0
  126. package/src/context.ts +278 -0
  127. package/src/cost-tracker.ts +84 -0
  128. package/src/entrypoints/cli.tsx +1542 -0
  129. package/src/entrypoints/mcp.ts +176 -0
  130. package/src/history.ts +25 -0
  131. package/src/hooks/useApiKeyVerification.ts +59 -0
  132. package/src/hooks/useArrowKeyHistory.ts +55 -0
  133. package/src/hooks/useCanUseTool.ts +138 -0
  134. package/src/hooks/useCancelRequest.ts +39 -0
  135. package/src/hooks/useDoublePress.ts +42 -0
  136. package/src/hooks/useExitOnCtrlCD.ts +31 -0
  137. package/src/hooks/useInterval.ts +25 -0
  138. package/src/hooks/useLogMessages.ts +16 -0
  139. package/src/hooks/useLogStartupTime.ts +12 -0
  140. package/src/hooks/useNotifyAfterTimeout.ts +65 -0
  141. package/src/hooks/usePermissionRequestLogging.ts +44 -0
  142. package/src/hooks/useTerminalSize.ts +49 -0
  143. package/src/hooks/useTextInput.ts +318 -0
  144. package/src/hooks/useUnifiedCompletion.ts +1405 -0
  145. package/src/messages.ts +38 -0
  146. package/src/permissions.ts +268 -0
  147. package/src/query.ts +715 -0
  148. package/src/screens/ConfigureNpmPrefix.tsx +197 -0
  149. package/src/screens/Doctor.tsx +219 -0
  150. package/src/screens/LogList.tsx +68 -0
  151. package/src/screens/REPL.tsx +809 -0
  152. package/src/screens/ResumeConversation.tsx +68 -0
  153. package/src/services/adapters/base.ts +38 -0
  154. package/src/services/adapters/chatCompletions.ts +90 -0
  155. package/src/services/adapters/responsesAPI.ts +170 -0
  156. package/src/services/browserMocks.ts +66 -0
  157. package/src/services/claude.ts +2197 -0
  158. package/src/services/customCommands.ts +704 -0
  159. package/src/services/fileFreshness.ts +377 -0
  160. package/src/services/gpt5ConnectionTest.ts +340 -0
  161. package/src/services/mcpClient.ts +564 -0
  162. package/src/services/mcpServerApproval.tsx +50 -0
  163. package/src/services/mentionProcessor.ts +273 -0
  164. package/src/services/modelAdapterFactory.ts +69 -0
  165. package/src/services/notifier.ts +40 -0
  166. package/src/services/oauth.ts +357 -0
  167. package/src/services/openai.ts +1338 -0
  168. package/src/services/responseStateManager.ts +90 -0
  169. package/src/services/sentry.ts +3 -0
  170. package/src/services/statsig.ts +172 -0
  171. package/src/services/statsigStorage.ts +86 -0
  172. package/src/services/systemReminder.ts +507 -0
  173. package/src/services/vcr.ts +161 -0
  174. package/src/test/testAdapters.ts +96 -0
  175. package/src/tools/ArchitectTool/ArchitectTool.tsx +122 -0
  176. package/src/tools/ArchitectTool/prompt.ts +15 -0
  177. package/src/tools/AskExpertModelTool/AskExpertModelTool.tsx +569 -0
  178. package/src/tools/BashTool/BashTool.tsx +243 -0
  179. package/src/tools/BashTool/BashToolResultMessage.tsx +38 -0
  180. package/src/tools/BashTool/OutputLine.tsx +49 -0
  181. package/src/tools/BashTool/prompt.ts +174 -0
  182. package/src/tools/BashTool/utils.ts +56 -0
  183. package/src/tools/FileEditTool/FileEditTool.tsx +315 -0
  184. package/src/tools/FileEditTool/prompt.ts +51 -0
  185. package/src/tools/FileEditTool/utils.ts +58 -0
  186. package/src/tools/FileReadTool/FileReadTool.tsx +404 -0
  187. package/src/tools/FileReadTool/prompt.ts +7 -0
  188. package/src/tools/FileWriteTool/FileWriteTool.tsx +297 -0
  189. package/src/tools/FileWriteTool/prompt.ts +10 -0
  190. package/src/tools/GlobTool/GlobTool.tsx +119 -0
  191. package/src/tools/GlobTool/prompt.ts +8 -0
  192. package/src/tools/GrepTool/GrepTool.tsx +147 -0
  193. package/src/tools/GrepTool/prompt.ts +11 -0
  194. package/src/tools/MCPTool/MCPTool.tsx +107 -0
  195. package/src/tools/MCPTool/prompt.ts +3 -0
  196. package/src/tools/MemoryReadTool/MemoryReadTool.tsx +127 -0
  197. package/src/tools/MemoryReadTool/prompt.ts +3 -0
  198. package/src/tools/MemoryWriteTool/MemoryWriteTool.tsx +89 -0
  199. package/src/tools/MemoryWriteTool/prompt.ts +3 -0
  200. package/src/tools/MultiEditTool/MultiEditTool.tsx +366 -0
  201. package/src/tools/MultiEditTool/prompt.ts +45 -0
  202. package/src/tools/NotebookEditTool/NotebookEditTool.tsx +298 -0
  203. package/src/tools/NotebookEditTool/prompt.ts +3 -0
  204. package/src/tools/NotebookReadTool/NotebookReadTool.tsx +258 -0
  205. package/src/tools/NotebookReadTool/prompt.ts +3 -0
  206. package/src/tools/StickerRequestTool/StickerRequestTool.tsx +93 -0
  207. package/src/tools/StickerRequestTool/prompt.ts +19 -0
  208. package/src/tools/TaskTool/TaskTool.tsx +466 -0
  209. package/src/tools/TaskTool/constants.ts +1 -0
  210. package/src/tools/TaskTool/prompt.ts +92 -0
  211. package/src/tools/ThinkTool/ThinkTool.tsx +54 -0
  212. package/src/tools/ThinkTool/prompt.ts +12 -0
  213. package/src/tools/TodoWriteTool/TodoWriteTool.tsx +313 -0
  214. package/src/tools/TodoWriteTool/prompt.ts +63 -0
  215. package/src/tools/URLFetcherTool/URLFetcherTool.tsx +178 -0
  216. package/src/tools/URLFetcherTool/cache.ts +55 -0
  217. package/src/tools/URLFetcherTool/htmlToMarkdown.ts +55 -0
  218. package/src/tools/URLFetcherTool/prompt.ts +17 -0
  219. package/src/tools/WebSearchTool/WebSearchTool.tsx +103 -0
  220. package/src/tools/WebSearchTool/prompt.ts +13 -0
  221. package/src/tools/WebSearchTool/searchProviders.ts +66 -0
  222. package/src/tools/lsTool/lsTool.tsx +272 -0
  223. package/src/tools/lsTool/prompt.ts +2 -0
  224. package/src/tools.ts +67 -0
  225. package/src/types/PermissionMode.ts +120 -0
  226. package/src/types/RequestContext.ts +72 -0
  227. package/src/types/conversation.ts +51 -0
  228. package/src/types/logs.ts +58 -0
  229. package/src/types/modelCapabilities.ts +64 -0
  230. package/src/types/notebook.ts +87 -0
  231. package/src/utils/Cursor.ts +436 -0
  232. package/src/utils/PersistentShell.ts +552 -0
  233. package/src/utils/advancedFuzzyMatcher.ts +290 -0
  234. package/src/utils/agentLoader.ts +278 -0
  235. package/src/utils/agentStorage.ts +97 -0
  236. package/src/utils/array.ts +3 -0
  237. package/src/utils/ask.tsx +99 -0
  238. package/src/utils/auth.ts +13 -0
  239. package/src/utils/autoCompactCore.ts +223 -0
  240. package/src/utils/autoUpdater.ts +458 -0
  241. package/src/utils/betas.ts +20 -0
  242. package/src/utils/browser.ts +14 -0
  243. package/src/utils/cleanup.ts +72 -0
  244. package/src/utils/commands.ts +261 -0
  245. package/src/utils/commonUnixCommands.ts +161 -0
  246. package/src/utils/config.ts +945 -0
  247. package/src/utils/conversationRecovery.ts +55 -0
  248. package/src/utils/debugLogger.ts +1235 -0
  249. package/src/utils/diff.ts +42 -0
  250. package/src/utils/env.ts +57 -0
  251. package/src/utils/errors.ts +21 -0
  252. package/src/utils/exampleCommands.ts +109 -0
  253. package/src/utils/execFileNoThrow.ts +51 -0
  254. package/src/utils/expertChatStorage.ts +136 -0
  255. package/src/utils/file.ts +405 -0
  256. package/src/utils/fileRecoveryCore.ts +71 -0
  257. package/src/utils/format.tsx +44 -0
  258. package/src/utils/fuzzyMatcher.ts +328 -0
  259. package/src/utils/generators.ts +62 -0
  260. package/src/utils/git.ts +92 -0
  261. package/src/utils/globalLogger.ts +77 -0
  262. package/src/utils/http.ts +10 -0
  263. package/src/utils/imagePaste.ts +38 -0
  264. package/src/utils/json.ts +13 -0
  265. package/src/utils/log.ts +382 -0
  266. package/src/utils/markdown.ts +213 -0
  267. package/src/utils/messageContextManager.ts +289 -0
  268. package/src/utils/messages.tsx +939 -0
  269. package/src/utils/model.ts +914 -0
  270. package/src/utils/permissions/filesystem.ts +127 -0
  271. package/src/utils/responseState.ts +23 -0
  272. package/src/utils/ripgrep.ts +167 -0
  273. package/src/utils/secureFile.ts +564 -0
  274. package/src/utils/sessionState.ts +49 -0
  275. package/src/utils/state.ts +25 -0
  276. package/src/utils/style.ts +29 -0
  277. package/src/utils/terminal.ts +50 -0
  278. package/src/utils/theme.ts +127 -0
  279. package/src/utils/thinking.ts +144 -0
  280. package/src/utils/todoStorage.ts +431 -0
  281. package/src/utils/tokens.ts +43 -0
  282. package/src/utils/toolExecutionController.ts +163 -0
  283. package/src/utils/unaryLogging.ts +26 -0
  284. package/src/utils/user.ts +37 -0
  285. package/src/utils/validate.ts +165 -0
  286. package/yoga.wasm +0 -0
@@ -0,0 +1,1338 @@
1
+ import { OpenAI } from 'openai'
2
+ import { getGlobalConfig, GlobalConfig } from '../utils/config'
3
+ import { ProxyAgent, fetch, Response } from 'undici'
4
+ import { setSessionState, getSessionState } from '../utils/sessionState'
5
+ import { logEvent } from '../services/statsig'
6
+ import { debug as debugLogger, getCurrentRequest, logAPIError } from '../utils/debugLogger'
7
+
8
+ // Helper function to calculate retry delay with exponential backoff
9
+ function getRetryDelay(attempt: number, retryAfter?: string | null): number {
10
+ // If server suggests a retry-after time, use it
11
+ if (retryAfter) {
12
+ const retryAfterMs = parseInt(retryAfter) * 1000
13
+ if (!isNaN(retryAfterMs) && retryAfterMs > 0) {
14
+ return Math.min(retryAfterMs, 60000) // Cap at 60 seconds
15
+ }
16
+ }
17
+
18
+ // Exponential backoff: base delay of 1 second, doubling each attempt
19
+ const baseDelay = 1000
20
+ const maxDelay = 32000 // Cap at 32 seconds
21
+ const delay = baseDelay * Math.pow(2, attempt - 1)
22
+
23
+ // Add some jitter to avoid thundering herd
24
+ const jitter = Math.random() * 0.1 * delay
25
+
26
+ return Math.min(delay + jitter, maxDelay)
27
+ }
28
+
29
+ // Helper function to create an abortable delay
30
+ function abortableDelay(delayMs: number, signal?: AbortSignal): Promise<void> {
31
+ return new Promise((resolve, reject) => {
32
+ // Check if already aborted
33
+ if (signal?.aborted) {
34
+ reject(new Error('Request was aborted'))
35
+ return
36
+ }
37
+
38
+ const timeoutId = setTimeout(() => {
39
+ resolve()
40
+ }, delayMs)
41
+
42
+ // If signal is provided, listen for abort event
43
+ if (signal) {
44
+ const abortHandler = () => {
45
+ clearTimeout(timeoutId)
46
+ reject(new Error('Request was aborted'))
47
+ }
48
+ signal.addEventListener('abort', abortHandler, { once: true })
49
+ }
50
+ })
51
+ }
52
+
53
+ enum ModelErrorType {
54
+ MaxLength = '1024',
55
+ MaxCompletionTokens = 'max_completion_tokens',
56
+ TemperatureRestriction = 'temperature_restriction',
57
+ StreamOptions = 'stream_options',
58
+ Citations = 'citations',
59
+ RateLimit = 'rate_limit',
60
+ }
61
+
62
+ function getModelErrorKey(
63
+ baseURL: string,
64
+ model: string,
65
+ type: ModelErrorType,
66
+ ): string {
67
+ return `${baseURL}:${model}:${type}`
68
+ }
69
+
70
+ function hasModelError(
71
+ baseURL: string,
72
+ model: string,
73
+ type: ModelErrorType,
74
+ ): boolean {
75
+ return !!getSessionState('modelErrors')[
76
+ getModelErrorKey(baseURL, model, type)
77
+ ]
78
+ }
79
+
80
+ function setModelError(
81
+ baseURL: string,
82
+ model: string,
83
+ type: ModelErrorType,
84
+ error: string,
85
+ ) {
86
+ setSessionState('modelErrors', {
87
+ [getModelErrorKey(baseURL, model, type)]: error,
88
+ })
89
+ }
90
+
91
+ // More flexible error detection system
92
+ type ErrorDetector = (errMsg: string) => boolean
93
+ type ErrorFixer = (
94
+ opts: OpenAI.ChatCompletionCreateParams,
95
+ ) => Promise<void> | void
96
+ interface ErrorHandler {
97
+ type: ModelErrorType
98
+ detect: ErrorDetector
99
+ fix: ErrorFixer
100
+ }
101
+
102
+ // GPT-5 specific error handlers with enhanced detection patterns
103
+ const GPT5_ERROR_HANDLERS: ErrorHandler[] = [
104
+ {
105
+ type: ModelErrorType.MaxCompletionTokens,
106
+ detect: errMsg => {
107
+ const lowerMsg = errMsg.toLowerCase()
108
+ return (
109
+ // Exact OpenAI GPT-5 error message
110
+ (lowerMsg.includes("unsupported parameter: 'max_tokens'") && lowerMsg.includes("'max_completion_tokens'")) ||
111
+ // Generic max_tokens error patterns
112
+ (lowerMsg.includes("max_tokens") && lowerMsg.includes("max_completion_tokens")) ||
113
+ (lowerMsg.includes("max_tokens") && lowerMsg.includes("not supported")) ||
114
+ (lowerMsg.includes("max_tokens") && lowerMsg.includes("use max_completion_tokens")) ||
115
+ // Additional patterns for various providers
116
+ (lowerMsg.includes("invalid parameter") && lowerMsg.includes("max_tokens")) ||
117
+ (lowerMsg.includes("parameter error") && lowerMsg.includes("max_tokens"))
118
+ )
119
+ },
120
+ fix: async opts => {
121
+ console.log(`🔧 GPT-5 Fix: Converting max_tokens (${opts.max_tokens}) to max_completion_tokens`)
122
+ if ('max_tokens' in opts) {
123
+ opts.max_completion_tokens = opts.max_tokens
124
+ delete opts.max_tokens
125
+ }
126
+ },
127
+ },
128
+ {
129
+ type: ModelErrorType.TemperatureRestriction,
130
+ detect: errMsg => {
131
+ const lowerMsg = errMsg.toLowerCase()
132
+ return (
133
+ lowerMsg.includes("temperature") &&
134
+ (lowerMsg.includes("only supports") || lowerMsg.includes("must be 1") || lowerMsg.includes("invalid temperature"))
135
+ )
136
+ },
137
+ fix: async opts => {
138
+ console.log(`🔧 GPT-5 Fix: Adjusting temperature from ${opts.temperature} to 1`)
139
+ opts.temperature = 1
140
+ },
141
+ },
142
+ // Add more GPT-5 specific handlers as needed
143
+ ]
144
+
145
+ // Standard error handlers
146
+ const ERROR_HANDLERS: ErrorHandler[] = [
147
+ {
148
+ type: ModelErrorType.MaxLength,
149
+ detect: errMsg =>
150
+ errMsg.includes('Expected a string with maximum length 1024'),
151
+ fix: async opts => {
152
+ const toolDescriptions = {}
153
+ for (const tool of opts.tools || []) {
154
+ if (tool.function.description.length <= 1024) continue
155
+ let str = ''
156
+ let remainder = ''
157
+ for (let line of tool.function.description.split('\n')) {
158
+ if (str.length + line.length < 1024) {
159
+ str += line + '\n'
160
+ } else {
161
+ remainder += line + '\n'
162
+ }
163
+ }
164
+ logEvent('truncated_tool_description', {
165
+ name: tool.function.name,
166
+ original_length: String(tool.function.description.length),
167
+ truncated_length: String(str.length),
168
+ remainder_length: String(remainder.length),
169
+ })
170
+ tool.function.description = str
171
+ toolDescriptions[tool.function.name] = remainder
172
+ }
173
+ if (Object.keys(toolDescriptions).length > 0) {
174
+ let content = '<additional-tool-usage-instructions>\n\n'
175
+ for (const [name, description] of Object.entries(toolDescriptions)) {
176
+ content += `<${name}>\n${description}\n</${name}>\n\n`
177
+ }
178
+ content += '</additional-tool-usage-instructions>'
179
+
180
+ for (let i = opts.messages.length - 1; i >= 0; i--) {
181
+ if (opts.messages[i].role === 'system') {
182
+ opts.messages.splice(i + 1, 0, {
183
+ role: 'system',
184
+ content,
185
+ })
186
+ break
187
+ }
188
+ }
189
+ }
190
+ },
191
+ },
192
+ {
193
+ type: ModelErrorType.MaxCompletionTokens,
194
+ detect: errMsg => errMsg.includes("Use 'max_completion_tokens'"),
195
+ fix: async opts => {
196
+ opts.max_completion_tokens = opts.max_tokens
197
+ delete opts.max_tokens
198
+ },
199
+ },
200
+ {
201
+ type: ModelErrorType.StreamOptions,
202
+ detect: errMsg => errMsg.includes('stream_options'),
203
+ fix: async opts => {
204
+ delete opts.stream_options
205
+ },
206
+ },
207
+ {
208
+ type: ModelErrorType.Citations,
209
+ detect: errMsg =>
210
+ errMsg.includes('Extra inputs are not permitted') &&
211
+ errMsg.includes('citations'),
212
+ fix: async opts => {
213
+ if (!opts.messages) return
214
+
215
+ for (const message of opts.messages) {
216
+ if (!message) continue
217
+
218
+ if (Array.isArray(message.content)) {
219
+ for (const item of message.content) {
220
+ // Convert to unknown first to safely access properties
221
+ if (item && typeof item === 'object') {
222
+ const itemObj = item as unknown as Record<string, unknown>
223
+ if ('citations' in itemObj) {
224
+ delete itemObj.citations
225
+ }
226
+ }
227
+ }
228
+ } else if (message.content && typeof message.content === 'object') {
229
+ // Convert to unknown first to safely access properties
230
+ const contentObj = message.content as unknown as Record<
231
+ string,
232
+ unknown
233
+ >
234
+ if ('citations' in contentObj) {
235
+ delete contentObj.citations
236
+ }
237
+ }
238
+ }
239
+ },
240
+ },
241
+ ]
242
+
243
+ // Rate limit specific detection
244
+ function isRateLimitError(errMsg: string): boolean {
245
+ if (!errMsg) return false
246
+ const lowerMsg = errMsg.toLowerCase()
247
+ return (
248
+ lowerMsg.includes('rate limit') ||
249
+ lowerMsg.includes('too many requests') ||
250
+ lowerMsg.includes('429')
251
+ )
252
+ }
253
+
254
+ // Model-specific feature flags - can be extended with more properties as needed
255
+ interface ModelFeatures {
256
+ usesMaxCompletionTokens: boolean
257
+ supportsResponsesAPI?: boolean
258
+ requiresTemperatureOne?: boolean
259
+ supportsVerbosityControl?: boolean
260
+ supportsCustomTools?: boolean
261
+ supportsAllowedTools?: boolean
262
+ }
263
+
264
+ // Map of model identifiers to their specific features
265
+ const MODEL_FEATURES: Record<string, ModelFeatures> = {
266
+ // OpenAI thinking models
267
+ o1: { usesMaxCompletionTokens: true },
268
+ 'o1-preview': { usesMaxCompletionTokens: true },
269
+ 'o1-mini': { usesMaxCompletionTokens: true },
270
+ 'o1-pro': { usesMaxCompletionTokens: true },
271
+ 'o3-mini': { usesMaxCompletionTokens: true },
272
+ // GPT-5 models
273
+ 'gpt-5': {
274
+ usesMaxCompletionTokens: true,
275
+ supportsResponsesAPI: true,
276
+ requiresTemperatureOne: true,
277
+ supportsVerbosityControl: true,
278
+ supportsCustomTools: true,
279
+ supportsAllowedTools: true,
280
+ },
281
+ 'gpt-5-mini': {
282
+ usesMaxCompletionTokens: true,
283
+ supportsResponsesAPI: true,
284
+ requiresTemperatureOne: true,
285
+ supportsVerbosityControl: true,
286
+ supportsCustomTools: true,
287
+ supportsAllowedTools: true,
288
+ },
289
+ 'gpt-5-nano': {
290
+ usesMaxCompletionTokens: true,
291
+ supportsResponsesAPI: true,
292
+ requiresTemperatureOne: true,
293
+ supportsVerbosityControl: true,
294
+ supportsCustomTools: true,
295
+ supportsAllowedTools: true,
296
+ },
297
+ 'gpt-5-chat-latest': {
298
+ usesMaxCompletionTokens: true,
299
+ supportsResponsesAPI: false, // Uses Chat Completions only
300
+ requiresTemperatureOne: true,
301
+ supportsVerbosityControl: true,
302
+ },
303
+ }
304
+
305
+ // Helper to get model features based on model ID/name
306
+ function getModelFeatures(modelName: string): ModelFeatures {
307
+ if (!modelName || typeof modelName !== 'string') {
308
+ return { usesMaxCompletionTokens: false }
309
+ }
310
+
311
+ // Check for exact matches first (highest priority)
312
+ if (MODEL_FEATURES[modelName]) {
313
+ return MODEL_FEATURES[modelName]
314
+ }
315
+
316
+ // Simple GPT-5 detection: any model name containing 'gpt-5'
317
+ if (modelName.toLowerCase().includes('gpt-5')) {
318
+ return {
319
+ usesMaxCompletionTokens: true,
320
+ supportsResponsesAPI: true,
321
+ requiresTemperatureOne: true,
322
+ supportsVerbosityControl: true,
323
+ supportsCustomTools: true,
324
+ supportsAllowedTools: true,
325
+ }
326
+ }
327
+
328
+ // Check for partial matches (e.g., other reasoning models)
329
+ for (const [key, features] of Object.entries(MODEL_FEATURES)) {
330
+ if (modelName.includes(key)) {
331
+ return features
332
+ }
333
+ }
334
+
335
+ // Default features for unknown models
336
+ return { usesMaxCompletionTokens: false }
337
+ }
338
+
339
+ // Apply model-specific parameter transformations based on model features
340
+ function applyModelSpecificTransformations(
341
+ opts: OpenAI.ChatCompletionCreateParams,
342
+ ): void {
343
+ if (!opts.model || typeof opts.model !== 'string') {
344
+ return
345
+ }
346
+
347
+ const features = getModelFeatures(opts.model)
348
+ const isGPT5 = opts.model.toLowerCase().includes('gpt-5')
349
+
350
+ // 🔥 Enhanced GPT-5 Detection and Transformation
351
+ if (isGPT5 || features.usesMaxCompletionTokens) {
352
+ // Force max_completion_tokens for all GPT-5 models
353
+ if ('max_tokens' in opts && !('max_completion_tokens' in opts)) {
354
+ console.log(`🔧 Transforming max_tokens (${opts.max_tokens}) to max_completion_tokens for ${opts.model}`)
355
+ opts.max_completion_tokens = opts.max_tokens
356
+ delete opts.max_tokens
357
+ }
358
+
359
+ // Force temperature = 1 for GPT-5 models
360
+ if (features.requiresTemperatureOne && 'temperature' in opts) {
361
+ if (opts.temperature !== 1 && opts.temperature !== undefined) {
362
+ console.log(
363
+ `🔧 GPT-5 temperature constraint: Adjusting temperature from ${opts.temperature} to 1 for ${opts.model}`
364
+ )
365
+ opts.temperature = 1
366
+ }
367
+ }
368
+
369
+ // Remove unsupported parameters for GPT-5
370
+ if (isGPT5) {
371
+ // Remove parameters that may not be supported by GPT-5
372
+ delete opts.frequency_penalty
373
+ delete opts.presence_penalty
374
+ delete opts.logit_bias
375
+ delete opts.user
376
+
377
+ // Add reasoning_effort if not present and model supports it
378
+ if (!opts.reasoning_effort && features.supportsVerbosityControl) {
379
+ opts.reasoning_effort = 'medium' // Default reasoning effort for coding tasks
380
+ }
381
+ }
382
+ }
383
+
384
+ // Apply transformations for non-GPT-5 models
385
+ else {
386
+ // Standard max_tokens to max_completion_tokens conversion for other reasoning models
387
+ if (
388
+ features.usesMaxCompletionTokens &&
389
+ 'max_tokens' in opts &&
390
+ !('max_completion_tokens' in opts)
391
+ ) {
392
+ opts.max_completion_tokens = opts.max_tokens
393
+ delete opts.max_tokens
394
+ }
395
+ }
396
+
397
+ // Add more transformations here as needed
398
+ }
399
+
400
+ async function applyModelErrorFixes(
401
+ opts: OpenAI.ChatCompletionCreateParams,
402
+ baseURL: string,
403
+ ) {
404
+ const isGPT5 = opts.model.startsWith('gpt-5')
405
+ const handlers = isGPT5 ? [...GPT5_ERROR_HANDLERS, ...ERROR_HANDLERS] : ERROR_HANDLERS
406
+
407
+ for (const handler of handlers) {
408
+ if (hasModelError(baseURL, opts.model, handler.type)) {
409
+ await handler.fix(opts)
410
+ return
411
+ }
412
+ }
413
+ }
414
+
415
+ // Helper function to try different endpoints for OpenAI-compatible providers
416
+ async function tryWithEndpointFallback(
417
+ baseURL: string,
418
+ opts: OpenAI.ChatCompletionCreateParams,
419
+ headers: Record<string, string>,
420
+ provider: string,
421
+ proxy: any,
422
+ signal?: AbortSignal, // 🔧 Add AbortSignal support
423
+ ): Promise<{ response: Response; endpoint: string }> {
424
+ const endpointsToTry = []
425
+
426
+ if (provider === 'minimax') {
427
+ endpointsToTry.push('/text/chatcompletion_v2', '/chat/completions')
428
+ } else {
429
+ endpointsToTry.push('/chat/completions')
430
+ }
431
+
432
+ let lastError = null
433
+
434
+ for (const endpoint of endpointsToTry) {
435
+ try {
436
+ const response = await fetch(`${baseURL}${endpoint}`, {
437
+ method: 'POST',
438
+ headers,
439
+ body: JSON.stringify(opts.stream ? { ...opts, stream: true } : opts),
440
+ dispatcher: proxy,
441
+ signal: signal, // 🔧 Connect AbortSignal to fetch call
442
+ })
443
+
444
+ // If successful, return immediately
445
+ if (response.ok) {
446
+ return { response, endpoint }
447
+ }
448
+
449
+ // If it's a 404, try the next endpoint
450
+ if (response.status === 404 && endpointsToTry.length > 1) {
451
+ console.log(
452
+ `Endpoint ${endpoint} returned 404, trying next endpoint...`,
453
+ )
454
+ continue
455
+ }
456
+
457
+ // For other error codes, return this response (don't try fallback)
458
+ return { response, endpoint }
459
+ } catch (error) {
460
+ lastError = error
461
+ // Network errors might be temporary, try next endpoint
462
+ if (endpointsToTry.indexOf(endpoint) < endpointsToTry.length - 1) {
463
+ console.log(`Network error on ${endpoint}, trying next endpoint...`)
464
+ continue
465
+ }
466
+ }
467
+ }
468
+
469
+ // If we get here, all endpoints failed
470
+ throw lastError || new Error('All endpoints failed')
471
+ }
472
+
473
+ // Export shared utilities for GPT-5 compatibility
474
+ export { getGPT5CompletionWithProfile, getModelFeatures, applyModelSpecificTransformations }
475
+
476
+ export async function getCompletionWithProfile(
477
+ modelProfile: any,
478
+ opts: OpenAI.ChatCompletionCreateParams,
479
+ attempt: number = 0,
480
+ maxAttempts: number = 10,
481
+ signal?: AbortSignal, // 🔧 CRITICAL FIX: Add AbortSignal support
482
+ ): Promise<OpenAI.ChatCompletion | AsyncIterable<OpenAI.ChatCompletionChunk>> {
483
+ if (attempt >= maxAttempts) {
484
+ throw new Error('Max attempts reached')
485
+ }
486
+
487
+ const provider = modelProfile?.provider || 'anthropic'
488
+ const baseURL = modelProfile?.baseURL
489
+ const apiKey = modelProfile?.apiKey
490
+ const proxy = getGlobalConfig().proxy
491
+ ? new ProxyAgent(getGlobalConfig().proxy)
492
+ : undefined
493
+
494
+ const headers: Record<string, string> = {
495
+ 'Content-Type': 'application/json',
496
+ }
497
+
498
+ if (apiKey) {
499
+ if (provider === 'azure') {
500
+ headers['api-key'] = apiKey
501
+ } else {
502
+ headers['Authorization'] = `Bearer ${apiKey}`
503
+ }
504
+ }
505
+
506
+ applyModelSpecificTransformations(opts)
507
+ await applyModelErrorFixes(opts, baseURL || '')
508
+
509
+ // 🔥 REAL-TIME API CALL DEBUG - 使用全局日志系统
510
+ debugLogger.api('OPENAI_API_CALL_START', {
511
+ endpoint: baseURL || 'DEFAULT_OPENAI',
512
+ model: opts.model,
513
+ provider,
514
+ apiKeyConfigured: !!apiKey,
515
+ apiKeyPrefix: apiKey ? apiKey.substring(0, 8) : null,
516
+ maxTokens: opts.max_tokens,
517
+ temperature: opts.temperature,
518
+ messageCount: opts.messages?.length || 0,
519
+ streamMode: opts.stream,
520
+ timestamp: new Date().toISOString(),
521
+ modelProfileModelName: modelProfile?.modelName,
522
+ modelProfileName: modelProfile?.name,
523
+ })
524
+
525
+ // Make sure all tool messages have string content
526
+ opts.messages = opts.messages.map(msg => {
527
+ if (msg.role === 'tool') {
528
+ if (Array.isArray(msg.content)) {
529
+ return {
530
+ ...msg,
531
+ content:
532
+ msg.content
533
+ .map(c => c.text || '')
534
+ .filter(Boolean)
535
+ .join('\n\n') || '(empty content)',
536
+ }
537
+ } else if (typeof msg.content !== 'string') {
538
+ return {
539
+ ...msg,
540
+ content:
541
+ typeof msg.content === 'undefined'
542
+ ? '(empty content)'
543
+ : JSON.stringify(msg.content),
544
+ }
545
+ }
546
+ }
547
+ return msg
548
+ })
549
+
550
+ // Define Azure-specific API endpoint with version
551
+ const azureApiVersion = '2024-06-01'
552
+ let endpoint = '/chat/completions'
553
+
554
+ if (provider === 'azure') {
555
+ endpoint = `/chat/completions?api-version=${azureApiVersion}`
556
+ } else if (provider === 'minimax') {
557
+ endpoint = '/text/chatcompletion_v2'
558
+ }
559
+
560
+ try {
561
+ if (opts.stream) {
562
+ const isOpenAICompatible = [
563
+ 'minimax',
564
+ 'kimi',
565
+ 'deepseek',
566
+ 'siliconflow',
567
+ 'qwen',
568
+ 'glm',
569
+ 'baidu-qianfan',
570
+ 'openai',
571
+ 'mistral',
572
+ 'xai',
573
+ 'groq',
574
+ 'custom-openai',
575
+ ].includes(provider)
576
+
577
+ let response: Response
578
+ let usedEndpoint: string
579
+
580
+ if (isOpenAICompatible && provider !== 'azure') {
581
+ const result = await tryWithEndpointFallback(
582
+ baseURL,
583
+ opts,
584
+ headers,
585
+ provider,
586
+ proxy,
587
+ signal, // 🔧 Pass AbortSignal to endpoint fallback
588
+ )
589
+ response = result.response
590
+ usedEndpoint = result.endpoint
591
+ } else {
592
+ response = await fetch(`${baseURL}${endpoint}`, {
593
+ method: 'POST',
594
+ headers,
595
+ body: JSON.stringify({ ...opts, stream: true }),
596
+ dispatcher: proxy,
597
+ signal: signal, // 🔧 CRITICAL FIX: Connect AbortSignal to fetch call
598
+ })
599
+ usedEndpoint = endpoint
600
+ }
601
+
602
+ if (!response.ok) {
603
+ // 🔧 CRITICAL FIX: Check abort signal BEFORE showing retry message
604
+ if (signal?.aborted) {
605
+ throw new Error('Request cancelled by user')
606
+ }
607
+
608
+ // 🔥 NEW: Parse error message to detect and handle specific API errors
609
+ try {
610
+ const errorData = await response.json()
611
+ const errorMessage = errorData?.error?.message || errorData?.message || `HTTP ${response.status}`
612
+
613
+ // Check if this is a parameter error that we can fix
614
+ const isGPT5 = opts.model.startsWith('gpt-5')
615
+ const handlers = isGPT5 ? [...GPT5_ERROR_HANDLERS, ...ERROR_HANDLERS] : ERROR_HANDLERS
616
+
617
+ for (const handler of handlers) {
618
+ if (handler.detect(errorMessage)) {
619
+ console.log(`🔧 Detected ${handler.type} error for ${opts.model}: ${errorMessage}`)
620
+
621
+ // Store this error for future requests
622
+ setModelError(baseURL || '', opts.model, handler.type, errorMessage)
623
+
624
+ // Apply the fix and retry immediately
625
+ await handler.fix(opts)
626
+ console.log(`🔧 Applied fix for ${handler.type}, retrying...`)
627
+
628
+ return getCompletionWithProfile(
629
+ modelProfile,
630
+ opts,
631
+ attempt + 1,
632
+ maxAttempts,
633
+ signal,
634
+ )
635
+ }
636
+ }
637
+
638
+ // If no specific handler found, log the error for debugging
639
+ console.log(`⚠️ Unhandled API error (${response.status}): ${errorMessage}`)
640
+
641
+ // Log API error using unified logger
642
+ logAPIError({
643
+ model: opts.model,
644
+ endpoint: `${baseURL}${endpoint}`,
645
+ status: response.status,
646
+ error: errorMessage,
647
+ request: opts,
648
+ response: errorData,
649
+ provider: provider
650
+ })
651
+ } catch (parseError) {
652
+ // If we can't parse the error, fall back to generic retry
653
+ console.log(`⚠️ Could not parse error response (${response.status})`)
654
+
655
+ // Log parse error
656
+ logAPIError({
657
+ model: opts.model,
658
+ endpoint: `${baseURL}${endpoint}`,
659
+ status: response.status,
660
+ error: `Could not parse error response: ${parseError.message}`,
661
+ request: opts,
662
+ response: { parseError: parseError.message },
663
+ provider: provider
664
+ })
665
+ }
666
+
667
+ const delayMs = getRetryDelay(attempt)
668
+ console.log(
669
+ ` ⎿ API error (${response.status}), retrying in ${Math.round(delayMs / 1000)}s... (attempt ${attempt + 1}/${maxAttempts})`,
670
+ )
671
+ try {
672
+ await abortableDelay(delayMs, signal)
673
+ } catch (error) {
674
+ // If aborted during delay, throw the error to stop retrying
675
+ if (error.message === 'Request was aborted') {
676
+ throw new Error('Request cancelled by user')
677
+ }
678
+ throw error
679
+ }
680
+ return getCompletionWithProfile(
681
+ modelProfile,
682
+ opts,
683
+ attempt + 1,
684
+ maxAttempts,
685
+ signal, // 🔧 Pass AbortSignal to recursive call
686
+ )
687
+ }
688
+
689
+ const stream = createStreamProcessor(response.body as any, signal)
690
+ return stream
691
+ }
692
+
693
+ // Non-streaming request
694
+ const isOpenAICompatible = [
695
+ 'minimax',
696
+ 'kimi',
697
+ 'deepseek',
698
+ 'siliconflow',
699
+ 'qwen',
700
+ 'glm',
701
+ 'baidu-qianfan',
702
+ 'openai',
703
+ 'mistral',
704
+ 'xai',
705
+ 'groq',
706
+ 'custom-openai',
707
+ ].includes(provider)
708
+
709
+ let response: Response
710
+ let usedEndpoint: string
711
+
712
+ if (isOpenAICompatible && provider !== 'azure') {
713
+ const result = await tryWithEndpointFallback(
714
+ baseURL,
715
+ opts,
716
+ headers,
717
+ provider,
718
+ proxy,
719
+ signal, // 🔧 Pass AbortSignal to endpoint fallback
720
+ )
721
+ response = result.response
722
+ usedEndpoint = result.endpoint
723
+ } else {
724
+ response = await fetch(`${baseURL}${endpoint}`, {
725
+ method: 'POST',
726
+ headers,
727
+ body: JSON.stringify(opts),
728
+ dispatcher: proxy,
729
+ signal: signal, // 🔧 CRITICAL FIX: Connect AbortSignal to non-streaming fetch call
730
+ })
731
+ usedEndpoint = endpoint
732
+ }
733
+
734
+ if (!response.ok) {
735
+ // 🔧 CRITICAL FIX: Check abort signal BEFORE showing retry message
736
+ if (signal?.aborted) {
737
+ throw new Error('Request cancelled by user')
738
+ }
739
+
740
+ // 🔥 NEW: Parse error message to detect and handle specific API errors
741
+ try {
742
+ const errorData = await response.json()
743
+ const errorMessage = errorData?.error?.message || errorData?.message || `HTTP ${response.status}`
744
+
745
+ // Check if this is a parameter error that we can fix
746
+ const isGPT5 = opts.model.startsWith('gpt-5')
747
+ const handlers = isGPT5 ? [...GPT5_ERROR_HANDLERS, ...ERROR_HANDLERS] : ERROR_HANDLERS
748
+
749
+ for (const handler of handlers) {
750
+ if (handler.detect(errorMessage)) {
751
+ console.log(`🔧 Detected ${handler.type} error for ${opts.model}: ${errorMessage}`)
752
+
753
+ // Store this error for future requests
754
+ setModelError(baseURL || '', opts.model, handler.type, errorMessage)
755
+
756
+ // Apply the fix and retry immediately
757
+ await handler.fix(opts)
758
+ console.log(`🔧 Applied fix for ${handler.type}, retrying...`)
759
+
760
+ return getCompletionWithProfile(
761
+ modelProfile,
762
+ opts,
763
+ attempt + 1,
764
+ maxAttempts,
765
+ signal,
766
+ )
767
+ }
768
+ }
769
+
770
+ // If no specific handler found, log the error for debugging
771
+ console.log(`⚠️ Unhandled API error (${response.status}): ${errorMessage}`)
772
+ } catch (parseError) {
773
+ // If we can't parse the error, fall back to generic retry
774
+ console.log(`⚠️ Could not parse error response (${response.status})`)
775
+ }
776
+
777
+ const delayMs = getRetryDelay(attempt)
778
+ console.log(
779
+ ` ⎿ API error (${response.status}), retrying in ${Math.round(delayMs / 1000)}s... (attempt ${attempt + 1}/${maxAttempts})`,
780
+ )
781
+ try {
782
+ await abortableDelay(delayMs, signal)
783
+ } catch (error) {
784
+ // If aborted during delay, throw the error to stop retrying
785
+ if (error.message === 'Request was aborted') {
786
+ throw new Error('Request cancelled by user')
787
+ }
788
+ throw error
789
+ }
790
+ return getCompletionWithProfile(
791
+ modelProfile,
792
+ opts,
793
+ attempt + 1,
794
+ maxAttempts,
795
+ signal, // 🔧 Pass AbortSignal to recursive call
796
+ )
797
+ }
798
+
799
+ const responseData = (await response.json()) as OpenAI.ChatCompletion
800
+ return responseData
801
+ } catch (error) {
802
+ // 🔧 CRITICAL FIX: Check abort signal BEFORE showing retry message
803
+ if (signal?.aborted) {
804
+ throw new Error('Request cancelled by user')
805
+ }
806
+
807
+ if (attempt < maxAttempts) {
808
+ // 🔧 Double-check abort status to avoid showing misleading retry message
809
+ if (signal?.aborted) {
810
+ throw new Error('Request cancelled by user')
811
+ }
812
+
813
+ const delayMs = getRetryDelay(attempt)
814
+ console.log(
815
+ ` ⎿ Network error, retrying in ${Math.round(delayMs / 1000)}s... (attempt ${attempt + 1}/${maxAttempts})`,
816
+ )
817
+ try {
818
+ await abortableDelay(delayMs, signal)
819
+ } catch (error) {
820
+ // If aborted during delay, throw the error to stop retrying
821
+ if (error.message === 'Request was aborted') {
822
+ throw new Error('Request cancelled by user')
823
+ }
824
+ throw error
825
+ }
826
+ return getCompletionWithProfile(
827
+ modelProfile,
828
+ opts,
829
+ attempt + 1,
830
+ maxAttempts,
831
+ signal, // 🔧 Pass AbortSignal to recursive call
832
+ )
833
+ }
834
+ throw error
835
+ }
836
+ }
837
+
838
+ export function createStreamProcessor(
839
+ stream: any,
840
+ signal?: AbortSignal,
841
+ ): AsyncGenerator<OpenAI.ChatCompletionChunk, void, unknown> {
842
+ if (!stream) {
843
+ throw new Error('Stream is null or undefined')
844
+ }
845
+
846
+ return (async function* () {
847
+ const reader = stream.getReader()
848
+ const decoder = new TextDecoder('utf-8')
849
+ let buffer = ''
850
+
851
+ try {
852
+ while (true) {
853
+ // Check for cancellation before attempting to read
854
+ if (signal?.aborted) {
855
+ break
856
+ }
857
+
858
+ let readResult
859
+ try {
860
+ readResult = await reader.read()
861
+ } catch (e) {
862
+ // If signal is aborted, this is user cancellation - exit silently
863
+ if (signal?.aborted) {
864
+ break
865
+ }
866
+ console.error('Error reading from stream:', e)
867
+ break
868
+ }
869
+
870
+ const { done, value } = readResult
871
+ if (done) {
872
+ break
873
+ }
874
+
875
+ const chunk = decoder.decode(value, { stream: true })
876
+ buffer += chunk
877
+
878
+ let lineEnd = buffer.indexOf('\n')
879
+ while (lineEnd !== -1) {
880
+ const line = buffer.substring(0, lineEnd).trim()
881
+ buffer = buffer.substring(lineEnd + 1)
882
+
883
+ if (line === 'data: [DONE]') {
884
+ continue
885
+ }
886
+
887
+ if (line.startsWith('data: ')) {
888
+ const data = line.slice(6).trim()
889
+ if (!data) continue
890
+
891
+ try {
892
+ const parsed = JSON.parse(data) as OpenAI.ChatCompletionChunk
893
+ yield parsed
894
+ } catch (e) {
895
+ console.error('Error parsing JSON:', data, e)
896
+ }
897
+ }
898
+
899
+ lineEnd = buffer.indexOf('\n')
900
+ }
901
+ }
902
+
903
+ // Process any remaining data in the buffer
904
+ if (buffer.trim()) {
905
+ const lines = buffer.trim().split('\n')
906
+ for (const line of lines) {
907
+ if (line.startsWith('data: ') && line !== 'data: [DONE]') {
908
+ const data = line.slice(6).trim()
909
+ if (!data) continue
910
+
911
+ try {
912
+ const parsed = JSON.parse(data) as OpenAI.ChatCompletionChunk
913
+ yield parsed
914
+ } catch (e) {
915
+ console.error('Error parsing final JSON:', data, e)
916
+ }
917
+ }
918
+ }
919
+ }
920
+ } catch (e) {
921
+ console.error('Unexpected error in stream processing:', e)
922
+ } finally {
923
+ try {
924
+ reader.releaseLock()
925
+ } catch (e) {
926
+ console.error('Error releasing reader lock:', e)
927
+ }
928
+ }
929
+ })()
930
+ }
931
+
932
+ export function streamCompletion(
933
+ stream: any,
934
+ signal?: AbortSignal,
935
+ ): AsyncGenerator<OpenAI.ChatCompletionChunk, void, unknown> {
936
+ return createStreamProcessor(stream, signal)
937
+ }
938
+
939
+ /**
940
+ * Call GPT-5 Responses API with proper parameter handling
941
+ */
942
+ export async function callGPT5ResponsesAPI(
943
+ modelProfile: any,
944
+ opts: any, // Using 'any' for Responses API params which differ from ChatCompletionCreateParams
945
+ signal?: AbortSignal,
946
+ ): Promise<any> {
947
+ const baseURL = modelProfile?.baseURL || 'https://api.openai.com/v1'
948
+ const apiKey = modelProfile?.apiKey
949
+ const proxy = getGlobalConfig().proxy
950
+ ? new ProxyAgent(getGlobalConfig().proxy)
951
+ : undefined
952
+
953
+ const headers: Record<string, string> = {
954
+ 'Content-Type': 'application/json',
955
+ Authorization: `Bearer ${apiKey}`,
956
+ }
957
+
958
+ // 🔥 Enhanced Responses API Parameter Mapping for GPT-5
959
+ const responsesParams: any = {
960
+ model: opts.model,
961
+ input: opts.messages, // Responses API uses 'input' instead of 'messages'
962
+ }
963
+
964
+ // 🔧 GPT-5 Token Configuration
965
+ if (opts.max_completion_tokens) {
966
+ responsesParams.max_completion_tokens = opts.max_completion_tokens
967
+ } else if (opts.max_tokens) {
968
+ // Fallback conversion if max_tokens is still present
969
+ responsesParams.max_completion_tokens = opts.max_tokens
970
+ }
971
+
972
+ // 🔧 GPT-5 Temperature Handling (only 1 or undefined)
973
+ if (opts.temperature === 1) {
974
+ responsesParams.temperature = 1
975
+ }
976
+ // Note: Do not pass temperature if it's not 1, GPT-5 will use default
977
+
978
+ // 🔧 GPT-5 Reasoning Configuration
979
+ const reasoningEffort = opts.reasoning_effort || 'medium'
980
+ responsesParams.reasoning = {
981
+ effort: reasoningEffort,
982
+ // 🚀 Enable reasoning summaries for transparency in coding tasks
983
+ generate_summary: true,
984
+ }
985
+
986
+ // 🔧 GPT-5 Tools Support
987
+ if (opts.tools && opts.tools.length > 0) {
988
+ responsesParams.tools = opts.tools
989
+
990
+ // 🚀 GPT-5 Tool Choice Configuration
991
+ if (opts.tool_choice) {
992
+ responsesParams.tool_choice = opts.tool_choice
993
+ }
994
+ }
995
+
996
+ // 🔧 GPT-5 System Instructions (separate from messages)
997
+ const systemMessages = opts.messages.filter(msg => msg.role === 'system')
998
+ const nonSystemMessages = opts.messages.filter(msg => msg.role !== 'system')
999
+
1000
+ if (systemMessages.length > 0) {
1001
+ responsesParams.instructions = systemMessages.map(msg => msg.content).join('\n\n')
1002
+ responsesParams.input = nonSystemMessages
1003
+ }
1004
+
1005
+ // Handle verbosity (if supported) - optimized for coding tasks
1006
+ const features = getModelFeatures(opts.model)
1007
+ if (features.supportsVerbosityControl) {
1008
+ // High verbosity for coding tasks to get detailed explanations and structured code
1009
+ // Based on GPT-5 best practices for agent-like coding environments
1010
+ responsesParams.text = {
1011
+ verbosity: 'high',
1012
+ }
1013
+ }
1014
+
1015
+ // Apply GPT-5 coding optimizations
1016
+ if (opts.model.startsWith('gpt-5')) {
1017
+ // Set reasoning effort based on task complexity
1018
+ if (!responsesParams.reasoning) {
1019
+ responsesParams.reasoning = {
1020
+ effort: 'medium', // Balanced for most coding tasks
1021
+ }
1022
+ }
1023
+
1024
+ // Add instructions parameter for coding-specific guidance
1025
+ if (!responsesParams.instructions) {
1026
+ responsesParams.instructions = `You are an expert programmer working in a terminal-based coding environment. Follow these guidelines:
1027
+ - Provide clear, concise code solutions
1028
+ - Use proper error handling and validation
1029
+ - Follow coding best practices and patterns
1030
+ - Explain complex logic when necessary
1031
+ - Focus on maintainable, readable code`
1032
+ }
1033
+ }
1034
+
1035
+ try {
1036
+ const response = await fetch(`${baseURL}/responses`, {
1037
+ method: 'POST',
1038
+ headers,
1039
+ body: JSON.stringify(responsesParams),
1040
+ dispatcher: proxy,
1041
+ signal: signal,
1042
+ })
1043
+
1044
+ if (!response.ok) {
1045
+ throw new Error(`GPT-5 Responses API error: ${response.status} ${response.statusText}`)
1046
+ }
1047
+
1048
+ const responseData = await response.json()
1049
+
1050
+ // Convert Responses API response back to Chat Completion format for compatibility
1051
+ return convertResponsesAPIToChatCompletion(responseData)
1052
+ } catch (error) {
1053
+ if (signal?.aborted) {
1054
+ throw new Error('Request cancelled by user')
1055
+ }
1056
+ throw error
1057
+ }
1058
+ }
1059
+
1060
+ /**
1061
+ * Convert Responses API response to Chat Completion format for compatibility
1062
+ * 🔥 Enhanced for GPT-5 with reasoning summary support
1063
+ */
1064
+ function convertResponsesAPIToChatCompletion(responsesData: any): any {
1065
+ // Extract content from Responses API format
1066
+ let outputText = responsesData.output_text || ''
1067
+ const usage = responsesData.usage || {}
1068
+
1069
+ // 🚀 GPT-5 Reasoning Summary Integration
1070
+ // If reasoning summary is available, prepend it to the output for transparency
1071
+ if (responsesData.output && Array.isArray(responsesData.output)) {
1072
+ const reasoningItems = responsesData.output.filter(item => item.type === 'reasoning' && item.summary)
1073
+ const messageItems = responsesData.output.filter(item => item.type === 'message')
1074
+
1075
+ if (reasoningItems.length > 0 && messageItems.length > 0) {
1076
+ const reasoningSummary = reasoningItems
1077
+ .map(item => item.summary?.map(s => s.text).join('\n'))
1078
+ .filter(Boolean)
1079
+ .join('\n\n')
1080
+
1081
+ const mainContent = messageItems
1082
+ .map(item => item.content?.map(c => c.text).join('\n'))
1083
+ .filter(Boolean)
1084
+ .join('\n\n')
1085
+
1086
+ if (reasoningSummary) {
1087
+ outputText = `**🧠 Reasoning Process:**\n${reasoningSummary}\n\n**📝 Response:**\n${mainContent}`
1088
+ } else {
1089
+ outputText = mainContent
1090
+ }
1091
+ }
1092
+ }
1093
+
1094
+ return {
1095
+ id: responsesData.id || `chatcmpl-${Date.now()}`,
1096
+ object: 'chat.completion',
1097
+ created: Math.floor(Date.now() / 1000),
1098
+ model: responsesData.model || '',
1099
+ choices: [
1100
+ {
1101
+ index: 0,
1102
+ message: {
1103
+ role: 'assistant',
1104
+ content: outputText,
1105
+ // 🚀 Include reasoning metadata if available
1106
+ ...(responsesData.reasoning && {
1107
+ reasoning: {
1108
+ effort: responsesData.reasoning.effort,
1109
+ summary: responsesData.reasoning.summary,
1110
+ },
1111
+ }),
1112
+ },
1113
+ finish_reason: responsesData.status === 'completed' ? 'stop' : 'length',
1114
+ },
1115
+ ],
1116
+ usage: {
1117
+ prompt_tokens: usage.input_tokens || 0,
1118
+ completion_tokens: usage.output_tokens || 0,
1119
+ total_tokens: (usage.input_tokens || 0) + (usage.output_tokens || 0),
1120
+ // 🔧 GPT-5 Enhanced Usage Details
1121
+ prompt_tokens_details: {
1122
+ cached_tokens: usage.input_tokens_details?.cached_tokens || 0,
1123
+ },
1124
+ completion_tokens_details: {
1125
+ reasoning_tokens: usage.output_tokens_details?.reasoning_tokens || 0,
1126
+ },
1127
+ },
1128
+ }
1129
+ }
1130
+
1131
+ /**
1132
+ * Enhanced getCompletionWithProfile that supports GPT-5 Responses API
1133
+ * 🔥 Optimized for both official OpenAI and third-party GPT-5 providers
1134
+ */
1135
+ async function getGPT5CompletionWithProfile(
1136
+ modelProfile: any,
1137
+ opts: OpenAI.ChatCompletionCreateParams,
1138
+ attempt: number = 0,
1139
+ maxAttempts: number = 10,
1140
+ signal?: AbortSignal,
1141
+ ): Promise<OpenAI.ChatCompletion | AsyncIterable<OpenAI.ChatCompletionChunk>> {
1142
+ const features = getModelFeatures(opts.model)
1143
+ const isOfficialOpenAI = !modelProfile.baseURL ||
1144
+ modelProfile.baseURL.includes('api.openai.com')
1145
+
1146
+ // 🚀 Try Responses API for official OpenAI non-streaming requests
1147
+ if (features.supportsResponsesAPI && !opts.stream && isOfficialOpenAI) {
1148
+ try {
1149
+ debugLogger.api('ATTEMPTING_GPT5_RESPONSES_API', {
1150
+ model: opts.model,
1151
+ baseURL: modelProfile.baseURL || 'official',
1152
+ provider: modelProfile.provider,
1153
+ stream: opts.stream,
1154
+ requestId: getCurrentRequest()?.id,
1155
+ })
1156
+
1157
+ const result = await callGPT5ResponsesAPI(modelProfile, opts, signal)
1158
+
1159
+ debugLogger.api('GPT5_RESPONSES_API_SUCCESS', {
1160
+ model: opts.model,
1161
+ baseURL: modelProfile.baseURL || 'official',
1162
+ requestId: getCurrentRequest()?.id,
1163
+ })
1164
+
1165
+ return result
1166
+ } catch (error) {
1167
+ debugLogger.api('GPT5_RESPONSES_API_FALLBACK', {
1168
+ model: opts.model,
1169
+ error: error.message,
1170
+ baseURL: modelProfile.baseURL || 'official',
1171
+ requestId: getCurrentRequest()?.id,
1172
+ })
1173
+
1174
+ console.warn(
1175
+ `🔄 GPT-5 Responses API failed, falling back to Chat Completions: ${error.message}`
1176
+ )
1177
+ // Fall through to Chat Completions API
1178
+ }
1179
+ }
1180
+
1181
+ // 🌐 Handle third-party GPT-5 providers with enhanced compatibility
1182
+ else if (!isOfficialOpenAI) {
1183
+ debugLogger.api('GPT5_THIRD_PARTY_PROVIDER', {
1184
+ model: opts.model,
1185
+ baseURL: modelProfile.baseURL,
1186
+ provider: modelProfile.provider,
1187
+ supportsResponsesAPI: features.supportsResponsesAPI,
1188
+ requestId: getCurrentRequest()?.id,
1189
+ })
1190
+
1191
+ // 🔧 Apply enhanced parameter optimization for third-party providers
1192
+ console.log(`🌐 Using GPT-5 via third-party provider: ${modelProfile.provider} (${modelProfile.baseURL})`)
1193
+
1194
+ // Some third-party providers may need additional parameter adjustments
1195
+ if (modelProfile.provider === 'azure') {
1196
+ // Azure OpenAI specific adjustments
1197
+ delete opts.reasoning_effort // Azure may not support this yet
1198
+ } else if (modelProfile.provider === 'custom-openai') {
1199
+ // Generic OpenAI-compatible provider optimizations
1200
+ console.log(`🔧 Applying OpenAI-compatible optimizations for custom provider`)
1201
+ }
1202
+ }
1203
+
1204
+ // 📡 Handle streaming requests (Responses API doesn't support streaming yet)
1205
+ else if (opts.stream) {
1206
+ debugLogger.api('GPT5_STREAMING_MODE', {
1207
+ model: opts.model,
1208
+ baseURL: modelProfile.baseURL || 'official',
1209
+ reason: 'responses_api_no_streaming',
1210
+ requestId: getCurrentRequest()?.id,
1211
+ })
1212
+
1213
+ console.log(`🔄 Using Chat Completions for streaming (Responses API streaming not available)`)
1214
+ }
1215
+
1216
+ // 🔧 Enhanced Chat Completions fallback with GPT-5 optimizations
1217
+ debugLogger.api('USING_CHAT_COMPLETIONS_FOR_GPT5', {
1218
+ model: opts.model,
1219
+ baseURL: modelProfile.baseURL || 'official',
1220
+ provider: modelProfile.provider,
1221
+ reason: isOfficialOpenAI ? 'streaming_or_fallback' : 'third_party_provider',
1222
+ requestId: getCurrentRequest()?.id,
1223
+ })
1224
+
1225
+ return await getCompletionWithProfile(
1226
+ modelProfile,
1227
+ opts,
1228
+ attempt,
1229
+ maxAttempts,
1230
+ signal,
1231
+ )
1232
+ }
1233
+
1234
+ /**
1235
+ * Fetch available models from custom OpenAI-compatible API
1236
+ */
1237
+ export async function fetchCustomModels(
1238
+ baseURL: string,
1239
+ apiKey: string,
1240
+ ): Promise<any[]> {
1241
+ try {
1242
+ // Check if baseURL already contains version number (e.g., v1, v2, etc.)
1243
+ const hasVersionNumber = /\/v\d+/.test(baseURL)
1244
+ const cleanBaseURL = baseURL.replace(/\/+$/, '')
1245
+ const modelsURL = hasVersionNumber
1246
+ ? `${cleanBaseURL}/models`
1247
+ : `${cleanBaseURL}/v1/models`
1248
+
1249
+ const response = await fetch(modelsURL, {
1250
+ method: 'GET',
1251
+ headers: {
1252
+ Authorization: `Bearer ${apiKey}`,
1253
+ 'Content-Type': 'application/json',
1254
+ },
1255
+ })
1256
+
1257
+ if (!response.ok) {
1258
+ // Provide user-friendly error messages based on status code
1259
+ if (response.status === 401) {
1260
+ throw new Error(
1261
+ 'Invalid API key. Please check your API key and try again.',
1262
+ )
1263
+ } else if (response.status === 403) {
1264
+ throw new Error(
1265
+ 'API key does not have permission to access models. Please check your API key permissions.',
1266
+ )
1267
+ } else if (response.status === 404) {
1268
+ throw new Error(
1269
+ 'API endpoint not found. Please check if the base URL is correct and supports the /models endpoint.',
1270
+ )
1271
+ } else if (response.status === 429) {
1272
+ throw new Error(
1273
+ 'Too many requests. Please wait a moment and try again.',
1274
+ )
1275
+ } else if (response.status >= 500) {
1276
+ throw new Error(
1277
+ 'API service is temporarily unavailable. Please try again later.',
1278
+ )
1279
+ } else {
1280
+ throw new Error(
1281
+ `Unable to connect to API (${response.status}). Please check your base URL, API key, and internet connection.`,
1282
+ )
1283
+ }
1284
+ }
1285
+
1286
+ const data = await response.json()
1287
+
1288
+ // Validate response format and extract models array
1289
+ let models = []
1290
+
1291
+ if (data && data.data && Array.isArray(data.data)) {
1292
+ // Standard OpenAI format: { data: [...] }
1293
+ models = data.data
1294
+ } else if (Array.isArray(data)) {
1295
+ // Direct array format
1296
+ models = data
1297
+ } else if (data && data.models && Array.isArray(data.models)) {
1298
+ // Alternative format: { models: [...] }
1299
+ models = data.models
1300
+ } else {
1301
+ throw new Error(
1302
+ 'API returned unexpected response format. Expected an array of models or an object with a "data" or "models" array.',
1303
+ )
1304
+ }
1305
+
1306
+ // Ensure we have an array and validate it contains model objects
1307
+ if (!Array.isArray(models)) {
1308
+ throw new Error('API response format error: models data is not an array.')
1309
+ }
1310
+
1311
+ return models
1312
+ } catch (error) {
1313
+ // If it's already our custom error, pass it through
1314
+ if (
1315
+ error instanceof Error &&
1316
+ (error.message.includes('API key') ||
1317
+ error.message.includes('API endpoint') ||
1318
+ error.message.includes('API service') ||
1319
+ error.message.includes('response format'))
1320
+ ) {
1321
+ throw error
1322
+ }
1323
+
1324
+ // For network errors or other issues
1325
+ console.error('Failed to fetch custom API models:', error)
1326
+
1327
+ // Check if it's a network error
1328
+ if (error instanceof Error && error.message.includes('fetch')) {
1329
+ throw new Error(
1330
+ 'Unable to connect to the API. Please check the base URL and your internet connection.',
1331
+ )
1332
+ }
1333
+
1334
+ throw new Error(
1335
+ 'Failed to fetch models from custom API. Please check your configuration and try again.',
1336
+ )
1337
+ }
1338
+ }