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
2 changes: 1 addition & 1 deletion e2e/testdata/cassettes/TestDebug_Title/Anthropic.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interactions:
proto_minor: 1
content_length: 0
host: api.anthropic.com
body: '{"max_tokens":20,"messages":[{"content":[{"text":"Based on the following recent user messages from a conversation with an AI assistant, generate a short, descriptive title (maximum 50 characters) that captures the main topic or purpose of the conversation. Return ONLY the title text on a single line, nothing else. Do not include any newlines, explanations, or formatting.\n\nRecent user messages:\n1. What can you do?","cache_control":{"type":"ephemeral"},"type":"text"}],"role":"user"}],"model":"claude-haiku-4-5","system":[{"text":"You are a helpful AI assistant that generates concise, descriptive titles for conversations. You will be given up to 2 recent user messages and asked to create a single-line title that captures the main topic. Never use newlines or line breaks in your response.","type":"text"}],"tools":[],"stream":true}'
body: '{"max_tokens":20,"messages":[{"content":[{"text":"Based on the following recent user messages from a conversation with an AI assistant, generate a short, descriptive title (maximum 50 characters) that captures the main topic or purpose of the conversation. Return ONLY the title text on a single line, nothing else. Do not include any newlines, explanations, or formatting.\n\nRecent user messages:\n1. What can you do?\n\n\n","cache_control":{"type":"ephemeral"},"type":"text"}],"role":"user"}],"model":"claude-haiku-4-5","system":[{"text":"You are a helpful AI assistant that generates concise, descriptive titles for conversations. You will be given up to 2 recent user messages and asked to create a single-line title that captures the main topic. Never use newlines or line breaks in your response.","type":"text"}],"tools":[],"stream":true}'
url: https://api.anthropic.com/v1/messages
method: POST
response:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interactions:
proto_minor: 1
content_length: 0
host: api.anthropic.com
body: '{"max_tokens":20,"messages":[{"content":[{"text":"Based on the following recent user messages from a conversation with an AI assistant, generate a short, descriptive title (maximum 50 characters) that captures the main topic or purpose of the conversation. Return ONLY the title text on a single line, nothing else. Do not include any newlines, explanations, or formatting.\n\nRecent user messages:\n1. What can you do?","cache_control":{"type":"ephemeral"},"type":"text"}],"role":"user"}],"model":"claude-opus-4-6","system":[{"text":"You are a helpful AI assistant that generates concise, descriptive titles for conversations. You will be given up to 2 recent user messages and asked to create a single-line title that captures the main topic. Never use newlines or line breaks in your response.","type":"text"}],"tools":[],"stream":true}'
body: '{"max_tokens":20,"messages":[{"content":[{"text":"Based on the following recent user messages from a conversation with an AI assistant, generate a short, descriptive title (maximum 50 characters) that captures the main topic or purpose of the conversation. Return ONLY the title text on a single line, nothing else. Do not include any newlines, explanations, or formatting.\n\nRecent user messages:\n1. What can you do?\n\n\n","cache_control":{"type":"ephemeral"},"type":"text"}],"role":"user"}],"model":"claude-opus-4-6","system":[{"text":"You are a helpful AI assistant that generates concise, descriptive titles for conversations. You will be given up to 2 recent user messages and asked to create a single-line title that captures the main topic. Never use newlines or line breaks in your response.","type":"text"}],"tools":[],"stream":true}'
url: https://api.anthropic.com/v1/messages
method: POST
response:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interactions:
proto_minor: 1
content_length: 0
host: api.anthropic.com
body: '{"max_tokens":20,"messages":[{"content":[{"text":"Based on the following recent user messages from a conversation with an AI assistant, generate a short, descriptive title (maximum 50 characters) that captures the main topic or purpose of the conversation. Return ONLY the title text on a single line, nothing else. Do not include any newlines, explanations, or formatting.\n\nRecent user messages:\n1. What can you do?","cache_control":{"type":"ephemeral"},"type":"text"}],"role":"user"}],"model":"claude-sonnet-4-5","system":[{"text":"You are a helpful AI assistant that generates concise, descriptive titles for conversations. You will be given up to 2 recent user messages and asked to create a single-line title that captures the main topic. Never use newlines or line breaks in your response.","type":"text"}],"tools":[],"stream":true}'
body: '{"max_tokens":20,"messages":[{"content":[{"text":"Based on the following recent user messages from a conversation with an AI assistant, generate a short, descriptive title (maximum 50 characters) that captures the main topic or purpose of the conversation. Return ONLY the title text on a single line, nothing else. Do not include any newlines, explanations, or formatting.\n\nRecent user messages:\n1. What can you do?\n\n\n","cache_control":{"type":"ephemeral"},"type":"text"}],"role":"user"}],"model":"claude-sonnet-4-5","system":[{"text":"You are a helpful AI assistant that generates concise, descriptive titles for conversations. You will be given up to 2 recent user messages and asked to create a single-line title that captures the main topic. Never use newlines or line breaks in your response.","type":"text"}],"tools":[],"stream":true}'
url: https://api.anthropic.com/v1/messages
method: POST
response:
Expand Down
2 changes: 1 addition & 1 deletion e2e/testdata/cassettes/TestExec_Anthropic_ToolCall.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ interactions:
proto_minor: 1
content_length: 0
host: api.anthropic.com
body: '{"max_tokens":64000,"messages":[{"content":[{"text":"How many files in testdata/working_dir? Only output the number.","type":"text"}],"role":"user"},{"content":[{"id":"toolu_012gmfqnoTX8c5aV3vMWUnas","input":{"path":"testdata/working_dir"},"name":"list_directory","cache_control":{"type":"ephemeral"},"type":"tool_use"}],"role":"assistant"},{"content":[{"tool_use_id":"toolu_012gmfqnoTX8c5aV3vMWUnas","is_error":false,"cache_control":{"type":"ephemeral"},"content":[{"text":"FILE README.me","type":"text"}],"type":"tool_result"}],"role":"user"}],"model":"claude-sonnet-4-0","system":[{"text":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.","type":"text"},{"text":"## Filesystem Tools\n\n- Relative paths resolve from the working directory; absolute paths and \"..\" work as expected\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","cache_control":{"type":"ephemeral"},"type":"text"}],"tools":[{"input_schema":{"properties":{"path":{"description":"The directory path to traverse (relative to working directory)","type":"string"}},"required":["path"],"type":"object"},"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure."},{"input_schema":{"properties":{"edits":{"description":"Array of edit operations","items":{"additionalProperties":false,"properties":{"newText":{"description":"The replacement text","type":"string"},"oldText":{"description":"The exact text to replace","type":"string"}},"required":["oldText","newText"],"type":"object"},"type":["null","array"]},"path":{"description":"The file path to edit","type":"string"}},"required":["path","edits"],"type":"object"},"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content."},{"input_schema":{"properties":{"path":{"description":"The directory path to list","type":"string"}},"required":["path"],"type":"object"},"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path."},{"input_schema":{"properties":{"path":{"description":"The file path to read","type":"string"}},"required":["path"],"type":"object"},"name":"read_file","description":"Read the complete contents of a file from the file system. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly."},{"input_schema":{"properties":{"json":{"description":"Whether to return the result as JSON","type":"boolean"},"paths":{"description":"Array of file paths to read","items":{"type":"string"},"type":["null","array"]}},"required":["paths"],"type":"object"},"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously."},{"input_schema":{"properties":{"excludePatterns":{"description":"Patterns to exclude from search","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"If true, treat query as regex; otherwise literal text","type":"boolean"},"path":{"description":"The starting directory path","type":"string"},"query":{"description":"The text or regex pattern to search for","type":"string"}},"required":["path","query"],"type":"object"},"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern."},{"input_schema":{"properties":{"content":{"description":"The content to write to the file","type":"string"},"path":{"description":"The file path to write","type":"string"}},"required":["path","content"],"type":"object"},"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content."},{"input_schema":{"properties":{"paths":{"description":"Array of directory paths to create","items":{"type":"string"},"type":["null","array"]}},"required":["paths"],"type":"object"},"name":"create_directory","description":"Create one or more new directories or nested directory structures."},{"input_schema":{"properties":{"paths":{"description":"Array of directory paths to remove","items":{"type":"string"},"type":["null","array"]}},"required":["paths"],"type":"object"},"name":"remove_directory","description":"Remove one or more empty directories."}],"stream":true}'
body: '{"max_tokens":64000,"messages":[{"content":[{"text":"How many files in testdata/working_dir? Only output the number.","type":"text"}],"role":"user"},{"content":[{"id":"toolu_012gmfqnoTX8c5aV3vMWUnas","input":{"path":"testdata/working_dir"},"name":"list_directory","cache_control":{"type":"ephemeral"},"type":"tool_use"}],"role":"assistant"},{"content":[{"tool_use_id":"toolu_012gmfqnoTX8c5aV3vMWUnas","is_error":false,"cache_control":{"type":"ephemeral"},"content":[{"text":"FILE README.me\n","type":"text"}],"type":"tool_result"}],"role":"user"}],"model":"claude-sonnet-4-0","system":[{"text":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.","type":"text"},{"text":"## Filesystem Tools\n\n- Relative paths resolve from the working directory; absolute paths and \"..\" work as expected\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","cache_control":{"type":"ephemeral"},"type":"text"}],"tools":[{"input_schema":{"properties":{"path":{"description":"The directory path to traverse (relative to working directory)","type":"string"}},"required":["path"],"type":"object"},"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure."},{"input_schema":{"properties":{"edits":{"description":"Array of edit operations","items":{"additionalProperties":false,"properties":{"newText":{"description":"The replacement text","type":"string"},"oldText":{"description":"The exact text to replace","type":"string"}},"required":["oldText","newText"],"type":"object"},"type":["null","array"]},"path":{"description":"The file path to edit","type":"string"}},"required":["path","edits"],"type":"object"},"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content."},{"input_schema":{"properties":{"path":{"description":"The directory path to list","type":"string"}},"required":["path"],"type":"object"},"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path."},{"input_schema":{"properties":{"path":{"description":"The file path to read","type":"string"}},"required":["path"],"type":"object"},"name":"read_file","description":"Read the complete contents of a file from the file system. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly."},{"input_schema":{"properties":{"json":{"description":"Whether to return the result as JSON","type":"boolean"},"paths":{"description":"Array of file paths to read","items":{"type":"string"},"type":["null","array"]}},"required":["paths"],"type":"object"},"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously."},{"input_schema":{"properties":{"excludePatterns":{"description":"Patterns to exclude from search","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"If true, treat query as regex; otherwise literal text","type":"boolean"},"path":{"description":"The starting directory path","type":"string"},"query":{"description":"The text or regex pattern to search for","type":"string"}},"required":["path","query"],"type":"object"},"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern."},{"input_schema":{"properties":{"content":{"description":"The content to write to the file","type":"string"},"path":{"description":"The file path to write","type":"string"}},"required":["path","content"],"type":"object"},"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content."},{"input_schema":{"properties":{"paths":{"description":"Array of directory paths to create","items":{"type":"string"},"type":["null","array"]}},"required":["paths"],"type":"object"},"name":"create_directory","description":"Create one or more new directories or nested directory structures."},{"input_schema":{"properties":{"paths":{"description":"Array of directory paths to remove","items":{"type":"string"},"type":["null","array"]}},"required":["paths"],"type":"object"},"name":"remove_directory","description":"Remove one or more empty directories."}],"stream":true}'
url: https://api.anthropic.com/v1/messages
method: POST
response:
Expand Down
3 changes: 2 additions & 1 deletion pkg/model/provider/anthropic/beta_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,8 @@ func TestExtractBetaSystemBlocks_MultipleSystemMessages(t *testing.T) {
assert.Equal(t, "Be concise", blocks[1].Text)
}

