mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
The HUD sends its locale with the summary context; the proxy names that
language in the instructions and the input, English when missing or
unsupported. The model returns {summary, feedState}; the HUD shows the
summary only when feedState matches its own state for a view that is not
nominal, and appends its own translated state tag.
partial joins the shared feed-state table between fallback and nominal,
so one partial layer no longer reads off and partial plus nominal no
longer reads nominal.
138 lines
4.4 KiB
JavaScript
138 lines
4.4 KiB
JavaScript
import {
|
|
HUD_SUMMARY_FEED_STATES,
|
|
HUD_SUMMARY_RESPONSE_FORMAT,
|
|
hudSummaryInput,
|
|
hudSummaryInstructions,
|
|
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();
|
|
}
|
|
|
|
/** The structured reply, or nulls when the model did not follow the format. */
|
|
function parseHudSummaryReply(text) {
|
|
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),
|
|
feedState: feedState === 'none' ? null : feedState,
|
|
};
|
|
}
|
|
|
|
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: 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),
|
|
);
|
|
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 };
|