(apiKey, messages, modelEnum, modelName, sessionId = null)
| 152 | * @param {string} [modelName] - Model name string (optional) |
| 153 | */ |
| 154 | export function buildRawGetChatMessageRequest(apiKey, messages, modelEnum, modelName, sessionId = null) { |
| 155 | const parts = []; |
| 156 | const conversationId = randomUUID(); |
| 157 | |
| 158 | // Field 1: Metadata — pass through the caller's session id so the |
| 159 | // legacy Raw channel uses the same per-LS session as Cascade instead |
| 160 | // of a fresh UUID per request (anti-fingerprint). |
| 161 | parts.push(writeMessageField(1, buildMetadata(apiKey, undefined, sessionId))); |
| 162 | |
| 163 | // Field 2: repeated ChatMessage (skip system, handled separately). |
| 164 | // Windsurf's legacy RawGetChatMessage backend rejects role=tool and |
| 165 | // doesn't know about assistant tool_calls. Degrade both to plain text |
| 166 | // so multi-turn conversations that carry tool history still flow |
| 167 | // through without triggering "proto: cannot parse invalid wire-format |
| 168 | // data" upstream. Cascade models are unaffected — they use a different |
| 169 | // endpoint (SendUserCascadeMessage) with full tool support. |
| 170 | let systemPrompt = ''; |
| 171 | for (const msg of messages) { |
| 172 | if (msg.role === 'system') { |
| 173 | systemPrompt += (systemPrompt ? '\n' : '') + |
| 174 | (typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)); |
| 175 | continue; |
| 176 | } |
| 177 | |
| 178 | let source; |
| 179 | let text; |
| 180 | const baseText = typeof msg.content === 'string' ? msg.content |
| 181 | : Array.isArray(msg.content) ? msg.content.filter(c => c.type === 'text').map(c => c.text).join('\n') |
| 182 | : msg.content == null ? '' : JSON.stringify(msg.content); |
| 183 | |
| 184 | switch (msg.role) { |
| 185 | case 'user': |
| 186 | source = SOURCE.USER; |
| 187 | text = baseText; |
| 188 | break; |
| 189 | case 'assistant': |
| 190 | source = SOURCE.ASSISTANT; |
| 191 | // If the assistant previously called tools, append the call descriptions |
| 192 | // so the model sees its own prior tool usage as text. Empty string OK. |
| 193 | if (Array.isArray(msg.tool_calls) && msg.tool_calls.length) { |
| 194 | const tcLines = msg.tool_calls.map(tc => |
| 195 | `[called tool ${tc.function?.name || 'unknown'} with ${tc.function?.arguments || '{}'}]` |
| 196 | ).join('\n'); |
| 197 | text = baseText ? `${baseText}\n${tcLines}` : tcLines; |
| 198 | } else { |
| 199 | text = baseText; |
| 200 | } |
| 201 | break; |
| 202 | case 'tool': |
| 203 | // Rewrite tool-result turn as a synthetic user utterance so the |
| 204 | // server-side schema accepts it. |
| 205 | source = SOURCE.USER; |
| 206 | text = `[tool result${msg.tool_call_id ? ` for ${msg.tool_call_id}` : ''}]: ${baseText}`; |
| 207 | break; |
| 208 | default: |
| 209 | source = SOURCE.USER; |
| 210 | text = baseText; |
| 211 | } |
no test coverage detected