Round-trip message timestamps through `VercelAIAdapter`'s `UIMessage.metadata`
Merged
GE
5mo ago+562+562+012345678901234567890123456789-39-39-01234567890123456789
Summary
Round-trip ModelResponse.timestamp (and ModelRequest.timestamp where present) through Vercel AI UIMessage.metadata, plus application-level metadata. Server-side fields (usage, model_name, provider_*, provider_response_id, finish_reason) are deliberately excluded — UIMessage.metadata is client-controlled, so dumping these would leak server infrastructure and loading them would trust client-submitted history for values the server owns (e.g. forging provider_response_id to chain OpenAI previous_response_id='auto' into another user's conversation).
Broadening the round-trip behind an explicit user-controlled opt-in is tracked in #5174 as additive follow-up work.
Changes
_PydanticAIMessageMetadata(privateBaseModel,extra='ignore') under the reservedpydantic_aikey inUIMessage.metadata. Forward-compat: future fields can be added without a wire-protocol break.VercelAIAdapter.dump_messages/load_messagesround-triptimestampplus applicationmetadata.apply_message_metadatais conservative on the load side — empty/missing app-side metadata never clobbers priormessage.metadata, which matters when consecutiveUIMessages merge into the sameModelRequest.VercelAIEventStream.handle_run_resultemits a singlemessage-metadatachunk per run beforefinish-step, keeping the AI SDK's merge semantics equivalent to assignment. Errored runs don't emit one (noAgentRunResultEventto sourcetimestampfrom).MessagesBuilder.checkpoint()/last_modified()+ newBuilderCheckpointdataclass — internal change-tracking the Vercel adapter uses to attribute streamed metadata to the right rebuilt message. Re-exported frompydantic_ai.ui.- Client-supplied
instructionsand any other field outsidetimestampare silently dropped on load (matches the PR #4087 trust-filter precedent forSystemPromptParts). - Streaming dump-path asymmetry documented: streamed
message-metadatachunks fire only for assistant responses, whereasdump_messagespopulates both request and response sides.
Fixes #5174.
Tests
tests/test_vercel_ai.py— full dump-load round-trip (test_adapter_dump_load_roundtrip_with_message_metadata),MessageMetadataChunkemission (test_event_stream_emits_message_metadata), single-chunk-per-run invariant in multi-step runs (test_event_stream_emits_single_message_metadata_per_run), instruction-injection rejection (test_adapter_load_rejects_client_supplied_instructions), malformed-payload rejection (test_adapter_load_ignores_malformed_pydantic_metadata), merged-ModelRequestapp-metadata preservation (test_adapter_load_preserves_application_metadata_across_merged_messages), and ~5 more around edge cases (emptypydantic_aiblock, no target message, app-only metadata, etc.). ~42 existing snapshots updated to useIsStr()matchers for runtime timestamps.