Initial commit with Dockerfile and demo code
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Runtime copilot settings.
|
||||
*
|
||||
* Precedence: saved settings file > environment > built-in defaults.
|
||||
*
|
||||
* Settings are persisted to a small JSON file rather than held only in memory, so
|
||||
* a model chosen while preparing a demo survives a server restart. They are
|
||||
* deliberately NOT written back into .env - that file holds the API key, and a
|
||||
* process that rewrites its own secrets file is a bad habit to build.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||
export const SETTINGS_FILE = path.join(HERE, '..', '..', '.copilot-settings.json');
|
||||
|
||||
/** Fields a client is allowed to change, with validation for each. */
|
||||
const FIELDS = {
|
||||
provider: {
|
||||
validate: (v) => ['auto', 'openrouter', 'ollama', 'fallback'].includes(v),
|
||||
error: 'provider must be auto, openrouter, ollama or fallback',
|
||||
},
|
||||
openRouterModel: {
|
||||
validate: (v) => typeof v === 'string' && v.length > 0 && v.length < 200,
|
||||
error: 'openRouterModel must be a non-empty model id',
|
||||
},
|
||||
ollamaModel: {
|
||||
validate: (v) => typeof v === 'string' && v.length > 0 && v.length < 200,
|
||||
error: 'ollamaModel must be a non-empty model id',
|
||||
},
|
||||
ollamaHost: {
|
||||
validate: (v) => typeof v === 'string' && v.length > 0 && v.length < 300,
|
||||
error: 'ollamaHost must be a URL or host:port',
|
||||
},
|
||||
temperature: {
|
||||
validate: (v) => Number.isFinite(v) && v >= 0 && v <= 2,
|
||||
error: 'temperature must be between 0 and 2',
|
||||
},
|
||||
maxTokens: {
|
||||
validate: (v) => Number.isInteger(v) && v >= 128 && v <= 8000,
|
||||
error: 'maxTokens must be an integer between 128 and 8000',
|
||||
},
|
||||
reasoningEffort: {
|
||||
validate: (v) => ['none', 'low', 'medium', 'high'].includes(v),
|
||||
error: 'reasoningEffort must be none, low, medium or high',
|
||||
},
|
||||
};
|
||||
|
||||
export function defaultsFromEnv(env = process.env) {
|
||||
return {
|
||||
// 'auto' resolves by availability at detect() time.
|
||||
provider: (env.COPILOT_PROVIDER || 'auto').trim().toLowerCase(),
|
||||
openRouterModel: (env.OPENROUTER_MODEL || 'meta-llama/llama-3.3-70b-instruct').trim(),
|
||||
ollamaModel: (env.OLLAMA_MODEL || 'qwen3.5-4b-32k:latest').trim(),
|
||||
ollamaHost: (env.OLLAMA_HOST || 'http://127.0.0.1:11434').trim(),
|
||||
temperature: 0.3,
|
||||
maxTokens: 2000,
|
||||
// Reasoning tokens count against the output budget on most providers, so
|
||||
// 'low' keeps answers from being swallowed by a reasoning block and keeps
|
||||
// time-to-first-token usable in a live demo.
|
||||
reasoningEffort: 'low',
|
||||
};
|
||||
}
|
||||
|
||||
export function loadSettings(env = process.env) {
|
||||
const base = defaultsFromEnv(env);
|
||||
try {
|
||||
const raw = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'));
|
||||
for (const key of Object.keys(FIELDS)) {
|
||||
if (raw[key] !== undefined && FIELDS[key].validate(raw[key])) base[key] = raw[key];
|
||||
}
|
||||
} catch {
|
||||
/* no saved settings yet, or unreadable - environment defaults stand */
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a patch. Returns { ok, errors, clean }.
|
||||
*
|
||||
* Unknown keys are rejected rather than ignored, so a typo in a field name fails
|
||||
* loudly instead of silently doing nothing.
|
||||
*/
|
||||
export function validatePatch(patch) {
|
||||
const errors = [];
|
||||
const clean = {};
|
||||
for (const [key, value] of Object.entries(patch || {})) {
|
||||
const field = FIELDS[key];
|
||||
if (!field) { errors.push(`unknown setting "${key}"`); continue; }
|
||||
if (!field.validate(value)) { errors.push(field.error); continue; }
|
||||
clean[key] = value;
|
||||
}
|
||||
return { ok: errors.length === 0, errors, clean };
|
||||
}
|
||||
|
||||
export function saveSettings(settings) {
|
||||
const out = {};
|
||||
for (const key of Object.keys(FIELDS)) {
|
||||
if (settings[key] !== undefined) out[key] = settings[key];
|
||||
}
|
||||
fs.writeFileSync(SETTINGS_FILE, `${JSON.stringify(out, null, 2)}\n`, 'utf8');
|
||||
return out;
|
||||
}
|
||||
|
||||
export function resetSettings() {
|
||||
try {
|
||||
fs.unlinkSync(SETTINGS_FILE);
|
||||
} catch {
|
||||
/* nothing saved - already at defaults */
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user