diff --git a/core/go.mod b/core/go.mod index 4ec19dd849f..e80e8698bc0 100644 --- a/core/go.mod +++ b/core/go.mod @@ -15,6 +15,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 github.com/aws/smithy-go v1.25.1 github.com/bytedance/sonic v1.15.0 + github.com/cespare/xxhash/v2 v2.3.0 github.com/fasthttp/websocket v1.5.12 github.com/google/uuid v1.6.0 github.com/hajimehoshi/go-mp3 v0.3.4 diff --git a/core/go.sum b/core/go.sum index 6a03db8fd45..9323132100d 100644 --- a/core/go.sum +++ b/core/go.sum @@ -64,6 +64,8 @@ github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uS github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= diff --git a/core/providers/bedrock/bedrock.go b/core/providers/bedrock/bedrock.go index 7fcccb36543..fe7fd58ad87 100644 --- a/core/providers/bedrock/bedrock.go +++ b/core/providers/bedrock/bedrock.go @@ -1222,7 +1222,7 @@ func (provider *BedrockProvider) ChatCompletionStream(ctx *schemas.BifrostContex var structuredOutputBuilder strings.Builder var isAccumulatingStructuredOutput bool - streamState := NewBedrockStreamState() + streamState := NewBedrockStreamStateWithContext(ctx) for { // If context was cancelled/timed out, let defer handle it @@ -1590,6 +1590,7 @@ func (provider *BedrockProvider) ResponsesStream(ctx *schemas.BifrostContext, po // Create stream state for stateful conversions streamState := acquireBedrockResponsesStreamState() streamState.Model = &request.Model + streamState.Ctx = ctx defer releaseBedrockResponsesStreamState(streamState) // Check for structured output mode - if set, we need to intercept tool calls diff --git a/core/providers/bedrock/bedrock_test.go b/core/providers/bedrock/bedrock_test.go index e87c70ea8cb..83dc16b5890 100644 --- a/core/providers/bedrock/bedrock_test.go +++ b/core/providers/bedrock/bedrock_test.go @@ -5340,6 +5340,236 @@ func TestToBedrockResponsesRequest_NonLlamaConvertResponsesToolChoiceForcesToolC assert.Equal(t, toolName, bedrockReq.ToolConfig.ToolChoice.Tool.Name) } +func TestToBedrockChatCompletionRequest_AliasesLongMCPToolNames(t *testing.T) { + toolName := "mcp__plugin_chrome-devtools-mcp_chrome-devtools__list_network_requests" + req := &schemas.BifrostChatRequest{ + Model: "us.anthropic.claude-sonnet-4-5-20250929-v1:0", + Input: []schemas.ChatMessage{{ + Role: schemas.ChatMessageRoleUser, + Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("use devtools")}, + }}, + Params: &schemas.ChatParameters{ + Tools: []schemas.ChatTool{{ + Type: schemas.ChatToolTypeFunction, + Function: &schemas.ChatToolFunction{ + Name: toolName, + Description: schemas.Ptr("List network requests"), + }, + }}, + ToolChoice: &schemas.ChatToolChoice{ + ChatToolChoiceStruct: &schemas.ChatToolChoiceStruct{ + Type: schemas.ChatToolChoiceTypeFunction, + Function: &schemas.ChatToolChoiceFunction{Name: toolName}, + }, + }, + }, + } + + ctx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline) + result, err := bedrock.ToBedrockChatCompletionRequest(ctx, req) + require.NoError(t, err) + require.NotNil(t, result.ToolConfig) + require.Len(t, result.ToolConfig.Tools, 1) + + alias := result.ToolConfig.Tools[0].ToolSpec.Name + require.LessOrEqual(t, len(alias), 64) + assert.NotEqual(t, toolName, alias) + assert.Contains(t, alias, "_list_network_requests") + assert.Regexp(t, `^[A-Za-z0-9_-]{1,64}$`, alias) + assert.Regexp(t, `^[0-9a-f]{8}_`, alias) + require.NotNil(t, result.ToolConfig.ToolChoice) + require.NotNil(t, result.ToolConfig.ToolChoice.Tool) + assert.Equal(t, alias, result.ToolConfig.ToolChoice.Tool.Name) +} + +func TestToBedrockChatCompletionRequest_AliasesToolNamesWithInvalidChars(t *testing.T) { + // Short name (<=64 chars) but with characters disallowed by Bedrock's + // `[a-zA-Z0-9_-]{1,64}` tool-name pattern. It must still be aliased into a + // Bedrock-valid name rather than passed through unchanged. + toolName := "search files/in dir:now.fast" + require.LessOrEqual(t, len(toolName), 64) + req := &schemas.BifrostChatRequest{ + Model: "us.anthropic.claude-sonnet-4-5-20250929-v1:0", + Input: []schemas.ChatMessage{{ + Role: schemas.ChatMessageRoleUser, + Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("search")}, + }}, + Params: &schemas.ChatParameters{ + Tools: []schemas.ChatTool{{ + Type: schemas.ChatToolTypeFunction, + Function: &schemas.ChatToolFunction{ + Name: toolName, + Description: schemas.Ptr("Search files"), + }, + }}, + ToolChoice: &schemas.ChatToolChoice{ + ChatToolChoiceStruct: &schemas.ChatToolChoiceStruct{ + Type: schemas.ChatToolChoiceTypeFunction, + Function: &schemas.ChatToolChoiceFunction{Name: toolName}, + }, + }, + }, + } + + ctx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline) + result, err := bedrock.ToBedrockChatCompletionRequest(ctx, req) + require.NoError(t, err) + require.NotNil(t, result.ToolConfig) + require.Len(t, result.ToolConfig.Tools, 1) + + alias := result.ToolConfig.Tools[0].ToolSpec.Name + assert.NotEqual(t, toolName, alias, "name with disallowed chars must be aliased") + assert.Regexp(t, `^[A-Za-z0-9_-]{1,64}$`, alias) + assert.Regexp(t, `^[0-9a-f]{8}_`, alias) + require.NotNil(t, result.ToolConfig.ToolChoice) + require.NotNil(t, result.ToolConfig.ToolChoice.Tool) + assert.Equal(t, alias, result.ToolConfig.ToolChoice.Tool.Name) +} + +func TestBedrockToBifrostChatResponse_RestoresAliasedToolName(t *testing.T) { + toolName := "mcp__plugin_chrome-devtools-mcp_chrome-devtools__list_network_requests" + req := &schemas.BifrostChatRequest{ + Model: "us.anthropic.claude-sonnet-4-5-20250929-v1:0", + Input: []schemas.ChatMessage{{ + Role: schemas.ChatMessageRoleUser, + Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("use devtools")}, + }}, + Params: &schemas.ChatParameters{ + Tools: []schemas.ChatTool{{ + Type: schemas.ChatToolTypeFunction, + Function: &schemas.ChatToolFunction{Name: toolName}, + }}, + }, + } + + ctx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline) + result, err := bedrock.ToBedrockChatCompletionRequest(ctx, req) + require.NoError(t, err) + alias := result.ToolConfig.Tools[0].ToolSpec.Name + + response := &bedrock.BedrockConverseResponse{ + StopReason: "tool_use", + Output: &bedrock.BedrockConverseOutput{ + Message: &bedrock.BedrockMessage{ + Role: bedrock.BedrockMessageRoleAssistant, + Content: []bedrock.BedrockContentBlock{{ + ToolUse: &bedrock.BedrockToolUse{ + ToolUseID: "tooluse_123", + Name: alias, + Input: json.RawMessage(`{"limit":10}`), + }, + }}, + }, + }, + } + + converted, err := response.ToBifrostChatResponse(ctx, req.Model) + require.NoError(t, err) + require.Len(t, converted.Choices, 1) + toolCalls := converted.Choices[0].ChatNonStreamResponseChoice.Message.ChatAssistantMessage.ToolCalls + require.Len(t, toolCalls, 1) + require.NotNil(t, toolCalls[0].Function.Name) + assert.Equal(t, toolName, *toolCalls[0].Function.Name) +} + +func TestToBedrockResponsesRequest_AliasesLongMCPToolNames(t *testing.T) { + toolName := "mcp__bifrost-this-is-imp-nasdkjadk-kanbsdjkabdkjbaskjdbasdaskjdbajksdkas__notion-notion-search" + req := &schemas.BifrostResponsesRequest{ + Model: "us.anthropic.claude-opus-4-7", + Input: []schemas.ResponsesMessage{{ + Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage), + Role: schemas.Ptr(schemas.ResponsesInputMessageRoleUser), + Content: &schemas.ResponsesMessageContent{ + ContentStr: schemas.Ptr("search docs for openai"), + }, + }}, + Params: &schemas.ResponsesParameters{ + Tools: []schemas.ResponsesTool{{ + Type: schemas.ResponsesToolTypeFunction, + Name: &toolName, + Description: schemas.Ptr("Search Notion"), + ResponsesToolFunction: &schemas.ResponsesToolFunction{ + Parameters: &schemas.ToolFunctionParameters{ + Type: "object", + Properties: schemas.NewOrderedMap(), + }, + }, + }}, + ToolChoice: &schemas.ResponsesToolChoice{ + ResponsesToolChoiceStruct: &schemas.ResponsesToolChoiceStruct{ + Type: schemas.ResponsesToolChoiceTypeFunction, + Name: &toolName, + }, + }, + }, + } + + ctx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline) + result, err := bedrock.ToBedrockResponsesRequest(ctx, req) + require.NoError(t, err) + require.NotNil(t, result.ToolConfig) + require.Len(t, result.ToolConfig.Tools, 1) + + alias := result.ToolConfig.Tools[0].ToolSpec.Name + require.LessOrEqual(t, len(alias), 64) + assert.NotEqual(t, toolName, alias) + assert.Contains(t, alias, "_notion-notion-search") + assert.Regexp(t, `^[A-Za-z0-9_-]{1,64}$`, alias) + assert.Regexp(t, `^[0-9a-f]{8}_`, alias) + require.NotNil(t, result.ToolConfig.ToolChoice) + require.NotNil(t, result.ToolConfig.ToolChoice.Tool) + assert.Equal(t, alias, result.ToolConfig.ToolChoice.Tool.Name) +} + +func TestBedrockToBifrostResponsesResponse_RestoresAliasedToolName(t *testing.T) { + toolName := "mcp__bifrost-this-is-imp-nasdkjadk-kanbsdjkabdkjbaskjdbasdaskjdbajksdkas__notion-notion-search" + req := &schemas.BifrostResponsesRequest{ + Model: "us.anthropic.claude-opus-4-7", + Input: []schemas.ResponsesMessage{{ + Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage), + Role: schemas.Ptr(schemas.ResponsesInputMessageRoleUser), + Content: &schemas.ResponsesMessageContent{ + ContentStr: schemas.Ptr("search docs for openai"), + }, + }}, + Params: &schemas.ResponsesParameters{ + Tools: []schemas.ResponsesTool{{ + Type: schemas.ResponsesToolTypeFunction, + Name: &toolName, + ResponsesToolFunction: &schemas.ResponsesToolFunction{}, + }}, + }, + } + + ctx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline) + result, err := bedrock.ToBedrockResponsesRequest(ctx, req) + require.NoError(t, err) + alias := result.ToolConfig.Tools[0].ToolSpec.Name + + response := &bedrock.BedrockConverseResponse{ + StopReason: "tool_use", + Output: &bedrock.BedrockConverseOutput{ + Message: &bedrock.BedrockMessage{ + Role: bedrock.BedrockMessageRoleAssistant, + Content: []bedrock.BedrockContentBlock{{ + ToolUse: &bedrock.BedrockToolUse{ + ToolUseID: "tooluse_456", + Name: alias, + Input: json.RawMessage(`{"query":"openai"}`), + }, + }}, + }, + }, + } + + converted, err := response.ToBifrostResponsesResponse(ctx) + require.NoError(t, err) + require.Len(t, converted.Output, 1) + require.NotNil(t, converted.Output[0].ResponsesToolMessage) + require.NotNil(t, converted.Output[0].ResponsesToolMessage.Name) + assert.Equal(t, toolName, *converted.Output[0].ResponsesToolMessage.Name) +} + // --------------------------------------------------------------------------- // Structured output (response_format: json_schema) round-trip tests – Bedrock // --------------------------------------------------------------------------- diff --git a/core/providers/bedrock/chat.go b/core/providers/bedrock/chat.go index 949c9e17049..ed089e70986 100644 --- a/core/providers/bedrock/chat.go +++ b/core/providers/bedrock/chat.go @@ -62,7 +62,7 @@ func ToBedrockChatCompletionRequest(ctx *schemas.BifrostContext, bifrostReq *sch } // Ensure tool config is present when needed - ensureChatToolConfigForConversation(bifrostReq, bedrockReq) + ensureChatToolConfigForConversation(ctx, bifrostReq, bedrockReq) if !schemas.BedrockModelSupportsCachePoints(bifrostReq.Model) { stripCachePointsFromBedrockRequest(bedrockReq) @@ -117,7 +117,7 @@ func (response *BedrockConverseResponse) ToBifrostChatResponse(ctx context.Conte } toolUseID := contentBlock.ToolUse.ToolUseID - toolUseName := contentBlock.ToolUse.Name + toolUseName := bedrockRestoreToolName(ctx, contentBlock.ToolUse.Name) toolCalls = append(toolCalls, schemas.ChatAssistantMessageToolCall{ Index: uint16(len(toolCalls)), @@ -310,6 +310,7 @@ func (response *BedrockConverseResponse) ToBifrostChatResponse(ctx context.Conte type BedrockStreamState struct { nextToolCallIndex int contentBlockToToolCallIdx map[int]int + ctx context.Context } // NewBedrockStreamState returns initialised stream state for one streaming response. @@ -319,6 +320,13 @@ func NewBedrockStreamState() *BedrockStreamState { } } +// NewBedrockStreamStateWithContext returns stream state that can restore aliased tool names. +func NewBedrockStreamStateWithContext(ctx context.Context) *BedrockStreamState { + state := NewBedrockStreamState() + state.ctx = ctx + return state +} + func (chunk *BedrockStreamEvent) ToBifrostChatCompletionStream(state *BedrockStreamState) (*schemas.BifrostChatResponse, *schemas.BifrostError, bool) { if state == nil { state = NewBedrockStreamState() @@ -361,7 +369,7 @@ func (chunk *BedrockStreamEvent) ToBifrostChatCompletionStream(state *BedrockStr toolCall.Index = uint16(toolCallIdx) toolCall.ID = schemas.Ptr(toolUseStart.ToolUseID) toolCall.Type = schemas.Ptr("function") - toolCall.Function.Name = schemas.Ptr(toolUseStart.Name) + toolCall.Function.Name = schemas.Ptr(bedrockRestoreToolName(state.ctx, toolUseStart.Name)) toolCall.Function.Arguments = "" // Start with empty arguments streamResponse := &schemas.BifrostChatResponse{ diff --git a/core/providers/bedrock/responses.go b/core/providers/bedrock/responses.go index d8ba3364745..905deb0a19f 100644 --- a/core/providers/bedrock/responses.go +++ b/core/providers/bedrock/responses.go @@ -39,6 +39,7 @@ type BedrockResponsesStreamState struct { HasEmittedCreated bool // Whether we've emitted response.created HasEmittedInProgress bool // Whether we've emitted response.in_progress UsedStructuredOutputTool bool // True when the SO tool block was intercepted and converted to text content + Ctx context.Context // Request context for restoring aliased tool names } // bedrockResponsesStreamStatePool provides a pool for Bedrock responses stream state objects. @@ -139,6 +140,7 @@ func acquireBedrockResponsesStreamState() *BedrockResponsesStreamState { state.HasEmittedCreated = false state.HasEmittedInProgress = false state.UsedStructuredOutputTool = false + state.Ctx = nil return state } @@ -579,7 +581,7 @@ func (chunk *BedrockStreamEvent) ToBifrostResponsesStream(sequenceNumber int, st state.CurrentOutputIndex++ toolUseID := chunk.Start.ToolUse.ToolUseID - toolName := chunk.Start.ToolUse.Name + toolName := bedrockRestoreToolName(state.Ctx, chunk.Start.ToolUse.Name) state.ItemIDs[outputIndex] = toolUseID state.ToolCallIDs[outputIndex] = toolUseID state.ToolCallNames[outputIndex] = toolName @@ -2424,6 +2426,7 @@ func ToBedrockResponsesRequest(ctx *schemas.BifrostContext, bifrostReq *schemas. return nil, fmt.Errorf("responses tool is missing required name for Bedrock function conversion") } name := *tool.Name + toolSpecName := bedrockAliasToolName(ctx, name) // Use the tool description if available, otherwise use a generic description description := "Function tool" @@ -2437,7 +2440,7 @@ func ToBedrockResponsesRequest(ctx *schemas.BifrostContext, bifrostReq *schemas. } bedrockTool := BedrockTool{ ToolSpec: &BedrockToolSpec{ - Name: name, + Name: toolSpecName, Description: &description, InputSchema: BedrockToolInputSchema{ JSON: json.RawMessage(schemaObjectBytes), @@ -2466,6 +2469,9 @@ func ToBedrockResponsesRequest(ctx *schemas.BifrostContext, bifrostReq *schemas. // Convert tool choice if bifrostReq.Params != nil && bifrostReq.Params.ToolChoice != nil { bedrockToolChoice := convertResponsesToolChoice(*bifrostReq.Params.ToolChoice) + if bedrockToolChoice != nil && bedrockToolChoice.Tool != nil && bedrockToolChoice.Tool.Name != "" { + bedrockToolChoice.Tool.Name = bedrockAliasToolName(ctx, bedrockToolChoice.Tool.Name) + } // Per-model gate: Bedrock Converse rejects toolConfig.toolChoice.tool // on Meta Llama variants ("This model doesn't support the // toolConfig.toolChoice.tool field"). Drop the forced specific-tool @@ -2514,7 +2520,7 @@ func ToBedrockResponsesRequest(ctx *schemas.BifrostContext, bifrostReq *schemas. } // Ensure tool config is present when tool content exists (similar to Chat Completions) - ensureResponsesToolConfigForConversation(bifrostReq, bedrockReq) + ensureResponsesToolConfigForConversation(ctx, bifrostReq, bedrockReq) if !schemas.BedrockModelSupportsCachePoints(bifrostReq.Model) { stripCachePointsFromBedrockRequest(bedrockReq) @@ -2743,19 +2749,19 @@ func extractBedrockTrace(v interface{}) *BedrockConverseTrace { } // ensureResponsesToolConfigForConversation ensures toolConfig is present when tool content exists -func ensureResponsesToolConfigForConversation(bifrostReq *schemas.BifrostResponsesRequest, bedrockReq *BedrockConverseRequest) { +func ensureResponsesToolConfigForConversation(ctx context.Context, bifrostReq *schemas.BifrostResponsesRequest, bedrockReq *BedrockConverseRequest) { if bedrockReq.ToolConfig != nil { return // Already has tool config } - hasToolContent, tools := extractToolsFromResponsesConversationHistory(bifrostReq.Input, bifrostReq.Model) + hasToolContent, tools := extractToolsFromResponsesConversationHistory(ctx, bifrostReq.Input, bifrostReq.Model) if hasToolContent && len(tools) > 0 { bedrockReq.ToolConfig = &BedrockToolConfig{Tools: tools} } } // extractToolsFromResponsesConversationHistory extracts tools from Responses conversation history -func extractToolsFromResponsesConversationHistory(messages []schemas.ResponsesMessage, model string) (bool, []BedrockTool) { +func extractToolsFromResponsesConversationHistory(ctx context.Context, messages []schemas.ResponsesMessage, model string) (bool, []BedrockTool) { var hasToolContent bool toolMap := make(map[string]*schemas.ResponsesTool) // Use map to deduplicate by name var hasNovaGrounding, hasNovaCodeInterpreter bool @@ -2813,7 +2819,7 @@ func extractToolsFromResponsesConversationHistory(messages []schemas.ResponsesMe schemaObjectBytes2, _ := providerUtils.MarshalSorted(schemaObject) bedrockTool := BedrockTool{ ToolSpec: &BedrockToolSpec{ - Name: *tool.Name, + Name: bedrockAliasToolName(ctx, *tool.Name), Description: &description, InputSchema: BedrockToolInputSchema{ JSON: json.RawMessage(schemaObjectBytes2), @@ -3212,7 +3218,7 @@ func ConvertBifrostMessagesToBedrockMessages(ctx context.Context, bifrostMessage if msg.ResponsesToolMessage != nil && msg.ResponsesToolMessage.CallID != nil { toolName := "" if msg.ResponsesToolMessage.Name != nil { - toolName = *msg.ResponsesToolMessage.Name + toolName = bedrockAliasToolName(ctx, *msg.ResponsesToolMessage.Name) } arguments := "" if msg.ResponsesToolMessage.Arguments != nil { @@ -3997,12 +4003,13 @@ func convertSingleBedrockMessageToBifrostMessages(ctx *schemas.BifrostContext, m if block.ToolUse.Input != nil { arguments = string(block.ToolUse.Input) } + restoredToolUseName := bedrockRestoreToolName(ctx, toolUseName) toolMsg := schemas.ResponsesMessage{ Type: schemas.Ptr(schemas.ResponsesMessageTypeFunctionCall), Status: schemas.Ptr("completed"), ResponsesToolMessage: &schemas.ResponsesToolMessage{ CallID: &toolUseID, - Name: &toolUseName, + Name: &restoredToolUseName, Arguments: schemas.Ptr(arguments), }, } diff --git a/core/providers/bedrock/utils.go b/core/providers/bedrock/utils.go index 5cfc6ba5a73..8ec4ee51cfe 100644 --- a/core/providers/bedrock/utils.go +++ b/core/providers/bedrock/utils.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/bytedance/sonic" + "github.com/cespare/xxhash/v2" "github.com/tidwall/sjson" "github.com/maximhq/bifrost/core/providers/anthropic" @@ -22,6 +23,10 @@ import ( // (?:-[a-z]+)+ allows multi-segment directional parts so GovCloud regions (us-gov-east-1) are // recognised alongside standard single-segment ones (eu-north-1, ap-southeast-2). var awsRegionRegex = regexp.MustCompile(`^[a-z]{2,3}(?:-[a-z]+)+-\d+$`) +var bedrockUnsafeToolNameCharRegex = regexp.MustCompile(`[^A-Za-z0-9_-]+`) + +// bedrockToolNameAliasKey stores Bedrock wire-name aliases on the request context. +type bedrockToolNameAliasKey struct{} // parseBedrockRegionAndModel splits a model string that optionally carries an AWS region prefix // into its region and bare model ID components. @@ -130,6 +135,51 @@ func normalizeBedrockFilename(filename string) string { return normalized } +// bedrockAliasToolName returns a Bedrock-safe tool name and records a reverse mapping. +func bedrockAliasToolName(ctx context.Context, name string) string { + if len(name) <= 64 && !bedrockUnsafeToolNameCharRegex.MatchString(name) { + return name + } + + semanticName := name + if parts := strings.Split(name, "__"); len(parts) > 1 { + semanticName = parts[len(parts)-1] + } + semanticName = strings.Trim(bedrockUnsafeToolNameCharRegex.ReplaceAllString(semanticName, "_"), "_") + if semanticName == "" { + semanticName = "tool" + } + + hash := fmt.Sprintf("%08x", uint32(xxhash.Sum64String(name))) + maxSemanticLen := 64 - len(hash) - 1 + if len(semanticName) > maxSemanticLen { + semanticName = semanticName[:maxSemanticLen] + } + alias := hash + "_" + semanticName + + if bifrostCtx, ok := ctx.(*schemas.BifrostContext); ok && alias != name { + aliases, _ := bifrostCtx.Value(bedrockToolNameAliasKey{}).(map[string]string) + if aliases == nil { + aliases = make(map[string]string) + bifrostCtx.SetValue(bedrockToolNameAliasKey{}, aliases) + } + aliases[alias] = name + } + return alias +} + +// bedrockRestoreToolName maps a Bedrock wire-name alias back to the caller's tool name. +func bedrockRestoreToolName(ctx context.Context, name string) string { + if bifrostCtx, ok := ctx.(*schemas.BifrostContext); ok { + if aliases, _ := bifrostCtx.Value(bedrockToolNameAliasKey{}).(map[string]string); aliases != nil { + if original, ok := aliases[name]; ok { + return original + } + } + } + return name +} + // convertParameters handles parameter conversion func convertChatParameters(ctx *schemas.BifrostContext, bifrostReq *schemas.BifrostChatRequest, bedrockReq *BedrockConverseRequest) error { // Parameters are optional - if not provided, just skip conversion @@ -155,7 +205,7 @@ func convertChatParameters(ctx *schemas.BifrostContext, bifrostReq *schemas.Bifr filteredTools, _ := anthropic.ValidateChatToolsForProvider(bifrostReq.Params.Tools, schemas.Bedrock) // Convert tool config (function/custom tools → Converse toolConfig.tools). - if toolConfig := convertToolConfigFromFiltered(bifrostReq.Model, bifrostReq.Params, filteredTools); toolConfig != nil { + if toolConfig := convertToolConfigFromFiltered(ctx, bifrostReq.Model, bifrostReq.Params, filteredTools); toolConfig != nil { bedrockReq.ToolConfig = toolConfig } @@ -611,12 +661,12 @@ func appendAnthropicBetaToFields(fields *schemas.OrderedMap, header string) { } // ensureChatToolConfigForConversation ensures toolConfig is present when tool content exists -func ensureChatToolConfigForConversation(bifrostReq *schemas.BifrostChatRequest, bedrockReq *BedrockConverseRequest) { +func ensureChatToolConfigForConversation(ctx context.Context, bifrostReq *schemas.BifrostChatRequest, bedrockReq *BedrockConverseRequest) { if bedrockReq.ToolConfig != nil { return // Already has tool config } - hasToolContent, tools := extractToolsFromConversationHistory(bifrostReq.Input) + hasToolContent, tools := extractToolsFromConversationHistory(ctx, bifrostReq.Input) if hasToolContent && len(tools) > 0 { bedrockReq.ToolConfig = &BedrockToolConfig{Tools: tools} } @@ -766,7 +816,7 @@ func convertMessage(ctx context.Context, msg schemas.ChatMessage) (BedrockMessag // Add tool calls last (for assistant messages) if msg.ChatAssistantMessage != nil && msg.ChatAssistantMessage.ToolCalls != nil { for _, toolCall := range msg.ChatAssistantMessage.ToolCalls { - contentBlocks = append(contentBlocks, convertToolCallToContentBlock(toolCall)) + contentBlocks = append(contentBlocks, convertToolCallToContentBlock(ctx, toolCall)) } } @@ -1620,14 +1670,14 @@ func convertToolConfig(model string, params *schemas.ChatParameters) *BedrockToo } // Strip unsupported server tools before the conversion loop. filtered, _ := anthropic.ValidateChatToolsForProvider(params.Tools, schemas.Bedrock) - return convertToolConfigFromFiltered(model, params, filtered) + return convertToolConfigFromFiltered(nil, model, params, filtered) } // convertToolConfigFromFiltered is the inner variant that accepts a // pre-filtered tool set. convertChatParameters uses this to avoid filtering // twice (once here, once in collectBedrockServerTools). The public // convertToolConfig entry point is a thin wrapper preserved for tests. -func convertToolConfigFromFiltered(model string, params *schemas.ChatParameters, filtered []schemas.ChatTool) *BedrockToolConfig { +func convertToolConfigFromFiltered(ctx context.Context, model string, params *schemas.ChatParameters, filtered []schemas.ChatTool) *BedrockToolConfig { if params == nil { return nil } @@ -1658,7 +1708,7 @@ func convertToolConfigFromFiltered(model string, params *schemas.ChatParameters, bedrockTool := BedrockTool{ ToolSpec: &BedrockToolSpec{ - Name: tool.Function.Name, + Name: bedrockAliasToolName(ctx, tool.Function.Name), Description: new(description), InputSchema: BedrockToolInputSchema{ JSON: json.RawMessage(schemaObjectBytes), @@ -1694,6 +1744,9 @@ func convertToolConfigFromFiltered(model string, params *schemas.ChatParameters, if params.ToolChoice != nil { toolChoice := convertToolChoice(*params.ToolChoice) if toolChoice != nil { + if toolChoice.Tool != nil && toolChoice.Tool.Name != "" { + toolChoice.Tool.Name = bedrockAliasToolName(ctx, toolChoice.Tool.Name) + } // Reconcile: if the choice forces a specific tool by name, // verify that name still exists in the filtered tool set. // Without this, a caller that pinned a server tool we just @@ -1775,12 +1828,12 @@ func convertToolChoice(toolChoice schemas.ChatToolChoice) *BedrockToolChoice { } // extractToolsFromConversationHistory analyzes conversation history for tool content -func extractToolsFromConversationHistory(messages []schemas.ChatMessage) (bool, []BedrockTool) { +func extractToolsFromConversationHistory(ctx context.Context, messages []schemas.ChatMessage) (bool, []BedrockTool) { hasToolContent := false toolsMap := make(map[string]BedrockTool) for _, msg := range messages { - hasToolContent = checkMessageForToolContent(msg, toolsMap) || hasToolContent + hasToolContent = checkMessageForToolContent(ctx, msg, toolsMap) || hasToolContent } tools := make([]BedrockTool, 0, len(toolsMap)) @@ -1792,7 +1845,7 @@ func extractToolsFromConversationHistory(messages []schemas.ChatMessage) (bool, } // checkMessageForToolContent checks a single message for tool content and updates the tools map -func checkMessageForToolContent(msg schemas.ChatMessage, toolsMap map[string]BedrockTool) bool { +func checkMessageForToolContent(ctx context.Context, msg schemas.ChatMessage, toolsMap map[string]BedrockTool) bool { hasContent := false // Check assistant tool calls @@ -1800,7 +1853,8 @@ func checkMessageForToolContent(msg schemas.ChatMessage, toolsMap map[string]Bed hasContent = true for _, toolCall := range msg.ChatAssistantMessage.ToolCalls { if toolCall.Function.Name != nil { - if _, exists := toolsMap[*toolCall.Function.Name]; !exists { + toolName := bedrockAliasToolName(ctx, *toolCall.Function.Name) + if _, exists := toolsMap[toolName]; !exists { // Create a complete schema object for extracted tools schemaObject := map[string]interface{}{ "type": "object", @@ -1808,9 +1862,9 @@ func checkMessageForToolContent(msg schemas.ChatMessage, toolsMap map[string]Bed } extractedSchemaBytes, _ := providerUtils.MarshalSorted(schemaObject) - toolsMap[*toolCall.Function.Name] = BedrockTool{ + toolsMap[toolName] = BedrockTool{ ToolSpec: &BedrockToolSpec{ - Name: *toolCall.Function.Name, + Name: toolName, Description: schemas.Ptr("Tool extracted from conversation history"), InputSchema: BedrockToolInputSchema{ JSON: json.RawMessage(extractedSchemaBytes), @@ -1840,7 +1894,7 @@ func checkMessageForToolContent(msg schemas.ChatMessage, toolsMap map[string]Bed } // convertToolCallToContentBlock converts a Bifrost tool call to a Bedrock content block -func convertToolCallToContentBlock(toolCall schemas.ChatAssistantMessageToolCall) BedrockContentBlock { +func convertToolCallToContentBlock(ctx context.Context, toolCall schemas.ChatAssistantMessageToolCall) BedrockContentBlock { toolUseID := "" if toolCall.ID != nil { toolUseID = *toolCall.ID @@ -1848,7 +1902,7 @@ func convertToolCallToContentBlock(toolCall schemas.ChatAssistantMessageToolCall toolName := "" if toolCall.Function.Name != nil { - toolName = *toolCall.Function.Name + toolName = bedrockAliasToolName(ctx, *toolCall.Function.Name) } // Preserve original key ordering of tool arguments for prompt caching. diff --git a/ui/app/workspace/logs/sheets/logDetailView.tsx b/ui/app/workspace/logs/sheets/logDetailView.tsx index 7070c167fc6..6face077fc2 100644 --- a/ui/app/workspace/logs/sheets/logDetailView.tsx +++ b/ui/app/workspace/logs/sheets/logDetailView.tsx @@ -1,67 +1,40 @@ -import { - formatCost, - formatLatency, -} from "@/app/workspace/dashboard/utils/chartUtils"; +import { formatCost, formatLatency } from "@/app/workspace/dashboard/utils/chartUtils"; import { formatCompactNumber } from "@/lib/utils/numbers"; import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, } from "@/components/ui/alertDialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { CodeEditor } from "@/components/ui/codeEditor"; import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, } from "@/components/ui/dropdownMenu"; import { DottedSeparator } from "@/components/ui/separator"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; -import { - ProviderIconType, - RenderProviderIcon, - RoutingEngineUsedIcons, -} from "@/lib/constants/icons"; -import { - RequestTypeColors, - RequestTypeLabels, - RoutingEngineUsedColors, - RoutingEngineUsedLabels, - Status, -} from "@/lib/constants/logs"; +import { ProviderIconType, RenderProviderIcon, RoutingEngineUsedIcons } from "@/lib/constants/icons"; +import { RequestTypeColors, RequestTypeLabels, RoutingEngineUsedColors, RoutingEngineUsedLabels, Status } from "@/lib/constants/logs"; import { ContentBlock, LogEntry, ResponsesMessage } from "@/lib/types/logs"; import { cn } from "@/lib/utils"; import { downloadAsJson } from "@/lib/utils/browser-download"; import { isJson } from "@/lib/utils/validation"; import { Link } from "@tanstack/react-router"; import { addMilliseconds, format } from "date-fns"; -import { - AlertCircle, - ChevronDown, - Clipboard, - Copy, - Download, - Loader2, - MoreVertical, - Trash2, - Wrench, -} from "lucide-react"; +import { AlertCircle, ChevronDown, Clipboard, Copy, Download, Loader2, MoreVertical, Trash2, Wrench } from "lucide-react"; import { useState, type ReactNode } from "react"; import { toast } from "sonner"; import BlockHeader from "../views/blockHeader"; @@ -76,3063 +49,2283 @@ import TranscriptionView from "../views/transcriptionView"; import VideoView from "../views/videoView"; const formatRealtimeTransport = (value: unknown): string => { - const transport = String(value ?? "").trim(); - switch (transport.toLowerCase()) { - case "websocket": - return "WebSocket"; - case "webrtc": - return "WebRTC"; - default: - return transport || "Unknown"; - } + const transport = String(value ?? "").trim(); + switch (transport.toLowerCase()) { + case "websocket": + return "WebSocket"; + case "webrtc": + return "WebRTC"; + default: + return transport || "Unknown"; + } }; const getRealtimeTransportBadgeClass = (value: unknown): string => { - switch (String(value ?? "").toLowerCase()) { - case "websocket": - return "border-indigo-300 bg-indigo-50 text-indigo-700 dark:border-indigo-600 dark:bg-indigo-950 dark:text-indigo-300"; - case "webrtc": - return "border-purple-300 bg-purple-50 text-purple-700 dark:border-purple-600 dark:bg-purple-950 dark:text-purple-300"; - default: - return "border-slate-300 bg-slate-50 text-slate-700 dark:border-slate-600 dark:bg-slate-950 dark:text-slate-300"; - } + switch (String(value ?? "").toLowerCase()) { + case "websocket": + return "border-indigo-300 bg-indigo-50 text-indigo-700 dark:border-indigo-600 dark:bg-indigo-950 dark:text-indigo-300"; + case "webrtc": + return "border-purple-300 bg-purple-50 text-purple-700 dark:border-purple-600 dark:bg-purple-950 dark:text-purple-300"; + default: + return "border-slate-300 bg-slate-50 text-slate-700 dark:border-slate-600 dark:bg-slate-950 dark:text-slate-300"; + } }; const formatRealtimeSource = (value: unknown): string => { - const source = String(value ?? "").trim(); - switch (source.toLowerCase()) { - case "ei": - return "Event Initiated"; - case "lm": - return "Language Model"; - default: - return source || "Unknown"; - } + const source = String(value ?? "").trim(); + switch (source.toLowerCase()) { + case "ei": + return "Event Initiated"; + case "lm": + return "Language Model"; + default: + return source || "Unknown"; + } }; const extractResponsesText = (msg: ResponsesMessage): string => { - if (msg.type === "reasoning") { - const summaryText = (msg.summary ?? []) - .map((s) => s.text) - .filter(Boolean) - .join("\n") - .trim(); - if (summaryText) return summaryText; - if (msg.encrypted_content) return msg.encrypted_content; - } - if (typeof msg.content === "string") return msg.content; - if (Array.isArray(msg.content)) { - return msg.content - .filter( - (b: any) => - b && - b.text && - (b.type === "input_text" || - b.type === "output_text" || - b.type === "reasoning_text" || - b.type === "refusal"), - ) - .map((b: any) => b.text as string) - .join("\n"); - } - if (typeof (msg as any).arguments === "string") - return (msg as any).arguments as string; - return ""; + if (msg.type === "reasoning") { + const summaryText = (msg.summary ?? []) + .map((s) => s.text) + .filter(Boolean) + .join("\n") + .trim(); + if (summaryText) return summaryText; + if (msg.encrypted_content) return msg.encrypted_content; + } + if (typeof msg.content === "string") return msg.content; + if (Array.isArray(msg.content)) { + return msg.content + .filter( + (b: any) => + b && b.text && (b.type === "input_text" || b.type === "output_text" || b.type === "reasoning_text" || b.type === "refusal"), + ) + .map((b: any) => b.text as string) + .join("\n"); + } + if (typeof (msg as any).arguments === "string") return (msg as any).arguments as string; + return ""; }; type ReasoningParts = { - summaries: string[]; - encrypted?: string; - signatures: string[]; - contentText?: string; + summaries: string[]; + encrypted?: string; + signatures: string[]; + contentText?: string; }; -const collectReasoningFromBlocks = ( - blocks: any[], -): { text: string; signatures: string[] } => { - const texts: string[] = []; - const signatures: string[] = []; - for (const b of blocks) { - if (!b || typeof b !== "object") continue; - const isReasoningish = - b.type === "input_text" || - b.type === "output_text" || - b.type === "reasoning_text" || - b.type === "refusal" || - !b.type; - if (isReasoningish && typeof b.text === "string" && b.text.trim()) { - texts.push(b.text); - } - if (typeof b.signature === "string" && b.signature.trim()) { - signatures.push(b.signature.trim()); - } - } - return { text: texts.join("\n"), signatures }; +const collectReasoningFromBlocks = (blocks: any[]): { text: string; signatures: string[] } => { + const texts: string[] = []; + const signatures: string[] = []; + for (const b of blocks) { + if (!b || typeof b !== "object") continue; + const isReasoningish = + b.type === "input_text" || b.type === "output_text" || b.type === "reasoning_text" || b.type === "refusal" || !b.type; + if (isReasoningish && typeof b.text === "string" && b.text.trim()) { + texts.push(b.text); + } + if (typeof b.signature === "string" && b.signature.trim()) { + signatures.push(b.signature.trim()); + } + } + return { text: texts.join("\n"), signatures }; }; const extractReasoningParts = (msg: ResponsesMessage): ReasoningParts => { - const summaries = (msg.summary ?? []) - .map((s) => (s?.text ?? "").trim()) - .filter(Boolean); - const encryptedRaw = (msg as any).encrypted_content?.trim?.(); - const encrypted = encryptedRaw ? encryptedRaw : undefined; - const signatures: string[] = []; - let contentText = ""; - if (typeof msg.content === "string") { - contentText = msg.content; - } else if (Array.isArray(msg.content)) { - const fromContent = collectReasoningFromBlocks(msg.content as any[]); - contentText = fromContent.text; - signatures.push(...fromContent.signatures); - } - // Some providers stash reasoning under `output` instead of `content` - const out = (msg as any).output; - if (out !== undefined) { - if (typeof out === "string" && out.trim() && !contentText) { - contentText = out; - } else if (Array.isArray(out)) { - const fromOutput = collectReasoningFromBlocks(out as any[]); - if (!contentText && fromOutput.text) contentText = fromOutput.text; - signatures.push(...fromOutput.signatures); - } - } - // Defensive: top-level text-bearing fields some variants use - if (!contentText) { - const topText = - (typeof (msg as any).text === "string" && (msg as any).text) || - (typeof (msg as any).thinking === "string" && (msg as any).thinking) || - ""; - if (topText.trim()) contentText = topText; - } - return { - summaries, - encrypted, - signatures, - contentText: contentText || undefined, - }; + const summaries = (msg.summary ?? []).map((s) => (s?.text ?? "").trim()).filter(Boolean); + const encryptedRaw = (msg as any).encrypted_content?.trim?.(); + const encrypted = encryptedRaw ? encryptedRaw : undefined; + const signatures: string[] = []; + let contentText = ""; + if (typeof msg.content === "string") { + contentText = msg.content; + } else if (Array.isArray(msg.content)) { + const fromContent = collectReasoningFromBlocks(msg.content as any[]); + contentText = fromContent.text; + signatures.push(...fromContent.signatures); + } + // Some providers stash reasoning under `output` instead of `content` + const out = (msg as any).output; + if (out !== undefined) { + if (typeof out === "string" && out.trim() && !contentText) { + contentText = out; + } else if (Array.isArray(out)) { + const fromOutput = collectReasoningFromBlocks(out as any[]); + if (!contentText && fromOutput.text) contentText = fromOutput.text; + signatures.push(...fromOutput.signatures); + } + } + // Defensive: top-level text-bearing fields some variants use + if (!contentText) { + const topText = + (typeof (msg as any).text === "string" && (msg as any).text) || + (typeof (msg as any).thinking === "string" && (msg as any).thinking) || + ""; + if (topText.trim()) contentText = topText; + } + return { + summaries, + encrypted, + signatures, + contentText: contentText || undefined, + }; }; const extractChatReasoning = (message: any): string => { - if (!message) return ""; - if (typeof message.reasoning === "string" && message.reasoning.trim()) { - return message.reasoning; - } - if (Array.isArray(message.reasoning_details)) { - const parts = (message.reasoning_details as any[]) - .map((d) => (typeof d?.text === "string" ? d.text : (d?.summary ?? ""))) - .map((t: string) => (typeof t === "string" ? t.trim() : "")) - .filter(Boolean); - if (parts.length > 0) return parts.join("\n"); - } - return ""; + if (!message) return ""; + if (typeof message.reasoning === "string" && message.reasoning.trim()) { + return message.reasoning; + } + if (Array.isArray(message.reasoning_details)) { + const parts = (message.reasoning_details as any[]) + .map((d) => (typeof d?.text === "string" ? d.text : (d?.summary ?? ""))) + .map((t: string) => (typeof t === "string" ? t.trim() : "")) + .filter(Boolean); + if (parts.length > 0) return parts.join("\n"); + } + return ""; }; const getResponsesRole = (msg: ResponsesMessage): MessageRole => { - if (msg.type === "reasoning") return "reasoning"; - if ( - msg.type && - (msg.type.endsWith("_call") || - msg.type.endsWith("_call_output") || - msg.type === "mcp_list_tools" || - msg.type === "mcp_approval_request" || - msg.type === "mcp_approval_responses") - ) { - return "tool"; - } - const r = msg.role; - if (r === "user") return "user"; - if (r === "assistant") return "assistant"; - if (r === "system" || r === "developer") return "system"; - return "assistant"; + if (msg.type === "reasoning") return "reasoning"; + if ( + msg.type && + (msg.type.endsWith("_call") || + msg.type.endsWith("_call_output") || + msg.type === "mcp_list_tools" || + msg.type === "mcp_approval_request" || + msg.type === "mcp_approval_responses") + ) { + return "tool"; + } + const r = msg.role; + if (r === "user") return "user"; + if (r === "assistant") return "assistant"; + if (r === "system" || r === "developer") return "system"; + return "assistant"; }; const isPlainAssistantResponsesMessage = (m: ResponsesMessage): boolean => { - if (m.type && m.type !== "message") return false; - return getResponsesRole(m) === "assistant"; + if (m.type && m.type !== "message") return false; + return getResponsesRole(m) === "assistant"; }; -const isReasoningResponsesMessage = (m: ResponsesMessage): boolean => - m.type === "reasoning"; +const isReasoningResponsesMessage = (m: ResponsesMessage): boolean => m.type === "reasoning"; // Streaming providers can emit a single logical assistant turn (or reasoning // item) as many small messages. Collapse adjacent ones so the UI shows one // bubble per turn instead of N "1 line" bubbles. -const coalesceResponsesMessages = ( - msgs: ResponsesMessage[], -): ResponsesMessage[] => { - const out: ResponsesMessage[] = []; - for (const m of msgs) { - const last = out[out.length - 1]; - if ( - last && - isPlainAssistantResponsesMessage(last) && - isPlainAssistantResponsesMessage(m) - ) { - const merged = extractResponsesText(last) + extractResponsesText(m); - out[out.length - 1] = { - ...last, - content: [{ type: "output_text", text: merged } as any], - }; - continue; - } - if ( - last && - isReasoningResponsesMessage(last) && - isReasoningResponsesMessage(m) - ) { - const aSum = last.summary ?? []; - const bSum = m.summary ?? []; - const aEnc = (last as any).encrypted_content ?? ""; - const bEnc = (m as any).encrypted_content ?? ""; - const joinedEnc = `${aEnc}${bEnc}`; - out[out.length - 1] = { - ...last, - summary: [...aSum, ...bSum], - encrypted_content: joinedEnc ? joinedEnc : undefined, - } as ResponsesMessage; - continue; - } - out.push(m); - } - return out.filter((m) => { - if (!isPlainAssistantResponsesMessage(m)) return true; - return extractResponsesText(m).length > 0; - }); +const coalesceResponsesMessages = (msgs: ResponsesMessage[]): ResponsesMessage[] => { + const out: ResponsesMessage[] = []; + for (const m of msgs) { + const last = out[out.length - 1]; + if (last && isPlainAssistantResponsesMessage(last) && isPlainAssistantResponsesMessage(m)) { + const merged = extractResponsesText(last) + extractResponsesText(m); + out[out.length - 1] = { + ...last, + content: [{ type: "output_text", text: merged } as any], + }; + continue; + } + if (last && isReasoningResponsesMessage(last) && isReasoningResponsesMessage(m)) { + const aSum = last.summary ?? []; + const bSum = m.summary ?? []; + const aEnc = (last as any).encrypted_content ?? ""; + const bEnc = (m as any).encrypted_content ?? ""; + const joinedEnc = `${aEnc}${bEnc}`; + out[out.length - 1] = { + ...last, + summary: [...aSum, ...bSum], + encrypted_content: joinedEnc ? joinedEnc : undefined, + } as ResponsesMessage; + continue; + } + out.push(m); + } + return out.filter((m) => { + if (!isPlainAssistantResponsesMessage(m)) return true; + return extractResponsesText(m).length > 0; + }); }; const extractMessageText = (message: any): string => { - if (!message || message.content == null) return ""; - if (typeof message.content === "string") return message.content; - if (Array.isArray(message.content)) { - return message.content - .filter( - (block: any) => - block && - (block.type === "text" || - block.type === "input_text" || - block.type === "output_text") && - block.text, - ) - .map((block: any) => block.text) - .join("\n"); - } - return ""; + if (!message || message.content == null) return ""; + if (typeof message.content === "string") return message.content; + if (Array.isArray(message.content)) { + return message.content + .filter((block: any) => block && (block.type === "text" || block.type === "input_text" || block.type === "output_text") && block.text) + .map((block: any) => block.text) + .join("\n"); + } + return ""; }; const formatJsonSafe = (str: string | undefined): string => { - try { - return JSON.stringify(JSON.parse(str || ""), null, 2); - } catch { - return str || ""; - } + try { + return JSON.stringify(JSON.parse(str || ""), null, 2); + } catch { + return str || ""; + } }; const formatToolChoice = (value: unknown): string => { - if (typeof value === "string") return value; - try { - return JSON.stringify(value); - } catch { - return String(value); - } + if (typeof value === "string") return value; + try { + return JSON.stringify(value); + } catch { + return String(value); + } }; // Helper to detect passthrough operations -const isPassthroughOperation = (object: string) => - object === "passthrough" || object === "passthrough_stream"; +const isPassthroughOperation = (object: string) => object === "passthrough" || object === "passthrough_stream"; // Helper to detect container operations (for hiding irrelevant fields like Model/Tokens) const isContainerOperation = (object: string) => { - const containerTypes = [ - "container_create", - "container_list", - "container_retrieve", - "container_delete", - "container_file_create", - "container_file_list", - "container_file_retrieve", - "container_file_content", - "container_file_delete", - ]; - return containerTypes.includes(object?.toLowerCase()); + const containerTypes = [ + "container_create", + "container_list", + "container_retrieve", + "container_delete", + "container_file_create", + "container_file_list", + "container_file_retrieve", + "container_file_content", + "container_file_delete", + ]; + return containerTypes.includes(object?.toLowerCase()); }; const statusPillStyles: Record = { - success: - "bg-green-50 text-green-700 border-green-200 dark:bg-green-950/40 dark:text-green-400 dark:border-green-900", - error: - "bg-red-50 text-red-700 border-red-200 dark:bg-red-950/40 dark:text-red-400 dark:border-red-900", - processing: - "bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-950/40 dark:text-blue-400 dark:border-blue-900", - cancelled: - "bg-gray-50 text-gray-700 border-gray-200 dark:bg-gray-900/40 dark:text-gray-400 dark:border-gray-800", + success: "bg-green-50 text-green-700 border-green-200 dark:bg-green-950/40 dark:text-green-400 dark:border-green-900", + error: "bg-red-50 text-red-700 border-red-200 dark:bg-red-950/40 dark:text-red-400 dark:border-red-900", + processing: "bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-950/40 dark:text-blue-400 dark:border-blue-900", + cancelled: "bg-gray-50 text-gray-700 border-gray-200 dark:bg-gray-900/40 dark:text-gray-400 dark:border-gray-800", }; const statusDotStyles: Record = { - success: "bg-green-500", - error: "bg-red-500", - processing: "bg-blue-500", - cancelled: "bg-gray-400", + success: "bg-green-500", + error: "bg-red-500", + processing: "bg-blue-500", + cancelled: "bg-gray-400", }; function StatusPill({ status }: { status: Status }) { - return ( - - - {status} - - ); + return ( + + + {status} + + ); } function HeroStat({ - label, - value, - sub, - mono = false, - valueClass, - hasRightBorder = false, + label, + value, + sub, + mono = false, + valueClass, + hasRightBorder = false, }: { - label: string; - value: ReactNode; - sub?: ReactNode; - mono?: boolean; - valueClass?: string; - hasRightBorder?: boolean; + label: string; + value: ReactNode; + sub?: ReactNode; + mono?: boolean; + valueClass?: string; + hasRightBorder?: boolean; }) { - return ( -
-
- {label} -
-
- {value} -
- {sub ? ( -
- {sub} -
- ) : null} -
- ); + return ( +
+
{label}
+
+ {value} +
+ {sub ?
{sub}
: null} +
+ ); } function CopyInlineButton({ text, testId }: { text: string; testId?: string }) { - const { copy } = useCopyToClipboard({ successMessage: "Copied" }); - return ( - - ); + const { copy } = useCopyToClipboard({ successMessage: "Copied" }); + return ( + + ); } type MessageRole = "system" | "user" | "assistant" | "reasoning" | "tool"; const messageToneClass: Record = { - system: "bg-zinc-50 border-zinc-200 dark:bg-zinc-900/40 dark:border-zinc-800", - user: "bg-blue-50/60 border-blue-200 dark:bg-blue-950/30 dark:border-blue-900", - assistant: "bg-white border-zinc-200 dark:bg-zinc-900 dark:border-zinc-800", - reasoning: - "bg-violet-50/70 border-violet-200 dark:bg-violet-950/30 dark:border-violet-900", - tool: "bg-amber-50/70 border-amber-200 dark:bg-amber-950/30 dark:border-amber-900", + system: "bg-zinc-50 border-zinc-200 dark:bg-zinc-900/40 dark:border-zinc-800", + user: "bg-blue-50/60 border-blue-200 dark:bg-blue-950/30 dark:border-blue-900", + assistant: "bg-white border-zinc-200 dark:bg-zinc-900 dark:border-zinc-800", + reasoning: "bg-violet-50/70 border-violet-200 dark:bg-violet-950/30 dark:border-violet-900", + tool: "bg-amber-50/70 border-amber-200 dark:bg-amber-950/30 dark:border-amber-900", }; const messageDotClass: Record = { - system: "bg-zinc-400", - user: "bg-blue-500", - assistant: "bg-zinc-900 dark:bg-zinc-100", - reasoning: "bg-violet-500", - tool: "bg-amber-500", + system: "bg-zinc-400", + user: "bg-blue-500", + assistant: "bg-zinc-900 dark:bg-zinc-100", + reasoning: "bg-violet-500", + tool: "bg-amber-500", }; const messageRoleLabel: Record = { - system: "System", - user: "User", - assistant: "Assistant", - reasoning: "Reasoning", - tool: "Tool Result", + system: "System", + user: "User", + assistant: "Assistant", + reasoning: "Reasoning", + tool: "Tool Result", }; function RoutingDecisionLogs({ logs }: { logs: string }) { - const { copy } = useCopyToClipboard({ successMessage: "Copied" }); - return ( -
-
-
Routing Decision Logs
- -
-
- {logs - .split("\n") - .filter((l) => l.trim()) - .map((line, i) => { - const m = line.match(/^\[(\d+)\]\s+\[([^\]]+)\]\s+-\s+(.*)$/); - const ts = m ? Number(m[1]) : null; - const scope = m ? m[2] : null; - const message = m ? m[3] : line; - return ( -
- {ts != null ? ( - - {format(new Date(ts), "HH:mm:ss.SSS")} - - ) : null} - {scope ? ( - - {RoutingEngineUsedLabels[ - scope as keyof typeof RoutingEngineUsedLabels - ] ?? scope} - - ) : null} - - {message} - -
- ); - })} -
-
- ); + const { copy } = useCopyToClipboard({ successMessage: "Copied" }); + return ( +
+
+
Routing Decision Logs
+ +
+
+ {logs + .split("\n") + .filter((l) => l.trim()) + .map((line, i) => { + const m = line.match(/^\[(\d+)\]\s+\[([^\]]+)\]\s+-\s+(.*)$/); + const ts = m ? Number(m[1]) : null; + const scope = m ? m[2] : null; + const message = m ? m[3] : line; + return ( +
+ {ts != null ? {format(new Date(ts), "HH:mm:ss.SSS")} : null} + {scope ? ( + + {RoutingEngineUsedLabels[scope as keyof typeof RoutingEngineUsedLabels] ?? scope} + + ) : null} + {message} +
+ ); + })} +
+
+ ); } function EncryptedReveal({ text, label }: { text: string; label: string }) { - const [open, setOpen] = useState(false); - return ( -
- - {open ? ( -
-          {text}
-        
- ) : null} -
- ); + const [open, setOpen] = useState(false); + return ( +
+ + {open ?
{text}
: null} +
+ ); } -function CollapsibleCode({ - text, - preview = 3, - lang, - mono = true, -}: { - text: string; - preview?: number; - lang?: string; - mono?: boolean; -}) { - const [open, setOpen] = useState(false); - const lines = text.split("\n"); - const shown = open ? lines : lines.slice(0, preview); - const hasMore = lines.length > preview; - const moreCount = lines.length - preview; - return ( - <> - {mono ? ( -
-          {shown.join("\n")}
-        
- ) : ( -
- {shown.join("\n")} -
- )} - {hasMore && ( -
- - - {lines.length} lines{lang ? ` · ${lang}` : ""} - -
- )} - - ); +function CollapsibleCode({ text, preview = 3, lang, mono = true }: { text: string; preview?: number; lang?: string; mono?: boolean }) { + const [open, setOpen] = useState(false); + const lines = text.split("\n"); + const shown = open ? lines : lines.slice(0, preview); + const hasMore = lines.length > preview; + const moreCount = lines.length - preview; + return ( + <> + {mono ? ( +
{shown.join("\n")}
+ ) : ( +
{shown.join("\n")}
+ )} + {hasMore && ( +
+ + + {lines.length} lines{lang ? ` · ${lang}` : ""} + +
+ )} + + ); } -function MessageRow({ - role, - meta, - children, - last = false, -}: { - role: MessageRole; - meta?: string; - children: ReactNode; - last?: boolean; -}) { - return ( -
-
- - {!last &&
} -
-
-
- - {messageRoleLabel[role]} - - {meta ? ( - {meta} - ) : null} -
-
- {children} -
-
-
- ); +function MessageRow({ role, meta, children, last = false }: { role: MessageRole; meta?: string; children: ReactNode; last?: boolean }) { + return ( +
+
+ + {!last &&
} +
+
+
+ {messageRoleLabel[role]} + {meta ? {meta} : null} +
+
{children}
+
+
+ ); } interface LogDetailViewProps { - log: LogEntry | null; - resolvedSelectedPromptName?: string; // Current prompt name from prompt-repo when `selected_prompt_id` is set; falls back to stored log name - loading?: boolean; - handleDelete?: (log: LogEntry) => void; - onClose?: () => void; - headerAction?: ReactNode; - onFilterByParentRequestId?: (parentRequestId: string) => void; + log: LogEntry | null; + resolvedSelectedPromptName?: string; // Current prompt name from prompt-repo when `selected_prompt_id` is set; falls back to stored log name + loading?: boolean; + handleDelete?: (log: LogEntry) => void; + onClose?: () => void; + headerAction?: ReactNode; + onFilterByParentRequestId?: (parentRequestId: string) => void; } export function LogDetailView({ - log, - resolvedSelectedPromptName, - loading = false, - handleDelete, - onClose, - headerAction, - onFilterByParentRequestId, + log, + resolvedSelectedPromptName, + loading = false, + handleDelete, + onClose, + headerAction, + onFilterByParentRequestId, }: LogDetailViewProps) { - const { copy: copyBody } = useCopyToClipboard({ - successMessage: "Request body copied to clipboard", - errorMessage: "Failed to copy request body", - }); - const allRoles: MessageRole[] = [ - "system", - "user", - "assistant", - "tool", - "reasoning", - ]; - const [visibleRoles, setVisibleRoles] = useState>( - new Set(allRoles), - ); + const { copy: copyBody } = useCopyToClipboard({ + successMessage: "Request body copied to clipboard", + errorMessage: "Failed to copy request body", + }); + const allRoles: MessageRole[] = ["system", "user", "assistant", "tool", "reasoning"]; + const [visibleRoles, setVisibleRoles] = useState>(new Set(allRoles)); - if (!log) return null; + if (!log) return null; - const selectedPromptDisplayName = - resolvedSelectedPromptName ?? log.selected_prompt_name ?? ""; + const selectedPromptDisplayName = resolvedSelectedPromptName ?? log.selected_prompt_name ?? ""; - const isContainer = isContainerOperation(log.object); - const showTabs = !isContainer; - const isPassthrough = isPassthroughOperation(log.object); - const isRealtimeTurn = log.object === "realtime.turn"; - const passthroughParams = isPassthrough - ? (log.params as { - method?: string; - path?: string; - raw_query?: string; - status_code?: number; - }) - : null; + const isContainer = isContainerOperation(log.object); + const showTabs = !isContainer; + const isPassthrough = isPassthroughOperation(log.object); + const isRealtimeTurn = log.object === "realtime.turn"; + const passthroughParams = isPassthrough + ? (log.params as { + method?: string; + path?: string; + raw_query?: string; + status_code?: number; + }) + : null; - let toolsParameter = null; - if (log.params?.tools) { - try { - toolsParameter = JSON.stringify(log.params.tools, null, 2); - } catch {} - } + let toolsParameter = null; + if (log.params?.tools) { + try { + toolsParameter = JSON.stringify(log.params.tools, null, 2); + } catch {} + } - const audioFormat = - (log.params as any)?.audio?.format || - (log.params as any)?.extra_params?.audio?.format || - undefined; - const rawRequest = log.raw_request; - const rawResponse = log.raw_response; - const passthroughRequestBody = log.passthrough_request_body; - const passthroughResponseBody = log.passthrough_response_body; - const videoOutput = - log.video_generation_output || - log.video_retrieve_output || - log.video_download_output; - const videoListOutput = log.video_list_output; - const pluginLogCount = (() => { - if (!log.plugin_logs) return 0; - try { - const parsed = JSON.parse(log.plugin_logs); - if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { - return Object.values(parsed).reduce( - (sum, v) => sum + (Array.isArray(v) ? v.length : 0), - 0, - ); - } - } catch {} - return 0; - })(); + const audioFormat = (log.params as any)?.audio?.format || (log.params as any)?.extra_params?.audio?.format || undefined; + const rawRequest = log.raw_request; + const rawResponse = log.raw_response; + const passthroughRequestBody = log.passthrough_request_body; + const passthroughResponseBody = log.passthrough_response_body; + const videoOutput = log.video_generation_output || log.video_retrieve_output || log.video_download_output; + const videoListOutput = log.video_list_output; + const pluginLogCount = (() => { + if (!log.plugin_logs) return 0; + try { + const parsed = JSON.parse(log.plugin_logs); + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + return Object.values(parsed).reduce((sum, v) => sum + (Array.isArray(v) ? v.length : 0), 0); + } + } catch {} + return 0; + })(); - return loading ? ( -
- -
- ) : ( - <> - {/* Breadcrumb header with actions */} -
-
- {headerAction} - Request details -
- {onClose ? ( - - - - - - - {!isPassthrough && ( - copyRequestBody(log, copyBody)} - data-testid="logdetails-copy-request-body-button" - > - - Copy request body - - )} - - downloadAsJson(log, `log-${log.id ?? "export"}.json`) - } - data-testid="logdetails-export-log-button" - > - - Export as JSON - + return loading ? ( +
+ +
+ ) : ( + <> + {/* Breadcrumb header with actions */} +
+
+ {headerAction} + Request details +
+ {onClose ? ( + + + + + + + {!isPassthrough && ( + copyRequestBody(log, copyBody)} data-testid="logdetails-copy-request-body-button"> + + Copy request body + + )} + downloadAsJson(log, `log-${log.id ?? "export"}.json`)} + data-testid="logdetails-export-log-button" + > + + Export as JSON + - {handleDelete ? ( - <> - - - - - Delete log - - {" "} - - ) : null} - - - - - - Are you sure you want to delete this log? - - - This action cannot be undone. This will permanently delete the - log entry. - - - - - Cancel - - { - if (handleDelete) handleDelete(log); - onClose(); - }} - > - Delete - - - - - ) : null} -
-
-
-
-
- - - {RequestTypeLabels[ - log.object as keyof typeof RequestTypeLabels - ] ?? log.object} - - {log.routing_rule && ( - - rule: {log.routing_rule.name} - - )} - {log.metadata?.isAsyncRequest ? ( - - Async - - ) : null} - {log.cache_debug?.hit_type === "direct" ? ( - - Direct Cache - - ) : null} - {log.cache_debug?.hit_type === "semantic" ? ( - - Semantic Cache - - ) : null} - {(log.is_large_payload_request || - log.is_large_payload_response) && ( - - Large Payload - - )} - {isRealtimeTurn && log.metadata?.realtime_transport && ( - - {formatRealtimeTransport(log.metadata.realtime_transport)} - - )} - {isRealtimeTurn && log.metadata?.realtime_voice && ( - - {log.metadata.realtime_voice} - - )} -
-
-
- Request -
- - {log.id || "—"} - - {log.id ? ( - - ) : null} -
- {log.cache_debug?.cache_id && ( -
-
- Cache {log.cache_debug.cache_hit ? "(hit)" : "(miss)"} -
- - {log.cache_debug.cache_id} - - -
- )} - {log.routing_rule && ( -
-
- Rule -
- - “{log.routing_rule.name}” - -
- )} - {log.selected_key && ( -
-
- Key -
- - {log.selected_key.name} - -
- )} -
-
- - {log.provider} -
-
-
- { - if (!log.timestamp) return ""; - const start = new Date(log.timestamp); - if (isNaN(start.getTime())) return ""; - const startStr = format(start, "HH:mm:ss"); - if (log.latency == null || isNaN(log.latency)) return startStr; - return `${startStr} → ${format(addMilliseconds(start, log.latency), "HH:mm:ss")}`; - })()} - hasRightBorder - /> - - - - {isRealtimeTurn ? ( - - ) : ( - - )} -
-
-
- - More details - - - timings, request meta, tokens, caching, metadata - - - - -
-
- -
- { - const d = log.timestamp ? new Date(log.timestamp) : null; - return d && !isNaN(d.getTime()) - ? format(d, "yyyy-MM-dd hh:mm:ss aa") - : "N/A"; - })()} - /> - { - const d = log.timestamp ? new Date(log.timestamp) : null; - return d && !isNaN(d.getTime()) - ? format( - addMilliseconds(d, log.latency || 0), - "yyyy-MM-dd hh:mm:ss aa", - ) - : "N/A"; - })()} - /> - {log.latency.toFixed(2)}ms
- ) - } - /> -
-
- -
- -
- - - {log.provider} - - } - /> - {!isContainer && ( - - )} - {!isContainer && log.alias && ( - - )} - - {RequestTypeLabels[ - log.object as keyof typeof RequestTypeLabels - ] ?? - log.object ?? - "unknown"} -
- } - /> - {log.stop_reason && ( - - {log.stop_reason} - - } - /> - )} - {log.parent_request_id && ( - - - - onFilterByParentRequestId( - log.parent_request_id as string, - ) - } - > - {log.parent_request_id} - - - - Filter this session - - - ) : ( - - {log.parent_request_id} - - ) - } - /> - )} - {log.selected_key && ( - - )} - {(log.selected_prompt_id || - log.selected_prompt_name || - log.selected_prompt_version) && ( - - {selectedPromptDisplayName} - {selectedPromptDisplayName && log.selected_prompt_version - ? " · " - : ""} - {log.selected_prompt_version ? ( - <>v{log.selected_prompt_version} - ) : null} - - } - /> - )} - {log.number_of_retries > 0 && ( - - )} - {log.team_id && ( - - {log.team_name || log.team_id} - - } - /> - )} - {log.customer_id && ( - - {log.customer_name || log.customer_id} - - } - /> - )} - {log.business_unit_id && ( - - {log.business_unit_name || log.business_unit_id} - - } - /> - )} - {log.user_id && ( - - - - {log.user_name || log.user_id} - - - - {log.user_name ? log.user_id : "Filter by user"} - - - } - /> - )} - {log.fallback_index > 0 && ( - - )} - {log.virtual_key && ( - - )} - {log.routing_engines_used && - log.routing_engines_used.length > 0 && ( - - {log.routing_engines_used.map((engine) => ( - -
- {RoutingEngineUsedIcons[ - engine as keyof typeof RoutingEngineUsedIcons - ]?.()} - - {RoutingEngineUsedLabels[ - engine as keyof typeof RoutingEngineUsedLabels - ] ?? engine} - -
-
- ))} -
- } - /> - )} - {log.routing_rule && ( - - )} + {handleDelete ? ( + <> + + + + + Delete log + + {" "} + + ) : null} + + + + + Are you sure you want to delete this log? + This action cannot be undone. This will permanently delete the log entry. + + + Cancel + { + if (handleDelete) handleDelete(log); + onClose(); + }} + > + Delete + + + + + ) : null} +
+
+
+
+
+ + + {RequestTypeLabels[log.object as keyof typeof RequestTypeLabels] ?? log.object} + + {log.routing_rule && ( + + rule: {log.routing_rule.name} + + )} + {log.metadata?.isAsyncRequest ? ( + + Async + + ) : null} + {log.cache_debug?.hit_type === "direct" ? ( + + Direct Cache + + ) : null} + {log.cache_debug?.hit_type === "semantic" ? ( + + Semantic Cache + + ) : null} + {(log.is_large_payload_request || log.is_large_payload_response) && ( + + Large Payload + + )} + {isRealtimeTurn && log.metadata?.realtime_transport && ( + + {formatRealtimeTransport(log.metadata.realtime_transport)} + + )} + {isRealtimeTurn && log.metadata?.realtime_voice && ( + + {log.metadata.realtime_voice} + + )} +
+
+
Request
+ {log.id || "—"} + {log.id ? : null} +
+ {log.cache_debug?.cache_id && ( +
+
+ Cache {log.cache_debug.cache_hit ? "(hit)" : "(miss)"} +
+ {log.cache_debug.cache_id} + +
+ )} + {log.routing_rule && ( +
+
Rule
+ “{log.routing_rule.name}” +
+ )} + {log.selected_key && ( +
+
Key
+ {log.selected_key.name} +
+ )} +
+
+ + {log.provider} +
+
+
+ { + if (!log.timestamp) return ""; + const start = new Date(log.timestamp); + if (isNaN(start.getTime())) return ""; + const startStr = format(start, "HH:mm:ss"); + if (log.latency == null || isNaN(log.latency)) return startStr; + return `${startStr} → ${format(addMilliseconds(start, log.latency), "HH:mm:ss")}`; + })()} + hasRightBorder + /> + + + + {isRealtimeTurn ? ( + + ) : ( + + )} +
+
+
+ + More details + + timings, request meta, tokens, caching, metadata + + + +
+
+ +
+ { + const d = log.timestamp ? new Date(log.timestamp) : null; + return d && !isNaN(d.getTime()) ? format(d, "yyyy-MM-dd hh:mm:ss aa") : "N/A"; + })()} + /> + { + const d = log.timestamp ? new Date(log.timestamp) : null; + return d && !isNaN(d.getTime()) ? format(addMilliseconds(d, log.latency || 0), "yyyy-MM-dd hh:mm:ss aa") : "N/A"; + })()} + /> + {log.latency.toFixed(2)}ms
} + /> +
+
+ +
+ +
+ + + {log.provider} + + } + /> + {!isContainer && } + {!isContainer && log.alias && } + + {RequestTypeLabels[log.object as keyof typeof RequestTypeLabels] ?? log.object ?? "unknown"} +
+ } + /> + {log.stop_reason && ( + + {log.stop_reason} + + } + /> + )} + {log.parent_request_id && ( + + + onFilterByParentRequestId(log.parent_request_id as string)} + > + {log.parent_request_id} + + + Filter this session + + ) : ( + {log.parent_request_id} + ) + } + /> + )} + {log.selected_key && } + {(log.selected_prompt_id || log.selected_prompt_name || log.selected_prompt_version) && ( + + {selectedPromptDisplayName} + {selectedPromptDisplayName && log.selected_prompt_version ? " · " : ""} + {log.selected_prompt_version ? <>v{log.selected_prompt_version} : null} + + } + /> + )} + {log.number_of_retries > 0 && ( + + )} + {log.team_id && ( + + {log.team_name || log.team_id} + + } + /> + )} + {log.customer_id && ( + + {log.customer_name || log.customer_id} + + } + /> + )} + {log.business_unit_id && ( + + {log.business_unit_name || log.business_unit_id} + + } + /> + )} + {log.user_id && ( + + + + {log.user_name || log.user_id} + + + {log.user_name ? log.user_id : "Filter by user"} + + } + /> + )} + {log.fallback_index > 0 && } + {log.virtual_key && } + {log.routing_engines_used && log.routing_engines_used.length > 0 && ( + + {log.routing_engines_used.map((engine) => ( + +
+ {RoutingEngineUsedIcons[engine as keyof typeof RoutingEngineUsedIcons]?.()} + {RoutingEngineUsedLabels[engine as keyof typeof RoutingEngineUsedLabels] ?? engine} +
+
+ ))} +
+ } + /> + )} + {log.routing_rule && } - {(log.params as any)?.audio && ( - <> - {(log.params as any).audio.format && ( - - )} - {(log.params as any).audio.voice && ( - - )} - - )} + {(log.params as any)?.audio && ( + <> + {(log.params as any).audio.format && ( + + )} + {(log.params as any).audio.voice && ( + + )} + + )} - {isRealtimeTurn && ( - <> - {log.metadata?.realtime_session_id && ( - - - {log.metadata.realtime_session_id} - - - - } - /> - )} - {log.metadata?.provider_session_id && ( - - - {log.metadata.provider_session_id} - - - - } - /> - )} - {log.metadata?.realtime_transport && ( - - )} - {log.metadata?.realtime_voice && ( - - )} - {log.metadata?.realtime_source && ( - - )} - {log.metadata?.realtime_event_type && ( - - {log.metadata.realtime_event_type} - - } - /> - )} - - )} + {isRealtimeTurn && ( + <> + {log.metadata?.realtime_session_id && ( + + {log.metadata.realtime_session_id} + + + } + /> + )} + {log.metadata?.provider_session_id && ( + + {log.metadata.provider_session_id} + + + } + /> + )} + {log.metadata?.realtime_transport && ( + + )} + {log.metadata?.realtime_voice && ( + + )} + {log.metadata?.realtime_source && ( + + )} + {log.metadata?.realtime_event_type && ( + {log.metadata.realtime_event_type}} + /> + )} + + )} - {passthroughParams && ( - <> - {passthroughParams.method && ( - - )} - {passthroughParams.path && ( - - )} - {passthroughParams.raw_query && ( - - )} - {(passthroughParams.status_code ?? 0) !== 0 && ( - - )} - - )} + {passthroughParams && ( + <> + {passthroughParams.method && } + {passthroughParams.path && } + {passthroughParams.raw_query && ( + + )} + {(passthroughParams.status_code ?? 0) !== 0 && ( + + )} + + )} - {log.params && - Object.keys(log.params).length > 0 && - Object.entries(log.params) - .filter(([key]) => { - const passthroughKeys = [ - "method", - "path", - "raw_query", - "status_code", - ]; - return ( - key !== "tools" && - key !== "instructions" && - key !== "audio" && - !(isPassthrough && passthroughKeys.includes(key)) - ); - }) - .filter( - ([_, value]) => - typeof value === "boolean" || - typeof value === "number" || - typeof value === "string", - ) - .map(([key, value]) => ( - - ))} -
-
- {log.status === "success" && !isContainer && !isPassthrough && ( - <> - -
- -
- - - - - {isRealtimeTurn && ( - <> - - - - - {(log.token_usage?.completion_tokens_details - ?.reasoning_tokens ?? 0) > 0 && ( - - )} - - )} - {!isRealtimeTurn && - log.token_usage?.prompt_tokens_details && ( - <> - {log.token_usage.prompt_tokens_details - .cached_read_tokens && ( - - )} - {log.token_usage.prompt_tokens_details - .cached_write_tokens && ( - - )} - {log.token_usage.prompt_tokens_details.audio_tokens && ( - - )} - - )} - {!isRealtimeTurn && - log.token_usage?.completion_tokens_details && ( - <> - {log.token_usage.completion_tokens_details - .reasoning_tokens && ( - - )} - {log.token_usage.completion_tokens_details - .audio_tokens && ( - - )} - {log.token_usage.completion_tokens_details - .accepted_prediction_tokens && ( - - )} - {log.token_usage.completion_tokens_details - .rejected_prediction_tokens && ( - - )} - - )} -
-
- {(() => { - const params = log.params as any; - const reasoning = params?.reasoning; - if ( - !reasoning || - typeof reasoning !== "object" || - Object.keys(reasoning).length === 0 - ) { - return null; - } - return ( - <> - -
- -
- {reasoning.effort && ( - - {reasoning.effort} - - } - /> - )} - {reasoning.summary && ( - - {reasoning.summary} - - } - /> - )} - {reasoning.generate_summary && ( - - {reasoning.generate_summary} - - } - /> - )} - {reasoning.max_tokens && ( - - )} -
-
- - ); - })()} - {log.cache_debug && ( - <> - -
- -
- {log.cache_debug.cache_hit ? ( - <> - - {log.cache_debug.hit_type} - - } - /> - {log.cache_debug.hit_type === "semantic" && ( - <> - {log.cache_debug.provider_used && ( - - {log.cache_debug.provider_used} - - } - /> - )} - {log.cache_debug.model_used && ( - - )} - {log.cache_debug.threshold && ( - - )} - {log.cache_debug.similarity && ( - - )} - {log.cache_debug.input_tokens && ( - - )} - - )} - - ) : ( - <> - {log.cache_debug.provider_used && ( - - {log.cache_debug.provider_used} - - } - /> - )} - {log.cache_debug.model_used && ( - - )} - {log.cache_debug.input_tokens && ( - - )} - - )} -
-
- - )} - - )} - {!isContainer && - !isPassthrough && - log.metadata && - Object.keys(log.metadata).filter((k) => { - if (k === "isAsyncRequest") return false; - if ( - isRealtimeTurn && - [ - "realtime_session_id", - "provider_session_id", - "realtime_source", - "realtime_event_type", - "realtime_transport", - "realtime_voice", - "realtime", - ].includes(k) - ) - return false; - return true; - }).length > 0 && ( - <> - -
- -
- {Object.entries(log.metadata) - .filter(([key]) => { - if (key === "isAsyncRequest") return false; - if ( - isRealtimeTurn && - [ - "realtime_session_id", - "provider_session_id", - "realtime_source", - "realtime_event_type", - "realtime_transport", - "realtime_voice", - "realtime", - ].includes(key) - ) - return false; - return true; - }) - .map(([key, value]) => ( - - ))} -
-
- - )} - - - - - {showTabs && ( - - Messages - {log.input_history?.length ? ( - - {log.input_history.length + (log.output_message ? 1 : 0)} - - ) : null} - - )} + {log.params && + Object.keys(log.params).length > 0 && + Object.entries(log.params) + .filter(([key]) => { + const passthroughKeys = ["method", "path", "raw_query", "status_code"]; + return ( + key !== "tools" && key !== "instructions" && key !== "audio" && !(isPassthrough && passthroughKeys.includes(key)) + ); + }) + .filter(([_, value]) => typeof value === "boolean" || typeof value === "number" || typeof value === "string") + .map(([key, value]) => )} + + + {log.status === "success" && !isContainer && !isPassthrough && ( + <> + +
+ +
+ + + + + {isRealtimeTurn && ( + <> + + + + + {(log.token_usage?.completion_tokens_details?.reasoning_tokens ?? 0) > 0 && ( + + )} + + )} + {!isRealtimeTurn && log.token_usage?.prompt_tokens_details && ( + <> + {log.token_usage.prompt_tokens_details.cached_read_tokens && ( + + )} + {log.token_usage.prompt_tokens_details.cached_write_tokens && ( + + )} + {log.token_usage.prompt_tokens_details.audio_tokens && ( + + )} + + )} + {!isRealtimeTurn && log.token_usage?.completion_tokens_details && ( + <> + {log.token_usage.completion_tokens_details.reasoning_tokens && ( + + )} + {log.token_usage.completion_tokens_details.audio_tokens && ( + + )} + {log.token_usage.completion_tokens_details.accepted_prediction_tokens && ( + + )} + {log.token_usage.completion_tokens_details.rejected_prediction_tokens && ( + + )} + + )} +
+
+ {(() => { + const params = log.params as any; + const reasoning = params?.reasoning; + if (!reasoning || typeof reasoning !== "object" || Object.keys(reasoning).length === 0) { + return null; + } + return ( + <> + +
+ +
+ {reasoning.effort && ( + + {reasoning.effort} + + } + /> + )} + {reasoning.summary && ( + + {reasoning.summary} + + } + /> + )} + {reasoning.generate_summary && ( + + {reasoning.generate_summary} + + } + /> + )} + {reasoning.max_tokens && } +
+
+ + ); + })()} + {log.cache_debug && ( + <> + +
+ +
+ {log.cache_debug.cache_hit ? ( + <> + + {log.cache_debug.hit_type} + + } + /> + {log.cache_debug.hit_type === "semantic" && ( + <> + {log.cache_debug.provider_used && ( + + {log.cache_debug.provider_used} + + } + /> + )} + {log.cache_debug.model_used && ( + + )} + {log.cache_debug.threshold && ( + + )} + {log.cache_debug.similarity && ( + + )} + {log.cache_debug.input_tokens && ( + + )} + + )} + + ) : ( + <> + {log.cache_debug.provider_used && ( + + {log.cache_debug.provider_used} + + } + /> + )} + {log.cache_debug.model_used && ( + + )} + {log.cache_debug.input_tokens && ( + + )} + + )} +
+
+ + )} + + )} + {!isContainer && + !isPassthrough && + log.metadata && + Object.keys(log.metadata).filter((k) => { + if (k === "isAsyncRequest") return false; + if ( + isRealtimeTurn && + [ + "realtime_session_id", + "provider_session_id", + "realtime_source", + "realtime_event_type", + "realtime_transport", + "realtime_voice", + "realtime", + ].includes(k) + ) + return false; + return true; + }).length > 0 && ( + <> + +
+ +
+ {Object.entries(log.metadata) + .filter(([key]) => { + if (key === "isAsyncRequest") return false; + if ( + isRealtimeTurn && + [ + "realtime_session_id", + "provider_session_id", + "realtime_source", + "realtime_event_type", + "realtime_transport", + "realtime_voice", + "realtime", + ].includes(key) + ) + return false; + return true; + }) + .map(([key, value]) => ( + + ))} +
+
+ + )} + + + + + {showTabs && ( + + Messages + {log.input_history?.length ? ( + + {log.input_history.length + (log.output_message ? 1 : 0)} + + ) : null} + + )} - {showTabs && !isPassthrough && !log.list_models_output && ( - - Tools - {log.params?.tools?.length ? ( - - {log.params.tools.length} - - ) : null} - - )} - {showTabs && ( - - Routing - {log.routing_engine_logs ? ( - - {log.routing_engine_logs.split("\n").filter(Boolean).length} - - ) : null} - - )} - - Plugin Logs - {pluginLogCount > 0 ? ( - - {pluginLogCount} - - ) : null} - - {!isPassthrough && ( - - Raw JSON - - )} - + {showTabs && !isPassthrough && !log.list_models_output && ( + + Tools + {log.params?.tools?.length ? ( + + {log.params.tools.length} + + ) : null} + + )} + {showTabs && ( + + Routing + {log.routing_engine_logs ? ( + + {log.routing_engine_logs.split("\n").filter(Boolean).length} + + ) : null} + + )} + + Plugin Logs + {pluginLogCount > 0 ? ( + + {pluginLogCount} + + ) : null} + + {!isPassthrough && ( + + Raw JSON + + )} +
- -
- - - - - - - setVisibleRoles(checked ? new Set(allRoles) : new Set()) - } - > - Show all messages - - - {( - [ - ["system", "System"], - ["user", "User"], - ["assistant", "Assistant"], - ["tool", "Tool"], - ["reasoning", "Reasoning"], - ] as [MessageRole, string][] - ).map(([role, label]) => ( - - setVisibleRoles((prev) => { - const next = new Set(prev); - checked ? next.add(role) : next.delete(role); - return next; - }) - } - > - - {label} - - ))} - - setVisibleRoles(new Set())} - className="text-muted-foreground justify-center text-[12px]" - > - Clear all - - - -
- {(log.ocr_input || log.ocr_output) && ( - - )} - {(log.speech_input || log.speech_output) && ( - - )} - {(log.transcription_input || log.transcription_output) && ( - - )} - {(log.image_generation_input || - log.image_edit_input || - log.image_variation_input || - log.image_generation_output) && ( - - )} - {(log.video_generation_input || videoOutput || videoListOutput) && ( - - )} + +
+ + + + + + setVisibleRoles(checked ? new Set(allRoles) : new Set())} + > + Show all messages + + + {( + [ + ["system", "System"], + ["user", "User"], + ["assistant", "Assistant"], + ["tool", "Tool"], + ["reasoning", "Reasoning"], + ] as [MessageRole, string][] + ).map(([role, label]) => ( + + setVisibleRoles((prev) => { + const next = new Set(prev); + checked ? next.add(role) : next.delete(role); + return next; + }) + } + > + + {label} + + ))} + + setVisibleRoles(new Set())} className="text-muted-foreground justify-center text-[12px]"> + Clear all + + + +
+ {(log.ocr_input || log.ocr_output) && } + {(log.speech_input || log.speech_output) && ( + + )} + {(log.transcription_input || log.transcription_output) && ( + + )} + {(log.image_generation_input || log.image_edit_input || log.image_variation_input || log.image_generation_output) && ( + + )} + {(log.video_generation_input || videoOutput || videoListOutput) && ( + + )} - {isPassthrough && passthroughRequestBody && ( - { - try { - return JSON.stringify( - JSON.parse(passthroughRequestBody || ""), - null, - 2, - ); - } catch { - return passthroughRequestBody || ""; - } - }} - > - { - try { - return JSON.stringify( - JSON.parse(passthroughRequestBody || ""), - null, - 2, - ); - } catch { - return passthroughRequestBody || ""; - } - })()} - lang="json" - readonly={true} - options={{ - showVerticalScrollbar: true, - scrollBeyondLastLine: false, - lineNumbers: "off", - alwaysConsumeMouseWheel: false, - }} - /> - - )} - {isPassthrough && - passthroughResponseBody && - log.status !== "processing" && ( - { - try { - return JSON.stringify( - JSON.parse(passthroughResponseBody || ""), - null, - 2, - ); - } catch { - return passthroughResponseBody || ""; - } - }} - > - { - try { - return JSON.stringify( - JSON.parse(passthroughResponseBody || ""), - null, - 2, - ); - } catch { - return passthroughResponseBody || ""; - } - })()} - lang="json" - readonly={true} - options={{ - showVerticalScrollbar: true, - scrollBeyondLastLine: false, - lineNumbers: "off", - alwaysConsumeMouseWheel: false, - }} - /> - - )} + {isPassthrough && passthroughRequestBody && ( + { + try { + return JSON.stringify(JSON.parse(passthroughRequestBody || ""), null, 2); + } catch { + return passthroughRequestBody || ""; + } + }} + > + { + try { + return JSON.stringify(JSON.parse(passthroughRequestBody || ""), null, 2); + } catch { + return passthroughRequestBody || ""; + } + })()} + lang="json" + readonly={true} + options={{ + collapsibleBlocks: true, + showVerticalScrollbar: true, + scrollBeyondLastLine: false, + lineNumbers: "off", + alwaysConsumeMouseWheel: false, + }} + /> + + )} + {isPassthrough && passthroughResponseBody && log.status !== "processing" && ( + { + try { + return JSON.stringify(JSON.parse(passthroughResponseBody || ""), null, 2); + } catch { + return passthroughResponseBody || ""; + } + }} + > + { + try { + return JSON.stringify(JSON.parse(passthroughResponseBody || ""), null, 2); + } catch { + return passthroughResponseBody || ""; + } + })()} + lang="json" + readonly={true} + options={{ + collapsibleBlocks: true, + showVerticalScrollbar: true, + scrollBeyondLastLine: false, + lineNumbers: "off", + alwaysConsumeMouseWheel: false, + }} + /> + + )} - {!isPassthrough && - ((log.input_history && log.input_history.length > 0) || - (log.output_message && !log.error_details?.error.message) || - log.stop_reason === "refusal" || - log.stop_reason === "content_filter" || - log.stop_reason === "safety") && ( -
- {(visibleRoles.size < allRoles.length - ? log.input_history?.filter((m) => { - if (!m) return false; - const mainRole = ((m.role as string) || - "user") as MessageRole; - const hasReasoning = !!extractChatReasoning(m); - return ( - visibleRoles.has(mainRole) || - (hasReasoning && visibleRoles.has("reasoning")) - ); - }) - : log.input_history?.filter(Boolean) - )?.flatMap((message, index) => { - const role = ((message.role as string) || - "user") as MessageRole; - const text = extractMessageText(message); - const reasoningText = extractChatReasoning(message); - const showAll = visibleRoles.size === allRoles.length; - const showMain = showAll || visibleRoles.has(role); - const showReasoning = - !!reasoningText && - (showAll || visibleRoles.has("reasoning")); - const hasToolCalls = - Array.isArray(message.tool_calls) && - message.tool_calls.length > 0; - const isOverallLast = - index === (log.input_history?.length ?? 0) - 1 && - !log.output_message && - !log.error_details?.error.message; - const lineCount = text ? text.split("\n").length : 0; - const approxTokens = text - ? Math.max(1, Math.round(text.length / 4)) - : 0; - const reasoningTokens = reasoningText - ? Math.max(1, Math.round(reasoningText.length / 4)) - : 0; - const meta = text - ? role === "system" || role === "tool" - ? `${lineCount} line${lineCount === 1 ? "" : "s"} · ~${approxTokens} tokens` - : `${lineCount} line${lineCount === 1 ? "" : "s"}` - : hasToolCalls - ? `${message.tool_calls!.length} tool call${message.tool_calls!.length === 1 ? "" : "s"}` - : undefined; - const usePlainText = role === "user" || role === "assistant"; - const rows: ReactNode[] = []; - if (showReasoning) { - rows.push( - - - , - ); - } - if (showMain) { - rows.push( - - {text ? ( - usePlainText && isJson(text) ? ( - { - try { - return JSON.stringify( - JSON.parse(text), - null, - 2, - ); - } catch { - return text; - } - })()} - lang="json" - readonly - autoResize - options={{ - showIndentLines: false, - disableHover: true, - }} - /> - ) : usePlainText ? ( - - ) : ( - - ) - ) : ( - - )} - {text && - Array.isArray(message.content) && - (message.content as ContentBlock[]) - .filter((b) => b.type === "image_url") - .map((b, i) => { - const src = b.image_url?.url; - if (!src) return null; - return ( - Attached image - ); - })} - {hasToolCalls && text ? ( -
- {message - .tool_calls!.map((tc) => tc.function?.name) - .filter(Boolean) - .join(", ") || - `${message.tool_calls!.length} tool call${message.tool_calls!.length === 1 ? "" : "s"}`} -
- ) : null} -
, - ); - } - return rows; - })} - {log.output_message && - !log.error_details?.error.message && - (() => { - const reasoningText = extractChatReasoning( - log.output_message, - ); - const showReasoning = - !!reasoningText && - (visibleRoles.size === allRoles.length || - visibleRoles.has("reasoning")); - const showAssistant = visibleRoles.has("assistant"); - if (!showReasoning && !showAssistant) return null; - const text = extractMessageText(log.output_message); - const refusalText = log.output_message.refusal; - const isStopReasonRefusal = - log.stop_reason === "refusal" || - log.stop_reason === "content_filter" || - log.stop_reason === "safety"; - const showRefusal = - refusalText || (!text && isStopReasonRefusal); - const lineCount = text ? text.split("\n").length : 0; - const tokenMeta = log.token_usage?.completion_tokens - ? `${log.token_usage.completion_tokens} tokens` - : undefined; - const meta = text - ? tokenMeta - ? `${lineCount} line${lineCount === 1 ? "" : "s"} · ${tokenMeta}` - : `${lineCount} line${lineCount === 1 ? "" : "s"}` - : showRefusal - ? "refusal" - : tokenMeta; - const reasoningTokens = reasoningText - ? log.token_usage?.completion_tokens_details - ?.reasoning_tokens || - Math.max(1, Math.round(reasoningText.length / 4)) - : 0; - return ( - <> - {showReasoning ? ( - - - - ) : null} - {showAssistant ? ( - - {showRefusal ? ( -
-
- - - Refusal - -
- {refusalText && ( -
- {refusalText} -
- )} -
- ) : text ? ( - isJson(text) ? ( - { - try { - return JSON.stringify( - JSON.parse(text), - null, - 2, - ); - } catch { - return text; - } - })()} - lang="json" - readonly - autoResize - options={{ - showIndentLines: false, - disableHover: true, - }} - /> - ) : ( - - ) - ) : ( - - )} -
- ) : null} - - ); - })()} - {!log.output_message && - !log.error_details?.error.message && - (log.stop_reason === "refusal" || - log.stop_reason === "content_filter" || - log.stop_reason === "safety") && ( - -
-
- - - Refusal - -
-
-
- )} -
- )} + {!isPassthrough && + ((log.input_history && log.input_history.length > 0) || + (log.output_message && !log.error_details?.error.message) || + log.stop_reason === "refusal" || + log.stop_reason === "content_filter" || + log.stop_reason === "safety") && ( +
+ {(visibleRoles.size < allRoles.length + ? log.input_history?.filter((m) => { + if (!m) return false; + const mainRole = ((m.role as string) || "user") as MessageRole; + const hasReasoning = !!extractChatReasoning(m); + return visibleRoles.has(mainRole) || (hasReasoning && visibleRoles.has("reasoning")); + }) + : log.input_history?.filter(Boolean) + )?.flatMap((message, index) => { + const role = ((message.role as string) || "user") as MessageRole; + const text = extractMessageText(message); + const reasoningText = extractChatReasoning(message); + const showAll = visibleRoles.size === allRoles.length; + const showMain = showAll || visibleRoles.has(role); + const showReasoning = !!reasoningText && (showAll || visibleRoles.has("reasoning")); + const hasToolCalls = Array.isArray(message.tool_calls) && message.tool_calls.length > 0; + const isOverallLast = + index === (log.input_history?.length ?? 0) - 1 && !log.output_message && !log.error_details?.error.message; + const lineCount = text ? text.split("\n").length : 0; + const approxTokens = text ? Math.max(1, Math.round(text.length / 4)) : 0; + const reasoningTokens = reasoningText ? Math.max(1, Math.round(reasoningText.length / 4)) : 0; + const meta = text + ? role === "system" || role === "tool" + ? `${lineCount} line${lineCount === 1 ? "" : "s"} · ~${approxTokens} tokens` + : `${lineCount} line${lineCount === 1 ? "" : "s"}` + : hasToolCalls + ? `${message.tool_calls!.length} tool call${message.tool_calls!.length === 1 ? "" : "s"}` + : undefined; + const usePlainText = role === "user" || role === "assistant"; + const rows: ReactNode[] = []; + if (showReasoning) { + rows.push( + + + , + ); + } + if (showMain) { + rows.push( + + {text ? ( + usePlainText && isJson(text) ? ( + { + try { + return JSON.stringify(JSON.parse(text), null, 2); + } catch { + return text; + } + })()} + lang="json" + readonly + autoResize + options={{ + collapsibleBlocks: true, + showIndentLines: false, + disableHover: true, + }} + /> + ) : usePlainText ? ( + + ) : ( + + ) + ) : ( + + )} + {text && + Array.isArray(message.content) && + (message.content as ContentBlock[]) + .filter((b) => b.type === "image_url") + .map((b, i) => { + const src = b.image_url?.url; + if (!src) return null; + return Attached image; + })} + {hasToolCalls && text ? ( +
+ {message + .tool_calls!.map((tc) => tc.function?.name) + .filter(Boolean) + .join(", ") || `${message.tool_calls!.length} tool call${message.tool_calls!.length === 1 ? "" : "s"}`} +
+ ) : null} +
, + ); + } + return rows; + })} + {log.output_message && + !log.error_details?.error.message && + (() => { + const reasoningText = extractChatReasoning(log.output_message); + const showReasoning = !!reasoningText && (visibleRoles.size === allRoles.length || visibleRoles.has("reasoning")); + const showAssistant = visibleRoles.has("assistant"); + if (!showReasoning && !showAssistant) return null; + const text = extractMessageText(log.output_message); + const refusalText = log.output_message.refusal; + const isStopReasonRefusal = + log.stop_reason === "refusal" || log.stop_reason === "content_filter" || log.stop_reason === "safety"; + const showRefusal = refusalText || (!text && isStopReasonRefusal); + const lineCount = text ? text.split("\n").length : 0; + const tokenMeta = log.token_usage?.completion_tokens ? `${log.token_usage.completion_tokens} tokens` : undefined; + const meta = text + ? tokenMeta + ? `${lineCount} line${lineCount === 1 ? "" : "s"} · ${tokenMeta}` + : `${lineCount} line${lineCount === 1 ? "" : "s"}` + : showRefusal + ? "refusal" + : tokenMeta; + const reasoningTokens = reasoningText + ? log.token_usage?.completion_tokens_details?.reasoning_tokens || Math.max(1, Math.round(reasoningText.length / 4)) + : 0; + return ( + <> + {showReasoning ? ( + + + + ) : null} + {showAssistant ? ( + + {showRefusal ? ( +
+
+ + Refusal +
+ {refusalText && ( +
+ {refusalText} +
+ )} +
+ ) : text ? ( + isJson(text) ? ( + { + try { + return JSON.stringify(JSON.parse(text), null, 2); + } catch { + return text; + } + })()} + lang="json" + readonly + autoResize + options={{ + collapsibleBlocks: true, + showIndentLines: false, + disableHover: true, + }} + /> + ) : ( + + ) + ) : ( + + )} +
+ ) : null} + + ); + })()} + {!log.output_message && + !log.error_details?.error.message && + (log.stop_reason === "refusal" || log.stop_reason === "content_filter" || log.stop_reason === "safety") && ( + +
+
+ + Refusal +
+
+
+ )} +
+ )} - {(() => { - const rawInput = log.responses_input_history ?? []; - const inputMsgs = - visibleRoles.size < allRoles.length - ? rawInput.filter((m) => visibleRoles.has(getResponsesRole(m))) - : rawInput; - const rawOutput = - log.status !== "processing" && !log.error_details?.error.message - ? (log.responses_output ?? []) - : []; - const outputMsgs = - visibleRoles.size < allRoles.length - ? rawOutput.filter((m) => visibleRoles.has(getResponsesRole(m))) - : rawOutput; - const all: ResponsesMessage[] = coalesceResponsesMessages([ - ...inputMsgs, - ...outputMsgs, - ]); - if (all.length === 0) return null; - return ( -
- {all.map((msg, index) => { - const role = getResponsesRole(msg); - const isLast = index === all.length - 1; - const reasoningParts = - role === "reasoning" ? extractReasoningParts(msg) : null; - const reasoningHasAny = - !!reasoningParts && - (reasoningParts.summaries.length > 0 || - !!reasoningParts.encrypted || - !!reasoningParts.contentText || - reasoningParts.signatures.length > 0); - const text = - role === "reasoning" ? "" : extractResponsesText(msg); - const lineCount = text ? text.split("\n").length : 0; - const approxTokens = text - ? Math.max(1, Math.round(text.length / 4)) - : 0; - let meta: string | undefined; - if (role === "reasoning" && reasoningParts) { - const totalLen = - reasoningParts.summaries.reduce( - (acc, s) => acc + s.length, - 0, - ) + - (reasoningParts.contentText?.length ?? 0) + - (reasoningParts.encrypted?.length ?? 0); - const totalApprox = totalLen - ? Math.max(1, Math.round(totalLen / 4)) - : 0; - const hasOpaqueOnly = - (!!reasoningParts.encrypted || - reasoningParts.signatures.length > 0) && - reasoningParts.summaries.length === 0 && - !reasoningParts.contentText; - meta = totalApprox - ? `~${totalApprox} tokens${hasOpaqueOnly ? " · encrypted" : ""}` - : hasOpaqueOnly - ? "encrypted" - : undefined; - } else { - meta = text - ? role === "system" || role === "tool" - ? msg.name - ? `${msg.name} · ${lineCount} line${lineCount === 1 ? "" : "s"} · ~${approxTokens} tokens` - : `${lineCount} line${lineCount === 1 ? "" : "s"} · ~${approxTokens} tokens` - : `${lineCount} line${lineCount === 1 ? "" : "s"}` - : msg.name - ? msg.name - : msg.type === "function_call_output" && msg.call_id - ? msg.call_id - : msg.type || undefined; - } - const usePlainText = role === "user" || role === "assistant"; - return ( - - {role === "reasoning" ? ( - reasoningHasAny && reasoningParts ? ( -
- {reasoningParts.contentText ? ( - - ) : null} - {reasoningParts.summaries.map((s, i) => ( -
- {reasoningParts.summaries.length > 1 ? ( -
- Summary {i + 1} -
- ) : null} - -
- ))} - {reasoningParts.encrypted ? ( -
-
- Encrypted -
- -
- ) : null} - {reasoningParts.signatures.length > 0 ? ( - 1 - ? "Encrypted signatures" - : "Encrypted signature" - } - /> - ) : null} -
- ) : ( -
- No reasoning content available -
- ) - ) : text ? ( - usePlainText ? ( - - ) : ( - - ) - ) : msg.output !== undefined ? ( - - ) : ( -
- No content -
- )} - {Array.isArray(msg.content) && - msg.content - .filter( - (b) => b?.type === "input_image" && b.image_url, - ) - .map((b, i) => ( - Attached image - ))} -
- ); - })} -
- ); - })()} + {(() => { + const rawInput = log.responses_input_history ?? []; + const inputMsgs = + visibleRoles.size < allRoles.length ? rawInput.filter((m) => visibleRoles.has(getResponsesRole(m))) : rawInput; + const rawOutput = log.status !== "processing" && !log.error_details?.error.message ? (log.responses_output ?? []) : []; + const outputMsgs = + visibleRoles.size < allRoles.length ? rawOutput.filter((m) => visibleRoles.has(getResponsesRole(m))) : rawOutput; + const all: ResponsesMessage[] = coalesceResponsesMessages([...inputMsgs, ...outputMsgs]); + if (all.length === 0) return null; + return ( +
+ {all.map((msg, index) => { + const role = getResponsesRole(msg); + const isLast = index === all.length - 1; + const reasoningParts = role === "reasoning" ? extractReasoningParts(msg) : null; + const reasoningHasAny = + !!reasoningParts && + (reasoningParts.summaries.length > 0 || + !!reasoningParts.encrypted || + !!reasoningParts.contentText || + reasoningParts.signatures.length > 0); + const text = role === "reasoning" ? "" : extractResponsesText(msg); + const lineCount = text ? text.split("\n").length : 0; + const approxTokens = text ? Math.max(1, Math.round(text.length / 4)) : 0; + let meta: string | undefined; + if (role === "reasoning" && reasoningParts) { + const totalLen = + reasoningParts.summaries.reduce((acc, s) => acc + s.length, 0) + + (reasoningParts.contentText?.length ?? 0) + + (reasoningParts.encrypted?.length ?? 0); + const totalApprox = totalLen ? Math.max(1, Math.round(totalLen / 4)) : 0; + const hasOpaqueOnly = + (!!reasoningParts.encrypted || reasoningParts.signatures.length > 0) && + reasoningParts.summaries.length === 0 && + !reasoningParts.contentText; + meta = totalApprox + ? `~${totalApprox} tokens${hasOpaqueOnly ? " · encrypted" : ""}` + : hasOpaqueOnly + ? "encrypted" + : undefined; + } else { + meta = text + ? role === "system" || role === "tool" + ? msg.name + ? `${msg.name} · ${lineCount} line${lineCount === 1 ? "" : "s"} · ~${approxTokens} tokens` + : `${lineCount} line${lineCount === 1 ? "" : "s"} · ~${approxTokens} tokens` + : `${lineCount} line${lineCount === 1 ? "" : "s"}` + : msg.name + ? msg.name + : msg.type === "function_call_output" && msg.call_id + ? msg.call_id + : msg.type || undefined; + } + const usePlainText = role === "user" || role === "assistant"; + return ( + + {role === "reasoning" ? ( + reasoningHasAny && reasoningParts ? ( +
+ {reasoningParts.contentText ? ( + + ) : null} + {reasoningParts.summaries.map((s, i) => ( +
+ {reasoningParts.summaries.length > 1 ? ( +
+ Summary {i + 1} +
+ ) : null} + +
+ ))} + {reasoningParts.encrypted ? ( +
+
Encrypted
+ +
+ ) : null} + {reasoningParts.signatures.length > 0 ? ( + 1 ? "Encrypted signatures" : "Encrypted signature"} + /> + ) : null} +
+ ) : ( +
No reasoning content available
+ ) + ) : text ? ( + usePlainText ? ( + + ) : ( + + ) + ) : msg.output !== undefined ? ( + + ) : ( +
No content
+ )} + {Array.isArray(msg.content) && + msg.content + .filter((b) => b?.type === "input_image" && b.image_url) + .map((b, i) => ( + Attached image + ))} +
+ ); + })} +
+ ); + })()} - {log.is_large_payload_request && - !log.input_history?.length && - !log.responses_input_history?.length && ( -
- Large payload request — input content was streamed directly to - the provider and is not available for display. - {log.raw_request && - " A truncated preview is available in the Raw JSON tab."} -
- )} - {log.is_large_payload_response && - !log.output_message && - !log.responses_output?.length && - log.status !== "processing" && ( -
- Large payload response — response content was streamed directly - to the client and is not available for display. - {log.raw_response && - " A truncated preview is available in the Raw JSON tab."} -
- )} + {log.is_large_payload_request && !log.input_history?.length && !log.responses_input_history?.length && ( +
+ Large payload request — input content was streamed directly to the provider and is not available for display. + {log.raw_request && " A truncated preview is available in the Raw JSON tab."} +
+ )} + {log.is_large_payload_response && !log.output_message && !log.responses_output?.length && log.status !== "processing" && ( +
+ Large payload response — response content was streamed directly to the client and is not available for display. + {log.raw_response && " A truncated preview is available in the Raw JSON tab."} +
+ )} - {log.status !== "processing" && - log.embedding_output && - log.embedding_output.length > 0 && - !log.error_details?.error.message && ( -
-
Embedding
- embedding.embedding, - ), - null, - 2, - ), - }} - /> -
- )} - {log.status !== "processing" && - log.rerank_output && - !log.error_details?.error.message && ( - JSON.stringify(log.rerank_output, null, 2)} - > - - - )} + {log.status !== "processing" && log.embedding_output && log.embedding_output.length > 0 && !log.error_details?.error.message && ( +
+
Embedding
+ embedding.embedding), + null, + 2, + ), + }} + /> +
+ )} + {log.status !== "processing" && log.rerank_output && !log.error_details?.error.message && ( + JSON.stringify(log.rerank_output, null, 2)}> + + + )} - {log.list_models_output && ( - JSON.stringify(log.list_models_output, null, 2)} - > - - - )} + {log.list_models_output && ( + JSON.stringify(log.list_models_output, null, 2)} + > + + + )} - {(log.error_details?.error.message || - log.error_details?.error.error != null) && ( -
-
- - Error - {log.error_details?.error.message ? ( - - ) : null} -
- {log.error_details?.error.message ? ( -
- {log.error_details.error.message} -
- ) : null} - {log.error_details?.error.error != null ? ( -
- - Details - - -
- {typeof log.error_details.error.error === "string" - ? log.error_details.error.error - : JSON.stringify(log.error_details.error.error, null, 2)} -
-
- ) : null} -
- )} -
+ {(log.error_details?.error.message || log.error_details?.error.error != null) && ( +
+
+ + Error + {log.error_details?.error.message ? : null} +
+ {log.error_details?.error.message ? ( +
+ {log.error_details.error.message} +
+ ) : null} + {log.error_details?.error.error != null ? ( +
+ + Details + + +
+ {typeof log.error_details.error.error === "string" + ? log.error_details.error.error + : JSON.stringify(log.error_details.error.error, null, 2)} +
+
+ ) : null} +
+ )} +
- - {toolsParameter ? ( -
-
- {log.params?.tools?.length ?? 0} tools exposed to the model - {(log.params as any)?.tool_choice != null ? ( - <> - {" "} - · tool_choice ={" "} - - {formatToolChoice((log.params as any).tool_choice)} - - - ) : null} -
-
- {(log.params?.tools as any[]).map((tool, i) => { - const name = - tool?.function?.name ?? tool?.name ?? `tool_${i}`; - const description = - tool?.function?.description ?? tool?.description ?? ""; - const schema = - tool?.function?.parameters ?? - tool?.input_schema ?? - tool?.parameters ?? - null; - const schemaJson = - schema != null ? JSON.stringify(schema, null, 2) : ""; - return ( -
- -
- -
-
-
- {name} -
- {description ? ( -
- {description} -
- ) : null} -
- -
- {schemaJson ? ( -
-
- Parameters - -
-
-                            {schemaJson}
-                          
-
- ) : ( -
- No parameter schema. -
- )} -
- ); - })} -
-
- ) : null} - {log.params?.instructions && ( - log.params?.instructions || ""} - > -
- {log.params.instructions} -
-
- )} - {!toolsParameter && !log.params?.instructions && ( -
- No tools or instructions on this request. -
- )} -
+ + {toolsParameter ? ( +
+
+ {log.params?.tools?.length ?? 0} tools exposed to the model + {(log.params as any)?.tool_choice != null ? ( + <> + {" "} + · tool_choice ={" "} + {formatToolChoice((log.params as any).tool_choice)} + + ) : null} +
+
+ {(log.params?.tools as any[]).map((tool, i) => { + const name = tool?.function?.name ?? tool?.name ?? `tool_${i}`; + const description = tool?.function?.description ?? tool?.description ?? ""; + const schema = tool?.function?.parameters ?? tool?.input_schema ?? tool?.parameters ?? null; + const schemaJson = schema != null ? JSON.stringify(schema, null, 2) : ""; + return ( +
+ +
+ +
+
+
{name}
+ {description ?
{description}
: null} +
+ +
+ {schemaJson ? ( +
+
+ Parameters + +
+
+														{schemaJson}
+													
+
+ ) : ( +
No parameter schema.
+ )} +
+ ); + })} +
+
+ ) : null} + {log.params?.instructions && ( + log.params?.instructions || ""}> +
+ {log.params.instructions} +
+
+ )} + {!toolsParameter && !log.params?.instructions && ( +
+ No tools or instructions on this request. +
+ )} +
- - {log.attempt_trail && log.attempt_trail.length > 1 && ( - JSON.stringify(log.attempt_trail, null, 2)} - > -
- - - - - - - - - - {log.attempt_trail.map((record) => ( - - - - - - ))} - -
#KeyResult
- {record.attempt + 1} - - {record.key_name || record.key_id} - - {record.fail_reason ? ( - - {record.fail_reason} - - ) : ( - - success - - )} -
-
-
- )} - {log.routing_engine_logs ? ( - - ) : ( -
- No routing logs for this request. -
- )} -
+ + {log.attempt_trail && log.attempt_trail.length > 1 && ( + JSON.stringify(log.attempt_trail, null, 2)} + > +
+ + + + + + + + + + {log.attempt_trail.map((record) => ( + + + + + + ))} + +
#KeyResult
{record.attempt + 1}{record.key_name || record.key_id} + {record.fail_reason ? ( + {record.fail_reason} + ) : ( + success + )} +
+
+
+ )} + {log.routing_engine_logs ? ( + + ) : ( +
+ No routing logs for this request. +
+ )} +
- - {log.plugin_logs ? ( - - ) : ( -
- No plugin logs for this request. -
- )} -
+ + {log.plugin_logs ? ( + + ) : ( +
+ No plugin logs for this request. +
+ )} +
- - {rawRequest && ( - <> -
- Raw Request sent to{" "} - - {log.provider} - - {log.is_large_payload_request && ( - - (truncated preview) - - )} -
- formatJsonSafe(rawRequest)} - > - - - - )} - {rawResponse && log.status !== "processing" && ( - <> -
- Raw Response from{" "} - - {log.provider} - - {log.is_large_payload_response && ( - - (truncated preview) - - )} -
- formatJsonSafe(rawResponse)} - > - - - - )} - {!rawRequest && - !rawResponse && - !passthroughRequestBody && - !passthroughResponseBody && ( -
- No raw JSON available. -
- )} -
-
- - ); + + {rawRequest && ( + <> +
+ Raw Request sent to {log.provider} + {log.is_large_payload_request && ( + (truncated preview) + )} +
+ formatJsonSafe(rawRequest)} + > + + + + )} + {rawResponse && log.status !== "processing" && ( + <> +
+ Raw Response from {log.provider} + {log.is_large_payload_response && ( + (truncated preview) + )} +
+ formatJsonSafe(rawResponse)} + > + + + + )} + {!rawRequest && !rawResponse && !passthroughRequestBody && !passthroughResponseBody && ( +
No raw JSON available.
+ )} +
+ + + ); } -const copyRequestBody = async ( - log: LogEntry, - copy: (text: string) => Promise, -) => { - try { - const isChat = - log.object === "chat.completion" || - log.object === "chat_completion" || - log.object === "chat.completion.chunk"; - const isResponses = - log.object === "response" || log.object === "response.completion.chunk"; - const isRealtimeTurn = log.object === "realtime.turn"; - const isSpeech = - log.object === "audio.speech" || log.object === "audio.speech.chunk"; - const isTextCompletion = - log.object === "text.completion" || - log.object === "text.completion.chunk"; - const isEmbedding = log.object === "list"; +const copyRequestBody = async (log: LogEntry, copy: (text: string) => Promise) => { + try { + const isChat = log.object === "chat.completion" || log.object === "chat_completion" || log.object === "chat.completion.chunk"; + const isResponses = log.object === "response" || log.object === "response.completion.chunk"; + const isRealtimeTurn = log.object === "realtime.turn"; + const isSpeech = log.object === "audio.speech" || log.object === "audio.speech.chunk"; + const isTextCompletion = log.object === "text.completion" || log.object === "text.completion.chunk"; + const isEmbedding = log.object === "list"; - const extractTextFromMessage = (message: any): string => { - if (!message || !message.content) { - return ""; - } - if (typeof message.content === "string") { - return message.content; - } - if (Array.isArray(message.content)) { - return message.content - .filter((block: any) => block && block.type === "text" && block.text) - .map((block: any) => block.text) - .join("\n"); - } - return ""; - }; + const extractTextFromMessage = (message: any): string => { + if (!message || !message.content) { + return ""; + } + if (typeof message.content === "string") { + return message.content; + } + if (Array.isArray(message.content)) { + return message.content + .filter((block: any) => block && block.type === "text" && block.text) + .map((block: any) => block.text) + .join("\n"); + } + return ""; + }; - const extractTextsFromMessage = (message: any): string[] => { - if (!message || !message.content) { - return []; - } - if (typeof message.content === "string") { - return message.content ? [message.content] : []; - } - if (Array.isArray(message.content)) { - return message.content - .filter((block: any) => block && block.type === "text" && block.text) - .map((block: any) => block.text); - } - return []; - }; + const extractTextsFromMessage = (message: any): string[] => { + if (!message || !message.content) { + return []; + } + if (typeof message.content === "string") { + return message.content ? [message.content] : []; + } + if (Array.isArray(message.content)) { + return message.content.filter((block: any) => block && block.type === "text" && block.text).map((block: any) => block.text); + } + return []; + }; - const isSupportedType = - isChat || - isResponses || - isRealtimeTurn || - isSpeech || - isTextCompletion || - isEmbedding; - if (!isSupportedType) { - if ( - log.object === "audio.transcription" || - log.object === "audio.transcription.chunk" - ) { - toast.error( - "Copy request body is not available for transcription requests", - ); - } else { - toast.error( - "Copy request body is only available for chat, responses, speech, text completion, and embedding requests", - ); - } - return; - } + const isSupportedType = isChat || isResponses || isRealtimeTurn || isSpeech || isTextCompletion || isEmbedding; + if (!isSupportedType) { + if (log.object === "audio.transcription" || log.object === "audio.transcription.chunk") { + toast.error("Copy request body is not available for transcription requests"); + } else { + toast.error("Copy request body is only available for chat, responses, speech, text completion, and embedding requests"); + } + return; + } - const requestBody: any = { - model: - log.provider && log.model - ? `${log.provider}/${log.model}` - : log.model || "", - }; + const requestBody: any = { + model: log.provider && log.model ? `${log.provider}/${log.model}` : log.model || "", + }; - if (isRealtimeTurn) { - if (log.input_history && log.input_history.length > 0) { - requestBody.messages = log.input_history; - } - if (log.output_message) { - requestBody.output = log.output_message; - } - } else if (isChat && log.input_history && log.input_history.length > 0) { - requestBody.messages = log.input_history; - } else if ( - isResponses && - log.responses_input_history && - log.responses_input_history.length > 0 - ) { - requestBody.input = log.responses_input_history; - } else if (isSpeech && log.speech_input) { - requestBody.input = log.speech_input.input; - } else if ( - isTextCompletion && - log.input_history && - log.input_history.length > 0 - ) { - const firstMessage = log.input_history[0]; - const prompt = extractTextFromMessage(firstMessage); - if (prompt) { - requestBody.prompt = prompt; - } - } else if ( - isEmbedding && - log.input_history && - log.input_history.length > 0 - ) { - const texts: string[] = []; - for (const message of log.input_history) { - const messageTexts = extractTextsFromMessage(message); - texts.push(...messageTexts); - } - if (texts.length > 0) { - requestBody.input = texts.length === 1 ? texts[0] : texts; - } - } + if (isRealtimeTurn) { + if (log.input_history && log.input_history.length > 0) { + requestBody.messages = log.input_history; + } + if (log.output_message) { + requestBody.output = log.output_message; + } + } else if (isChat && log.input_history && log.input_history.length > 0) { + requestBody.messages = log.input_history; + } else if (isResponses && log.responses_input_history && log.responses_input_history.length > 0) { + requestBody.input = log.responses_input_history; + } else if (isSpeech && log.speech_input) { + requestBody.input = log.speech_input.input; + } else if (isTextCompletion && log.input_history && log.input_history.length > 0) { + const firstMessage = log.input_history[0]; + const prompt = extractTextFromMessage(firstMessage); + if (prompt) { + requestBody.prompt = prompt; + } + } else if (isEmbedding && log.input_history && log.input_history.length > 0) { + const texts: string[] = []; + for (const message of log.input_history) { + const messageTexts = extractTextsFromMessage(message); + texts.push(...messageTexts); + } + if (texts.length > 0) { + requestBody.input = texts.length === 1 ? texts[0] : texts; + } + } - if (log.params) { - const paramsCopy = { ...log.params }; - delete paramsCopy.tools; - delete paramsCopy.instructions; - Object.assign(requestBody, paramsCopy); - } + if (log.params) { + const paramsCopy = { ...log.params }; + delete paramsCopy.tools; + delete paramsCopy.instructions; + Object.assign(requestBody, paramsCopy); + } - if ( - (isChat || isResponses || isRealtimeTurn) && - log.params?.tools && - Array.isArray(log.params.tools) && - log.params.tools.length > 0 - ) { - requestBody.tools = log.params.tools; - } - if ((isResponses || isRealtimeTurn) && log.params?.instructions) { - requestBody.instructions = log.params.instructions; - } + if ((isChat || isResponses || isRealtimeTurn) && log.params?.tools && Array.isArray(log.params.tools) && log.params.tools.length > 0) { + requestBody.tools = log.params.tools; + } + if ((isResponses || isRealtimeTurn) && log.params?.instructions) { + requestBody.instructions = log.params.instructions; + } - const requestBodyJson = JSON.stringify(requestBody, null, 2); - await copy(requestBodyJson); - } catch { - toast.error("Failed to copy request body"); - } -}; + const requestBodyJson = JSON.stringify(requestBody, null, 2); + await copy(requestBodyJson); + } catch { + toast.error("Failed to copy request body"); + } +}; \ No newline at end of file diff --git a/ui/components/ui/codeEditor.tsx b/ui/components/ui/codeEditor.tsx index fa8a6589dbe..91aa386e434 100644 --- a/ui/components/ui/codeEditor.tsx +++ b/ui/components/ui/codeEditor.tsx @@ -150,9 +150,9 @@ export function CodeEditor(props: CodeEditorProps) { padding: { top: 2, bottom: 2 }, wordWrap: props.wrap ? ("on" as const) : ("off" as const), folding: isFoldingEnabled, - glyphMargin: false, + glyphMargin: isFoldingEnabled, lineNumbersMinChars: props.options?.lineNumbersMinChars ?? 4, - lineDecorationsWidth: 8, + lineDecorationsWidth: isFoldingEnabled ? 18 : 8, showFoldingControls: isFoldingEnabled ? ("always" as const) : ("mouseover" as const), overviewRulerLanes: props.options?.overviewRulerLanes ?? 0, renderLineHighlight: "none" as const, @@ -237,4 +237,4 @@ export function CodeEditor(props: CodeEditorProps) { /> ); -} \ No newline at end of file +}