| 1 | package cli |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "strings" |
| 6 | |
| 7 | "reasonix/internal/config" |
| 8 | "reasonix/internal/extension/providerext" |
| 9 | ) |
| 10 | |
| 11 | // resolveModelForCLI picks the chat model a CLI subcommand should boot on. A |
| 12 | // keyless default falls through to the first configured chat provider, while |
| 13 | // explicit model choices remain strict. |
| 14 | // |
| 15 | // Semantics: |
| 16 | // |
| 17 | // - explicitRef == "": Config.ResolveNewSessionChatModel supplies the shared |
| 18 | // default/fallback policy used by boot and desktop. Non-chat models are |
| 19 | // never selected as a fallback. |
| 20 | // |
| 21 | // - explicitRef != "": the caller asked for this exact ref (--model flag, |
| 22 | // ACP session param, etc.). The ref must resolve AND be configured. |
| 23 | // There is no silent fallback — explicit choices that misfire must |
| 24 | // fail loudly so the user is not quietly rerouted onto a model they |
| 25 | // did not ask for. Plugin-namespaced refs (plugin/<plugin>/<provider>/ |
| 26 | // <model>) pass through unresolved: they belong to extension sidecars, |
| 27 | // not the config catalog, and boot's merged resolver is their only gate. |
| 28 | func resolveModelForCLI(explicitRef string, cfg *config.Config) (ref string, fallback bool, err error) { |
| 29 | explicitRef = strings.TrimSpace(explicitRef) |
| 30 | if explicitRef != "" { |
| 31 | if providerext.PluginRefOwner(explicitRef) != "" { |
| 32 | return explicitRef, false, nil |
| 33 | } |
| 34 | entry, ok := cfg.ResolveModel(explicitRef) |
| 35 | if !ok { |
| 36 | return "", false, fmt.Errorf("unknown model %q", explicitRef) |
| 37 | } |
| 38 | if !entry.Configured() { |
| 39 | return "", false, fmt.Errorf("provider %q requires %s", explicitRef, entry.APIKeyEnv) |
| 40 | } |
| 41 | return entry.Name + "/" + entry.Model, false, nil |
| 42 | } |
| 43 | ref, fallback, _ = cfg.ResolveNewSessionChatModel() |
| 44 | return ref, fallback, nil |
| 45 | } |
| 46 | |
| 47 | // resolveServeModel keeps serve's implicit model scoped to the user config. |
| 48 | // A project reasonix.toml may configure the served workspace, but it must not |
| 49 | // replace the account-level model used for new serve sessions. |
| 50 | func resolveServeModel(modelName string) string { |
| 51 | if strings.TrimSpace(modelName) != "" { |
| 52 | return modelName |
| 53 | } |
| 54 | cfg := config.LoadForEdit(config.UserConfigPath()) |
| 55 | if resolved, _, ok := cfg.ResolveNewSessionChatModel(); ok { |
| 56 | return resolved |
| 57 | } |
| 58 | return modelName |
| 59 | } |
| 60 |