ValidateConfig validates a thinking configuration against model capabilities. This function performs comprehensive validation: - Checks if the model supports thinking - Auto-converts between Budget and Level formats based on model capability - Validates that requested level is in the model's suppor
(config ThinkingConfig, modelInfo *registry.ModelInfo, fromFormat, toFormat string, fromSuffix bool)
| 36 | // - Level-only model + Budget config → Budget converted to Level |
| 37 | // - Hybrid model → preserve original format |
| 38 | func ValidateConfig(config ThinkingConfig, modelInfo *registry.ModelInfo, fromFormat, toFormat string, fromSuffix bool) (*ThinkingConfig, error) { |
| 39 | fromFormat, toFormat = strings.ToLower(strings.TrimSpace(fromFormat)), strings.ToLower(strings.TrimSpace(toFormat)) |
| 40 | model := "unknown" |
| 41 | support := (*registry.ThinkingSupport)(nil) |
| 42 | if modelInfo != nil { |
| 43 | if modelInfo.ID != "" { |
| 44 | model = modelInfo.ID |
| 45 | } |
| 46 | support = modelInfo.Thinking |
| 47 | } |
| 48 | |
| 49 | if support == nil { |
| 50 | if config.Mode != ModeNone { |
| 51 | return nil, NewThinkingErrorWithModel(ErrThinkingNotSupported, "thinking not supported for this model", model) |
| 52 | } |
| 53 | return &config, nil |
| 54 | } |
| 55 | |
| 56 | // allowClampUnsupported determines whether to clamp unsupported levels instead of returning an error. |
| 57 | // This applies when crossing provider families (e.g., openai→gemini, claude→gemini) and the target |
| 58 | // model supports discrete levels. Same-family conversions require strict validation. |
| 59 | toCapability := detectModelCapability(modelInfo) |
| 60 | toHasLevelSupport := toCapability == CapabilityLevelOnly || toCapability == CapabilityHybrid |
| 61 | allowClampUnsupported := toHasLevelSupport && !isSameProviderFamily(fromFormat, toFormat) |
| 62 | |
| 63 | // strictBudget determines whether to enforce strict budget range validation. |
| 64 | // This applies when: (1) config comes from request body (not suffix), (2) source format is known, |
| 65 | // and (3) source and target are in the same provider family. Cross-family or suffix-based configs |
| 66 | // are clamped instead of rejected to improve interoperability. |
| 67 | strictBudget := !fromSuffix && fromFormat != "" && isSameProviderFamily(fromFormat, toFormat) |
| 68 | budgetDerivedFromLevel := false |
| 69 | |
| 70 | capability := detectModelCapability(modelInfo) |
| 71 | switch capability { |
| 72 | case CapabilityBudgetOnly: |
| 73 | if config.Mode == ModeLevel { |
| 74 | if config.Level == LevelAuto { |
| 75 | break |
| 76 | } |
| 77 | budget, ok := ConvertLevelToBudget(string(config.Level)) |
| 78 | if !ok { |
| 79 | return nil, NewThinkingError(ErrUnknownLevel, fmt.Sprintf("unknown level: %s", config.Level)) |
| 80 | } |
| 81 | config.Mode = ModeBudget |
| 82 | config.Budget = budget |
| 83 | config.Level = "" |
| 84 | budgetDerivedFromLevel = true |
| 85 | } |
| 86 | case CapabilityLevelOnly: |
| 87 | if config.Mode == ModeBudget { |
| 88 | level, ok := ConvertBudgetToLevel(config.Budget) |
| 89 | if !ok { |
| 90 | return nil, NewThinkingError(ErrUnknownLevel, fmt.Sprintf("budget %d cannot be converted to a valid level", config.Budget)) |
| 91 | } |
| 92 | // When converting Budget -> Level for level-only models, clamp the derived standard level |
| 93 | // to the nearest supported level. Special values (none/auto) are preserved. |
| 94 | config.Mode = ModeLevel |
| 95 | config.Level = clampLevel(ThinkingLevel(level), modelInfo, toFormat) |
no test coverage detected