u
This commit is contained in:
@@ -906,7 +906,7 @@
|
|||||||
* type: string
|
* type: string
|
||||||
* role:
|
* role:
|
||||||
* type: string
|
* type: string
|
||||||
* enum: [system, user, assistant, tool, reasoning]
|
* enum: [system, user, assistant, tool, reasoning, usage]
|
||||||
* message:
|
* message:
|
||||||
* type: string
|
* type: string
|
||||||
* ModelMessageCreateInput:
|
* ModelMessageCreateInput:
|
||||||
|
|||||||
@@ -163,48 +163,61 @@ async function buildSystemPrompt(vfsRoot, googleAuthId) {
|
|||||||
* `idle`/`busy` state. The finer SSE states (`thinking`, `working`, `done`)
|
* `idle`/`busy` state. The finer SSE states (`thinking`, `working`, `done`)
|
||||||
* and the streamed `delta` chunks are SSE-only.
|
* and the streamed `delta` chunks are SSE-only.
|
||||||
*/
|
*/
|
||||||
export function toLlmMessage(m) {
|
export function historyToLlmMessages(history) {
|
||||||
let payload = {};
|
const messages = [];
|
||||||
if (typeof m.message === 'string') {
|
for (const m of history) {
|
||||||
|
if (m.role === 'usage' || m.role === 'reasoning') continue;
|
||||||
|
let payload;
|
||||||
try {
|
try {
|
||||||
payload = JSON.parse(m.message);
|
payload = typeof m.message === 'string' ? JSON.parse(m.message) : m.message;
|
||||||
} catch {
|
} catch {
|
||||||
payload = { content: m.message };
|
payload = { content: m.message };
|
||||||
}
|
}
|
||||||
} else if (m.message && typeof m.message === 'object') {
|
if (m.role === 'user') {
|
||||||
payload = m.message;
|
messages.push({ role: 'user', content: payload.content ?? (typeof m.message === 'string' ? m.message : '') });
|
||||||
}
|
} else if (m.role === 'assistant') {
|
||||||
|
messages.push({
|
||||||
if (m.role === 'user') {
|
role: 'assistant',
|
||||||
return { role: 'user', content: payload.content ?? (typeof m.message === 'string' ? m.message : '') };
|
content: payload.content ?? null,
|
||||||
}
|
...(Array.isArray(payload.tool_calls) && payload.tool_calls.length > 0 ? { tool_calls: payload.tool_calls } : {}),
|
||||||
if (m.role === 'assistant') {
|
});
|
||||||
const msg = {
|
} else if (m.role === 'tool') {
|
||||||
role: 'assistant',
|
const toolCallId = payload.tool_call_id ?? '';
|
||||||
content: payload.content ?? null,
|
const toolName = payload.name ?? '';
|
||||||
};
|
const toolParams = payload.parameters ?? payload.arguments ?? '{}';
|
||||||
if (Array.isArray(payload.tool_calls) && payload.tool_calls.length > 0) {
|
const prevMsg = messages[messages.length - 1];
|
||||||
msg.tool_calls = payload.tool_calls;
|
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 messages;
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ConversationManager {
|
export class ConversationManager {
|
||||||
@@ -391,12 +404,11 @@ export class ConversationManager {
|
|||||||
|
|
||||||
const messages = [
|
const messages = [
|
||||||
{ role: 'system', content: await buildSystemPrompt(vfsRoot, googleAuthId) },
|
{ role: 'system', content: await buildSystemPrompt(vfsRoot, googleAuthId) },
|
||||||
...history.map(toLlmMessage),
|
...historyToLlmMessages(history),
|
||||||
];
|
];
|
||||||
|
|
||||||
let text = '';
|
let text = '';
|
||||||
let currentReasoning = '';
|
let currentReasoning = '';
|
||||||
let savedToolCallsTurn = null;
|
|
||||||
|
|
||||||
this.orbiter = new LLMOrbiter(new OpenRouterProvider({ apiKey, apiUrl: env.openrouterApiUrl }), {
|
this.orbiter = new LLMOrbiter(new OpenRouterProvider({ apiKey, apiUrl: env.openrouterApiUrl }), {
|
||||||
onStatus: (status, meta) => {
|
onStatus: (status, meta) => {
|
||||||
@@ -410,43 +422,37 @@ export class ConversationManager {
|
|||||||
text += chunk;
|
text += chunk;
|
||||||
this.broadcast('delta', { delta: chunk, text });
|
this.broadcast('delta', { delta: chunk, text });
|
||||||
},
|
},
|
||||||
onToolCall: async ({ name, args, id, callItem, tool_calls, reasoning_details, reasoning, content }) => {
|
onAssistantMessage: async ({ content, tool_calls, reasoning_details, reasoning }) => {
|
||||||
try {
|
const plain = (reasoning || currentReasoning.trim() || reasoning_details?.find((d) => d?.type === 'reasoning.summary')?.summary || '').trim();
|
||||||
const effectiveToolCalls = tool_calls || (callItem ? [callItem] : [{
|
const encrypted = reasoning_details?.find((d) => d?.type === 'reasoning.encrypted')?.data || '';
|
||||||
id: id || randomUUID(),
|
currentReasoning = '';
|
||||||
type: 'function',
|
|
||||||
function: {
|
|
||||||
name,
|
|
||||||
arguments: typeof args === 'string' ? args : JSON.stringify(args || {}),
|
|
||||||
},
|
|
||||||
}]);
|
|
||||||
|
|
||||||
if (savedToolCallsTurn !== effectiveToolCalls) {
|
if (plain || encrypted) {
|
||||||
savedToolCallsTurn = effectiveToolCalls;
|
try {
|
||||||
const effectiveReasoningDetails = (Array.isArray(reasoning_details) && reasoning_details.length > 0)
|
await db.addMessage(googleAuthId, this.conversationId, {
|
||||||
? reasoning_details
|
role: 'reasoning',
|
||||||
: undefined;
|
message: JSON.stringify({ plain, encrypted }),
|
||||||
const effectiveReasoning = reasoning || currentReasoning.trim() || undefined;
|
});
|
||||||
currentReasoning = '';
|
} catch (err) {
|
||||||
|
console.error(`[ConversationManager ${this.conversationId}] failed to save reasoning message:`, err);
|
||||||
const assistantPayload = {
|
}
|
||||||
content: (content || text).trim() || '',
|
}
|
||||||
tool_calls: effectiveToolCalls,
|
|
||||||
...(effectiveReasoningDetails ? { reasoning_details: effectiveReasoningDetails } : {}),
|
|
||||||
...(effectiveReasoning ? { reasoning: effectiveReasoning } : {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const trimmedContent = (content || text).trim();
|
||||||
|
if (trimmedContent) {
|
||||||
|
try {
|
||||||
await db.addMessage(googleAuthId, this.conversationId, {
|
await db.addMessage(googleAuthId, this.conversationId, {
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
message: JSON.stringify(assistantPayload),
|
message: JSON.stringify({ content: trimmedContent }),
|
||||||
});
|
});
|
||||||
text = '';
|
text = '';
|
||||||
emitCompanyEvent('modelconversation', { companyId: this.companyId });
|
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 }) => {
|
onToolResult: async ({ name, result, id, callItem }) => {
|
||||||
const rawArgs = callItem?.function?.arguments || '';
|
const rawArgs = callItem?.function?.arguments || '';
|
||||||
this.broadcast('state', {
|
this.broadcast('state', {
|
||||||
@@ -461,6 +467,8 @@ export class ConversationManager {
|
|||||||
await db.addMessage(googleAuthId, this.conversationId, {
|
await db.addMessage(googleAuthId, this.conversationId, {
|
||||||
role: 'tool',
|
role: 'tool',
|
||||||
message: JSON.stringify({
|
message: JSON.stringify({
|
||||||
|
name,
|
||||||
|
parameters: rawArgs,
|
||||||
content: typeof result === 'string' ? result : JSON.stringify(result),
|
content: typeof result === 'string' ? result : JSON.stringify(result),
|
||||||
tool_call_id: id || callItem?.id || '',
|
tool_call_id: id || callItem?.id || '',
|
||||||
}),
|
}),
|
||||||
@@ -485,6 +493,8 @@ export class ConversationManager {
|
|||||||
await db.addMessage(googleAuthId, this.conversationId, {
|
await db.addMessage(googleAuthId, this.conversationId, {
|
||||||
role: 'tool',
|
role: 'tool',
|
||||||
message: JSON.stringify({
|
message: JSON.stringify({
|
||||||
|
name,
|
||||||
|
parameters: rawArgs,
|
||||||
content: `Tool error [${name}]: ${error}`,
|
content: `Tool error [${name}]: ${error}`,
|
||||||
tool_call_id: id || callItem?.id || '',
|
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);
|
console.error(`[ConversationManager ${this.conversationId}] failed to save tool error message:`, err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onUsage: (u) => {
|
onUsage: async (u) => {
|
||||||
if (u) {
|
if (u) {
|
||||||
this.broadcast('usage', {
|
this.broadcast('usage', {
|
||||||
input: u.input ?? 0,
|
input: u.input ?? 0,
|
||||||
@@ -502,6 +512,20 @@ export class ConversationManager {
|
|||||||
output: u.output ?? 0,
|
output: u.output ?? 0,
|
||||||
...(u.cost != null ? { cost: u.cost } : {}),
|
...(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
|
// A call that consumed 0 input tokens AND produced 0 output means the
|
||||||
// provider returned nothing at all — not a real generation. LLMOrbiter
|
// 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.';
|
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) {
|
} catch (err) {
|
||||||
if (this.destroyed) return;
|
if (this.destroyed) return;
|
||||||
error = err.message;
|
error = err.message;
|
||||||
|
|||||||
Reference in New Issue
Block a user