| 1 | package config |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | |
| 6 | "github.com/BurntSushi/toml" |
| 7 | |
| 8 | fileencoding "reasonix/internal/fileutil/encoding" |
| 9 | ) |
| 10 | |
| 11 | func decodeTOMLFile(path string, v any) (toml.MetaData, error) { |
| 12 | resolved, err := resolveConfigReadPath(path) |
| 13 | if err != nil { |
| 14 | return toml.MetaData{}, err |
| 15 | } |
| 16 | return decodeTOMLFileResolved(resolved, v) |
| 17 | } |
| 18 | |
| 19 | func decodeTOMLFileResolved(path string, v any) (toml.MetaData, error) { |
| 20 | data, err := fileencoding.ReadFileUTF8(path) |
| 21 | if err != nil { |
| 22 | return toml.MetaData{}, err |
| 23 | } |
| 24 | return decodeTOMLBytes(data, v) |
| 25 | } |
| 26 | |
| 27 | func decodeTOMLBytes(data []byte, v any) (toml.MetaData, error) { |
| 28 | text := string(fileencoding.DecodeToUTF8(data)) |
| 29 | meta, err := toml.Decode(text, v) |
| 30 | if err != nil { |
| 31 | return meta, err |
| 32 | } |
| 33 | if cfg, ok := v.(*Config); ok { |
| 34 | if err := isolateDecodedProviders(text, meta, cfg); err != nil { |
| 35 | return meta, err |
| 36 | } |
| 37 | } |
| 38 | return meta, nil |
| 39 | } |
| 40 | |
| 41 | // isolateDecodedProviders stops one vendor's defaults from being written onto |
| 42 | // another vendor's provider entry. |
| 43 | // |
| 44 | // TOML array-of-tables decoding is positional: BurntSushi/toml unifies |
| 45 | // [[providers]][i] onto whatever the destination slice already holds at index i |
| 46 | // instead of onto a zero value. Every config load seeds from Default(), which |
| 47 | // ships two official DeepSeek entries, so a user's first two [[providers]] |
| 48 | // inherit every DeepSeek field they did not set themselves — balance_url, |
| 49 | // the USD price table and context_window = 1000000 (#7357, #7358). |
| 50 | // |
| 51 | // Re-decoding the same bytes onto an empty slice makes a declared provider list |
| 52 | // carry only what the file declares. Defaults that are genuinely correct for an |
| 53 | // entry are then reapplied by the explicit backfill helpers, which key on the |
| 54 | // endpoint rather than on list position. |
| 55 | func isolateDecodedProviders(text string, meta toml.MetaData, cfg *Config) error { |
| 56 | if cfg == nil || len(cfg.Providers) == 0 || !meta.IsDefined("providers") { |
| 57 | return nil |
| 58 | } |
| 59 | var isolated struct { |
| 60 | Providers []ProviderEntry `toml:"providers"` |
| 61 | } |
| 62 | if _, err := toml.Decode(text, &isolated); err != nil { |
| 63 | return fmt.Errorf("decode providers: %w", err) |
| 64 | } |
| 65 | cfg.Providers = isolated.Providers |
| 66 | return nil |
| 67 | } |
| 68 |