-
Notifications
You must be signed in to change notification settings - Fork 567
fix: correct Anthropic streaming response format and usage tracking #186
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5c715cc
74e2674
e59957a
f15544e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -70,7 +70,7 @@ type AnthropicStreamEvent struct { | |||
| Index *int `json:"index,omitempty"` | ||||
| ContentBlock *AnthropicContentBlock `json:"content_block,omitempty"` | ||||
| Delta *AnthropicDelta `json:"delta,omitempty"` | ||||
| Usage *schemas.LLMUsage `json:"usage,omitempty"` | ||||
| Usage *AnthropicUsage `json:"usage,omitempty"` | ||||
| Error *AnthropicStreamError `json:"error,omitempty"` | ||||
| } | ||||
|
|
||||
|
|
@@ -84,7 +84,35 @@ type AnthropicStreamMessage struct { | |||
| Model string `json:"model"` | ||||
| StopReason *string `json:"stop_reason"` | ||||
| StopSequence *string `json:"stop_sequence"` | ||||
| Usage *schemas.LLMUsage `json:"usage"` | ||||
| Usage *AnthropicUsage `json:"usage"` | ||||
| } | ||||
|
|
||||
| // AnthropicUsage represents usage information in Anthropic format | ||||
| type AnthropicUsage struct { | ||||
| InputTokens int `json:"input_tokens,omitempty"` | ||||
| OutputTokens int `json:"output_tokens"` | ||||
| CacheCreationInputTokens int `json:"cache_creation_input_tokens,omitempty"` | ||||
| CacheReadInputTokens int `json:"cache_read_input_tokens,omitempty"` | ||||
| } | ||||
|
|
||||
| func (u *AnthropicUsage) ToLLMUsage() *schemas.LLMUsage { | ||||
| if u == nil { | ||||
| return nil | ||||
| } | ||||
|
|
||||
| llmUsage := &schemas.LLMUsage{ | ||||
| PromptTokens: u.InputTokens, | ||||
| CompletionTokens: u.OutputTokens, | ||||
| TotalTokens: u.InputTokens + u.OutputTokens, | ||||
| } | ||||
|
|
||||
| if u.CacheReadInputTokens > 0 { | ||||
| llmUsage.TokenDetails = &schemas.TokenDetails{ | ||||
| CachedTokens: u.CacheReadInputTokens, | ||||
| } | ||||
| } | ||||
|
|
||||
| return llmUsage | ||||
| } | ||||
|
|
||||
| // AnthropicContentBlock represents a content block in Anthropic responses. | ||||
|
|
@@ -487,6 +515,10 @@ func prepareAnthropicChatRequest(messages []schemas.BifrostMessage, params *sche | |||
| "tool_use_id": *msg.ToolMessage.ToolCallID, | ||||
| } | ||||
|
|
||||
| if msg.ToolMessage.IsError != nil { | ||||
| toolCallResult["is_error"] = *msg.ToolMessage.IsError | ||||
| } | ||||
|
|
||||
| var toolCallResultContent []map[string]interface{} | ||||
|
|
||||
| if msg.Content.ContentStr != nil { | ||||
|
|
@@ -871,6 +903,7 @@ func handleAnthropicStreaming( | |||
| // Track minimal state needed for response format | ||||
| var messageID string | ||||
| var modelName string | ||||
| var usage *AnthropicUsage | ||||
|
|
||||
| // Track SSE event parsing state | ||||
| var eventType string | ||||
|
|
@@ -899,6 +932,8 @@ func handleAnthropicStreaming( | |||
| continue | ||||
| } | ||||
|
|
||||
| // logger.Debug(fmt.Sprintf("Received event: %s, %s", eventType, eventData)) | ||||
|
|
||||
|
Comment on lines
+935
to
+936
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Consider removing commented debug logging. If debug logging is no longer needed, consider removing it entirely rather than leaving it commented out. - // logger.Debug(fmt.Sprintf("Received event: %s, %s", eventType, eventData))
-📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||
| // Handle different event types | ||||
| switch eventType { | ||||
| case "message_start": | ||||
|
|
@@ -910,6 +945,7 @@ func handleAnthropicStreaming( | |||
| if event.Message != nil { | ||||
| messageID = event.Message.ID | ||||
| modelName = event.Message.Model | ||||
| usage = event.Message.Usage | ||||
| } | ||||
|
|
||||
| case "content_block_start": | ||||
|
|
@@ -948,6 +984,7 @@ func handleAnthropicStreaming( | |||
| }, | ||||
| }, | ||||
| }, | ||||
| Usage: usage.ToLLMUsage(), | ||||
| ExtraFields: schemas.BifrostResponseExtraFields{ | ||||
| Provider: providerType, | ||||
| }, | ||||
|
|
@@ -986,6 +1023,7 @@ func handleAnthropicStreaming( | |||
| }, | ||||
| }, | ||||
| }, | ||||
| Usage: usage.ToLLMUsage(), | ||||
| ExtraFields: schemas.BifrostResponseExtraFields{ | ||||
| Provider: providerType, | ||||
| }, | ||||
|
|
@@ -1027,6 +1065,7 @@ func handleAnthropicStreaming( | |||
| }, | ||||
| }, | ||||
| }, | ||||
| Usage: usage.ToLLMUsage(), | ||||
| ExtraFields: schemas.BifrostResponseExtraFields{ | ||||
| Provider: providerType, | ||||
| }, | ||||
|
|
@@ -1065,6 +1104,7 @@ func handleAnthropicStreaming( | |||
| }, | ||||
| }, | ||||
| }, | ||||
| Usage: usage.ToLLMUsage(), | ||||
| ExtraFields: schemas.BifrostResponseExtraFields{ | ||||
| Provider: providerType, | ||||
| }, | ||||
|
|
@@ -1096,6 +1136,7 @@ func handleAnthropicStreaming( | |||
| }, | ||||
| }, | ||||
| }, | ||||
| Usage: usage.ToLLMUsage(), | ||||
| ExtraFields: schemas.BifrostResponseExtraFields{ | ||||
| Provider: providerType, | ||||
| }, | ||||
|
|
@@ -1128,14 +1169,16 @@ func handleAnthropicStreaming( | |||
| } | ||||
|
|
||||
| // Handle delta changes to the top-level message | ||||
| if event.Usage != nil && usage != nil { | ||||
| usage.OutputTokens = event.Usage.OutputTokens | ||||
| } | ||||
|
|
||||
| // Send usage information immediately if present | ||||
| if event.Usage != nil { | ||||
| streamResponse := &schemas.BifrostResponse{ | ||||
| ID: messageID, | ||||
| Object: "chat.completion.chunk", | ||||
| Model: modelName, | ||||
| Usage: event.Usage, | ||||
| Choices: []schemas.BifrostResponseChoice{ | ||||
| { | ||||
| Index: 0, | ||||
|
|
@@ -1145,6 +1188,7 @@ func handleAnthropicStreaming( | |||
| FinishReason: event.Delta.StopReason, | ||||
| }, | ||||
| }, | ||||
| Usage: usage.ToLLMUsage(), | ||||
| ExtraFields: schemas.BifrostResponseExtraFields{ | ||||
| Provider: providerType, | ||||
| }, | ||||
|
|
||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -222,12 +222,23 @@ func prepareOpenAIChatRequest(messages []schemas.BifrostMessage, params *schemas | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for _, msg := range messages { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if msg.Role == schemas.ModelChatMessageRoleAssistant { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assistantMessage := map[string]interface{}{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "role": msg.Role, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "content": msg.Content, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "role": msg.Role, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not needed, bifrost content blocks follow openai's format only so we can directly embed the content |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if msg.AssistantMessage != nil && msg.AssistantMessage.ToolCalls != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assistantMessage["tool_calls"] = *msg.AssistantMessage.ToolCalls | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if msg.Content.ContentStr != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assistantMessage["content"] = *msg.Content.ContentStr | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if msg.Content.ContentBlocks != nil && len(*msg.Content.ContentBlocks) > 0 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var sb strings.Builder | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for _, block := range *msg.Content.ContentBlocks { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if block.Text != nil && *block.Text != "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sb.WriteString(*block.Text) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sb.WriteString(" ") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assistantMessage["content"] = sb.String() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+230
to
+241
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Extract duplicated content serialization logic into a helper function. The content serialization logic is duplicated between assistant messages and tool messages. This violates the DRY principle and makes maintenance harder. Extract the common logic into a helper function: +// serializeMessageContent converts MessageContent to a string representation
+func serializeMessageContent(content schemas.MessageContent) string {
+ if content.ContentStr != nil {
+ return *content.ContentStr
+ } else if content.ContentBlocks != nil && len(*content.ContentBlocks) > 0 {
+ var sb strings.Builder
+ for _, block := range *content.ContentBlocks {
+ if block.Text != nil && *block.Text != "" {
+ sb.WriteString(*block.Text)
+ sb.WriteString(" ")
+ } else if block.ImageURL != nil {
+ sb.WriteString(block.ImageURL.URL)
+ sb.WriteString(" ")
+ }
+ }
+ return sb.String()
+ }
+ return ""
+}
func prepareOpenAIChatRequest(messages []schemas.BifrostMessage, params *schemas.ModelParameters) ([]map[string]interface{}, map[string]interface{}) {
// ... existing code ...
if msg.AssistantMessage != nil && msg.AssistantMessage.ToolCalls != nil {
assistantMessage["tool_calls"] = *msg.AssistantMessage.ToolCalls
}
- if msg.Content.ContentStr != nil {
- assistantMessage["content"] = *msg.Content.ContentStr
- } else if msg.Content.ContentBlocks != nil && len(*msg.Content.ContentBlocks) > 0 {
- var sb strings.Builder
- for _, block := range *msg.Content.ContentBlocks {
- if block.Text != nil && *block.Text != "" {
- sb.WriteString(*block.Text)
- sb.WriteString(" ")
- }
- }
- assistantMessage["content"] = sb.String()
- }
+ assistantMessage["content"] = serializeMessageContent(msg.Content)
// ... later in the code ...
- content := message["content"]
- if contentBlocks, ok := content.([]schemas.ContentBlock); ok {
- var sb strings.Builder
- for _, block := range contentBlocks {
- if block.Text != nil && *block.Text != "" {
- sb.WriteString(*block.Text)
- sb.WriteString(" ")
- } else if block.ImageURL != nil {
- sb.WriteString(block.ImageURL.URL)
- sb.WriteString(" ")
- }
- }
- message["content"] = sb.String()
- }
+ message["content"] = serializeMessageContent(msg.Content)Also applies to: 268-281 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| formattedMessages = append(formattedMessages, assistantMessage) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message := map[string]interface{}{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -250,6 +261,24 @@ func prepareOpenAIChatRequest(messages []schemas.BifrostMessage, params *schemas | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if msg.ToolMessage != nil && msg.ToolMessage.ToolCallID != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message["tool_call_id"] = *msg.ToolMessage.ToolCallID | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if msg.IsError != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message["is_error"] = *msg.IsError | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| content := message["content"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if contentBlocks, ok := content.([]schemas.ContentBlock); ok { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var sb strings.Builder | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for _, block := range contentBlocks { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if block.Text != nil && *block.Text != "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sb.WriteString(*block.Text) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sb.WriteString(" ") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if block.ImageURL != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sb.WriteString(block.ImageURL.URL) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sb.WriteString(" ") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message["content"] = sb.String() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+264
to
+281
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix incorrect field access for IsError. The code accesses Apply this fix: - if msg.IsError != nil {
- message["is_error"] = *msg.IsError
+ if msg.ToolMessage.IsError != nil {
+ message["is_error"] = *msg.ToolMessage.IsError
}The rest of the content block processing logic looks correct for converting content blocks to a plain string format suitable for OpenAI's API. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| formattedMessages = append(formattedMessages, message) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -565,6 +594,9 @@ func handleOpenAIStreaming( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Handle usage-only chunks (when stream_options include_usage is true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if len(response.Choices) == 0 && response.Usage != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Empty choices array. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| response.Choices = []schemas.BifrostResponseChoice{} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // This is a usage information chunk at the end of stream | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if params != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| response.ExtraFields.Params = *params | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -590,9 +622,7 @@ func handleOpenAIStreaming( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| response.ExtraFields.Provider = providerType | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| processAndSendResponse(ctx, postHookRunner, &response, responseChan) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // End stream processing after finish reason | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dont break here, listen for [DONE] for breaking |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Handle regular content chunks | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -603,6 +633,7 @@ func handleOpenAIStreaming( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| response.ExtraFields.Provider = providerType | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| processAndSendResponse(ctx, postHookRunner, &response, responseChan) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,11 @@ const ( | |
| DefaultInitialPoolSize = 100 | ||
| ) | ||
|
|
||
| // StreamOptions represents the options for streaming requests. | ||
| type StreamOptions struct { | ||
| IncludeUsage bool `json:"include_usage"` | ||
| } | ||
|
|
||
| // BifrostConfig represents the configuration for initializing a Bifrost instance. | ||
| // It contains the necessary components for setting up the system including account details, | ||
| // plugins, logging, and initial pool size. | ||
|
|
@@ -161,19 +166,20 @@ type Fallback struct { | |
| // your request to the model. Bifrost follows a standard set of parameters which | ||
| // mapped to the provider's parameters. | ||
| type ModelParameters struct { | ||
| ToolChoice *ToolChoice `json:"tool_choice,omitempty"` // Whether to call a tool | ||
| Tools *[]Tool `json:"tools,omitempty"` // Tools to use | ||
| Temperature *float64 `json:"temperature,omitempty"` // Controls randomness in the output | ||
| TopP *float64 `json:"top_p,omitempty"` // Controls diversity via nucleus sampling | ||
| TopK *int `json:"top_k,omitempty"` // Controls diversity via top-k sampling | ||
| MaxTokens *int `json:"max_tokens,omitempty"` // Maximum number of tokens to generate | ||
| StopSequences *[]string `json:"stop_sequences,omitempty"` // Sequences that stop generation | ||
| PresencePenalty *float64 `json:"presence_penalty,omitempty"` // Penalizes repeated tokens | ||
| FrequencyPenalty *float64 `json:"frequency_penalty,omitempty"` // Penalizes frequent tokens | ||
| ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"` // Enables parallel tool calls | ||
| EncodingFormat *string `json:"encoding_format,omitempty"` // Format for embedding output (e.g., "float", "base64") | ||
| Dimensions *int `json:"dimensions,omitempty"` // Number of dimensions for embedding output | ||
| User *string `json:"user,omitempty"` // User identifier for tracking | ||
| ToolChoice *ToolChoice `json:"tool_choice,omitempty"` // Whether to call a tool | ||
| Tools *[]Tool `json:"tools,omitempty"` // Tools to use | ||
| Temperature *float64 `json:"temperature,omitempty"` // Controls randomness in the output | ||
| TopP *float64 `json:"top_p,omitempty"` // Controls diversity via nucleus sampling | ||
| TopK *int `json:"top_k,omitempty"` // Controls diversity via top-k sampling | ||
| MaxTokens *int `json:"max_tokens,omitempty"` // Maximum number of tokens to generate | ||
| StopSequences *[]string `json:"stop_sequences,omitempty"` // Sequences that stop generation | ||
| PresencePenalty *float64 `json:"presence_penalty,omitempty"` // Penalizes repeated tokens | ||
| FrequencyPenalty *float64 `json:"frequency_penalty,omitempty"` // Penalizes frequent tokens | ||
| ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"` // Enables parallel tool calls | ||
| EncodingFormat *string `json:"encoding_format,omitempty"` // Format for embedding output (e.g., "float", "base64") | ||
| Dimensions *int `json:"dimensions,omitempty"` // Number of dimensions for embedding output | ||
| User *string `json:"user,omitempty"` // User identifier for tracking | ||
| StreamOptions *StreamOptions `json:"stream_options,omitempty"` // Stream options for streaming requests | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this streamOptions openai format or they directly take the include_usage parameter? |
||
| // Dynamic parameters that can be provider-specific, they are directly | ||
| // added to the request as is. | ||
| ExtraParams map[string]interface{} `json:"-"` | ||
|
|
@@ -351,6 +357,7 @@ type ContentBlock struct { | |
| // ToolMessage represents a message from a tool | ||
| type ToolMessage struct { | ||
| ToolCallID *string `json:"tool_call_id,omitempty"` | ||
| IsError *bool `json:"is_error,omitempty"` | ||
| } | ||
|
|
||
| // AssistantMessage represents a message from an assistant | ||
|
|
@@ -371,18 +378,41 @@ type ImageURLStruct struct { | |
|
|
||
| // BifrostResponse represents the complete result from any bifrost request. | ||
| type BifrostResponse struct { | ||
| ID string `json:"id,omitempty"` | ||
| Object string `json:"object,omitempty"` // text.completion, chat.completion, or embedding | ||
| Choices []BifrostResponseChoice `json:"choices,omitempty"` | ||
| Embedding [][]float32 `json:"data,omitempty"` // Maps to "data" field in provider responses (e.g., OpenAI embedding format) | ||
| Speech *BifrostSpeech `json:"speech,omitempty"` // Maps to "speech" field in provider responses (e.g., OpenAI speech format) | ||
| Transcribe *BifrostTranscribe `json:"transcribe,omitempty"` // Maps to "transcribe" field in provider responses (e.g., OpenAI transcription format) | ||
| Model string `json:"model,omitempty"` | ||
| Created int `json:"created,omitempty"` // The Unix timestamp (in seconds). | ||
| ServiceTier *string `json:"service_tier,omitempty"` | ||
| SystemFingerprint *string `json:"system_fingerprint,omitempty"` | ||
| Usage *LLMUsage `json:"usage,omitempty"` | ||
| ExtraFields BifrostResponseExtraFields `json:"extra_fields"` | ||
| ID string `json:"id,omitempty"` | ||
| Object string `json:"object,omitempty"` // text.completion, chat.completion, or embedding | ||
| Choices []BifrostResponseChoice `json:"choices,omitempty"` | ||
| Embedding [][]float32 `json:"data,omitempty"` // Maps to "data" field in provider responses (e.g., OpenAI embedding format) | ||
| Speech *BifrostSpeech `json:"speech,omitempty"` // Maps to "speech" field in provider responses (e.g., OpenAI speech format) | ||
| Transcribe *BifrostTranscribe `json:"transcribe,omitempty"` // Maps to "transcribe" field in provider responses (e.g., OpenAI transcription format) | ||
| Model string `json:"model,omitempty"` | ||
| Created int `json:"created,omitempty"` // The Unix timestamp (in seconds). | ||
| ServiceTier *string `json:"service_tier,omitempty"` | ||
| SystemFingerprint *string `json:"system_fingerprint,omitempty"` | ||
| Usage *LLMUsage `json:"usage,omitempty"` | ||
| PromptFilterResults *[]PromptFilterResult `json:"prompt_filter_results,omitempty"` // Azure OpenAI Service | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. []PromptFilterResult is already a pointer so we can remove the * here. |
||
| ExtraFields BifrostResponseExtraFields `json:"extra_fields"` | ||
| } | ||
|
|
||
| // FilterResult represents the result of a content filter. | ||
| type FilterResult struct { | ||
| Filtered bool `json:"filtered"` | ||
| Severity bool `json:"severity"` | ||
| } | ||
|
|
||
| // ContentFilterResult represents the result of a content filter. | ||
| type ContentFilterResult struct { | ||
| HateSpeech FilterResult `json:"hate_speech,omitempty"` | ||
| SelfHarm FilterResult `json:"self_harm,omitempty"` | ||
| Sexual FilterResult `json:"sexual,omitempty"` | ||
| Violence FilterResult `json:"violence,omitempty"` | ||
| Jailbreak FilterResult `json:"jailbreak,omitempty"` | ||
| Profanity FilterResult `json:"profanity,omitempty"` | ||
| } | ||
|
|
||
| // PromptFilterResult represents the result of a prompt filter. | ||
| type PromptFilterResult struct { | ||
| PromptIndex int `json:"prompt_index"` | ||
| ContentFilterResults *ContentFilterResult `json:"content_filter_results"` | ||
| } | ||
|
|
||
| // LLMUsage represents token usage information | ||
|
|
@@ -394,6 +424,36 @@ type LLMUsage struct { | |
| CompletionTokensDetails *CompletionTokensDetails `json:"completion_tokens_details,omitempty"` | ||
| } | ||
|
|
||
| func (u *LLMUsage) Clone() *LLMUsage { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you please specify where this is being used? I guess in most cases we can directly give out LLMUsage obj, but lmk if there's any specific use case for this |
||
| if u == nil { | ||
| return nil | ||
| } | ||
|
|
||
| ret := &LLMUsage{ | ||
| PromptTokens: u.PromptTokens, | ||
| CompletionTokens: u.CompletionTokens, | ||
| TotalTokens: u.TotalTokens, | ||
| } | ||
|
|
||
| if u.TokenDetails != nil { | ||
| ret.TokenDetails = &TokenDetails{ | ||
| CachedTokens: u.TokenDetails.CachedTokens, | ||
| AudioTokens: u.TokenDetails.AudioTokens, | ||
| } | ||
| } | ||
|
|
||
| if u.CompletionTokensDetails != nil { | ||
| ret.CompletionTokensDetails = &CompletionTokensDetails{ | ||
| ReasoningTokens: u.CompletionTokensDetails.ReasoningTokens, | ||
| AudioTokens: u.CompletionTokensDetails.AudioTokens, | ||
| AcceptedPredictionTokens: u.CompletionTokensDetails.AcceptedPredictionTokens, | ||
| RejectedPredictionTokens: u.CompletionTokensDetails.RejectedPredictionTokens, | ||
| } | ||
| } | ||
|
|
||
| return ret | ||
| } | ||
|
|
||
| type AudioLLMUsage struct { | ||
| InputTokens int `json:"input_tokens"` | ||
| InputTokensDetails *AudioTokenDetails `json:"input_tokens_details,omitempty"` | ||
|
|
@@ -494,8 +554,9 @@ type Annotation struct { | |
| // IMPORTANT: Only one of BifrostNonStreamResponseChoice or BifrostStreamResponseChoice | ||
| // should be non-nil at a time. | ||
| type BifrostResponseChoice struct { | ||
| Index int `json:"index"` | ||
| FinishReason *string `json:"finish_reason,omitempty"` | ||
| Index int `json:"index"` | ||
| FinishReason *string `json:"finish_reason,omitempty"` | ||
| ContentFilterResults *ContentFilterResult `json:"content_filter_results,omitempty"` // Azure OpenAI Service or DeepSeek | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We also have ContentFilterResult on outer response, can we have we only in one place to avoid confusion? We can internally parse the providers which return filter results the other way |
||
|
|
||
| *BifrostNonStreamResponseChoice | ||
| *BifrostStreamResponseChoice | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Consider including cache creation tokens in TokenDetails.
The
ToLLMUsage()method only includesCacheReadInputTokensin theTokenDetailsbut notCacheCreationInputTokens. If the schema supports it, consider including both cache token types for complete usage tracking.🏁 Script executed:
Length of output: 299
TokenDetails missing cache creation tokens support
The
TokenDetailsstruct (core/schemas/bifrost.go) only defines:CachedTokens intAudioTokens intIt doesn’t have a field for cache-creation tokens, so
CacheCreationInputTokenscannot be included inToLLMUsage()as-is.If you intend to track creation tokens, please:
• Add a new field in
TokenDetails, e.g.• Update
ToLLMUsage()(core/providers/anthropic.go) accordingly:if u.CacheReadInputTokens > 0 { llmUsage.TokenDetails = &schemas.TokenDetails{ CachedTokens: u.CacheReadInputTokens, + CreatedCachedTokens: u.CacheCreationInputTokens, } }If you don’t need to track creation tokens, you can ignore
CacheCreationInputTokens.🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@fbzhong we can add this new field to the schema if required
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.