mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(coderd/chatd): unify chat storage on SDK parts and fix file-reference rendering (#22958)
File-reference parts in user messages were flattened to `TextContent` at write time because fantasy has no file-reference content type. The frontend never saw them as structured parts. This moves all write paths (user, assistant, tool) from fantasy envelope format to `codersdk.ChatMessagePart`. The streaming layer (`chatloop`) is untouched, the conversion happens at the serialization boundary in `persistStep`. Old rows are still readable. `ParseContent` uses a structural heuristic (`isFantasyEnvelopeFormat`) to distinguish legacy envelopes from SDK parts. We chose this over try/fallback because fantasy envelopes partially unmarshal into `ChatMessagePart` (the `type` field matches) while silently losing content. A guard test enforces that no SDK part can produce the envelope shape. This is forward-only: new rows are unreadable by old code. Chat is behind a feature flag so rollback risk is contained. Also adds a typed `ChatMessageRole` to replace raw strings and `fantasy.MessageRole*` casts at the persistence boundary. The type covers `ChatMessage.Role`, `ChatStreamMessagePart.Role`, the `PublishMessagePart` callback chain, and all DB write sites. `fantasy.MessageRole*` remains only where we build `fantasy.Message` structs for LLM dispatch. Separately, `ProviderMetadata` was leaking to SSE clients via `publishMessagePart`. `StripInternal` now runs on both the SSE and REST paths, covering this. Other cleanup: - Old `db2sdk.contentBlockToPart` silently dropped metadata on text/reasoning/tool-call content. New code preserves it. - `providerMetadataToOptions` now logs warnings instead of silently returning nil. - `db2sdk` shrinks from ~250 lines of parallel conversion to ~15 lines delegating to `chatprompt.ParseContent()`, removing the `fantasy` import entirely. Refs #22821
This commit is contained in:
@@ -4,6 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
@@ -51,3 +53,59 @@ func TestChatModelProviderOptions_UnmarshalJSON_ParsesPlainProviderPayloads(t *t
|
||||
*decoded.Anthropic.Effort,
|
||||
)
|
||||
}
|
||||
|
||||
func TestChatMessagePart_StripInternal(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("StripsProviderMetadata", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
part := codersdk.ChatMessagePart{
|
||||
Type: codersdk.ChatMessagePartTypeToolCall,
|
||||
ToolCallID: "call-1",
|
||||
ToolName: "some_tool",
|
||||
Args: json.RawMessage(`{"key":"value"}`),
|
||||
ProviderMetadata: json.RawMessage(`{"type":"ephemeral"}`),
|
||||
}
|
||||
part.StripInternal()
|
||||
assert.Nil(t, part.ProviderMetadata)
|
||||
// Public fields preserved.
|
||||
assert.Equal(t, codersdk.ChatMessagePartTypeToolCall, part.Type)
|
||||
assert.Equal(t, "call-1", part.ToolCallID)
|
||||
assert.Equal(t, "some_tool", part.ToolName)
|
||||
assert.JSONEq(t, `{"key":"value"}`, string(part.Args))
|
||||
})
|
||||
|
||||
t.Run("StripsFileDataWhenFileIDSet", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
id := uuid.New()
|
||||
part := codersdk.ChatMessagePart{
|
||||
Type: codersdk.ChatMessagePartTypeFile,
|
||||
FileID: uuid.NullUUID{UUID: id, Valid: true},
|
||||
MediaType: "image/png",
|
||||
Data: []byte("binary-payload"),
|
||||
}
|
||||
part.StripInternal()
|
||||
assert.Nil(t, part.Data)
|
||||
assert.Equal(t, id, part.FileID.UUID)
|
||||
assert.Equal(t, "image/png", part.MediaType)
|
||||
})
|
||||
|
||||
t.Run("PreservesDataWhenNoFileID", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
part := codersdk.ChatMessagePart{
|
||||
Type: codersdk.ChatMessagePartTypeFile,
|
||||
MediaType: "image/png",
|
||||
Data: []byte("inline-data"),
|
||||
}
|
||||
part.StripInternal()
|
||||
assert.Equal(t, []byte("inline-data"), part.Data)
|
||||
})
|
||||
|
||||
t.Run("NoopOnCleanPart", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
part := codersdk.ChatMessageText("hello")
|
||||
part.StripInternal()
|
||||
assert.Equal(t, "hello", part.Text)
|
||||
assert.Equal(t, codersdk.ChatMessagePartTypeText, part.Type)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user