mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
- The proxy keeps combining marks, in-word apostrophes and name dots (ar vowel signs, fr d'Orléans, ja ・), normalizes to NFC, caps every summary in characters and cuts at a word boundary. - feedState is nullable instead of a `none` token, and the summary has at most five words: "exactly five" made the model pad short views with "no data" or "unavailable". - The HUD caches only the model's words and adds the current warning on every paint; an RTL locale gets a leading right-to-left mark. - The echo check also rejects an all-Latin reply in a non-Latin language, translated layer ids and, with every feed healthy, the UNAVAILABLE word; places are matched by word and one-word layer names are skipped in Latin-script locales. - The warning tag names the worst layers first; a partial analyst card shows one partial chip.
174 lines
5.7 KiB
JavaScript
174 lines
5.7 KiB
JavaScript
import {
|
|
HUD_SUMMARY_FEED_STATES,
|
|
HUD_SUMMARY_MAX_CHARS,
|
|
HUD_SUMMARY_MAX_UNSPACED_CHARS,
|
|
HUD_SUMMARY_RESPONSE_FORMAT,
|
|
hudSummaryInput,
|
|
hudSummaryInstructions,
|
|
hudSummaryIsUnspaced,
|
|
hudSummaryLocale,
|
|
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();
|
|
}
|
|
|
|
/** Parse the summary and normalize the canonical feed state. */
|
|
function parseHudSummaryReply(text, locale) {
|
|
let reply = null;
|
|
try {
|
|
reply = JSON.parse(text);
|
|
} catch {
|
|
/* not JSON: no summary */
|
|
}
|
|
const feedState = HUD_SUMMARY_FEED_STATES.includes(reply?.feedState)
|
|
? reply.feedState
|
|
: null;
|
|
return {
|
|
summary: toFiveWordHudSummary(reply?.summary, locale),
|
|
feedState,
|
|
};
|
|
}
|
|
|
|
function toFiveWordHudSummary(value, requested) {
|
|
const locale = hudSummaryLocale(requested);
|
|
const words = String(value || '')
|
|
.normalize('NFC')
|
|
// Marks, and apostrophes and dots inside a word, belong to it: ar vowel
|
|
// signs, fr d'Orléans, ja サンフランシスコ・ベイエリア.
|
|
.replace(/[^\p{L}\p{M}\p{N}\s'’·・-]/gu, ' ')
|
|
.split(/\s+/)
|
|
.map((word) => word.replace(/^['’·・-]+|['’·・-]+$/gu, ''))
|
|
.filter(Boolean);
|
|
// Japanese and Chinese have no spaces to count words by: cap characters.
|
|
if (hudSummaryIsUnspaced(locale))
|
|
return capAtWord(words.join(' '), HUD_SUMMARY_MAX_UNSPACED_CHARS, locale);
|
|
return capAtWord(words.slice(0, 5).join(' '), HUD_SUMMARY_MAX_CHARS, locale);
|
|
}
|
|
|
|
/**
|
|
* Cut `text` to `max` characters at a word boundary; a first word longer
|
|
* than that is cut where the cap falls.
|
|
*/
|
|
function capAtWord(text, max, locale) {
|
|
const graphemes = (value) => [
|
|
...new Intl.Segmenter(locale, { granularity: 'grapheme' }).segment(value),
|
|
];
|
|
if (graphemes(text).length <= max) return text;
|
|
let kept = '';
|
|
for (const { segment } of new Intl.Segmenter(locale, {
|
|
granularity: 'word',
|
|
}).segment(text)) {
|
|
if (graphemes(kept + segment).length > max) break;
|
|
kept += segment;
|
|
}
|
|
return (
|
|
kept.trim() ||
|
|
graphemes(text)
|
|
.slice(0, max)
|
|
.map(({ segment }) => segment)
|
|
.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: hudSummaryInstructions(context?.locale),
|
|
input: hudSummaryInput(context),
|
|
text: { format: HUD_SUMMARY_RESPONSE_FORMAT },
|
|
reasoning: { effort: 'minimal' },
|
|
max_output_tokens: 200,
|
|
}),
|
|
});
|
|
const data = await response.json().catch(() => ({}));
|
|
const { summary, feedState } = parseHudSummaryReply(
|
|
extractOpenAiResponseText(data),
|
|
context?.locale,
|
|
);
|
|
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,
|
|
feedState,
|
|
// 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 };
|