diff --git a/core/changelog.md b/core/changelog.md index e69de29bb..c38bb97eb 100644 --- a/core/changelog.md +++ b/core/changelog.md @@ -0,0 +1,2 @@ +- fix: image url and input audio handling in gemini chat converters +- fix: support both responseJsonSchema and responseSchema for JSON response formatting in gemini \ No newline at end of file diff --git a/core/internal/testutil/account.go b/core/internal/testutil/account.go index 688db69fa..826e20953 100644 --- a/core/internal/testutil/account.go +++ b/core/internal/testutil/account.go @@ -250,7 +250,7 @@ func (account *ComprehensiveTestAccount) GetKeysForProvider(ctx *context.Context return []schemas.Key{ { Value: os.Getenv("VERTEX_API_KEY"), - Models: []string{}, + Models: []string{"text-multilingual-embedding-002", "google/gemini-2.0-flash-001"}, Weight: 1.0, VertexKeyConfig: &schemas.VertexKeyConfig{ ProjectID: os.Getenv("VERTEX_PROJECT_ID"), @@ -259,6 +259,17 @@ func (account *ComprehensiveTestAccount) GetKeysForProvider(ctx *context.Context }, UseForBatchAPI: bifrost.Ptr(true), }, + { + Value: os.Getenv("VERTEX_API_KEY"), + Models: []string{"claude-sonnet-4-5", "claude-4.5-haiku", "claude-opus-4-5"}, + Weight: 1.0, + VertexKeyConfig: &schemas.VertexKeyConfig{ + ProjectID: os.Getenv("VERTEX_PROJECT_ID"), + Region: getEnvWithDefault("VERTEX_REGION_ANTHROPIC", "us-east5"), + AuthCredentials: os.Getenv("VERTEX_CREDENTIALS"), + }, + UseForBatchAPI: bifrost.Ptr(true), + }, }, nil case schemas.Mistral: return []schemas.Key{ diff --git a/core/internal/testutil/image_base64.go b/core/internal/testutil/image_base64.go index b11265319..a4ba88c64 100644 --- a/core/internal/testutil/image_base64.go +++ b/core/internal/testutil/image_base64.go @@ -6,7 +6,6 @@ import ( "strings" "testing" - bifrost "github.com/maximhq/bifrost/core" "github.com/maximhq/bifrost/core/schemas" ) @@ -72,7 +71,7 @@ func RunImageBase64Test(t *testing.T, client *bifrost.Bifrost, ctx context.Conte Model: testConfig.VisionModel, Input: chatMessages, Params: &schemas.ChatParameters{ - MaxCompletionTokens: bifrost.Ptr(200), + MaxCompletionTokens: bifrost.Ptr(500), }, Fallbacks: testConfig.Fallbacks, } @@ -85,7 +84,7 @@ func RunImageBase64Test(t *testing.T, client *bifrost.Bifrost, ctx context.Conte Model: testConfig.VisionModel, Input: responsesMessages, Params: &schemas.ResponsesParameters{ - MaxOutputTokens: bifrost.Ptr(200), + MaxOutputTokens: bifrost.Ptr(500), }, Fallbacks: testConfig.Fallbacks, } @@ -146,12 +145,13 @@ func validateBase64ImageContent(t *testing.T, content string, apiName string) { strings.Contains(lowerContent, "cat") || strings.Contains(lowerContent, "feline") if len(content) < 10 { - t.Logf("⚠️ %s response seems quite short for image description: %s", apiName, content) - } else if foundAnimal { - t.Logf("✅ %s vision model successfully identified animal in base64 image", apiName) - } else { - t.Logf("✅ %s vision model processed base64 image but may not have clearly identified the animal", apiName) + t.Fatalf("❌ %s response too short for image description: %s", apiName, content) + } + + if !foundAnimal { + t.Fatalf("❌ %s vision model failed to identify any animal in base64 image: %s", apiName, content) } + t.Logf("✅ %s vision model successfully identified animal in base64 image", apiName) t.Logf("✅ %s lion base64 image processing completed: %s", apiName, content) } diff --git a/core/providers/anthropic/responses.go b/core/providers/anthropic/responses.go index 1c8803831..1e2403eba 100644 --- a/core/providers/anthropic/responses.go +++ b/core/providers/anthropic/responses.go @@ -3462,7 +3462,10 @@ func (block AnthropicContentBlock) toBifrostResponsesDocumentBlock() schemas.Res if block.Source.MediaType != nil { mediaType = *block.Source.MediaType } - dataURL := "data:" + mediaType + ";base64," + *block.Source.Data + dataURL := *block.Source.Data + if !strings.HasPrefix(dataURL, "data:") { + dataURL = "data:" + mediaType + ";base64," + *block.Source.Data + } resultBlock.ResponsesInputMessageContentBlockFile.FileData = &dataURL } case "text": diff --git a/core/providers/bedrock/responses.go b/core/providers/bedrock/responses.go index 8d0c13965..6478d5c7c 100644 --- a/core/providers/bedrock/responses.go +++ b/core/providers/bedrock/responses.go @@ -2728,9 +2728,9 @@ func convertSingleBedrockMessageToBifrostMessages(ctx *context.Context, msg *Bed fileBlock.ResponsesInputMessageContentBlockFile.Filename = &block.Document.Name } + fileType := "application/pdf" // Set file type based on format if block.Document.Format != "" { - var fileType string switch block.Document.Format { case "pdf": fileType = "application/pdf" @@ -2749,7 +2749,11 @@ func convertSingleBedrockMessageToBifrostMessages(ctx *context.Context, msg *Bed fileBlock.ResponsesInputMessageContentBlockFile.FileData = block.Document.Source.Text } else if block.Document.Source.Bytes != nil { // Base64 encoded bytes (PDF) - fileBlock.ResponsesInputMessageContentBlockFile.FileData = block.Document.Source.Bytes + fileDataURL := *block.Document.Source.Bytes + if !strings.HasPrefix(fileDataURL, "data:") { + fileDataURL = fmt.Sprintf("data:%s;base64,%s", fileType, fileDataURL) + } + fileBlock.ResponsesInputMessageContentBlockFile.FileData = &fileDataURL } } diff --git a/core/providers/gemini/chat.go b/core/providers/gemini/chat.go index 9e310abf0..bd5fef5f1 100644 --- a/core/providers/gemini/chat.go +++ b/core/providers/gemini/chat.go @@ -57,7 +57,11 @@ func ToGeminiChatCompletionRequest(bifrostReq *schemas.BifrostChatRequest) *Gemi } // Convert chat completion messages to Gemini format - geminiReq.Contents = convertBifrostMessagesToGemini(bifrostReq.Input) + contents, systemInstruction := convertBifrostMessagesToGemini(bifrostReq.Input) + if systemInstruction != nil { + geminiReq.SystemInstruction = systemInstruction + } + geminiReq.Contents = contents return geminiReq } diff --git a/core/providers/gemini/gemini_test.go b/core/providers/gemini/gemini_test.go index 6e99f48fd..2a8b12a39 100644 --- a/core/providers/gemini/gemini_test.go +++ b/core/providers/gemini/gemini_test.go @@ -31,7 +31,7 @@ func TestGemini(t *testing.T) { Fallbacks: []schemas.Fallback{ {Provider: schemas.Gemini, Model: "gemini-2.5-flash"}, }, - VisionModel: "gemini-2.0-flash", + VisionModel: "gemini-2.5-flash", EmbeddingModel: "text-embedding-004", TranscriptionModel: "gemini-2.5-flash", SpeechSynthesisModel: "gemini-2.5-flash-preview-tts", diff --git a/core/providers/gemini/responses.go b/core/providers/gemini/responses.go index 1a0fd646d..87d6a8c07 100644 --- a/core/providers/gemini/responses.go +++ b/core/providers/gemini/responses.go @@ -726,7 +726,7 @@ func (response *GenerateContentResponse) ToBifrostResponsesStream(sequenceNumber // Check for finish reason (indicates end of generation) // Only close if we've actually started emitting content (text, tool calls, etc.) // This prevents emitting response.completed for empty chunks with just finishReason - if candidate.FinishReason != "" && (state.HasStartedText || state.HasStartedToolCall) { + if candidate.FinishReason != "" && len(state.ItemIDs) > 0 { // Close any open items closeResponses := closeGeminiOpenItems(state, response.UsageMetadata, sequenceNumber+len(responses)) responses = append(responses, closeResponses...) @@ -1578,18 +1578,27 @@ func convertGeminiInlineDataToContentBlock(blob *Blob) *schemas.ResponsesMessage } } - // Handle other files - encodedData := base64.StdEncoding.EncodeToString(blob.Data) + // Handle other files - format as data URL + mimeTypeForFile := mimeType + if mimeTypeForFile == "" { + mimeTypeForFile = "application/pdf" + } + + filename := blob.DisplayName + if filename == "" { + filename = "unnamed_file" + } + + fileDataURL := base64.StdEncoding.EncodeToString(blob.Data) + if !strings.HasPrefix(fileDataURL, "data:") { + fileDataURL = fmt.Sprintf("data:%s;base64,%s", mimeTypeForFile, fileDataURL) + } return &schemas.ResponsesMessageContentBlock{ Type: schemas.ResponsesInputMessageContentBlockTypeFile, ResponsesInputMessageContentBlockFile: &schemas.ResponsesInputMessageContentBlockFile{ - FileData: &encodedData, - FileType: func() *string { - if blob.MIMEType != "" { - return &blob.MIMEType - } - return nil - }(), + FileData: &fileDataURL, + FileType: &mimeTypeForFile, + Filename: &filename, }, } } diff --git a/core/providers/gemini/utils.go b/core/providers/gemini/utils.go index eaf62d869..b50d0af3c 100644 --- a/core/providers/gemini/utils.go +++ b/core/providers/gemini/utils.go @@ -772,10 +772,36 @@ func addSpeechConfigToGenerationConfig(config *GenerationConfig, voiceConfig *sc } // convertBifrostMessagesToGemini converts Bifrost messages to Gemini format -func convertBifrostMessagesToGemini(messages []schemas.ChatMessage) []Content { +func convertBifrostMessagesToGemini(messages []schemas.ChatMessage) ([]Content, *Content) { var contents []Content + var systemInstruction *Content for _, message := range messages { + // Handle system messages separately - Gemini requires them in SystemInstruction field + if message.Role == schemas.ChatMessageRoleSystem { + if systemInstruction == nil { + systemInstruction = &Content{} + } + + // Extract system message content + if message.Content != nil { + if message.Content.ContentStr != nil && *message.Content.ContentStr != "" { + systemInstruction.Parts = append(systemInstruction.Parts, &Part{ + Text: *message.Content.ContentStr, + }) + } else if message.Content.ContentBlocks != nil { + for _, block := range message.Content.ContentBlocks { + if block.Text != nil && *block.Text != "" { + systemInstruction.Parts = append(systemInstruction.Parts, &Part{ + Text: *block.Text, + }) + } + } + } + } + continue + } + var parts []*Part // Handle content @@ -826,8 +852,74 @@ func convertBifrostMessagesToGemini(messages []schemas.ChatMessage) []Content { }) } } + } else if block.ImageURLStruct != nil { + // Handle image blocks + imageURL := block.ImageURLStruct.URL + + // Sanitize and parse the image URL + sanitizedURL, err := schemas.SanitizeImageURL(imageURL) + if err != nil { + // Skip this block if URL is invalid + continue + } + + urlInfo := schemas.ExtractURLTypeInfo(sanitizedURL) + + // Determine MIME type + mimeType := "image/jpeg" // default + if urlInfo.MediaType != nil { + mimeType = *urlInfo.MediaType + } + + if urlInfo.Type == schemas.ImageContentTypeBase64 { + // Data URL - convert to InlineData (Blob) + if urlInfo.DataURLWithoutPrefix != nil { + decodedData, err := base64.StdEncoding.DecodeString(*urlInfo.DataURLWithoutPrefix) + if err == nil && len(decodedData) > 0 { + parts = append(parts, &Part{ + InlineData: &Blob{ + MIMEType: mimeType, + Data: decodedData, + }, + }) + } + } + } else { + // Regular URL - use FileData + parts = append(parts, &Part{ + FileData: &FileData{ + MIMEType: mimeType, + FileURI: sanitizedURL, + }, + }) + } + } else if block.InputAudio != nil { + // Decode the audio data (already base64 encoded in the schema) + decodedData, err := base64.StdEncoding.DecodeString(block.InputAudio.Data) + if err != nil || len(decodedData) == 0 { + continue + } + + // Determine MIME type + mimeType := "audio/mpeg" // default + if block.InputAudio.Format != nil { + format := strings.ToLower(strings.TrimSpace(*block.InputAudio.Format)) + if format != "" { + if strings.HasPrefix(format, "audio/") { + mimeType = format + } else { + mimeType = "audio/" + format + } + } + } + + parts = append(parts, &Part{ + InlineData: &Blob{ + MIMEType: mimeType, + Data: decodedData, + }, + }) } - // Handle other content block types as needed } } } @@ -945,7 +1037,7 @@ func convertBifrostMessagesToGemini(messages []schemas.ChatMessage) []Content { } } - return contents + return contents, systemInstruction } // normalizeSchemaTypes recursively normalizes type values from uppercase to lowercase diff --git a/core/providers/vertex/vertex_test.go b/core/providers/vertex/vertex_test.go index 8060e2414..12224ed7d 100644 --- a/core/providers/vertex/vertex_test.go +++ b/core/providers/vertex/vertex_test.go @@ -25,10 +25,10 @@ func TestVertex(t *testing.T) { testConfig := testutil.ComprehensiveTestConfig{ Provider: schemas.Vertex, ChatModel: "google/gemini-2.0-flash-001", - VisionModel: "gemini-2.0-flash-001", + VisionModel: "claude-sonnet-4-5", TextModel: "", // Vertex doesn't support text completion in newer models EmbeddingModel: "text-multilingual-embedding-002", - ReasoningModel: "claude-4.5-haiku", + ReasoningModel: "claude-opus-4-5", Scenarios: testutil.TestScenarios{ TextCompletion: false, // Not supported SimpleChat: true, @@ -39,13 +39,13 @@ func TestVertex(t *testing.T) { MultipleToolCalls: true, End2EndToolCalling: true, AutomaticFunctionCall: true, - ImageURL: true, + ImageURL: false, ImageBase64: true, - MultipleImages: true, + MultipleImages: false, CompleteEnd2End: true, FileBase64: true, Embedding: true, - Reasoning: false, // Not supported right now because we are not using native gemini converters + Reasoning: true, ListModels: false, CountTokens: true, StructuredOutputs: true, // Structured outputs with nullable enum support diff --git a/core/version b/core/version index c3df28669..4d668e72f 100644 --- a/core/version +++ b/core/version @@ -1 +1 @@ -1.2.47 \ No newline at end of file +1.2.48 \ No newline at end of file diff --git a/framework/changelog.md b/framework/changelog.md index e69de29bb..ec5959c8f 100644 --- a/framework/changelog.md +++ b/framework/changelog.md @@ -0,0 +1 @@ +- chore: upgrades core to v1.2.48 \ No newline at end of file diff --git a/framework/version b/framework/version index 29c1447b4..5df3530cc 100644 --- a/framework/version +++ b/framework/version @@ -1 +1 @@ -1.1.59 \ No newline at end of file +1.1.60 \ No newline at end of file diff --git a/plugins/governance/changelog.md b/plugins/governance/changelog.md index e69de29bb..3c4869077 100644 --- a/plugins/governance/changelog.md +++ b/plugins/governance/changelog.md @@ -0,0 +1 @@ +- chore: upgrades core to v1.2.48 and framework to 1.1.60 \ No newline at end of file diff --git a/plugins/governance/version b/plugins/governance/version index 0629a7403..ee96f2fc4 100644 --- a/plugins/governance/version +++ b/plugins/governance/version @@ -1 +1 @@ -1.3.60 \ No newline at end of file +1.3.61 \ No newline at end of file diff --git a/plugins/jsonparser/changelog.md b/plugins/jsonparser/changelog.md index e69de29bb..3c4869077 100644 --- a/plugins/jsonparser/changelog.md +++ b/plugins/jsonparser/changelog.md @@ -0,0 +1 @@ +- chore: upgrades core to v1.2.48 and framework to 1.1.60 \ No newline at end of file diff --git a/plugins/jsonparser/version b/plugins/jsonparser/version index 0629a7403..ee96f2fc4 100644 --- a/plugins/jsonparser/version +++ b/plugins/jsonparser/version @@ -1 +1 @@ -1.3.60 \ No newline at end of file +1.3.61 \ No newline at end of file diff --git a/plugins/logging/changelog.md b/plugins/logging/changelog.md index e69de29bb..3c4869077 100644 --- a/plugins/logging/changelog.md +++ b/plugins/logging/changelog.md @@ -0,0 +1 @@ +- chore: upgrades core to v1.2.48 and framework to 1.1.60 \ No newline at end of file diff --git a/plugins/logging/version b/plugins/logging/version index 0629a7403..ee96f2fc4 100644 --- a/plugins/logging/version +++ b/plugins/logging/version @@ -1 +1 @@ -1.3.60 \ No newline at end of file +1.3.61 \ No newline at end of file diff --git a/plugins/maxim/changelog.md b/plugins/maxim/changelog.md index e69de29bb..3c4869077 100644 --- a/plugins/maxim/changelog.md +++ b/plugins/maxim/changelog.md @@ -0,0 +1 @@ +- chore: upgrades core to v1.2.48 and framework to 1.1.60 \ No newline at end of file diff --git a/plugins/maxim/version b/plugins/maxim/version index a6a0eccbc..60d7b0dfc 100644 --- a/plugins/maxim/version +++ b/plugins/maxim/version @@ -1 +1 @@ -1.4.61 \ No newline at end of file +1.4.62 \ No newline at end of file diff --git a/plugins/mocker/changelog.md b/plugins/mocker/changelog.md index e69de29bb..3c4869077 100644 --- a/plugins/mocker/changelog.md +++ b/plugins/mocker/changelog.md @@ -0,0 +1 @@ +- chore: upgrades core to v1.2.48 and framework to 1.1.60 \ No newline at end of file diff --git a/plugins/mocker/version b/plugins/mocker/version index f33f3c631..94f8697dc 100644 --- a/plugins/mocker/version +++ b/plugins/mocker/version @@ -1 +1 @@ -1.3.58 \ No newline at end of file +1.3.59 \ No newline at end of file diff --git a/plugins/otel/changelog.md b/plugins/otel/changelog.md index e69de29bb..3c4869077 100644 --- a/plugins/otel/changelog.md +++ b/plugins/otel/changelog.md @@ -0,0 +1 @@ +- chore: upgrades core to v1.2.48 and framework to 1.1.60 \ No newline at end of file diff --git a/plugins/otel/version b/plugins/otel/version index 1b5deead0..633e893b5 100644 --- a/plugins/otel/version +++ b/plugins/otel/version @@ -1 +1 @@ -1.0.59 \ No newline at end of file +1.0.60 \ No newline at end of file diff --git a/plugins/semanticcache/changelog.md b/plugins/semanticcache/changelog.md index e69de29bb..3c4869077 100644 --- a/plugins/semanticcache/changelog.md +++ b/plugins/semanticcache/changelog.md @@ -0,0 +1 @@ +- chore: upgrades core to v1.2.48 and framework to 1.1.60 \ No newline at end of file diff --git a/plugins/semanticcache/version b/plugins/semanticcache/version index 94f8697dc..0629a7403 100644 --- a/plugins/semanticcache/version +++ b/plugins/semanticcache/version @@ -1 +1 @@ -1.3.59 \ No newline at end of file +1.3.60 \ No newline at end of file diff --git a/plugins/telemetry/changelog.md b/plugins/telemetry/changelog.md index e69de29bb..3c4869077 100644 --- a/plugins/telemetry/changelog.md +++ b/plugins/telemetry/changelog.md @@ -0,0 +1 @@ +- chore: upgrades core to v1.2.48 and framework to 1.1.60 \ No newline at end of file diff --git a/plugins/telemetry/version b/plugins/telemetry/version index 94f8697dc..0629a7403 100644 --- a/plugins/telemetry/version +++ b/plugins/telemetry/version @@ -1 +1 @@ -1.3.59 \ No newline at end of file +1.3.60 \ No newline at end of file diff --git a/tests/integrations/pyproject.toml b/tests/integrations/pyproject.toml index 94f8f93f0..9a9340299 100644 --- a/tests/integrations/pyproject.toml +++ b/tests/integrations/pyproject.toml @@ -45,6 +45,7 @@ dependencies = [ "langchain>=1.1.0", "langchain-community>=0.4.1", "langchain-aws>=1.1.0", + "pytest-xdist>=3.8.0", ] [project.optional-dependencies] diff --git a/tests/integrations/tests/test_openai.py b/tests/integrations/tests/test_openai.py index f7f9275fa..e262ebdc1 100644 --- a/tests/integrations/tests/test_openai.py +++ b/tests/integrations/tests/test_openai.py @@ -1673,78 +1673,8 @@ def test_38_responses_reasoning(self, test_config, provider, model, vk_enabled): @skip_if_no_api_key("openai") @pytest.mark.parametrize("provider,model,vk_enabled", get_cross_provider_params_with_vk_for_scenario("thinking")) - def test_38a_responses_reasoning_streaming(self, test_config, provider, model, vk_enabled): - """Test Case 38a: Responses API with reasoning streaming""" - client = get_provider_openai_client(provider, vk_enabled=vk_enabled) - model_to_use = format_provider_model(provider, model) - - stream = client.responses.create( - model=model_to_use, - input=RESPONSES_REASONING_INPUT, - max_output_tokens=1200, - reasoning={ - "effort": "high", - }, - stream=True, - ) - - # Collect streaming content - content, chunk_count, tool_calls_detected, event_types = ( - collect_responses_streaming_content(stream, timeout=300) - ) - - # Validate streaming results - assert chunk_count > 0, "Should receive at least one chunk" - assert len(content) > 30, "Should receive substantial reasoning content" - assert not tool_calls_detected, "Reasoning test shouldn't have tool calls" - - # Validate mathematical reasoning content - content_lower = content.lower() - reasoning_keywords = [ - "train", - "meet", - "time", - "hour", - "pm", - "distance", - "speed", - "mile", - ] - - # Should mention at least some reasoning keywords - keyword_matches = sum(1 for keyword in reasoning_keywords if keyword in content_lower) - assert keyword_matches >= 3, ( - f"Streaming response should contain reasoning about trains problem. " - f"Found {keyword_matches} keywords out of {len(reasoning_keywords)}. " - f"Content: {content[:200]}..." - ) - - # Check for step-by-step reasoning indicators - step_indicators = [ - "step", - "first", - "then", - "next", - "calculate", - "therefore", - "because", - "since", - ] - - has_steps = any(indicator in content_lower for indicator in step_indicators) - assert ( - has_steps - ), f"Streaming response should show step-by-step reasoning. Content: {content[:200]}..." - - # Should have multiple chunks for streaming - assert chunk_count > 1, f"Streaming should have multiple chunks, got {chunk_count}" - - print(f"Success: Reasoning streaming test completed with {chunk_count} chunks") - - @skip_if_no_api_key("openai") - @pytest.mark.parametrize("provider,model,vk_enabled", get_cross_provider_params_with_vk_for_scenario("thinking")) - def test_38b_responses_reasoning_streaming_with_summary(self, test_config, provider, model, vk_enabled): - """Test Case 38b: Responses API with reasoning streaming and detailed summary""" + def test_38a_responses_reasoning_streaming_with_summary(self, test_config, provider, model, vk_enabled): + """Test Case 38a: Responses API with reasoning streaming and detailed summary""" client = get_provider_openai_client(provider, vk_enabled=vk_enabled) model_to_use = format_provider_model(provider, model) @@ -1762,7 +1692,7 @@ def test_38b_responses_reasoning_streaming_with_summary(self, test_config, provi # Collect streaming content content, chunk_count, tool_calls_detected, event_types = ( - collect_responses_streaming_content(stream, timeout=300) + collect_responses_streaming_content(stream, timeout=900) ) # Validate streaming results diff --git a/tests/integrations/tests/utils/common.py b/tests/integrations/tests/utils/common.py index 5f5914c7b..6ff7ca1ce 100644 --- a/tests/integrations/tests/utils/common.py +++ b/tests/integrations/tests/utils/common.py @@ -1970,7 +1970,7 @@ def collect_responses_streaming_content( content_parts.append(chunk.delta) # collect summary text deltas - if event_type == "response.summary_text.delta" and hasattr(chunk, "delta"): + if event_type == "response.reasoning_summary_text.delta" and hasattr(chunk, "delta") and chunk.delta: content_parts.append(chunk.delta) # Check for function calls diff --git a/tests/integrations/uv.lock b/tests/integrations/uv.lock index 9c2bbdd32..bef448eef 100644 --- a/tests/integrations/uv.lock +++ b/tests/integrations/uv.lock @@ -278,6 +278,7 @@ dependencies = [ { name = "pytest-mock" }, { name = "pytest-rerunfailures" }, { name = "pytest-timeout" }, + { name = "pytest-xdist" }, { name = "python-dotenv" }, { name = "pyyaml" }, { name = "requests" }, @@ -323,6 +324,7 @@ requires-dist = [ { name = "pytest-mock", specifier = ">=3.11.0" }, { name = "pytest-rerunfailures", specifier = ">=11.0" }, { name = "pytest-timeout", specifier = ">=2.1.0" }, + { name = "pytest-xdist", specifier = ">=3.8.0" }, { name = "python-dotenv", specifier = ">=1.0.0" }, { name = "pyyaml", specifier = ">=6.0" }, { name = "requests", specifier = ">=2.28.0" }, @@ -920,6 +922,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + [[package]] name = "executing" version = "2.2.1" @@ -4214,6 +4225,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, ] +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" diff --git a/transports/changelog.md b/transports/changelog.md index e69de29bb..ad68d5007 100644 --- a/transports/changelog.md +++ b/transports/changelog.md @@ -0,0 +1,3 @@ +- fix: image url and input audio handling in gemini chat converters +- fix: support both responseJsonSchema and responseSchema for JSON response formatting in gemini +- chore: upgrades core to v1.2.48 and framework to 1.1.60 \ No newline at end of file diff --git a/transports/version b/transports/version index 0629a7403..ee96f2fc4 100644 --- a/transports/version +++ b/transports/version @@ -1 +1 @@ -1.3.60 \ No newline at end of file +1.3.61 \ No newline at end of file