This commit is contained in:
seb
2026-10-05 11:56:17 +02:00
parent 4a08e7f20b
commit 296fceedc0
6 changed files with 53 additions and 19 deletions

View File

@@ -37,5 +37,7 @@ 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_FREE_MODEL=openrouter/free
# OPENROUTER_MODEL=openrouter/free OPENROUTER_PAID_MODEL=openai/gpt-6-luna:floor

View File

@@ -6,6 +6,7 @@ import { LLMOrbiter, OpenRouterProvider, defineTool, DirectReturnError } from 'l
import * as dbCompanySetting from '../../db/companysetting.js'; import * as dbCompanySetting from '../../db/companysetting.js';
import * as dbSupplier from '../../db/supplier.js'; import * as dbSupplier from '../../db/supplier.js';
import env from '../../config/env.js'; import env from '../../config/env.js';
import { resolveDefaultModel } from '../../tools/llm-key.js';
// ─── Multer — memory storage (no temp files); accepts PDF or text ─────────────── // ─── Multer — memory storage (no temp files); accepts PDF or text ───────────────
const upload = multer({ const upload = multer({
@@ -294,7 +295,7 @@ async function parseDocumentManifest({ req, res, pages, sourceLabel }) {
}); });
// ── LLMOrbiter setup ───────────────────────────────────────────── // ── LLMOrbiter setup ─────────────────────────────────────────────
const model = env.defaultModel; const model = await resolveDefaultModel(req.user.id);
const provider = new OpenRouterProvider({ apiKey }); const provider = new OpenRouterProvider({ apiKey });
const orbiter = new LLMOrbiter(provider, { const orbiter = new LLMOrbiter(provider, {
onStatus: (status) => { onStatus: (status) => {

View File

@@ -1,9 +1,6 @@
import * as db from '../../db/modelconversation.js'; import * as db from '../../db/modelconversation.js';
import env from '../../config/env.js'; import env from '../../config/env.js';
import { RETIRED_MODELS, resolveDefaultModel } from '../../tools/llm-key.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';
@@ -155,7 +152,7 @@ export function register(app, basePath = '') {
// ignoring the client's choice entirely, so an explicit model selection still works. // ignoring the client's choice entirely, so an explicit model selection still works.
const requestedModel = typeof model === 'string' ? model.trim() : ''; const requestedModel = typeof model === 'string' ? model.trim() : '';
const effectiveModel = RETIRED_MODELS.has(requestedModel) || !requestedModel const effectiveModel = RETIRED_MODELS.has(requestedModel) || !requestedModel
? env.defaultModel ? await resolveDefaultModel(req.user.id)
: requestedModel; : requestedModel;
try { try {

View File

@@ -27,11 +27,9 @@ export default {
jwtSecret: process.env.JWT_SECRET || 'fallback_secret_please_change_in_production', jwtSecret: process.env.JWT_SECRET || 'fallback_secret_please_change_in_production',
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,
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, 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'),
}, },

View File

@@ -20,7 +20,7 @@ import { createPatchTool } from '../tools/patch.js';
import { createRmTool } from '../tools/rm.js'; import { createRmTool } from '../tools/rm.js';
import { createSearchImagesTool } from '../tools/search-images.js'; import { createSearchImagesTool } from '../tools/search-images.js';
import { createAnalyzeImageTool } from '../tools/analyze-image.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; const KEEP_ALIVE_MS = 20_000;
@@ -345,12 +345,15 @@ 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 // Fall back to the configured default when the stored model is missing, blank, or
// an old or malformed conversation row cannot send an empty `model` and make the // retired. If the stored model was defaulted to the free model and the company now has
// provider return an empty response. // a custom key configured, transition to the configured paid default model.
const model = (typeof conversation.model === 'string' && conversation.model.trim()) const defaultModel = await resolveDefaultModel(googleAuthId);
? conversation.model.trim() const hasCustomKey = await hasConfiguredOpenRouterKey(googleAuthId);
: env.defaultModel; 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); 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).

View File

@@ -24,3 +24,36 @@ export async function resolveOpenRouterKey(googleAuthId) {
if (!apiKey) throw new Error('OpenRouter API key not configured (companysetting: keys.openrouter)'); if (!apiKey) throw new Error('OpenRouter API key not configured (companysetting: keys.openrouter)');
return apiKey; 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<boolean>}
*/
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<string>}
*/
export async function resolveDefaultModel(googleAuthId) {
const hasKey = await hasConfiguredOpenRouterKey(googleAuthId);
return hasKey ? env.openrouterPaidModel : env.openrouterFreeModel;
}