(config, messages)
| 2556 | |
| 2557 | // Stream a final text response (no tools, just text output) |
| 2558 | async function streamFinalResponse(config, messages) { |
| 2559 | const target = config.activeModelTarget || getModelTarget(config, 'default'); |
| 2560 | const requestConfig = withModelTarget(config, target); |
| 2561 | const baseUrl = target.baseUrl; |
| 2562 | const systemMsg = { |
| 2563 | role: 'system', |
| 2564 | content: `You are SmallCode, a coding assistant. Summarize what you just did in 1-2 sentences. Be concise.` |
| 2565 | }; |
| 2566 | |
| 2567 | try { |
| 2568 | const headers = buildAuthHeaders(requestConfig); |
| 2569 | |
| 2570 | // Fix #3: Only include messages that form valid pairs. Strip tool_call |
| 2571 | // assistant messages that don't have a following tool result (which causes |
| 2572 | // 400 errors on strict providers). Also strip tool messages whose assistant |
| 2573 | // owner was already dropped by the slice. |
| 2574 | const recent = messages.slice(-8); |
| 2575 | const safeMessages = []; |
| 2576 | for (let i = 0; i < recent.length; i++) { |
| 2577 | const m = recent[i]; |
| 2578 | if (m.tool_calls) { |
| 2579 | // Only include if ALL tool_call_ids have a matching tool result after it |
| 2580 | const ids = m.tool_calls.map(tc => tc.id); |
| 2581 | const hasAll = ids.every(id => recent.slice(i + 1).some(r => r.role === 'tool' && r.tool_call_id === id)); |
| 2582 | if (hasAll) safeMessages.push(m); |
| 2583 | // else skip it |
| 2584 | } else if (m.role === 'tool') { |
| 2585 | // Only include if there's a preceding assistant with this tool_call_id |
| 2586 | const hasOwner = safeMessages.some(s => s.tool_calls && s.tool_calls.some(tc => tc.id === m.tool_call_id)); |
| 2587 | if (hasOwner) safeMessages.push(m); |
| 2588 | // else skip orphan |
| 2589 | } else { |
| 2590 | safeMessages.push(m); |
| 2591 | } |
| 2592 | } |
| 2593 | |
| 2594 | const controller = new AbortController(); |
| 2595 | const timeout = setTimeout(() => controller.abort(), 30000); // 30s timeout for summary |
| 2596 | |
| 2597 | const response = await fetch(`${baseUrl}/chat/completions`, { |
| 2598 | method: 'POST', |
| 2599 | headers, |
| 2600 | body: JSON.stringify({ |
| 2601 | model: target.model, |
| 2602 | messages: [systemMsg, ...safeMessages.slice(-6)], |
| 2603 | stream: true, |
| 2604 | temperature: 0.1, |
| 2605 | max_tokens: 256, |
| 2606 | }), |
| 2607 | signal: controller.signal, |
| 2608 | }); |
| 2609 | clearTimeout(timeout); |
| 2610 | |
| 2611 | if (!response.ok) return null; |
| 2612 | |
| 2613 | const reader = response.body.getReader(); |
| 2614 | const decoder = new TextDecoder(); |
| 2615 | let buffer = ''; |
no test coverage detected