Files
gods-eye-view/server/providers/openai/hud-summary.js
T
Sameh Khamis c1f60cb7e7 Cut the Google requests the HUD summary makes on its own
The HUD builds place context for its AI summary whenever the camera
settles, and each build paid for up to four reverse geocodes plus a Nearby
Places search, even when no OpenAI key was configured to use them.

- Ask GET /api/openai/hud-summary/status once; with no summary configured,
  or after an unconfigured reply, make no place lookups for the page.
- Reverse-geocode at most two points: the view center and one viewport
  point.
- Reuse a lookup until the view center moves a quarter of the visible
  width, the camera's distance to it doubles or halves, or the camera
  crosses a lookup height. The reused points are asked again, so only a
  timeout, failure or empty answer spends.
- Repaint the last AI reply when a small pan leaves the context unchanged.

Cached-only hosts and voice context lookups are unchanged.
2026-10-28 14:59:33 -07:00

114 lines
3.8 KiB
JavaScript

import {
HUD_SUMMARY_INSTRUCTIONS,
keylessHudSummaryResponse,
} from '../../../src/hudSummaryResponse.js';
import { enforceRateLimit, openAiRateLimiter } from './rate-limit.js';
import { readRequestBody } from '../common/request.js';
import { OPENAI_HUD_SUMMARY_MODEL_DEFAULT } from './constants.js';
function extractOpenAiResponseText(data) {
if (typeof data?.output_text === 'string' && data.output_text.trim()) {
return data.output_text.trim();
}
if (!Array.isArray(data?.output)) return '';
return data.output
.flatMap((item) => (Array.isArray(item?.content) ? item.content : []))
.map((part) => part?.text || part?.output_text || '')
.join(' ')
.trim();
}
function toFiveWordHudSummary(value) {
return String(value || '')
.replace(/[^\p{L}\p{N}\s-]/gu, ' ')
.trim()
.split(/\s+/)
.filter(Boolean)
.slice(0, 5)
.join(' ');
}
async function handleHudSummary(req, res) {
// Capability probe: lets the HUD skip its paid place lookups when there is
// no summary to feed them to.
if (req.method === 'GET' && req.url?.split('?')[0] === '/status') {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.setHeader('Cache-Control', 'no-store');
res.end(
JSON.stringify({
configured: !keylessHudSummaryResponse(process.env.OPENAI_API_KEY),
}),
);
return;
}
if (req.method !== 'POST') {
res.statusCode = 405;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'Method not allowed' }));
return;
}
const apiKey = process.env.OPENAI_API_KEY;
const keyless = keylessHudSummaryResponse(apiKey);
if (keyless) {
res.statusCode = keyless.statusCode;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.setHeader('Cache-Control', 'no-store');
res.end(JSON.stringify(keyless.payload));
return;
}
// Per-IP throttle (GEV_RATELIMIT_OPENAI_PER_MIN). On by default; 0 disables.
// Keyless HUD fallback has no provider cost and resolves above without
// consuming a paid-endpoint quota slot.
if (!enforceRateLimit(openAiRateLimiter(), req, res)) return;
try {
const body = await readRequestBody(req, 64 * 1024);
const context = JSON.parse(body || '{}');
const response = await fetch('https://api.openai.com/v1/responses', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model:
process.env.OPENAI_HUD_SUMMARY_MODEL ||
OPENAI_HUD_SUMMARY_MODEL_DEFAULT,
instructions: HUD_SUMMARY_INSTRUCTIONS,
input: JSON.stringify(context),
reasoning: { effort: 'minimal' },
max_output_tokens: 100,
}),
});
const data = await response.json().catch(() => ({}));
const summary = toFiveWordHudSummary(extractOpenAiResponseText(data));
res.statusCode = response.ok && summary ? 200 : response.status || 502;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.setHeader('Cache-Control', 'no-store');
if (!response.ok)
console.warn(`[hud-summary] upstream HTTP ${response.status}`);
res.end(
JSON.stringify({
summary: summary || null,
// Never relay `data.error.message`: that is OpenAI's own wording, and
// it carries request ids, organization hints and quota phrasing.
error: response.ok ? null : 'OpenAI HUD summary request failed',
}),
);
} catch {
console.warn('[hud-summary] request failed');
res.statusCode = 502;
res.setHeader('Content-Type', 'application/json');
res.end(
JSON.stringify({
error: 'OpenAI HUD summary request failed',
}),
);
}
}
export { handleHudSummary };