This commit is contained in:
seb
2026-10-05 23:57:08 +02:00
parent 18699eb40b
commit 609ca55af2
2 changed files with 91 additions and 78 deletions

View File

@@ -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:

View File

@@ -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;