mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
Drops provider-executed tool history (calls and results) from assistant rows whose producing provider ID differs from the target turn's provider ID, before the prompt is built. Same-provider history is left untouched, so normal `web_search` replay is unaffected. - When a model config has an `AIProviderID`, use this as identity so two providers of the same type (e.g. two `openai-compat` providers at different base URLs) are correctly distinguished. Falls back to the normalized provider type name. - Sanitization runs at the `database.ChatMessage` row level in `prepareGeneration`. The `chatloop` pre-request and reload paths are untouched. - Foreign provider-executed results are dropped and not converted to text. - Unknown origin (unresolvable `ModelConfigID`) fails closed (strip). - Adds tests for the pure `stripForeignProviderExecutedToolRows`. - Adds unit tests for `modelConfigProviderIdentity`. _This pull request was created by Coder Agents on behalf of @johnstcn._
248 lines
8.0 KiB
Go
248 lines
8.0 KiB
Go
package chatd
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/sqlc-dev/pqtype"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/x/chatd/chatprompt"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
)
|
|
|
|
func TestStripForeignProviderExecutedToolRows(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
anthropic = "anthropic"
|
|
bedrock = "bedrock"
|
|
openai = "openai"
|
|
)
|
|
|
|
anthropicCfg := uuid.New()
|
|
openAICfg := uuid.New()
|
|
unknownCfg := uuid.New()
|
|
|
|
vllmProviderID := uuid.New()
|
|
togetherProviderID := uuid.New()
|
|
vllmCfg := uuid.New()
|
|
|
|
peCall := func(id string) codersdk.ChatMessagePart {
|
|
p := codersdk.ChatMessageToolCall(id, "web_search", json.RawMessage(`{"query":"x"}`))
|
|
p.ProviderExecuted = true
|
|
return p
|
|
}
|
|
peResult := func(id string) codersdk.ChatMessagePart {
|
|
p := codersdk.ChatMessageToolResult(id, "web_search", json.RawMessage(`{"ok":true}`), false, false)
|
|
p.ProviderExecuted = true
|
|
return p
|
|
}
|
|
localCall := func(id string) codersdk.ChatMessagePart {
|
|
return codersdk.ChatMessageToolCall(id, "read_file", json.RawMessage(`{}`))
|
|
}
|
|
text := codersdk.ChatMessageText
|
|
|
|
assistantRow := func(t *testing.T, cfg uuid.UUID, parts ...codersdk.ChatMessagePart) database.ChatMessage {
|
|
t.Helper()
|
|
content, err := chatprompt.MarshalParts(parts)
|
|
require.NoError(t, err)
|
|
return database.ChatMessage{
|
|
Role: database.ChatMessageRoleAssistant,
|
|
ModelConfigID: uuid.NullUUID{UUID: cfg, Valid: cfg != uuid.Nil},
|
|
Content: content,
|
|
ContentVersion: chatprompt.ContentVersionV1,
|
|
}
|
|
}
|
|
userRow := func(t *testing.T, s string) database.ChatMessage {
|
|
t.Helper()
|
|
content, err := chatprompt.MarshalParts([]codersdk.ChatMessagePart{text(s)})
|
|
require.NoError(t, err)
|
|
return database.ChatMessage{
|
|
Role: database.ChatMessageRoleUser,
|
|
Content: content,
|
|
ContentVersion: chatprompt.ContentVersionV1,
|
|
}
|
|
}
|
|
|
|
origin := func(providerByConfig map[uuid.UUID]string) func(uuid.NullUUID) (string, bool) {
|
|
return func(id uuid.NullUUID) (string, bool) {
|
|
if !id.Valid {
|
|
return "", false
|
|
}
|
|
provider, ok := providerByConfig[id.UUID]
|
|
return provider, ok
|
|
}
|
|
}
|
|
resolver := origin(map[uuid.UUID]string{
|
|
anthropicCfg: anthropic,
|
|
openAICfg: openai,
|
|
vllmCfg: vllmProviderID.String(),
|
|
})
|
|
|
|
partsOf := func(t *testing.T, row database.ChatMessage) []codersdk.ChatMessagePart {
|
|
t.Helper()
|
|
parts, err := chatprompt.ParseContent(row)
|
|
require.NoError(t, err)
|
|
return parts
|
|
}
|
|
|
|
t.Run("same provider kept", func(t *testing.T) {
|
|
t.Parallel()
|
|
rows := []database.ChatMessage{
|
|
userRow(t, "hi"),
|
|
assistantRow(t, anthropicCfg, peCall("ws"), peResult("ws"), text("done")),
|
|
}
|
|
got, stats := stripForeignProviderExecutedToolRows(rows, anthropic, resolver)
|
|
require.Equal(t, rows, got)
|
|
require.Zero(t, stats)
|
|
})
|
|
|
|
t.Run("anthropic to bedrock drops provider blocks", func(t *testing.T) {
|
|
t.Parallel()
|
|
rows := []database.ChatMessage{
|
|
userRow(t, "hi"),
|
|
assistantRow(t, anthropicCfg, peCall("ws"), peResult("ws"), text("done")),
|
|
}
|
|
got, stats := stripForeignProviderExecutedToolRows(rows, bedrock, resolver)
|
|
require.Len(t, got, 2)
|
|
require.Equal(t, []codersdk.ChatMessagePart{text("done")}, partsOf(t, got[1]))
|
|
require.Equal(t, providerSwitchStripStats{RemovedToolCalls: 1, RemovedToolResults: 1}, stats)
|
|
})
|
|
|
|
t.Run("foreign-only row dropped", func(t *testing.T) {
|
|
t.Parallel()
|
|
rows := []database.ChatMessage{
|
|
userRow(t, "hi"),
|
|
assistantRow(t, anthropicCfg, peCall("ws")),
|
|
userRow(t, "again"),
|
|
}
|
|
got, stats := stripForeignProviderExecutedToolRows(rows, bedrock, resolver)
|
|
require.Len(t, got, 2)
|
|
require.Equal(t, database.ChatMessageRoleUser, got[0].Role)
|
|
require.Equal(t, database.ChatMessageRoleUser, got[1].Role)
|
|
require.Equal(t, providerSwitchStripStats{RemovedToolCalls: 1, DroppedMessages: 1}, stats)
|
|
})
|
|
|
|
t.Run("multi-provider keeps native strips foreign", func(t *testing.T) {
|
|
t.Parallel()
|
|
rows := []database.ChatMessage{
|
|
assistantRow(t, openAICfg, peCall("os"), peResult("os"), text("openai")),
|
|
assistantRow(t, anthropicCfg, peCall("as"), peResult("as"), text("anthropic")),
|
|
}
|
|
got, stats := stripForeignProviderExecutedToolRows(rows, anthropic, resolver)
|
|
require.Len(t, got, 2)
|
|
require.Equal(t, []codersdk.ChatMessagePart{text("openai")}, partsOf(t, got[0]))
|
|
require.Equal(t, rows[1], got[1])
|
|
require.Equal(t, providerSwitchStripStats{RemovedToolCalls: 1, RemovedToolResults: 1}, stats)
|
|
})
|
|
|
|
t.Run("non-provider-executed parts untouched", func(t *testing.T) {
|
|
t.Parallel()
|
|
rows := []database.ChatMessage{
|
|
assistantRow(t, anthropicCfg, text("hello"), localCall("local")),
|
|
}
|
|
got, stats := stripForeignProviderExecutedToolRows(rows, bedrock, resolver)
|
|
require.Equal(t, rows, got)
|
|
require.Zero(t, stats)
|
|
})
|
|
|
|
t.Run("empty target is a no-op", func(t *testing.T) {
|
|
t.Parallel()
|
|
rows := []database.ChatMessage{
|
|
assistantRow(t, anthropicCfg, peCall("ws"), peResult("ws")),
|
|
}
|
|
got, stats := stripForeignProviderExecutedToolRows(rows, "", resolver)
|
|
require.Equal(t, rows, got)
|
|
require.Zero(t, stats)
|
|
})
|
|
|
|
t.Run("unknown origin fails closed", func(t *testing.T) {
|
|
t.Parallel()
|
|
rows := []database.ChatMessage{
|
|
assistantRow(t, unknownCfg, peResult("ws"), text("done")),
|
|
}
|
|
got, stats := stripForeignProviderExecutedToolRows(rows, bedrock, resolver)
|
|
require.Len(t, got, 1)
|
|
require.Equal(t, []codersdk.ChatMessagePart{text("done")}, partsOf(t, got[0]))
|
|
require.Equal(t, providerSwitchStripStats{RemovedToolResults: 1}, stats)
|
|
})
|
|
|
|
t.Run("unparsable foreign row kept unchanged", func(t *testing.T) {
|
|
t.Parallel()
|
|
rows := []database.ChatMessage{{
|
|
Role: database.ChatMessageRoleAssistant,
|
|
ModelConfigID: uuid.NullUUID{UUID: anthropicCfg, Valid: true},
|
|
Content: pqtype.NullRawMessage{RawMessage: []byte("{not json"), Valid: true},
|
|
ContentVersion: chatprompt.ContentVersionV1,
|
|
}}
|
|
got, stats := stripForeignProviderExecutedToolRows(rows, bedrock, resolver)
|
|
require.Equal(t, rows, got)
|
|
require.Zero(t, stats)
|
|
})
|
|
|
|
t.Run("same type different instance drops provider blocks", func(t *testing.T) {
|
|
t.Parallel()
|
|
rows := []database.ChatMessage{
|
|
userRow(t, "hi"),
|
|
assistantRow(t, vllmCfg, peCall("ws"), peResult("ws"), text("done")),
|
|
}
|
|
got, stats := stripForeignProviderExecutedToolRows(rows, togetherProviderID.String(), resolver)
|
|
require.Len(t, got, 2)
|
|
require.Equal(t, []codersdk.ChatMessagePart{text("done")}, partsOf(t, got[1]))
|
|
require.Equal(t, providerSwitchStripStats{RemovedToolCalls: 1, RemovedToolResults: 1}, stats)
|
|
})
|
|
|
|
t.Run("same instance keeps provider blocks", func(t *testing.T) {
|
|
t.Parallel()
|
|
rows := []database.ChatMessage{
|
|
userRow(t, "hi"),
|
|
assistantRow(t, vllmCfg, peCall("ws"), peResult("ws"), text("done")),
|
|
}
|
|
got, stats := stripForeignProviderExecutedToolRows(rows, vllmProviderID.String(), resolver)
|
|
require.Equal(t, rows, got)
|
|
require.Zero(t, stats)
|
|
})
|
|
}
|
|
|
|
func TestModelConfigProviderIdentity(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
providerID := uuid.New()
|
|
|
|
t.Run("AIProviderID valid returns provider UUID", func(t *testing.T) {
|
|
t.Parallel()
|
|
cfg := database.ChatModelConfig{
|
|
AIProviderID: uuid.NullUUID{UUID: providerID, Valid: true},
|
|
}
|
|
got := modelConfigProviderIdentity(cfg, "openai-compat")
|
|
require.Equal(t, providerID.String(), got)
|
|
})
|
|
|
|
t.Run("AIProviderID invalid falls back to normalized type", func(t *testing.T) {
|
|
t.Parallel()
|
|
cfg := database.ChatModelConfig{
|
|
AIProviderID: uuid.NullUUID{},
|
|
}
|
|
got := modelConfigProviderIdentity(cfg, "anthropic")
|
|
require.Equal(t, "anthropic", got)
|
|
})
|
|
|
|
t.Run("same type different provider IDs are distinguished", func(t *testing.T) {
|
|
t.Parallel()
|
|
otherProviderID := uuid.New()
|
|
cfgA := database.ChatModelConfig{
|
|
AIProviderID: uuid.NullUUID{UUID: providerID, Valid: true},
|
|
}
|
|
cfgB := database.ChatModelConfig{
|
|
AIProviderID: uuid.NullUUID{UUID: otherProviderID, Valid: true},
|
|
}
|
|
require.NotEqual(t,
|
|
modelConfigProviderIdentity(cfgA, "openai-compat"),
|
|
modelConfigProviderIdentity(cfgB, "openai-compat"),
|
|
)
|
|
})
|
|
}
|