Summary
We currently treat tool call_id as our internal tool_calls.uuid instead of the model/provider's opaque string ID. That came from the older classifier-style tool execution path, where we generated the tool call ourselves. The temporary Kimi replay normalization merged to master works, but this should be cleaned up at the storage and protocol layers.
Current behavior
src/web/responses/handlers.rs::append_streamed_tool_calls() and finalize_first_model_tool_call() do not retain provider tool_calls[*].id
src/web/responses/handlers.rs::execute_tool_call_and_wait() mints Uuid::new_v4() for tool_call_id
tool_calls.uuid is persisted and then reused as the external call_id
src/web/responses/conversions.rs still types ConversationItem::{FunctionToolCall, FunctionToolCallOutput}.call_id as Uuid
src/web/responses/context_builder.rs replays those UUID strings back into assistant/tool history, with a Kimi-only normalization pass layered on top
Proposed fix
- Add a nullable text column on
tool_calls, e.g. protocol_call_id (or similar)
- Keep the existing UUID column for internal joins and
tool_call_fk
- Persist the real provider/model-emitted tool call ID when available
- Resolve external/API
call_id as protocol_call_id.unwrap_or(uuid.to_string()) so legacy rows still work
- Change protocol-facing Rust types/serialization to use
String for call_id instead of Uuid
- Continue to normalize replayed IDs for Kimi models at the request boundary, but normalize from the resolved external/protocol
call_id
- Do not add a duplicate call-id column to
tool_outputs; resolve through tool_call_fk
Acceptance criteria
- Migration adds nullable string column to
tool_calls
- Responses/Conversations code persists real provider/model tool-call IDs when present
- API output treats
call_id as an opaque string everywhere
- Replay/build-prompt path uses stored protocol call IDs with UUID fallback for old rows
- Kimi models still receive normalized
functions.{name}:{index} history IDs
- Regression tests cover legacy UUID-only rows, stored protocol IDs, and Kimi replay normalization
Context
The Kimi-specific replay fix is already merged and working; this issue tracks the follow-up to remove the underlying UUID == protocol call_id tech debt.
Summary
We currently treat tool
call_idas our internaltool_calls.uuidinstead of the model/provider's opaque string ID. That came from the older classifier-style tool execution path, where we generated the tool call ourselves. The temporary Kimi replay normalization merged tomasterworks, but this should be cleaned up at the storage and protocol layers.Current behavior
src/web/responses/handlers.rs::append_streamed_tool_calls()andfinalize_first_model_tool_call()do not retain providertool_calls[*].idsrc/web/responses/handlers.rs::execute_tool_call_and_wait()mintsUuid::new_v4()fortool_call_idtool_calls.uuidis persisted and then reused as the externalcall_idsrc/web/responses/conversions.rsstill typesConversationItem::{FunctionToolCall, FunctionToolCallOutput}.call_idasUuidsrc/web/responses/context_builder.rsreplays those UUID strings back into assistant/tool history, with a Kimi-only normalization pass layered on topProposed fix
tool_calls, e.g.protocol_call_id(or similar)tool_call_fkcall_idasprotocol_call_id.unwrap_or(uuid.to_string())so legacy rows still workStringforcall_idinstead ofUuidcall_idtool_outputs; resolve throughtool_call_fkAcceptance criteria
tool_callscall_idas an opaque string everywherefunctions.{name}:{index}history IDsContext
The Kimi-specific replay fix is already merged and working; this issue tracks the follow-up to remove the underlying
UUID == protocol call_idtech debt.