From 296fceedc0682569f32307736e8715589607614e Mon Sep 17 00:00:00 2001 From: seb Date: Mon, 5 Oct 2026 11:56:17 +0200 Subject: [PATCH] u --- .env.example | 6 ++++-- src/api/routes/deliveryParse.js | 3 ++- src/api/routes/modelconversation.js | 7 ++---- src/config/env.js | 6 ++---- src/services/conversationManager.js | 17 +++++++++------ src/tools/llm-key.js | 33 +++++++++++++++++++++++++++++ 6 files changed, 53 insertions(+), 19 deletions(-) diff --git a/.env.example b/.env.example index bef201e..18d2d37 100644 --- a/.env.example +++ b/.env.example @@ -37,5 +37,7 @@ KAT_LISTS=0 STEUER_ZONE=Zone-EU OPENROUTER_FREE_KEY=sk-or-v1-3d3c4e -# Default model for agent conversations (optional, defaults to 'openrouter/free') -# OPENROUTER_MODEL=openrouter/free +OPENROUTER_FREE_MODEL=openrouter/free +OPENROUTER_PAID_MODEL=openai/gpt-6-luna:floor + + diff --git a/src/api/routes/deliveryParse.js b/src/api/routes/deliveryParse.js index 3d5f93a..7d03c83 100644 --- a/src/api/routes/deliveryParse.js +++ b/src/api/routes/deliveryParse.js @@ -6,6 +6,7 @@ import { LLMOrbiter, OpenRouterProvider, defineTool, DirectReturnError } from 'l import * as dbCompanySetting from '../../db/companysetting.js'; import * as dbSupplier from '../../db/supplier.js'; import env from '../../config/env.js'; +import { resolveDefaultModel } from '../../tools/llm-key.js'; // ─── Multer — memory storage (no temp files); accepts PDF or text ─────────────── const upload = multer({ @@ -294,7 +295,7 @@ async function parseDocumentManifest({ req, res, pages, sourceLabel }) { }); // ── LLMOrbiter setup ───────────────────────────────────────────── - const model = env.defaultModel; + const model = await resolveDefaultModel(req.user.id); const provider = new OpenRouterProvider({ apiKey }); const orbiter = new LLMOrbiter(provider, { onStatus: (status) => { diff --git a/src/api/routes/modelconversation.js b/src/api/routes/modelconversation.js index 25e95cc..4ce9009 100644 --- a/src/api/routes/modelconversation.js +++ b/src/api/routes/modelconversation.js @@ -1,9 +1,6 @@ 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 { RETIRED_MODELS, resolveDefaultModel } from '../../tools/llm-key.js'; import { emit } from '../sse.js'; import { getOrCreateManager, removeManager } from '../../services/conversationManager.js'; @@ -155,7 +152,7 @@ export function register(app, basePath = '') { // 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 + ? await resolveDefaultModel(req.user.id) : requestedModel; try { diff --git a/src/config/env.js b/src/config/env.js index 3baff0b..4fb3839 100644 --- a/src/config/env.js +++ b/src/config/env.js @@ -27,11 +27,9 @@ export default { jwtSecret: process.env.JWT_SECRET || 'fallback_secret_please_change_in_production', dbProvider: process.env.DB_PROVIDER || (process.env.SQLITE_PATH ? 'sqlite' : 'mssql'), 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', 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: { filename: process.env.SQLITE_PATH || process.env.SQLITE_FILENAME || resolve(__dirname, '../../data/dash.db'), }, diff --git a/src/services/conversationManager.js b/src/services/conversationManager.js index a724096..24fc356 100644 --- a/src/services/conversationManager.js +++ b/src/services/conversationManager.js @@ -20,7 +20,7 @@ import { createPatchTool } from '../tools/patch.js'; import { createRmTool } from '../tools/rm.js'; import { createSearchImagesTool } from '../tools/search-images.js'; import { createAnalyzeImageTool } from '../tools/analyze-image.js'; -import { resolveOpenRouterKey } from '../tools/llm-key.js'; +import { resolveOpenRouterKey, resolveDefaultModel, RETIRED_MODELS, hasConfiguredOpenRouterKey } from '../tools/llm-key.js'; const KEEP_ALIVE_MS = 20_000; @@ -345,12 +345,15 @@ export class ConversationManager { if (conversation.provider !== 'openrouter') { 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; + // Fall back to the configured default when the stored model is missing, blank, or + // retired. If the stored model was defaulted to the free model and the company now has + // a custom key configured, transition to the configured paid default model. + const defaultModel = await resolveDefaultModel(googleAuthId); + const hasCustomKey = await hasConfiguredOpenRouterKey(googleAuthId); + const storedModel = typeof conversation.model === 'string' ? conversation.model.trim() : ''; + const model = (!storedModel || RETIRED_MODELS.has(storedModel) || (storedModel === env.openrouterFreeModel && hasCustomKey)) + ? defaultModel + : storedModel; const apiKey = await resolveOpenRouterKey(googleAuthId); // Transcript comes from the database (includes user, assistant, tool, reasoning). diff --git a/src/tools/llm-key.js b/src/tools/llm-key.js index a9b2f6b..0fef8ba 100644 --- a/src/tools/llm-key.js +++ b/src/tools/llm-key.js @@ -23,4 +23,37 @@ export async function resolveOpenRouterKey(googleAuthId) { if (!apiKey) apiKey = env.openrouterFreeKey?.trim(); if (!apiKey) throw new Error('OpenRouter API key not configured (companysetting: keys.openrouter)'); return apiKey; +} + +// Model ids that are known to return empty responses from OpenRouter. Requests naming one of +// these are transparently rerouted to the default model instead of failing at generation time. +export const RETIRED_MODELS = new Set(['stealth/space-bunny-alpha']); + +/** + * Check if the user's selected company has a custom OpenRouter key configured. + * + * @param {number} googleAuthId + * @returns {Promise} + */ +export async function hasConfiguredOpenRouterKey(googleAuthId) { + if (!googleAuthId) return false; + try { + const setting = await dbCompanySetting.getByKey(googleAuthId, 'keys.openrouter', { maskWriteonly: false }); + return Boolean(setting?.value?.trim()); + } catch { + return false; + } +} + +/** + * Resolve the default model for a user/company: + * - As soon as the user/company has configured `keys.openrouter`, use `env.openrouterPaidModel`. + * - Otherwise (using the server's free key), use `env.openrouterFreeModel`. + * + * @param {number} [googleAuthId] + * @returns {Promise} + */ +export async function resolveDefaultModel(googleAuthId) { + const hasKey = await hasConfiguredOpenRouterKey(googleAuthId); + return hasKey ? env.openrouterPaidModel : env.openrouterFreeModel; } \ No newline at end of file