refactor: type both chat message parsers (#23176)

Both message parsers accepted untyped input and relied on scattered
asRecord/asString calls to extract fields at runtime. With the
discriminated ChatMessagePart union, both accept typed input directly
and narrow via switch (part.type).

parseMessageContent narrows from (content: unknown) to
(content: readonly ChatMessagePart[] | undefined), removing legacy
input shape handling the Go backend normalizes away.
applyMessagePartToStreamState narrows from Record<string, unknown>
to ChatMessagePart.

The SSE type guards had a & Record<string, unknown> intersection
that widened everything untyped downstream. Since the data comes
from our own API, the intersection was removed and all handlers in
ChatContext now use generated types directly.

Fixes tool_call_id and tool_name variant tags in codersdk/chats.go:
marked optional to match reality (Go guards against empty values,
omitempty omits them at the wire level).

Refs #23168, #23175
This commit is contained in:
Mathias Fredriksson
2026-03-18 15:50:57 +02:00
committed by GitHub
parent d69dcf18de
commit 8b4d35798a
8 changed files with 207 additions and 323 deletions
+2 -2
View File
@@ -129,8 +129,8 @@ type ChatMessagePart struct {
Type ChatMessagePartType `json:"type"`
Text string `json:"text,omitempty" variants:"text,reasoning"`
Signature string `json:"signature,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty" variants:"tool-call,tool-result"`
ToolName string `json:"tool_name,omitempty" variants:"tool-call,tool-result"`
ToolCallID string `json:"tool_call_id,omitempty" variants:"tool-call?,tool-result?"`
ToolName string `json:"tool_name,omitempty" variants:"tool-call?,tool-result?"`
Args json.RawMessage `json:"args,omitempty" variants:"tool-call?"`
ArgsDelta string `json:"args_delta,omitempty" variants:"tool-call?"`
Result json.RawMessage `json:"result,omitempty" variants:"tool-result?"`
-11
View File
@@ -223,7 +223,6 @@ func TestChatMessagePartVariantTags(t *testing.T) {
// Parse all variants tags from the struct and validate them.
typ := reflect.TypeOf(codersdk.ChatMessagePart{})
coveredTypes := make(map[codersdk.ChatMessagePartType]bool)
hasRequired := make(map[codersdk.ChatMessagePartType]bool)
for i := range typ.NumField() {
f := typ.Field(i)
@@ -245,7 +244,6 @@ func TestChatMessagePartVariantTags(t *testing.T) {
"the discriminant field must not have a variants tag; %s", editHint)
for _, entry := range strings.Split(varTag, ",") {
isOptional := strings.HasSuffix(entry, "?")
typeLit := codersdk.ChatMessagePartType(strings.TrimSuffix(entry, "?"))
assert.True(t, knownTypes[typeLit],
@@ -253,9 +251,6 @@ func TestChatMessagePartVariantTags(t *testing.T) {
f.Name, typeLit, editHint)
coveredTypes[typeLit] = true
if !isOptional {
hasRequired[typeLit] = true
}
}
}
@@ -264,12 +259,6 @@ func TestChatMessagePartVariantTags(t *testing.T) {
assert.True(t, coveredTypes[pt],
"ChatMessagePartType %q is not referenced by any variants tag; %s", pt, editHint)
}
// Every variant must have at least one required field.
for pt := range coveredTypes {
assert.True(t, hasRequired[pt],
"variant %q has no required fields (all have ? suffix); %s", pt, editHint)
}
}
func TestModelCostConfig_LegacyNumericJSON(t *testing.T) {