Multi-Modell + Vision: OpenRouter (GLM 5.3 Flash), Bild-Upload, Tour-Feature, Dashboard-Fixes

- Modell-Registry (DeepSeek direkt + OpenRouter z-ai/glm-5.3-flash), Modellwahl pro Session, Reasoning-Effort max (env-konfigurierbar)
- Bild-Upload für Vision-Modelle (User) + Agent-screenshot()-Tool (nur Vision)
- Tour-Planung: geocode + save_tour mit GPX-Export und Karten-Sektion
- Fix: System-Prompt wird jetzt wirklich an die API gesendet, inkl. aktuellem Dashboard-Dateinamen für Folge-Updates
- Fix: History-Sanitizer gegen 400-Fehler (nie mitten in Tool-Gruppen kürzen)
- Fix: reasoning_content-Roundtrip (Pflicht bei DeepSeek mit tools)
- Fix: Dashboards ohne Browser-Cache ausliefern
This commit is contained in:
nico committed 2026-08-27 17:43:13 +02:00
1 parent eee59363d4
commit 87eb7029f1
10 files changed
+843 -70

No files matched your search

+95 -19
View File
@@ -49,14 +49,15 @@ const now = () => new Date().toISOString();
const uid = () => crypto.randomBytes(12).toString('hex');
const shareToken = () => crypto.randomBytes(16).toString('hex');
// DeepSeek v4 Flash Preise (USD pro 1M Tokens, Peak) – obere Schätzung
// DeepSeek v4 Flash Preise (USD pro 1M Tokens, Peak) – obere Schätzung.
// Andere Modelle (z.B. OpenRouter) legen eigene Preise in der Registry an.
const PRICES = { inputCacheHit: 0.014, inputCacheMiss: 0.44, output: 1.32 };
function computeCost(u) {
function computeCost(u, prices = PRICES) {
return (
((u.cacheHitTokens || 0) * PRICES.inputCacheHit +
(u.cacheMissTokens || 0) * PRICES.inputCacheMiss +
(u.completionTokens || 0) * PRICES.output) /
((u.cacheHitTokens || 0) * prices.inputCacheHit +
(u.cacheMissTokens || 0) * prices.inputCacheMiss +
(u.completionTokens || 0) * prices.output) /
1e6
);
}
@@ -72,12 +73,23 @@ function computeCost(u) {
add('cache_hit_tokens', 'INTEGER DEFAULT 0');
add('cache_miss_tokens', 'INTEGER DEFAULT 0');
add('cost_estimate', 'REAL DEFAULT 0');
add('model', 'TEXT');
}
// Migration: Messages-Spalten ergänzen (Reasoning-Roundtrip + Bild-Upload)
{
const cols = db.prepare(`PRAGMA table_info(messages)`).all().map((c) => c.name);
const add = (name, def) => {
if (!cols.includes(name)) db.exec(`ALTER TABLE messages ADD COLUMN ${name} ${def}`);
};
add('reasoning', 'TEXT');
add('image', 'TEXT');
}
const stmts = {
createSession: db.prepare(
`INSERT INTO sessions (id, title, owner_name, share_token, status, created_at, updated_at)
VALUES (?, ?, ?, ?, 'idle', ?, ?)`
`INSERT INTO sessions (id, title, owner_name, share_token, status, model, created_at, updated_at)
VALUES (?, ?, ?, ?, 'idle', ?, ?, ?)`
),
getSession: db.prepare(`SELECT * FROM sessions WHERE id = ?`),
getSessionByShare: db.prepare(`SELECT * FROM sessions WHERE share_token = ?`),
@@ -90,6 +102,7 @@ const stmts = {
),
setSessionTitle: db.prepare(`UPDATE sessions SET title = ?, updated_at = ? WHERE id = ?`),
setSessionDashboard: db.prepare(`UPDATE sessions SET dashboard = ?, updated_at = ? WHERE id = ?`),
setSessionModel: db.prepare(`UPDATE sessions SET model = ?, updated_at = ? WHERE id = ?`),
addUsage: db.prepare(
`UPDATE sessions SET prompt_tokens = prompt_tokens + ?, completion_tokens = completion_tokens + ?,
cache_hit_tokens = cache_hit_tokens + ?, cache_miss_tokens = cache_miss_tokens + ?,
@@ -98,8 +111,8 @@ const stmts = {
deleteSession: db.prepare(`DELETE FROM sessions WHERE id = ?`),
insertMessage: db.prepare(
`INSERT INTO messages (session_id, role, content, tool_name, tool_call_id, tool_calls, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`
`INSERT INTO messages (session_id, role, content, tool_name, tool_call_id, tool_calls, reasoning, image, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
),
listMessages: db.prepare(`SELECT * FROM messages WHERE session_id = ? ORDER BY id ASC`),
lastMessage: db.prepare(`SELECT * FROM messages WHERE session_id = ? ORDER BY id DESC LIMIT 1`),
@@ -113,13 +126,13 @@ const stmts = {
listPushSubs: db.prepare(`SELECT * FROM push_subscriptions`),
};
function createSession(ownerName, title = 'Neue Recherche') {
function createSession(ownerName, title = 'Neue Recherche', model = null) {
const id = uid();
stmts.createSession.run(id, title, ownerName, shareToken(), now(), now());
stmts.createSession.run(id, title, ownerName, shareToken(), model, now(), now());
return stmts.getSession.get(id);
}
function createMessage(sessionId, { role, content, toolName, toolCallId, toolCalls }) {
function createMessage(sessionId, { role, content, toolName, toolCallId, toolCalls, reasoning, image }) {
stmts.insertMessage.run(
sessionId,
role,
@@ -127,6 +140,8 @@ function createMessage(sessionId, { role, content, toolName, toolCallId, toolCal
toolName ?? null,
toolCallId ?? null,
toolCalls ? JSON.stringify(toolCalls) : null,
reasoning ?? null,
image ?? null,
now()
);
return stmts.lastMessage.get(sessionId);
@@ -139,23 +154,83 @@ function getSessionMessages(sessionId) {
}));
}
/** Baut aus dem Chat-Verlauf das Messages-Array für die DeepSeek-API. */
function toApiMessages(sessionId) {
const API_HISTORY_MAX = 60;
/**
* Bereinigt den Verlauf für die API: entfernt unvollständige Tool-Gruppen
* (assistant-Tool-Call ohne passende tool-Antwort, z.B. nach einem abgebrochenen
* Job) und kürzt auf die letzten API_HISTORY_MAX Nachrichten, ohne jemals mitten
* in einer Tool-Gruppe zu schneiden. Verwaiste tool-Nachrichten lehnt die
* DeepSeek-API sonst mit 400 ab ("Messages with role 'tool' must be a response
* to a preceding message with 'tool_calls'").
*/
function sanitizeHistory(out, max = API_HISTORY_MAX) {
const resolved = new Set();
const withCalls = [];
const owner = new Array(out.length).fill(-1);
let cur = -1;
for (let i = 0; i < out.length; i++) {
const m = out[i];
if (m.role === 'assistant' && m.tool_calls) {
withCalls.push(i);
cur = i;
} else if (m.role === 'tool') {
owner[i] = cur;
if (m.tool_call_id) resolved.add(m.tool_call_id);
}
}
const incomplete = new Set(
withCalls.filter((i) => (out[i].tool_calls || []).some((tc) => !resolved.has(tc.id)))
);
const kept = [];
for (let i = 0; i < out.length; i++) {
if (incomplete.has(i)) continue;
if (out[i].role === 'tool' && incomplete.has(owner[i])) continue;
kept.push(out[i]);
}
if (kept.length <= max) return kept;
let start = kept.length - max;
while (start < kept.length && kept[start].role === 'tool') start++;
return kept.slice(start);
}
/**
* Baut aus dem Chat-Verlauf das Messages-Array für die Modell-API.
* @param {object} opts { provider, vision } – steuert Bild-Content (Vision-Modelle)
* und den reasoning_content-Roundtrip (Pflicht bei DeepSeek mit tools).
*/
function toApiMessages(sessionId, opts = {}) {
const { provider = 'deepseek', vision = false } = opts;
const msgs = getSessionMessages(sessionId);
const out = [];
for (const m of msgs) {
if (m.role === 'user') out.push({ role: 'user', content: m.content });
else if (m.role === 'assistant') {
if (m.role === 'user') {
const msg = { role: 'user' };
if (m.image && vision) {
msg.content = [
{ type: 'text', text: m.content || '' },
{ type: 'image_url', image_url: { url: m.image } },
];
} else {
msg.content = m.content;
}
out.push(msg);
} else if (m.role === 'assistant') {
const msg = { role: 'assistant' };
if (m.tool_calls) msg.tool_calls = m.tool_calls;
if (m.content) msg.content = m.content;
else if (!m.tool_calls) msg.content = '';
// DeepSeek verlangt bei tools-Requests den Roundtrip des Reasoning-Texts
if (provider === 'deepseek' && m.reasoning) msg.reasoning_content = m.reasoning;
out.push(msg);
} else if (m.role === 'tool') {
out.push({ role: 'tool', tool_call_id: m.tool_call_id, content: m.content ?? '' });
}
}
return out.slice(-60);
return sanitizeHistory(out);
}
module.exports = {
@@ -168,13 +243,14 @@ module.exports = {
setSessionStatus: (id, status) => stmts.setSessionStatus.run(status, now(), id),
setSessionTitle: (id, title) => stmts.setSessionTitle.run(title, now(), id),
setSessionDashboard: (id, dashboard) => stmts.setSessionDashboard.run(dashboard, now(), id),
setSessionUsage: (id, u) =>
setSessionModel: (id, model) => stmts.setSessionModel.run(model, now(), id),
setSessionUsage: (id, u, prices) =>
stmts.addUsage.run(
u.promptTokens || 0,
u.completionTokens || 0,
u.cacheHitTokens || 0,
u.cacheMissTokens || 0,
computeCost(u),
computeCost(u, prices),
id
),
computeCost,