From 609ca55af2a6b45505b915af9d576283318f443b Mon Sep 17 00:00:00 2001 From: seb Date: Mon, 5 Oct 2026 23:57:08 +0200 Subject: [PATCH] u --- src/api/openapi.js | 2 +- src/services/conversationManager.js | 167 +++++++++++++++------------- 2 files changed, 91 insertions(+), 78 deletions(-) diff --git a/src/api/openapi.js b/src/api/openapi.js index 44ac969..4c3cd47 100644 --- a/src/api/openapi.js +++ b/src/api/openapi.js @@ -906,7 +906,7 @@ * type: string * role: * type: string - * enum: [system, user, assistant, tool, reasoning] + * enum: [system, user, assistant, tool, reasoning, usage] * message: * type: string * ModelMessageCreateInput: diff --git a/src/services/conversationManager.js b/src/services/conversationManager.js index fadaa81..0accad5 100644 --- a/src/services/conversationManager.js +++ b/src/services/conversationManager.js @@ -163,48 +163,61 @@ async function buildSystemPrompt(vfsRoot, googleAuthId) { * `idle`/`busy` state. The finer SSE states (`thinking`, `working`, `done`) * and the streamed `delta` chunks are SSE-only. */ -export function toLlmMessage(m) { - let payload = {}; - if (typeof m.message === 'string') { +export function historyToLlmMessages(history) { + const messages = []; + for (const m of history) { + if (m.role === 'usage' || m.role === 'reasoning') continue; + let payload; try { - payload = JSON.parse(m.message); + payload = typeof m.message === 'string' ? JSON.parse(m.message) : m.message; } catch { payload = { content: m.message }; } - } else if (m.message && typeof m.message === 'object') { - payload = m.message; - } - - if (m.role === 'user') { - return { role: 'user', content: payload.content ?? (typeof m.message === 'string' ? m.message : '') }; - } - if (m.role === 'assistant') { - const msg = { - role: 'assistant', - content: payload.content ?? null, - }; - if (Array.isArray(payload.tool_calls) && payload.tool_calls.length > 0) { - msg.tool_calls = payload.tool_calls; + if (m.role === 'user') { + messages.push({ role: 'user', content: payload.content ?? (typeof m.message === 'string' ? m.message : '') }); + } else if (m.role === 'assistant') { + messages.push({ + role: 'assistant', + content: payload.content ?? null, + ...(Array.isArray(payload.tool_calls) && payload.tool_calls.length > 0 ? { tool_calls: payload.tool_calls } : {}), + }); + } else if (m.role === 'tool') { + const toolCallId = payload.tool_call_id ?? ''; + const toolName = payload.name ?? ''; + const toolParams = payload.parameters ?? payload.arguments ?? '{}'; + const prevMsg = messages[messages.length - 1]; + if (!prevMsg || prevMsg.role !== 'assistant' || !prevMsg.tool_calls) { + messages.push({ + role: 'assistant', + content: null, + tool_calls: [{ + id: toolCallId, + type: 'function', + function: { + name: toolName, + arguments: typeof toolParams === 'string' ? toolParams : JSON.stringify(toolParams), + }, + }], + }); + } else if (!prevMsg.tool_calls.some((tc) => tc.id === toolCallId)) { + prevMsg.tool_calls.push({ + id: toolCallId, + type: 'function', + function: { + name: toolName, + arguments: typeof toolParams === 'string' ? toolParams : JSON.stringify(toolParams), + }, + }); + } + messages.push({ + role: 'tool', + tool_call_id: toolCallId, + content: typeof payload.content === 'string' ? payload.content : JSON.stringify(payload.content ?? ''), + ...(toolName ? { name: toolName } : {}), + }); } - if (Array.isArray(payload.reasoning_details) && payload.reasoning_details.length > 0) { - msg.reasoning_details = payload.reasoning_details; - } - if (payload.reasoning) { - msg.reasoning = payload.reasoning; - } else if (payload.reasoning_content) { - msg.reasoning = payload.reasoning_content; - } - return msg; } - if (m.role === 'tool') { - return { - role: 'tool', - tool_call_id: payload.tool_call_id ?? '', - content: typeof payload.content === 'string' ? payload.content : JSON.stringify(payload.content ?? ''), - ...(payload.name ? { name: payload.name } : {}), - }; - } - return { role: m.role, content: payload.content ?? m.message }; + return messages; } export class ConversationManager { @@ -391,12 +404,11 @@ export class ConversationManager { const messages = [ { role: 'system', content: await buildSystemPrompt(vfsRoot, googleAuthId) }, - ...history.map(toLlmMessage), + ...historyToLlmMessages(history), ]; let text = ''; let currentReasoning = ''; - let savedToolCallsTurn = null; this.orbiter = new LLMOrbiter(new OpenRouterProvider({ apiKey, apiUrl: env.openrouterApiUrl }), { onStatus: (status, meta) => { @@ -410,43 +422,37 @@ export class ConversationManager { text += chunk; this.broadcast('delta', { delta: chunk, text }); }, - onToolCall: async ({ name, args, id, callItem, tool_calls, reasoning_details, reasoning, content }) => { - try { - const effectiveToolCalls = tool_calls || (callItem ? [callItem] : [{ - id: id || randomUUID(), - type: 'function', - function: { - name, - arguments: typeof args === 'string' ? args : JSON.stringify(args || {}), - }, - }]); + onAssistantMessage: async ({ content, tool_calls, reasoning_details, reasoning }) => { + const plain = (reasoning || currentReasoning.trim() || reasoning_details?.find((d) => d?.type === 'reasoning.summary')?.summary || '').trim(); + const encrypted = reasoning_details?.find((d) => d?.type === 'reasoning.encrypted')?.data || ''; + currentReasoning = ''; - if (savedToolCallsTurn !== effectiveToolCalls) { - savedToolCallsTurn = effectiveToolCalls; - const effectiveReasoningDetails = (Array.isArray(reasoning_details) && reasoning_details.length > 0) - ? reasoning_details - : undefined; - const effectiveReasoning = reasoning || currentReasoning.trim() || undefined; - currentReasoning = ''; - - const assistantPayload = { - content: (content || text).trim() || '', - tool_calls: effectiveToolCalls, - ...(effectiveReasoningDetails ? { reasoning_details: effectiveReasoningDetails } : {}), - ...(effectiveReasoning ? { reasoning: effectiveReasoning } : {}), - }; + if (plain || encrypted) { + try { + await db.addMessage(googleAuthId, this.conversationId, { + role: 'reasoning', + message: JSON.stringify({ plain, encrypted }), + }); + } catch (err) { + console.error(`[ConversationManager ${this.conversationId}] failed to save reasoning message:`, err); + } + } + const trimmedContent = (content || text).trim(); + if (trimmedContent) { + try { await db.addMessage(googleAuthId, this.conversationId, { role: 'assistant', - message: JSON.stringify(assistantPayload), + message: JSON.stringify({ content: trimmedContent }), }); text = ''; emitCompanyEvent('modelconversation', { companyId: this.companyId }); + } catch (err) { + console.error(`[ConversationManager ${this.conversationId}] failed to save assistant message:`, err); } - } catch (err) { - console.error(`[ConversationManager ${this.conversationId}] failed to save tool call message:`, err); } }, + onToolCall: async () => {}, onToolResult: async ({ name, result, id, callItem }) => { const rawArgs = callItem?.function?.arguments || ''; this.broadcast('state', { @@ -461,6 +467,8 @@ export class ConversationManager { await db.addMessage(googleAuthId, this.conversationId, { role: 'tool', message: JSON.stringify({ + name, + parameters: rawArgs, content: typeof result === 'string' ? result : JSON.stringify(result), tool_call_id: id || callItem?.id || '', }), @@ -485,6 +493,8 @@ export class ConversationManager { await db.addMessage(googleAuthId, this.conversationId, { role: 'tool', message: JSON.stringify({ + name, + parameters: rawArgs, content: `Tool error [${name}]: ${error}`, tool_call_id: id || callItem?.id || '', }), @@ -494,7 +504,7 @@ export class ConversationManager { console.error(`[ConversationManager ${this.conversationId}] failed to save tool error message:`, err); } }, - onUsage: (u) => { + onUsage: async (u) => { if (u) { this.broadcast('usage', { input: u.input ?? 0, @@ -502,6 +512,20 @@ export class ConversationManager { output: u.output ?? 0, ...(u.cost != null ? { cost: u.cost } : {}), }); + try { + await db.addMessage(googleAuthId, this.conversationId, { + role: 'usage', + message: JSON.stringify({ + input: u.input ?? 0, + cached: u.cached ?? 0, + output: u.output ?? 0, + ...(u.cost != null ? { cost: u.cost } : {}), + }), + }); + emitCompanyEvent('modelconversation', { companyId: this.companyId }); + } catch (err) { + console.error(`[ConversationManager ${this.conversationId}] failed to save usage message:`, err); + } } // A call that consumed 0 input tokens AND produced 0 output means the // provider returned nothing at all — not a real generation. LLMOrbiter @@ -585,17 +609,6 @@ export class ConversationManager { ); error = 'The model returned an empty response. Please try again.'; } - - if (content || finalReasoningDetails || finalReasoning) { - await db.addMessage(googleAuthId, this.conversationId, { - message: JSON.stringify({ - content: content || '', - ...(finalReasoningDetails ? { reasoning_details: finalReasoningDetails } : {}), - ...(finalReasoning ? { reasoning: finalReasoning } : {}), - }), - role: 'assistant', - }); - } } catch (err) { if (this.destroyed) return; error = err.message;