| 1 | package config |
| 2 | |
| 3 | import ( |
| 4 | "crypto/sha256" |
| 5 | "encoding/hex" |
| 6 | "encoding/json" |
| 7 | "maps" |
| 8 | "strings" |
| 9 | ) |
| 10 | |
| 11 | func reasoningSnapshotHash(ov ProviderModelOverride) string { |
| 12 | data, _ := json.Marshal(struct { |
| 13 | Protocol string |
| 14 | Efforts []string |
| 15 | Default string |
| 16 | }{ov.ReasoningProtocol, ov.SupportedEfforts, ov.DefaultEffort}) |
| 17 | sum := sha256.Sum256(data) |
| 18 | return hex.EncodeToString(sum[:]) |
| 19 | } |
| 20 | |
| 21 | func reasoningCompatibilitySnapshots(entries []ProviderEntry) []ProviderEntry { |
| 22 | snapshots := make([]ProviderEntry, len(entries)) |
| 23 | for i, entry := range entries { |
| 24 | snapshots[i] = reasoningCompatibilitySnapshot(entry) |
| 25 | } |
| 26 | return snapshots |
| 27 | } |
| 28 | |
| 29 | // explicitModelReasoning peels only untouched generated fields. Legacy writers |
| 30 | // that change a declaration invalidate the digest, preserving the entire edit. |
| 31 | // A writer that drops the optional marker also leaves explicit declarations. |
| 32 | func explicitModelReasoning(ov ProviderModelOverride) ProviderModelOverride { |
| 33 | mask, digest, ok := strings.Cut(ov.ReasoningDefaults, ":") |
| 34 | ov.ReasoningDefaults = "" |
| 35 | if !ok || digest != reasoningSnapshotHash(ov) || strings.Trim(mask, "psd") != "" { |
| 36 | return ov |
| 37 | } |
| 38 | if strings.Contains(mask, "p") { |
| 39 | ov.ReasoningProtocol = "" |
| 40 | } |
| 41 | if strings.Contains(mask, "s") { |
| 42 | ov.SupportedEfforts = nil |
| 43 | } |
| 44 | if strings.Contains(mask, "d") { |
| 45 | ov.DefaultEffort = "" |
| 46 | } |
| 47 | return ov |
| 48 | } |
| 49 | |
| 50 | // reasoningCompatibilitySnapshot keeps older readers functional without making |
| 51 | // generated defaults permanent user overrides in current readers. This copy is |
| 52 | // used only at serialization; runtime and settings retain explicit fields only. |
| 53 | func reasoningCompatibilitySnapshot(entry ProviderEntry) ProviderEntry { |
| 54 | e := entry |
| 55 | stripRuntimeReasoningDefaults(&e) |
| 56 | e.ModelOverrides = maps.Clone(entry.ModelOverrides) |
| 57 | for _, model := range e.ModelList() { |
| 58 | ov := explicitModelReasoning(e.ModelOverrides[model]) |
| 59 | selected := e |
| 60 | selected.Model = model |
| 61 | resolved := ResolveReasoningEntry(&selected) |
| 62 | mask := "" |
| 63 | if ov.ReasoningProtocol == "" && explicitReasoningProtocol(&e) == "" && resolved.ReasoningProtocol != "" { |
| 64 | ov.ReasoningProtocol = resolved.ReasoningProtocol |
| 65 | mask += "p" |
| 66 | } |
| 67 | if len(ov.SupportedEfforts) == 0 && len(e.SupportedEfforts) == 0 && len(resolved.SupportedEfforts) > 0 { |
| 68 | ov.SupportedEfforts = append([]string(nil), resolved.SupportedEfforts...) |
| 69 | mask += "s" |
| 70 | } |
| 71 | if ov.DefaultEffort == "" && e.DefaultEffort == "" && resolved.DefaultEffort != "" { |
| 72 | ov.DefaultEffort = resolved.DefaultEffort |
| 73 | mask += "d" |
| 74 | } |
| 75 | if mask == "" { |
| 76 | continue |
| 77 | } |
| 78 | ov.ReasoningDefaults = mask + ":" + reasoningSnapshotHash(ov) |
| 79 | if e.ModelOverrides == nil { |
| 80 | e.ModelOverrides = map[string]ProviderModelOverride{} |
| 81 | } |
| 82 | e.ModelOverrides[model] = ov |
| 83 | } |
| 84 | return e |
| 85 | } |
| 86 |