Container-ready via docker/ compose (frontend nginx + backend Node). Compose adjusted for Coolify on the prod server: frontend uses expose:80 (no host binding — host 8080 is taken by the Coolify proxy; Traefik routes visigine.de), backend ALLOWED_ORIGINS=https://visigine.de. Secrets stay in server/.env (git-ignored); see server/.env.example. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
57 lines
2.1 KiB
JavaScript
57 lines
2.1 KiB
JavaScript
// Anthropic Messages API. Cheapest current Haiku model.
|
|
const ENDPOINT = 'https://api.anthropic.com/v1/messages'
|
|
const MODEL = 'claude-haiku-4-5-20251001'
|
|
const TIMEOUT_MS = 30_000
|
|
const SYSTEM_PROMPT =
|
|
'You are a helpful AI search assistant. Answer the user\'s question concisely, in German if the question is in German. Recommend specific companies, services, or products when relevant.'
|
|
|
|
// Approximate per-1M token rates in USD for Haiku 4.5.
|
|
const COST = { inputPer1M: 1.00, outputPer1M: 5.00 }
|
|
|
|
async function once(prompt, signal) {
|
|
return fetch(ENDPOINT, {
|
|
method: 'POST',
|
|
signal,
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
'x-api-key': process.env.ANTHROPIC_KEY,
|
|
'anthropic-version': '2023-06-01',
|
|
},
|
|
body: JSON.stringify({
|
|
model: MODEL,
|
|
max_tokens: 600,
|
|
temperature: 0.3,
|
|
system: SYSTEM_PROMPT,
|
|
messages: [{ role: 'user', content: prompt }],
|
|
}),
|
|
})
|
|
}
|
|
|
|
export async function query(prompt) {
|
|
const t0 = Date.now()
|
|
const controller = new AbortController()
|
|
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS)
|
|
try {
|
|
let res = await once(prompt, controller.signal)
|
|
if (res.status >= 500) {
|
|
res = await once(prompt, controller.signal)
|
|
}
|
|
if (!res.ok) {
|
|
return { provider: 'anthropic', content: '', ms: Date.now() - t0, costUsd: 0, error: `ANTHROPIC_${res.status}` }
|
|
}
|
|
const data = await res.json()
|
|
// Claude returns content as an array of blocks. Concatenate `text` blocks.
|
|
const blocks = Array.isArray(data?.content) ? data.content : []
|
|
const content = blocks.map((b) => b?.text || '').join('')
|
|
const inTok = data?.usage?.input_tokens || 0
|
|
const outTok = data?.usage?.output_tokens || 0
|
|
const costUsd = (inTok * COST.inputPer1M + outTok * COST.outputPer1M) / 1_000_000
|
|
return { provider: 'anthropic', content, ms: Date.now() - t0, costUsd, error: null }
|
|
} catch (e) {
|
|
const code = e?.name === 'AbortError' ? 'TIMEOUT' : 'NETWORK'
|
|
return { provider: 'anthropic', content: '', ms: Date.now() - t0, costUsd: 0, error: code }
|
|
} finally {
|
|
clearTimeout(timer)
|
|
}
|
|
}
|