| 1 | package acp |
| 2 | |
| 3 | import ( |
| 4 | "strings" |
| 5 | |
| 6 | "reasonix/internal/extension/providerext" |
| 7 | "reasonix/internal/provider" |
| 8 | ) |
| 9 | |
| 10 | // enrichStateWithExtensionModels folds a live controller's merged provider |
| 11 | // catalog into a session config state, so plugin/... models contributed by |
| 12 | // installed extensions are discoverable (and switchable) in BOTH the legacy |
| 13 | // model list and the category:"model" config option — not only when one is |
| 14 | // already the current model. The state is otherwise config-derived: config |
| 15 | // entries keep precedence and extension refs only fill gaps. A nil catalog |
| 16 | // (no extension sidecars, or a controller without one) is a no-op. |
| 17 | func enrichStateWithExtensionModels(state SessionConfigState, catalog []provider.Descriptor) SessionConfigState { |
| 18 | // Collect the extension refs first: when the config backend gave no model |
| 19 | // state at all, extension models still deserve a selector. |
| 20 | type extModel struct{ ref, name string } |
| 21 | var ext []extModel |
| 22 | for _, desc := range catalog { |
| 23 | ref := strings.TrimSpace(desc.Ref) |
| 24 | if ref == "" || providerext.PluginRefOwner(ref) == "" { |
| 25 | continue |
| 26 | } |
| 27 | name := strings.TrimSpace(desc.DisplayName) |
| 28 | if name == "" { |
| 29 | name = ref |
| 30 | } |
| 31 | ext = append(ext, extModel{ref, name}) |
| 32 | } |
| 33 | if len(ext) == 0 { |
| 34 | return state |
| 35 | } |
| 36 | if state.Models == nil { |
| 37 | state.Models = &SessionModelState{CurrentModelID: state.Model} |
| 38 | } |
| 39 | hasModelOption := false |
| 40 | for _, m := range ext { |
| 41 | if !hasModelInfo(state.Models.AvailableModels, m.ref) { |
| 42 | state.Models.AvailableModels = append(state.Models.AvailableModels, ModelInfo{ |
| 43 | ModelID: m.ref, |
| 44 | Name: m.name, |
| 45 | Description: m.name, |
| 46 | }) |
| 47 | } |
| 48 | for i := range state.ConfigOptions { |
| 49 | opt := &state.ConfigOptions[i] |
| 50 | if opt.Category != "model" { |
| 51 | continue |
| 52 | } |
| 53 | hasModelOption = true |
| 54 | if !hasSelectOption(opt.Options, m.ref) { |
| 55 | opt.Options = append(opt.Options, SessionConfigSelectOption{ |
| 56 | Value: m.ref, |
| 57 | Name: m.name, |
| 58 | Description: m.name, |
| 59 | }) |
| 60 | } |
| 61 | } |
| 62 | } |
| 63 | if !hasModelOption { |
| 64 | options := make([]SessionConfigSelectOption, 0, len(ext)) |
| 65 | for _, m := range ext { |
| 66 | options = append(options, SessionConfigSelectOption{Value: m.ref, Name: m.name, Description: m.name}) |
| 67 | } |
| 68 | state.ConfigOptions = append(state.ConfigOptions, SessionConfigOption{ |
| 69 | ID: "model", Name: "Model", Category: "model", Type: "select", |
| 70 | CurrentValue: state.Model, |
| 71 | Options: options, |
| 72 | }) |
| 73 | } |
| 74 | return state |
| 75 | } |
| 76 | |
| 77 | func hasModelInfo(models []ModelInfo, ref string) bool { |
| 78 | for _, m := range models { |
| 79 | if m.ModelID == ref { |
| 80 | return true |
| 81 | } |
| 82 | } |
| 83 | return false |
| 84 | } |
| 85 | |
| 86 | func hasSelectOption(options []SessionConfigSelectOption, ref string) bool { |
| 87 | for _, o := range options { |
| 88 | if o.Value == ref { |
| 89 | return true |
| 90 | } |
| 91 | } |
| 92 | return false |
| 93 | } |
| 94 |