// TestExtractBetaSystemBlocks_SkipsEmptyText tests that empty system text is skipped
// TestExtractBetaSystemBlocks_SkipsEmptyText tests that empty system text is skipped.
// System blocks are trimmed because YAML literal-block instructions always append a trailing newline.
func TestExtractBetaSystemBlocks_SkipsEmptyText(t *testing.T) {
msgs := []chat.Message{
{
Expand Down
32 changes: 17 additions & 15 deletions pkg/model/provider/anthropic/beta_converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ func (c *Client) convertBetaMessages(ctx context.Context, messages []chat.Messag
Content: contentBlocks,
})
}
} else if txt := strings.TrimSpace(msg.Content); txt != "" {
} else {
betaMessages = append(betaMessages, anthropic.BetaMessageParam{
Role: anthropic.BetaMessageParamRoleUser,
Content: []anthropic.BetaContentBlockParamUnion{
{OfText: &anthropic.BetaTextBlockParam{Text: txt}},
{OfText: &anthropic.BetaTextBlockParam{Text: msg.Content}},
},
})
}
Expand All @@ -68,9 +68,9 @@ func (c *Client) convertBetaMessages(ctx context.Context, messages []chat.Messag
}

// Add text content if present
if txt := strings.TrimSpace(msg.Content); txt != "" {
if msg.Content != "" {
contentBlocks = append(contentBlocks, anthropic.BetaContentBlockParamUnion{
OfText: &anthropic.BetaTextBlockParam{Text: txt},
OfText: &anthropic.BetaTextBlockParam{Text: msg.Content},
})
}

Expand Down Expand Up @@ -138,11 +138,17 @@ func (c *Client) convertBetaMessages(ctx context.Context, messages []chat.Messag
// including any image content from MultiContent.
func convertBetaToolResultBlock(msg *chat.Message) anthropic.BetaContentBlockParamUnion {
if !hasImageMultiContent(msg.MultiContent) {
// tool_result must be present for every preceding tool_use; we cannot skip
// it. Normalize whitespace-only content to empty string rather than skipping.
content := msg.Content
if strings.TrimSpace(content) == "" {
content = ""
}
return anthropic.BetaContentBlockParamUnion{
OfToolResult: &anthropic.BetaToolResultBlockParam{
ToolUseID: msg.ToolCallID,
Content: []anthropic.BetaToolResultBlockParamContentUnion{
{OfText: &anthropic.BetaTextBlockParam{Text: strings.TrimSpace(msg.Content)}},
{OfText: &anthropic.BetaTextBlockParam{Text: content}},
},
},
}
Expand All @@ -152,11 +158,9 @@ func convertBetaToolResultBlock(msg *chat.Message) anthropic.BetaContentBlockPar
for _, part := range msg.MultiContent {
switch part.Type {
case chat.MessagePartTypeText:
if txt := strings.TrimSpace(part.Text); txt != "" {
content = append(content, anthropic.BetaToolResultBlockParamContentUnion{
OfText: &anthropic.BetaTextBlockParam{Text: txt},
})
}
content = append(content, anthropic.BetaToolResultBlockParamContentUnion{
OfText: &anthropic.BetaTextBlockParam{Text: part.Text},
})
case chat.MessagePartTypeImageURL:
if part.ImageURL == nil {
continue
Expand Down Expand Up @@ -194,11 +198,9 @@ func (c *Client) convertBetaUserMultiContent(ctx context.Context, parts []chat.M
for _, part := range parts {
switch part.Type {
case chat.MessagePartTypeText:
if txt := strings.TrimSpace(part.Text); txt != "" {
contentBlocks = append(contentBlocks, anthropic.BetaContentBlockParamUnion{
OfText: &anthropic.BetaTextBlockParam{Text: txt},
})
}
contentBlocks = append(contentBlocks, anthropic.BetaContentBlockParamUnion{
OfText: &anthropic.BetaTextBlockParam{Text: part.Text},
})

case chat.MessagePartTypeImageURL:
if part.ImageURL == nil {
Expand Down
Loading
Loading