u
This commit is contained in:
@@ -36,6 +36,8 @@ KAT_LISTS=0
|
||||
# Tax zone name for VAT rates (tSteuerzone.cName)
|
||||
STEUER_ZONE=Zone-EU
|
||||
|
||||
# OpenRouter (leave OPENROUTER_API_URL unset for default OpenRouter endpoint)
|
||||
# OPENROUTER_API_URL=http://127.0.0.1:8787/api/v1/chat/completions
|
||||
OPENROUTER_FREE_KEY=sk-or-v1-3d3c4e
|
||||
OPENROUTER_FREE_MODEL=openrouter/free
|
||||
OPENROUTER_PAID_MODEL=openai/gpt-6-luna:floor
|
||||
|
||||
@@ -5,11 +5,11 @@ Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||
Stream conversation events (SSE)
|
||||
|
||||
Opens a Server-Sent Events stream for the specified conversation.
|
||||
Streams `message` events (persisted user/assistant messages), live `delta`
|
||||
text chunks of the model answer, and `state` events (`thinking`, `working`,
|
||||
`done`). The SSE states are not stored; the database only knows `idle`/`busy`.
|
||||
`done` carries `usage` (`input`, `cached`, `output`, `cost`) on success or
|
||||
`error` (string) if the run failed.
|
||||
Streams live `delta` text chunks of the model answer, `usage` stats per turn,
|
||||
and `state` events (`thinking`, `connected`, `firstevent`, `reasoning`,
|
||||
`content`, `working`, `toolcall`, `done`). The SSE states are not stored;
|
||||
the database only knows `idle`/`busy`.
|
||||
`done` indicates generation has finished, carrying `error` (string) only if the run failed.
|
||||
|
||||
|
||||
## Path parameters
|
||||
@@ -20,7 +20,7 @@ text chunks of the model answer, and `state` events (`thinking`, `working`,
|
||||
|
||||
## Response `200`
|
||||
|
||||
SSE stream (`text/event-stream`). Emits `state`, `delta`, and `message` events.
|
||||
SSE stream (`text/event-stream`). Emits `state`, `delta`, and `usage` events.
|
||||
|
||||
|
||||
## Errors
|
||||
|
||||
@@ -296,7 +296,7 @@ async function parseDocumentManifest({ req, res, pages, sourceLabel }) {
|
||||
|
||||
// ── LLMOrbiter setup ─────────────────────────────────────────────
|
||||
const model = await resolveDefaultModel(req.user.id);
|
||||
const provider = new OpenRouterProvider({ apiKey });
|
||||
const provider = new OpenRouterProvider({ apiKey, apiUrl: env.openrouterApiUrl });
|
||||
const orbiter = new LLMOrbiter(provider, {
|
||||
onStatus: (status) => {
|
||||
console.log(`[llmorbiter] Status: ${status}`);
|
||||
|
||||
@@ -313,11 +313,11 @@ export function register(app, basePath = '') {
|
||||
* summary: Stream conversation events (SSE)
|
||||
* description: |
|
||||
* Opens a Server-Sent Events stream for the specified conversation.
|
||||
* Streams `message` events (persisted user/assistant messages), live `delta`
|
||||
* text chunks of the model answer, and `state` events (`thinking`, `working`,
|
||||
* `done`). The SSE states are not stored; the database only knows `idle`/`busy`.
|
||||
* `done` carries `usage` (`input`, `cached`, `output`, `cost`) on success or
|
||||
* `error` (string) if the run failed.
|
||||
* Streams live `delta` text chunks of the model answer, `usage` stats per turn,
|
||||
* and `state` events (`thinking`, `connected`, `firstevent`, `reasoning`,
|
||||
* `content`, `working`, `toolcall`, `done`). The SSE states are not stored;
|
||||
* the database only knows `idle`/`busy`.
|
||||
* `done` indicates generation has finished, carrying `error` (string) only if the run failed.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
@@ -330,7 +330,7 @@ export function register(app, basePath = '') {
|
||||
* responses:
|
||||
* 200:
|
||||
* description: |
|
||||
* SSE stream (`text/event-stream`). Emits `state`, `delta`, and `message` events.
|
||||
* SSE stream (`text/event-stream`). Emits `state`, `delta`, and `usage` events.
|
||||
* content:
|
||||
* text/event-stream:
|
||||
* schema:
|
||||
@@ -340,16 +340,22 @@ export function register(app, basePath = '') {
|
||||
* data: {"state":"thinking"}
|
||||
*
|
||||
* event: state
|
||||
* data: {"state":"working"}
|
||||
* data: {"state":"connected"}
|
||||
*
|
||||
* event: state
|
||||
* data: {"state":"firstevent"}
|
||||
*
|
||||
* event: state
|
||||
* data: {"state":"content"}
|
||||
*
|
||||
* event: delta
|
||||
* data: {"delta":"Hel","text":"Hel"}
|
||||
*
|
||||
* event: message
|
||||
* data: {"id":2,"conversation_id":1,"created_at":"...","role":"assistant","message":"Hello there"}
|
||||
* event: usage
|
||||
* data: {"input":20,"cached":0,"output":3,"cost":0.0001}
|
||||
*
|
||||
* event: state
|
||||
* data: {"state":"done","usage":{"input":20,"cached":0,"output":3,"cost":0.0001}}
|
||||
* data: {"state":"done"}
|
||||
* 400:
|
||||
* description: Invalid ID or no company selected
|
||||
* content:
|
||||
|
||||
@@ -26,6 +26,7 @@ export default {
|
||||
implicitCreate: process.env.IMPLICIT_CREATE === 'true',
|
||||
jwtSecret: process.env.JWT_SECRET || 'fallback_secret_please_change_in_production',
|
||||
dbProvider: process.env.DB_PROVIDER || (process.env.SQLITE_PATH ? 'sqlite' : 'mssql'),
|
||||
openrouterApiUrl: process.env.OPENROUTER_API_URL,
|
||||
openrouterFreeKey: process.env.OPENROUTER_FREE_KEY,
|
||||
openrouterFreeModel: process.env.OPENROUTER_FREE_MODEL || 'openrouter/free',
|
||||
openrouterPaidModel: process.env.OPENROUTER_PAID_MODEL || 'openai/gpt-6-luna:floor',
|
||||
|
||||
@@ -303,7 +303,6 @@ export class ConversationManager {
|
||||
throw err;
|
||||
}
|
||||
|
||||
this.broadcast('message', userMsg);
|
||||
this.broadcast('state', { state: 'thinking' });
|
||||
emitCompanyEvent('modelconversation', { companyId: this.companyId });
|
||||
|
||||
@@ -343,7 +342,6 @@ export class ConversationManager {
|
||||
throw err;
|
||||
}
|
||||
|
||||
this.broadcast('message', toolMsg);
|
||||
this.broadcast('state', { state: 'working' });
|
||||
emitCompanyEvent('modelconversation', { companyId: this.companyId });
|
||||
|
||||
@@ -361,7 +359,6 @@ export class ConversationManager {
|
||||
* @param {number} googleAuthId
|
||||
*/
|
||||
async runGeneration(googleAuthId) {
|
||||
let usage = null;
|
||||
let error = null;
|
||||
|
||||
try {
|
||||
@@ -397,33 +394,23 @@ export class ConversationManager {
|
||||
...history.map(toLlmMessage),
|
||||
];
|
||||
|
||||
let working = false;
|
||||
let text = '';
|
||||
let currentReasoning = '';
|
||||
let savedToolCallsTurn = null;
|
||||
|
||||
const markWorking = () => {
|
||||
if (working) return;
|
||||
working = true;
|
||||
this.broadcast('state', { state: 'working' });
|
||||
};
|
||||
|
||||
this.orbiter = new LLMOrbiter(new OpenRouterProvider({ apiKey }), {
|
||||
onStatus: (status) => {
|
||||
if (status === 'working') markWorking();
|
||||
this.orbiter = new LLMOrbiter(new OpenRouterProvider({ apiKey, apiUrl: env.openrouterApiUrl }), {
|
||||
onStatus: (status, meta) => {
|
||||
this.broadcast('state', { state: status, ...(meta || {}) });
|
||||
},
|
||||
onReasoning: (reasoning) => {
|
||||
markWorking();
|
||||
currentReasoning = reasoning;
|
||||
this.broadcast('reasoning', { text: reasoning });
|
||||
},
|
||||
onContent: (chunk) => {
|
||||
markWorking();
|
||||
text += chunk;
|
||||
this.broadcast('delta', { delta: chunk, text });
|
||||
},
|
||||
onToolCall: async ({ name, args, id, callItem, tool_calls, reasoning_details, reasoning, content }) => {
|
||||
markWorking();
|
||||
try {
|
||||
const effectiveToolCalls = tool_calls || (callItem ? [callItem] : [{
|
||||
id: id || randomUUID(),
|
||||
@@ -449,12 +436,11 @@ export class ConversationManager {
|
||||
...(effectiveReasoning ? { reasoning: effectiveReasoning } : {}),
|
||||
};
|
||||
|
||||
const callMsg = await db.addMessage(googleAuthId, this.conversationId, {
|
||||
await db.addMessage(googleAuthId, this.conversationId, {
|
||||
role: 'assistant',
|
||||
message: JSON.stringify(assistantPayload),
|
||||
});
|
||||
text = '';
|
||||
this.broadcast('message', callMsg);
|
||||
emitCompanyEvent('modelconversation', { companyId: this.companyId });
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -462,15 +448,23 @@ export class ConversationManager {
|
||||
}
|
||||
},
|
||||
onToolResult: async ({ name, result, id, callItem }) => {
|
||||
const rawArgs = callItem?.function?.arguments || '';
|
||||
this.broadcast('state', {
|
||||
state: 'toolcall',
|
||||
id: id || callItem?.id || '',
|
||||
name,
|
||||
arguments: rawArgs,
|
||||
content: rawArgs,
|
||||
result,
|
||||
});
|
||||
try {
|
||||
const resMsg = await db.addMessage(googleAuthId, this.conversationId, {
|
||||
await db.addMessage(googleAuthId, this.conversationId, {
|
||||
role: 'tool',
|
||||
message: JSON.stringify({
|
||||
content: typeof result === 'string' ? result : JSON.stringify(result),
|
||||
tool_call_id: id || callItem?.id || '',
|
||||
}),
|
||||
});
|
||||
this.broadcast('message', resMsg);
|
||||
emitCompanyEvent('modelconversation', { companyId: this.companyId });
|
||||
} catch (err) {
|
||||
console.error(`[ConversationManager ${this.conversationId}] failed to save tool result message:`, err);
|
||||
@@ -478,21 +472,37 @@ export class ConversationManager {
|
||||
},
|
||||
onToolError: async ({ name, error, id, callItem }) => {
|
||||
console.error(`[ConversationManager ${this.conversationId}] Tool "${name}" errored:`, error);
|
||||
const rawArgs = callItem?.function?.arguments || '';
|
||||
this.broadcast('state', {
|
||||
state: 'toolcall',
|
||||
id: id || callItem?.id || '',
|
||||
name,
|
||||
arguments: rawArgs,
|
||||
content: rawArgs,
|
||||
error: String(error),
|
||||
});
|
||||
try {
|
||||
const errMsg = await db.addMessage(googleAuthId, this.conversationId, {
|
||||
await db.addMessage(googleAuthId, this.conversationId, {
|
||||
role: 'tool',
|
||||
message: JSON.stringify({
|
||||
content: `Tool error [${name}]: ${error}`,
|
||||
tool_call_id: id || callItem?.id || '',
|
||||
}),
|
||||
});
|
||||
this.broadcast('message', errMsg);
|
||||
emitCompanyEvent('modelconversation', { companyId: this.companyId });
|
||||
} catch (err) {
|
||||
console.error(`[ConversationManager ${this.conversationId}] failed to save tool error message:`, err);
|
||||
}
|
||||
},
|
||||
onUsage: (u) => {
|
||||
if (u) {
|
||||
this.broadcast('usage', {
|
||||
input: u.input ?? 0,
|
||||
cached: u.cached ?? 0,
|
||||
output: u.output ?? 0,
|
||||
...(u.cost != null ? { cost: u.cost } : {}),
|
||||
});
|
||||
}
|
||||
// A call that consumed 0 input tokens AND produced 0 output means the
|
||||
// provider returned nothing at all — not a real generation. LLMOrbiter
|
||||
// treats "no content and no tool calls" as "keep looping", so several of
|
||||
@@ -577,7 +587,7 @@ export class ConversationManager {
|
||||
}
|
||||
|
||||
if (content || finalReasoningDetails || finalReasoning) {
|
||||
const assistantMsg = await db.addMessage(googleAuthId, this.conversationId, {
|
||||
await db.addMessage(googleAuthId, this.conversationId, {
|
||||
message: JSON.stringify({
|
||||
content: content || '',
|
||||
...(finalReasoningDetails ? { reasoning_details: finalReasoningDetails } : {}),
|
||||
@@ -585,15 +595,7 @@ export class ConversationManager {
|
||||
}),
|
||||
role: 'assistant',
|
||||
});
|
||||
this.broadcast('message', assistantMsg);
|
||||
}
|
||||
|
||||
usage = {
|
||||
input: result.usage.input,
|
||||
cached: result.usage.cached,
|
||||
output: result.usage.output,
|
||||
cost: result.usage.cost,
|
||||
};
|
||||
} catch (err) {
|
||||
if (this.destroyed) return;
|
||||
error = err.message;
|
||||
@@ -609,7 +611,7 @@ export class ConversationManager {
|
||||
console.error(`[ConversationManager ${this.conversationId}] failed to reset state:`, err);
|
||||
}
|
||||
emitCompanyEvent('modelconversation', { companyId: this.companyId });
|
||||
this.broadcast('state', error ? { state: 'done', error } : { state: 'done', usage });
|
||||
this.broadcast('state', error ? { state: 'done', error } : { state: 'done' });
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
||||
@@ -373,7 +373,7 @@ export function createAnalyzeImageTool({ googleAuthId, model }) {
|
||||
: question || 'Describe this image, judge its visual quality, flag any watermark, text overlay or defect, and say whether it is usable as a product or category image.';
|
||||
|
||||
const apiKey = await resolveOpenRouterKey(googleAuthId);
|
||||
const provider = new OpenRouterProvider({ apiKey });
|
||||
const provider = new OpenRouterProvider({ apiKey, apiUrl: env.openrouterApiUrl });
|
||||
const response = await provider.chatCompletions({
|
||||
model,
|
||||
messages: [
|
||||
|
||||
Reference in New Issue
Block a user