1613 lines
65 KiB
Go
1613 lines
65 KiB
Go
package schemas
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// BifrostChatRequest is the request struct for chat completion requests
|
|
type BifrostChatRequest struct {
|
|
Provider ModelProvider `json:"provider"`
|
|
Model string `json:"model"`
|
|
Input []ChatMessage `json:"input,omitempty"`
|
|
Params *ChatParameters `json:"params,omitempty"`
|
|
Fallbacks []Fallback `json:"fallbacks,omitempty"`
|
|
RawRequestBody []byte `json:"-"` // set bifrost-use-raw-request-body to true in ctx to use the raw request body. Bifrost will directly send this to the downstream provider.
|
|
}
|
|
|
|
// GetRawRequestBody returns the raw request body
|
|
func (cr *BifrostChatRequest) GetRawRequestBody() []byte {
|
|
return cr.RawRequestBody
|
|
}
|
|
|
|
func (cr *BifrostChatRequest) GetExtraParams() map[string]interface{} {
|
|
if cr.Params == nil {
|
|
return make(map[string]interface{}, 0)
|
|
}
|
|
return cr.Params.ExtraParams
|
|
}
|
|
|
|
// BifrostChatResponse represents the complete result from a chat completion request.
|
|
type BifrostChatResponse struct {
|
|
ID string `json:"id"`
|
|
Choices []BifrostResponseChoice `json:"choices"`
|
|
Created int `json:"created"` // The Unix timestamp (in seconds).
|
|
Model string `json:"model"`
|
|
Object string `json:"object"` // "chat.completion" or "chat.completion.chunk"
|
|
ServiceTier *string `json:"service_tier,omitempty"`
|
|
SystemFingerprint string `json:"system_fingerprint"`
|
|
Usage *BifrostLLMUsage `json:"usage"`
|
|
ExtraFields BifrostResponseExtraFields `json:"extra_fields"`
|
|
ExtraParams map[string]interface{} `json:"-"`
|
|
|
|
// Perplexity-specific fields
|
|
SearchResults []SearchResult `json:"search_results,omitempty"`
|
|
Videos []VideoResult `json:"videos,omitempty"`
|
|
Citations []string `json:"citations,omitempty"`
|
|
}
|
|
|
|
// BackfillParams populates response fields from the request that are needed
|
|
func (cr *BifrostChatResponse) BackfillParams(request *BifrostChatRequest) {
|
|
if cr == nil || request == nil {
|
|
return
|
|
}
|
|
if cr.Model == "" {
|
|
cr.Model = request.Model
|
|
}
|
|
if cr.Object == "" {
|
|
cr.Object = "chat.completion"
|
|
}
|
|
if cr.Created == 0 {
|
|
cr.Created = int(time.Now().Unix())
|
|
}
|
|
}
|
|
|
|
// ToTextCompletionResponse converts a BifrostChatResponse to a BifrostTextCompletionResponse
|
|
func (cr *BifrostChatResponse) ToTextCompletionResponse() *BifrostTextCompletionResponse {
|
|
if cr == nil {
|
|
return nil
|
|
}
|
|
|
|
if len(cr.Choices) == 0 {
|
|
return &BifrostTextCompletionResponse{
|
|
ID: cr.ID,
|
|
Model: cr.Model,
|
|
Object: "text_completion",
|
|
SystemFingerprint: cr.SystemFingerprint,
|
|
Usage: cr.Usage,
|
|
ExtraFields: BifrostResponseExtraFields{
|
|
RequestType: TextCompletionRequest,
|
|
ChunkIndex: cr.ExtraFields.ChunkIndex,
|
|
Provider: cr.ExtraFields.Provider,
|
|
OriginalModelRequested: cr.ExtraFields.OriginalModelRequested,
|
|
ResolvedModelUsed: cr.ExtraFields.ResolvedModelUsed,
|
|
Latency: cr.ExtraFields.Latency,
|
|
RawResponse: cr.ExtraFields.RawResponse,
|
|
CacheDebug: cr.ExtraFields.CacheDebug,
|
|
ProviderResponseHeaders: cr.ExtraFields.ProviderResponseHeaders,
|
|
},
|
|
}
|
|
}
|
|
|
|
choice := cr.Choices[0]
|
|
|
|
// Handle streaming response choice
|
|
if choice.ChatStreamResponseChoice != nil && choice.ChatStreamResponseChoice.Delta != nil {
|
|
return &BifrostTextCompletionResponse{
|
|
ID: cr.ID,
|
|
Model: cr.Model,
|
|
Object: "text_completion",
|
|
SystemFingerprint: cr.SystemFingerprint,
|
|
Choices: []BifrostResponseChoice{
|
|
{
|
|
Index: 0,
|
|
TextCompletionResponseChoice: &TextCompletionResponseChoice{
|
|
Text: choice.ChatStreamResponseChoice.Delta.Content,
|
|
},
|
|
FinishReason: choice.FinishReason,
|
|
LogProbs: choice.LogProbs,
|
|
},
|
|
},
|
|
Usage: cr.Usage,
|
|
ExtraFields: BifrostResponseExtraFields{
|
|
RequestType: TextCompletionRequest,
|
|
ChunkIndex: cr.ExtraFields.ChunkIndex,
|
|
Provider: cr.ExtraFields.Provider,
|
|
OriginalModelRequested: cr.ExtraFields.OriginalModelRequested,
|
|
ResolvedModelUsed: cr.ExtraFields.ResolvedModelUsed,
|
|
Latency: cr.ExtraFields.Latency,
|
|
RawResponse: cr.ExtraFields.RawResponse,
|
|
CacheDebug: cr.ExtraFields.CacheDebug,
|
|
ProviderResponseHeaders: cr.ExtraFields.ProviderResponseHeaders,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Handle non-streaming response choice
|
|
if choice.ChatNonStreamResponseChoice != nil {
|
|
msg := choice.ChatNonStreamResponseChoice.Message
|
|
var textContent *string
|
|
if msg != nil && msg.Content != nil && msg.Content.ContentStr != nil {
|
|
textContent = msg.Content.ContentStr
|
|
}
|
|
return &BifrostTextCompletionResponse{
|
|
ID: cr.ID,
|
|
Model: cr.Model,
|
|
Object: "text_completion",
|
|
SystemFingerprint: cr.SystemFingerprint,
|
|
Choices: []BifrostResponseChoice{
|
|
{
|
|
Index: 0,
|
|
TextCompletionResponseChoice: &TextCompletionResponseChoice{
|
|
Text: textContent,
|
|
},
|
|
FinishReason: choice.FinishReason,
|
|
LogProbs: choice.LogProbs,
|
|
},
|
|
},
|
|
Usage: cr.Usage,
|
|
ExtraFields: BifrostResponseExtraFields{
|
|
RequestType: TextCompletionRequest,
|
|
ChunkIndex: cr.ExtraFields.ChunkIndex,
|
|
Provider: cr.ExtraFields.Provider,
|
|
OriginalModelRequested: cr.ExtraFields.OriginalModelRequested,
|
|
ResolvedModelUsed: cr.ExtraFields.ResolvedModelUsed,
|
|
Latency: cr.ExtraFields.Latency,
|
|
RawResponse: cr.ExtraFields.RawResponse,
|
|
CacheDebug: cr.ExtraFields.CacheDebug,
|
|
ProviderResponseHeaders: cr.ExtraFields.ProviderResponseHeaders,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Fallback case - return basic response structure
|
|
return &BifrostTextCompletionResponse{
|
|
ID: cr.ID,
|
|
Model: cr.Model,
|
|
Object: "text_completion",
|
|
SystemFingerprint: cr.SystemFingerprint,
|
|
Usage: cr.Usage,
|
|
ExtraFields: BifrostResponseExtraFields{
|
|
RequestType: TextCompletionRequest,
|
|
ChunkIndex: cr.ExtraFields.ChunkIndex,
|
|
Provider: cr.ExtraFields.Provider,
|
|
OriginalModelRequested: cr.ExtraFields.OriginalModelRequested,
|
|
ResolvedModelUsed: cr.ExtraFields.ResolvedModelUsed,
|
|
Latency: cr.ExtraFields.Latency,
|
|
RawResponse: cr.ExtraFields.RawResponse,
|
|
CacheDebug: cr.ExtraFields.CacheDebug,
|
|
ProviderResponseHeaders: cr.ExtraFields.ProviderResponseHeaders,
|
|
},
|
|
}
|
|
}
|
|
|
|
// ChatParameters represents the parameters for a chat completion.
|
|
type ChatParameters struct {
|
|
Audio *ChatAudioParameters `json:"audio,omitempty"` // Audio parameters
|
|
FrequencyPenalty *float64 `json:"frequency_penalty,omitempty"` // Penalizes frequent tokens
|
|
LogitBias *map[string]float64 `json:"logit_bias,omitempty"` // Bias for logit values
|
|
LogProbs *bool `json:"logprobs,omitempty"` // Number of logprobs to return
|
|
MaxCompletionTokens *int `json:"max_completion_tokens,omitempty"` // Maximum number of tokens to generate
|
|
Metadata *map[string]any `json:"metadata,omitempty"` // Metadata to be returned with the response
|
|
Modalities []string `json:"modalities,omitempty"` // Modalities to be returned with the response
|
|
N *int `json:"n,omitempty"` // Number of chat completions to generate when supported
|
|
ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"`
|
|
Prediction *ChatPrediction `json:"prediction,omitempty"` // Predicted output content (OpenAI only)
|
|
PresencePenalty *float64 `json:"presence_penalty,omitempty"` // Penalizes repeated tokens
|
|
PromptCacheKey *string `json:"prompt_cache_key,omitempty"` // Prompt cache key
|
|
PromptCacheRetention *string `json:"prompt_cache_retention,omitempty"` // Prompt cache retention ("in-memory" or "24h")
|
|
Reasoning *ChatReasoning `json:"reasoning,omitempty"` // Reasoning parameters
|
|
ResponseFormat *interface{} `json:"response_format,omitempty"` // Format for the response
|
|
SafetyIdentifier *string `json:"safety_identifier,omitempty"` // Safety identifier
|
|
Seed *int `json:"seed,omitempty"`
|
|
ServiceTier *string `json:"service_tier,omitempty"`
|
|
StreamOptions *ChatStreamOptions `json:"stream_options,omitempty"`
|
|
Stop []string `json:"stop,omitempty"`
|
|
Store *bool `json:"store,omitempty"`
|
|
Temperature *float64 `json:"temperature,omitempty"`
|
|
TopLogProbs *int `json:"top_logprobs,omitempty"`
|
|
TopP *float64 `json:"top_p,omitempty"` // Controls diversity via nucleus sampling
|
|
ToolChoice *ChatToolChoice `json:"tool_choice,omitempty"` // Whether to call a tool
|
|
Tools []ChatTool `json:"tools,omitempty"` // Tools to use
|
|
User *string `json:"user,omitempty"` // User identifier for tracking
|
|
Verbosity *string `json:"verbosity,omitempty"` // "low" | "medium" | "high"
|
|
WebSearchOptions *ChatWebSearchOptions `json:"web_search_options,omitempty"` // Web search options (OpenAI only)
|
|
|
|
// Anthropic-native knobs promoted to the neutral layer. These pass through
|
|
// typed to Anthropic-family providers (honored/stripped per ProviderFeatures
|
|
// in core/providers/anthropic/types.go). Non-Anthropic providers (OpenAI
|
|
// etc.) silently ignore them.
|
|
TopK *int `json:"top_k,omitempty"` // Anthropic top_k sampling
|
|
Speed *string `json:"speed,omitempty"` // "fast" (Anthropic fast-mode-2026-02-01 beta, Opus 4.6 only)
|
|
InferenceGeo *string `json:"inference_geo,omitempty"` // Anthropic inference_geo (Claude API only)
|
|
MCPServers []ChatMCPServer `json:"mcp_servers,omitempty"` // Anthropic MCP connector (mcp-client-2025-11-20)
|
|
Container *ChatContainer `json:"container,omitempty"` // Anthropic container (string id, or object with skills[] — beta skills-2025-10-02)
|
|
CacheControl *CacheControl `json:"cache_control,omitempty"` // Top-level request cache control (Anthropic family)
|
|
TaskBudget *ChatTaskBudget `json:"task_budget,omitempty"` // Anthropic output_config.task_budget (task-budgets-2026-03-13 beta)
|
|
ContextManagement json.RawMessage `json:"context_management,omitempty"` // Anthropic context_management — complex union, passed as raw JSON to the provider layer
|
|
|
|
// Dynamic parameters that can be provider-specific, they are directly
|
|
// added to the request as is.
|
|
ExtraParams map[string]interface{} `json:"-"`
|
|
}
|
|
|
|
// UnmarshalJSON implements custom JSON unmarshalling for ChatParameters.
|
|
func (cp *ChatParameters) UnmarshalJSON(data []byte) error {
|
|
// Alias to avoid recursion
|
|
type Alias ChatParameters
|
|
|
|
// Aux struct adds reasoning_effort for decoding
|
|
var aux struct {
|
|
*Alias
|
|
ReasoningEffort *string `json:"reasoning_effort"` // only for input
|
|
ReasoningMaxTokens *int `json:"reasoning_max_tokens"`
|
|
}
|
|
|
|
aux.Alias = (*Alias)(cp)
|
|
|
|
// Single unmarshal
|
|
if err := Unmarshal(data, &aux); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Now aux.Reasoning (from Alias) and aux.ReasoningEffort are filled
|
|
|
|
// Validate that specific fields don't conflict
|
|
if aux.ReasoningEffort != nil && aux.Reasoning != nil && aux.Reasoning.Effort != nil {
|
|
return fmt.Errorf("both reasoning_effort and reasoning.effort cannot be present at the same time")
|
|
}
|
|
if aux.ReasoningMaxTokens != nil && aux.Reasoning != nil && aux.Reasoning.MaxTokens != nil {
|
|
return fmt.Errorf("both reasoning_max_tokens and reasoning.max_tokens cannot be present at the same time")
|
|
}
|
|
|
|
if aux.ReasoningEffort != nil || aux.ReasoningMaxTokens != nil {
|
|
if cp.Reasoning == nil {
|
|
cp.Reasoning = &ChatReasoning{}
|
|
}
|
|
// Merge top-level fields into the reasoning object
|
|
if aux.ReasoningEffort != nil {
|
|
cp.Reasoning.Effort = aux.ReasoningEffort
|
|
}
|
|
if aux.ReasoningMaxTokens != nil {
|
|
cp.Reasoning.MaxTokens = aux.ReasoningMaxTokens
|
|
}
|
|
}
|
|
// ExtraParams etc. are already handled by the alias
|
|
return nil
|
|
}
|
|
|
|
// ChatAudioParameters represents the parameters for a chat audio completion. (Only supported by OpenAI Models that support audio input)
|
|
type ChatAudioParameters struct {
|
|
Format string `json:"format,omitempty"` // Format for the audio completion
|
|
Voice string `json:"voice,omitempty"` // Voice to use for the audio completion
|
|
}
|
|
|
|
// Not in OpenAI's spec, but needed to support extra parameters for reasoning.
|
|
type ChatReasoning struct {
|
|
Enabled *bool `json:"enabled,omitempty"` // Explicitly enable or disable reasoning (required by OpenRouter to disable reasoning for some models)
|
|
Effort *string `json:"effort,omitempty"` // "none" | "minimal" | "low" | "medium" | "high" (any value other than "none" will enable reasoning)
|
|
MaxTokens *int `json:"max_tokens,omitempty"` // Maximum number of tokens to generate for the reasoning output (required for anthropic)
|
|
Display *string `json:"display,omitempty"` // Anthropic thinking.display: "summarized" | "omitted" (requires model support for adaptive thinking)
|
|
}
|
|
|
|
// ChatPrediction represents predicted output content for the model to reference (OpenAI only).
|
|
// Providing prediction content can significantly reduce latency for certain models.
|
|
type ChatPrediction struct {
|
|
Type string `json:"type"` // Always "content"
|
|
Content interface{} `json:"content"` // String or array of content parts
|
|
}
|
|
|
|
// ChatWebSearchOptions represents web search options for chat completions (OpenAI only).
|
|
type ChatWebSearchOptions struct {
|
|
SearchContextSize *string `json:"search_context_size,omitempty"` // "low" | "medium" | "high"
|
|
UserLocation *ChatWebSearchOptionsUserLocation `json:"user_location,omitempty"`
|
|
}
|
|
|
|
// ChatWebSearchOptionsUserLocation represents user location for web search.
|
|
type ChatWebSearchOptionsUserLocation struct {
|
|
Type string `json:"type"` // "approximate"
|
|
Approximate *ChatWebSearchOptionsUserLocationApproximate `json:"approximate,omitempty"`
|
|
}
|
|
|
|
// ChatWebSearchOptionsUserLocationApproximate represents approximate user location details.
|
|
type ChatWebSearchOptionsUserLocationApproximate struct {
|
|
City *string `json:"city,omitempty"`
|
|
Country *string `json:"country,omitempty"` // Two-letter ISO country code (e.g., "US")
|
|
Region *string `json:"region,omitempty"` // e.g., "California"
|
|
Timezone *string `json:"timezone,omitempty"` // IANA timezone (e.g., "America/Los_Angeles")
|
|
}
|
|
|
|
// ChatStreamOptions represents the stream options for a chat completion.
|
|
type ChatStreamOptions struct {
|
|
IncludeObfuscation *bool `json:"include_obfuscation,omitempty"`
|
|
IncludeUsage *bool `json:"include_usage,omitempty"` // Bifrost marks this as true by default
|
|
}
|
|
|
|
// ChatToolType represents the type of tool.
|
|
type ChatToolType string
|
|
|
|
// ChatToolType values
|
|
const (
|
|
ChatToolTypeFunction ChatToolType = "function"
|
|
ChatToolTypeCustom ChatToolType = "custom"
|
|
)
|
|
|
|
type MCPToolAnnotations struct {
|
|
Title string `json:"title,omitempty"` // Human-readable title for the tool
|
|
ReadOnlyHint *bool `json:"readOnlyHint,omitempty"` // If true, the tool does not modify its environment
|
|
DestructiveHint *bool `json:"destructiveHint,omitempty"` // If true, the tool may perform destructive updates
|
|
IdempotentHint *bool `json:"idempotentHint,omitempty"` // If true, repeated calls with same args have no additional effect
|
|
OpenWorldHint *bool `json:"openWorldHint,omitempty"` // If true, the tool interacts with external entities
|
|
}
|
|
|
|
// ChatTool represents a tool definition.
|
|
//
|
|
// Three shapes coexist under this type:
|
|
// 1. OpenAI function tool: Type="function", Function non-nil.
|
|
// 2. Custom tool: Type="custom", Custom non-nil.
|
|
// 3. Anthropic server tool: Type=server-tool version string (e.g.
|
|
// "web_search_20260209", "computer_20251124", "mcp_toolset"), Function/Custom
|
|
// nil, Name populated at top level, and the variant-specific fields
|
|
// (MaxUses, DisplayWidthPx, etc.) populated inline.
|
|
//
|
|
// JSON shape for (3) matches Anthropic's native tool format directly
|
|
// (e.g. {"type":"web_search_20260209","name":"web_search","max_uses":5}).
|
|
//
|
|
// Custom MarshalJSON/UnmarshalJSON enforce the union invariant:
|
|
// - On marshal, fields that don't match Type are cleared on a copy so the
|
|
// wire format always carries exactly one variant. Mixed caller state
|
|
// (e.g. Type="web_search_20260209" with Function also set) gets
|
|
// canonicalized instead of being forwarded ambiguously to providers.
|
|
// - On unmarshal, tolerantly accept whatever JSON shape comes in, then
|
|
// normalize the decoded struct so downstream code sees a canonical shape.
|
|
type ChatTool struct {
|
|
Type ChatToolType `json:"type"`
|
|
Function *ChatToolFunction `json:"function,omitempty"` // Function definition (shape 1)
|
|
Custom *ChatToolCustom `json:"custom,omitempty"` // Custom tool definition (shape 2)
|
|
CacheControl *CacheControl `json:"cache_control,omitempty"` // Cache control for the tool
|
|
Annotations *MCPToolAnnotations `json:"-"` // MCP tool annotations (Bifrost-internal, never forwarded to providers)
|
|
|
|
// Anthropic-native tool flags promoted to the neutral layer. All optional;
|
|
// ignored by providers that don't support them. Gating per ProviderFeatures
|
|
// in core/providers/anthropic/types.go.
|
|
DeferLoading *bool `json:"defer_loading,omitempty"` // Anthropic advanced-tool-use: defer loading of tool definition
|
|
AllowedCallers []string `json:"allowed_callers,omitempty"` // Anthropic advanced-tool-use: which callers can invoke this tool ("direct", "code_execution_20250825", "code_execution_20260120")
|
|
InputExamples []ChatToolInputExample `json:"input_examples,omitempty"` // Anthropic tool-examples-2025-10-29: example inputs for the tool
|
|
EagerInputStreaming *bool `json:"eager_input_streaming,omitempty"` // Anthropic fine-grained-tool-streaming-2025-05-14: stream input_json_delta before full args are determined (custom tools only)
|
|
|
|
// Anthropic server-tool fields (shape 3). All optional; only populated when
|
|
// Type is a server-tool version string. Function tools carry their name
|
|
// inside Function.Name — use omitempty here so Name doesn't double-emit.
|
|
Name string `json:"name,omitempty"`
|
|
|
|
// web_search_* and web_fetch_*:
|
|
MaxUses *int `json:"max_uses,omitempty"`
|
|
AllowedDomains []string `json:"allowed_domains,omitempty"`
|
|
BlockedDomains []string `json:"blocked_domains,omitempty"`
|
|
UserLocation *ChatToolUserLocation `json:"user_location,omitempty"`
|
|
|
|
// web_fetch_* only:
|
|
MaxContentTokens *int `json:"max_content_tokens,omitempty"`
|
|
Citations *ChatToolCitationsConfig `json:"citations,omitempty"`
|
|
UseCache *bool `json:"use_cache,omitempty"` // web_fetch_20260309+ only
|
|
|
|
// computer_*:
|
|
DisplayWidthPx *int `json:"display_width_px,omitempty"`
|
|
DisplayHeightPx *int `json:"display_height_px,omitempty"`
|
|
DisplayNumber *int `json:"display_number,omitempty"`
|
|
EnableZoom *bool `json:"enable_zoom,omitempty"` // computer_20251124 only
|
|
|
|
// text_editor_20250728+:
|
|
MaxCharacters *int `json:"max_characters,omitempty"`
|
|
|
|
// mcp_toolset:
|
|
MCPServerName string `json:"mcp_server_name,omitempty"`
|
|
DefaultConfig *ChatMCPToolsetConfig `json:"default_config,omitempty"`
|
|
Configs map[string]*ChatMCPToolsetConfig `json:"configs,omitempty"`
|
|
}
|
|
|
|
// normalizeShape clears fields that don't belong to the ChatTool's active
|
|
// variant, encoding the three-way union invariant:
|
|
//
|
|
// 1. Type="function": keep Function; nil Custom, server-tool Name, and
|
|
// variant metadata (function tools carry their name inside Function.Name).
|
|
// 2. Type="custom": keep Custom and top-level Name; nil Function and
|
|
// server-tool variant metadata.
|
|
// 3. Any other Type: server-tool variant — keep Name and variant fields;
|
|
// nil Function and Custom.
|
|
//
|
|
// Called by both Marshal (strict wire format) and Unmarshal (canonicalize
|
|
// after tolerant decode of potentially mixed input).
|
|
func (t *ChatTool) normalizeShape() {
|
|
switch t.Type {
|
|
case ChatToolTypeFunction:
|
|
t.Custom = nil
|
|
t.Name = ""
|
|
t.clearServerToolVariantFields()
|
|
case ChatToolTypeCustom:
|
|
t.Function = nil
|
|
t.clearServerToolVariantFields()
|
|
default:
|
|
t.Function = nil
|
|
t.Custom = nil
|
|
}
|
|
}
|
|
|
|
func (t *ChatTool) clearServerToolVariantFields() {
|
|
t.MaxUses = nil
|
|
t.AllowedDomains = nil
|
|
t.BlockedDomains = nil
|
|
t.UserLocation = nil
|
|
t.MaxContentTokens = nil
|
|
t.Citations = nil
|
|
t.UseCache = nil
|
|
t.DisplayWidthPx = nil
|
|
t.DisplayHeightPx = nil
|
|
t.DisplayNumber = nil
|
|
t.EnableZoom = nil
|
|
t.MaxCharacters = nil
|
|
t.MCPServerName = ""
|
|
t.DefaultConfig = nil
|
|
t.Configs = nil
|
|
}
|
|
|
|
// MarshalJSON enforces the ChatTool union invariant: exactly one variant's
|
|
// fields are emitted on the wire, matching Type. A mix-state tool
|
|
// (e.g. Type="web_search_20260209" with Function also populated) would
|
|
// otherwise serialize both, and downstream provider converters — which
|
|
// dispatch on the top-level Type/Name shape — could misinterpret or
|
|
// silently forward the stray fields.
|
|
func (t ChatTool) MarshalJSON() ([]byte, error) {
|
|
normalized := t
|
|
normalized.normalizeShape()
|
|
type Alias ChatTool
|
|
return MarshalSorted((*Alias)(&normalized))
|
|
}
|
|
|
|
// UnmarshalJSON tolerantly decodes whatever JSON shape arrives, then
|
|
// canonicalizes the struct via normalizeShape so downstream code sees a
|
|
// single-variant result even if the input mixed multiple variants.
|
|
// Resets the receiver before decoding so omitted optional fields from a
|
|
// prior payload don't survive the new decode; mirrors ChatContainer.UnmarshalJSON.
|
|
func (t *ChatTool) UnmarshalJSON(data []byte) error {
|
|
trimmed := bytes.TrimSpace(data)
|
|
if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
|
|
*t = ChatTool{}
|
|
return nil
|
|
}
|
|
|
|
type Alias ChatTool
|
|
var temp Alias
|
|
if err := Unmarshal(data, &temp); err != nil {
|
|
return err
|
|
}
|
|
*t = ChatTool(temp)
|
|
t.normalizeShape()
|
|
return nil
|
|
}
|
|
|
|
// ChatToolUserLocation is the neutral user_location for web_search tools.
|
|
type ChatToolUserLocation struct {
|
|
Type *string `json:"type,omitempty"` // "approximate"
|
|
City *string `json:"city,omitempty"`
|
|
Region *string `json:"region,omitempty"`
|
|
Country *string `json:"country,omitempty"`
|
|
Timezone *string `json:"timezone,omitempty"`
|
|
}
|
|
|
|
// ChatToolCitationsConfig is the request-side citations config on web_fetch
|
|
// ({"enabled": true/false}). Distinct from response-side text citations.
|
|
type ChatToolCitationsConfig struct {
|
|
Enabled *bool `json:"enabled,omitempty"`
|
|
}
|
|
|
|
// ChatMCPToolsetConfig configures an MCP toolset entry (mcp_toolset tool).
|
|
type ChatMCPToolsetConfig struct {
|
|
Enabled *bool `json:"enabled,omitempty"`
|
|
DeferLoading *bool `json:"defer_loading,omitempty"`
|
|
}
|
|
|
|
// ChatToolFunction represents a function definition.
|
|
type ChatToolFunction struct {
|
|
Name string `json:"name"` // Name of the function
|
|
Description *string `json:"description,omitempty"` // Description of the parameters
|
|
Parameters *ToolFunctionParameters `json:"parameters,omitempty"` // A JSON schema object describing the parameters
|
|
Strict *bool `json:"strict,omitempty"` // Whether to enforce strict parameter validation
|
|
}
|
|
|
|
// ToolFunctionParameters represents the parameters for a function definition.
|
|
// It supports JSON Schema fields used by various providers (OpenAI, Anthropic, Gemini, etc.).
|
|
// Field order follows JSON Schema / OpenAI conventions for consistent serialization.
|
|
//
|
|
// IMPORTANT: When marshalling to JSON, key order is preserved from the original input
|
|
// (captured during UnmarshalJSON). When constructing programmatically, the default
|
|
// struct field declaration order is used. This is critical because LLMs are
|
|
// sensitive to JSON key ordering in tool schemas.
|
|
type ToolFunctionParameters struct {
|
|
Type string `json:"type"` // Type of the parameters
|
|
Description *string `json:"description,omitempty"` // Description of the parameters
|
|
Properties *OrderedMap `json:"properties"` // Parameter properties - always include even if empty (required by JSON Schema and some providers like OpenAI)
|
|
Required []string `json:"required,omitempty"` // Required parameter names
|
|
AdditionalProperties *AdditionalPropertiesStruct `json:"additionalProperties,omitempty"` // Whether to allow additional properties
|
|
Enum []string `json:"enum,omitempty"` // Enum values for the parameters
|
|
|
|
// JSON Schema definition fields
|
|
Defs *OrderedMap `json:"$defs,omitempty"` // JSON Schema draft 2019-09+ definitions
|
|
Definitions *OrderedMap `json:"definitions,omitempty"` // Legacy JSON Schema draft-07 definitions
|
|
Ref *string `json:"$ref,omitempty"` // Reference to definition
|
|
|
|
// Array schema fields
|
|
Items *OrderedMap `json:"items,omitempty"` // Array element schema
|
|
MinItems *int64 `json:"minItems,omitempty"` // Minimum array length
|
|
MaxItems *int64 `json:"maxItems,omitempty"` // Maximum array length
|
|
|
|
// Composition fields (union types)
|
|
AnyOf []OrderedMap `json:"anyOf,omitempty"` // Union types (any of these schemas)
|
|
OneOf []OrderedMap `json:"oneOf,omitempty"` // Exclusive union types (exactly one of these)
|
|
AllOf []OrderedMap `json:"allOf,omitempty"` // Schema intersection (all of these)
|
|
|
|
// String validation fields
|
|
Format *string `json:"format,omitempty"` // String format (email, date, uri, etc.)
|
|
Pattern *string `json:"pattern,omitempty"` // Regex pattern for strings
|
|
MinLength *int64 `json:"minLength,omitempty"` // Minimum string length
|
|
MaxLength *int64 `json:"maxLength,omitempty"` // Maximum string length
|
|
|
|
// Number validation fields
|
|
Minimum *float64 `json:"minimum,omitempty"` // Minimum number value
|
|
Maximum *float64 `json:"maximum,omitempty"` // Maximum number value
|
|
|
|
// Misc fields
|
|
Title *string `json:"title,omitempty"` // Schema title
|
|
Default interface{} `json:"default,omitempty"` // Default value
|
|
Nullable *bool `json:"nullable,omitempty"` // Nullable indicator (OpenAPI 3.0 style)
|
|
|
|
// keyOrder preserves the JSON key order from the original input so that
|
|
// MarshalJSON can emit keys in the same order the client sent them.
|
|
keyOrder JSONKeyOrder `json:"-"`
|
|
// explicitEmptyObject tracks a client-supplied raw {} schema.
|
|
explicitEmptyObject bool `json:"-"`
|
|
}
|
|
|
|
// MarshalJSON serializes ToolFunctionParameters while preserving the original
|
|
// top-level key order when available. A client-supplied raw `{}` stays `{}`;
|
|
// otherwise object schemas always emit `properties` as an object, never null.
|
|
func (t ToolFunctionParameters) MarshalJSON() ([]byte, error) {
|
|
if t.explicitEmptyObject && !t.hasDefinedSchemaFields() {
|
|
return []byte("{}"), nil
|
|
}
|
|
if t.Properties == nil {
|
|
// Initialize with an empty map (not nil values) so it marshals to {} instead of null
|
|
// Required by OpenAI and JSON Schema spec
|
|
t.Properties = &OrderedMap{values: make(map[string]interface{})}
|
|
}
|
|
type Alias ToolFunctionParameters
|
|
data, err := MarshalSorted(Alias(t))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return t.keyOrder.Apply(data)
|
|
}
|
|
|
|
// UnmarshalJSON implements custom JSON unmarshalling for ToolFunctionParameters.
|
|
// It handles both JSON object format (standard) and JSON string format (used by some providers like xAI).
|
|
// It captures the original key order for order-preserving re-serialization and
|
|
// records whether the client provided an explicit empty object schema.
|
|
func (t *ToolFunctionParameters) UnmarshalJSON(data []byte) error {
|
|
// Try to unmarshal as a JSON string first (xAI sends parameters as a string)
|
|
var jsonStr string
|
|
if err := Unmarshal(data, &jsonStr); err == nil {
|
|
data = []byte(jsonStr)
|
|
}
|
|
type Alias ToolFunctionParameters
|
|
var temp Alias
|
|
if err := Unmarshal(data, &temp); err != nil {
|
|
return fmt.Errorf("failed to unmarshal ToolFunctionParameters: %w", err)
|
|
}
|
|
*t = ToolFunctionParameters(temp)
|
|
|
|
// Normalize additionalProperties: null to omitted field
|
|
if t.AdditionalProperties != nil &&
|
|
t.AdditionalProperties.AdditionalPropertiesBool == nil &&
|
|
t.AdditionalProperties.AdditionalPropertiesMap == nil {
|
|
t.AdditionalProperties = nil
|
|
}
|
|
|
|
trimmed := bytes.TrimSpace(data)
|
|
if len(trimmed) >= 2 && trimmed[0] == '{' && trimmed[len(trimmed)-1] == '}' {
|
|
inner := bytes.TrimSpace(trimmed[1 : len(trimmed)-1])
|
|
t.explicitEmptyObject = len(inner) == 0
|
|
} else {
|
|
t.explicitEmptyObject = false
|
|
}
|
|
t.keyOrder.Capture(data)
|
|
return nil
|
|
}
|
|
|
|
// Normalized returns a shallow copy of the ToolFunctionParameters with JSON
|
|
// Schema structural keys sorted by priority (type, description, properties,
|
|
// required first, then alphabetically), while preserving the client's original
|
|
// ordering of user-defined property names inside "properties" maps. The copy
|
|
// shares primitive values with the original but has independent key slices,
|
|
// so sorting does not mutate the caller's data.
|
|
//
|
|
// User-defined property names (e.g., "chain_of_thought", "answer") are kept
|
|
// in their original order because LLMs generate structured output fields in
|
|
// schema-declared order. Reordering them alphabetically can degrade output
|
|
// quality (e.g., forcing the model to write an answer before its reasoning).
|
|
//
|
|
// The captured keyOrder is cleared so the struct field declaration order is
|
|
// used for the top-level keys. This produces deterministic JSON serialization
|
|
// regardless of the client's original structural key ordering, which is
|
|
// critical for Anthropic's prefix-based prompt caching.
|
|
func (t *ToolFunctionParameters) Normalized() *ToolFunctionParameters {
|
|
if t == nil {
|
|
return nil
|
|
}
|
|
out := *t
|
|
out.keyOrder = JSONKeyOrder{}
|
|
// Properties contains user-defined field names whose order is semantically
|
|
// meaningful for LLM structured output generation. Preserve their key order
|
|
// while sorting nested schema structural keys for caching determinism.
|
|
out.Properties = t.Properties.preserveKeysWithPropertyAwareness()
|
|
out.Defs = t.Defs.SortedCopyPreservingProperties()
|
|
out.Definitions = t.Definitions.SortedCopyPreservingProperties()
|
|
out.Items = t.Items.SortedCopyPreservingProperties()
|
|
if len(t.AnyOf) > 0 {
|
|
out.AnyOf = make([]OrderedMap, len(t.AnyOf))
|
|
for i := range t.AnyOf {
|
|
if cp := t.AnyOf[i].SortedCopyPreservingProperties(); cp != nil {
|
|
out.AnyOf[i] = *cp
|
|
}
|
|
}
|
|
}
|
|
if len(t.OneOf) > 0 {
|
|
out.OneOf = make([]OrderedMap, len(t.OneOf))
|
|
for i := range t.OneOf {
|
|
if cp := t.OneOf[i].SortedCopyPreservingProperties(); cp != nil {
|
|
out.OneOf[i] = *cp
|
|
}
|
|
}
|
|
}
|
|
if len(t.AllOf) > 0 {
|
|
out.AllOf = make([]OrderedMap, len(t.AllOf))
|
|
for i := range t.AllOf {
|
|
if cp := t.AllOf[i].SortedCopyPreservingProperties(); cp != nil {
|
|
out.AllOf[i] = *cp
|
|
}
|
|
}
|
|
}
|
|
if t.AdditionalProperties != nil && t.AdditionalProperties.AdditionalPropertiesMap != nil {
|
|
out.AdditionalProperties = &AdditionalPropertiesStruct{
|
|
AdditionalPropertiesBool: t.AdditionalProperties.AdditionalPropertiesBool,
|
|
AdditionalPropertiesMap: t.AdditionalProperties.AdditionalPropertiesMap.SortedCopyPreservingProperties(),
|
|
}
|
|
}
|
|
switch v := t.Default.(type) {
|
|
case *OrderedMap:
|
|
out.Default = v.SortedCopy()
|
|
case map[string]interface{}:
|
|
out.Default = OrderedMapFromMap(v).SortedCopy()
|
|
case []interface{}:
|
|
out.Default = sortedCopySlice(v)
|
|
}
|
|
return &out
|
|
}
|
|
|
|
// hasDefinedSchemaFields reports whether the schema contains any real JSON Schema
|
|
// fields, allowing MarshalJSON to distinguish an explicit raw `{}` from a
|
|
// populated object schema such as `{"type":"object","properties":{}}`.
|
|
func (t *ToolFunctionParameters) hasDefinedSchemaFields() bool {
|
|
if t == nil {
|
|
return false
|
|
}
|
|
if t.Type != "" || t.Description != nil || len(t.Required) > 0 || t.AdditionalProperties != nil || len(t.Enum) > 0 {
|
|
return true
|
|
}
|
|
if t.Properties != nil || t.Defs != nil || t.Definitions != nil || t.Ref != nil {
|
|
return true
|
|
}
|
|
if t.Items != nil || t.MinItems != nil || t.MaxItems != nil {
|
|
return true
|
|
}
|
|
if len(t.AnyOf) > 0 || len(t.OneOf) > 0 || len(t.AllOf) > 0 {
|
|
return true
|
|
}
|
|
if t.Format != nil || t.Pattern != nil || t.MinLength != nil || t.MaxLength != nil {
|
|
return true
|
|
}
|
|
if t.Minimum != nil || t.Maximum != nil {
|
|
return true
|
|
}
|
|
return t.Title != nil || t.Default != nil || t.Nullable != nil
|
|
}
|
|
|
|
type AdditionalPropertiesStruct struct {
|
|
AdditionalPropertiesBool *bool
|
|
AdditionalPropertiesMap *OrderedMap
|
|
}
|
|
|
|
// MarshalJSON implements custom JSON marshalling for AdditionalPropertiesStruct.
|
|
// It marshals either AdditionalPropertiesBool or AdditionalPropertiesMap based on which is set.
|
|
func (a AdditionalPropertiesStruct) MarshalJSON() ([]byte, error) {
|
|
// if both are set, return an error
|
|
if a.AdditionalPropertiesBool != nil && a.AdditionalPropertiesMap != nil {
|
|
return nil, fmt.Errorf("both AdditionalPropertiesBool and AdditionalPropertiesMap are set; only one should be non-nil")
|
|
}
|
|
|
|
// If bool is set, marshal as boolean
|
|
if a.AdditionalPropertiesBool != nil {
|
|
return MarshalSorted(*a.AdditionalPropertiesBool)
|
|
}
|
|
|
|
// If map is set, marshal as object
|
|
if a.AdditionalPropertiesMap != nil {
|
|
return MarshalSorted(a.AdditionalPropertiesMap)
|
|
}
|
|
|
|
// If both are nil, return null
|
|
return nil, fmt.Errorf("additionalProperties cannot be null; omit the field instead")
|
|
}
|
|
|
|
// UnmarshalJSON implements custom JSON unmarshalling for AdditionalPropertiesStruct.
|
|
// It handles both boolean and object types for additionalProperties.
|
|
func (a *AdditionalPropertiesStruct) UnmarshalJSON(data []byte) error {
|
|
if bytes.Equal(bytes.TrimSpace(data), []byte("null")) {
|
|
a.AdditionalPropertiesBool = nil
|
|
a.AdditionalPropertiesMap = nil
|
|
return nil
|
|
}
|
|
|
|
// First, try to unmarshal as a boolean
|
|
var boolValue bool
|
|
if err := Unmarshal(data, &boolValue); err == nil {
|
|
a.AdditionalPropertiesMap = nil
|
|
a.AdditionalPropertiesBool = &boolValue
|
|
return nil
|
|
}
|
|
|
|
// If that fails, try to unmarshal as a map
|
|
var mapValue OrderedMap
|
|
if err := Unmarshal(data, &mapValue); err == nil {
|
|
a.AdditionalPropertiesBool = nil
|
|
a.AdditionalPropertiesMap = &mapValue
|
|
return nil
|
|
}
|
|
|
|
// If both fail, return an error
|
|
return fmt.Errorf("additionalProperties must be either a boolean or an object")
|
|
}
|
|
|
|
type ChatToolCustom struct {
|
|
Format *ChatToolCustomFormat `json:"format,omitempty"` // The input format
|
|
}
|
|
|
|
type ChatToolCustomFormat struct {
|
|
Type string `json:"type"` // always "text"
|
|
Grammar *ChatToolCustomGrammarFormat `json:"grammar,omitempty"`
|
|
}
|
|
|
|
// ChatToolCustomGrammarFormat - A grammar defined by the user
|
|
type ChatToolCustomGrammarFormat struct {
|
|
Definition string `json:"definition"` // The grammar definition
|
|
Syntax string `json:"syntax"` // "lark" | "regex"
|
|
}
|
|
|
|
// ChatToolChoiceType for all providers, make sure to check the provider's
|
|
// documentation to see which tool choices are supported.
|
|
type ChatToolChoiceType string
|
|
|
|
// ChatToolChoiceType values
|
|
const (
|
|
ChatToolChoiceTypeNone ChatToolChoiceType = "none"
|
|
ChatToolChoiceTypeAuto ChatToolChoiceType = "auto"
|
|
ChatToolChoiceTypeAny ChatToolChoiceType = "any"
|
|
ChatToolChoiceTypeRequired ChatToolChoiceType = "required"
|
|
// ChatToolChoiceTypeFunction means a specific tool must be called
|
|
ChatToolChoiceTypeFunction ChatToolChoiceType = "function"
|
|
// ChatToolChoiceTypeAllowedTools means a specific tool must be called
|
|
ChatToolChoiceTypeAllowedTools ChatToolChoiceType = "allowed_tools"
|
|
// ChatToolChoiceTypeCustom means a custom tool must be called
|
|
ChatToolChoiceTypeCustom ChatToolChoiceType = "custom"
|
|
)
|
|
|
|
// ChatToolChoiceStruct represents a tool choice.
|
|
type ChatToolChoiceStruct struct {
|
|
Type ChatToolChoiceType `json:"type"` // Type of tool choice
|
|
Function *ChatToolChoiceFunction `json:"function,omitempty"` // Function to call if type is ToolChoiceTypeFunction
|
|
Custom *ChatToolChoiceCustom `json:"custom,omitempty"` // Custom tool to call if type is ToolChoiceTypeCustom
|
|
AllowedTools *ChatToolChoiceAllowedTools `json:"allowed_tools,omitempty"` // Allowed tools to call if type is ToolChoiceTypeAllowedTools
|
|
}
|
|
|
|
// MarshalJSON serializes ChatToolChoiceStruct to JSON, emitting only the "type"
|
|
// field and the active variant. This prevents zero-value fields from unused
|
|
// variants (e.g., "custom", "allowed_tools") from appearing in the output,
|
|
// and ensures consistent field ordering with "type" always first.
|
|
func (s ChatToolChoiceStruct) MarshalJSON() ([]byte, error) {
|
|
var buf bytes.Buffer
|
|
buf.WriteByte('{')
|
|
|
|
// Always emit "type" first
|
|
typeBytes, err := MarshalSorted(string(s.Type))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
buf.WriteString(`"type":`)
|
|
buf.Write(typeBytes)
|
|
|
|
switch s.Type {
|
|
case ChatToolChoiceTypeFunction:
|
|
if s.Function != nil {
|
|
funcBytes, err := MarshalSorted(s.Function)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
buf.WriteString(`,"function":`)
|
|
buf.Write(funcBytes)
|
|
}
|
|
case ChatToolChoiceTypeCustom:
|
|
if s.Custom != nil {
|
|
customBytes, err := MarshalSorted(s.Custom)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
buf.WriteString(`,"custom":`)
|
|
buf.Write(customBytes)
|
|
}
|
|
case ChatToolChoiceTypeAllowedTools:
|
|
if s.AllowedTools != nil {
|
|
allowedBytes, err := MarshalSorted(s.AllowedTools)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
buf.WriteString(`,"allowed_tools":`)
|
|
buf.Write(allowedBytes)
|
|
}
|
|
}
|
|
|
|
buf.WriteByte('}')
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// UnmarshalJSON deserializes JSON into ChatToolChoiceStruct and cleans up
|
|
// zero-value pointers that sonic may allocate for absent fields.
|
|
func (s *ChatToolChoiceStruct) UnmarshalJSON(data []byte) error {
|
|
type Alias ChatToolChoiceStruct
|
|
var temp Alias
|
|
if err := Unmarshal(data, &temp); err != nil {
|
|
return err
|
|
}
|
|
*s = ChatToolChoiceStruct(temp)
|
|
|
|
// Clean up zero-value pointers that sonic may allocate even when the
|
|
// corresponding key was absent from the JSON input.
|
|
switch s.Type {
|
|
case ChatToolChoiceTypeFunction:
|
|
s.Custom = nil
|
|
s.AllowedTools = nil
|
|
case ChatToolChoiceTypeCustom:
|
|
s.Function = nil
|
|
s.AllowedTools = nil
|
|
case ChatToolChoiceTypeAllowedTools:
|
|
s.Function = nil
|
|
s.Custom = nil
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type ChatToolChoice struct {
|
|
ChatToolChoiceStr *string
|
|
ChatToolChoiceStruct *ChatToolChoiceStruct
|
|
}
|
|
|
|
// MarshalJSON implements custom JSON marshalling for ChatMessageContent.
|
|
// It marshals either ContentStr or ContentBlocks directly without wrapping.
|
|
func (ctc ChatToolChoice) MarshalJSON() ([]byte, error) {
|
|
// Validation: ensure only one field is set at a time
|
|
if ctc.ChatToolChoiceStr != nil && ctc.ChatToolChoiceStruct != nil {
|
|
return nil, fmt.Errorf("both ChatToolChoiceStr, ChatToolChoiceStruct are set; only one should be non-nil")
|
|
}
|
|
|
|
if ctc.ChatToolChoiceStr != nil {
|
|
return MarshalSorted(ctc.ChatToolChoiceStr)
|
|
}
|
|
if ctc.ChatToolChoiceStruct != nil {
|
|
return MarshalSorted(ctc.ChatToolChoiceStruct)
|
|
}
|
|
// If both are nil, return null
|
|
return MarshalSorted(nil)
|
|
}
|
|
|
|
// UnmarshalJSON implements custom JSON unmarshalling for ChatMessageContent.
|
|
// It determines whether "content" is a string or array and assigns to the appropriate field.
|
|
// It also handles direct string/array content without a wrapper object.
|
|
func (ctc *ChatToolChoice) UnmarshalJSON(data []byte) error {
|
|
// First, try to unmarshal as a direct string
|
|
var toolChoiceStr string
|
|
if err := Unmarshal(data, &toolChoiceStr); err == nil {
|
|
ctc.ChatToolChoiceStr = &toolChoiceStr
|
|
ctc.ChatToolChoiceStruct = nil
|
|
return nil
|
|
}
|
|
|
|
// Try to unmarshal as a direct array of ContentBlock
|
|
var chatToolChoice ChatToolChoiceStruct
|
|
if err := Unmarshal(data, &chatToolChoice); err == nil {
|
|
ctc.ChatToolChoiceStr = nil
|
|
ctc.ChatToolChoiceStruct = &chatToolChoice
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("tool_choice field is neither a string nor a ChatToolChoiceStruct object")
|
|
}
|
|
|
|
// ChatToolChoiceFunction represents a function choice.
|
|
type ChatToolChoiceFunction struct {
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// ChatToolChoiceCustom represents a custom choice.
|
|
type ChatToolChoiceCustom struct {
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// ChatToolChoiceAllowedTools represents a allowed tools choice.
|
|
type ChatToolChoiceAllowedTools struct {
|
|
Mode string `json:"mode"` // "auto" | "required"
|
|
Tools []ChatToolChoiceAllowedToolsTool `json:"tools"`
|
|
}
|
|
|
|
// ChatToolChoiceAllowedToolsTool represents a allowed tools tool.
|
|
type ChatToolChoiceAllowedToolsTool struct {
|
|
Type string `json:"type"` // "function"
|
|
Function ChatToolChoiceFunction `json:"function,omitempty"`
|
|
}
|
|
|
|
// ChatMessageRole represents the role of a chat message
|
|
type ChatMessageRole string
|
|
|
|
// ChatMessageRole values
|
|
const (
|
|
ChatMessageRoleAssistant ChatMessageRole = "assistant"
|
|
ChatMessageRoleUser ChatMessageRole = "user"
|
|
ChatMessageRoleSystem ChatMessageRole = "system"
|
|
ChatMessageRoleTool ChatMessageRole = "tool"
|
|
ChatMessageRoleDeveloper ChatMessageRole = "developer"
|
|
)
|
|
|
|
// ChatMessage represents a message in a chat conversation.
|
|
type ChatMessage struct {
|
|
Name *string `json:"name,omitempty"` // for chat completions
|
|
Role ChatMessageRole `json:"role,omitempty"`
|
|
Content *ChatMessageContent `json:"content,omitempty"`
|
|
|
|
// Embedded pointer structs - when non-nil, their exported fields are flattened into the top-level JSON object
|
|
// IMPORTANT: Only one of the following can be non-nil at a time, otherwise the JSON marshalling will override the common fields
|
|
*ChatToolMessage
|
|
*ChatAssistantMessage
|
|
}
|
|
|
|
// UnmarshalJSON implements custom JSON unmarshalling for ChatMessage.
|
|
// This is needed because ChatAssistantMessage has a custom UnmarshalJSON method,
|
|
// which interferes with the JSON library's handling of other fields in ChatMessage.
|
|
func (cm *ChatMessage) UnmarshalJSON(data []byte) error {
|
|
// Unmarshal the base fields directly
|
|
type baseFields struct {
|
|
Name *string `json:"name,omitempty"`
|
|
Role ChatMessageRole `json:"role,omitempty"`
|
|
Content *ChatMessageContent `json:"content,omitempty"`
|
|
}
|
|
var base baseFields
|
|
if err := Unmarshal(data, &base); err != nil {
|
|
return err
|
|
}
|
|
cm.Name = base.Name
|
|
cm.Role = base.Role
|
|
cm.Content = base.Content
|
|
|
|
// Unmarshal ChatToolMessage fields
|
|
type toolMsgAlias ChatToolMessage
|
|
var toolMsg toolMsgAlias
|
|
if err := Unmarshal(data, &toolMsg); err != nil {
|
|
return err
|
|
}
|
|
if toolMsg.ToolCallID != nil {
|
|
cm.ChatToolMessage = (*ChatToolMessage)(&toolMsg)
|
|
}
|
|
|
|
// Unmarshal ChatAssistantMessage (which has its own custom unmarshaller)
|
|
var assistantMsg ChatAssistantMessage
|
|
if err := Unmarshal(data, &assistantMsg); err != nil {
|
|
return err
|
|
}
|
|
// Only set if any field is populated
|
|
if assistantMsg.Refusal != nil || assistantMsg.Reasoning != nil ||
|
|
len(assistantMsg.ReasoningDetails) > 0 || len(assistantMsg.Annotations) > 0 ||
|
|
len(assistantMsg.ToolCalls) > 0 || assistantMsg.Audio != nil {
|
|
cm.ChatAssistantMessage = &assistantMsg
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ChatMessageContent represents a content in a message.
|
|
type ChatMessageContent struct {
|
|
ContentStr *string
|
|
ContentBlocks []ChatContentBlock
|
|
}
|
|
|
|
// MarshalJSON implements custom JSON marshalling for ChatMessageContent.
|
|
// It marshals either ContentStr or ContentBlocks directly without wrapping.
|
|
func (mc ChatMessageContent) MarshalJSON() ([]byte, error) {
|
|
// Validation: ensure only one field is set at a time
|
|
if mc.ContentStr != nil && mc.ContentBlocks != nil {
|
|
return nil, fmt.Errorf("both Content string and Content blocks are set; only one should be non-nil")
|
|
}
|
|
|
|
if mc.ContentStr != nil {
|
|
return MarshalSorted(*mc.ContentStr)
|
|
}
|
|
if mc.ContentBlocks != nil {
|
|
return MarshalSorted(mc.ContentBlocks)
|
|
}
|
|
// If both are nil, return null
|
|
return MarshalSorted(nil)
|
|
}
|
|
|
|
// UnmarshalJSON implements custom JSON unmarshalling for ChatMessageContent.
|
|
// It determines whether "content" is a string or array and assigns to the appropriate field.
|
|
// It also handles direct string/array content without a wrapper object.
|
|
func (mc *ChatMessageContent) UnmarshalJSON(data []byte) error {
|
|
trimmed := bytes.TrimSpace(data)
|
|
if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
|
|
mc.ContentStr = nil
|
|
mc.ContentBlocks = nil
|
|
return nil
|
|
}
|
|
|
|
// First, try to unmarshal as a direct string
|
|
var stringContent string
|
|
if err := Unmarshal(data, &stringContent); err == nil {
|
|
mc.ContentStr = &stringContent
|
|
mc.ContentBlocks = nil
|
|
return nil
|
|
}
|
|
|
|
// Try to unmarshal as a direct array of ContentBlock
|
|
var arrayContent []ChatContentBlock
|
|
if err := Unmarshal(data, &arrayContent); err == nil {
|
|
mc.ContentBlocks = arrayContent
|
|
mc.ContentStr = nil
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("content field is neither a string nor an array of Content blocks")
|
|
}
|
|
|
|
// ChatContentBlockType represents the type of content block in a message.
|
|
type ChatContentBlockType string
|
|
|
|
// ChatContentBlockType values
|
|
const (
|
|
ChatContentBlockTypeText ChatContentBlockType = "text"
|
|
ChatContentBlockTypeImage ChatContentBlockType = "image_url"
|
|
ChatContentBlockTypeInputAudio ChatContentBlockType = "input_audio"
|
|
ChatContentBlockTypeFile ChatContentBlockType = "file"
|
|
ChatContentBlockTypeRefusal ChatContentBlockType = "refusal"
|
|
)
|
|
|
|
// ChatContentBlock represents a content block in a message.
|
|
type ChatContentBlock struct {
|
|
Type ChatContentBlockType `json:"type"`
|
|
Text *string `json:"text,omitempty"`
|
|
Refusal *string `json:"refusal,omitempty"`
|
|
ImageURLStruct *ChatInputImage `json:"image_url,omitempty"`
|
|
InputAudio *ChatInputAudio `json:"input_audio,omitempty"`
|
|
File *ChatInputFile `json:"file,omitempty"`
|
|
|
|
// Not in OpenAI's schemas, but sent by a few providers (Anthropic, Bedrock are some of them)
|
|
CacheControl *CacheControl `json:"cache_control,omitempty"`
|
|
Citations *Citations `json:"citations,omitempty"`
|
|
|
|
// CachePoint is a Bedrock-specific field for standalone cache point blocks
|
|
// When present without other content, this indicates a cache point marker
|
|
CachePoint *CachePoint `json:"cachePoint,omitempty"`
|
|
}
|
|
|
|
// CachePoint represents a cache point marker (Bedrock-specific)
|
|
type CachePoint struct {
|
|
Type string `json:"type"` // "default"
|
|
}
|
|
|
|
type CacheControlType string
|
|
|
|
const (
|
|
CacheControlTypeEphemeral CacheControlType = "ephemeral"
|
|
)
|
|
|
|
type CacheControl struct {
|
|
Type CacheControlType `json:"type"`
|
|
TTL *string `json:"ttl,omitempty"` // "1m" | "1h"
|
|
Scope *string `json:"scope,omitempty"` // "user" | "global"
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Neutral mirror types for Anthropic-native knobs promoted onto ChatParameters
|
|
// ---------------------------------------------------------------------------
|
|
// These live in schemas/ (not provider-specific) so ChatParameters stays
|
|
// import-free of provider packages. The anthropic provider reads them in
|
|
// ToAnthropicChatRequest and maps them to AnthropicMessageRequest fields.
|
|
|
|
// ChatContainerSkill describes one skill attached to a container.
|
|
// Origin: Anthropic container.skills[] (beta skills-2025-10-02).
|
|
type ChatContainerSkill struct {
|
|
SkillID string `json:"skill_id"`
|
|
Type string `json:"type"` // "anthropic" | "custom"
|
|
Version *string `json:"version,omitempty"` // Optional version pin
|
|
}
|
|
|
|
// ChatContainerObject is the object form of ChatContainer.
|
|
// Both fields are optional — ID alone is a bare container reference;
|
|
// adding Skills makes it beta-gated.
|
|
type ChatContainerObject struct {
|
|
ID *string `json:"id,omitempty"`
|
|
Skills []ChatContainerSkill `json:"skills,omitempty"`
|
|
}
|
|
|
|
// ChatContainer is the union "container" field on a chat request.
|
|
// Anthropic's API accepts either a plain string (container id) or an object
|
|
// with id + skills[]. Mirrors AnthropicContainer in the provider package.
|
|
type ChatContainer struct {
|
|
ContainerStr *string
|
|
ContainerObject *ChatContainerObject
|
|
}
|
|
|
|
// MarshalJSON emits the raw string or the object form directly.
|
|
func (c ChatContainer) MarshalJSON() ([]byte, error) {
|
|
if c.ContainerStr != nil && c.ContainerObject != nil {
|
|
return nil, fmt.Errorf("both ContainerStr and ContainerObject are set; only one should be non-nil")
|
|
}
|
|
if c.ContainerStr != nil {
|
|
return MarshalSorted(*c.ContainerStr)
|
|
}
|
|
if c.ContainerObject != nil {
|
|
return MarshalSorted(c.ContainerObject)
|
|
}
|
|
return MarshalSorted(nil)
|
|
}
|
|
|
|
// UnmarshalJSON accepts either a plain string or the object form.
|
|
// Uses the build-tag-aware package-level Unmarshal (sonic on native, stdlib
|
|
// json on wasm/tinygo) and clears the inactive union arm on each success so
|
|
// repeated decodes into the same value don't leave both arms populated.
|
|
// JSON null clears both arms. Follows the ChatToolChoice.UnmarshalJSON pattern.
|
|
func (c *ChatContainer) UnmarshalJSON(data []byte) error {
|
|
trimmed := bytes.TrimSpace(data)
|
|
if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
|
|
c.ContainerStr = nil
|
|
c.ContainerObject = nil
|
|
return nil
|
|
}
|
|
|
|
var s string
|
|
if err := Unmarshal(data, &s); err == nil {
|
|
c.ContainerStr = &s
|
|
c.ContainerObject = nil
|
|
return nil
|
|
}
|
|
var obj ChatContainerObject
|
|
if err := Unmarshal(data, &obj); err == nil {
|
|
c.ContainerStr = nil
|
|
c.ContainerObject = &obj
|
|
return nil
|
|
}
|
|
return fmt.Errorf("container field is neither a string nor an object")
|
|
}
|
|
|
|
// ChatTaskBudget advises the model of a full-loop token budget.
|
|
// Origin: Anthropic output_config.task_budget (beta task-budgets-2026-03-13).
|
|
type ChatTaskBudget struct {
|
|
Type string `json:"type"` // Always "tokens"
|
|
Total int `json:"total"` // Total advisory budget
|
|
Remaining *int `json:"remaining,omitempty"` // Optional client-side counter
|
|
}
|
|
|
|
// ChatToolInputExample is one example input for a tool, shown to the model.
|
|
// Origin: Anthropic tool.input_examples (beta tool-examples-2025-10-29).
|
|
type ChatToolInputExample struct {
|
|
Input json.RawMessage `json:"input"`
|
|
Description *string `json:"description,omitempty"`
|
|
}
|
|
|
|
// ChatMCPServer is an MCP server definition attached to a chat request.
|
|
// Origin: Anthropic mcp_servers[] (mcp-client-2025-11-20 format).
|
|
type ChatMCPServer struct {
|
|
Type string `json:"type"` // "url"
|
|
URL string `json:"url"`
|
|
Name string `json:"name"`
|
|
AuthorizationToken *string `json:"authorization_token,omitempty"`
|
|
}
|
|
|
|
// ChatInputImage represents image data in a message.
|
|
type ChatInputImage struct {
|
|
URL string `json:"url"`
|
|
Detail *string `json:"detail,omitempty"`
|
|
}
|
|
|
|
// ChatInputAudio represents audio data in a message.
|
|
// Data carries the audio payload as a string (e.g., data URL or provider-accepted encoded content).
|
|
// Format is optional (e.g., "wav", "mp3"); when nil, providers may attempt auto-detection.
|
|
type ChatInputAudio struct {
|
|
Data string `json:"data"`
|
|
Format *string `json:"format,omitempty"`
|
|
}
|
|
|
|
// ChatInputFile represents a file in a message.
|
|
type ChatInputFile struct {
|
|
FileData *string `json:"file_data,omitempty"` // Base64 encoded file data
|
|
FileURL *string `json:"file_url,omitempty"` // Direct URL to file
|
|
FileID *string `json:"file_id,omitempty"` // Reference to uploaded file
|
|
Filename *string `json:"filename,omitempty"` // Name of the file
|
|
FileType *string `json:"file_type,omitempty"` // Type of the file
|
|
}
|
|
|
|
// ChatToolMessage represents a tool message in a chat conversation.
|
|
type ChatToolMessage struct {
|
|
ToolCallID *string `json:"tool_call_id,omitempty"`
|
|
}
|
|
|
|
// ChatAssistantMessage represents a message in a chat conversation.
|
|
type ChatAssistantMessage struct {
|
|
Refusal *string `json:"refusal,omitempty"`
|
|
Audio *ChatAudioMessageAudio `json:"audio,omitempty"`
|
|
Reasoning *string `json:"reasoning,omitempty"`
|
|
ReasoningDetails []ChatReasoningDetails `json:"reasoning_details,omitempty"`
|
|
Annotations []ChatAssistantMessageAnnotation `json:"annotations,omitempty"`
|
|
ToolCalls []ChatAssistantMessageToolCall `json:"tool_calls,omitempty"`
|
|
}
|
|
|
|
// UnmarshalJSON implements custom unmarshalling for ChatAssistantMessage.
|
|
// If Reasoning is non-nil and ReasoningDetails is nil/empty, it adds a single
|
|
// ChatReasoningDetails entry of type "reasoning.text" with the text set to Reasoning.
|
|
func (cm *ChatAssistantMessage) UnmarshalJSON(data []byte) error {
|
|
if cm == nil {
|
|
return nil
|
|
}
|
|
|
|
// Alias to avoid infinite recursion
|
|
type Alias ChatAssistantMessage
|
|
|
|
// Auxiliary struct to capture xAI's reasoning_content field
|
|
var aux struct {
|
|
Alias
|
|
ReasoningContent *string `json:"reasoning_content,omitempty"` // xAI uses this field name
|
|
}
|
|
|
|
if err := Unmarshal(data, &aux); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Copy decoded data back into the original type
|
|
*cm = ChatAssistantMessage(aux.Alias)
|
|
|
|
// Map xAI's reasoning_content to Bifrost's Reasoning field
|
|
// This allows both OpenAI's "reasoning" and xAI's "reasoning_content" to work
|
|
if aux.ReasoningContent != nil && cm.Reasoning == nil {
|
|
cm.Reasoning = aux.ReasoningContent
|
|
}
|
|
|
|
// If Reasoning is present and there are no reasoning_details,
|
|
// synthesize a text reasoning_details entry.
|
|
if cm.Reasoning != nil && len(cm.ReasoningDetails) == 0 {
|
|
text := *cm.Reasoning
|
|
cm.ReasoningDetails = []ChatReasoningDetails{
|
|
{
|
|
Index: 0,
|
|
Type: BifrostReasoningDetailsTypeText,
|
|
Text: &text,
|
|
},
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ChatAssistantMessageAnnotation represents an annotation in a response.
|
|
type ChatAssistantMessageAnnotation struct {
|
|
Type string `json:"type"`
|
|
URLCitation ChatAssistantMessageAnnotationCitation `json:"url_citation"`
|
|
}
|
|
|
|
// ChatAssistantMessageAnnotationCitation represents a citation in a response.
|
|
type ChatAssistantMessageAnnotationCitation struct {
|
|
StartIndex int `json:"start_index"`
|
|
EndIndex int `json:"end_index"`
|
|
Title string `json:"title"`
|
|
URL *string `json:"url,omitempty"`
|
|
Sources *interface{} `json:"sources,omitempty"`
|
|
Type *string `json:"type,omitempty"`
|
|
}
|
|
|
|
// ChatAssistantMessageToolCall represents a tool call in a message
|
|
type ChatAssistantMessageToolCall struct {
|
|
Index uint16 `json:"index"`
|
|
Type *string `json:"type,omitempty"`
|
|
ID *string `json:"id,omitempty"`
|
|
Function ChatAssistantMessageToolCallFunction `json:"function"`
|
|
}
|
|
|
|
// ChatAssistantMessageToolCallFunction represents a call to a function.
|
|
type ChatAssistantMessageToolCallFunction struct {
|
|
Name *string `json:"name"`
|
|
Arguments string `json:"arguments"` // stringified json as retured by OpenAI, might not be a valid JSON always
|
|
}
|
|
|
|
// ChatAudioMessageAudio represents audio data in a message.
|
|
type ChatAudioMessageAudio struct {
|
|
ID string `json:"id"`
|
|
Data string `json:"data"`
|
|
ExpiresAt int `json:"expires_at"`
|
|
Transcript string `json:"transcript"`
|
|
}
|
|
|
|
// BifrostResponseChoice represents a choice in the completion result.
|
|
// This struct can represent either a streaming or non-streaming response choice.
|
|
// IMPORTANT: Only one of TextCompletionResponseChoice, NonStreamResponseChoice or StreamResponseChoice
|
|
// should be non-nil at a time.
|
|
type BifrostResponseChoice struct {
|
|
Index int `json:"index"`
|
|
FinishReason *string `json:"finish_reason,omitempty"`
|
|
LogProbs *BifrostLogProbs `json:"logprobs,omitempty"`
|
|
|
|
*TextCompletionResponseChoice
|
|
*ChatNonStreamResponseChoice
|
|
*ChatStreamResponseChoice
|
|
}
|
|
|
|
// BifrostFinishReason represents the reason why the model stopped generating.
|
|
type BifrostFinishReason string
|
|
|
|
// BifrostFinishReason values
|
|
const (
|
|
BifrostFinishReasonStop BifrostFinishReason = "stop"
|
|
BifrostFinishReasonLength BifrostFinishReason = "length"
|
|
BifrostFinishReasonToolCalls BifrostFinishReason = "tool_calls"
|
|
)
|
|
|
|
type BifrostReasoningDetailsType string
|
|
|
|
const (
|
|
BifrostReasoningDetailsTypeSummary BifrostReasoningDetailsType = "reasoning.summary"
|
|
BifrostReasoningDetailsTypeEncrypted BifrostReasoningDetailsType = "reasoning.encrypted"
|
|
BifrostReasoningDetailsTypeText BifrostReasoningDetailsType = "reasoning.text"
|
|
BifrostReasoningDetailsTypeContentBlocks BifrostReasoningDetailsType = "reasoning.content_blocks"
|
|
)
|
|
|
|
// Not in OpenAI's spec, but needed to support inter provider reasoning capabilities.
|
|
type ChatReasoningDetails struct {
|
|
ID *string `json:"id,omitempty"`
|
|
Index int `json:"index"`
|
|
Type BifrostReasoningDetailsType `json:"type"`
|
|
Summary *string `json:"summary,omitempty"`
|
|
Text *string `json:"text,omitempty"`
|
|
Signature *string `json:"signature,omitempty"`
|
|
Data *string `json:"data,omitempty"` // for encrypted data
|
|
}
|
|
|
|
// BifrostLogProbs represents the log probabilities for different aspects of a response.
|
|
type BifrostLogProbs struct {
|
|
Content []ContentLogProb `json:"content,omitempty"`
|
|
Refusal []LogProb `json:"refusal,omitempty"`
|
|
|
|
*TextCompletionLogProb
|
|
}
|
|
|
|
type TextCompletionResponseChoice struct {
|
|
Text *string `json:"text,omitempty"`
|
|
}
|
|
|
|
// ChatNonStreamResponseChoice represents a choice in the non-stream response
|
|
type ChatNonStreamResponseChoice struct {
|
|
Message *ChatMessage `json:"message"`
|
|
StopString *string `json:"stop,omitempty"`
|
|
}
|
|
|
|
// ChatStreamResponseChoice represents a choice in the stream response
|
|
type ChatStreamResponseChoice struct {
|
|
Delta *ChatStreamResponseChoiceDelta `json:"delta,omitempty"` // Partial message info
|
|
}
|
|
|
|
// ChatStreamResponseChoiceDelta represents a delta in the stream response
|
|
type ChatStreamResponseChoiceDelta struct {
|
|
Role *string `json:"role,omitempty"` // Only in the first chunk
|
|
Content *string `json:"content,omitempty"` // May be empty string or null
|
|
Refusal *string `json:"refusal,omitempty"` // Refusal content if any
|
|
Audio *ChatAudioMessageAudio `json:"audio,omitempty"` // Audio data if any
|
|
Reasoning *string `json:"reasoning,omitempty"` // May be empty string or null
|
|
ReasoningDetails []ChatReasoningDetails `json:"reasoning_details,omitempty"`
|
|
ToolCalls []ChatAssistantMessageToolCall `json:"tool_calls,omitempty"` // If tool calls used (supports incremental updates)
|
|
}
|
|
|
|
// UnmarshalJSON implements custom unmarshalling for ChatStreamResponseChoiceDelta.
|
|
// If Reasoning is non-nil and ReasoningDetails is nil/empty, it adds a single
|
|
// ChatReasoningDetails entry of type "reasoning.text" with the text set to Reasoning.
|
|
func (d *ChatStreamResponseChoiceDelta) UnmarshalJSON(data []byte) error {
|
|
// Alias to avoid infinite recursion
|
|
type Alias ChatStreamResponseChoiceDelta
|
|
|
|
// Auxiliary struct to capture xAI's reasoning_content field
|
|
var aux struct {
|
|
Alias
|
|
ReasoningContent *string `json:"reasoning_content,omitempty"` // xAI uses this field name
|
|
}
|
|
|
|
if err := Unmarshal(data, &aux); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Copy decoded data back into the original type
|
|
*d = ChatStreamResponseChoiceDelta(aux.Alias)
|
|
|
|
// Map xAI's reasoning_content to Bifrost's Reasoning field
|
|
// This allows both OpenAI's "reasoning" and xAI's "reasoning_content" to work
|
|
if aux.ReasoningContent != nil && d.Reasoning == nil {
|
|
d.Reasoning = aux.ReasoningContent
|
|
}
|
|
|
|
// If Reasoning is present and there are no reasoning_details,
|
|
// synthesize a text reasoning_details entry.
|
|
if d.Reasoning != nil && len(d.ReasoningDetails) == 0 {
|
|
text := *d.Reasoning
|
|
d.ReasoningDetails = []ChatReasoningDetails{
|
|
{
|
|
Index: 0,
|
|
Type: BifrostReasoningDetailsTypeText,
|
|
Text: &text,
|
|
},
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LogProb represents the log probability of a token.
|
|
type LogProb struct {
|
|
Bytes []int `json:"bytes,omitempty"`
|
|
LogProb float64 `json:"logprob"`
|
|
Token string `json:"token"`
|
|
}
|
|
|
|
// ContentLogProb represents log probability information for content.
|
|
type ContentLogProb struct {
|
|
Bytes []int `json:"bytes"`
|
|
LogProb float64 `json:"logprob"`
|
|
Token string `json:"token"`
|
|
TopLogProbs []LogProb `json:"top_logprobs"`
|
|
}
|
|
|
|
// BifrostLLMUsage represents token usage information
|
|
type BifrostLLMUsage struct {
|
|
PromptTokens int `json:"prompt_tokens,omitempty"`
|
|
PromptTokensDetails *ChatPromptTokensDetails `json:"prompt_tokens_details,omitempty"`
|
|
CompletionTokens int `json:"completion_tokens,omitempty"`
|
|
CompletionTokensDetails *ChatCompletionTokensDetails `json:"completion_tokens_details,omitempty"`
|
|
TotalTokens int `json:"total_tokens"`
|
|
Cost *BifrostCost `json:"cost,omitempty"` // Only for the providers which support cost calculation
|
|
}
|
|
|
|
type ChatPromptTokensDetails struct {
|
|
TextTokens int `json:"text_tokens,omitempty"`
|
|
AudioTokens int `json:"audio_tokens,omitempty"`
|
|
ImageTokens int `json:"image_tokens,omitempty"`
|
|
|
|
// For Providers which don't separate between cache creation and cache read tokens (like Openai, Gemini, etc), this is the total number of cached tokens read.
|
|
CachedReadTokens int `json:"cached_read_tokens,omitempty"`
|
|
CachedWriteTokens int `json:"cached_write_tokens,omitempty"`
|
|
}
|
|
|
|
// UnmarshalJSON maps OpenAI's cached_tokens into CachedReadTokens for compatibility.
|
|
func (d *ChatPromptTokensDetails) UnmarshalJSON(data []byte) error {
|
|
var raw struct {
|
|
TextTokens int `json:"text_tokens"`
|
|
AudioTokens int `json:"audio_tokens"`
|
|
ImageTokens int `json:"image_tokens"`
|
|
CachedReadTokens int `json:"cached_read_tokens"`
|
|
CachedWriteTokens int `json:"cached_write_tokens"`
|
|
CachedTokens *int `json:"cached_tokens"`
|
|
}
|
|
if err := Unmarshal(data, &raw); err != nil {
|
|
return err
|
|
}
|
|
d.TextTokens = raw.TextTokens
|
|
d.AudioTokens = raw.AudioTokens
|
|
d.ImageTokens = raw.ImageTokens
|
|
d.CachedReadTokens = raw.CachedReadTokens
|
|
d.CachedWriteTokens = raw.CachedWriteTokens
|
|
// OpenAI spec providers send just cached_tokens, not separate read and write tokens and we handle them as read tokens in pricing calculations.
|
|
if raw.CachedTokens != nil && raw.CachedReadTokens == 0 && raw.CachedWriteTokens == 0 {
|
|
d.CachedReadTokens = *raw.CachedTokens
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MarshalJSON emits cached_tokens (read+write) alongside the individual fields for OpenAI spec compatibility.
|
|
func (d ChatPromptTokensDetails) MarshalJSON() ([]byte, error) {
|
|
type raw struct {
|
|
TextTokens int `json:"text_tokens,omitempty"`
|
|
AudioTokens int `json:"audio_tokens,omitempty"`
|
|
ImageTokens int `json:"image_tokens,omitempty"`
|
|
CachedReadTokens int `json:"cached_read_tokens,omitempty"`
|
|
CachedWriteTokens int `json:"cached_write_tokens,omitempty"`
|
|
CachedTokens int `json:"cached_tokens"`
|
|
}
|
|
return MarshalSorted(raw{
|
|
TextTokens: d.TextTokens,
|
|
AudioTokens: d.AudioTokens,
|
|
ImageTokens: d.ImageTokens,
|
|
CachedReadTokens: d.CachedReadTokens,
|
|
CachedWriteTokens: d.CachedWriteTokens,
|
|
CachedTokens: d.CachedReadTokens + d.CachedWriteTokens,
|
|
})
|
|
}
|
|
|
|
type ChatCompletionTokensDetails struct {
|
|
TextTokens int `json:"text_tokens,omitempty"`
|
|
AcceptedPredictionTokens int `json:"accepted_prediction_tokens,omitempty"`
|
|
AudioTokens int `json:"audio_tokens,omitempty"`
|
|
CitationTokens *int `json:"citation_tokens,omitempty"`
|
|
NumSearchQueries *int `json:"num_search_queries,omitempty"`
|
|
ReasoningTokens int `json:"reasoning_tokens,omitempty"`
|
|
ImageTokens *int `json:"image_tokens,omitempty"`
|
|
RejectedPredictionTokens int `json:"rejected_prediction_tokens,omitempty"`
|
|
}
|
|
|
|
type BifrostCost struct {
|
|
InputTokensCost float64 `json:"input_tokens_cost,omitempty"`
|
|
OutputTokensCost float64 `json:"output_tokens_cost,omitempty"`
|
|
ReasoningTokensCost float64 `json:"reasoning_tokens_cost,omitempty"`
|
|
CitationTokensCost float64 `json:"citation_tokens_cost,omitempty"`
|
|
SearchQueriesCost float64 `json:"search_queries_cost,omitempty"`
|
|
RequestCost float64 `json:"request_cost,omitempty"`
|
|
TotalCost float64 `json:"total_cost,omitempty"`
|
|
}
|
|
|
|
// UnmarshalJSON implements custom JSON unmarshalling for BifrostCost.
|
|
func (bc *BifrostCost) UnmarshalJSON(data []byte) error {
|
|
// First, try to unmarshal as a direct float
|
|
var costFloat float64
|
|
if err := Unmarshal(data, &costFloat); err == nil {
|
|
bc.TotalCost = costFloat
|
|
return nil
|
|
}
|
|
|
|
// Try to unmarshal as a full BifrostCost struct
|
|
// Use a type alias to avoid infinite recursion
|
|
type Alias BifrostCost
|
|
var costStruct Alias
|
|
if err := Unmarshal(data, &costStruct); err == nil {
|
|
*bc = BifrostCost(costStruct)
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("cost field is neither a float nor an object")
|
|
}
|
|
|
|
type SearchResult struct {
|
|
Title string `json:"title"`
|
|
URL string `json:"url"`
|
|
Date *string `json:"date,omitempty"`
|
|
LastUpdated *string `json:"last_updated,omitempty"`
|
|
Snippet *string `json:"snippet,omitempty"`
|
|
Source *string `json:"source,omitempty"`
|
|
}
|
|
|
|
type VideoResult struct {
|
|
URL string `json:"url"`
|
|
ThumbnailURL *string `json:"thumbnail_url,omitempty"`
|
|
ThumbnailWidth *int `json:"thumbnail_width,omitempty"`
|
|
ThumbnailHeight *int `json:"thumbnail_height,omitempty"`
|
|
Duration *float64 `json:"duration,omitempty"`
|
|
}
|