| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| export const config = { runtime: 'edge', regions: ['iad1', 'lhr1', 'fra1', 'sfo1'] }; |
|
|
| import { authenticateInternalRequest } from '../../server/_shared/internal-auth'; |
| import { normalizeCountryToIso2 } from '../../server/_shared/country-normalize'; |
| import { assembleBriefStoryContext } from '../../server/worldmonitor/intelligence/v1/brief-story-context'; |
| import { |
| buildAnalystWhyMattersPrompt, |
| sanitizeStoryFields, |
| } from '../../server/worldmonitor/intelligence/v1/brief-why-matters-prompt'; |
| import { callLlm } from '../../server/_shared/llm'; |
| import { readRawJsonFromUpstash, setCachedData, redisPipeline } from '../_upstash-json.js'; |
| |
| import { captureSilentError } from '../_sentry-edge.js'; |
| import { |
| buildWhyMattersUserPrompt, |
| hashBriefStory, |
| hasTerminalPunctuation, |
| parseWhyMatters, |
| parseWhyMattersV2, |
| } from '../../shared/brief-llm-core.js'; |
|
|
| |
| |
|
|
| function readConfig(env: Record<string, string | undefined> = process.env as Record<string, string | undefined>): { |
| primary: 'analyst' | 'gemini'; |
| invalidPrimaryRaw: string | null; |
| shadowEnabled: boolean; |
| sampleHardRoll: (hash16: string) => boolean; |
| invalidSamplePctRaw: string | null; |
| } { |
| |
| const rawPrimary = (env.BRIEF_WHY_MATTERS_PRIMARY ?? '').trim().toLowerCase(); |
| let primary: 'analyst' | 'gemini'; |
| let invalidPrimaryRaw: string | null = null; |
| if (rawPrimary === '' || rawPrimary === 'analyst') { |
| primary = 'analyst'; |
| } else if (rawPrimary === 'gemini') { |
| primary = 'gemini'; |
| } else { |
| primary = 'gemini'; |
| invalidPrimaryRaw = rawPrimary; |
| } |
|
|
| |
| |
| |
| |
| const shadowEnabled = env.BRIEF_WHY_MATTERS_SHADOW === '1'; |
|
|
| |
| const rawSample = env.BRIEF_WHY_MATTERS_SHADOW_SAMPLE_PCT; |
| let samplePct = 100; |
| let invalidSamplePctRaw: string | null = null; |
| if (rawSample !== undefined && rawSample !== '') { |
| const parsed = Number.parseInt(rawSample, 10); |
| if (Number.isInteger(parsed) && parsed >= 0 && parsed <= 100 && String(parsed) === rawSample.trim()) { |
| samplePct = parsed; |
| } else { |
| invalidSamplePctRaw = rawSample; |
| } |
| } |
|
|
| |
| |
| const sampleHardRoll = (hash16: string): boolean => { |
| if (samplePct >= 100) return true; |
| if (samplePct <= 0) return false; |
| const bucket = Number.parseInt(hash16.slice(0, 8), 16) % 100; |
| return bucket < samplePct; |
| }; |
|
|
| return { primary, invalidPrimaryRaw, shadowEnabled, sampleHardRoll, invalidSamplePctRaw }; |
| } |
|
|
| |
| const WHY_MATTERS_TTL_SEC = 6 * 60 * 60; |
| const SHADOW_TTL_SEC = 7 * 24 * 60 * 60; |
|
|
| |
| |
| |
| |
| |
| |
| const WHY_MATTERS_PROVIDER_ORDER = ['openrouter', 'groq']; |
| const WHY_MATTERS_MODEL_OVERRIDES = { openrouter: 'deepseek/deepseek-v4-flash' } as const; |
|
|
| |
| const VALID_THREAT_LEVELS = new Set(['critical', 'high', 'medium', 'low']); |
| |
| |
| |
| const MAX_BODY_BYTES = 8192; |
| const CAPS = { |
| headline: 400, |
| source: 120, |
| category: 80, |
| country: 80, |
| description: 1000, |
| }; |
|
|
| interface StoryPayload { |
| headline: string; |
| source: string; |
| threatLevel: string; |
| category: string; |
| country: string; |
| |
| description?: string; |
| } |
|
|
| type ValidationOk = { ok: true; story: StoryPayload }; |
| type ValidationErr = { ok: false; status: number; error: string }; |
|
|
| function json(body: unknown, status: number): Response { |
| return new Response(JSON.stringify(body), { |
| status, |
| headers: { 'Content-Type': 'application/json' }, |
| }); |
| } |
|
|
| function validateStoryBody(raw: unknown): ValidationOk | ValidationErr { |
| if (!raw || typeof raw !== 'object') { |
| return { ok: false, status: 400, error: 'body must be an object' }; |
| } |
| const storyRaw = (raw as { story?: unknown }).story; |
| if (!storyRaw || typeof storyRaw !== 'object') { |
| return { ok: false, status: 400, error: 'body.story must be an object' }; |
| } |
| const s = storyRaw as Record<string, unknown>; |
|
|
| |
| for (const field of ['headline', 'source', 'category'] as const) { |
| const v = s[field]; |
| if (typeof v !== 'string' || v.length === 0) { |
| return { ok: false, status: 400, error: `story.${field} must be a non-empty string` }; |
| } |
| if (v.length > CAPS[field]) { |
| return { ok: false, status: 400, error: `story.${field} exceeds ${CAPS[field]} chars` }; |
| } |
| } |
|
|
| |
| if (typeof s.threatLevel !== 'string' || !VALID_THREAT_LEVELS.has(s.threatLevel)) { |
| return { |
| ok: false, |
| status: 400, |
| error: `story.threatLevel must be one of critical|high|medium|low`, |
| }; |
| } |
|
|
| |
| let country = ''; |
| if (s.country !== undefined && s.country !== null) { |
| if (typeof s.country !== 'string') { |
| return { ok: false, status: 400, error: 'story.country must be a string' }; |
| } |
| if (s.country.length > CAPS.country) { |
| return { ok: false, status: 400, error: `story.country exceeds ${CAPS.country} chars` }; |
| } |
| country = s.country; |
| } |
|
|
| |
| |
| let description: string | undefined; |
| if (s.description !== undefined && s.description !== null) { |
| if (typeof s.description !== 'string') { |
| return { ok: false, status: 400, error: 'story.description must be a string' }; |
| } |
| if (s.description.length > CAPS.description) { |
| return { ok: false, status: 400, error: `story.description exceeds ${CAPS.description} chars` }; |
| } |
| if (s.description.length > 0) description = s.description; |
| } |
|
|
| return { |
| ok: true, |
| story: { |
| headline: s.headline as string, |
| source: s.source as string, |
| threatLevel: s.threatLevel, |
| category: s.category as string, |
| country, |
| ...(description ? { description } : {}), |
| }, |
| }; |
| } |
|
|
| |
|
|
| function rejectLengthLimitedCompletion(path: 'analyst' | 'gemini', finishReason: string | null): boolean { |
| if (finishReason !== 'length') return false; |
| console.warn(`[brief-why-matters] ${path} completion_reject reason=length`); |
| return true; |
| } |
|
|
| async function runAnalystPath(story: StoryPayload, iso2: string | null): Promise<string | null> { |
| try { |
| const context = await assembleBriefStoryContext({ iso2, category: story.category }); |
| const { system, user, policyLabel } = buildAnalystWhyMattersPrompt(story, context); |
| |
| |
| |
| console.log( |
| `[brief-why-matters] analyst gate policy=${policyLabel} category="${story.category}" promptLen=${user.length}`, |
| ); |
| const result = await callLlm({ |
| messages: [ |
| { role: 'system', content: system }, |
| { role: 'user', content: user }, |
| ], |
| |
| |
| |
| maxTokens: 260, |
| temperature: 0.4, |
| timeoutMs: 15_000, |
| stage: 'brief-why-matters-analyst', |
| |
| |
| providerOrder: WHY_MATTERS_PROVIDER_ORDER, |
| modelOverrides: WHY_MATTERS_MODEL_OVERRIDES, |
| |
| |
| |
| retryOnLengthLimit: true, |
| |
| |
| |
| |
| }); |
| if (!result) return null; |
| if (rejectLengthLimitedCompletion('analyst', result.finishReason)) return null; |
| |
| |
| |
| |
| return parseWhyMattersV2(result.content, { |
| publicStory: { |
| headline: story.headline, |
| description: story.description, |
| source: story.source, |
| }, |
| privateForecasts: context.forecasts, |
| }); |
| } catch (err) { |
| console.warn(`[brief-why-matters] analyst path failed: ${err instanceof Error ? err.message : String(err)}`); |
| |
| |
| |
| |
| await captureSilentError(err, { tags: { route: 'api/internal/brief-why-matters', step: 'analyst-path', severity: 'warn' } }); |
| return null; |
| } |
| } |
|
|
| async function runGeminiPath(story: StoryPayload): Promise<string | null> { |
| try { |
| |
| |
| |
| const { system, user } = buildWhyMattersUserPrompt(sanitizeStoryFields(story)); |
| const result = await callLlm({ |
| messages: [ |
| { role: 'system', content: system }, |
| { role: 'user', content: user }, |
| ], |
| maxTokens: 120, |
| temperature: 0.4, |
| timeoutMs: 10_000, |
| stage: 'brief-why-matters-gemini', |
| |
| |
| providerOrder: WHY_MATTERS_PROVIDER_ORDER, |
| modelOverrides: WHY_MATTERS_MODEL_OVERRIDES, |
| |
| |
| retryOnLengthLimit: true, |
| |
| |
| |
| |
| |
| }); |
| if (!result) return null; |
| if (rejectLengthLimitedCompletion('gemini', result.finishReason)) return null; |
| return parseWhyMatters(result.content); |
| } catch (err) { |
| console.warn(`[brief-why-matters] gemini path failed: ${err instanceof Error ? err.message : String(err)}`); |
| await captureSilentError(err, { tags: { route: 'api/internal/brief-why-matters', step: 'gemini-path', severity: 'warn' } }); |
| return null; |
| } |
| } |
|
|
| |
| interface WhyMattersEnvelope { |
| whyMatters: string; |
| producedBy: 'analyst' | 'gemini'; |
| at: string; |
| } |
|
|
| function isEnvelope(v: unknown): v is WhyMattersEnvelope { |
| if (!v || typeof v !== 'object') return false; |
| const e = v as Record<string, unknown>; |
| return ( |
| typeof e.whyMatters === 'string' && |
| hasTerminalPunctuation(e.whyMatters) && |
| (e.producedBy === 'analyst' || e.producedBy === 'gemini') && |
| typeof e.at === 'string' |
| ); |
| } |
|
|
| |
|
|
| |
| |
| |
| |
| |
| interface EdgeContext { |
| waitUntil?: (promise: Promise<unknown>) => void; |
| } |
|
|
| export default async function handler(req: Request, ctx?: EdgeContext): Promise<Response> { |
| if (req.method !== 'POST') { |
| return json({ error: 'Method not allowed' }, 405); |
| } |
|
|
| |
| const unauthorized = await authenticateInternalRequest(req, 'RELAY_SHARED_SECRET'); |
| if (unauthorized) return unauthorized; |
|
|
| |
| const contentLengthRaw = req.headers.get('content-length'); |
| if (contentLengthRaw) { |
| const cl = Number.parseInt(contentLengthRaw, 10); |
| if (Number.isFinite(cl) && cl > MAX_BODY_BYTES) { |
| return json({ error: `body exceeds ${MAX_BODY_BYTES} bytes` }, 400); |
| } |
| } |
|
|
| |
| let bodyText: string; |
| try { |
| bodyText = await req.text(); |
| } catch { |
| return json({ error: 'failed to read body' }, 400); |
| } |
| if (new TextEncoder().encode(bodyText).byteLength > MAX_BODY_BYTES) { |
| return json({ error: `body exceeds ${MAX_BODY_BYTES} bytes` }, 400); |
| } |
|
|
| let bodyParsed: unknown; |
| try { |
| bodyParsed = JSON.parse(bodyText); |
| } catch { |
| return json({ error: 'invalid JSON' }, 400); |
| } |
|
|
| const validation = validateStoryBody(bodyParsed); |
| if (!validation.ok) { |
| console.warn(`[brief-why-matters] validation_reject error=${validation.error}`); |
| return json({ error: validation.error }, validation.status); |
| } |
| const story = validation.story; |
|
|
| |
| |
| const iso2 = normalizeCountryToIso2(story.country); |
|
|
| |
| const cfg = readConfig(); |
| if (cfg.invalidPrimaryRaw !== null) { |
| console.warn( |
| `[brief-why-matters] unrecognised BRIEF_WHY_MATTERS_PRIMARY=${cfg.invalidPrimaryRaw} — falling back to gemini (safe path). Valid values: analyst | gemini.`, |
| ); |
| } |
| if (cfg.invalidSamplePctRaw !== null) { |
| console.warn( |
| `[brief-why-matters] unrecognised BRIEF_WHY_MATTERS_SHADOW_SAMPLE_PCT=${cfg.invalidSamplePctRaw} — defaulting to 100. Must be integer 0-100.`, |
| ); |
| } |
|
|
| |
| const hash = await hashBriefStory(story); |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const cacheKey = `brief:llm:whymatters:v10:${hash}`; |
| |
| |
| const shadowKey = `brief:llm:whymatters:shadow:v7:${hash}`; |
|
|
| |
| let cached: WhyMattersEnvelope | null = null; |
| try { |
| const raw = await readRawJsonFromUpstash(cacheKey); |
| if (raw !== null && isEnvelope(raw)) { |
| cached = raw; |
| } |
| } catch (err) { |
| console.warn(`[brief-why-matters] cache read degraded: ${err instanceof Error ? err.message : String(err)}`); |
| await captureSilentError(err, { tags: { route: 'api/internal/brief-why-matters', step: 'cache-read', severity: 'warn' } }); |
| } |
|
|
| if (cached) { |
| return json({ |
| whyMatters: cached.whyMatters, |
| source: 'cache', |
| producedBy: cached.producedBy, |
| hash, |
| }, 200); |
| } |
|
|
| |
| const runShadow = cfg.shadowEnabled && cfg.sampleHardRoll(hash); |
|
|
| let analystResult: string | null = null; |
| let geminiResult: string | null = null; |
| let chosenProducer: 'analyst' | 'gemini'; |
| let chosenValue: string | null; |
|
|
| if (runShadow) { |
| const [a, g] = await Promise.allSettled([ |
| runAnalystPath(story, iso2), |
| runGeminiPath(story), |
| ]); |
| analystResult = a.status === 'fulfilled' ? a.value : null; |
| geminiResult = g.status === 'fulfilled' ? g.value : null; |
| if (cfg.primary === 'analyst') { |
| |
| chosenProducer = analystResult !== null ? 'analyst' : 'gemini'; |
| chosenValue = analystResult ?? geminiResult; |
| } else { |
| chosenProducer = geminiResult !== null ? 'gemini' : 'analyst'; |
| chosenValue = geminiResult ?? analystResult; |
| } |
| } else if (cfg.primary === 'analyst') { |
| analystResult = await runAnalystPath(story, iso2); |
| chosenProducer = 'analyst'; |
| chosenValue = analystResult; |
| } else { |
| geminiResult = await runGeminiPath(story); |
| chosenProducer = 'gemini'; |
| chosenValue = geminiResult; |
| } |
|
|
| |
| |
| const now = new Date().toISOString(); |
| if (chosenValue !== null) { |
| const envelope: WhyMattersEnvelope = { |
| whyMatters: chosenValue, |
| producedBy: chosenProducer, |
| at: now, |
| }; |
| try { |
| await setCachedData(cacheKey, envelope, WHY_MATTERS_TTL_SEC); |
| } catch (err) { |
| console.warn(`[brief-why-matters] cache write degraded: ${err instanceof Error ? err.message : String(err)}`); |
| await captureSilentError(err, { tags: { route: 'api/internal/brief-why-matters', step: 'cache-write', severity: 'warn' } }); |
| } |
| } |
|
|
| |
| |
| |
| |
| if (runShadow) { |
| const record = { |
| analyst: analystResult, |
| gemini: geminiResult, |
| chosen: chosenProducer, |
| at: now, |
| }; |
| const shadowWrite = redisPipeline([ |
| ['SET', shadowKey, JSON.stringify(record), 'EX', String(SHADOW_TTL_SEC)], |
| ]).then(() => undefined).catch(() => { |
| |
| }); |
| if (typeof ctx?.waitUntil === 'function') { |
| ctx.waitUntil(shadowWrite); |
| } |
| |
| |
| } |
|
|
| const response: { |
| whyMatters: string | null; |
| source: 'analyst' | 'gemini'; |
| producedBy: 'analyst' | 'gemini' | null; |
| hash: string; |
| shadow?: { analyst: string | null; gemini: string | null }; |
| } = { |
| whyMatters: chosenValue, |
| source: chosenProducer, |
| producedBy: chosenValue !== null ? chosenProducer : null, |
| hash, |
| }; |
| if (runShadow) { |
| response.shadow = { analyst: analystResult, gemini: geminiResult }; |
| } |
|
|
| return json(response, 200); |
| } |
|
|