Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions coderd/x/chatd/chatloop/chatloop.go
Original file line number Diff line number Diff line change
Expand Up @@ -439,10 +439,12 @@ func GenerateAssistant(ctx context.Context, opts GenerateAssistantOptions) (Assi
ctx, opts.Logger, provider, modelName,
"assistant_helper", 0, result.finishReason, result.content,
)
// A content-filter finish with no content means the provider's
// safety classifiers blocked the whole response (e.g. Anthropic
// stop_reason "refusal").
if len(result.content) == 0 && result.finishReason == fantasy.FinishReasonContentFilter {
// A content-filter finish without user-visible output means the
// provider's safety classifiers blocked the whole response (e.g.
// Anthropic stop_reason "refusal"). The refusal can arrive after
// reasoning has already streamed, so reasoning alone must not
// count as output.
if result.finishReason == fantasy.FinishReasonContentFilter && !hasUserVisibleContent(result.content) {
return AssistantOutcome{}, contentFilterError(errorProvider, result.providerMetadata)
}
step := PersistedStep{
Expand Down Expand Up @@ -479,6 +481,20 @@ func wrapProviderStreamError(provider string, err error) error {
return xerrors.Errorf("stream response: %w", chaterror.WithClassification(err, classified))
}

// hasUserVisibleContent reports whether any content part carries output the
// user can see. Reasoning parts do not count: they stream transiently and are
// not a substitute for a response.
func hasUserVisibleContent(content []fantasy.Content) bool {
for _, part := range content {
switch part.(type) {
case fantasy.ReasoningContent, *fantasy.ReasoningContent:
default:
return true
}
}
return false
}

func contentFilterError(provider string, metadata fantasy.ProviderMetadata) error {
classified := chaterror.ClassifiedError{
Kind: codersdk.ChatErrorKindContentFilter,
Expand Down
39 changes: 39 additions & 0 deletions coderd/x/chatd/chatloop/contentfilter_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,45 @@ func TestGenerateAssistant_ContentFilterRefusal(t *testing.T) {
require.Equal(t, "The response was blocked.", classified.Detail)
})

t.Run("ReasoningOnlyContentSurfacesTerminalError", func(t *testing.T) {
t.Parallel()

model := &chattest.FakeModel{
ProviderName: "anthropic",
ModelName: "test-model",
StreamFn: func(_ context.Context, _ fantasy.Call) (fantasy.StreamResponse, error) {
return streamFromParts([]fantasy.StreamPart{
{Type: fantasy.StreamPartTypeReasoningStart, ID: "reasoning-1"},
{Type: fantasy.StreamPartTypeReasoningDelta, ID: "reasoning-1", Delta: "planning next steps"},
{Type: fantasy.StreamPartTypeReasoningEnd, ID: "reasoning-1"},
{
Type: fantasy.StreamPartTypeFinish,
FinishReason: fantasy.FinishReasonContentFilter,
ProviderMetadata: refusalProviderMetadataForTest(
"harmful_content", "The response was blocked.",
),
},
}), nil
},
}

outcome, err := GenerateAssistant(context.Background(), GenerateAssistantOptions{
Model: model,
Messages: []fantasy.Message{
textMessage(fantasy.MessageRoleUser, "hello"),
},
})
require.ErrorIs(t, err, ErrContentFiltered)
require.Empty(t, outcome.Step.Content)

classified := chaterror.Classify(err)
require.Equal(t, codersdk.ChatErrorKindContentFilter, classified.Kind)
require.Equal(t, "anthropic", classified.Provider)
require.False(t, classified.Retryable)
require.Equal(t, "Anthropic blocked this response under its content policy (harmful_content).", classified.Message)
require.Equal(t, "The response was blocked.", classified.Detail)
})

t.Run("PartialContentIsPersistedNotErrored", func(t *testing.T) {
t.Parallel()

Expand Down
Loading