feat(platform-api): map hexgate.messages spans into LlmMessageEvent - #186
Conversation
befe0a7 to
06e6470
Compare
06e6470 to
110e2da
Compare
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
110e2da to
71dd5d4
Compare
32aaaad to
4cf0de1
Compare
Nested JSON-string tool-call
|
guillaume-hexamind
left a comment
There was a problem hiding this comment.
One blocking issue above; I approve of the logic otherwise
4cf0de1 to
7573f5e
Compare
LlmMessageEvent mirrors the llm_message columns; capped_input_messages / capped_output_messages / capped_system_instructions redact by key and cap head+tail through the SDK helpers, returning the cut flag that _message_fields ORs into the SDK's own. SCOPE_MESSAGES stays out of KNOWN_SCOPES: the consumer has no bucket for it yet, so accepting it now would validate and silently drop the span. It joins the tuple with the insert path.
A tool-call part in a gen_ai.* message may carry its arguments as a
serialized JSON string, the raw OpenAI wire shape, so the secret keys sit
one JSON level below what as_json_value parsed and the substring key match
never saw them. redact() now takes an opt-in set of keys whose string
values are parsed, redacted inside and serialized back; enforcement and
the DLQ envelope pass {"arguments"}. Exact key names only, so a user
message whose content happens to be JSON is never rewritten.
7573f5e to
594aae8
Compare
Fixed in 594aae8: redact now parses string values under arguments, redacts inside, and serialises back, at both the enforcement and DLQ call sites, with string-shaped tests beside the object-shaped ones; the pre-existing decision-path gap under arbitrary keys stays out of scope here. |
Design: LLM message logging design · implementation spec.
What is changing
LlmMessageEvent(AuditEnvelope)inschemas.py, mirroring thellm_messagecolumns of #183 includingrun_id;capped_input_messages()/capped_output_messages()/capped_system_instructions()injobs/enricher/enforcement.py(key-redact like arguments, head+tail cap via the PR 1 helpers, truncate never reject, each returning the cut flag);_message_fields()and amap_spanbranch injobs/enricher/mapping.pythat ORs the enricher's cuts into the SDK'struncated;as_json_value()incoerce.pyfor the array-shaped attributes (the two bool flags reach the model raw — Pydantic coerces them).SCOPE_MESSAGESis not added toKNOWN_SCOPEShere — the scope stays a loudunknown_scopeDLQ reject until PR 4.jobs/enricher/dlq.pynow parses and redacts the threegen_ai.*message arrays before dead-lettering, the way it already does forarguments/hint/attributes— the DLQ is this PR's intended destination for every message span, and a tool-call message carries the same caller arguments a decision does.Why is this change necessary
consumer.pybuckets events by type and commits the offset after the inserts, so a scope accepted before it has a bucket would be validated and then silently dropped with no DLQ record. Keeping registration for PR 4 means this PR can be reverted alone and main stays non-lossy. The DLQ redaction closes the gap that routing message spans there would otherwise open on a 30-day, no-ACL topic.Tests
platform/api/tests/jobs/enricher/test_mapping.py(calls_message_fields()directly: happy path, absent optional fields, SDKtruncatedkept, over-cap stored short, required attributes,run_idin/out);test_enforcement.py(cap boundary at and one byte over, head+tail marker, tool-call secret redacted, 8 KiB output/system caps);test_coerce.py(as_json_value);test_dlq.py(secret inside a message array redacted, bare-string field dropped).🤖 Generated with Claude Code