diff --git a/core/bifrost.go b/core/bifrost.go index e72d9c9723..f745ef6b67 100644 --- a/core/bifrost.go +++ b/core/bifrost.go @@ -1135,7 +1135,18 @@ func (bifrost *Bifrost) tryStreamRequest(req *schemas.BifrostRequest, ctx contex bifrost.releaseChannelMessage(msg) return stream, nil case bifrostErrVal := <-msg.Err: + bifrost.logger.Warn("error while executing stream request: %v", bifrostErrVal.Error.Message) + // Marking final chunk + ctx = context.WithValue(ctx, schemas.BifrostContextKeyStreamEndIndicator, true) + // On error we will complete post-hooks + recoveredResp, recoveredErr := pipeline.RunPostHooks(&ctx, nil, &bifrostErrVal, len(bifrost.plugins)) bifrost.releaseChannelMessage(msg) + if recoveredErr != nil { + return nil, recoveredErr + } + if recoveredResp != nil { + return newBifrostMessageChan(recoveredResp), nil + } return nil, &bifrostErrVal } } diff --git a/core/changelog.md b/core/changelog.md index a0e9b0d602..5ccb752e6a 100644 --- a/core/changelog.md +++ b/core/changelog.md @@ -1,2 +1,4 @@ - \ No newline at end of file + + +- Fix: Updates token calculation for streaming responses. #520 \ No newline at end of file diff --git a/core/providers/openai.go b/core/providers/openai.go index 9bae458024..2533456539 100644 --- a/core/providers/openai.go +++ b/core/providers/openai.go @@ -499,9 +499,24 @@ func handleOpenAIStreaming( } // Handle usage-only chunks (when stream_options include_usage is true) - if len(response.Choices) == 0 && response.Usage != nil { + if response.Usage != nil { // Collect usage information and send at the end of the stream - usage = response.Usage + // Here in some cases usage comes before final message + // So we need to check if the response.Usage is nil and then if usage != nil + // then add up all tokens + if response.Usage.PromptTokens > usage.PromptTokens { + usage.PromptTokens = response.Usage.PromptTokens + } + if response.Usage.CompletionTokens > usage.CompletionTokens { + usage.CompletionTokens = response.Usage.CompletionTokens + } + if response.Usage.TotalTokens > usage.TotalTokens { + usage.TotalTokens = response.Usage.TotalTokens + } + calculatedTotal := usage.PromptTokens + usage.CompletionTokens + if calculatedTotal > usage.TotalTokens { + usage.TotalTokens = calculatedTotal + } response.Usage = nil } diff --git a/core/version b/core/version index 36638c8584..54eae6b4d8 100644 --- a/core/version +++ b/core/version @@ -1 +1 @@ -1.1.37 +1.1.38 diff --git a/framework/changelog.md b/framework/changelog.md index 57e331e113..aeca76e348 100644 --- a/framework/changelog.md +++ b/framework/changelog.md @@ -1,4 +1,4 @@ -- Pricing module now accommodates nested model names i.e. groq/openai/gpt-oss-20b was getting skipped while computing costs. \ No newline at end of file +- upgrade: core upgrades to 1.1.38 \ No newline at end of file diff --git a/framework/logstore/sqlite.go b/framework/logstore/sqlite.go index e09ec7cf45..39f4f3de37 100644 --- a/framework/logstore/sqlite.go +++ b/framework/logstore/sqlite.go @@ -179,7 +179,7 @@ func (s *SQLiteLogStore) SearchLogs(filters SearchFilters, pagination Pagination } return nil, err } - + return &SearchResult{ Logs: logs, Pagination: pagination, diff --git a/framework/logstore/tables.go b/framework/logstore/tables.go index a815142ec4..6e6286963c 100644 --- a/framework/logstore/tables.go +++ b/framework/logstore/tables.go @@ -233,6 +233,7 @@ func (l *Log) SerializeFields() error { } if l.ErrorDetailsParsed != nil { + l.ErrorDetailsParsed.Error.Error = nil if data, err := json.Marshal(l.ErrorDetailsParsed); err != nil { return err } else { diff --git a/framework/version b/framework/version index 154b9fce5b..79728fe87f 100644 --- a/framework/version +++ b/framework/version @@ -1 +1 @@ -1.0.23 +1.0.24 diff --git a/plugins/governance/changelog.md b/plugins/governance/changelog.md index 6dcfe4eddc..c47f6fa2b6 100644 --- a/plugins/governance/changelog.md +++ b/plugins/governance/changelog.md @@ -1,4 +1,5 @@ -- Upgrades framework to 1.0.23 \ No newline at end of file +- upgrade: core to 1.1.38 +- upgrade: framework to 1.0.24 \ No newline at end of file diff --git a/plugins/governance/version b/plugins/governance/version index f69752ab13..b66183a60b 100644 --- a/plugins/governance/version +++ b/plugins/governance/version @@ -1 +1 @@ -1.2.16 +1.2.17 diff --git a/plugins/jsonparser/changelog.md b/plugins/jsonparser/changelog.md index 6dcfe4eddc..c47f6fa2b6 100644 --- a/plugins/jsonparser/changelog.md +++ b/plugins/jsonparser/changelog.md @@ -1,4 +1,5 @@ -- Upgrades framework to 1.0.23 \ No newline at end of file +- upgrade: core to 1.1.38 +- upgrade: framework to 1.0.24 \ No newline at end of file diff --git a/plugins/jsonparser/version b/plugins/jsonparser/version index 05060b805b..a96f385f15 100644 --- a/plugins/jsonparser/version +++ b/plugins/jsonparser/version @@ -1 +1 @@ -1.2.15 \ No newline at end of file +1.2.16 \ No newline at end of file diff --git a/plugins/logging/changelog.md b/plugins/logging/changelog.md index 06359c9de0..e12a27e83b 100644 --- a/plugins/logging/changelog.md +++ b/plugins/logging/changelog.md @@ -1,5 +1,6 @@ -- Upgrades framework to 1.0.23 -- Fixes pricing computation for nested model names. \ No newline at end of file +- fix: fixes error logging for streaming and non-streaming responses. +- upgrade: core to 1.1.38 +- upgrade: framework to 1.0.24 \ No newline at end of file diff --git a/plugins/logging/main.go b/plugins/logging/main.go index fbeb3e2b7b..57f6e5330d 100644 --- a/plugins/logging/main.go +++ b/plugins/logging/main.go @@ -371,7 +371,7 @@ func (p *LoggerPlugin) PostHook(ctx *context.Context, result *schemas.BifrostRes if !ok { p.logger.Error("model not found in context") return result, err, nil - } + } // Check if this is a streaming response requestType, ok := (*ctx).Value(schemas.BifrostContextKeyRequestType).(schemas.RequestType) if !ok { diff --git a/plugins/logging/operations.go b/plugins/logging/operations.go index 4a344c1712..7d9173f330 100644 --- a/plugins/logging/operations.go +++ b/plugins/logging/operations.go @@ -37,7 +37,7 @@ func (p *LoggerPlugin) updateLogEntry(ctx context.Context, requestID string, tim updates := make(map[string]interface{}) if !timestamp.IsZero() { // Try to get original timestamp from context first for latency calculation - latency, err := p.calculateLatency(requestID, timestamp, ctx) + latency, err := p.calculateLatency(ctx, requestID, timestamp) if err != nil { return err } @@ -142,7 +142,7 @@ func (p *LoggerPlugin) processStreamUpdate(ctx context.Context, requestID string // Handle error case first if data.ErrorDetails != nil { - latency, err := p.calculateLatency(requestID, timestamp, ctx) + latency, err := p.calculateLatency(ctx, requestID, timestamp) if err != nil { // If we can't get created_at, just update status and error tempEntry := &logstore.Log{} @@ -182,7 +182,7 @@ func (p *LoggerPlugin) processStreamUpdate(ctx context.Context, requestID string if isFinalChunk { // Stream is finishing, calculate latency var err error - latency, err = p.calculateLatency(requestID, timestamp, ctx) + latency, err = p.calculateLatency(ctx, requestID, timestamp) if err != nil { return fmt.Errorf("failed to get created_at for latency calculation: %w", err) } @@ -267,14 +267,17 @@ func (p *LoggerPlugin) processStreamUpdate(ctx context.Context, requestID string } // calculateLatency computes latency in milliseconds from creation time -func (p *LoggerPlugin) calculateLatency(requestID string, currentTime time.Time, ctx context.Context) (float64, error) { +func (p *LoggerPlugin) calculateLatency(ctx context.Context, requestID string, currentTime time.Time) (float64, error) { // Try to get original timestamp from context first if ctxTimestamp, ok := ctx.Value(CreatedTimestampKey).(time.Time); ok { return float64(currentTime.Sub(ctxTimestamp).Nanoseconds()) / 1e6, nil } - - // Fallback to database query if not found in context - originalEntry, err := p.store.FindFirst(map[string]interface{}{"id": requestID}, "created_at") + var originalEntry *logstore.Log + err := retryOnNotFound(ctx, func() error { + var opErr error + originalEntry, opErr = p.store.FindFirst(map[string]interface{}{"id": requestID}, "created_at") + return opErr + }) if err != nil { return 0, err } diff --git a/plugins/logging/streaming.go b/plugins/logging/streaming.go index 206f8822ce..4cdaac72a3 100644 --- a/plugins/logging/streaming.go +++ b/plugins/logging/streaming.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + "github.com/bytedance/sonic" bifrost "github.com/maximhq/bifrost/core" "github.com/maximhq/bifrost/core/schemas" "github.com/maximhq/bifrost/framework/logstore" @@ -125,7 +126,7 @@ func (p *LoggerPlugin) addStreamChunk(requestID string, chunk *StreamChunk, obje } // processAccumulatedChunks processes all accumulated chunks in order -func (p *LoggerPlugin) processAccumulatedChunks(requestID string) error { +func (p *LoggerPlugin) processAccumulatedChunks(ctx context.Context, requestID string, respErr *schemas.BifrostError) error { accumulator := p.getOrCreateStreamAccumulator(requestID) accumulator.mu.Lock() @@ -138,7 +139,7 @@ func (p *LoggerPlugin) processAccumulatedChunks(requestID string) error { completeMessage := p.buildCompleteMessageFromChunks(accumulator.Chunks) // Calculate final latency - latency, err := p.calculateLatency(requestID, accumulator.FinalTimestamp, context.Background()) + latency, err := p.calculateLatency(ctx, requestID, accumulator.FinalTimestamp) if err != nil { p.logger.Error("failed to calculate latency for request %s: %v", requestID, err) latency = 0 @@ -147,6 +148,9 @@ func (p *LoggerPlugin) processAccumulatedChunks(requestID string) error { // Update database with complete message updates := make(map[string]interface{}) updates["status"] = "success" + if respErr != nil { + updates["status"] = "error" + } updates["stream"] = true updates["latency"] = latency updates["timestamp"] = accumulator.FinalTimestamp @@ -158,13 +162,19 @@ func (p *LoggerPlugin) processAccumulatedChunks(requestID string) error { if completeMessage.AssistantMessage != nil && completeMessage.AssistantMessage.ToolCalls != nil { tempEntry.ToolCallsParsed = completeMessage.AssistantMessage.ToolCalls } - if err := tempEntry.SerializeFields(); err != nil { return fmt.Errorf("failed to serialize complete message: %w", err) } - - updates["output_message"] = tempEntry.OutputMessage - updates["content_summary"] = tempEntry.ContentSummary + if respErr != nil { + if b, mErr := sonic.Marshal(respErr); mErr == nil { + updates["error_details"] = string(b) + } else { + updates["error_details"] = fmt.Sprintf(`{"message":"failed to marshal error: %v"}`, mErr) + } + } else { + updates["output_message"] = tempEntry.OutputMessage + updates["content_summary"] = tempEntry.ContentSummary + } if tempEntry.ToolCalls != "" { updates["tool_calls"] = tempEntry.ToolCalls } @@ -402,7 +412,7 @@ func (p *LoggerPlugin) handleStreamingResponse(ctx *context.Context, result *sch if shouldProcess { - if processErr := p.processAccumulatedChunks(requestID); processErr != nil { + if processErr := p.processAccumulatedChunks(*ctx, requestID, err); processErr != nil { p.logger.Error("failed to process accumulated chunks for request %s: %v", requestID, processErr) } diff --git a/plugins/maxim/changelog.md b/plugins/maxim/changelog.md index 6dcfe4eddc..c47f6fa2b6 100644 --- a/plugins/maxim/changelog.md +++ b/plugins/maxim/changelog.md @@ -1,4 +1,5 @@ -- Upgrades framework to 1.0.23 \ No newline at end of file +- upgrade: core to 1.1.38 +- upgrade: framework to 1.0.24 \ No newline at end of file diff --git a/plugins/maxim/version b/plugins/maxim/version index 95b25aee25..3336003dcc 100644 --- a/plugins/maxim/version +++ b/plugins/maxim/version @@ -1 +1 @@ -1.3.6 +1.3.7 diff --git a/plugins/mocker/changelog.md b/plugins/mocker/changelog.md index 6dcfe4eddc..c47f6fa2b6 100644 --- a/plugins/mocker/changelog.md +++ b/plugins/mocker/changelog.md @@ -1,4 +1,5 @@ -- Upgrades framework to 1.0.23 \ No newline at end of file +- upgrade: core to 1.1.38 +- upgrade: framework to 1.0.24 \ No newline at end of file diff --git a/plugins/mocker/version b/plugins/mocker/version index fd9d1a5aca..1fc5b8206a 100644 --- a/plugins/mocker/version +++ b/plugins/mocker/version @@ -1 +1 @@ -1.2.14 +1.2.15 diff --git a/plugins/semanticcache/changelog.md b/plugins/semanticcache/changelog.md index 6dcfe4eddc..c47f6fa2b6 100644 --- a/plugins/semanticcache/changelog.md +++ b/plugins/semanticcache/changelog.md @@ -1,4 +1,5 @@ -- Upgrades framework to 1.0.23 \ No newline at end of file +- upgrade: core to 1.1.38 +- upgrade: framework to 1.0.24 \ No newline at end of file diff --git a/plugins/semanticcache/version b/plugins/semanticcache/version index 591bdbcd6a..9ddbe16da5 100644 --- a/plugins/semanticcache/version +++ b/plugins/semanticcache/version @@ -1 +1 @@ -1.2.18 +1.2.19 diff --git a/plugins/telemetry/changelog.md b/plugins/telemetry/changelog.md index 6dcfe4eddc..c47f6fa2b6 100644 --- a/plugins/telemetry/changelog.md +++ b/plugins/telemetry/changelog.md @@ -1,4 +1,5 @@ -- Upgrades framework to 1.0.23 \ No newline at end of file +- upgrade: core to 1.1.38 +- upgrade: framework to 1.0.24 \ No newline at end of file diff --git a/plugins/telemetry/version b/plugins/telemetry/version index 05060b805b..a96f385f15 100644 --- a/plugins/telemetry/version +++ b/plugins/telemetry/version @@ -1 +1 @@ -1.2.15 \ No newline at end of file +1.2.16 \ No newline at end of file diff --git a/tests/core-providers/openrouter_test.go b/tests/core-providers/openrouter_test.go index 8ce5ca342a..ea371b9cfc 100644 --- a/tests/core-providers/openrouter_test.go +++ b/tests/core-providers/openrouter_test.go @@ -1,7 +1,6 @@ package tests import ( - "os" "testing" "github.com/maximhq/bifrost/tests/core-providers/config" @@ -10,9 +9,6 @@ import ( ) func TestOpenRouter(t *testing.T) { - if os.Getenv("OPENROUTER_API_KEY") == "" { - t.Skip("OPENROUTER_API_KEY not set; skipping OpenRouter tests") - } client, ctx, cancel, err := config.SetupTest() if err != nil { t.Fatalf("Error initializing test setup: %v", err) @@ -44,3 +40,71 @@ func TestOpenRouter(t *testing.T) { runAllComprehensiveTests(t, client, ctx, testConfig) } + +// TestOpenRouterAnthropic tests Anthropic models via OpenRouter +func TestOpenRouterAnthropic(t *testing.T) { + client, ctx, cancel, err := config.SetupTest() + if err != nil { + t.Fatalf("Error initializing test setup: %v", err) + } + defer cancel() + defer client.Shutdown() + + testConfig := config.ComprehensiveTestConfig{ + Provider: schemas.OpenRouter, + ChatModel: "anthropic/claude-3.5-sonnet", // Using Claude 3.5 Sonnet via OpenRouter + TextModel: "", // Anthropic models don't support text completion + EmbeddingModel: "", + Scenarios: config.TestScenarios{ + TextCompletion: false, // Not supported by Anthropic + SimpleChat: true, + ChatCompletionStream: true, + MultiTurnConversation: true, + ToolCalls: true, + MultipleToolCalls: true, + End2EndToolCalling: true, + AutomaticFunctionCall: true, + ImageURL: true, + ImageBase64: true, + MultipleImages: true, + CompleteEnd2End: true, + ProviderSpecific: false, // Skip provider-specific tests + }, + } + + runAllComprehensiveTests(t, client, ctx, testConfig) +} + +// TestOpenRouterMetaLlama tests Meta's Llama models via OpenRouter +func TestOpenRouterMetaLlama(t *testing.T) { + client, ctx, cancel, err := config.SetupTest() + if err != nil { + t.Fatalf("Error initializing test setup: %v", err) + } + defer cancel() + defer client.Shutdown() + + testConfig := config.ComprehensiveTestConfig{ + Provider: schemas.OpenRouter, + ChatModel: "meta-llama/llama-3.1-70b-instruct", // Using Llama 3.1 70B via OpenRouter + TextModel: "meta-llama/llama-3.1-8b-instruct", // Using smaller model for text completion + EmbeddingModel: "", + Scenarios: config.TestScenarios{ + TextCompletion: true, + SimpleChat: true, + ChatCompletionStream: true, + MultiTurnConversation: true, + ToolCalls: true, + MultipleToolCalls: true, + End2EndToolCalling: true, + AutomaticFunctionCall: true, + ImageURL: false, // Llama models typically don't support image inputs + ImageBase64: false, + MultipleImages: false, + CompleteEnd2End: true, + ProviderSpecific: false, + }, + } + + runAllComprehensiveTests(t, client, ctx, testConfig) +} \ No newline at end of file diff --git a/tests/core-providers/scenarios/chat_completion_stream.go b/tests/core-providers/scenarios/chat_completion_stream.go index 6af2e6c3ba..672a5fd85f 100644 --- a/tests/core-providers/scenarios/chat_completion_stream.go +++ b/tests/core-providers/scenarios/chat_completion_stream.go @@ -46,6 +46,7 @@ func RunChatCompletionStreamTest(t *testing.T, client *bifrost.Bifrost, ctx cont var fullContent strings.Builder var responseCount int var lastResponse *schemas.BifrostStream + var hasReceivedUsage bool // Create a timeout context for the stream reading streamCtx, cancel := context.WithTimeout(ctx, 30*time.Second) @@ -97,6 +98,22 @@ func RunChatCompletionStreamTest(t *testing.T, client *bifrost.Bifrost, ctx cont } } + // Check if this response contains usage information + if response.Usage != nil { + hasReceivedUsage = true + t.Logf("📊 Token usage received - Prompt: %d, Completion: %d, Total: %d", + response.Usage.PromptTokens, + response.Usage.CompletionTokens, + response.Usage.TotalTokens) + + // Validate token counts + assert.Greater(t, response.Usage.PromptTokens, 0, "Prompt tokens should be greater than 0") + assert.Greater(t, response.Usage.CompletionTokens, 0, "Completion tokens should be greater than 0") + assert.Greater(t, response.Usage.TotalTokens, 0, "Total tokens should be greater than 0") + assert.Equal(t, response.Usage.PromptTokens+response.Usage.CompletionTokens, + response.Usage.TotalTokens, "Total tokens should equal prompt + completion tokens") + } + responseCount++ // Safety check to prevent infinite loops in case of issues @@ -110,19 +127,37 @@ func RunChatCompletionStreamTest(t *testing.T, client *bifrost.Bifrost, ctx cont } streamComplete: + // Validate that we received usage information at some point in the stream + assert.True(t, hasReceivedUsage, "Should have received token usage information during streaming") + // Validate that the last response contains usage information and/or finish reason // with empty choices (typical final chunk pattern) if lastResponse != nil && lastResponse.BifrostResponse != nil { // Check if this is a final metadata chunk (empty choices with usage/finish info) if len(lastResponse.Choices) == 0 && lastResponse.Usage != nil { + // Comprehensive validation of final usage + assert.Greater(t, lastResponse.Usage.PromptTokens, 0, "Final chunk should have prompt token count") + assert.Greater(t, lastResponse.Usage.CompletionTokens, 0, "Final chunk should have completion token count") assert.Greater(t, lastResponse.Usage.TotalTokens, 0, "Final chunk should have total token count") - t.Logf("📊 Final metadata chunk - Total tokens: %d", lastResponse.Usage.TotalTokens) + assert.Equal(t, lastResponse.Usage.PromptTokens+lastResponse.Usage.CompletionTokens, + lastResponse.Usage.TotalTokens, "Total tokens should equal prompt + completion tokens") + t.Logf("📊 Final metadata chunk - Prompt: %d, Completion: %d, Total: %d", + lastResponse.Usage.PromptTokens, + lastResponse.Usage.CompletionTokens, + lastResponse.Usage.TotalTokens) } else if len(lastResponse.Choices) > 0 { // Check if final choice has finish reason finalChoice := lastResponse.Choices[0] if finalChoice.FinishReason != nil { t.Logf("🏁 Stream ended with finish reason: %s", *finalChoice.FinishReason) } + + // Even with choices, we should have usage info in the last response or earlier + if lastResponse.Usage != nil { + assert.Greater(t, lastResponse.Usage.PromptTokens, 0, "Should have prompt tokens") + assert.Greater(t, lastResponse.Usage.CompletionTokens, 0, "Should have completion tokens") + assert.Greater(t, lastResponse.Usage.TotalTokens, 0, "Should have total tokens") + } } else { t.Fatal("Last response should have choices or usage") } @@ -143,6 +178,9 @@ func RunChatCompletionStreamTest(t *testing.T, client *bifrost.Bifrost, ctx cont } else { // This is a metadata-only chunk, which is valid for final chunks assert.NotNil(t, lastResponse.Usage, "Usage should not be nil") + // Additional validation for the usage in metadata-only chunk + assert.Greater(t, lastResponse.Usage.PromptTokens, 0, "Metadata chunk should have prompt tokens") + assert.Greater(t, lastResponse.Usage.CompletionTokens, 0, "Metadata chunk should have completion tokens") } } @@ -176,6 +214,8 @@ func RunChatCompletionStreamTest(t *testing.T, client *bifrost.Bifrost, ctx cont var toolCallDetected bool var responseCount int + var hasReceivedUsageWithTools bool + var lastResponseWithTools *schemas.BifrostStream streamCtx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() @@ -190,8 +230,25 @@ func RunChatCompletionStreamTest(t *testing.T, client *bifrost.Bifrost, ctx cont } require.NotNil(t, response, "Streaming response should not be nil") + lastResponseWithTools = response responseCount++ + // Check for usage information in tool call streaming + if response.Usage != nil { + hasReceivedUsageWithTools = true + t.Logf("📊 Tool stream usage - Prompt: %d, Completion: %d, Total: %d", + response.Usage.PromptTokens, + response.Usage.CompletionTokens, + response.Usage.TotalTokens) + + // Validate token counts for tool calls + assert.Greater(t, response.Usage.PromptTokens, 0, "Tool stream should have prompt tokens") + assert.Greater(t, response.Usage.CompletionTokens, 0, "Tool stream should have completion tokens") + assert.Greater(t, response.Usage.TotalTokens, 0, "Tool stream should have total tokens") + assert.Equal(t, response.Usage.PromptTokens+response.Usage.CompletionTokens, + response.Usage.TotalTokens, "Total should equal prompt + completion for tool stream") + } + for _, choice := range response.Choices { if choice.BifrostStreamResponseChoice != nil { delta := choice.BifrostStreamResponseChoice.Delta @@ -225,6 +282,15 @@ func RunChatCompletionStreamTest(t *testing.T, client *bifrost.Bifrost, ctx cont toolStreamComplete: assert.Greater(t, responseCount, 0, "Should receive at least one streaming response") assert.True(t, toolCallDetected, "Should detect tool calls in streaming response") + assert.True(t, hasReceivedUsageWithTools, "Should have received token usage for tool call stream") + + // Validate final response has proper usage information + if lastResponseWithTools != nil && lastResponseWithTools.Usage != nil { + assert.Greater(t, lastResponseWithTools.Usage.PromptTokens, 0, "Final tool stream should have prompt tokens") + assert.Greater(t, lastResponseWithTools.Usage.CompletionTokens, 0, "Final tool stream should have completion tokens") + assert.Greater(t, lastResponseWithTools.Usage.TotalTokens, 0, "Final tool stream should have total tokens") + } + t.Logf("✅ Streaming with tools test completed successfully") }) } diff --git a/transports/bifrost-http/handlers/logging.go b/transports/bifrost-http/handlers/logging.go index 915a35497b..7029382b9c 100644 --- a/transports/bifrost-http/handlers/logging.go +++ b/transports/bifrost-http/handlers/logging.go @@ -147,8 +147,7 @@ func (h *LoggingHandler) getLogs(ctx *fasthttp.RequestCtx) { h.logger.Error("failed to search logs: %v", err) SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Search failed: %v", err), h.logger) return - } - + } SendJSON(ctx, result, h.logger) } diff --git a/transports/changelog.md b/transports/changelog.md index 9c26654f07..fe8d5b5f88 100644 --- a/transports/changelog.md +++ b/transports/changelog.md @@ -1,4 +1,6 @@ -- Fixes pricing computation for nested model names i.e. groq/openai/gpt-oss-20b. \ No newline at end of file +- Fix: Users can now delete custom providers from the UI +- Fix: Token count no longer displays as N/A in certain streaming response cases +- Fix: Streaming responses now properly display errors on the UI instead of getting stuck in processing state \ No newline at end of file diff --git a/transports/version b/transports/version index 9728bd69ac..9a83513ab0 100644 --- a/transports/version +++ b/transports/version @@ -1 +1 @@ -1.2.21 +1.2.22 diff --git a/ui/app/logs/views/logDetailsSheet.tsx b/ui/app/logs/views/logDetailsSheet.tsx index 45477d305f..52906a3f19 100644 --- a/ui/app/logs/views/logDetailsSheet.tsx +++ b/ui/app/logs/views/logDetailsSheet.tsx @@ -274,7 +274,7 @@ export function LogDetailSheet({ log, open, onOpenChange }: LogDetailSheetProps) {log.status !== "processing" && ( <> - {log.output_message && ( + {log.output_message && !log.error_details?.error.message && ( <>
Response