diff --git a/docs/api/routes/companysetting/set.md b/docs/api/routes/companysetting/set.md index 8706456..84ad76d 100644 --- a/docs/api/routes/companysetting/set.md +++ b/docs/api/routes/companysetting/set.md @@ -6,7 +6,8 @@ Upsert company setting Sets (upserts) a company setting for the selected company. Only a fixed allow-list of keys can be set: `ui.theme`, `keys.tavily`, -`keys.openrouter`, `keys.serper`, `labelprinter.addr`, `labelprinter.type`. +`keys.openrouter`, `keys.serper`, `systemprompt-prefix`, +`systemprompt-suffix`, `default-model`. ## Path parameters diff --git a/src/api/routes/companysetting.js b/src/api/routes/companysetting.js index e38057f..95ff0dc 100644 --- a/src/api/routes/companysetting.js +++ b/src/api/routes/companysetting.js @@ -5,8 +5,9 @@ const ALLOWED_KEYS = [ 'keys.tavily', 'keys.openrouter', 'keys.serper', - 'labelprinter.addr', - 'labelprinter.type', + 'systemprompt-prefix', + 'systemprompt-suffix', + 'default-model', ]; function requireSelectedCompany(req) { @@ -206,7 +207,8 @@ export function register(app, basePath = '') { * description: | * Sets (upserts) a company setting for the selected company. * Only a fixed allow-list of keys can be set: `ui.theme`, `keys.tavily`, - * `keys.openrouter`, `keys.serper`, `labelprinter.addr`, `labelprinter.type`. + * `keys.openrouter`, `keys.serper`, `systemprompt-prefix`, + * `systemprompt-suffix`, `default-model`. * security: * - cookieAuth: [] * x-markdown: companysetting/set.md diff --git a/src/services/conversationManager.js b/src/services/conversationManager.js index 8de6391..efbabfe 100644 --- a/src/services/conversationManager.js +++ b/src/services/conversationManager.js @@ -73,9 +73,27 @@ function buildSkillIndex(vfsRoot) { * (docs/api/routes.md), read on each run so it stays current. * * @param {string} vfsRoot Resolved VFS root for this user's company, used to list SKILL files. - * @returns {string} + * @param {number} [googleAuthId] + * @returns {Promise} */ -async function buildSystemPrompt(vfsRoot) { +async function buildSystemPrompt(vfsRoot, googleAuthId) { + let prefix = ''; + let suffix = ''; + if (googleAuthId) { + try { + const prefixSetting = await dbCompanySetting.getByKey(googleAuthId, 'systemprompt-prefix', { maskWriteonly: false }); + prefix = prefixSetting?.value?.trim() || ''; + } catch (err) { + console.error('conversation: failed to read systemprompt-prefix:', err.message); + } + try { + const suffixSetting = await dbCompanySetting.getByKey(googleAuthId, 'systemprompt-suffix', { maskWriteonly: false }); + suffix = suffixSetting?.value?.trim() || ''; + } catch (err) { + console.error('conversation: failed to read systemprompt-suffix:', err.message); + } + } + let routesDoc = ''; try { routesDoc = readFileSync(ROUTES_DOC_PATH, 'utf8').trim(); @@ -83,7 +101,7 @@ async function buildSystemPrompt(vfsRoot) { console.error('conversation: failed to read routes doc:', err); } const today = new Date().toISOString().split('T')[0]; - return [ + const basePrompt = [ 'You are an intelligent assistant specialized in exploring, querying, and managing data from the Dashback REST API.', `Today's date: ${today}`, '', @@ -129,6 +147,12 @@ async function buildSystemPrompt(vfsRoot) { '- Format responses clearly using markdown tables, bullet lists, or bold key-value pairs where appropriate.', ...(routesDoc ? ['', 'DASHBACK API ROUTES:', routesDoc] : []), ].join('\n'); + + const sections = []; + if (prefix) sections.push(prefix); + sections.push(basePrompt); + if (suffix) sections.push(suffix); + return sections.join('\n\n'); } /** @@ -351,7 +375,7 @@ export class ConversationManager { 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)) + const model = (!storedModel || RETIRED_MODELS.has(storedModel) || (storedModel === env.openrouterFreeModel && (hasCustomKey || defaultModel !== env.openrouterFreeModel))) ? defaultModel : storedModel; const apiKey = await resolveOpenRouterKey(googleAuthId); @@ -369,7 +393,7 @@ export class ConversationManager { } const messages = [ - { role: 'system', content: await buildSystemPrompt(vfsRoot) }, + { role: 'system', content: await buildSystemPrompt(vfsRoot, googleAuthId) }, ...history.map(toLlmMessage), ]; diff --git a/src/tools/llm-key.js b/src/tools/llm-key.js index 0fef8ba..850dacd 100644 --- a/src/tools/llm-key.js +++ b/src/tools/llm-key.js @@ -47,13 +47,27 @@ export async function hasConfiguredOpenRouterKey(googleAuthId) { /** * 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`. + * - If companysetting `default-model` is set, return it (works in both paid and no-key modes). + * - Otherwise, if the user/company has configured `keys.openrouter`, use `env.openrouterPaidModel` + * (which defaults to 'openai/gpt-6-luna:floor'). + * - Otherwise (no key set / free mode), use `env.openrouterFreeModel` + * (which defaults to 'openrouter/free'). * * @param {number} [googleAuthId] * @returns {Promise} */ export async function resolveDefaultModel(googleAuthId) { + if (googleAuthId) { + try { + const setting = await dbCompanySetting.getByKey(googleAuthId, 'default-model', { maskWriteonly: false }); + const customModel = setting?.value?.trim(); + if (customModel) return customModel; + } catch (err) { + console.error('default-model: failed to read company setting:', err.message); + } + } const hasKey = await hasConfiguredOpenRouterKey(googleAuthId); - return hasKey ? env.openrouterPaidModel : env.openrouterFreeModel; + return hasKey + ? (env.openrouterPaidModel || 'openai/gpt-6-luna:floor') + : (env.openrouterFreeModel || 'openrouter/free'); } \ No newline at end of file