178 lines
5.9 KiB
Go
178 lines
5.9 KiB
Go
// Package interactions applies native Interactions thinking configuration.
|
|
package interactions
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/sjson"
|
|
)
|
|
|
|
// Applier implements thinking.ProviderApplier for the native Interactions API.
|
|
type Applier struct{}
|
|
|
|
// NewApplier creates a new Interactions thinking applier.
|
|
func NewApplier() *Applier {
|
|
return &Applier{}
|
|
}
|
|
|
|
func init() {
|
|
thinking.RegisterProvider("interactions", NewApplier())
|
|
}
|
|
|
|
// Apply writes thinking configuration using native Interactions generation_config fields.
|
|
func (a *Applier) Apply(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(`{}`)
|
|
}
|
|
|
|
result := stripInteractionsThinkingFields(body)
|
|
switch config.Mode {
|
|
case thinking.ModeLevel:
|
|
return applyInteractionsLevel(result, body, string(config.Level), modelInfo), nil
|
|
case thinking.ModeBudget:
|
|
return applyInteractionsBudget(result, body, config.Budget, modelInfo), nil
|
|
case thinking.ModeAuto:
|
|
return setInteractionsThinkingSummaries(result, body), nil
|
|
case thinking.ModeNone:
|
|
return applyInteractionsNone(result, body, config, modelInfo), nil
|
|
default:
|
|
return body, nil
|
|
}
|
|
}
|
|
|
|
func applyInteractionsBudget(result, original []byte, budget int, modelInfo *registry.ModelInfo) []byte {
|
|
level, ok := thinking.ConvertBudgetToLevel(budget)
|
|
if !ok {
|
|
return setInteractionsThinkingSummaries(result, original)
|
|
}
|
|
switch level {
|
|
case string(thinking.LevelNone), string(thinking.LevelAuto):
|
|
// Thinking amount and summary visibility are independent. Interactions has
|
|
// no wire-level "none" thinking level, so preserve only explicit summary
|
|
// intent and otherwise let the target model use its documented default.
|
|
return setInteractionsThinkingSummaries(result, original)
|
|
default:
|
|
return applyInteractionsLevel(result, original, level, modelInfo)
|
|
}
|
|
}
|
|
|
|
func applyInteractionsLevel(result, original []byte, level string, modelInfo *registry.ModelInfo) []byte {
|
|
level = normalizeInteractionsLevel(level, modelInfo)
|
|
if level != "" {
|
|
result, _ = sjson.SetBytes(result, "generation_config.thinking_level", level)
|
|
}
|
|
return setInteractionsThinkingSummaries(result, original)
|
|
}
|
|
|
|
func applyInteractionsNone(result, original []byte, config thinking.ThinkingConfig, modelInfo *registry.ModelInfo) []byte {
|
|
if config.Level != "" {
|
|
return applyInteractionsLevel(result, original, string(config.Level), modelInfo)
|
|
}
|
|
if config.Budget > 0 {
|
|
return applyInteractionsBudget(result, original, config.Budget, modelInfo)
|
|
}
|
|
// With the amount fully disabled, visibility is irrelevant. Restoring
|
|
// thinking_summaries alone could make a default-on model reason and return a
|
|
// summary despite the explicit none override.
|
|
return result
|
|
}
|
|
|
|
func stripInteractionsThinkingFields(body []byte) []byte {
|
|
result := body
|
|
for _, path := range []string{
|
|
"generation_config.thinking_level",
|
|
"generation_config.thinkingLevel",
|
|
"generation_config.thinking_budget",
|
|
"generation_config.thinkingBudget",
|
|
"generation_config.thinking_summaries",
|
|
"generation_config.thinkingSummaries",
|
|
"generation_config.thinking_config",
|
|
"generation_config.thinkingConfig",
|
|
"generationConfig.thinkingLevel",
|
|
"generationConfig.thinking_level",
|
|
"generationConfig.thinkingBudget",
|
|
"generationConfig.thinking_budget",
|
|
"generationConfig.thinkingSummaries",
|
|
"generationConfig.thinking_summaries",
|
|
"generationConfig.thinkingConfig",
|
|
} {
|
|
result, _ = sjson.DeleteBytes(result, path)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func setInteractionsThinkingSummaries(result, original []byte) []byte {
|
|
if value, okValue := originalInteractionsThinkingSummaries(original); okValue {
|
|
result, _ = sjson.SetBytes(result, "generation_config.thinking_summaries", value)
|
|
return result
|
|
}
|
|
if includeThoughts, okValue := originalInteractionsIncludeThoughts(original); okValue {
|
|
value := "none"
|
|
if includeThoughts {
|
|
value = "auto"
|
|
}
|
|
result, _ = sjson.SetBytes(result, "generation_config.thinking_summaries", value)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func originalInteractionsThinkingSummaries(body []byte) (string, bool) {
|
|
for _, path := range []string{
|
|
"generation_config.thinking_summaries",
|
|
"generation_config.thinkingSummaries",
|
|
} {
|
|
value := gjson.GetBytes(body, path)
|
|
if value.Type != gjson.String {
|
|
continue
|
|
}
|
|
switch normalized := strings.ToLower(strings.TrimSpace(value.String())); normalized {
|
|
case "auto", "none":
|
|
return normalized, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func originalInteractionsIncludeThoughts(body []byte) (bool, bool) {
|
|
for _, path := range []string{
|
|
"generation_config.thinking_config.include_thoughts",
|
|
"generation_config.thinking_config.includeThoughts",
|
|
"generation_config.thinkingConfig.include_thoughts",
|
|
"generation_config.thinkingConfig.includeThoughts",
|
|
} {
|
|
switch value := gjson.GetBytes(body, path); value.Type {
|
|
case gjson.True:
|
|
return true, true
|
|
case gjson.False:
|
|
return false, true
|
|
}
|
|
}
|
|
return false, false
|
|
}
|
|
|
|
func normalizeInteractionsLevel(level string, modelInfo *registry.ModelInfo) string {
|
|
level = strings.ToLower(strings.TrimSpace(level))
|
|
if level == "" || level == string(thinking.LevelNone) || level == string(thinking.LevelAuto) {
|
|
return ""
|
|
}
|
|
if modelInfo != nil && modelInfo.Thinking != nil && len(modelInfo.Thinking.Levels) > 0 {
|
|
for _, candidate := range modelInfo.Thinking.Levels {
|
|
if strings.EqualFold(candidate, level) {
|
|
return strings.ToLower(candidate)
|
|
}
|
|
}
|
|
return strings.ToLower(modelInfo.Thinking.Levels[len(modelInfo.Thinking.Levels)-1])
|
|
}
|
|
switch level {
|
|
case string(thinking.LevelMax), string(thinking.LevelXHigh):
|
|
return string(thinking.LevelHigh)
|
|
default:
|
|
return level
|
|
}
|
|
}
|