timmy-talking-turd/src/vision-config.js

59 lines
2.1 KiB
JavaScript

const PROFILES = {
hosted: {
baseUrl: 'http://127.0.0.1:8645/v1',
apiKey: 'local-proxy',
model: 'stepfun/step-3.7-flash:free',
processor: 'third-party',
},
selfhost: {
baseUrl: 'http://127.0.0.1:8080/v1',
apiKey: 'local-selfhost',
model: 'SmolVLM2-2.2B-Instruct',
processor: 'self-hosted',
},
};
export function resolveVisionConfig(env = process.env) {
const profile = env.TIMMY_VISION_PROFILE || 'hosted';
if (!PROFILES[profile]) throw new Error('TIMMY_VISION_PROFILE must be hosted or selfhost.');
const defaults = PROFILES[profile];
const config = {
enabled: env.TIMMY_VISION_ENABLED !== '0',
profile,
processor: defaults.processor,
baseUrl: env.TIMMY_VISION_BASE_URL || defaults.baseUrl,
apiKey: env.TIMMY_VISION_API_KEY || defaults.apiKey,
model: env.TIMMY_VISION_MODEL || defaults.model,
requestTimeoutMs: Number(env.TIMMY_VISION_TIMEOUT_MS || (profile === 'selfhost' ? 120_000 : 60_000)),
};
config.publicStatus = () => ({
enabled: config.enabled,
profile: config.profile,
processor: config.processor,
model: config.enabled ? config.model : null,
});
return config;
}
function modelsEndpoint(baseUrl) {
const url = new URL(baseUrl);
if (!['http:', 'https:'].includes(url.protocol)) throw new Error('Invalid AI provider URL.');
return `${url.toString().replace(/\/$/, '')}/models`;
}
export async function probeVisionProvider(config, fetchImpl = fetch) {
if (!config.enabled) return { ready: false, modelSeen: false };
try {
const response = await fetchImpl(modelsEndpoint(config.baseUrl), {
headers: { authorization: `Bearer ${config.apiKey}` },
signal: AbortSignal.timeout(2_000),
});
if (!response.ok) return { ready: false, modelSeen: false };
const body = await response.json();
const ids = Array.isArray(body?.data) ? body.data.map(item => item?.id).filter(Boolean) : [];
return { ready: ids.length > 0, modelSeen: ids.some(id => id === config.model || id.includes(config.model)) };
} catch {
return { ready: false, modelSeen: false };
}
}