mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 19:34: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
|
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, typed or spoken text, and tool
|
# 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
|
# arguments/results (including serialized outputs) by default. Set to 1 to
|
||||||
# when you are done.
|
# 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
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
# Changelog
|
# 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
|
- Unqualified Alps searches use the bundled European range for navigation
|
||||||
and annotations instead of a same-named peak returned by a geocoder.
|
and annotations instead of a same-named peak returned by a geocoder.
|
||||||
Coordinates, supplied presets and geographically qualified place names
|
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
|
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, text and tool
|
the session cost and cap. The debug log omits transcripts, text, tool arguments
|
||||||
arguments unless `GEV_VOICE_LOG_CONTENT=1`.
|
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
|
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
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ 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, 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
|
* 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.
|
||||||
@@ -40,7 +40,7 @@ const VOICE_TEXT_FIELDS = new Set([
|
|||||||
const VOICE_CONTENT_MAX_DEPTH = 12;
|
const VOICE_CONTENT_MAX_DEPTH = 12;
|
||||||
const VOICE_CONTENT_MAX_DEPTH_MARKER = '[omitted: max depth]';
|
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) {
|
function omitVoiceContent(value, depth = 0) {
|
||||||
if (depth > VOICE_CONTENT_MAX_DEPTH) return VOICE_CONTENT_MAX_DEPTH_MARKER;
|
if (depth > VOICE_CONTENT_MAX_DEPTH) return VOICE_CONTENT_MAX_DEPTH_MARKER;
|
||||||
if (value === null || typeof value !== 'object') return value;
|
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));
|
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)) {
|
||||||
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] =
|
output[key] =
|
||||||
typeof item === 'string'
|
typeof item === 'string'
|
||||||
? `[omitted ${item.length} chars]`
|
? `[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 { realtimeInstructions } from '../../server/providers/openai/instructions.js';
|
||||||
import { GEV_REALTIME_TOOLS } from '../../server/providers/openai/tools.js';
|
import { GEV_REALTIME_TOOLS } from '../../server/providers/openai/tools.js';
|
||||||
import { createDebugLogHandler } from '../../server/providers/openai/debug-log.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';
|
import { standaloneVoiceTools } from '../../server/standalone/voiceTools.js';
|
||||||
|
|
||||||
function install(plugin, preview = false) {
|
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) => {
|
test('the debug-log sink stays bounded, rate limited, and quiet about failures', async (t) => {
|
||||||
const sourceRoot = root(t);
|
const sourceRoot = root(t);
|
||||||
const handler = install(openAiRealtimeProxy({ sourceRoot })).get(
|
const handler = install(openAiRealtimeProxy({ sourceRoot })).get(
|
||||||
|
|||||||
Reference in new issue
Block a user