diff --git a/core/internal/llmtests/account.go b/core/internal/llmtests/account.go index ac850830fd..0632f16b6b 100644 --- a/core/internal/llmtests/account.go +++ b/core/internal/llmtests/account.go @@ -88,7 +88,9 @@ type TestScenarios struct { Realtime bool // Realtime API (bidirectional audio/text) Compaction bool // Server-side compaction (context management) InterleavedThinking bool // Interleaved thinking between tool calls (beta) - FastMode bool // Fast mode for Opus 4.6 (beta: research preview) + FastMode bool // Fast mode for Opus 4.6 (beta: research preview) + EagerInputStreaming bool // Fine-grained tool input streaming (Anthropic fine-grained-tool-streaming-2025-05-14) + ServerToolsViaOpenAIEndpoint bool // Anthropic server-tool shapes in tools[] via /v1/chat/completions (web_search / web_fetch / code_execution) } // ComprehensiveTestConfig extends TestConfig with additional scenarios diff --git a/core/internal/llmtests/eager_input_streaming.go b/core/internal/llmtests/eager_input_streaming.go new file mode 100644 index 0000000000..0f074c46af --- /dev/null +++ b/core/internal/llmtests/eager_input_streaming.go @@ -0,0 +1,134 @@ +package llmtests + +import ( + "context" + "os" + "testing" + + bifrost "github.com/maximhq/bifrost/core" + "github.com/maximhq/bifrost/core/schemas" +) + +// RunEagerInputStreamingTest tests that setting eager_input_streaming: true on +// a custom tool succeeds end-to-end against the target Anthropic-family +// provider. Per Table 20 (verified against A overview + B-header), the +// fine-grained-tool-streaming-2025-05-14 beta is supported on Anthropic, +// Bedrock, Vertex, and Azure. +// +// The test verifies: +// 1. The request is accepted (no upstream 400 — which would indicate the +// fine-grained-tool-streaming-2025-05-14 beta header wasn't injected or +// is rejected by the target provider). +// 2. The stream produces a tool call with a valid JSON arguments payload. +// 3. The response is otherwise well-formed. +// +// This intentionally runs across all four providers (no single-provider gate +// unlike RunFastModeTest, which is Opus-4.6-only). +func RunEagerInputStreamingTest(t *testing.T, client *bifrost.Bifrost, ctx context.Context, testConfig ComprehensiveTestConfig) { + if !testConfig.Scenarios.EagerInputStreaming { + t.Logf("EagerInputStreaming not supported for provider %s", testConfig.Provider) + return + } + + t.Run("EagerInputStreaming", func(t *testing.T) { + if os.Getenv("SKIP_PARALLEL_TESTS") != "true" { + t.Parallel() + } + + chatTool := GetSampleChatTool(SampleToolTypeWeather) + // Opt the tool into fine-grained input streaming. The neutral flag + // on ChatTool is promoted through ToAnthropicChatRequest, which also + // triggers the fine-grained-tool-streaming-2025-05-14 beta header. + eager := true + chatTool.EagerInputStreaming = &eager + + chatMessages := []schemas.ChatMessage{ + CreateBasicChatMessage("What's the weather like in San Francisco? answer in celsius"), + } + + request := &schemas.BifrostChatRequest{ + Provider: testConfig.Provider, + Model: testConfig.ChatModel, + Input: chatMessages, + Params: &schemas.ChatParameters{ + MaxCompletionTokens: bifrost.Ptr(200), + Tools: []schemas.ChatTool{*chatTool}, + }, + Fallbacks: testConfig.Fallbacks, + } + + retryConfig := StreamingRetryConfig() + retryContext := TestRetryContext{ + ScenarioName: "EagerInputStreaming", + ExpectedBehavior: map[string]interface{}{ + "should_stream_content": true, + "should_have_tool_calls": true, + "tool_name": "get_weather", + }, + TestMetadata: map[string]interface{}{ + "provider": testConfig.Provider, + "model": testConfig.ChatModel, + "eager_input_streaming": true, + }, + } + + responseChannel, err := WithStreamRetry(t, retryConfig, retryContext, func() (chan *schemas.BifrostStreamChunk, *schemas.BifrostError) { + bfCtx := schemas.NewBifrostContext(ctx, schemas.NoDeadline) + return client.ChatCompletionStreamRequest(bfCtx, request) + }) + + RequireNoError(t, err, "Eager input streaming request failed") + if responseChannel == nil { + t.Fatal("Response channel should not be nil") + } + + accumulator := NewStreamingToolCallAccumulator() + var responseCount int + var sawAny bool + + t.Logf("🔧 Testing eager input streaming (fine-grained-tool-streaming-2025-05-14)...") + + for response := range responseChannel { + if response == nil || response.BifrostChatResponse == nil { + continue + } + responseCount++ + sawAny = true + + if response.BifrostChatResponse.Choices != nil { + for i, choice := range response.BifrostChatResponse.Choices { + if choice.ChatStreamResponseChoice != nil && choice.ChatStreamResponseChoice.Delta != nil { + delta := choice.ChatStreamResponseChoice.Delta + for _, tc := range delta.ToolCalls { + accumulator.AccumulateChatToolCall(i, tc) + } + } + } + } + } + + if !sawAny { + t.Fatal("Expected at least one streaming response chunk") + } + t.Logf("Received %d chunks", responseCount) + + // Validate the accumulated tool call is well-formed. If the + // fine-grained-tool-streaming beta header weren't sent (or the + // provider rejected it), the upstream would have returned a 400 + // before any tool_use blocks were emitted. + toolCalls := accumulator.GetFinalChatToolCalls() + if len(toolCalls) == 0 { + t.Error("Expected at least one tool call in stream") + } + for _, tc := range toolCalls { + if tc.Name == "" { + t.Error("Tool call missing function name") + } + if tc.Arguments == "" { + t.Error("Tool call missing arguments JSON") + } + } + + t.Logf("EagerInputStreaming passed: %d tool calls accumulated", len(toolCalls)) + }) +} diff --git a/core/internal/llmtests/provider_feature_support_test.go b/core/internal/llmtests/provider_feature_support_test.go index 6e4738c282..539f4049c0 100644 --- a/core/internal/llmtests/provider_feature_support_test.go +++ b/core/internal/llmtests/provider_feature_support_test.go @@ -654,6 +654,77 @@ func TestProviderBetaHeaderInjection(t *testing.T) { }, expectHeaders: []string{"computer-use-2025-01-24"}, }, + + // ── Fine-grained tool streaming header (eager_input_streaming) ── + // Per cited citations (A overview table + B-header): EagerInputStreaming + // is supported on Anthropic, Bedrock, Vertex, and Azure — all four + // should auto-inject fine-grained-tool-streaming-2025-05-14 when a + // tool has eager_input_streaming: true. + { + name: "Anthropic/eager_input_streaming_header_added", + provider: schemas.Anthropic, + setupReq: func() *anthropic.AnthropicMessageRequest { + eager := true + return &anthropic.AnthropicMessageRequest{ + Tools: []anthropic.AnthropicTool{{Name: "t1", EagerInputStreaming: &eager}}, + } + }, + expectHeaders: []string{"fine-grained-tool-streaming-2025-05-14"}, + }, + { + name: "Bedrock/eager_input_streaming_header_added", + provider: schemas.Bedrock, + setupReq: func() *anthropic.AnthropicMessageRequest { + eager := true + return &anthropic.AnthropicMessageRequest{ + Tools: []anthropic.AnthropicTool{{Name: "t1", EagerInputStreaming: &eager}}, + } + }, + expectHeaders: []string{"fine-grained-tool-streaming-2025-05-14"}, + }, + { + name: "Vertex/eager_input_streaming_header_added", + provider: schemas.Vertex, + setupReq: func() *anthropic.AnthropicMessageRequest { + eager := true + return &anthropic.AnthropicMessageRequest{ + Tools: []anthropic.AnthropicTool{{Name: "t1", EagerInputStreaming: &eager}}, + } + }, + expectHeaders: []string{"fine-grained-tool-streaming-2025-05-14"}, + }, + { + name: "Azure/eager_input_streaming_header_added", + provider: schemas.Azure, + setupReq: func() *anthropic.AnthropicMessageRequest { + eager := true + return &anthropic.AnthropicMessageRequest{ + Tools: []anthropic.AnthropicTool{{Name: "t1", EagerInputStreaming: &eager}}, + } + }, + expectHeaders: []string{"fine-grained-tool-streaming-2025-05-14"}, + }, + { + name: "eager_input_streaming_header_skipped_when_flag_false", + provider: schemas.Anthropic, + setupReq: func() *anthropic.AnthropicMessageRequest { + eager := false + return &anthropic.AnthropicMessageRequest{ + Tools: []anthropic.AnthropicTool{{Name: "t1", EagerInputStreaming: &eager}}, + } + }, + unexpectHeaders: []string{"fine-grained-tool-streaming-2025-05-14"}, + }, + { + name: "eager_input_streaming_header_skipped_when_unset", + provider: schemas.Anthropic, + setupReq: func() *anthropic.AnthropicMessageRequest { + return &anthropic.AnthropicMessageRequest{ + Tools: []anthropic.AnthropicTool{{Name: "t1"}}, + } + }, + unexpectHeaders: []string{"fine-grained-tool-streaming-2025-05-14"}, + }, } for _, tt := range tests { diff --git a/core/internal/llmtests/tests.go b/core/internal/llmtests/tests.go index af3006b9a1..c8050cad6d 100644 --- a/core/internal/llmtests/tests.go +++ b/core/internal/llmtests/tests.go @@ -120,6 +120,7 @@ func RunAllComprehensiveTests(t *testing.T, client *bifrost.Bifrost, ctx context RunCompactionTest, RunInterleavedThinkingTest, RunFastModeTest, + RunEagerInputStreamingTest, } // Execute all test scenarios without raw request/response (default behavior) @@ -239,6 +240,7 @@ func printTestSummary(t *testing.T, testConfig ComprehensiveTestConfig) { {"Compaction", testConfig.Scenarios.Compaction}, {"InterleavedThinking", testConfig.Scenarios.InterleavedThinking}, {"FastMode", testConfig.Scenarios.FastMode}, + {"EagerInputStreaming", testConfig.Scenarios.EagerInputStreaming}, } supported := 0 diff --git a/core/providers/anthropic/anthropic_test.go b/core/providers/anthropic/anthropic_test.go index d64b10aa82..6cb05f8c8c 100644 --- a/core/providers/anthropic/anthropic_test.go +++ b/core/providers/anthropic/anthropic_test.go @@ -72,7 +72,9 @@ func TestAnthropic(t *testing.T) { PassthroughAPI: true, Compaction: true, InterleavedThinking: true, - FastMode: false, // Enable when test API key has Opus 4.6 access + FastMode: false, // Enable when test API key has Opus 4.6 access + EagerInputStreaming: true, // fine-grained-tool-streaming-2025-05-14 (GA on Anthropic) + ServerToolsViaOpenAIEndpoint: true, // web_search / web_fetch / code_execution via /v1/chat/completions }, } diff --git a/core/providers/anthropic/chat.go b/core/providers/anthropic/chat.go index 25d6938246..e07a9f3093 100644 --- a/core/providers/anthropic/chat.go +++ b/core/providers/anthropic/chat.go @@ -151,6 +151,13 @@ func ToAnthropicChatRequest(ctx *schemas.BifrostContext, bifrostReq *schemas.Bif anthropicTool.CacheControl = tool.CacheControl } + // Fine-grained tool streaming — promoted neutral flag on ChatTool. + // Anthropic auto-injects beta header fine-grained-tool-streaming-2025-05-14 + // via AddMissingBetaHeadersToContext when this is set. + if tool.EagerInputStreaming != nil { + anthropicTool.EagerInputStreaming = tool.EagerInputStreaming + } + tools = append(tools, anthropicTool) } if anthropicReq.Tools == nil { diff --git a/core/providers/anthropic/chat_test.go b/core/providers/anthropic/chat_test.go index ca300c7fba..04df3bd02f 100644 --- a/core/providers/anthropic/chat_test.go +++ b/core/providers/anthropic/chat_test.go @@ -85,7 +85,7 @@ func TestToAnthropicChatRequest_CachingDeterminism(t *testing.T) { Model: "claude-sonnet-4-20250514", Input: []schemas.ChatMessage{{ Role: schemas.ChatMessageRoleUser, - Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("test")}, + Content: &schemas.ChatMessageContent{ContentStr: new("test")}, }}, Params: &schemas.ChatParameters{ Tools: []schemas.ChatTool{{ diff --git a/core/providers/anthropic/responses.go b/core/providers/anthropic/responses.go index 59980d75e2..f18b28d4ec 100644 --- a/core/providers/anthropic/responses.go +++ b/core/providers/anthropic/responses.go @@ -3437,7 +3437,7 @@ func convertAnthropicContentBlocksToResponsesMessagesGrouped(contentBlocks []Ant case AnthropicContentBlockTypeImage: // Don't emit accumulated text or tool_use blocks for images - if block.Source != nil { + if block.Source != nil && block.Source.SourceObj != nil { bifrostMsg := schemas.ResponsesMessage{ Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage), Role: role, @@ -3453,7 +3453,7 @@ func convertAnthropicContentBlocksToResponsesMessagesGrouped(contentBlocks []Ant case AnthropicContentBlockTypeDocument: // Handle document blocks similar to images - if block.Source != nil { + if block.Source != nil && block.Source.SourceObj != nil { bifrostMsg := schemas.ResponsesMessage{ Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage), Role: role, @@ -3543,7 +3543,7 @@ func convertAnthropicContentBlocksToResponsesMessagesGrouped(contentBlocks []Ant }) } case AnthropicContentBlockTypeImage: - if contentBlock.Source != nil { + if contentBlock.Source != nil && contentBlock.Source.SourceObj != nil { toolMsgContentBlocks = append(toolMsgContentBlocks, contentBlock.toBifrostResponsesImageBlock()) } } @@ -3756,7 +3756,7 @@ func convertAnthropicContentBlocksToResponsesMessages(ctx *schemas.BifrostContex bifrostMessages = append(bifrostMessages, bifrostMsg) } case AnthropicContentBlockTypeImage: - if block.Source != nil { + if block.Source != nil && block.Source.SourceObj != nil { bifrostMsg := schemas.ResponsesMessage{ Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage), Role: role, @@ -3770,7 +3770,7 @@ func convertAnthropicContentBlocksToResponsesMessages(ctx *schemas.BifrostContex bifrostMessages = append(bifrostMessages, bifrostMsg) } case AnthropicContentBlockTypeDocument: - if block.Source != nil { + if block.Source != nil && block.Source.SourceObj != nil { bifrostMsg := schemas.ResponsesMessage{ Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage), Role: role, @@ -3904,7 +3904,7 @@ func convertAnthropicContentBlocksToResponsesMessages(ctx *schemas.BifrostContex }) } case AnthropicContentBlockTypeImage: - if contentBlock.Source != nil { + if contentBlock.Source != nil && contentBlock.Source.SourceObj != nil { toolMsgContentBlocks = append(toolMsgContentBlocks, contentBlock.toBifrostResponsesImageBlock()) } } @@ -5200,36 +5200,40 @@ func (block AnthropicContentBlock) toBifrostResponsesDocumentBlock() schemas.Res resultBlock.ResponsesInputMessageContentBlockFile.Filename = block.Title } - if block.Source == nil { + if block.Source == nil || block.Source.SourceObj == nil { + // File-block rendering only applies to object-form sources + // (image / document). String-form sources (search_result) are + // handled elsewhere. return resultBlock } + src := block.Source.SourceObj // Handle different source types - switch block.Source.Type { + switch src.Type { case "url": // URL source - if block.Source.URL != nil { - resultBlock.ResponsesInputMessageContentBlockFile.FileURL = block.Source.URL + if src.URL != nil { + resultBlock.ResponsesInputMessageContentBlockFile.FileURL = src.URL } case "base64": // Base64 encoded data - if block.Source.Data != nil { + if src.Data != nil { // Construct data URL with media type mediaType := "application/pdf" - if block.Source.MediaType != nil { - mediaType = *block.Source.MediaType + if src.MediaType != nil { + mediaType = *src.MediaType } - dataURL := *block.Source.Data + dataURL := *src.Data if !strings.HasPrefix(dataURL, "data:") { - dataURL = "data:" + mediaType + ";base64," + *block.Source.Data + dataURL = "data:" + mediaType + ";base64," + *src.Data } resultBlock.ResponsesInputMessageContentBlockFile.FileData = &dataURL } case "text": // Plain text source - if block.Source.Data != nil { + if src.Data != nil { resultBlock.ResponsesInputMessageContentBlockFile.FileType = schemas.Ptr("text/plain") - resultBlock.ResponsesInputMessageContentBlockFile.FileData = block.Source.Data + resultBlock.ResponsesInputMessageContentBlockFile.FileData = src.Data } } diff --git a/core/providers/anthropic/types.go b/core/providers/anthropic/types.go index 39749cd72c..0d2284e712 100644 --- a/core/providers/anthropic/types.go +++ b/core/providers/anthropic/types.go @@ -26,6 +26,12 @@ const ( AnthropicStructuredOutputsBetaHeader = "structured-outputs-2025-11-13" // AnthropicAdvancedToolUseBetaHeader is required for defer_loading, input_examples, and allowed_callers. AnthropicAdvancedToolUseBetaHeader = "advanced-tool-use-2025-11-20" + // AnthropicToolExamplesBetaHeader is required for tool.input_examples as a + // standalone feature (Bedrock supports this narrow header without the full + // advanced-tool-use-2025-11-20 bundle). + // Source: AWS Bedrock user guide beta-header list: + // https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages.html + AnthropicToolExamplesBetaHeader = "tool-examples-2025-10-29" // AnthropicMCPClientBetaHeader is required for MCP servers (current version). AnthropicMCPClientBetaHeader = "mcp-client-2025-11-20" // AnthropicMCPClientBetaHeaderDeprecated is the previous MCP beta header (kept for fallback). @@ -50,6 +56,10 @@ const ( AnthropicRedactThinkingBetaHeader = "redact-thinking-2026-02-12" // AnthropicTaskBudgetsBetaHeader is required for output_config.task_budget (Opus 4.7+). AnthropicTaskBudgetsBetaHeader = "task-budgets-2026-03-13" + // AnthropicEagerInputStreamingBetaHeader is required for eager_input_streaming + // on custom tools (streams input_json_delta before full args are determined). + // Per Table 20: GA on Anthropic/Bedrock/Vertex, Beta on Azure. + AnthropicEagerInputStreamingBetaHeader = "fine-grained-tool-streaming-2025-05-14" // AnthropicComputerUseBetaHeader is required for computer use (version-specific). // computer_20251124 (Opus 4.6, Sonnet 4.6, Opus 4.5) uses the newer beta header. @@ -61,6 +71,7 @@ const ( // Use these with strings.HasPrefix when filtering headers per provider, // so that future date bumps (e.g. structured-outputs-2025-12-15) are still matched. AnthropicAdvancedToolUseBetaHeaderPrefix = "advanced-tool-use-" + AnthropicToolExamplesBetaHeaderPrefix = "tool-examples-" AnthropicStructuredOutputsBetaHeaderPrefix = "structured-outputs-" AnthropicPromptCachingScopeBetaHeaderPrefix = "prompt-caching-scope-" AnthropicMCPClientBetaHeaderPrefix = "mcp-client-" @@ -70,65 +81,122 @@ const ( AnthropicFastModeBetaHeaderPrefix = "fast-mode-" AnthropicRedactThinkingBetaHeaderPrefix = "redact-thinking-" AnthropicTaskBudgetsBetaHeaderPrefix = "task-budgets-" + AnthropicEagerInputStreamingBetaHeaderPrefix = "fine-grained-tool-streaming-" ) // ProviderFeatureSupport defines which Anthropic features a given provider supports. -// Source: https://docs.anthropic.com/en/build-with-claude/overview (March 2026) +// +// Authoritative sources (verified 2026-04-17): +// A = Anthropic feature-availability table: +// https://platform.claude.com/docs/en/build-with-claude/overview +// B-header = AWS Bedrock user guide beta-header list: +// https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages.html +// B-platform = https://platform.claude.com/docs/en/build-with-claude/claude-on-amazon-bedrock +// V-platform = https://platform.claude.com/docs/en/build-with-claude/claude-on-vertex-ai +// Az-platform = https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry +// MCP-excl = MCP connector explicit Bedrock/Vertex exclusion: +// https://platform.claude.com/docs/en/agents-and-tools/mcp-connector +// Advisor-excl = Advisor tool Claude-API-only: +// https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool type ProviderFeatureSupport struct { - WebSearch bool // web_search server tool - WebSearchDynamic bool // web_search_20260209 (dynamic filtering, requires code_execution) - WebFetch bool // web_fetch server tool - CodeExecution bool // code_execution server tool - ComputerUse bool // computer_use client tool - Bash bool // bash client tool - Memory bool // memory client tool - TextEditor bool // text_editor client tool - ToolSearch bool // tool_search server tool - MCP bool // MCP connector - AdvancedToolUse bool // advanced-tool-use (defer_loading, input_examples, allowed_callers) - StructuredOutputs bool // strict tool validation and output_format - PromptCachingScope bool // prompt caching scope - Compaction bool // server-side context compaction - ContextEditing bool // context editing (clear_tool_uses, clear_thinking) - FilesAPI bool // Files API - InterleavedThinking bool // interleaved thinking between tool calls - Skills bool // Agent Skills - Context1M bool // 1M context window beta (for Sonnet 4.5/4 only) - FastMode bool // fast mode (Opus 4.6 only, research preview) - RedactThinking bool // redact thinking blocks in responses - TaskBudgets bool // task_budget in output_config (Opus 4.7+) + WebSearch bool // web_search server tool (cite: A) + WebSearchDynamic bool // web_search_20260209 dynamic filtering (cite: A) + WebFetch bool // web_fetch server tool (cite: A) + CodeExecution bool // code_execution server tool (cite: A) + ComputerUse bool // computer_use client tool (cite: A, B-header) + Bash bool // bash client tool (cite: A, B-header) + Memory bool // memory client tool — on Bedrock bundled under context-management-2025-06-27 (cite: A, B-header) + TextEditor bool // text_editor client tool (cite: A) + ToolSearch bool // tool_search server tool — tool-search-tool-2025-10-19 (cite: A, B-header) + MCP bool // MCP connector — explicit "not supported on Bedrock/Vertex" (cite: MCP-excl) + AdvancedToolUse bool // advanced-tool-use-2025-11-20 bundle: defer_loading + input_examples + allowed_callers (cite: A) + InputExamples bool // tool.input_examples standalone — tool-examples-2025-10-29. Bedrock supports this independently of the AdvancedToolUse bundle (cite: B-header). On Anthropic / Azure the bundle implicitly covers it. + StructuredOutputs bool // strict tool validation / output_format (cite: A) + PromptCachingScope bool // cache_control.scope — prompt-caching-scope-2026-01-05 (cite: A) + Compaction bool // compact_20260112 (cite: A, B-header) + ContextEditing bool // clear_tool_uses / clear_thinking (cite: A, B-header) + FilesAPI bool // files-api-2025-04-14, file_id source (cite: A) + InterleavedThinking bool // interleaved thinking between tool calls (cite: A, B-header; fails on non-allowlisted models on Bedrock/Vertex) + Skills bool // Agent Skills — container.skills object (cite: A) + ContainerBasic bool // Bare string-form container id — universally supported (cite: A) + Context1M bool // 1M context window — context-1m-2025-08-07 (cite: A) + FastMode bool // Opus 4.6 research preview — fast-mode-2026-02-01 (cite: A) + RedactThinking bool // redact-thinking-2026-02-12 (cite: A) — note Bedrock has its own "thinking encryption" (different mechanism) + TaskBudgets bool // output_config.task_budget — task-budgets-2026-03-13 (cite: A) + InferenceGeo bool // inference_geo field — Claude API only; Bedrock/Vertex/Azure use their own region-routing mechanisms (cite: A) + EagerInputStreaming bool // fine-grained-tool-streaming-2025-05-14 (cite: A, B-header) + AdvisorTool bool // advisor_tool_result block — Anthropic only (cite: Advisor-excl) FileSearch bool // file_search server tool (OpenAI-only) ImageGeneration bool // image_generation server tool (OpenAI-only) } // ProviderFeatures maps each provider to its supported Anthropic features. +// +// Every cell below is sourced from the docs named in ProviderFeatureSupport. +// "Not documented" in upstream docs is treated as unsupported here; if a user +// needs a pass-through, ExtraParams still works. var ProviderFeatures = map[schemas.ModelProvider]ProviderFeatureSupport{ + // Anthropic Claude API direct (cite: A across the board). schemas.Anthropic: { WebSearch: true, WebSearchDynamic: true, WebFetch: true, CodeExecution: true, ComputerUse: true, Bash: true, Memory: true, TextEditor: true, ToolSearch: true, - MCP: true, AdvancedToolUse: true, StructuredOutputs: true, PromptCachingScope: true, + MCP: true, AdvancedToolUse: true, InputExamples: true, StructuredOutputs: true, PromptCachingScope: true, Compaction: true, ContextEditing: true, FilesAPI: true, - InterleavedThinking: true, Skills: true, Context1M: true, FastMode: true, - RedactThinking: true, TaskBudgets: true, + InterleavedThinking: true, Skills: true, ContainerBasic: true, Context1M: true, + FastMode: true, RedactThinking: true, TaskBudgets: true, + InferenceGeo: true, EagerInputStreaming: true, AdvisorTool: true, }, + // Google Vertex AI — cite: A (overview table) and V-platform. + // Notably NOT supported: MCP (MCP-excl), Skills/container.skills, + // InferenceGeo, FastMode, TaskBudgets, AdvisorTool, StructuredOutputs, + // PromptCachingScope (400 "unexpected beta header" per LiteLLM #19984), + // FilesAPI, WebFetch, CodeExecution, AdvancedToolUse, RedactThinking. schemas.Vertex: { - WebSearch: true, // only web_search_20250305 (basic), NOT dynamic filtering + WebSearch: true, // web search GA on Vertex per A; earlier code restricted to web_search_20250305 — A doesn't qualify ComputerUse: true, Bash: true, Memory: true, TextEditor: true, ToolSearch: true, - Compaction: true, ContextEditing: true, - InterleavedThinking: true, Context1M: true, + ContainerBasic: true, + Compaction: true, + ContextEditing: true, + InterleavedThinking: true, // V-platform confirms; fails on non-allowlisted 4-series + Context1M: true, + EagerInputStreaming: true, // fine-grained-tool-streaming GA per A }, + // AWS Bedrock — cite: A + B-header (definitive beta-header list). + // Notably NOT supported per docs: MCP, Skills, FilesAPI, WebFetch, + // WebSearch, CodeExecution, FastMode, TaskBudgets, AdvisorTool, + // InferenceGeo, RedactThinking, AdvancedToolUse (full), PromptCachingScope. schemas.Bedrock: { ComputerUse: true, Bash: true, Memory: true, TextEditor: true, ToolSearch: true, - StructuredOutputs: true, Compaction: true, ContextEditing: true, - InterleavedThinking: true, Context1M: true, + ContainerBasic: true, + // StructuredOutputs: kept true to match pre-existing behavior and the + // provider_feature_support_test.go assertion, but NEITHER B-header + // NOR B-platform upstream docs document strict tool validation / + // output_format on Bedrock. Needs live verification. If Bedrock's + // Converse API actually rejects `strict: true`, flip this to false + // and update the corresponding test assertion. + StructuredOutputs: true, + Compaction: true, // compact-2026-01-12 per B-header + ContextEditing: true, // context-management-2025-06-27 per B-header (bundles memory) + InterleavedThinking: true, // per B-header; model-allowlisted + Context1M: true, // Opus 4.6 / Sonnet 4.6 per A + EagerInputStreaming: true, // fine-grained-tool-streaming-2025-05-14 per B-header + InputExamples: true, // tool-examples-2025-10-29 per B-header (standalone; Bedrock doesn't accept the full advanced-tool-use-2025-11-20 bundle — see TestFilterBetaHeadersForProvider) + // AdvancedToolUse intentionally OFF on Bedrock. The bundle header + // (advanced-tool-use-2025-11-20) is not listed in B-header; only the + // narrow tool-examples-2025-10-29 header is, gated via InputExamples above. }, + // Microsoft Azure AI Foundry — cite: A (most features azureAiBeta) + + // Az-platform ("supports most of Claude's features"). Excluded per + // Az-platform: Admin API, Models API, Message Batch API (not in scope). schemas.Azure: { WebSearch: true, WebSearchDynamic: true, WebFetch: true, CodeExecution: true, ComputerUse: true, Bash: true, Memory: true, TextEditor: true, ToolSearch: true, - MCP: true, AdvancedToolUse: true, StructuredOutputs: true, PromptCachingScope: true, + MCP: true, AdvancedToolUse: true, InputExamples: true, StructuredOutputs: true, PromptCachingScope: true, Compaction: true, ContextEditing: true, FilesAPI: true, - InterleavedThinking: true, Skills: true, Context1M: true, + InterleavedThinking: true, Skills: true, ContainerBasic: true, Context1M: true, RedactThinking: true, TaskBudgets: true, + EagerInputStreaming: true, + // FastMode, InferenceGeo, AdvisorTool — not in Az-platform; leave off. }, } @@ -178,6 +246,72 @@ type AnthropicOutputConfig struct { TaskBudget *AnthropicTaskBudget `json:"task_budget,omitempty"` // advisory token budget; requires task-budgets-2026-03-13 beta header } +// AnthropicContainerSkill represents a single skill attached to a container. +// Requires beta header "skills-2025-10-02". +type AnthropicContainerSkill struct { + SkillID string `json:"skill_id"` // Unique identifier for the skill + Type string `json:"type"` // "anthropic" (built-in) | "custom" (user-defined) + Version *string `json:"version,omitempty"` // Optional version pin +} + +// AnthropicContainerObject represents the object form of the container field: +// { id?: string, skills?: [...] }. The skills[] array is gated by the +// skills-2025-10-02 beta header; a bare id-only container is GA. +type AnthropicContainerObject struct { + ID *string `json:"id,omitempty"` + Skills []AnthropicContainerSkill `json:"skills,omitempty"` +} + +// AnthropicContainer is the "container" field on AnthropicMessageRequest. +// Per Anthropic docs it can be either a bare string (container id) or an +// object with id+skills[]. The object-with-skills form requires beta header +// "skills-2025-10-02"; the string form is GA. +// Source: https://platform.claude.com/docs/en/api/messages/create +type AnthropicContainer struct { + ContainerStr *string + ContainerObject *AnthropicContainerObject +} + +// MarshalJSON encodes the union as either a raw string or the object form. +func (c AnthropicContainer) MarshalJSON() ([]byte, error) { + if c.ContainerStr != nil && c.ContainerObject != nil { + return nil, fmt.Errorf("both ContainerStr and ContainerObject are set; only one should be non-nil") + } + if c.ContainerStr != nil { + return providerUtils.MarshalSorted(*c.ContainerStr) + } + if c.ContainerObject != nil { + return providerUtils.MarshalSorted(c.ContainerObject) + } + return providerUtils.MarshalSorted(nil) +} + +// UnmarshalJSON decodes either a string or the object form into the union. +// Clears the inactive arm on each success so a reused struct never ends up +// with both fields populated (which MarshalJSON rejects). Explicitly handles +// JSON null. Matches the ChatContainer / ChatToolChoice union patterns. +func (c *AnthropicContainer) UnmarshalJSON(data []byte) error { + trimmed := bytes.TrimSpace(data) + if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) { + c.ContainerStr = nil + c.ContainerObject = nil + return nil + } + var s string + if err := sonic.Unmarshal(data, &s); err == nil { + c.ContainerStr = &s + c.ContainerObject = nil + return nil + } + var obj AnthropicContainerObject + if err := sonic.Unmarshal(data, &obj); err == nil { + c.ContainerStr = nil + c.ContainerObject = &obj + return nil + } + return fmt.Errorf("container field is neither a string nor a container object") +} + // AnthropicMessageRequest represents an Anthropic messages API request type AnthropicMessageRequest struct { Model string `json:"model"` @@ -201,6 +335,7 @@ type AnthropicMessageRequest struct { ServiceTier *string `json:"service_tier,omitempty"` // "auto" or "standard_only" InferenceGeo *string `json:"inference_geo,omitempty"` // the geographic region for inference processing. If not specified, the workspace's default_inference_geo is used. ContextManagement *ContextManagement `json:"context_management,omitempty"` + Container *AnthropicContainer `json:"container,omitempty"` // string id OR object with skills[]; skills require skills-2025-10-02 beta // Extra params for advanced use cases ExtraParams map[string]interface{} `json:"-"` @@ -477,6 +612,7 @@ var anthropicMessageRequestKnownFields = map[string]bool{ "service_tier": true, "inference_geo": true, "context_management": true, + "container": true, "extra_params": true, "fallbacks": true, } @@ -701,54 +837,205 @@ func (mc *AnthropicContent) UnmarshalJSON(data []byte) error { type AnthropicContentBlockType string const ( - AnthropicContentBlockTypeText AnthropicContentBlockType = "text" - AnthropicContentBlockTypeImage AnthropicContentBlockType = "image" - AnthropicContentBlockTypeDocument AnthropicContentBlockType = "document" - AnthropicContentBlockTypeToolUse AnthropicContentBlockType = "tool_use" - AnthropicContentBlockTypeServerToolUse AnthropicContentBlockType = "server_tool_use" - AnthropicContentBlockTypeToolResult AnthropicContentBlockType = "tool_result" - AnthropicContentBlockTypeWebSearchToolResult AnthropicContentBlockType = "web_search_tool_result" - AnthropicContentBlockTypeWebSearchToolResultError AnthropicContentBlockType = "web_search_tool_result_error" - AnthropicContentBlockTypeWebSearchResult AnthropicContentBlockType = "web_search_result" - AnthropicContentBlockTypeWebFetchToolResult AnthropicContentBlockType = "web_fetch_tool_result" - AnthropicContentBlockTypeMCPToolUse AnthropicContentBlockType = "mcp_tool_use" - AnthropicContentBlockTypeMCPToolResult AnthropicContentBlockType = "mcp_tool_result" - AnthropicContentBlockTypeThinking AnthropicContentBlockType = "thinking" - AnthropicContentBlockTypeRedactedThinking AnthropicContentBlockType = "redacted_thinking" - AnthropicContentBlockTypeCompaction AnthropicContentBlockType = "compaction" + AnthropicContentBlockTypeText AnthropicContentBlockType = "text" + AnthropicContentBlockTypeImage AnthropicContentBlockType = "image" + AnthropicContentBlockTypeDocument AnthropicContentBlockType = "document" + AnthropicContentBlockTypeSearchResult AnthropicContentBlockType = "search_result" + AnthropicContentBlockTypeToolUse AnthropicContentBlockType = "tool_use" + AnthropicContentBlockTypeServerToolUse AnthropicContentBlockType = "server_tool_use" + AnthropicContentBlockTypeToolResult AnthropicContentBlockType = "tool_result" + AnthropicContentBlockTypeWebSearchToolResult AnthropicContentBlockType = "web_search_tool_result" + AnthropicContentBlockTypeWebSearchToolResultError AnthropicContentBlockType = "web_search_tool_result_error" + AnthropicContentBlockTypeWebSearchResult AnthropicContentBlockType = "web_search_result" + AnthropicContentBlockTypeWebFetchToolResult AnthropicContentBlockType = "web_fetch_tool_result" + AnthropicContentBlockTypeCodeExecutionToolResult AnthropicContentBlockType = "code_execution_tool_result" + AnthropicContentBlockTypeBashCodeExecutionToolResult AnthropicContentBlockType = "bash_code_execution_tool_result" + AnthropicContentBlockTypeTextEditorCodeExecutionToolResult AnthropicContentBlockType = "text_editor_code_execution_tool_result" + AnthropicContentBlockTypeToolSearchToolResult AnthropicContentBlockType = "tool_search_tool_result" + AnthropicContentBlockTypeToolReference AnthropicContentBlockType = "tool_reference" + AnthropicContentBlockTypeContainerUpload AnthropicContentBlockType = "container_upload" + AnthropicContentBlockTypeAdvisorToolResult AnthropicContentBlockType = "advisor_tool_result" + AnthropicContentBlockTypeMCPToolUse AnthropicContentBlockType = "mcp_tool_use" + AnthropicContentBlockTypeMCPToolResult AnthropicContentBlockType = "mcp_tool_result" + AnthropicContentBlockTypeThinking AnthropicContentBlockType = "thinking" + AnthropicContentBlockTypeRedactedThinking AnthropicContentBlockType = "redacted_thinking" + AnthropicContentBlockTypeCompaction AnthropicContentBlockType = "compaction" ) -// AnthropicContentBlock represents content in Anthropic message format +// AnthropicToolCallerType identifies which agentic caller produced a tool +// invocation. Appears on tool_use, server_tool_use, and every *_tool_result +// block per Anthropic docs. +// Source: https://platform.claude.com/docs/en/api/beta/messages/create +type AnthropicToolCallerType string + +const ( + AnthropicToolCallerTypeDirect AnthropicToolCallerType = "direct" + AnthropicToolCallerTypeCodeExecution20250825 AnthropicToolCallerType = "code_execution_20250825" + AnthropicToolCallerTypeCodeExecution20260120 AnthropicToolCallerType = "code_execution_20260120" +) + +// AnthropicToolCaller represents the "caller" union on tool-use and +// tool-result blocks. For the two code-execution variants, ToolID is required +// and identifies the upstream server tool that invoked the nested tool. +type AnthropicToolCaller struct { + Type AnthropicToolCallerType `json:"type"` + ToolID *string `json:"tool_id,omitempty"` // Required for code_execution_* caller types +} + +// AnthropicContentBlock represents content in Anthropic message format. +// This is a fat struct: every optional field here is used by at least one +// block type. Consult Anthropic's content-block docs before adding a field +// so we reuse existing ones where semantics align. type AnthropicContentBlock struct { - Type AnthropicContentBlockType `json:"type"` // "text", "image", "document", "tool_use", "tool_result", "thinking" - Text *string `json:"text,omitempty"` // For text content - Thinking *string `json:"thinking,omitempty"` // For thinking content - Signature *string `json:"signature,omitempty"` // For signature content - Data *string `json:"data,omitempty"` // For data content (encrypted data for redacted thinking, signature does not come with this) - ToolUseID *string `json:"tool_use_id,omitempty"` // For tool_result content - ID *string `json:"id,omitempty"` // For tool_use content - Name *string `json:"name,omitempty"` // For tool_use content - Input json.RawMessage `json:"input,omitempty"` // For tool_use content (json.RawMessage preserves key ordering for prompt caching) - ServerName *string `json:"server_name,omitempty"` // For mcp_tool_use content - Content *AnthropicContent `json:"content,omitempty"` // For tool_result content - IsError *bool `json:"is_error,omitempty"` // For tool_result content, indicates error state - Source *AnthropicSource `json:"source,omitempty"` // For image/document content - CacheControl *schemas.CacheControl `json:"cache_control,omitempty"` // For cache control content - Citations *AnthropicCitations `json:"citations,omitempty"` // For document content - Context *string `json:"context,omitempty"` // For document content - Title *string `json:"title,omitempty"` // For document content - URL *string `json:"url,omitempty"` // For web_search_result content - EncryptedContent *string `json:"encrypted_content,omitempty"` // For web_search_result content - PageAge *string `json:"page_age,omitempty"` // For web_search_result content - ErrorCode *string `json:"error_code,omitempty"` // For web_search_tool_result_error content -} - -// AnthropicSource represents image or document source in Anthropic format + Type AnthropicContentBlockType `json:"type"` // Discriminator + Text *string `json:"text,omitempty"` // text block; also "advisor_result" variant + Thinking *string `json:"thinking,omitempty"` // thinking block + Signature *string `json:"signature,omitempty"` // thinking block signature + Data *string `json:"data,omitempty"` // redacted_thinking encrypted data (no signature) + ToolUseID *string `json:"tool_use_id,omitempty"` // tool_result, *_tool_result blocks + ID *string `json:"id,omitempty"` // tool_use, server_tool_use, mcp_tool_use + Name *string `json:"name,omitempty"` // tool_use, server_tool_use; also reused for tool_reference's tool_name via ToolName + Input json.RawMessage `json:"input,omitempty"` // tool_use / server_tool_use (json.RawMessage preserves key ordering for prompt caching) + ServerName *string `json:"server_name,omitempty"` // mcp_tool_use + Content *AnthropicContent `json:"content,omitempty"` // tool_result, *_tool_result; inner structured content or string + IsError *bool `json:"is_error,omitempty"` // tool_result, *_tool_result + Source *AnthropicBlockSource `json:"source,omitempty"` // image, document (SourceObj) or search_result (SourceStr) — union type + CacheControl *schemas.CacheControl `json:"cache_control,omitempty"` // any block + Citations *AnthropicCitations `json:"citations,omitempty"` // text, document, search_result (request config) or response citations array + Context *string `json:"context,omitempty"` // document + Title *string `json:"title,omitempty"` // document, search_result, web_search_result + URL *string `json:"url,omitempty"` // web_search_result, web_fetch_result + EncryptedContent *string `json:"encrypted_content,omitempty"` // web_search_result, advisor_redacted_result, compaction + PageAge *string `json:"page_age,omitempty"` // web_search_result + ErrorCode *string `json:"error_code,omitempty"` // any *_tool_result_error variant + Caller *AnthropicToolCaller `json:"caller,omitempty"` // tool_use, server_tool_use, every *_tool_result block + + // search_result block: the API uses the literal key "source" with a plain + // string value, which collides with the existing Source *AnthropicSource + // field (object form, used by image/document). Supporting both requires + // either (a) a string-or-object union type for Source, or (b) full custom + // Marshal/Unmarshal on AnthropicContentBlock. Deferred until we decide the + // representation — search_result block enum is present above but its + // source string has no typed slot yet. Callers needing it can use + // ExtraParams pass-through on the request side in the meantime. + + // code_execution_tool_result / bash_code_execution_tool_result result-variant fields + Stdout *string `json:"stdout,omitempty"` + Stderr *string `json:"stderr,omitempty"` + ReturnCode *int `json:"return_code,omitempty"` + EncryptedStdout *string `json:"encrypted_stdout,omitempty"` + + // text_editor_code_execution_tool_result variants + FileType *string `json:"file_type,omitempty"` // view_result: "text"|"image"|"pdf" + StartLine *int `json:"start_line,omitempty"` // view_result + NumLines *int `json:"num_lines,omitempty"` // view_result + TotalLines *int `json:"total_lines,omitempty"` // view_result + IsFileUpdate *bool `json:"is_file_update,omitempty"` // create_result + OldStart *int `json:"old_start,omitempty"` // str_replace_result + OldLines *int `json:"old_lines,omitempty"` // str_replace_result + NewStart *int `json:"new_start,omitempty"` // str_replace_result + NewLines *int `json:"new_lines,omitempty"` // str_replace_result + Lines []string `json:"lines,omitempty"` // str_replace_result + ErrorMessage *string `json:"error_message,omitempty"` // text_editor error variant + + // tool_search_tool_result success variant + ToolReferences []AnthropicContentBlock `json:"tool_references,omitempty"` // tool_search_tool_search_result (array of tool_reference blocks) + + // tool_reference block — tool_name field on the block itself + ToolName *string `json:"tool_name,omitempty"` + + // container_upload block + web_fetch_result inner file_id reference + FileID *string `json:"file_id,omitempty"` + + // web_fetch_tool_result / web_fetch_result inner retrieval timestamp + RetrievedAt *string `json:"retrieved_at,omitempty"` +} + +// AnthropicSource represents image or document source in Anthropic format. +// +// Per docs (https://platform.claude.com/docs/en/api/messages/create) the +// documented type values and their carrying fields are: +// - "base64" → MediaType + Data +// - "url" → URL +// - "text" → MediaType ("text/plain") + Data +// - "content_block" → Content (nested string OR array of inner blocks); +// recursive ContentBlockSource used inside DocumentBlockParam +// - "file" → FileID (requires files-api-2025-04-14 beta) +// +// The struct is a superset — only the fields relevant to Type should be set +// at a time. type AnthropicSource struct { - Type string `json:"type"` // "base64", "url", "text", "content_block" - MediaType *string `json:"media_type,omitempty"` // "image/jpeg", "image/png", "application/pdf", etc. - Data *string `json:"data,omitempty"` // Base64-encoded data (for base64 type) - URL *string `json:"url,omitempty"` // URL (for url type) + Type string `json:"type"` // "base64" | "url" | "text" | "content" | "content_block" (alias) | "file" + MediaType *string `json:"media_type,omitempty"` // "image/jpeg", "image/png", "application/pdf", etc. + Data *string `json:"data,omitempty"` // Base64-encoded data (base64 type) or text payload (text type) + URL *string `json:"url,omitempty"` // URL (url type) + FileID *string `json:"file_id,omitempty"` // File ID (file type; requires files-api-2025-04-14 beta) + Content json.RawMessage `json:"content,omitempty"` // For content_block type: nested content — string OR array of inner blocks (TextBlockParam / ImageBlockParam). json.RawMessage preserves exact bytes for prompt caching. +} + +// AnthropicBlockSource is the union "source" field on a content block. +// +// Anthropic's API uses the literal JSON key "source" for two incompatible +// shapes depending on which block the key appears on: +// +// - On `image` / `document` blocks: an OBJECT describing the source +// (type + media_type + data/url/file_id). Modeled by AnthropicSource. +// - On `search_result` blocks: a plain STRING identifier (URL/path). +// +// This union wrapper lets AnthropicContentBlock carry either shape under +// the single "source" JSON key. +// +// Docs: +// - https://platform.claude.com/docs/en/api/messages/create (ImageBlockParam, DocumentBlockParam) +// - https://platform.claude.com/docs/en/api/beta/messages/create (SearchResultBlockParam) +type AnthropicBlockSource struct { + SourceStr *string // search_result: plain string (URL, path, identifier) + SourceObj *AnthropicSource // image / document: object form +} + +// MarshalJSON emits either the string or the object form directly (unwrapped). +// Matches the union-type idiom used by AnthropicCitations, AnthropicContainer, +// and CompactManagementEditTypeAndValue. +func (s AnthropicBlockSource) MarshalJSON() ([]byte, error) { + if s.SourceStr != nil && s.SourceObj != nil { + return nil, fmt.Errorf("both SourceStr and SourceObj are set; only one should be non-nil") + } + if s.SourceStr != nil { + return providerUtils.MarshalSorted(*s.SourceStr) + } + if s.SourceObj != nil { + return providerUtils.MarshalSorted(s.SourceObj) + } + return providerUtils.MarshalSorted(nil) +} + +// UnmarshalJSON decodes either the string or the object form into the union. +// Matches AnthropicCitations.UnmarshalJSON: sonic-decode into each variant, +// first success wins. +// UnmarshalJSON decodes either the string form (search_result blocks) or the +// object form (image/document blocks) into the union. Clears the inactive +// arm on each success so a reused struct never ends up with both fields +// populated (which MarshalJSON rejects). Explicitly handles JSON null. +func (s *AnthropicBlockSource) UnmarshalJSON(data []byte) error { + trimmed := bytes.TrimSpace(data) + if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) { + s.SourceStr = nil + s.SourceObj = nil + return nil + } + var str string + if err := sonic.Unmarshal(data, &str); err == nil { + s.SourceStr = &str + s.SourceObj = nil + return nil + } + var obj AnthropicSource + if err := sonic.Unmarshal(data, &obj); err == nil { + s.SourceStr = nil + s.SourceObj = &obj + return nil + } + return fmt.Errorf("source field is neither a string nor an AnthropicSource object") } type AnthropicCitationType string @@ -856,7 +1143,9 @@ type AnthropicToolType string const ( AnthropicToolTypeCustom AnthropicToolType = "custom" + AnthropicToolTypeBash20241022 AnthropicToolType = "bash_20241022" // computer-use-2024-10-22 beta AnthropicToolTypeBash20250124 AnthropicToolType = "bash_20250124" + AnthropicToolTypeComputer20241022 AnthropicToolType = "computer_20241022" // computer-use-2024-10-22 beta AnthropicToolTypeComputer20250124 AnthropicToolType = "computer_20250124" AnthropicToolTypeComputer20251124 AnthropicToolType = "computer_20251124" // for claude-opus-4.5, claude-opus-4.6, claude-sonnet-4.6 AnthropicToolTypeTextEditor20250124 AnthropicToolType = "text_editor_20250124" @@ -924,10 +1213,19 @@ type AnthropicToolWebSearch struct { } type AnthropicToolWebFetch struct { - MaxUses *int `json:"max_uses,omitempty"` - AllowedDomains []string `json:"allowed_domains,omitempty"` - BlockedDomains []string `json:"blocked_domains,omitempty"` - MaxContentTokens *int `json:"max_content_tokens,omitempty"` + MaxUses *int `json:"max_uses,omitempty"` + AllowedDomains []string `json:"allowed_domains,omitempty"` + BlockedDomains []string `json:"blocked_domains,omitempty"` + MaxContentTokens *int `json:"max_content_tokens,omitempty"` + Citations *AnthropicCitations `json:"citations,omitempty"` // {enabled: bool} — toggles citation emission on fetched documents + UseCache *bool `json:"use_cache,omitempty"` // web_fetch_20260309+ only — enables server-side page cache +} + +// AnthropicToolTextEditor holds fields specific to the text_editor tool +// variants. Only text_editor_20250728 (and later) honours max_characters +// as a view-truncation cap. +type AnthropicToolTextEditor struct { + MaxCharacters *int `json:"max_characters,omitempty"` // text_editor_20250728+ only } // AnthropicToolInputExample represents an input example for a tool (beta feature) @@ -938,19 +1236,21 @@ type AnthropicToolInputExample struct { // AnthropicTool represents a tool in Anthropic format type AnthropicTool struct { - Name string `json:"name"` - Type *AnthropicToolType `json:"type,omitempty"` - Description *string `json:"description,omitempty"` - InputSchema *schemas.ToolFunctionParameters `json:"input_schema,omitempty"` - CacheControl *schemas.CacheControl `json:"cache_control,omitempty"` - DeferLoading *bool `json:"defer_loading,omitempty"` // Beta: defer loading of tool definition - Strict *bool `json:"strict,omitempty"` // Whether to enforce strict parameter validation - AllowedCallers []string `json:"allowed_callers,omitempty"` // Beta: which callers can use this tool - InputExamples []AnthropicToolInputExample `json:"input_examples,omitempty"` // Beta: example inputs for the tool + Name string `json:"name"` + Type *AnthropicToolType `json:"type,omitempty"` + Description *string `json:"description,omitempty"` + InputSchema *schemas.ToolFunctionParameters `json:"input_schema,omitempty"` + CacheControl *schemas.CacheControl `json:"cache_control,omitempty"` + DeferLoading *bool `json:"defer_loading,omitempty"` // Beta: defer loading of tool definition + Strict *bool `json:"strict,omitempty"` // Whether to enforce strict parameter validation + AllowedCallers []string `json:"allowed_callers,omitempty"` // Beta: which callers can use this tool + InputExamples []AnthropicToolInputExample `json:"input_examples,omitempty"` // Beta: example inputs for the tool + EagerInputStreaming *bool `json:"eager_input_streaming,omitempty"` // Custom tools only; beta fine-grained-tool-streaming-2025-05-14 *AnthropicToolComputerUse *AnthropicToolWebSearch *AnthropicToolWebFetch + *AnthropicToolTextEditor // MCP toolset (mcp-client-2025-11-20 format) — embedded when Type is nil and MCPToolset is set MCPToolset *AnthropicMCPToolsetTool `json:"-"` // Serialized via custom MarshalJSON diff --git a/core/providers/anthropic/utils.go b/core/providers/anthropic/utils.go index 6dc058df20..765b3225d8 100644 --- a/core/providers/anthropic/utils.go +++ b/core/providers/anthropic/utils.go @@ -281,15 +281,38 @@ func AddMissingBetaHeadersToContext(ctx *schemas.BifrostContext, req *AnthropicM headers = appendUniqueHeader(headers, AnthropicStructuredOutputsBetaHeader) } } - // Check for advanced-tool-use features + // Advanced-tool-use features. defer_loading and allowed_callers + // are only in the advanced-tool-use-2025-11-20 bundle; emit the + // bundle header only when the target provider supports the full + // bundle (Anthropic/Azure). if tool.DeferLoading != nil && *tool.DeferLoading { - headers = appendUniqueHeader(headers, AnthropicAdvancedToolUseBetaHeader) + if !hasProvider || features.AdvancedToolUse { + headers = appendUniqueHeader(headers, AnthropicAdvancedToolUseBetaHeader) + } + } + if len(tool.AllowedCallers) > 0 { + if !hasProvider || features.AdvancedToolUse { + headers = appendUniqueHeader(headers, AnthropicAdvancedToolUseBetaHeader) + } } + // input_examples has both bundle coverage AND a standalone header. + // Prefer the bundle header when the provider accepts the bundle + // (covers input_examples transitively); fall back to the narrow + // standalone header (Bedrock) when only InputExamples is set. if len(tool.InputExamples) > 0 { - headers = appendUniqueHeader(headers, AnthropicAdvancedToolUseBetaHeader) + if !hasProvider || features.AdvancedToolUse { + headers = appendUniqueHeader(headers, AnthropicAdvancedToolUseBetaHeader) + } else if features.InputExamples { + headers = appendUniqueHeader(headers, AnthropicToolExamplesBetaHeader) + } } - if len(tool.AllowedCallers) > 0 { - headers = appendUniqueHeader(headers, AnthropicAdvancedToolUseBetaHeader) + // Check for fine-grained tool streaming (eager_input_streaming). + // Beta fine-grained-tool-streaming-2025-05-14 — required for + // input_json_delta streaming on custom tools. + if tool.EagerInputStreaming != nil && *tool.EagerInputStreaming { + if !hasProvider || features.EagerInputStreaming { + headers = appendUniqueHeader(headers, AnthropicEagerInputStreamingBetaHeader) + } } // Check for cache control with scope if !hasCachingScope && tool.CacheControl != nil && tool.CacheControl.Scope != nil { @@ -415,12 +438,14 @@ var betaHeaderPrefixKnown = []string{ "context-management-", "files-api-", AnthropicAdvancedToolUseBetaHeaderPrefix, + AnthropicToolExamplesBetaHeaderPrefix, AnthropicInterleavedThinkingBetaHeaderPrefix, AnthropicSkillsBetaHeaderPrefix, AnthropicContext1MBetaHeaderPrefix, AnthropicFastModeBetaHeaderPrefix, AnthropicRedactThinkingBetaHeaderPrefix, AnthropicTaskBudgetsBetaHeaderPrefix, + AnthropicEagerInputStreamingBetaHeaderPrefix, } // betaHeaderPrefixExists checks if any header in existing shares a known prefix with newHeader. @@ -610,12 +635,14 @@ var betaHeaderPrefixToFeature = map[string]func(ProviderFeatureSupport) bool{ "context-management-": func(f ProviderFeatureSupport) bool { return f.ContextEditing }, "files-api-": func(f ProviderFeatureSupport) bool { return f.FilesAPI }, AnthropicAdvancedToolUseBetaHeaderPrefix: func(f ProviderFeatureSupport) bool { return f.AdvancedToolUse }, + AnthropicToolExamplesBetaHeaderPrefix: func(f ProviderFeatureSupport) bool { return f.InputExamples }, AnthropicInterleavedThinkingBetaHeaderPrefix: func(f ProviderFeatureSupport) bool { return f.InterleavedThinking }, AnthropicSkillsBetaHeaderPrefix: func(f ProviderFeatureSupport) bool { return f.Skills }, AnthropicContext1MBetaHeaderPrefix: func(f ProviderFeatureSupport) bool { return f.Context1M }, AnthropicFastModeBetaHeaderPrefix: func(f ProviderFeatureSupport) bool { return f.FastMode }, AnthropicRedactThinkingBetaHeaderPrefix: func(f ProviderFeatureSupport) bool { return f.RedactThinking }, AnthropicTaskBudgetsBetaHeaderPrefix: func(f ProviderFeatureSupport) bool { return f.TaskBudgets }, + AnthropicEagerInputStreamingBetaHeaderPrefix: func(f ProviderFeatureSupport) bool { return f.EagerInputStreaming }, } // MergeBetaHeaders collects anthropic-beta values from provider ExtraHeaders and @@ -1104,7 +1131,7 @@ func ConvertToAnthropicImageBlock(block schemas.ChatContentBlock) AnthropicConte imageBlock := AnthropicContentBlock{ Type: AnthropicContentBlockTypeImage, CacheControl: block.CacheControl, - Source: &AnthropicSource{}, + Source: &AnthropicBlockSource{SourceObj: &AnthropicSource{}}, } if block.ImageURLStruct == nil { @@ -1115,8 +1142,8 @@ func ConvertToAnthropicImageBlock(block schemas.ChatContentBlock) AnthropicConte sanitizedURL, err := schemas.SanitizeImageURL(block.ImageURLStruct.URL) if err != nil { // Best-effort: treat as a regular URL without sanitization - imageBlock.Source.Type = "url" - imageBlock.Source.URL = &block.ImageURLStruct.URL + imageBlock.Source.SourceObj.Type = "url" + imageBlock.Source.SourceObj.URL = &block.ImageURLStruct.URL return imageBlock } urlTypeInfo := schemas.ExtractURLTypeInfo(sanitizedURL) @@ -1137,18 +1164,18 @@ func ConvertToAnthropicImageBlock(block schemas.ChatContentBlock) AnthropicConte // Convert to Anthropic source format if formattedImgContent.Type == schemas.ImageContentTypeURL { - imageBlock.Source.Type = "url" - imageBlock.Source.URL = &formattedImgContent.URL + imageBlock.Source.SourceObj.Type = "url" + imageBlock.Source.SourceObj.URL = &formattedImgContent.URL } else { if formattedImgContent.MediaType != "" { - imageBlock.Source.MediaType = &formattedImgContent.MediaType + imageBlock.Source.SourceObj.MediaType = &formattedImgContent.MediaType } - imageBlock.Source.Type = "base64" + imageBlock.Source.SourceObj.Type = "base64" // Use the base64 data without the data URL prefix if urlTypeInfo.DataURLWithoutPrefix != nil { - imageBlock.Source.Data = urlTypeInfo.DataURLWithoutPrefix + imageBlock.Source.SourceObj.Data = urlTypeInfo.DataURLWithoutPrefix } else { - imageBlock.Source.Data = &formattedImgContent.URL + imageBlock.Source.SourceObj.Data = &formattedImgContent.URL } } @@ -1160,7 +1187,7 @@ func ConvertToAnthropicDocumentBlock(block schemas.ChatContentBlock) AnthropicCo documentBlock := AnthropicContentBlock{ Type: AnthropicContentBlockTypeDocument, CacheControl: block.CacheControl, - Source: &AnthropicSource{}, + Source: &AnthropicBlockSource{SourceObj: &AnthropicSource{}}, } if block.Citations != nil { @@ -1180,8 +1207,8 @@ func ConvertToAnthropicDocumentBlock(block schemas.ChatContentBlock) AnthropicCo // Handle file URL if file.FileURL != nil && *file.FileURL != "" { - documentBlock.Source.Type = "url" - documentBlock.Source.URL = file.FileURL + documentBlock.Source.SourceObj.Type = "url" + documentBlock.Source.SourceObj.URL = file.FileURL return documentBlock } @@ -1191,8 +1218,8 @@ func ConvertToAnthropicDocumentBlock(block schemas.ChatContentBlock) AnthropicCo // Check if it's plain text based on file type if file.FileType != nil && (*file.FileType == "text/plain" || *file.FileType == "txt") { - documentBlock.Source.Type = "text" - documentBlock.Source.Data = &fileData + documentBlock.Source.SourceObj.Type = "text" + documentBlock.Source.SourceObj.Data = &fileData return documentBlock } @@ -1201,30 +1228,30 @@ func ConvertToAnthropicDocumentBlock(block schemas.ChatContentBlock) AnthropicCo if urlTypeInfo.DataURLWithoutPrefix != nil { // It's a data URL, extract the base64 content - documentBlock.Source.Type = "base64" - documentBlock.Source.Data = urlTypeInfo.DataURLWithoutPrefix + documentBlock.Source.SourceObj.Type = "base64" + documentBlock.Source.SourceObj.Data = urlTypeInfo.DataURLWithoutPrefix // Set media type from data URL or file type if urlTypeInfo.MediaType != nil { - documentBlock.Source.MediaType = urlTypeInfo.MediaType + documentBlock.Source.SourceObj.MediaType = urlTypeInfo.MediaType } else if file.FileType != nil { - documentBlock.Source.MediaType = file.FileType + documentBlock.Source.SourceObj.MediaType = file.FileType } return documentBlock } } // Default to base64 for binary files - documentBlock.Source.Type = "base64" - documentBlock.Source.Data = &fileData + documentBlock.Source.SourceObj.Type = "base64" + documentBlock.Source.SourceObj.Data = &fileData // Set media type if file.FileType != nil { - documentBlock.Source.MediaType = file.FileType + documentBlock.Source.SourceObj.MediaType = file.FileType } else { // Default to PDF if not specified mediaType := "application/pdf" - documentBlock.Source.MediaType = &mediaType + documentBlock.Source.SourceObj.MediaType = &mediaType } return documentBlock } @@ -1237,7 +1264,7 @@ func ConvertResponsesFileBlockToAnthropic(fileBlock *schemas.ResponsesInputMessa documentBlock := AnthropicContentBlock{ Type: AnthropicContentBlockTypeDocument, CacheControl: cacheControl, - Source: &AnthropicSource{}, + Source: &AnthropicBlockSource{SourceObj: &AnthropicSource{}}, } if citations != nil { @@ -1259,9 +1286,9 @@ func ConvertResponsesFileBlockToAnthropic(fileBlock *schemas.ResponsesInputMessa // Check if it's plain text based on file type if fileBlock.FileType != nil && (*fileBlock.FileType == "text/plain" || *fileBlock.FileType == "txt") { - documentBlock.Source.Type = "text" - documentBlock.Source.Data = &fileData - documentBlock.Source.MediaType = schemas.Ptr("text/plain") + documentBlock.Source.SourceObj.Type = "text" + documentBlock.Source.SourceObj.Data = &fileData + documentBlock.Source.SourceObj.MediaType = schemas.Ptr("text/plain") return documentBlock } @@ -1271,38 +1298,38 @@ func ConvertResponsesFileBlockToAnthropic(fileBlock *schemas.ResponsesInputMessa if urlTypeInfo.DataURLWithoutPrefix != nil { // It's a data URL, extract the base64 content - documentBlock.Source.Type = "base64" - documentBlock.Source.Data = urlTypeInfo.DataURLWithoutPrefix + documentBlock.Source.SourceObj.Type = "base64" + documentBlock.Source.SourceObj.Data = urlTypeInfo.DataURLWithoutPrefix // Set media type from data URL or file type if urlTypeInfo.MediaType != nil { - documentBlock.Source.MediaType = urlTypeInfo.MediaType + documentBlock.Source.SourceObj.MediaType = urlTypeInfo.MediaType } else if fileBlock.FileType != nil { - documentBlock.Source.MediaType = fileBlock.FileType + documentBlock.Source.SourceObj.MediaType = fileBlock.FileType } return documentBlock } } // Default to base64 for binary files (raw base64 without prefix) - documentBlock.Source.Type = "base64" - documentBlock.Source.Data = &fileData + documentBlock.Source.SourceObj.Type = "base64" + documentBlock.Source.SourceObj.Data = &fileData // Set media type if fileBlock.FileType != nil { - documentBlock.Source.MediaType = fileBlock.FileType + documentBlock.Source.SourceObj.MediaType = fileBlock.FileType } else { // Default to PDF if not specified mediaType := "application/pdf" - documentBlock.Source.MediaType = &mediaType + documentBlock.Source.SourceObj.MediaType = &mediaType } return documentBlock } // Handle file URL if fileBlock.FileURL != nil && *fileBlock.FileURL != "" { - documentBlock.Source.Type = "url" - documentBlock.Source.URL = fileBlock.FileURL + documentBlock.Source.SourceObj.Type = "url" + documentBlock.Source.SourceObj.URL = fileBlock.FileURL return documentBlock } @@ -1319,22 +1346,24 @@ func (block AnthropicContentBlock) ToBifrostContentImageBlock() schemas.ChatCont } func getImageURLFromBlock(block AnthropicContentBlock) string { - if block.Source == nil { + // Image blocks always carry object-form sources (never string form). + if block.Source == nil || block.Source.SourceObj == nil { return "" } + src := block.Source.SourceObj // Handle base64 data - convert to data URL - if block.Source.Data != nil { + if src.Data != nil { mime := "image/png" - if block.Source.MediaType != nil && *block.Source.MediaType != "" { - mime = *block.Source.MediaType + if src.MediaType != nil && *src.MediaType != "" { + mime = *src.MediaType } - return "data:" + mime + ";base64," + *block.Source.Data + return "data:" + mime + ";base64," + *src.Data } // Handle regular URLs - if block.Source.URL != nil { - return *block.Source.URL + if src.URL != nil { + return *src.URL } return "" diff --git a/core/providers/anthropic/utils_test.go b/core/providers/anthropic/utils_test.go index 5c37b2b168..8c92ab4cf1 100644 --- a/core/providers/anthropic/utils_test.go +++ b/core/providers/anthropic/utils_test.go @@ -797,6 +797,58 @@ func TestAddMissingBetaHeadersToContext_PerProvider(t *testing.T) { }, unexpectHeaders: []string{AnthropicFastModeBetaHeader}, }, + // Fine-grained tool streaming (eager_input_streaming) — per Table 20: + // GA on Anthropic / Bedrock / Vertex, Beta on Azure. All four should + // auto-inject fine-grained-tool-streaming-2025-05-14 when a tool has + // eager_input_streaming: true. + { + name: "Anthropic gets eager_input_streaming header", + provider: schemas.Anthropic, + req: &AnthropicMessageRequest{ + Tools: []AnthropicTool{{Name: "t1", EagerInputStreaming: schemas.Ptr(true)}}, + }, + expectHeaders: []string{AnthropicEagerInputStreamingBetaHeader}, + }, + { + name: "Bedrock gets eager_input_streaming header", + provider: schemas.Bedrock, + req: &AnthropicMessageRequest{ + Tools: []AnthropicTool{{Name: "t1", EagerInputStreaming: schemas.Ptr(true)}}, + }, + expectHeaders: []string{AnthropicEagerInputStreamingBetaHeader}, + }, + { + name: "Vertex gets eager_input_streaming header", + provider: schemas.Vertex, + req: &AnthropicMessageRequest{ + Tools: []AnthropicTool{{Name: "t1", EagerInputStreaming: schemas.Ptr(true)}}, + }, + expectHeaders: []string{AnthropicEagerInputStreamingBetaHeader}, + }, + { + name: "Azure gets eager_input_streaming header", + provider: schemas.Azure, + req: &AnthropicMessageRequest{ + Tools: []AnthropicTool{{Name: "t1", EagerInputStreaming: schemas.Ptr(true)}}, + }, + expectHeaders: []string{AnthropicEagerInputStreamingBetaHeader}, + }, + { + name: "eager_input_streaming header absent when flag is false", + provider: schemas.Anthropic, + req: &AnthropicMessageRequest{ + Tools: []AnthropicTool{{Name: "t1", EagerInputStreaming: schemas.Ptr(false)}}, + }, + unexpectHeaders: []string{AnthropicEagerInputStreamingBetaHeader}, + }, + { + name: "eager_input_streaming header absent when unset", + provider: schemas.Anthropic, + req: &AnthropicMessageRequest{ + Tools: []AnthropicTool{{Name: "t1"}}, + }, + unexpectHeaders: []string{AnthropicEagerInputStreamingBetaHeader}, + }, } for _, tt := range tests { @@ -998,6 +1050,7 @@ func TestFilterBetaHeadersForProvider(t *testing.T) { AnthropicContextManagementBetaHeader, AnthropicInterleavedThinkingBetaHeader, AnthropicContext1MBetaHeader, + AnthropicEagerInputStreamingBetaHeader, } result := FilterBetaHeadersForProvider(supported, schemas.Vertex) if len(result) != len(supported) { @@ -1049,6 +1102,7 @@ func TestFilterBetaHeadersForProvider(t *testing.T) { AnthropicSkillsBetaHeader, AnthropicContext1MBetaHeader, AnthropicRedactThinkingBetaHeader, + AnthropicEagerInputStreamingBetaHeader, } result := FilterBetaHeadersForProvider(supported, schemas.Azure) if len(result) != len(supported) { @@ -1064,6 +1118,7 @@ func TestFilterBetaHeadersForProvider(t *testing.T) { AnthropicContextManagementBetaHeader, AnthropicInterleavedThinkingBetaHeader, AnthropicContext1MBetaHeader, + AnthropicEagerInputStreamingBetaHeader, } result := FilterBetaHeadersForProvider(supported, schemas.Bedrock) if len(result) != len(supported) { diff --git a/core/providers/azure/azure_test.go b/core/providers/azure/azure_test.go index b1080b7db3..5727cb343d 100644 --- a/core/providers/azure/azure_test.go +++ b/core/providers/azure/azure_test.go @@ -78,8 +78,10 @@ func TestAzure(t *testing.T) { VideoRemix: false, VideoList: false, VideoDelete: false, - InterleavedThinking: true, - PassthroughAPI: true, + InterleavedThinking: true, + PassthroughAPI: true, + EagerInputStreaming: true, // fine-grained-tool-streaming-2025-05-14 (Beta on Azure Foundry) + ServerToolsViaOpenAIEndpoint: true, // web_search / web_fetch / code_execution on Azure per Table 20 }, DisableParallelFor: []string{"Transcription"}, // Azure Whisper has 3 calls/minute quota } diff --git a/core/providers/bedrock/bedrock_test.go b/core/providers/bedrock/bedrock_test.go index 8bfe81df3e..6c4168c6f2 100644 --- a/core/providers/bedrock/bedrock_test.go +++ b/core/providers/bedrock/bedrock_test.go @@ -228,6 +228,7 @@ func TestBedrock(t *testing.T) { ImageVariation: true, StructuredOutputs: true, InterleavedThinking: true, + EagerInputStreaming: true, // fine-grained-tool-streaming-2025-05-14 (per B-header) }, } diff --git a/core/providers/vertex/vertex_test.go b/core/providers/vertex/vertex_test.go index 7203bf3080..03baf347fa 100644 --- a/core/providers/vertex/vertex_test.go +++ b/core/providers/vertex/vertex_test.go @@ -68,8 +68,10 @@ func TestVertex(t *testing.T) { PromptCaching: true, ListModels: false, CountTokens: true, - StructuredOutputs: true, // Structured outputs with nullable enum support - InterleavedThinking: true, + StructuredOutputs: true, // Structured outputs with nullable enum support + InterleavedThinking: true, + EagerInputStreaming: true, // fine-grained-tool-streaming-2025-05-14 (GA on Vertex) + ServerToolsViaOpenAIEndpoint: true, // web_search only on Vertex per Table 20 (web_fetch/code_execution skip) }, } diff --git a/core/schemas/chatcompletions.go b/core/schemas/chatcompletions.go index a381ef1220..10d73df249 100644 --- a/core/schemas/chatcompletions.go +++ b/core/schemas/chatcompletions.go @@ -314,6 +314,14 @@ const ( ChatToolTypeCustom ChatToolType = "custom" ) +type MCPToolAnnotations struct { + Title string `json:"title,omitempty"` // Human-readable title for the tool + ReadOnlyHint *bool `json:"readOnlyHint,omitempty"` // If true, the tool does not modify its environment + DestructiveHint *bool `json:"destructiveHint,omitempty"` // If true, the tool may perform destructive updates + IdempotentHint *bool `json:"idempotentHint,omitempty"` // If true, repeated calls with same args have no additional effect + OpenWorldHint *bool `json:"openWorldHint,omitempty"` // If true, the tool interacts with external entities +} + // ChatTool represents a tool definition. type ChatTool struct { Type ChatToolType `json:"type"` @@ -321,17 +329,12 @@ type ChatTool struct { Custom *ChatToolCustom `json:"custom,omitempty"` // Custom tool definition CacheControl *CacheControl `json:"cache_control,omitempty"` // Cache control for the tool Annotations *MCPToolAnnotations `json:"-"` // MCP tool annotations (Bifrost-internal, never forwarded to providers) -} -// MCPToolAnnotations carries optional MCP spec hints describing tool behavior. -// These are forwarded as-is from the MCP server and help agents make reasoning decisions -// (e.g. distinguishing read-only vs. mutating tools). -type MCPToolAnnotations struct { - Title string `json:"title,omitempty"` // Human-readable title for the tool - ReadOnlyHint *bool `json:"readOnlyHint,omitempty"` // If true, the tool does not modify its environment - DestructiveHint *bool `json:"destructiveHint,omitempty"` // If true, the tool may perform destructive updates - IdempotentHint *bool `json:"idempotentHint,omitempty"` // If true, repeated calls with same args have no additional effect - OpenWorldHint *bool `json:"openWorldHint,omitempty"` // If true, the tool interacts with external entities + // EagerInputStreaming enables fine-grained tool input streaming + // (Anthropic fine-grained-tool-streaming-2025-05-14). On Anthropic-family + // providers the model emits input_json_delta events before full arguments + // are determined. Silently ignored on providers that don't support it. + EagerInputStreaming *bool `json:"eager_input_streaming,omitempty"` } // ChatToolFunction represents a function definition. diff --git a/transports/bifrost-http/lib/validator_test.go b/transports/bifrost-http/lib/validator_test.go index afac1d4975..e0cbcd8e4b 100644 --- a/transports/bifrost-http/lib/validator_test.go +++ b/transports/bifrost-http/lib/validator_test.go @@ -680,17 +680,15 @@ func TestValidateConfigSchema_MCPClientConfig_Valid_Stdio(t *testing.T) { } } -func TestValidateConfigSchema_MCPClientConfig_Valid_Websocket(t *testing.T) { - // Valid MCP client config with websocket connection type +func TestValidateConfigSchema_MCPClientConfig_Valid_Sse(t *testing.T) { + // Valid MCP client config with sse connection type validConfig := `{ "mcp": { "client_configs": [ { "name": "my-mcp-client", - "connection_type": "websocket", - "websocket_config": { - "url": "ws://localhost:8080" - } + "connection_type": "sse", + "connection_string": "http://localhost:8080" } ] } @@ -698,7 +696,7 @@ func TestValidateConfigSchema_MCPClientConfig_Valid_Websocket(t *testing.T) { err := ValidateConfigSchema([]byte(validConfig), loadLocalSchema(t)) if err != nil { - t.Errorf("expected valid MCP client config (websocket) to pass validation, got error: %v", err) + t.Errorf("expected valid MCP client config (sse) to pass validation, got error: %v", err) } } @@ -710,9 +708,7 @@ func TestValidateConfigSchema_MCPClientConfig_Valid_Http(t *testing.T) { { "name": "my-mcp-client", "connection_type": "http", - "http_config": { - "url": "http://localhost:8080" - } + "connection_string": "http://localhost:8080" } ] } @@ -1202,7 +1198,7 @@ func TestValidateConfigSchema_OtelPlugin_Valid(t *testing.T) { "name": "otel", "config": { "collector_url": "http://localhost:4318", - "trace_type": "otel", + "trace_type": "genai_extension", "protocol": "http" } } @@ -1223,7 +1219,7 @@ func TestValidateConfigSchema_OtelPlugin_MissingCollectorUrl(t *testing.T) { "enabled": true, "name": "otel", "config": { - "trace_type": "otel", + "trace_type": "genai_extension", "protocol": "http" } } @@ -1266,7 +1262,7 @@ func TestValidateConfigSchema_OtelPlugin_MissingProtocol(t *testing.T) { "name": "otel", "config": { "collector_url": "http://localhost:4318", - "trace_type": "otel" + "trace_type": "genai_extension" } } ]