@@ -1124,9 +1124,15 @@ type toolCallBuffer struct {
11241124 arguments strings.Builder
11251125}
11261126
1127+ // thinkingBlockedBySchema reports whether extended thinking must be dropped
1128+ // for this request: Anthropic rejects thinking combined with structured output.
1129+ func (b * LLM ) thinkingBlockedBySchema (cfg llm.LanguageModelConfig ) bool {
1130+ return b .provider == schemas .Anthropic && cfg .JSONOutputFormat != nil
1131+ }
1132+
11271133// buildChatReasoning creates a ChatReasoning configuration if reasoning is enabled.
11281134func (b * LLM ) buildChatReasoning (cfg llm.LanguageModelConfig ) * schemas.ChatReasoning {
1129- if ! b .reasoningEnabled || cfg .ReasoningDisabled {
1135+ if ! b .reasoningEnabled || cfg .ReasoningDisabled || b . thinkingBlockedBySchema ( cfg ) {
11301136 return nil
11311137 }
11321138
@@ -1171,7 +1177,7 @@ func (b *LLM) calculateThinkingBudget(maxGeneratedTokens int) int {
11711177
11721178// convertToBifrostRequest converts our CompletionRequest to Bifrost's format.
11731179func (b * LLM ) convertToBifrostRequest (request llm.CompletionRequest , cfg llm.LanguageModelConfig ) * schemas.BifrostChatRequest {
1174- messages := b .convertMessages (request .Posts )
1180+ messages := b .convertMessages (request .Posts , cfg )
11751181 tools := b .convertTools (request , cfg )
11761182
11771183 req := & schemas.BifrostChatRequest {
@@ -1198,6 +1204,9 @@ func (b *LLM) convertToBifrostRequest(request llm.CompletionRequest, cfg llm.Lan
11981204 if cfg .JSONOutputFormat != nil {
11991205 params .ResponseFormat = buildChatResponseFormat (cfg .JSONOutputFormat )
12001206 }
1207+ if b .promptCachingEnabled () {
1208+ params .CacheControl = & schemas.CacheControl {Type : schemas .CacheControlTypeEphemeral }
1209+ }
12011210 req .Params = params
12021211
12031212 // Attach fallback chain so Bifrost retries with alternative providers on failure.
@@ -1207,7 +1216,7 @@ func (b *LLM) convertToBifrostRequest(request llm.CompletionRequest, cfg llm.Lan
12071216}
12081217
12091218// convertMessages converts llm.Post messages to Bifrost ChatMessage format.
1210- func (b * LLM ) convertMessages (posts []llm.Post ) []schemas.ChatMessage {
1219+ func (b * LLM ) convertMessages (posts []llm.Post , cfg llm. LanguageModelConfig ) []schemas.ChatMessage {
12111220 messages := make ([]schemas.ChatMessage , 0 , len (posts ))
12121221
12131222 for _ , post := range posts {
@@ -1255,7 +1264,11 @@ func (b *LLM) convertMessages(posts []llm.Post) []schemas.ChatMessage {
12551264 // signature arrived, we persist partial reasoning for display only; do
12561265 // not replay it to Anthropic as an unsigned thinking block. Other
12571266 // providers may accept unsigned reasoning, so preserve it for them.
1258- if post .Reasoning != "" && (b .provider != schemas .Anthropic || post .ReasoningSignature != "" ) {
1267+ // Also skip replay when thinking is disabled for this request:
1268+ // Anthropic rejects input thinking blocks when thinking is off.
1269+ if post .Reasoning != "" &&
1270+ (b .provider != schemas .Anthropic || post .ReasoningSignature != "" ) &&
1271+ ! b .thinkingBlockedBySchema (cfg ) {
12591272 if msg .ChatAssistantMessage == nil {
12601273 msg .ChatAssistantMessage = & schemas.ChatAssistantMessage {}
12611274 }
@@ -1590,6 +1603,15 @@ func (b *LLM) shouldUseResponsesAPI(cfg llm.LanguageModelConfig) bool {
15901603 if b .useResponsesAPI {
15911604 return true
15921605 }
1606+
1607+ // Direct OpenAI always sets useResponsesAPI during service construction.
1608+ // A false value for OpenAI-base or Azure providers therefore represents an
1609+ // explicit operator choice to use Chat Completions and must not be overridden
1610+ // by native tool configuration.
1611+ if b .provider == schemas .OpenAI || b .provider == schemas .Azure {
1612+ return false
1613+ }
1614+
15931615 if b .providerSupportsNativeTools () && len (b .enabledNativeTools ) > 0 {
15941616 return true
15951617 }
@@ -1599,6 +1621,27 @@ func (b *LLM) shouldUseResponsesAPI(cfg llm.LanguageModelConfig) bool {
15991621 return false
16001622}
16011623
1624+ // promptCachingEnabled reports whether to request Anthropic automatic prompt
1625+ // caching (top-level cache_control). Anthropic caches nothing unless asked,
1626+ // so without this every turn re-bills the full system prompt, tool schemas,
1627+ // and history at the base input rate. OpenAI-family and Gemini cache prompt
1628+ // prefixes automatically and need no marker. Bifrost forwards the field
1629+ // unstripped to non-Anthropic providers, so it is only attached when the
1630+ // primary and every fallback are Anthropic; a mixed chain would 400 the
1631+ // fallback request.
1632+ func (b * LLM ) promptCachingEnabled () bool {
1633+ if b .provider != schemas .Anthropic {
1634+ return false
1635+ }
1636+ for _ , fb := range b .fallbacks {
1637+ if fb .Provider != schemas .Anthropic &&
1638+ ! strings .HasPrefix (string (fb .Provider ), string (schemas .Anthropic )+ "::" ) {
1639+ return false
1640+ }
1641+ }
1642+ return true
1643+ }
1644+
16021645// isNativeToolEnabled checks if a native tool is enabled by name.
16031646func (b * LLM ) isNativeToolEnabled (name string ) bool {
16041647 for _ , t := range b .enabledNativeTools {
@@ -1810,7 +1853,7 @@ func (b *LLM) convertToResponsesTools(request llm.CompletionRequest, cfg llm.Lan
18101853
18111854// buildResponsesReasoning creates a ResponsesParametersReasoning configuration if reasoning is enabled.
18121855func (b * LLM ) buildResponsesReasoning (cfg llm.LanguageModelConfig ) * schemas.ResponsesParametersReasoning {
1813- if ! b .reasoningEnabled || cfg .ReasoningDisabled {
1856+ if ! b .reasoningEnabled || cfg .ReasoningDisabled || b . thinkingBlockedBySchema ( cfg ) {
18141857 return nil
18151858 }
18161859
@@ -1889,6 +1932,13 @@ func (b *LLM) convertToBifrostResponsesRequest(request llm.CompletionRequest, cf
18891932 }
18901933 params .Text = textConfig
18911934 }
1935+ // The Anthropic provider reads cache_control from ExtraParams on the
1936+ // Responses path (there is no typed field on ResponsesParameters).
1937+ if b .promptCachingEnabled () {
1938+ params .ExtraParams = map [string ]interface {}{
1939+ "cache_control" : & schemas.CacheControl {Type : schemas .CacheControlTypeEphemeral },
1940+ }
1941+ }
18921942 req .Params = params
18931943
18941944 // Attach fallback chain so Bifrost retries with alternative providers on failure.
0 commit comments