mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
fix(voice): omit tool result content from debug logs
This commit is contained in:
1 parent
ae3612a356
commit
ae1b6abda1
5 files changed
+105
-7
No files matched your search
+2
-2
@@ -72,8 +72,8 @@ OPENAI_REALTIME_CONTEXT_RETENTION=0.5
|
||||
OPENAI_REALTIME_TRANSCRIBE_MODEL=gpt-4o-mini-transcribe
|
||||
# The voice debug log (.gev-logs/realtime-conversations.jsonl, rotated at
|
||||
# 32 MB, never expired) omits transcripts, typed or spoken text, and tool
|
||||
# arguments by default. Set to 1 to keep them while debugging; delete the log
|
||||
# when you are done.
|
||||
# arguments/results (including serialized outputs) by default. Set to 1 to
|
||||
# keep them while debugging; delete the log when you are done.
|
||||
GEV_VOICE_LOG_CONTENT=
|
||||
OPENAI_HUD_SUMMARY_MODEL=gpt-5-nano
|
||||
# Per-IP rate limit for the OpenAI cost endpoints
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
- Voice debug logs omit complete tool-result bodies and serialized function
|
||||
outputs by default, including location queries and generated spoken replies.
|
||||
`GEV_VOICE_LOG_CONTENT=1` retains them for explicit content debugging.
|
||||
|
||||
- Unqualified Alps searches use the bundled European range for navigation
|
||||
and annotations instead of a same-named peak returned by a geocoder.
|
||||
Coordinates, supplied presets and geographically qualified place names
|
||||
|
||||
@@ -383,8 +383,10 @@ the speaker (`mayVoiceClaimSpeaker()`). Progress lines stay outside the reply
|
||||
lifecycle and Radio handoff. `turn.span` debug records carry generation and
|
||||
playback latency, silence, preamble and word-count figures. Caption
|
||||
transcription (`OPENAI_REALTIME_TRANSCRIBE_MODEL`, default on) is metered into
|
||||
the session cost and cap. The debug log omits transcripts, text and tool
|
||||
arguments unless `GEV_VOICE_LOG_CONTENT=1`.
|
||||
the session cost and cap. The debug log omits transcripts, text, tool arguments
|
||||
and complete tool-result bodies (including serialized function outputs) unless
|
||||
`GEV_VOICE_LOG_CONTENT=1`.
|
||||
Tool names, call IDs, protocol event metadata and usage remain available.
|
||||
|
||||
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
|
||||
|
||||
@@ -25,7 +25,7 @@ const REALTIME_DEBUG_LOG_MAX_PER_MIN = 120;
|
||||
|
||||
/**
|
||||
* Fields that carry what people said or heard: transcripts, typed and spoken
|
||||
* text, streamed deltas, structured tool arguments and the span's quoted
|
||||
* text, streamed deltas, tool arguments/results and the span's quoted
|
||||
* 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
|
||||
* speech by default.
|
||||
@@ -40,7 +40,7 @@ const VOICE_TEXT_FIELDS = new Set([
|
||||
const VOICE_CONTENT_MAX_DEPTH = 12;
|
||||
const VOICE_CONTENT_MAX_DEPTH_MARKER = '[omitted: max depth]';
|
||||
|
||||
/** Replace spoken or typed content and structured tool arguments. */
|
||||
/** Replace spoken or typed content and complete tool argument/result bodies. */
|
||||
function omitVoiceContent(value, depth = 0) {
|
||||
if (depth > VOICE_CONTENT_MAX_DEPTH) return VOICE_CONTENT_MAX_DEPTH_MARKER;
|
||||
if (value === null || typeof value !== 'object') return value;
|
||||
@@ -48,7 +48,15 @@ function omitVoiceContent(value, depth = 0) {
|
||||
return value.map((item) => omitVoiceContent(item, depth + 1));
|
||||
const output = {};
|
||||
for (const [key, item] of Object.entries(value)) {
|
||||
if (key === 'arguments') {
|
||||
// Tool bodies have open-ended schemas: omit the whole body rather than
|
||||
// chasing query, label, say, display, or future tool-specific fields.
|
||||
// Protocol response.output arrays still carry useful event metadata; only
|
||||
// string output is a serialized function result and must be opaque.
|
||||
if (
|
||||
key === 'arguments' ||
|
||||
key === 'result' ||
|
||||
(key === 'output' && typeof item === 'string')
|
||||
) {
|
||||
output[key] =
|
||||
typeof item === 'string'
|
||||
? `[omitted ${item.length} chars]`
|
||||
|
||||
@@ -22,6 +22,8 @@ import { keySetupEndpoint } from 'gods-eye-view/server/standalone/key-setup';
|
||||
import { realtimeInstructions } from '../../server/providers/openai/instructions.js';
|
||||
import { GEV_REALTIME_TOOLS } from '../../server/providers/openai/tools.js';
|
||||
import { createDebugLogHandler } from '../../server/providers/openai/debug-log.js';
|
||||
import { attachVoiceResult } from '../voice/speech.js';
|
||||
import { sanitizeDebugValue } from '../voice/realtimeDiagnostics.js';
|
||||
import { standaloneVoiceTools } from '../../server/standalone/voiceTools.js';
|
||||
|
||||
function install(plugin, preview = false) {
|
||||
@@ -580,6 +582,88 @@ test('the debug-log sink omits what people said and tool arguments unless conten
|
||||
}
|
||||
});
|
||||
|
||||
test('debug logs omit structured and serialized tool results unless content logging is enabled', async (t) => {
|
||||
const secret = 'private-address-fixture';
|
||||
const result = attachVoiceResult('fly_to_location', {
|
||||
ok: true,
|
||||
query: secret,
|
||||
label: secret,
|
||||
arrived: true,
|
||||
// New tool-specific fields must not need their own redaction rule.
|
||||
futureField: { nested: [secret] },
|
||||
});
|
||||
const records = [
|
||||
{
|
||||
event: 'tool.result',
|
||||
payload: { name: 'fly_to_location', callId: 'call-1', result },
|
||||
},
|
||||
{
|
||||
event: 'client.function_call_output',
|
||||
payload: {
|
||||
message: {
|
||||
type: 'conversation.item.create',
|
||||
item: {
|
||||
type: 'function_call_output',
|
||||
call_id: 'call-1',
|
||||
output: JSON.stringify(result),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
event: 'server.event',
|
||||
payload: {
|
||||
response: {
|
||||
id: 'response-1',
|
||||
output: [
|
||||
{
|
||||
type: 'message',
|
||||
content: [{ type: 'output_text', text: secret }],
|
||||
},
|
||||
],
|
||||
usage: { input_tokens: 40, output_tokens: 10 },
|
||||
},
|
||||
},
|
||||
},
|
||||
].map((record) => sanitizeDebugValue(record));
|
||||
for (const includeContent of [false, true]) {
|
||||
const sourceRoot = root(t);
|
||||
const handler = createDebugLogHandler({ sourceRoot, includeContent });
|
||||
for (const record of records) {
|
||||
const response = await request(handler, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(record),
|
||||
});
|
||||
assert.equal(response.status, 204);
|
||||
}
|
||||
const logged = readFileSync(
|
||||
path.join(sourceRoot, '.gev-logs/realtime-conversations.jsonl'),
|
||||
'utf8',
|
||||
);
|
||||
assert.equal(logged.includes(secret), includeContent);
|
||||
const [tool, client, server] = logged.trim().split('\n').map(JSON.parse);
|
||||
assert.equal(tool.payload.name, 'fly_to_location');
|
||||
assert.equal(tool.payload.callId, 'call-1');
|
||||
assert.equal(client.payload.message.item.call_id, 'call-1');
|
||||
assert.equal(server.payload.response.id, 'response-1');
|
||||
assert.equal(server.payload.response.output[0].type, 'message');
|
||||
assert.deepEqual(
|
||||
server.payload.response.usage,
|
||||
records[2].payload.response.usage,
|
||||
);
|
||||
if (includeContent) {
|
||||
assert.deepEqual(tool.payload.result, result);
|
||||
assert.equal(client.payload.message.item.output, JSON.stringify(result));
|
||||
} else {
|
||||
assert.equal(tool.payload.result, '[omitted]');
|
||||
assert.match(
|
||||
client.payload.message.item.output,
|
||||
/^\[omitted \d+ chars\]$/,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('the debug-log sink stays bounded, rate limited, and quiet about failures', async (t) => {
|
||||
const sourceRoot = root(t);
|
||||
const handler = install(openAiRealtimeProxy({ sourceRoot })).get(
|
||||
|
||||
Reference in new issue
Block a user