mirror of
https://github.com/ollama/ollama.git
synced 2026-03-27 02:58:43 +07:00
843 lines
23 KiB
Go
843 lines
23 KiB
Go
package launch
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/ollama/ollama/api"
|
|
"github.com/ollama/ollama/cmd/config"
|
|
"github.com/spf13/cobra"
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
// LauncherState is the launch-owned snapshot used to render the root launcher menu.
|
|
type LauncherState struct {
|
|
LastSelection string
|
|
RunModel string
|
|
RunModelUsable bool
|
|
Integrations map[string]LauncherIntegrationState
|
|
}
|
|
|
|
// LauncherIntegrationState is the launch-owned status for one launcher integration.
|
|
type LauncherIntegrationState struct {
|
|
Name string
|
|
DisplayName string
|
|
Description string
|
|
Installed bool
|
|
AutoInstallable bool
|
|
Selectable bool
|
|
Changeable bool
|
|
CurrentModel string
|
|
ModelUsable bool
|
|
InstallHint string
|
|
Editor bool
|
|
}
|
|
|
|
// RunModelRequest controls how the root launcher resolves the chat model.
|
|
type RunModelRequest struct {
|
|
ForcePicker bool
|
|
Policy *LaunchPolicy
|
|
}
|
|
|
|
// LaunchConfirmMode controls confirmation behavior across launch flows.
|
|
type LaunchConfirmMode int
|
|
|
|
const (
|
|
// LaunchConfirmPrompt prompts the user for confirmation.
|
|
LaunchConfirmPrompt LaunchConfirmMode = iota
|
|
// LaunchConfirmAutoApprove skips prompts and treats confirmation as accepted.
|
|
LaunchConfirmAutoApprove
|
|
// LaunchConfirmRequireYes rejects confirmation requests with a --yes hint.
|
|
LaunchConfirmRequireYes
|
|
)
|
|
|
|
// LaunchMissingModelMode controls local missing-model handling in launch flows.
|
|
type LaunchMissingModelMode int
|
|
|
|
const (
|
|
// LaunchMissingModelPromptToPull prompts to pull a missing local model.
|
|
LaunchMissingModelPromptToPull LaunchMissingModelMode = iota
|
|
// LaunchMissingModelAutoPull pulls a missing local model without prompting.
|
|
LaunchMissingModelAutoPull
|
|
// LaunchMissingModelFail fails immediately when a local model is missing.
|
|
LaunchMissingModelFail
|
|
)
|
|
|
|
// LaunchPolicy controls launch behavior that may vary by caller context.
|
|
type LaunchPolicy struct {
|
|
Confirm LaunchConfirmMode
|
|
MissingModel LaunchMissingModelMode
|
|
}
|
|
|
|
func defaultLaunchPolicy(interactive bool, yes bool) LaunchPolicy {
|
|
policy := LaunchPolicy{
|
|
Confirm: LaunchConfirmPrompt,
|
|
MissingModel: LaunchMissingModelPromptToPull,
|
|
}
|
|
switch {
|
|
case yes:
|
|
// if yes flag is set, auto approve and auto pull
|
|
policy.Confirm = LaunchConfirmAutoApprove
|
|
policy.MissingModel = LaunchMissingModelAutoPull
|
|
case !interactive:
|
|
// otherwise make sure to stop when needed
|
|
policy.Confirm = LaunchConfirmRequireYes
|
|
policy.MissingModel = LaunchMissingModelFail
|
|
}
|
|
return policy
|
|
}
|
|
|
|
func (p LaunchPolicy) confirmPolicy() launchConfirmPolicy {
|
|
switch p.Confirm {
|
|
case LaunchConfirmAutoApprove:
|
|
return launchConfirmPolicy{yes: true}
|
|
case LaunchConfirmRequireYes:
|
|
return launchConfirmPolicy{requireYesMessage: true}
|
|
default:
|
|
return launchConfirmPolicy{}
|
|
}
|
|
}
|
|
|
|
func (p LaunchPolicy) missingModelPolicy() missingModelPolicy {
|
|
switch p.MissingModel {
|
|
case LaunchMissingModelAutoPull:
|
|
return missingModelAutoPull
|
|
case LaunchMissingModelFail:
|
|
return missingModelFail
|
|
default:
|
|
return missingModelPromptPull
|
|
}
|
|
}
|
|
|
|
// IntegrationLaunchRequest controls the canonical integration launcher flow.
|
|
type IntegrationLaunchRequest struct {
|
|
Name string
|
|
ModelOverride string
|
|
ForceConfigure bool
|
|
ConfigureOnly bool
|
|
ExtraArgs []string
|
|
Policy *LaunchPolicy
|
|
}
|
|
|
|
var isInteractiveSession = func() bool {
|
|
return term.IsTerminal(int(os.Stdin.Fd())) && term.IsTerminal(int(os.Stdout.Fd()))
|
|
}
|
|
|
|
// Runner executes a model with an integration.
|
|
type Runner interface {
|
|
Run(model string, args []string) error
|
|
String() string
|
|
}
|
|
|
|
// Editor can edit config files for integrations that support model configuration.
|
|
type Editor interface {
|
|
Paths() []string
|
|
Edit(models []string) error
|
|
Models() []string
|
|
}
|
|
|
|
type modelInfo struct {
|
|
Name string
|
|
Remote bool
|
|
ToolCapable bool
|
|
}
|
|
|
|
// ModelInfo re-exports launcher model inventory details for callers.
|
|
type ModelInfo = modelInfo
|
|
|
|
// ModelItem represents a model for selection UIs.
|
|
type ModelItem struct {
|
|
Name string
|
|
Description string
|
|
Recommended bool
|
|
}
|
|
|
|
// LaunchCmd returns the cobra command for launching integrations.
|
|
// The runTUI callback is called when the root launcher UI should be shown.
|
|
func LaunchCmd(checkServerHeartbeat func(cmd *cobra.Command, args []string) error, runTUI func(cmd *cobra.Command)) *cobra.Command {
|
|
var modelFlag string
|
|
var configFlag bool
|
|
var yesFlag bool
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "launch [INTEGRATION] [-- [EXTRA_ARGS...]]",
|
|
Short: "Launch the Ollama menu or an integration",
|
|
Long: `Launch the Ollama interactive menu, or directly launch a specific integration.
|
|
|
|
Without arguments, this is equivalent to running 'ollama' directly.
|
|
Flags and extra arguments require an integration name.
|
|
|
|
Supported integrations:
|
|
claude Claude Code
|
|
cline Cline
|
|
codex Codex
|
|
droid Droid
|
|
opencode OpenCode
|
|
openclaw OpenClaw (aliases: clawdbot, moltbot)
|
|
pi Pi
|
|
|
|
Examples:
|
|
ollama launch
|
|
ollama launch claude
|
|
ollama launch claude --model <model>
|
|
ollama launch droid --config (does not auto-launch)
|
|
ollama launch codex -- -p myprofile (pass extra args to integration)
|
|
ollama launch codex -- --sandbox workspace-write`,
|
|
Args: cobra.ArbitraryArgs,
|
|
PreRunE: checkServerHeartbeat,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
policy := defaultLaunchPolicy(isInteractiveSession(), yesFlag)
|
|
// reset when done to make sure state doens't leak between launches
|
|
restoreConfirmPolicy := withLaunchConfirmPolicy(policy.confirmPolicy())
|
|
defer restoreConfirmPolicy()
|
|
|
|
var name string
|
|
var passArgs []string
|
|
dashIdx := cmd.ArgsLenAtDash()
|
|
|
|
if dashIdx == -1 {
|
|
if len(args) > 1 {
|
|
return fmt.Errorf("unexpected arguments: %v\nUse '--' to pass extra arguments to the integration", args[1:])
|
|
}
|
|
if len(args) == 1 {
|
|
name = args[0]
|
|
}
|
|
} else {
|
|
if dashIdx > 1 {
|
|
return fmt.Errorf("expected at most 1 integration name before '--', got %d", dashIdx)
|
|
}
|
|
if dashIdx == 1 {
|
|
name = args[0]
|
|
}
|
|
passArgs = args[dashIdx:]
|
|
}
|
|
|
|
if name == "" {
|
|
if cmd.Flags().Changed("model") || cmd.Flags().Changed("config") || cmd.Flags().Changed("yes") || len(passArgs) > 0 {
|
|
return fmt.Errorf("flags and extra args require an integration name, for example: 'ollama launch claude --model qwen3.5'")
|
|
}
|
|
runTUI(cmd)
|
|
return nil
|
|
}
|
|
|
|
if modelFlag != "" && isCloudModelName(modelFlag) {
|
|
if client, err := api.ClientFromEnvironment(); err == nil {
|
|
if disabled, _ := cloudStatusDisabled(cmd.Context(), client); disabled {
|
|
fmt.Fprintf(os.Stderr, "Warning: ignoring --model %s because cloud is disabled\n", modelFlag)
|
|
modelFlag = ""
|
|
}
|
|
}
|
|
}
|
|
|
|
headlessYes := yesFlag && !isInteractiveSession()
|
|
err := LaunchIntegration(cmd.Context(), IntegrationLaunchRequest{
|
|
Name: name,
|
|
ModelOverride: modelFlag,
|
|
ForceConfigure: configFlag || (modelFlag == "" && !headlessYes),
|
|
ConfigureOnly: configFlag,
|
|
ExtraArgs: passArgs,
|
|
Policy: &policy,
|
|
})
|
|
if errors.Is(err, ErrCancelled) {
|
|
return nil
|
|
}
|
|
return err
|
|
},
|
|
}
|
|
|
|
cmd.Flags().StringVar(&modelFlag, "model", "", "Model to use")
|
|
cmd.Flags().BoolVar(&configFlag, "config", false, "Configure without launching")
|
|
cmd.Flags().BoolVarP(&yesFlag, "yes", "y", false, "Automatically answer yes to confirmation prompts")
|
|
return cmd
|
|
}
|
|
|
|
type launcherClient struct {
|
|
apiClient *api.Client
|
|
modelInventory []ModelInfo
|
|
inventoryLoaded bool
|
|
policy LaunchPolicy
|
|
}
|
|
|
|
func newLauncherClient(policy LaunchPolicy) (*launcherClient, error) {
|
|
apiClient, err := api.ClientFromEnvironment()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &launcherClient{
|
|
apiClient: apiClient,
|
|
policy: policy,
|
|
}, nil
|
|
}
|
|
|
|
// BuildLauncherState returns the launch-owned root launcher menu snapshot.
|
|
func BuildLauncherState(ctx context.Context) (*LauncherState, error) {
|
|
launchClient, err := newLauncherClient(defaultLaunchPolicy(isInteractiveSession(), false))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return launchClient.buildLauncherState(ctx)
|
|
}
|
|
|
|
// ResolveRunModel returns the model that should be used for interactive chat.
|
|
func ResolveRunModel(ctx context.Context, req RunModelRequest) (string, error) {
|
|
// Called by the launcher TUI "Run a model" action (cmd/runLauncherAction),
|
|
// which resolves models separately from LaunchIntegration. Callers can pass
|
|
// Policy directly; otherwise we fall back to ambient --yes/session defaults.
|
|
policy := defaultLaunchPolicy(isInteractiveSession(), currentLaunchConfirmPolicy.yes)
|
|
if req.Policy != nil {
|
|
policy = *req.Policy
|
|
}
|
|
|
|
launchClient, err := newLauncherClient(policy)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return launchClient.resolveRunModel(ctx, req)
|
|
}
|
|
|
|
// LaunchIntegration runs the canonical launcher flow for one integration.
|
|
func LaunchIntegration(ctx context.Context, req IntegrationLaunchRequest) error {
|
|
name, runner, err := LookupIntegration(req.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !req.ConfigureOnly {
|
|
if err := EnsureIntegrationInstalled(name, runner); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
var policy LaunchPolicy
|
|
// TUI does not set a policy, whereas ollama launch <app> does as it can have flags which change the behavior
|
|
if req.Policy == nil {
|
|
policy = defaultLaunchPolicy(isInteractiveSession(), false)
|
|
} else {
|
|
policy = *req.Policy
|
|
}
|
|
|
|
launchClient, err := newLauncherClient(policy)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
saved, _ := loadStoredIntegrationConfig(name)
|
|
// In headless --yes mode we cannot prompt, so require an explicit --model.
|
|
if policy.Confirm == LaunchConfirmAutoApprove && !isInteractiveSession() && req.ModelOverride == "" {
|
|
return fmt.Errorf("headless --yes launch for %s requires --model <model>", name)
|
|
}
|
|
|
|
if editor, ok := runner.(Editor); ok {
|
|
return launchClient.launchEditorIntegration(ctx, name, runner, editor, saved, req)
|
|
}
|
|
return launchClient.launchSingleIntegration(ctx, name, runner, saved, req)
|
|
}
|
|
|
|
func (c *launcherClient) buildLauncherState(ctx context.Context) (*LauncherState, error) {
|
|
_ = c.loadModelInventoryOnce(ctx)
|
|
|
|
state := &LauncherState{
|
|
LastSelection: config.LastSelection(),
|
|
RunModel: config.LastModel(),
|
|
Integrations: make(map[string]LauncherIntegrationState),
|
|
}
|
|
runModelUsable, err := c.savedModelUsable(ctx, state.RunModel)
|
|
if err != nil {
|
|
runModelUsable = false
|
|
}
|
|
state.RunModelUsable = runModelUsable
|
|
|
|
for _, info := range ListIntegrationInfos() {
|
|
integrationState, err := c.buildLauncherIntegrationState(ctx, info)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
state.Integrations[info.Name] = integrationState
|
|
}
|
|
|
|
return state, nil
|
|
}
|
|
|
|
func (c *launcherClient) buildLauncherIntegrationState(ctx context.Context, info IntegrationInfo) (LauncherIntegrationState, error) {
|
|
integration, err := integrationFor(info.Name)
|
|
if err != nil {
|
|
return LauncherIntegrationState{}, err
|
|
}
|
|
currentModel, usable, err := c.launcherModelState(ctx, info.Name, integration.editor)
|
|
if err != nil {
|
|
return LauncherIntegrationState{}, err
|
|
}
|
|
|
|
return LauncherIntegrationState{
|
|
Name: info.Name,
|
|
DisplayName: info.DisplayName,
|
|
Description: info.Description,
|
|
Installed: integration.installed,
|
|
AutoInstallable: integration.autoInstallable,
|
|
Selectable: integration.installed || integration.autoInstallable,
|
|
Changeable: integration.installed || integration.autoInstallable,
|
|
CurrentModel: currentModel,
|
|
ModelUsable: usable,
|
|
InstallHint: integration.installHint,
|
|
Editor: integration.editor,
|
|
}, nil
|
|
}
|
|
|
|
func (c *launcherClient) launcherModelState(ctx context.Context, name string, isEditor bool) (string, bool, error) {
|
|
cfg, loadErr := loadStoredIntegrationConfig(name)
|
|
hasModels := loadErr == nil && len(cfg.Models) > 0
|
|
if !hasModels {
|
|
return "", false, nil
|
|
}
|
|
|
|
if isEditor {
|
|
filtered := c.filterDisabledCloudModels(ctx, cfg.Models)
|
|
if len(filtered) > 0 {
|
|
return filtered[0], true, nil
|
|
}
|
|
return cfg.Models[0], false, nil
|
|
}
|
|
|
|
model := cfg.Models[0]
|
|
usable, usableErr := c.savedModelUsable(ctx, model)
|
|
return model, usableErr == nil && usable, nil
|
|
}
|
|
|
|
func (c *launcherClient) resolveRunModel(ctx context.Context, req RunModelRequest) (string, error) {
|
|
current := config.LastModel()
|
|
if !req.ForcePicker && current != "" && c.policy.Confirm == LaunchConfirmAutoApprove && !isInteractiveSession() {
|
|
if err := c.ensureModelsReady(ctx, []string{current}); err != nil {
|
|
return "", err
|
|
}
|
|
fmt.Fprintf(os.Stderr, "Headless mode: auto-selected last used model %q\n", current)
|
|
if err := config.SetLastModel(current); err != nil {
|
|
return "", err
|
|
}
|
|
return current, nil
|
|
}
|
|
|
|
if !req.ForcePicker {
|
|
usable, err := c.savedModelUsable(ctx, current)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if usable {
|
|
if err := c.ensureModelsReady(ctx, []string{current}); err != nil {
|
|
return "", err
|
|
}
|
|
if err := config.SetLastModel(current); err != nil {
|
|
return "", err
|
|
}
|
|
return current, nil
|
|
}
|
|
}
|
|
|
|
model, err := c.selectSingleModelWithSelector(ctx, "Select model to run:", current, DefaultSingleSelector)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := config.SetLastModel(model); err != nil {
|
|
return "", err
|
|
}
|
|
return model, nil
|
|
}
|
|
|
|
func (c *launcherClient) launchSingleIntegration(ctx context.Context, name string, runner Runner, saved *config.IntegrationConfig, req IntegrationLaunchRequest) error {
|
|
current := primaryModelFromConfig(saved)
|
|
target := req.ModelOverride
|
|
needsConfigure := req.ForceConfigure
|
|
|
|
if target == "" {
|
|
target = current
|
|
usable, err := c.savedModelUsable(ctx, target)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !usable {
|
|
needsConfigure = true
|
|
}
|
|
}
|
|
|
|
if needsConfigure {
|
|
selected, err := c.selectSingleModelWithSelector(ctx, fmt.Sprintf("Select model for %s:", runner), target, DefaultSingleSelector)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
target = selected
|
|
} else if err := c.ensureModelsReady(ctx, []string{target}); err != nil {
|
|
return err
|
|
}
|
|
|
|
if target == "" {
|
|
return nil
|
|
}
|
|
|
|
if err := config.SaveIntegration(name, []string{target}); err != nil {
|
|
return fmt.Errorf("failed to save: %w", err)
|
|
}
|
|
|
|
return launchAfterConfiguration(name, runner, target, req)
|
|
}
|
|
|
|
func (c *launcherClient) launchEditorIntegration(ctx context.Context, name string, runner Runner, editor Editor, saved *config.IntegrationConfig, req IntegrationLaunchRequest) error {
|
|
models, needsConfigure := c.resolveEditorLaunchModels(ctx, saved, req)
|
|
|
|
if needsConfigure {
|
|
selected, err := c.selectMultiModelsForIntegration(ctx, runner, models)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
models = selected
|
|
} else if err := c.ensureModelsReady(ctx, models); err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(models) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if needsConfigure || req.ModelOverride != "" {
|
|
if err := prepareEditorIntegration(name, runner, editor, models); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return launchAfterConfiguration(name, runner, models[0], req)
|
|
}
|
|
|
|
func (c *launcherClient) selectSingleModelWithSelector(ctx context.Context, title, current string, selector SingleSelector) (string, error) {
|
|
if selector == nil {
|
|
return "", fmt.Errorf("no selector configured")
|
|
}
|
|
|
|
items, _, err := c.loadSelectableModels(ctx, nil, current, "no models available, run 'ollama pull <model>' first")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
selected, err := selector(title, items, current)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := c.ensureModelsReady(ctx, []string{selected}); err != nil {
|
|
return "", err
|
|
}
|
|
return selected, nil
|
|
}
|
|
|
|
func (c *launcherClient) selectMultiModelsForIntegration(ctx context.Context, runner Runner, preChecked []string) ([]string, error) {
|
|
if DefaultMultiSelector == nil {
|
|
return nil, fmt.Errorf("no selector configured")
|
|
}
|
|
|
|
current := firstModel(preChecked)
|
|
|
|
items, orderedChecked, err := c.loadSelectableModels(ctx, preChecked, current, "no models available")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(preChecked) > 0 {
|
|
// Keep list order stable in multi-select even when there are existing checks.
|
|
// checked/default state still comes from orderedChecked.
|
|
stableItems, _, stableErr := c.loadSelectableModels(ctx, nil, current, "no models available")
|
|
if stableErr != nil {
|
|
return nil, stableErr
|
|
}
|
|
items = stableItems
|
|
}
|
|
|
|
selected, err := DefaultMultiSelector(fmt.Sprintf("Select models for %s:", runner), items, orderedChecked)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := c.ensureModelsReady(ctx, selected); err != nil {
|
|
return nil, err
|
|
}
|
|
return selected, nil
|
|
}
|
|
|
|
func (c *launcherClient) loadSelectableModels(ctx context.Context, preChecked []string, current, emptyMessage string) ([]ModelItem, []string, error) {
|
|
if err := c.loadModelInventoryOnce(ctx); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
cloudDisabled, _ := cloudStatusDisabled(ctx, c.apiClient)
|
|
items, orderedChecked, _, _ := buildModelList(c.modelInventory, preChecked, current)
|
|
if cloudDisabled {
|
|
items = filterCloudItems(items)
|
|
orderedChecked = c.filterDisabledCloudModels(ctx, orderedChecked)
|
|
}
|
|
if len(items) == 0 {
|
|
return nil, nil, errors.New(emptyMessage)
|
|
}
|
|
return items, orderedChecked, nil
|
|
}
|
|
|
|
func (c *launcherClient) ensureModelsReady(ctx context.Context, models []string) error {
|
|
var deduped []string
|
|
seen := make(map[string]bool, len(models))
|
|
for _, model := range models {
|
|
if model == "" || seen[model] {
|
|
continue
|
|
}
|
|
seen[model] = true
|
|
deduped = append(deduped, model)
|
|
}
|
|
models = deduped
|
|
if len(models) == 0 {
|
|
return nil
|
|
}
|
|
|
|
cloudModels := make(map[string]bool, len(models))
|
|
for _, model := range models {
|
|
isCloudModel := isCloudModelName(model)
|
|
if isCloudModel {
|
|
cloudModels[model] = true
|
|
}
|
|
if err := showOrPullWithPolicy(ctx, c.apiClient, model, c.policy.missingModelPolicy(), isCloudModel); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return ensureAuth(ctx, c.apiClient, cloudModels, models)
|
|
}
|
|
|
|
func (c *launcherClient) resolveEditorLaunchModels(ctx context.Context, saved *config.IntegrationConfig, req IntegrationLaunchRequest) ([]string, bool) {
|
|
if req.ForceConfigure {
|
|
return editorPreCheckedModels(saved, req.ModelOverride), true
|
|
}
|
|
|
|
if req.ModelOverride != "" {
|
|
models := append([]string{req.ModelOverride}, additionalSavedModels(saved, req.ModelOverride)...)
|
|
models = c.filterDisabledCloudModels(ctx, models)
|
|
return models, len(models) == 0
|
|
}
|
|
|
|
if saved == nil || len(saved.Models) == 0 {
|
|
return nil, true
|
|
}
|
|
|
|
models := c.filterDisabledCloudModels(ctx, saved.Models)
|
|
return models, len(models) == 0
|
|
}
|
|
|
|
func (c *launcherClient) filterDisabledCloudModels(ctx context.Context, models []string) []string {
|
|
// if connection cannot be established or there is a 404, cloud models will continue to be displayed
|
|
cloudDisabled, _ := cloudStatusDisabled(ctx, c.apiClient)
|
|
if !cloudDisabled {
|
|
return append([]string(nil), models...)
|
|
}
|
|
|
|
filtered := make([]string, 0, len(models))
|
|
for _, model := range models {
|
|
if !isCloudModelName(model) {
|
|
filtered = append(filtered, model)
|
|
}
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
func (c *launcherClient) savedModelUsable(ctx context.Context, name string) (bool, error) {
|
|
if err := c.loadModelInventoryOnce(ctx); err != nil {
|
|
return c.showBasedModelUsable(ctx, name)
|
|
}
|
|
return c.singleModelUsable(ctx, name), nil
|
|
}
|
|
|
|
func (c *launcherClient) showBasedModelUsable(ctx context.Context, name string) (bool, error) {
|
|
if name == "" {
|
|
return false, nil
|
|
}
|
|
|
|
info, err := c.apiClient.Show(ctx, &api.ShowRequest{Model: name})
|
|
if err != nil {
|
|
var statusErr api.StatusError
|
|
if errors.As(err, &statusErr) && statusErr.StatusCode == http.StatusNotFound {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
if isCloudModelName(name) || info.RemoteModel != "" {
|
|
cloudDisabled, _ := cloudStatusDisabled(ctx, c.apiClient)
|
|
|
|
return !cloudDisabled, nil
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (c *launcherClient) singleModelUsable(ctx context.Context, name string) bool {
|
|
if name == "" {
|
|
return false
|
|
}
|
|
if isCloudModelName(name) {
|
|
cloudDisabled, _ := cloudStatusDisabled(ctx, c.apiClient)
|
|
return !cloudDisabled
|
|
}
|
|
return c.hasLocalModel(name)
|
|
}
|
|
|
|
func (c *launcherClient) hasLocalModel(name string) bool {
|
|
for _, model := range c.modelInventory {
|
|
if model.Remote {
|
|
continue
|
|
}
|
|
if model.Name == name || strings.HasPrefix(model.Name, name+":") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *launcherClient) loadModelInventoryOnce(ctx context.Context) error {
|
|
if c.inventoryLoaded {
|
|
return nil
|
|
}
|
|
|
|
resp, err := c.apiClient.List(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.modelInventory = c.modelInventory[:0]
|
|
for _, model := range resp.Models {
|
|
c.modelInventory = append(c.modelInventory, ModelInfo{
|
|
Name: model.Name,
|
|
Remote: model.RemoteModel != "",
|
|
})
|
|
}
|
|
|
|
cloudDisabled, _ := cloudStatusDisabled(ctx, c.apiClient)
|
|
if cloudDisabled {
|
|
c.modelInventory = filterCloudModels(c.modelInventory)
|
|
}
|
|
c.inventoryLoaded = true
|
|
return nil
|
|
}
|
|
|
|
func runIntegration(runner Runner, modelName string, args []string) error {
|
|
fmt.Fprintf(os.Stderr, "\nLaunching %s with %s...\n", runner, modelName)
|
|
return runner.Run(modelName, args)
|
|
}
|
|
|
|
func launchAfterConfiguration(name string, runner Runner, model string, req IntegrationLaunchRequest) error {
|
|
if req.ConfigureOnly {
|
|
launch, err := ConfirmPrompt(fmt.Sprintf("Launch %s now?", runner))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !launch {
|
|
return nil
|
|
}
|
|
}
|
|
if err := EnsureIntegrationInstalled(name, runner); err != nil {
|
|
return err
|
|
}
|
|
return runIntegration(runner, model, req.ExtraArgs)
|
|
}
|
|
|
|
func loadStoredIntegrationConfig(name string) (*config.IntegrationConfig, error) {
|
|
cfg, err := config.LoadIntegration(name)
|
|
if err == nil {
|
|
return cfg, nil
|
|
}
|
|
if !errors.Is(err, os.ErrNotExist) {
|
|
return nil, err
|
|
}
|
|
|
|
spec, specErr := LookupIntegrationSpec(name)
|
|
if specErr != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, alias := range spec.Aliases {
|
|
legacy, legacyErr := config.LoadIntegration(alias)
|
|
if legacyErr == nil {
|
|
migrateLegacyIntegrationConfig(spec.Name, legacy)
|
|
if migrated, migratedErr := config.LoadIntegration(spec.Name); migratedErr == nil {
|
|
return migrated, nil
|
|
}
|
|
return legacy, nil
|
|
}
|
|
if legacyErr != nil && !errors.Is(legacyErr, os.ErrNotExist) {
|
|
return nil, legacyErr
|
|
}
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
func migrateLegacyIntegrationConfig(canonical string, legacy *config.IntegrationConfig) {
|
|
if legacy == nil {
|
|
return
|
|
}
|
|
|
|
_ = config.SaveIntegration(canonical, append([]string(nil), legacy.Models...))
|
|
if len(legacy.Aliases) > 0 {
|
|
_ = config.SaveAliases(canonical, cloneAliases(legacy.Aliases))
|
|
}
|
|
if legacy.Onboarded {
|
|
_ = config.MarkIntegrationOnboarded(canonical)
|
|
}
|
|
}
|
|
|
|
func primaryModelFromConfig(cfg *config.IntegrationConfig) string {
|
|
if cfg == nil || len(cfg.Models) == 0 {
|
|
return ""
|
|
}
|
|
return cfg.Models[0]
|
|
}
|
|
|
|
func cloneAliases(aliases map[string]string) map[string]string {
|
|
if len(aliases) == 0 {
|
|
return make(map[string]string)
|
|
}
|
|
|
|
cloned := make(map[string]string, len(aliases))
|
|
for key, value := range aliases {
|
|
cloned[key] = value
|
|
}
|
|
return cloned
|
|
}
|
|
|
|
func singleModelPrechecked(current string) []string {
|
|
if current == "" {
|
|
return nil
|
|
}
|
|
return []string{current}
|
|
}
|
|
|
|
func firstModel(models []string) string {
|
|
if len(models) == 0 {
|
|
return ""
|
|
}
|
|
return models[0]
|
|
}
|
|
|
|
func editorPreCheckedModels(saved *config.IntegrationConfig, override string) []string {
|
|
if override == "" {
|
|
if saved == nil {
|
|
return nil
|
|
}
|
|
return append([]string(nil), saved.Models...)
|
|
}
|
|
return append([]string{override}, additionalSavedModels(saved, override)...)
|
|
}
|
|
|
|
func additionalSavedModels(saved *config.IntegrationConfig, exclude string) []string {
|
|
if saved == nil {
|
|
return nil
|
|
}
|
|
|
|
var models []string
|
|
for _, model := range saved.Models {
|
|
if model != exclude {
|
|
models = append(models, model)
|
|
}
|
|
}
|
|
return models
|
|
}
|