Files
coder/coderd/x/chatd/compaction_sanitize_internal_test.go
T
Michael Suchacz 0e16e356b0 refactor(coderd/x/chatd): read the OpenAI transport from the model (#27704)
Stacked on #27703.

Provider option conversion, reasoning effort injection, and file part
acceptance each recomputed the OpenAI wire format from `(provider,
modelID, override)`. They now read it from `chatprovider.Model`, so a
decision cannot drift from the client it was built for.

`ProviderOptionsFromChatConfig` takes a `Transport`,
`ApplyReasoningEffort` takes a `Model`, and `AcceptsFilePartMediaType`
becomes a `Model` method. `UsesResponsesAPI` and `UsesResponsesOptions`
are deleted. The override extraction is unexported and reachable only
from `ModelFromConfig`, which now takes the model's
`ChatModelOpenAIConfig` directly, removing the six scattered extractions
at call sites.

That also resolves the computer-use mismatch. The computer-use model is
a hardcoded default with no config row of its own: its client was built
without an override while request preparation applied the chat model's.
Preparation now reads the computer-use model's own transport, so the two
agree without one model's client settings following a different model.
Passing the chat model's `openai_config` into the computer-use client
would have made them agree on the wrong value.

`TestModelTransportConsumersAgree` pins the invariant in one test: the
HTTP path the client actually hits, the concrete provider option struct
type, the type created by reasoning effort, and text/image file
acceptance.

> Mux prepared this PR on Mike's behalf.
2026-08-04 08:00:19 +00:00

199 lines
6.8 KiB
Go

package chatd
import (
"testing"
"charm.land/fantasy"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"cdr.dev/slog/v3/sloggers/slogtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/x/chatd/chatprovider"
"github.com/coder/coder/v2/coderd/x/chatd/chattest"
"github.com/coder/coder/v2/testutil"
)
func TestSameCompactionProviderIdentity(t *testing.T) {
t.Parallel()
providerID := uuid.New()
require.True(t, sameCompactionProviderIdentity(configWithProvider(providerID), configWithProvider(providerID)))
require.False(t, sameCompactionProviderIdentity(configWithProvider(providerID), configWithProvider(uuid.New())))
// Legacy configs without a provider FK compare as different (fail closed).
require.False(t, sameCompactionProviderIdentity(database.ChatModelConfig{}, configWithProvider(providerID)))
require.False(t, sameCompactionProviderIdentity(database.ChatModelConfig{}, database.ChatModelConfig{}))
}
func configWithProvider(id uuid.UUID) database.ChatModelConfig {
return database.ChatModelConfig{AIProviderID: uuid.NullUUID{UUID: id, Valid: true}}
}
func TestSanitizeCompactionPrompt_FlattensForeignProviderExecutedToolParts(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
prompt := []fantasy.Message{
{
Role: fantasy.MessageRoleUser,
Content: []fantasy.MessagePart{
fantasy.TextPart{Text: "search the web"},
},
},
{
Role: fantasy.MessageRoleAssistant,
Content: []fantasy.MessagePart{
fantasy.TextPart{Text: "searching"},
fantasy.ToolCallPart{
ToolCallID: "ws-1",
ToolName: "web_search",
Input: `{"query":"coder"}`,
ProviderExecuted: true,
},
fantasy.ToolResultPart{
ToolCallID: "ws-1",
Output: fantasy.ToolResultOutputContentText{Text: "results"},
ProviderExecuted: true,
},
},
},
{
Role: fantasy.MessageRoleAssistant,
Content: []fantasy.MessagePart{
fantasy.ToolCallPart{
ToolCallID: "local-1",
ToolName: "read_file",
Input: `{"path":"/tmp/a.txt"}`,
},
},
},
}
compactionModel := chatprovider.NewModel(&chattest.FakeModel{ProviderName: "openai", ModelName: "gpt-4.1-mini"}, nil)
sanitized := sanitizeCompactionPrompt(ctx, logger, prompt, compactionModel, configWithProvider(uuid.New()), configWithProvider(uuid.New()))
require.Len(t, sanitized, 3)
// Provider-executed parts are flattened to text so the summary keeps
// their content without the provider-specific wire shape.
require.Len(t, sanitized[1].Content, 3)
require.Equal(t, fantasy.TextPart{Text: "searching"}, sanitized[1].Content[0])
require.Equal(t, fantasy.TextPart{Text: `[Server tool call: web_search] {"query":"coder"}`}, sanitized[1].Content[1])
require.Equal(t, fantasy.TextPart{Text: "[Server tool result: web_search] results"}, sanitized[1].Content[2])
// Local tool calls replay fine across providers and must survive.
require.Len(t, sanitized[2].Content, 1)
require.Equal(t, "read_file", sanitized[2].Content[0].(fantasy.ToolCallPart).ToolName)
// The original prompt used for assistant generation is untouched.
require.Equal(t, fantasy.ToolCallPart{
ToolCallID: "ws-1",
ToolName: "web_search",
Input: `{"query":"coder"}`,
ProviderExecuted: true,
}, prompt[1].Content[1])
}
func TestSanitizeCompactionPrompt_DropsNonAssistantProviderExecutedParts(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
// Provider-executed parts outside assistant messages are anomalous; a
// flattened text part is not valid tool-message content, so they drop.
prompt := []fantasy.Message{
{
Role: fantasy.MessageRoleTool,
Content: []fantasy.MessagePart{
fantasy.ToolResultPart{
ToolCallID: "ws-1",
Output: fantasy.ToolResultOutputContentText{Text: "results"},
ProviderExecuted: true,
},
},
},
{
Role: fantasy.MessageRoleUser,
Content: []fantasy.MessagePart{
fantasy.TextPart{Text: "hello"},
},
},
}
compactionModel := chatprovider.NewModel(&chattest.FakeModel{ProviderName: "openai", ModelName: "gpt-4.1-mini"}, nil)
sanitized := sanitizeCompactionPrompt(ctx, logger, prompt, compactionModel, configWithProvider(uuid.New()), configWithProvider(uuid.New()))
require.Len(t, sanitized, 1)
require.Equal(t, fantasy.MessageRoleUser, sanitized[0].Role)
require.Len(t, prompt, 2)
}
func TestSanitizeCompactionPrompt_ReplacesUnsupportedFileParts(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
prompt := []fantasy.Message{
{
Role: fantasy.MessageRoleUser,
Content: []fantasy.MessagePart{
fantasy.TextPart{Text: "look at this"},
fantasy.FilePart{
Filename: "diagram.pdf",
Data: []byte("%PDF-"),
MediaType: "application/pdf",
},
},
},
}
// Mistral accepts images but not PDFs, so the PDF part must become a
// placeholder while the prompt stays otherwise intact.
compactionModel := chatprovider.NewModel(&chattest.FakeModel{ProviderName: "mistral", ModelName: "mistral-large"}, nil)
sharedProviderID := uuid.New()
sanitized := sanitizeCompactionPrompt(ctx, logger, prompt, compactionModel, configWithProvider(sharedProviderID), configWithProvider(sharedProviderID))
require.Len(t, sanitized, 1)
require.Len(t, sanitized[0].Content, 2)
textPart, ok := sanitized[0].Content[1].(fantasy.TextPart)
require.True(t, ok)
require.Contains(t, textPart.Text, "diagram.pdf")
require.Contains(t, textPart.Text, "not supported by the compaction model")
// The original prompt keeps its file part.
_, ok = prompt[0].Content[1].(fantasy.FilePart)
require.True(t, ok)
}
func TestSanitizeCompactionPrompt_SameProviderKeepsProviderExecutedParts(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
prompt := []fantasy.Message{
{
Role: fantasy.MessageRoleAssistant,
Content: []fantasy.MessagePart{
fantasy.ToolCallPart{
ToolCallID: "ws-1",
ToolName: "web_search",
Input: `{"query":"coder"}`,
ProviderExecuted: true,
},
fantasy.ToolResultPart{
ToolCallID: "ws-1",
Output: fantasy.ToolResultOutputContentText{Text: "results"},
ProviderExecuted: true,
},
},
},
}
compactionModel := chatprovider.NewModel(&chattest.FakeModel{ProviderName: "openai", ModelName: "gpt-4.1-mini"}, nil)
sharedProviderID := uuid.New()
sanitized := sanitizeCompactionPrompt(ctx, logger, prompt, compactionModel, configWithProvider(sharedProviderID), configWithProvider(sharedProviderID))
require.Len(t, sanitized, 1)
require.Len(t, sanitized[0].Content, 2)
}