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"` }