mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
fix voice debug-log privacy and docs drift
omit tool arguments unless content logging is enabled correct the remaining current tool-count references
This commit is contained in:
1 parent
a697ceae40
commit
ae3612a356
5 files changed
+195
-36
No files matched your search
+3
-2
@@ -71,8 +71,9 @@ OPENAI_REALTIME_CONTEXT_RETENTION=0.5
|
|||||||
# counted in the session cost meter and cap; off disables them.
|
# counted in the session cost meter and cap; off disables them.
|
||||||
OPENAI_REALTIME_TRANSCRIBE_MODEL=gpt-4o-mini-transcribe
|
OPENAI_REALTIME_TRANSCRIBE_MODEL=gpt-4o-mini-transcribe
|
||||||
# The voice debug log (.gev-logs/realtime-conversations.jsonl, rotated at
|
# The voice debug log (.gev-logs/realtime-conversations.jsonl, rotated at
|
||||||
# 32 MB, never expired) omits transcripts and typed or spoken text by default.
|
# 32 MB, never expired) omits transcripts, typed or spoken text, and tool
|
||||||
# Set to 1 to keep them while debugging; delete the log when you are done.
|
# arguments by default. Set to 1 to keep them while debugging; delete the log
|
||||||
|
# when you are done.
|
||||||
GEV_VOICE_LOG_CONTENT=
|
GEV_VOICE_LOG_CONTENT=
|
||||||
OPENAI_HUD_SUMMARY_MODEL=gpt-5-nano
|
OPENAI_HUD_SUMMARY_MODEL=gpt-5-nano
|
||||||
# Per-IP rate limit for the OpenAI cost endpoints
|
# Per-IP rate limit for the OpenAI cost endpoints
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ Click **GEV MIC**, grant the microphone, and just talk. This is more than a voic
|
|||||||
- **🎬 Cinematic framing.** _"Show me the planes overhead"_ pulls the camera back, angles it, and frames the live traffic like a director.
|
- **🎬 Cinematic framing.** _"Show me the planes overhead"_ pulls the camera back, angles it, and frames the live traffic like a director.
|
||||||
- **🔒 Honest and secure.** The agent only confirms actions that succeeded. Your `OPENAI_API_KEY` never touches the browser; the client only gets a short-lived session token.
|
- **🔒 Honest and secure.** The agent only confirms actions that succeeded. Your `OPENAI_API_KEY` never touches the browser; the client only gets a short-lived session token.
|
||||||
|
|
||||||
Twenty-nine tools, four jobs — the commands below come straight from the product's voice test suite and tool playbook:
|
Thirty tools, four jobs — the commands below come straight from the product's voice test suite and tool playbook:
|
||||||
|
|
||||||
**🎥 Direct it** — drone-operator camera verbs:
|
**🎥 Direct it** — drone-operator camera verbs:
|
||||||
|
|
||||||
@@ -384,7 +384,7 @@ src/
|
|||||||
├── hud.js # Intelligence HUD + AI scene summary
|
├── hud.js # Intelligence HUD + AI scene summary
|
||||||
├── keySetup.js # POWER UP panel — in-app provider keys (dev server only)
|
├── keySetup.js # POWER UP panel — in-app provider keys (dev server only)
|
||||||
├── mapStackController.js # Basemap switching — Google 3D / Esri / OSM / ion stacks
|
├── mapStackController.js # Basemap switching — Google 3D / Esri / OSM / ion stacks
|
||||||
├── voice/ # OpenAI Realtime session + 29 voice tools
|
├── voice/ # OpenAI Realtime session + 30 voice tools
|
||||||
├── layers/ # Layer components — weather, wind, cyclones, transit, ALPR, …
|
├── layers/ # Layer components — weather, wind, cyclones, transit, ALPR, …
|
||||||
├── data/ # One module per layer + orchestration + context store
|
├── data/ # One module per layer + orchestration + context store
|
||||||
│ ├── iconOrientation.js # Screen-projected headings + horizon cull
|
│ ├── iconOrientation.js # Screen-projected headings + horizon cull
|
||||||
|
|||||||
@@ -383,8 +383,8 @@ the speaker (`mayVoiceClaimSpeaker()`). Progress lines stay outside the reply
|
|||||||
lifecycle and Radio handoff. `turn.span` debug records carry generation and
|
lifecycle and Radio handoff. `turn.span` debug records carry generation and
|
||||||
playback latency, silence, preamble and word-count figures. Caption
|
playback latency, silence, preamble and word-count figures. Caption
|
||||||
transcription (`OPENAI_REALTIME_TRANSCRIBE_MODEL`, default on) is metered into
|
transcription (`OPENAI_REALTIME_TRANSCRIBE_MODEL`, default on) is metered into
|
||||||
the session cost and cap. The debug log omits transcripts and text unless
|
the session cost and cap. The debug log omits transcripts, text and tool
|
||||||
`GEV_VOICE_LOG_CONTENT=1`.
|
arguments unless `GEV_VOICE_LOG_CONTENT=1`.
|
||||||
|
|
||||||
Point and ask: `pointerContext.js` reads what the cursor is on. A Space hold
|
Point and ask: `pointerContext.js` reads what the cursor is on. A Space hold
|
||||||
is the pointing gesture: its keydown snapshot (released with the key) is sent
|
is the pointing gesture: its keydown snapshot (released with the key) is sent
|
||||||
@@ -2939,7 +2939,7 @@ test:track` 43 tracking invariants · headless QA harnesses under
|
|||||||
> ISS pass prediction, and per-layer data attribution. Gate at close: unit 98/98, build clean,
|
> ISS pass prediction, and per-layer data attribution. Gate at close: unit 98/98, build clean,
|
||||||
> track 19/19, + five QA harnesses (heading 16/16, sprites 9/9, cctv 5/5, failstate 5/5,
|
> track 19/19, + five QA harnesses (heading 16/16, sprites 9/9, cctv 5/5, failstate 5/5,
|
||||||
> attribution 18/18). New modules: `src/data/{motionModel,aircraftMeta,aircraftClass,aircraftIcons,issPass,routePlausible,dataCredits}.js`.
|
> attribution 18/18). New modules: `src/data/{motionModel,aircraftMeta,aircraftClass,aircraftIcons,issPass,routePlausible,dataCredits}.js`.
|
||||||
> The live runtime now declares 29 voice tools; the 17→20 count above is retained only as milestone history.
|
> The live runtime now declares 30 voice tools; the 17→20 count above is retained only as milestone history.
|
||||||
|
|
||||||
## Canonical Docs Order
|
## Canonical Docs Order
|
||||||
|
|
||||||
|
|||||||
@@ -25,30 +25,41 @@ const REALTIME_DEBUG_LOG_MAX_PER_MIN = 120;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Fields that carry what people said or heard: transcripts, typed and spoken
|
* Fields that carry what people said or heard: transcripts, typed and spoken
|
||||||
* text, streamed deltas and the span's quoted replies. They are dropped from
|
* text, streamed deltas, structured tool arguments and the span's quoted
|
||||||
* the persisted log unless content logging is explicitly enabled
|
* replies. They are dropped from the persisted log unless content logging is explicitly enabled
|
||||||
* (GEV_VOICE_LOG_CONTENT=1), so timing and tool diagnostics never retain
|
* (GEV_VOICE_LOG_CONTENT=1), so timing and tool diagnostics never retain
|
||||||
* speech by default.
|
* speech by default.
|
||||||
*/
|
*/
|
||||||
const VOICE_CONTENT_FIELDS = new Set([
|
const VOICE_TEXT_FIELDS = new Set([
|
||||||
'transcript',
|
'transcript',
|
||||||
'text',
|
'text',
|
||||||
'delta',
|
'delta',
|
||||||
'answer_text',
|
'answer_text',
|
||||||
'preamble_text',
|
'preamble_text',
|
||||||
]);
|
]);
|
||||||
|
const VOICE_CONTENT_MAX_DEPTH = 12;
|
||||||
|
const VOICE_CONTENT_MAX_DEPTH_MARKER = '[omitted: max depth]';
|
||||||
|
|
||||||
/** Replace spoken or typed content with its length. */
|
/** Replace spoken or typed content and structured tool arguments. */
|
||||||
function omitVoiceContent(value, depth = 0) {
|
function omitVoiceContent(value, depth = 0) {
|
||||||
if (depth > 12 || value === null || typeof value !== 'object') return value;
|
if (depth > VOICE_CONTENT_MAX_DEPTH) return VOICE_CONTENT_MAX_DEPTH_MARKER;
|
||||||
|
if (value === null || typeof value !== 'object') return value;
|
||||||
if (Array.isArray(value))
|
if (Array.isArray(value))
|
||||||
return value.map((item) => omitVoiceContent(item, depth + 1));
|
return value.map((item) => omitVoiceContent(item, depth + 1));
|
||||||
const output = {};
|
const output = {};
|
||||||
for (const [key, item] of Object.entries(value)) {
|
for (const [key, item] of Object.entries(value)) {
|
||||||
output[key] =
|
if (key === 'arguments') {
|
||||||
VOICE_CONTENT_FIELDS.has(key) && typeof item === 'string'
|
output[key] =
|
||||||
? `[omitted ${item.length} chars]`
|
typeof item === 'string'
|
||||||
: omitVoiceContent(item, depth + 1);
|
? `[omitted ${item.length} chars]`
|
||||||
|
: '[omitted]';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (VOICE_TEXT_FIELDS.has(key) && typeof item === 'string') {
|
||||||
|
output[key] = `[omitted ${item.length} chars]`;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
output[key] = omitVoiceContent(item, depth + 1);
|
||||||
}
|
}
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -399,37 +399,184 @@ test('OpenAI routes answer generically when the upstream or the request fails',
|
|||||||
assert.equal(token.body.includes('fixture-upstream-secret'), false);
|
assert.equal(token.body.includes('fixture-upstream-secret'), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the debug-log sink omits what people said unless content logging is enabled', async (t) => {
|
test('the debug-log sink omits what people said and tool arguments unless content logging is enabled', async (t) => {
|
||||||
const record = {
|
const nestObject = (value, levels) => {
|
||||||
event: 'server.event',
|
let nested = value;
|
||||||
payload: {
|
for (let index = 0; index < levels; index += 1) nested = { nested };
|
||||||
type: 'conversation.item.input_audio_transcription.completed',
|
return nested;
|
||||||
transcript: 'my home address is 12 Elm Street',
|
|
||||||
usage: { input_tokens: 40 },
|
|
||||||
nested: [{ type: 'input_text', text: 'call my sister' }],
|
|
||||||
},
|
|
||||||
span: { answer_text: 'Flying to Tokyo.', answer_words: 3 },
|
|
||||||
};
|
};
|
||||||
|
const nestArray = (value, levels) => {
|
||||||
|
let nested = value;
|
||||||
|
for (let index = 0; index < levels; index += 1) nested = [nested];
|
||||||
|
return nested;
|
||||||
|
};
|
||||||
|
const records = [
|
||||||
|
{
|
||||||
|
timestamp: '2026-10-05T00:00:00.000Z',
|
||||||
|
sessionId: 'gev-session-1',
|
||||||
|
event: 'tool.call',
|
||||||
|
status: 'active',
|
||||||
|
payload: {
|
||||||
|
name: 'analyst_query',
|
||||||
|
callId: 'call-1',
|
||||||
|
arguments: { location: '12 Elm Street', note: 'meet Alice there' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamp: '2026-10-05T00:00:01.000Z',
|
||||||
|
sessionId: 'gev-session-1',
|
||||||
|
event: 'server.event',
|
||||||
|
status: 'active',
|
||||||
|
payload: {
|
||||||
|
type: 'response.function_call_arguments.done',
|
||||||
|
eventId: 'event-1',
|
||||||
|
responseId: 'response-1',
|
||||||
|
payload: {
|
||||||
|
type: 'response.function_call_arguments.done',
|
||||||
|
event_id: 'event-1',
|
||||||
|
response_id: 'response-1',
|
||||||
|
item_id: 'item-1',
|
||||||
|
output_index: 0,
|
||||||
|
call_id: 'call-1',
|
||||||
|
name: 'analyst_query',
|
||||||
|
arguments: '{"query":"Tokyo Station"}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: 'server.event',
|
||||||
|
status: 'active',
|
||||||
|
payload: {
|
||||||
|
type: 'response.audio_transcript.done',
|
||||||
|
transcript: 'my home address is 12 Elm Street',
|
||||||
|
text: 'call my sister',
|
||||||
|
delta: 'Flying to Tokyo.',
|
||||||
|
usage: { input_tokens: 40 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: 'voice.cost',
|
||||||
|
status: 'active',
|
||||||
|
payload: { delta: 17, total: 41 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: 'tool.call',
|
||||||
|
status: 'active',
|
||||||
|
payload: { name: 'adjust_camera_zoom', callId: 'call-2', arguments: 2 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: 'deep.object',
|
||||||
|
status: 'active',
|
||||||
|
payload: nestObject(
|
||||||
|
{
|
||||||
|
transcript: 'deep-object-transcript-secret',
|
||||||
|
arguments: { query: 'deep-object-argument-secret' },
|
||||||
|
},
|
||||||
|
14,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: 'deep.array',
|
||||||
|
status: 'active',
|
||||||
|
payload: nestArray(
|
||||||
|
{
|
||||||
|
transcript: 'deep-array-transcript-secret',
|
||||||
|
arguments: { query: 'deep-array-argument-secret' },
|
||||||
|
},
|
||||||
|
14,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
for (const includeContent of [false, true]) {
|
for (const includeContent of [false, true]) {
|
||||||
const sourceRoot = root(t);
|
const sourceRoot = root(t);
|
||||||
const handler = createDebugLogHandler({ sourceRoot, includeContent });
|
const handler = createDebugLogHandler({ sourceRoot, includeContent });
|
||||||
const response = await request(handler, {
|
for (const record of records) {
|
||||||
method: 'POST',
|
const response = await request(handler, {
|
||||||
body: JSON.stringify(record),
|
method: 'POST',
|
||||||
});
|
body: JSON.stringify(record),
|
||||||
assert.equal(response.status, 204);
|
});
|
||||||
|
assert.equal(response.status, 204);
|
||||||
|
}
|
||||||
const logged = readFileSync(
|
const logged = readFileSync(
|
||||||
path.join(sourceRoot, '.gev-logs/realtime-conversations.jsonl'),
|
path.join(sourceRoot, '.gev-logs/realtime-conversations.jsonl'),
|
||||||
'utf8',
|
'utf8',
|
||||||
);
|
);
|
||||||
assert.equal(logged.includes('Elm Street'), includeContent);
|
assert.equal(logged.includes('Elm Street'), includeContent);
|
||||||
assert.equal(logged.includes('call my sister'), includeContent);
|
assert.equal(logged.includes('call my sister'), includeContent);
|
||||||
|
assert.equal(logged.includes('meet Alice there'), includeContent);
|
||||||
|
assert.equal(logged.includes('Tokyo Station'), includeContent);
|
||||||
assert.equal(logged.includes('Flying to Tokyo'), includeContent);
|
assert.equal(logged.includes('Flying to Tokyo'), includeContent);
|
||||||
const parsed = JSON.parse(logged);
|
assert.equal(
|
||||||
assert.equal(parsed.payload.usage.input_tokens, 40, 'numbers stay');
|
logged.includes('deep-object-transcript-secret'),
|
||||||
assert.equal(parsed.span.answer_words, 3);
|
includeContent,
|
||||||
if (!includeContent)
|
);
|
||||||
assert.equal(parsed.payload.transcript, '[omitted 32 chars]');
|
assert.equal(
|
||||||
|
logged.includes('deep-object-argument-secret'),
|
||||||
|
includeContent,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
logged.includes('deep-array-transcript-secret'),
|
||||||
|
includeContent,
|
||||||
|
);
|
||||||
|
assert.equal(logged.includes('deep-array-argument-secret'), includeContent);
|
||||||
|
const [
|
||||||
|
toolCall,
|
||||||
|
serverEvent,
|
||||||
|
transcriptEvent,
|
||||||
|
numericDelta,
|
||||||
|
numericArguments,
|
||||||
|
deepObject,
|
||||||
|
deepArray,
|
||||||
|
] = logged
|
||||||
|
.split('\n')
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((line) => JSON.parse(line));
|
||||||
|
assert.equal(toolCall.timestamp, records[0].timestamp);
|
||||||
|
assert.equal(toolCall.sessionId, 'gev-session-1');
|
||||||
|
assert.equal(toolCall.event, 'tool.call');
|
||||||
|
assert.equal(toolCall.status, 'active');
|
||||||
|
assert.equal(toolCall.payload.name, 'analyst_query');
|
||||||
|
assert.equal(toolCall.payload.callId, 'call-1');
|
||||||
|
assert.equal(serverEvent.payload.eventId, 'event-1');
|
||||||
|
assert.equal(serverEvent.payload.responseId, 'response-1');
|
||||||
|
assert.equal(serverEvent.payload.payload.event_id, 'event-1');
|
||||||
|
assert.equal(serverEvent.payload.payload.response_id, 'response-1');
|
||||||
|
assert.equal(serverEvent.payload.payload.item_id, 'item-1');
|
||||||
|
assert.equal(serverEvent.payload.payload.output_index, 0);
|
||||||
|
assert.equal(serverEvent.payload.payload.call_id, 'call-1');
|
||||||
|
assert.equal(serverEvent.payload.payload.name, 'analyst_query');
|
||||||
|
assert.equal(transcriptEvent.payload.usage.input_tokens, 40);
|
||||||
|
assert.equal(numericDelta.payload.delta, 17);
|
||||||
|
assert.equal(numericDelta.payload.total, 41);
|
||||||
|
if (!includeContent) {
|
||||||
|
assert.equal(toolCall.payload.arguments, '[omitted]');
|
||||||
|
assert.match(
|
||||||
|
serverEvent.payload.payload.arguments,
|
||||||
|
/^\[omitted \d+ chars\]$/,
|
||||||
|
);
|
||||||
|
assert.equal(transcriptEvent.payload.transcript, '[omitted 32 chars]');
|
||||||
|
assert.equal(transcriptEvent.payload.text, '[omitted 14 chars]');
|
||||||
|
assert.equal(transcriptEvent.payload.delta, '[omitted 16 chars]');
|
||||||
|
assert.equal(numericArguments.payload.arguments, '[omitted]');
|
||||||
|
assert.equal(
|
||||||
|
JSON.stringify(deepObject.payload).includes('[omitted: max depth]'),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
JSON.stringify(deepArray.payload).includes('[omitted: max depth]'),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
assert.deepEqual(
|
||||||
|
toolCall.payload.arguments,
|
||||||
|
records[0].payload.arguments,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
serverEvent.payload.payload.arguments,
|
||||||
|
records[1].payload.payload.arguments,
|
||||||
|
);
|
||||||
|
assert.equal(numericArguments.payload.arguments, 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user