(config, messages)
| 2126 | |
| 2127 | // Make a chat completion request (non-streaming for tool use, streaming for final response) |
| 2128 | async function chatCompletion(config, messages) { |
| 2129 | let target = config.activeModelTarget || getModelTarget(config, 'default'); |
| 2130 | let requestConfig = withModelTarget(config, target); |
| 2131 | let baseUrl = target.baseUrl; |
| 2132 | const systemMsg = { |
| 2133 | role: 'system', |
| 2134 | content: buildCompactSystemPrompt(currentTaskType, messages), |
| 2135 | }; |
| 2136 | |
| 2137 | try { |
| 2138 | // Strip ANSI escape codes from all message content before sending to model. |
| 2139 | // Thinking models (Qwen3, etc.) will reproduce ANSI codes they see in context, |
| 2140 | // causing corrupted bash commands like "find ... -\x1b[38;2mtype f". |
| 2141 | function stripAnsiFromMsg(msg) { |
| 2142 | if (!msg || typeof msg.content !== 'string') return msg; |
| 2143 | return { ...msg, content: msg.content.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').replace(/\x1b\][^\x07]*\x07/g, '') }; |
| 2144 | } |
| 2145 | const processedMessages = messages.map(stripAnsiFromMsg); |
| 2146 | |
| 2147 | // Cache-split (Feature #14): when SMALLCODE_CACHE_SPLIT=true, dynamic |
| 2148 | // context (memory, knowledge) is moved out of the system prompt and into |
| 2149 | // a [CONTEXT] block prepended to the latest user message. This keeps the |
| 2150 | // system prompt stable across turns so remote APIs with prefix caching |
| 2151 | // (Anthropic, OpenAI) get cache hits on the static portion. |
| 2152 | const dynamicCtx = buildDynamicContext(messages); |
| 2153 | if (dynamicCtx) { |
| 2154 | const lastIdx = processedMessages.reduce((last, m, i) => m.role === 'user' ? i : last, -1); |
| 2155 | if (lastIdx >= 0) { |
| 2156 | const lastMsg = processedMessages[lastIdx]; |
| 2157 | if (typeof lastMsg.content === 'string') { |
| 2158 | processedMessages[lastIdx] = { |
| 2159 | ...lastMsg, |
| 2160 | content: dynamicCtx + lastMsg.content, |
| 2161 | }; |
| 2162 | } |
| 2163 | // If last user message is multimodal (image array), prepend as first text element |
| 2164 | else if (Array.isArray(lastMsg.content)) { |
| 2165 | const firstText = lastMsg.content.find(c => c.type === 'text'); |
| 2166 | if (firstText) { |
| 2167 | processedMessages[lastIdx] = { |
| 2168 | ...lastMsg, |
| 2169 | content: [ |
| 2170 | { type: 'text', text: dynamicCtx + firstText.text }, |
| 2171 | ...lastMsg.content.filter(c => c !== firstText), |
| 2172 | ], |
| 2173 | }; |
| 2174 | } |
| 2175 | } |
| 2176 | } |
| 2177 | } |
| 2178 | |
| 2179 | // Transform messages with images into multimodal format |
| 2180 | // OPTIMIZATION: Only re-extract images from the LAST user message (the new one). |
| 2181 | // Older messages that had images already had their content consumed; re-reading |
| 2182 | // the image from disk on every call is both wasteful (disk I/O) and causes |
| 2183 | // context overflow (a 1MB PNG = ~330k base64 tokens sent on EVERY API call). |
| 2184 | const { extractImages, formatImagesForAPI, modelSupportsVision } = require('../src/session/images'); |
| 2185 | const lastUserIdx = processedMessages.length > 0 |
no test coverage detected