mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
Our fantasy fork had drifted far behind upstream charmbracelet/fantasy (base v0.31.0 vs current v0.40.0). This PR updates the pinned forks after reconciling which fork hacks upstream has fixed and which we still need, and adapts this repo to the new APIs. ## Fork updates - `charm.land/fantasy` -> [coder/fantasy#51](https://github.com/coder/fantasy/pull/51) (merged): `coder_2_33` synced with upstream v0.40.0, pinned at the merge commit `bb10946892ef`. - `github.com/openai/openai-go/v3` -> [coder/openai-go#10](https://github.com/coder/openai-go/pull/10) (merged): `coder/pinned` rebased from v3.16.0 onto upstream v3.50.0 (required by upstream fantasy), pinned at the merge commit `92b5addb22d2`. - `coder/anthropic-sdk-go` pin unchanged; the fantasy fork now tracks the same revision this repo ships. ## Hack reconciliation summary Dropped from our fantasy diff (upstream now has equivalents, often stricter): truncated-stream fail-closed detection, Anthropic EffortXHigh / computer use / thinking effort / thinking display, replay fidelity for signed reasoning and web_search errors, PDF and text documents with sanitized filename titles, refusal finish-reason mapping (upstream also maps Bedrock `content_filtered`/`guardrail_intervened`), gpt-5.5/5.6 Responses routing, the Go 1.25 downgrade, and the openai-go SSE decoder and appendCompact patches. Still fork-only and preserved: OpenAI computer use, OpenAI Responses replay continuity validation, Anthropic pre-4.6 budget-thinking conversion plus explicit thinking disable for effort none, Anthropic RefusalMetadata parsing, Bedrock cross-region inference profile region mirroring, and openai-go deferred body serialization with the WithJSONSet fix. Picked up new upstream features: stream transport retry with in-band SSE error classification, Bedrock expired-credential refresh, per-message cache markers for OpenAI-compatible models, tool panic recovery, extra usage fields in provider metadata, and ClientMetadata on tool results. ## Changes in this repo - `aibridge/intercept/responses`: `ResponseOutputItemUnion.Arguments` became a union type in openai-go v3.50; read function-call arguments via `.OfString` (plus test literal updates). - `coderd/x/chatd/chatdebug`: register the new fantasy `Call.Headers`, `ObjectCall.Headers`, and `ToolResultPart.ClientMetadata` fields in the normalization coverage map (all skipped). - `aibridge/internal/integrationtest`: make the RST test listener drain the request before resetting the connection. The new SDK's write path exposed the previous 1-byte-read race as sporadic `use of closed network connection` failures; the fix holds over 40 consecutive runs. - `go.mod`: rewrite the fork provenance comments to describe the post-sync state. ## Validation - `go build ./...` and `go vet ./...` clean (vet findings identical to base). - Fresh (`-count=1`) runs of `./coderd/x/chatd/...`, `./aibridge/...`, `./coderd/aibridged/...`, `./coderd/database/db2sdk/`: 37 packages pass. - `TestClientAndConnectionError` stress-tested 40x clean. - Both fork PRs have green CI. > Mux acted on Mike's behalf to create this PR.
336 lines
13 KiB
Go
336 lines
13 KiB
Go
package chatdebug
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
|
|
"charm.land/fantasy"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// fieldDisposition documents whether a fantasy struct field is captured
|
|
// by the corresponding normalized struct ("normalized") or
|
|
// intentionally omitted ("skipped: <reason>"). The test fails when a
|
|
// fantasy type gains a field that is not yet classified, forcing the
|
|
// developer to decide whether to normalize or skip it.
|
|
//
|
|
// This mirrors the audit-table exhaustiveness check in
|
|
// enterprise/audit/table.go; same idea, different domain.
|
|
type fieldDisposition = map[string]string
|
|
|
|
// TestNormalizationFieldCoverage ensures every exported field on the
|
|
// fantasy types that model.go normalizes is explicitly accounted for.
|
|
// When the fantasy library adds a field the test fails, surfacing the
|
|
// drift at `go test` time rather than silently dropping data.
|
|
func TestNormalizationFieldCoverage(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
typ reflect.Type
|
|
fields fieldDisposition
|
|
}{
|
|
// ── struct-to-struct mappings ──────────────────────────
|
|
|
|
{
|
|
name: "fantasy.Usage → normalizedUsage",
|
|
typ: reflect.TypeFor[fantasy.Usage](),
|
|
fields: fieldDisposition{
|
|
"InputTokens": "normalized",
|
|
"OutputTokens": "normalized",
|
|
"TotalTokens": "normalized",
|
|
"ReasoningTokens": "normalized",
|
|
"CacheCreationTokens": "normalized",
|
|
"CacheReadTokens": "normalized",
|
|
},
|
|
},
|
|
{
|
|
name: "fantasy.Call → normalizedCallPayload",
|
|
typ: reflect.TypeFor[fantasy.Call](),
|
|
fields: fieldDisposition{
|
|
"Prompt": "normalized",
|
|
"MaxOutputTokens": "normalized",
|
|
"Temperature": "normalized",
|
|
"TopP": "normalized",
|
|
"TopK": "normalized",
|
|
"PresencePenalty": "normalized",
|
|
"FrequencyPenalty": "normalized",
|
|
"Tools": "normalized",
|
|
"ToolChoice": "normalized",
|
|
"UserAgent": "skipped: internal transport header, not useful for debug panel",
|
|
"Headers": "skipped: transport headers, may carry credentials",
|
|
"ProviderOptions": "skipped: opaque provider data, only count preserved",
|
|
},
|
|
},
|
|
{
|
|
name: "fantasy.ObjectCall → normalizedObjectCallPayload",
|
|
typ: reflect.TypeFor[fantasy.ObjectCall](),
|
|
fields: fieldDisposition{
|
|
"Prompt": "normalized",
|
|
"Schema": "skipped: full schema too large; SchemaName+SchemaDescription captured instead",
|
|
"SchemaName": "normalized",
|
|
"SchemaDescription": "normalized",
|
|
"MaxOutputTokens": "normalized",
|
|
"Temperature": "normalized",
|
|
"TopP": "normalized",
|
|
"TopK": "normalized",
|
|
"PresencePenalty": "normalized",
|
|
"FrequencyPenalty": "normalized",
|
|
"UserAgent": "skipped: internal transport header, not useful for debug panel",
|
|
"Headers": "skipped: transport headers, may carry credentials",
|
|
"ProviderOptions": "skipped: opaque provider data, only count preserved",
|
|
"RepairText": "skipped: function value, not serializable",
|
|
},
|
|
},
|
|
{
|
|
name: "fantasy.Response → normalizedResponsePayload",
|
|
typ: reflect.TypeFor[fantasy.Response](),
|
|
fields: fieldDisposition{
|
|
"Content": "normalized",
|
|
"FinishReason": "normalized",
|
|
"Usage": "normalized",
|
|
"Warnings": "normalized",
|
|
"ProviderMetadata": "skipped: opaque provider-specific metadata",
|
|
},
|
|
},
|
|
{
|
|
name: "fantasy.ObjectResponse → normalizedObjectResponsePayload",
|
|
typ: reflect.TypeFor[fantasy.ObjectResponse](),
|
|
fields: fieldDisposition{
|
|
"Object": "skipped: arbitrary user type, not serializable generically",
|
|
"RawText": "normalized: as RawTextLength (length only, content unbounded)",
|
|
"Usage": "normalized",
|
|
"FinishReason": "normalized",
|
|
"Warnings": "normalized",
|
|
"ProviderMetadata": "skipped: opaque provider-specific metadata",
|
|
},
|
|
},
|
|
{
|
|
name: "fantasy.CallWarning → normalizedWarning",
|
|
typ: reflect.TypeFor[fantasy.CallWarning](),
|
|
fields: fieldDisposition{
|
|
"Type": "normalized",
|
|
"Setting": "normalized",
|
|
"Tool": "skipped: interface value, warning message+type sufficient for debug panel",
|
|
"Details": "normalized",
|
|
"Message": "normalized",
|
|
},
|
|
},
|
|
{
|
|
name: "fantasy.StreamPart → appendNormalizedStreamContent",
|
|
typ: reflect.TypeFor[fantasy.StreamPart](),
|
|
fields: fieldDisposition{
|
|
"Type": "normalized",
|
|
"ID": "normalized: as ToolCallID in content parts",
|
|
"ToolCallName": "normalized: as ToolName in content parts",
|
|
"ToolCallInput": "normalized: as Arguments or Result (bounded)",
|
|
"Delta": "normalized: accumulated into text/reasoning content parts",
|
|
"ProviderExecuted": "skipped: provider vs client distinction not needed for debug panel",
|
|
"Usage": "normalized: captured in stream finalize",
|
|
"FinishReason": "normalized: captured in stream finalize",
|
|
"Error": "normalized: captured in stream error handling",
|
|
"Warnings": "normalized: captured in stream warning accumulation",
|
|
"SourceType": "normalized",
|
|
"URL": "normalized",
|
|
"Title": "normalized",
|
|
"ProviderMetadata": "skipped: opaque provider-specific metadata",
|
|
},
|
|
},
|
|
{
|
|
name: "fantasy.ObjectStreamPart → wrapObjectStreamSeq",
|
|
typ: reflect.TypeFor[fantasy.ObjectStreamPart](),
|
|
fields: fieldDisposition{
|
|
"Type": "normalized: drives switch in wrapObjectStreamSeq",
|
|
"Object": "skipped: arbitrary user type, only ObjectPartCount tracked",
|
|
"Delta": "normalized: accumulated into rawTextLength",
|
|
"Error": "normalized: captured in stream error handling",
|
|
"Usage": "normalized: captured in stream finalize",
|
|
"FinishReason": "normalized: captured in stream finalize",
|
|
"Warnings": "normalized: captured in stream warning accumulation",
|
|
"ProviderMetadata": "skipped: opaque provider-specific metadata",
|
|
},
|
|
},
|
|
|
|
// ── message part types (normalizeMessageParts) ────────
|
|
|
|
{
|
|
name: "fantasy.TextPart → normalizedMessagePart",
|
|
typ: reflect.TypeFor[fantasy.TextPart](),
|
|
fields: fieldDisposition{
|
|
"Text": "normalized: bounded to MaxMessagePartTextLength",
|
|
"ProviderOptions": "skipped: opaque provider-specific options",
|
|
},
|
|
},
|
|
{
|
|
name: "fantasy.ReasoningPart → normalizedMessagePart",
|
|
typ: reflect.TypeFor[fantasy.ReasoningPart](),
|
|
fields: fieldDisposition{
|
|
"Text": "normalized: bounded to MaxMessagePartTextLength",
|
|
"ProviderOptions": "skipped: opaque provider-specific options",
|
|
},
|
|
},
|
|
{
|
|
name: "fantasy.FilePart → normalizedMessagePart",
|
|
typ: reflect.TypeFor[fantasy.FilePart](),
|
|
fields: fieldDisposition{
|
|
"Filename": "normalized",
|
|
"Data": "skipped: binary data never stored in debug records",
|
|
"MediaType": "normalized",
|
|
"ProviderOptions": "skipped: opaque provider-specific options",
|
|
},
|
|
},
|
|
{
|
|
name: "fantasy.ToolCallPart → normalizedMessagePart",
|
|
typ: reflect.TypeFor[fantasy.ToolCallPart](),
|
|
fields: fieldDisposition{
|
|
"ToolCallID": "normalized",
|
|
"ToolName": "normalized",
|
|
"Input": "normalized: as Arguments (bounded)",
|
|
"ProviderExecuted": "skipped: provider vs client distinction not needed for debug panel",
|
|
"ProviderOptions": "skipped: opaque provider-specific options",
|
|
},
|
|
},
|
|
{
|
|
name: "fantasy.ToolResultPart → normalizedMessagePart",
|
|
typ: reflect.TypeFor[fantasy.ToolResultPart](),
|
|
fields: fieldDisposition{
|
|
"ToolCallID": "normalized",
|
|
"Output": "normalized: text extracted via normalizeToolResultOutput",
|
|
"ProviderExecuted": "skipped: provider vs client distinction not needed for debug panel",
|
|
"ProviderOptions": "skipped: opaque provider-specific options",
|
|
"ClientMetadata": "skipped: client execution metadata not needed for debug panel",
|
|
},
|
|
},
|
|
|
|
// ── response content types (normalizeContentParts) ────
|
|
|
|
{
|
|
name: "fantasy.TextContent → normalizedContentPart",
|
|
typ: reflect.TypeFor[fantasy.TextContent](),
|
|
fields: fieldDisposition{
|
|
"Text": "normalized: bounded to MaxMessagePartTextLength",
|
|
"ProviderMetadata": "skipped: opaque provider-specific metadata",
|
|
},
|
|
},
|
|
{
|
|
name: "fantasy.ReasoningContent → normalizedContentPart",
|
|
typ: reflect.TypeFor[fantasy.ReasoningContent](),
|
|
fields: fieldDisposition{
|
|
"Text": "normalized: bounded to MaxMessagePartTextLength",
|
|
"ProviderMetadata": "skipped: opaque provider-specific metadata",
|
|
},
|
|
},
|
|
{
|
|
name: "fantasy.FileContent → normalizedContentPart",
|
|
typ: reflect.TypeFor[fantasy.FileContent](),
|
|
fields: fieldDisposition{
|
|
"MediaType": "normalized",
|
|
"Data": "skipped: binary data never stored in debug records",
|
|
"ProviderMetadata": "skipped: opaque provider-specific metadata",
|
|
},
|
|
},
|
|
{
|
|
name: "fantasy.SourceContent → normalizedContentPart",
|
|
typ: reflect.TypeFor[fantasy.SourceContent](),
|
|
fields: fieldDisposition{
|
|
"SourceType": "normalized",
|
|
"ID": "skipped: provider-internal identifier, not actionable in debug panel",
|
|
"URL": "normalized",
|
|
"Title": "normalized",
|
|
"MediaType": "skipped: only relevant for document sources, rarely useful for debugging",
|
|
"Filename": "skipped: only relevant for document sources, rarely useful for debugging",
|
|
"ProviderMetadata": "skipped: opaque provider-specific metadata",
|
|
},
|
|
},
|
|
{
|
|
name: "fantasy.ToolCallContent → normalizedContentPart",
|
|
typ: reflect.TypeFor[fantasy.ToolCallContent](),
|
|
fields: fieldDisposition{
|
|
"ToolCallID": "normalized",
|
|
"ToolName": "normalized",
|
|
"Input": "normalized: as Arguments (bounded), InputLength tracks original",
|
|
"ProviderExecuted": "skipped: provider vs client distinction not needed for debug panel",
|
|
"ProviderMetadata": "skipped: opaque provider-specific metadata",
|
|
"Invalid": "skipped: validation state not surfaced in debug panel",
|
|
"ValidationError": "skipped: validation state not surfaced in debug panel",
|
|
},
|
|
},
|
|
{
|
|
name: "fantasy.ToolResultContent → normalizedContentPart",
|
|
typ: reflect.TypeFor[fantasy.ToolResultContent](),
|
|
fields: fieldDisposition{
|
|
"ToolCallID": "normalized",
|
|
"ToolName": "normalized",
|
|
"Result": "normalized: text extracted via normalizeToolResultOutput",
|
|
"ClientMetadata": "skipped: client execution metadata not needed for debug panel",
|
|
"ProviderExecuted": "skipped: provider vs client distinction not needed for debug panel",
|
|
"ProviderMetadata": "skipped: opaque provider-specific metadata",
|
|
"StopTurn": "skipped: control-flow flag is not rendered in the debug panel",
|
|
},
|
|
},
|
|
|
|
// ── tool types (normalizeTools) ───────────────────────
|
|
|
|
{
|
|
name: "fantasy.FunctionTool → normalizedTool",
|
|
typ: reflect.TypeFor[fantasy.FunctionTool](),
|
|
fields: fieldDisposition{
|
|
"Name": "normalized",
|
|
"Description": "normalized",
|
|
"InputSchema": "normalized: preserved as JSON for debug panel rendering",
|
|
"ProviderOptions": "skipped: opaque provider-specific options",
|
|
},
|
|
},
|
|
{
|
|
name: "fantasy.ProviderDefinedTool → normalizedTool",
|
|
typ: reflect.TypeFor[fantasy.ProviderDefinedTool](),
|
|
fields: fieldDisposition{
|
|
"ID": "normalized",
|
|
"Name": "normalized",
|
|
"Args": "skipped: provider-specific configuration not needed for debug panel",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Every exported field on the fantasy type must be
|
|
// registered as "normalized" or "skipped: <reason>".
|
|
for i := range tt.typ.NumField() {
|
|
field := tt.typ.Field(i)
|
|
if !field.IsExported() {
|
|
continue
|
|
}
|
|
disposition, ok := tt.fields[field.Name]
|
|
if !ok {
|
|
require.Failf(t, "unregistered field",
|
|
"%s.%s is not in the coverage map: "+
|
|
"add it as \"normalized\" or \"skipped: <reason>\"",
|
|
tt.typ.Name(), field.Name)
|
|
}
|
|
require.NotEmptyf(t, disposition,
|
|
"%s.%s has an empty disposition: "+
|
|
"use \"normalized\" or \"skipped: <reason>\"",
|
|
tt.typ.Name(), field.Name)
|
|
}
|
|
|
|
// Catch stale entries that reference removed fields.
|
|
for name := range tt.fields {
|
|
found := false
|
|
for i := range tt.typ.NumField() {
|
|
if tt.typ.Field(i).Name == name {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
require.Truef(t, found,
|
|
"stale coverage entry %s.%s: "+
|
|
"field no longer exists in fantasy, remove it",
|
|
tt.typ.Name(), name)
|
|
}
|
|
})
|
|
}
|
|
}
|