mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 20:40:52 +08:00
202 lines
7.1 KiB
Go
202 lines
7.1 KiB
Go
// Package antigravity implements thinking configuration for Antigravity API format.
|
|
//
|
|
// Antigravity uses request.generationConfig.thinkingConfig.* path (same as gemini-cli)
|
|
// but requires additional normalization for Claude models:
|
|
// - Ensure thinking budget < max_tokens
|
|
// - Remove thinkingConfig if budget < minimum allowed
|
|
package antigravity
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/sjson"
|
|
)
|
|
|
|
// Applier applies thinking configuration for Antigravity API format.
|
|
type Applier struct{}
|
|
|
|
var _ thinking.ProviderApplier = (*Applier)(nil)
|
|
|
|
// NewApplier creates a new Antigravity thinking applier.
|
|
func NewApplier() *Applier {
|
|
return &Applier{}
|
|
}
|
|
|
|
func init() {
|
|
thinking.RegisterProvider("antigravity", NewApplier())
|
|
}
|
|
|
|
// Apply applies thinking configuration to Antigravity request body.
|
|
//
|
|
// For Claude models, additional constraints are applied:
|
|
// - Ensure thinking budget < max_tokens
|
|
// - Remove thinkingConfig if budget < minimum allowed
|
|
func (a *Applier) Apply(body []byte, config thinking.ThinkingConfig, modelInfo *registry.ModelInfo) ([]byte, error) {
|
|
if thinking.IsUserDefinedModel(modelInfo) {
|
|
return a.applyCompatible(body, config, modelInfo)
|
|
}
|
|
if modelInfo.Thinking == nil {
|
|
return body, nil
|
|
}
|
|
|
|
if config.Mode != thinking.ModeBudget && config.Mode != thinking.ModeLevel && config.Mode != thinking.ModeNone && config.Mode != thinking.ModeAuto {
|
|
return body, nil
|
|
}
|
|
|
|
if len(body) == 0 || !gjson.ValidBytes(body) {
|
|
body = []byte(`{}`)
|
|
}
|
|
|
|
isClaude := strings.Contains(strings.ToLower(modelInfo.ID), "claude")
|
|
|
|
// ModeAuto: Always use Budget format with thinkingBudget=-1
|
|
if config.Mode == thinking.ModeAuto {
|
|
return a.applyBudgetFormat(body, config, modelInfo, isClaude)
|
|
}
|
|
if config.Mode == thinking.ModeBudget {
|
|
return a.applyBudgetFormat(body, config, modelInfo, isClaude)
|
|
}
|
|
|
|
// For non-auto modes, choose format based on model capabilities
|
|
support := modelInfo.Thinking
|
|
if len(support.Levels) > 0 {
|
|
return a.applyLevelFormat(body, config)
|
|
}
|
|
return a.applyBudgetFormat(body, config, modelInfo, isClaude)
|
|
}
|
|
|
|
func (a *Applier) applyCompatible(body []byte, config thinking.ThinkingConfig, modelInfo *registry.ModelInfo) ([]byte, error) {
|
|
if config.Mode != thinking.ModeBudget && config.Mode != thinking.ModeLevel && config.Mode != thinking.ModeNone && config.Mode != thinking.ModeAuto {
|
|
return body, nil
|
|
}
|
|
|
|
if len(body) == 0 || !gjson.ValidBytes(body) {
|
|
body = []byte(`{}`)
|
|
}
|
|
|
|
isClaude := false
|
|
if modelInfo != nil {
|
|
isClaude = strings.Contains(strings.ToLower(modelInfo.ID), "claude")
|
|
}
|
|
|
|
if config.Mode == thinking.ModeAuto {
|
|
return a.applyBudgetFormat(body, config, modelInfo, isClaude)
|
|
}
|
|
|
|
if config.Mode == thinking.ModeLevel || (config.Mode == thinking.ModeNone && config.Level != "") {
|
|
return a.applyLevelFormat(body, config)
|
|
}
|
|
|
|
return a.applyBudgetFormat(body, config, modelInfo, isClaude)
|
|
}
|
|
|
|
func (a *Applier) applyLevelFormat(body []byte, config thinking.ThinkingConfig) ([]byte, error) {
|
|
// Remove conflicting field to avoid both thinkingLevel and thinkingBudget in output
|
|
result, _ := sjson.DeleteBytes(body, "request.generationConfig.thinkingConfig.thinkingBudget")
|
|
// Normalize includeThoughts field name to avoid oneof conflicts in upstream JSON parsing.
|
|
result, _ = sjson.DeleteBytes(result, "request.generationConfig.thinkingConfig.include_thoughts")
|
|
|
|
if config.Mode == thinking.ModeNone {
|
|
result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.includeThoughts", false)
|
|
if config.Level != "" {
|
|
result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.thinkingLevel", string(config.Level))
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// Only handle ModeLevel - budget conversion should be done by upper layer
|
|
if config.Mode != thinking.ModeLevel {
|
|
return body, nil
|
|
}
|
|
|
|
level := string(config.Level)
|
|
result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.thinkingLevel", level)
|
|
result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.includeThoughts", true)
|
|
return result, nil
|
|
}
|
|
|
|
func (a *Applier) applyBudgetFormat(body []byte, config thinking.ThinkingConfig, modelInfo *registry.ModelInfo, isClaude bool) ([]byte, error) {
|
|
// Remove conflicting field to avoid both thinkingLevel and thinkingBudget in output
|
|
result, _ := sjson.DeleteBytes(body, "request.generationConfig.thinkingConfig.thinkingLevel")
|
|
// Normalize includeThoughts field name to avoid oneof conflicts in upstream JSON parsing.
|
|
result, _ = sjson.DeleteBytes(result, "request.generationConfig.thinkingConfig.include_thoughts")
|
|
|
|
budget := config.Budget
|
|
includeThoughts := false
|
|
switch config.Mode {
|
|
case thinking.ModeNone:
|
|
includeThoughts = false
|
|
case thinking.ModeAuto:
|
|
includeThoughts = true
|
|
default:
|
|
includeThoughts = budget > 0
|
|
}
|
|
|
|
// Apply Claude-specific constraints
|
|
if isClaude && modelInfo != nil {
|
|
budget, result = a.normalizeClaudeBudget(budget, result, modelInfo)
|
|
// Check if budget was removed entirely
|
|
if budget == -2 {
|
|
return result, nil
|
|
}
|
|
}
|
|
|
|
result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.thinkingBudget", budget)
|
|
result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.includeThoughts", includeThoughts)
|
|
return result, nil
|
|
}
|
|
|
|
// normalizeClaudeBudget applies Claude-specific constraints to thinking budget.
|
|
//
|
|
// It handles:
|
|
// - Ensuring thinking budget < max_tokens
|
|
// - Removing thinkingConfig if budget < minimum allowed
|
|
//
|
|
// Returns the normalized budget and updated payload.
|
|
// Returns budget=-2 as a sentinel indicating thinkingConfig was removed entirely.
|
|
func (a *Applier) normalizeClaudeBudget(budget int, payload []byte, modelInfo *registry.ModelInfo) (int, []byte) {
|
|
if modelInfo == nil {
|
|
return budget, payload
|
|
}
|
|
|
|
// Get effective max tokens
|
|
effectiveMax, setDefaultMax := a.effectiveMaxTokens(payload, modelInfo)
|
|
if effectiveMax > 0 && budget >= effectiveMax {
|
|
budget = effectiveMax - 1
|
|
}
|
|
|
|
// Check minimum budget
|
|
minBudget := 0
|
|
if modelInfo.Thinking != nil {
|
|
minBudget = modelInfo.Thinking.Min
|
|
}
|
|
if minBudget > 0 && budget >= 0 && budget < minBudget {
|
|
// Budget is below minimum, remove thinking config entirely
|
|
payload, _ = sjson.DeleteBytes(payload, "request.generationConfig.thinkingConfig")
|
|
return -2, payload
|
|
}
|
|
|
|
// Set default max tokens if needed
|
|
if setDefaultMax && effectiveMax > 0 {
|
|
payload, _ = sjson.SetBytes(payload, "request.generationConfig.maxOutputTokens", effectiveMax)
|
|
}
|
|
|
|
return budget, payload
|
|
}
|
|
|
|
// effectiveMaxTokens returns the max tokens to cap thinking:
|
|
// prefer request-provided maxOutputTokens; otherwise fall back to model default.
|
|
// The boolean indicates whether the value came from the model default (and thus should be written back).
|
|
func (a *Applier) effectiveMaxTokens(payload []byte, modelInfo *registry.ModelInfo) (max int, fromModel bool) {
|
|
if maxTok := gjson.GetBytes(payload, "request.generationConfig.maxOutputTokens"); maxTok.Exists() && maxTok.Int() > 0 {
|
|
return int(maxTok.Int()), false
|
|
}
|
|
if modelInfo != nil && modelInfo.MaxCompletionTokens > 0 {
|
|
return modelInfo.MaxCompletionTokens, true
|
|
}
|
|
return 0, false
|
|
}
|