u
This commit is contained in:
@@ -37,3 +37,5 @@ KAT_LISTS=0
|
|||||||
STEUER_ZONE=Zone-EU
|
STEUER_ZONE=Zone-EU
|
||||||
|
|
||||||
OPENROUTER_FREE_KEY=sk-or-v1-3d3c4e
|
OPENROUTER_FREE_KEY=sk-or-v1-3d3c4e
|
||||||
|
# Default model for agent conversations (optional, defaults to 'openrouter/free')
|
||||||
|
# OPENROUTER_MODEL=openrouter/free
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ async function parseDocumentManifest({ req, res, pages, sourceLabel }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ── LLMOrbiter setup ─────────────────────────────────────────────
|
// ── LLMOrbiter setup ─────────────────────────────────────────────
|
||||||
const model = 'stealth/space-bunny-alpha';//'~openai/gpt-luna-latest:floor';
|
const model = env.defaultModel;
|
||||||
const provider = new OpenRouterProvider({ apiKey });
|
const provider = new OpenRouterProvider({ apiKey });
|
||||||
const orbiter = new LLMOrbiter(provider, {
|
const orbiter = new LLMOrbiter(provider, {
|
||||||
onStatus: (status) => {
|
onStatus: (status) => {
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import * as db from '../../db/modelconversation.js';
|
import * as db from '../../db/modelconversation.js';
|
||||||
|
import env from '../../config/env.js';
|
||||||
|
|
||||||
|
// Model ids that are known to return empty responses from OpenRouter. Requests naming one of
|
||||||
|
// these are transparently rerouted to `env.defaultModel` instead of failing at generation time.
|
||||||
|
const RETIRED_MODELS = new Set(['stealth/space-bunny-alpha']);
|
||||||
import { emit } from '../sse.js';
|
import { emit } from '../sse.js';
|
||||||
import { getOrCreateManager, removeManager } from '../../services/conversationManager.js';
|
import { getOrCreateManager, removeManager } from '../../services/conversationManager.js';
|
||||||
|
|
||||||
@@ -145,8 +150,16 @@ export function register(app, basePath = '') {
|
|||||||
|
|
||||||
const { provider, model } = req.body ?? {};
|
const { provider, model } = req.body ?? {};
|
||||||
|
|
||||||
|
// Clients may still send the retired `stealth/space-bunny-alpha` id, which now returns
|
||||||
|
// empty responses from the provider. Map it onto the configured default rather than
|
||||||
|
// ignoring the client's choice entirely, so an explicit model selection still works.
|
||||||
|
const requestedModel = typeof model === 'string' ? model.trim() : '';
|
||||||
|
const effectiveModel = RETIRED_MODELS.has(requestedModel) || !requestedModel
|
||||||
|
? env.defaultModel
|
||||||
|
: requestedModel;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const conversation = await db.create(req.user.id, { provider, model });
|
const conversation = await db.create(req.user.id, { provider, model: effectiveModel });
|
||||||
emit('modelconversation', { companyId: selected.companyId });
|
emit('modelconversation', { companyId: selected.companyId });
|
||||||
res.status(201).json({ conversation });
|
res.status(201).json({ conversation });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ export default {
|
|||||||
dbProvider: process.env.DB_PROVIDER || (process.env.SQLITE_PATH ? 'sqlite' : 'mssql'),
|
dbProvider: process.env.DB_PROVIDER || (process.env.SQLITE_PATH ? 'sqlite' : 'mssql'),
|
||||||
openrouterFreeKey: process.env.OPENROUTER_FREE_KEY,
|
openrouterFreeKey: process.env.OPENROUTER_FREE_KEY,
|
||||||
serperApiKey: process.env.SERPER_API_KEY,
|
serperApiKey: process.env.SERPER_API_KEY,
|
||||||
|
// Default model for agent conversations. `openrouter/free` routes to whatever free model
|
||||||
|
// OpenRouter currently offers, which avoids pinning to a specific upstream id that can be
|
||||||
|
// withdrawn or become unavailable. A conversation's stored model still wins when set.
|
||||||
|
defaultModel: process.env.OPENROUTER_MODEL || 'openrouter/free',
|
||||||
sqlite: {
|
sqlite: {
|
||||||
filename: process.env.SQLITE_PATH || process.env.SQLITE_FILENAME || resolve(__dirname, '../../data/dash.db'),
|
filename: process.env.SQLITE_PATH || process.env.SQLITE_FILENAME || resolve(__dirname, '../../data/dash.db'),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -345,6 +345,12 @@ export class ConversationManager {
|
|||||||
if (conversation.provider !== 'openrouter') {
|
if (conversation.provider !== 'openrouter') {
|
||||||
throw new Error(`Unsupported provider: ${conversation.provider}`);
|
throw new Error(`Unsupported provider: ${conversation.provider}`);
|
||||||
}
|
}
|
||||||
|
// Fall back to the configured default when the stored model is missing or blank, so
|
||||||
|
// an old or malformed conversation row cannot send an empty `model` and make the
|
||||||
|
// provider return an empty response.
|
||||||
|
const model = (typeof conversation.model === 'string' && conversation.model.trim())
|
||||||
|
? conversation.model.trim()
|
||||||
|
: env.defaultModel;
|
||||||
const apiKey = await resolveOpenRouterKey(googleAuthId);
|
const apiKey = await resolveOpenRouterKey(googleAuthId);
|
||||||
|
|
||||||
// Transcript comes from the database (includes user, assistant, tool, reasoning).
|
// Transcript comes from the database (includes user, assistant, tool, reasoning).
|
||||||
@@ -459,16 +465,37 @@ export class ConversationManager {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onUsage: (u) => {
|
onUsage: (u) => {
|
||||||
|
// 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
|
||||||
|
// these in a row burn iterations for nothing. Flag them loudly so the
|
||||||
|
// cause is visible instead of showing up as a silent weird loop.
|
||||||
|
if (u.input === 0 && u.output === 0) {
|
||||||
|
console.warn(
|
||||||
|
`[conversation ${this.conversationId}] EMPTY LLM RESPONSE ` +
|
||||||
|
`(iteration reported no usage at all). The provider returned no content ` +
|
||||||
|
`and no tool calls, so the orchestrator will retry. ` +
|
||||||
|
`If this repeats, the upstream model/API is failing — check the model id ` +
|
||||||
|
`and the key.`
|
||||||
|
);
|
||||||
|
}
|
||||||
console.log(
|
console.log(
|
||||||
`[conversation ${this.conversationId}] usage: input=${u.input}, cached=${u.cached}, output=${u.output}` +
|
`[conversation ${this.conversationId}] usage: input=${u.input}, cached=${u.cached}, output=${u.output}` +
|
||||||
(u.cost != null ? `, cost=$${u.cost.toFixed(4)}` : '')
|
(u.cost != null ? `, cost=$${u.cost.toFixed(4)}` : '')
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
onLLMCall: (call) => {
|
||||||
|
console.log(
|
||||||
|
`[conversation ${this.conversationId}] LLM call #${call.iteration}: ` +
|
||||||
|
`model=${call.model}, messages=${call.messageCount}, flow=${call.messages}, ` +
|
||||||
|
`promptChars=${call.promptSize}`
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await this.orbiter.run({
|
const result = await this.orbiter.run({
|
||||||
messages,
|
messages,
|
||||||
model: conversation.model,
|
model,
|
||||||
tools: [
|
tools: [
|
||||||
createDashbackRequestTool({ googleAuthId }),
|
createDashbackRequestTool({ googleAuthId }),
|
||||||
createAskUserTool(),
|
createAskUserTool(),
|
||||||
@@ -481,7 +508,7 @@ export class ConversationManager {
|
|||||||
createPatchTool({ googleAuthId }),
|
createPatchTool({ googleAuthId }),
|
||||||
createRmTool({ googleAuthId }),
|
createRmTool({ googleAuthId }),
|
||||||
createSearchImagesTool({ googleAuthId }),
|
createSearchImagesTool({ googleAuthId }),
|
||||||
createAnalyzeImageTool({ googleAuthId, model: conversation.model }),
|
createAnalyzeImageTool({ googleAuthId, model }),
|
||||||
createResetVfsTool({ googleAuthId }),
|
createResetVfsTool({ googleAuthId }),
|
||||||
createApiSuggestionsOrIssuesTool({ googleAuthId, companyId: this.companyId }),
|
createApiSuggestionsOrIssuesTool({ googleAuthId, companyId: this.companyId }),
|
||||||
],
|
],
|
||||||
@@ -489,6 +516,17 @@ export class ConversationManager {
|
|||||||
|
|
||||||
if (this.destroyed) return;
|
if (this.destroyed) return;
|
||||||
|
|
||||||
|
// Log the shape of every run, including degenerate ones. When nothing comes back
|
||||||
|
// there is nothing persisted and the UI just sits there — this line is the only
|
||||||
|
// clue, so make it explicit about which field was empty.
|
||||||
|
console.log(
|
||||||
|
`[conversation ${this.conversationId}] run finished: ` +
|
||||||
|
`iterations=${result?.iterations}, ` +
|
||||||
|
`content=${result?.content ? `${result.content.length} chars` : 'EMPTY'}, ` +
|
||||||
|
`toolCalls=${result?.toolResults ? Object.keys(result.toolResults).length : 0}, ` +
|
||||||
|
`directReturn=${result?.directReturn === true}`
|
||||||
|
);
|
||||||
|
|
||||||
// Direct return (e.g. from ask_user) halts without adding an additional assistant message.
|
// Direct return (e.g. from ask_user) halts without adding an additional assistant message.
|
||||||
if (result?.directReturn) {
|
if (result?.directReturn) {
|
||||||
return;
|
return;
|
||||||
@@ -500,6 +538,16 @@ export class ConversationManager {
|
|||||||
: undefined;
|
: undefined;
|
||||||
const finalReasoning = result.reasoning || currentReasoning.trim() || undefined;
|
const finalReasoning = result.reasoning || currentReasoning.trim() || undefined;
|
||||||
|
|
||||||
|
// Nothing came back at all: no content, no reasoning. Surface it to the user rather
|
||||||
|
// than silently returning to `idle` with an empty turn, which reads as a hang.
|
||||||
|
if (!content && !finalReasoningDetails && !finalReasoning) {
|
||||||
|
console.error(
|
||||||
|
`[conversation ${this.conversationId}] generation produced no content after ` +
|
||||||
|
`${result?.iterations ?? 0} iteration(s). The model returned neither text nor tool calls.`
|
||||||
|
);
|
||||||
|
error = 'The model returned an empty response. Please try again.';
|
||||||
|
}
|
||||||
|
|
||||||
if (content || finalReasoningDetails || finalReasoning) {
|
if (content || finalReasoningDetails || finalReasoning) {
|
||||||
const assistantMsg = await db.addMessage(googleAuthId, this.conversationId, {
|
const assistantMsg = await db.addMessage(googleAuthId, this.conversationId, {
|
||||||
message: JSON.stringify({
|
message: JSON.stringify({
|
||||||
|
|||||||
8
todo.md
8
todo.md
@@ -1,15 +1,9 @@
|
|||||||
Fehler gefunden: Verletzungen der Eindeutigkeit werden als HTTP-500-Fehler weitergegeben
|
|
||||||
Konsistent und reproduzierbar an vier Endpunkten – ein doppelter Name/Wert trifft auf eine eindeutige Datenbankbeschränkung und wird als Fehler 500 {"error":"Internal server error"}anstelle eines 4xx-Fehlers angezeigt:
|
|
||||||
|
|
||||||
POST /attributename/{id}/valuemit einem bestehenden Wert → 500
|
|
||||||
POST /attributenamemit bestehendem Namen → 500
|
|
||||||
POST /propertynamemit bereits existierendem Namen ( Notiz) → 500
|
|
||||||
Nichts wird gespeichert (verifiziert), daher ist es zwar kosmetisch, aber fehleranfällig – dennoch ist es die einzige Klasse, die interne Informationen preisgibt und die Fehlertoleranz erhöht. Bemerkenswerterweise POST /categoryfunktioniert es auch mit einem doppelten Namen (201), das Verhalten ist also auch zwischen den Modulen inkonsistent .
|
|
||||||
|
|
||||||
Beobachtungen, keine Fehler
|
Beobachtungen, keine Fehler
|
||||||
PUT /artificlelist/ optionslistDoppelte Einträge ablehnen statt zusammenführen – vertretbar, aber nicht dokumentiert.
|
PUT /artificlelist/ optionslistDoppelte Einträge ablehnen statt zusammenführen – vertretbar, aber nicht dokumentiert.
|
||||||
Lange Werte werden von der Datenbank stillschweigend abgeschnitten, anstatt validiert zu werden (5000 Zeichen → 201 mit einer abgeschnittenen Zeile).
|
Lange Werte werden von der Datenbank stillschweigend abgeschnitten, anstatt validiert zu werden (5000 Zeichen → 201 mit einer abgeschnittenen Zeile).
|
||||||
POST /article/{sku}/childrenDie Dokumentation beschreibt ein optionales Element, skudas von der API jedoch immer abgelehnt wird sku is not allowed.... Der angegebene Pfad ist nicht funktionsfähig – um einen bestehenden Artikel einzubinden, ist eine direkte Verknüpfung erforderlich POST /article/{sku}/parent, die in der Dokumentation nicht beschrieben wird. Eine Korrektur der Dokumentation oder eine zusätzliche Route wäre wünschenswert.
|
POST /article/{sku}/childrenDie Dokumentation beschreibt ein optionales Element, skudas von der API jedoch immer abgelehnt wird sku is not allowed.... Der angegebene Pfad ist nicht funktionsfähig – um einen bestehenden Artikel einzubinden, ist eine direkte Verknüpfung erforderlich POST /article/{sku}/father, die in der Dokumentation nicht beschrieben wird. Eine Korrektur der Dokumentation oder eine zusätzliche Route wäre wünschenswert.
|
||||||
⚠️ Aufräumarbeiten unvollständig – Handlungsbedarf erforderlich
|
⚠️ Aufräumarbeiten unvollständig – Handlungsbedarf erforderlich
|
||||||
Mein Aufräumskript löste eine Fehlermeldung aus GET /articleIndex( windowSize must be an integer between 1 and 1000; ich habe 100000 übergeben), bevor irgendwelche Löschvorgänge durchgeführt wurden. Es wurde nichts entfernt. Zurück blieb:
|
Mein Aufräumskript löste eine Fehlermeldung aus GET /articleIndex( windowSize must be an integer between 1 and 1000; ich habe 100000 übergeben), bevor irgendwelche Löschvorgänge durchgeführt wurden. Es wurde nichts entfernt. Zurück blieb:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user