mirror of
https://github.com/ollama/ollama.git
synced 2026-07-16 05:05:37 -05:00
855 lines
25 KiB
Go
855 lines
25 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"runtime"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
coreagent "github.com/ollama/ollama/agent"
|
|
agenttools "github.com/ollama/ollama/agent/tools"
|
|
"github.com/ollama/ollama/api"
|
|
"github.com/ollama/ollama/cmd/config"
|
|
"github.com/ollama/ollama/cmd/launch"
|
|
agentchat "github.com/ollama/ollama/cmd/tui/chat"
|
|
"github.com/ollama/ollama/format"
|
|
internalcloud "github.com/ollama/ollama/internal/cloud"
|
|
"github.com/ollama/ollama/internal/modelref"
|
|
"github.com/ollama/ollama/types/model"
|
|
)
|
|
|
|
type agentTUIOptions struct {
|
|
Model string
|
|
OpenModelPicker bool
|
|
System string
|
|
Format string
|
|
Options map[string]any
|
|
Think *api.ThinkValue
|
|
KeepAlive *api.Duration
|
|
ContextWindowTokens int
|
|
AllowAllTools bool
|
|
ToolsDisabled bool
|
|
MultiModal bool
|
|
}
|
|
|
|
func registerAgentFlags(cmd *cobra.Command) {
|
|
cmd.Flags().String("model", "", "Model to use")
|
|
cmd.Flags().String("keepalive", "", "Duration to keep a model loaded (e.g. 5m)")
|
|
cmd.Flags().String("format", "", "Response format (e.g. json)")
|
|
cmd.Flags().String("think", "", "Enable thinking mode: true/false or high/medium/low for supported models")
|
|
cmd.Flags().Lookup("think").NoOptDefVal = "true"
|
|
cmd.Flags().Bool("auto-approve-tools", false, "Allow agent tools to run without prompting")
|
|
cmd.Flags().Bool("yolo", false, "Alias for --auto-approve-tools")
|
|
cmd.Flags().Bool("no-tools", false, "Disable agent tools")
|
|
}
|
|
|
|
func AgentHandler(cmd *cobra.Command, _ []string) error {
|
|
opts := agentTUIOptions{
|
|
Model: strings.TrimSpace(config.LastModel()),
|
|
Options: map[string]any{},
|
|
}
|
|
thinkExplicit, err := applyAgentFlags(cmd, &opts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if strings.TrimSpace(opts.Model) == "" {
|
|
opts.OpenModelPicker = true
|
|
} else if cmd.Flags().Lookup("model") == nil || !cmd.Flags().Lookup("model").Changed {
|
|
opts.OpenModelPicker = true
|
|
}
|
|
|
|
client, err := api.ClientFromEnvironment()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if opts.OpenModelPicker {
|
|
modelName, err := selectAgentModel(cmd.Context(), client, opts.Model)
|
|
if errors.Is(err, launch.ErrCancelled) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
opts.Model = modelName
|
|
opts.OpenModelPicker = false
|
|
}
|
|
|
|
if strings.TrimSpace(opts.Model) != "" {
|
|
info, err := prepareAgentModel(cmd, client, &opts, thinkExplicit)
|
|
if err != nil {
|
|
if handleCloudAuthorizationError(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
opts.System = info.System
|
|
if err := saveLastAgentModel(opts.Model); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := GenerateAgentTUI(cmd, client, opts); err != nil {
|
|
if handleCloudAuthorizationError(err) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("error running agent: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func applyAgentFlags(cmd *cobra.Command, opts *agentTUIOptions) (bool, error) {
|
|
if flag := cmd.Flags().Lookup("model"); flag != nil && flag.Changed {
|
|
modelName, err := cmd.Flags().GetString("model")
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
modelName = strings.TrimSpace(modelName)
|
|
if modelName == "" {
|
|
return false, errors.New("--model cannot be empty")
|
|
}
|
|
opts.Model = modelName
|
|
opts.OpenModelPicker = false
|
|
}
|
|
|
|
format, err := cmd.Flags().GetString("format")
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
opts.Format = format
|
|
|
|
thinkExplicit := false
|
|
thinkFlag := cmd.Flags().Lookup("think")
|
|
if thinkFlag != nil && thinkFlag.Changed {
|
|
thinkExplicit = true
|
|
thinkStr, err := cmd.Flags().GetString("think")
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
switch thinkStr {
|
|
case "", "true":
|
|
opts.Think = &api.ThinkValue{Value: true}
|
|
case "false":
|
|
opts.Think = &api.ThinkValue{Value: false}
|
|
case "high", "medium", "low", "max":
|
|
opts.Think = &api.ThinkValue{Value: thinkStr}
|
|
default:
|
|
return false, fmt.Errorf("invalid value for --think: %q (must be true, false, high, medium, low, or max)", thinkStr)
|
|
}
|
|
}
|
|
|
|
keepAlive, err := cmd.Flags().GetString("keepalive")
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if keepAlive != "" {
|
|
d, err := time.ParseDuration(keepAlive)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
opts.KeepAlive = &api.Duration{Duration: d}
|
|
}
|
|
|
|
autoApprove, err := cmd.Flags().GetBool("auto-approve-tools")
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
yolo, err := cmd.Flags().GetBool("yolo")
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
opts.AllowAllTools = autoApprove || yolo
|
|
toolsDisabled, err := cmd.Flags().GetBool("no-tools")
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
opts.ToolsDisabled = toolsDisabled
|
|
return thinkExplicit, nil
|
|
}
|
|
|
|
func saveLastAgentModel(model string) error {
|
|
model = strings.TrimSpace(model)
|
|
if model == "" {
|
|
return nil
|
|
}
|
|
return config.SetLastModel(model)
|
|
}
|
|
|
|
func prepareAgentModel(cmd *cobra.Command, client *api.Client, opts *agentTUIOptions, thinkExplicit bool) (*api.ShowResponse, error) {
|
|
requestedCloud := modelref.HasExplicitCloudSource(opts.Model)
|
|
info, err := func() (*api.ShowResponse, error) {
|
|
info, err := client.Show(cmd.Context(), &api.ShowRequest{Model: opts.Model})
|
|
var se api.StatusError
|
|
if errors.As(err, &se) && se.StatusCode == http.StatusNotFound {
|
|
if requestedCloud {
|
|
return nil, err
|
|
}
|
|
if err := PullHandler(cmd, []string{opts.Model}); err != nil {
|
|
return nil, err
|
|
}
|
|
return client.Show(cmd.Context(), &api.ShowRequest{Model: opts.Model})
|
|
}
|
|
return info, err
|
|
}()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ensureCloudStub(cmd.Context(), client, opts.Model)
|
|
opts.Think, err = inferThinkingOption(&info.Capabilities, &runOptions{Model: opts.Model, Think: opts.Think}, thinkExplicit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
opts.MultiModal = showResponseSupportsMultimodal(info)
|
|
opts.ContextWindowTokens = showResponseContextWindow(info)
|
|
return info, nil
|
|
}
|
|
|
|
func GenerateAgentTUI(cmd *cobra.Command, client *api.Client, opts agentTUIOptions) error {
|
|
cwd := agentWorkingDir()
|
|
contextWindowForModel := func(ctx context.Context, model string, fallback int) int {
|
|
return agentContextWindowForModel(ctx, client, model, fallback)
|
|
}
|
|
|
|
var registry *coreagent.Registry
|
|
registryForModel := func(ctx context.Context, model string) *coreagent.Registry {
|
|
return agentToolsRegistry(ctx, client, model)
|
|
}
|
|
if opts.Model != "" {
|
|
registry = agentToolsRegistry(cmd.Context(), client, opts.Model)
|
|
}
|
|
systemPrompt := agentSystemPromptWithWorkingDir(opts.Model, opts.System, "", cwd)
|
|
|
|
_, err := agentchat.Run(cmd.Context(), agentchat.Options{
|
|
Model: opts.Model,
|
|
Client: client,
|
|
Tools: registry,
|
|
ToolRegistryForModel: registryForModel,
|
|
ToolsDisabled: opts.ToolsDisabled,
|
|
MultiModalForModel: func(ctx context.Context, model string) bool {
|
|
return agentModelSupportsMultimodal(ctx, client, model)
|
|
},
|
|
ModelOptions: func(ctx context.Context) ([]agentchat.ModelOption, error) {
|
|
return agentModelOptions(ctx, client)
|
|
},
|
|
OnModelSelected: func(_ context.Context, model string) error {
|
|
return config.SetLastModel(model)
|
|
},
|
|
SystemPromptForModel: func(ctx context.Context, model string, registry *coreagent.Registry) string {
|
|
return agentSystemPromptWithWorkingDir(model, agentSystemFromShow(ctx, client, model), "", cwd)
|
|
},
|
|
SystemPrompt: systemPrompt,
|
|
WorkingDir: cwd,
|
|
Format: opts.Format,
|
|
Options: opts.Options,
|
|
Think: opts.Think,
|
|
KeepAlive: opts.KeepAlive,
|
|
MultiModal: opts.MultiModal,
|
|
AllowAllTools: opts.AllowAllTools,
|
|
ContextWindowTokens: opts.ContextWindowTokens,
|
|
Compactor: &coreagent.SimpleCompactor{
|
|
Client: client,
|
|
Options: coreagent.CompactionOptions{ContextWindowTokens: opts.ContextWindowTokens},
|
|
},
|
|
ContextWindowTokensForModel: func(ctx context.Context, model string, fallback int) int {
|
|
return contextWindowForModel(ctx, model, fallback)
|
|
},
|
|
PreloadModel: func(ctx context.Context, model string, think *api.ThinkValue) (int, error) {
|
|
return preloadAgentModelIfLocal(ctx, client, opts, model, think)
|
|
},
|
|
CheckCloudModel: func(ctx context.Context, model, requiredPlan string) error {
|
|
return ensureCloudModelAccess(ctx, client, model, requiredPlan)
|
|
},
|
|
OpenBrowser: launch.OpenBrowser,
|
|
PollCloudAuth: func(ctx context.Context) (string, bool, error) {
|
|
user, err := client.Whoami(ctx)
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
if user == nil || user.Name == "" {
|
|
return "", false, nil
|
|
}
|
|
return user.Name, true, nil
|
|
},
|
|
})
|
|
return err
|
|
}
|
|
|
|
func selectAgentModel(ctx context.Context, client *api.Client, current string) (string, error) {
|
|
models, err := agentModelOptions(ctx, client)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(models) == 0 {
|
|
return "", errors.New("no models available, run 'ollama pull <model>' first")
|
|
}
|
|
|
|
items := agentSelectionItems(models)
|
|
switch {
|
|
case launch.DefaultSingleSelectorWithUpdates != nil:
|
|
return launch.DefaultSingleSelectorWithUpdates("Select model to run:", items, current, nil)
|
|
case launch.DefaultSingleSelector != nil:
|
|
return launch.DefaultSingleSelector("Select model to run:", items, current)
|
|
default:
|
|
return "", errors.New("no selector configured")
|
|
}
|
|
}
|
|
|
|
func agentSelectionItems(models []agentchat.ModelOption) []launch.SelectionItem {
|
|
items := make([]launch.SelectionItem, 0, len(models))
|
|
for _, model := range models {
|
|
items = append(items, launch.SelectionItem{
|
|
Name: model.Name,
|
|
Description: agentSelectionDescription(model),
|
|
Recommended: model.Recommended,
|
|
AvailabilityBadge: model.AvailabilityBadge,
|
|
})
|
|
}
|
|
return items
|
|
}
|
|
|
|
func agentSelectionDescription(model agentchat.ModelOption) string {
|
|
return strings.TrimSpace(model.Description)
|
|
}
|
|
|
|
var agentGetwd = os.Getwd
|
|
|
|
func agentWorkingDir() string {
|
|
cwd, err := agentGetwd()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return cwd
|
|
}
|
|
|
|
func agentSystemPrompt(modelName string, modelSystem string, extra string) string {
|
|
return agentSystemPromptWithWorkingDir(modelName, modelSystem, extra, agentWorkingDir())
|
|
}
|
|
|
|
func agentSystemPromptWithWorkingDir(modelName string, modelSystem string, extra string, workingDir string) string {
|
|
return agentSystemPromptAtWithWorkingDir(time.Now(), modelName, modelSystem, extra, workingDir)
|
|
}
|
|
|
|
func agentSystemPromptAt(now time.Time, modelName string, modelSystem string, extra string) string {
|
|
return agentSystemPromptAtWithWorkingDir(now, modelName, modelSystem, extra, agentWorkingDir())
|
|
}
|
|
|
|
func agentSystemPromptAtWithWorkingDir(now time.Time, modelName string, modelSystem string, extra string, workingDir string) string {
|
|
var parts []string
|
|
parts = append(parts, agentDefaultSystemPromptWithWorkingDir(now, modelName, workingDir))
|
|
if strings.TrimSpace(modelSystem) != "" {
|
|
parts = append(parts, strings.TrimSpace(modelSystem))
|
|
}
|
|
if strings.TrimSpace(extra) != "" {
|
|
parts = append(parts, strings.TrimSpace(extra))
|
|
}
|
|
return strings.Join(parts, "\n\n")
|
|
}
|
|
|
|
func agentDefaultSystemPrompt(now time.Time, modelName string) string {
|
|
return agentDefaultSystemPromptWithWorkingDir(now, modelName, agentWorkingDir())
|
|
}
|
|
|
|
func agentDefaultSystemPromptWithWorkingDir(now time.Time, modelName string, workingDir string) string {
|
|
date := now.Format("Monday, January 2, 2006")
|
|
shellName := "bash"
|
|
if runtime.GOOS == "windows" {
|
|
shellName = "PowerShell"
|
|
}
|
|
parts := []string{
|
|
"You are running in Ollama, in a harness to help the user accomplish tasks, and the model is " + modelName + ".",
|
|
"",
|
|
"Current date: " + date + ".",
|
|
"",
|
|
}
|
|
if workingDir != "" {
|
|
parts = append(parts, "Current working directory: "+strconv.Quote(workingDir)+".", "")
|
|
}
|
|
parts = append(parts,
|
|
"Be concise, practical, and action-oriented. Use tools when they materially help. Verify current or fast-changing facts with web tools when available; otherwise state uncertainty.",
|
|
"",
|
|
"Use "+shellName+" carefully. Prefer read-only inspection first. Stay within the current working directory unless explicitly asked. Surface intent before risky actions such as writes, deletes, moves, installs, git state changes, service changes, sudo, secrets access, network scripts, or commands outside the working directory. Request approval when required and do not work around denied approvals.",
|
|
"",
|
|
"Tell the user about meaningful changes, verification, failures, blockers, assumptions, and risks. Summarize routine tool output instead of dumping it.",
|
|
)
|
|
return strings.Join(parts, "\n")
|
|
}
|
|
|
|
func agentSystemFromShow(ctx context.Context, client *api.Client, modelName string) string {
|
|
if client == nil || strings.TrimSpace(modelName) == "" {
|
|
return ""
|
|
}
|
|
resp, err := client.Show(ctx, &api.ShowRequest{Model: modelName})
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "\033[1mwarning:\033[0m could not load model system prompt: %v\n", err)
|
|
return ""
|
|
}
|
|
return resp.System
|
|
}
|
|
|
|
func agentToolsRegistry(ctx context.Context, client *api.Client, modelName string) *coreagent.Registry {
|
|
supportsTools, err := agentModelSupportsTools(ctx, client, modelName)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "\033[1mwarning:\033[0m could not check model capabilities: %v\n", err)
|
|
}
|
|
if !supportsTools {
|
|
return nil
|
|
}
|
|
|
|
registry := &coreagent.Registry{}
|
|
if os.Getenv("OLLAMA_AGENT_DISABLE_SHELL") == "" {
|
|
registry.Register(&agenttools.Bash{})
|
|
}
|
|
registry.Register(&agenttools.Read{})
|
|
registry.Register(&agenttools.Edit{})
|
|
|
|
if os.Getenv("OLLAMA_AGENT_DISABLE_WEBSEARCH") == "" {
|
|
if disabled, known := agentCloudStatusDisabled(ctx, client); !known || !disabled {
|
|
registry.Register(&agenttools.WebSearch{})
|
|
registry.Register(&agenttools.WebFetch{})
|
|
} else {
|
|
fmt.Fprintf(os.Stderr, "%s\n", internalcloud.DisabledError("web search is unavailable"))
|
|
}
|
|
}
|
|
return registry
|
|
}
|
|
|
|
func agentModelSupportsTools(ctx context.Context, client *api.Client, modelName string) (bool, error) {
|
|
if client == nil || strings.TrimSpace(modelName) == "" {
|
|
return false, nil
|
|
}
|
|
resp, err := client.Show(ctx, &api.ShowRequest{Model: modelName})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return slices.Contains(resp.Capabilities, model.CapabilityTools), nil
|
|
}
|
|
|
|
func agentModelSupportsMultimodal(ctx context.Context, client *api.Client, modelName string) bool {
|
|
if client == nil || strings.TrimSpace(modelName) == "" {
|
|
return false
|
|
}
|
|
resp, err := client.Show(ctx, &api.ShowRequest{Model: modelName})
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "\033[1mwarning:\033[0m could not check model capabilities: %v\n", err)
|
|
return false
|
|
}
|
|
return showResponseSupportsMultimodal(resp)
|
|
}
|
|
|
|
func showResponseSupportsMultimodal(resp *api.ShowResponse) bool {
|
|
if resp == nil {
|
|
return false
|
|
}
|
|
if slices.Contains(resp.Capabilities, model.CapabilityVision) || slices.Contains(resp.Capabilities, model.CapabilityAudio) {
|
|
return true
|
|
}
|
|
if len(resp.ProjectorInfo) != 0 {
|
|
return true
|
|
}
|
|
for key := range resp.ModelInfo {
|
|
if strings.Contains(key, ".vision.") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func agentContextWindowForModel(ctx context.Context, client *api.Client, modelName string, fallback int) int {
|
|
if client == nil || strings.TrimSpace(modelName) == "" {
|
|
return fallback
|
|
}
|
|
if tokens := loadedContextWindowForModel(ctx, client, modelName); tokens > 0 {
|
|
return tokens
|
|
}
|
|
if modelref.HasExplicitCloudSource(modelName) {
|
|
if tokens := agentRecommendationContextWindowForModel(ctx, client, modelName); tokens > 0 {
|
|
return tokens
|
|
}
|
|
}
|
|
resp, err := client.Show(ctx, &api.ShowRequest{Model: modelName})
|
|
if err != nil {
|
|
return fallback
|
|
}
|
|
if tokens := showResponseContextWindow(resp); tokens > 0 {
|
|
return tokens
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func agentRecommendationContextWindowForModel(ctx context.Context, client *api.Client, modelName string) int {
|
|
if client == nil {
|
|
return 0
|
|
}
|
|
recs, err := client.ModelRecommendationsExperimental(ctx)
|
|
if err != nil || recs == nil {
|
|
return 0
|
|
}
|
|
return contextWindowFromRecommendations(modelName, recs.Recommendations)
|
|
}
|
|
|
|
func contextWindowFromRecommendations(modelName string, recommendations []api.ModelRecommendation) int {
|
|
for _, rec := range recommendations {
|
|
if rec.ContextLength <= 0 {
|
|
continue
|
|
}
|
|
if sameModelRef(modelName, rec.Model) {
|
|
return rec.ContextLength
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func sameModelRef(a, b string) bool {
|
|
a = comparableModelRef(a)
|
|
b = comparableModelRef(b)
|
|
if strings.EqualFold(a, b) {
|
|
return true
|
|
}
|
|
pa, errA := modelref.ParseRef(a)
|
|
pb, errB := modelref.ParseRef(b)
|
|
if errA != nil || errB != nil {
|
|
return false
|
|
}
|
|
if !strings.EqualFold(pa.Base, pb.Base) {
|
|
return false
|
|
}
|
|
return pa.Source == pb.Source ||
|
|
pa.Source == modelref.ModelSourceUnspecified ||
|
|
pb.Source == modelref.ModelSourceUnspecified
|
|
}
|
|
|
|
func comparableModelRef(value string) string {
|
|
value = strings.TrimSpace(value)
|
|
if strings.HasSuffix(strings.ToLower(value), ":latest") {
|
|
return strings.TrimSpace(value[:len(value)-len(":latest")])
|
|
}
|
|
return value
|
|
}
|
|
|
|
func showResponseContextWindow(resp *api.ShowResponse) int {
|
|
if resp == nil {
|
|
return 0
|
|
}
|
|
if resp.Details.ContextLength > 0 {
|
|
return resp.Details.ContextLength
|
|
}
|
|
if n, ok := numericModelInfo(resp.ModelInfo["general.context_length"]); ok {
|
|
return n
|
|
}
|
|
best := 0
|
|
for key, value := range resp.ModelInfo {
|
|
if key != "context_length" && !strings.HasSuffix(key, ".context_length") {
|
|
continue
|
|
}
|
|
if n, ok := numericModelInfo(value); ok && n > best {
|
|
best = n
|
|
}
|
|
}
|
|
return best
|
|
}
|
|
|
|
func numericModelInfo(value any) (int, bool) {
|
|
switch v := value.(type) {
|
|
case int:
|
|
return v, v > 0
|
|
case int32:
|
|
return int(v), v > 0
|
|
case int64:
|
|
return int(v), v > 0
|
|
case uint:
|
|
return int(v), v > 0
|
|
case uint32:
|
|
return int(v), v > 0
|
|
case uint64:
|
|
return int(v), v > 0
|
|
case float64:
|
|
return int(v), v > 0
|
|
case string:
|
|
n, err := strconv.Atoi(strings.TrimSpace(v))
|
|
return n, err == nil && n > 0
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|
|
|
|
func preloadAgentModelIfLocal(ctx context.Context, client *api.Client, opts agentTUIOptions, modelName string, think *api.ThinkValue) (int, error) {
|
|
modelName = strings.TrimSpace(modelName)
|
|
if client == nil || modelName == "" {
|
|
return 0, nil
|
|
}
|
|
if modelref.HasExplicitCloudSource(modelName) {
|
|
return 0, nil
|
|
}
|
|
info, err := client.Show(ctx, &api.ShowRequest{Model: modelName})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if info.RemoteHost != "" {
|
|
return 0, nil
|
|
}
|
|
if err := client.Generate(ctx, &api.GenerateRequest{
|
|
Model: modelName,
|
|
KeepAlive: opts.KeepAlive,
|
|
Options: opts.Options,
|
|
Think: think,
|
|
}, func(api.GenerateResponse) error {
|
|
return nil
|
|
}); err != nil {
|
|
return 0, err
|
|
}
|
|
return loadedContextWindowForModel(ctx, client, modelName), nil
|
|
}
|
|
|
|
func loadedContextWindowForModel(ctx context.Context, client *api.Client, modelName string) int {
|
|
if client == nil || strings.TrimSpace(modelName) == "" {
|
|
return 0
|
|
}
|
|
resp, err := client.ListRunning(ctx)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return processContextWindowForModel(modelName, resp)
|
|
}
|
|
|
|
func processContextWindowForModel(modelName string, resp *api.ProcessResponse) int {
|
|
if resp == nil {
|
|
return 0
|
|
}
|
|
for _, running := range resp.Models {
|
|
if running.ContextLength <= 0 {
|
|
continue
|
|
}
|
|
if sameModelRef(modelName, running.Name) || sameModelRef(modelName, running.Model) {
|
|
return running.ContextLength
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func agentModelOptions(ctx context.Context, client *api.Client) ([]agentchat.ModelOption, error) {
|
|
if client == nil {
|
|
return nil, errors.New("model picker requires an API client")
|
|
}
|
|
|
|
list, err := client.List(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
seen := make(map[string]struct{})
|
|
var options []agentchat.ModelOption
|
|
add := func(name, description string, recommended bool, requiredPlan string, cloud bool) {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
return
|
|
}
|
|
key := strings.ToLower(name)
|
|
if _, ok := seen[key]; ok {
|
|
return
|
|
}
|
|
seen[key] = struct{}{}
|
|
options = append(options, agentchat.ModelOption{
|
|
Name: name,
|
|
Description: strings.TrimSpace(description),
|
|
Recommended: recommended,
|
|
RequiredPlan: requiredPlan,
|
|
Cloud: cloud,
|
|
})
|
|
}
|
|
|
|
if disabled, known := agentCloudStatusDisabled(ctx, client); !known || !disabled {
|
|
if recs, err := client.ModelRecommendationsExperimental(ctx); err == nil {
|
|
for _, rec := range recs.Recommendations {
|
|
name := strings.TrimSpace(rec.Model)
|
|
if !modelref.HasExplicitCloudSource(name) {
|
|
continue
|
|
}
|
|
add(name, agentRecommendationDescription(rec), true, strings.TrimSpace(rec.RequiredPlan), true)
|
|
}
|
|
}
|
|
}
|
|
|
|
local := slices.Clone(list.Models)
|
|
slices.SortStableFunc(local, func(a, b api.ListModelResponse) int {
|
|
return strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name))
|
|
})
|
|
for _, model := range local {
|
|
name := strings.TrimSpace(model.Name)
|
|
if name == "" {
|
|
name = strings.TrimSpace(model.Model)
|
|
}
|
|
name = strings.TrimSuffix(name, ":latest")
|
|
if modelref.HasExplicitCloudSource(name) {
|
|
add(name, agentCloudModelDescription(model), false, "", true)
|
|
continue
|
|
}
|
|
add(name, agentLocalModelDescription(model), false, "", false)
|
|
}
|
|
|
|
badges, signInURLs := cloudAvailabilityBadges(ctx, client, options)
|
|
for i := range options {
|
|
options[i].AvailabilityBadge = badges[options[i].Name]
|
|
options[i].SignInURL = signInURLs[options[i].Name]
|
|
}
|
|
return options, nil
|
|
}
|
|
|
|
func cloudAvailabilityBadges(ctx context.Context, client *api.Client, options []agentchat.ModelOption) (map[string]string, map[string]string) {
|
|
badges := make(map[string]string)
|
|
signInURLs := make(map[string]string)
|
|
hasCloud := false
|
|
for _, opt := range options {
|
|
if opt.Cloud {
|
|
hasCloud = true
|
|
break
|
|
}
|
|
}
|
|
if !hasCloud {
|
|
return badges, signInURLs
|
|
}
|
|
|
|
if disabled, known := agentCloudStatusDisabled(ctx, client); known && disabled {
|
|
return badges, signInURLs
|
|
}
|
|
|
|
whoamiCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
|
defer cancel()
|
|
user, err := client.Whoami(whoamiCtx)
|
|
if err != nil {
|
|
var authErr api.AuthorizationError
|
|
signInURL := ""
|
|
if errors.As(err, &authErr) && (authErr.StatusCode == http.StatusUnauthorized || authErr.SigninURL != "") {
|
|
if authErr.SigninURL != "" {
|
|
signInURL = authErr.SigninURL
|
|
}
|
|
} else {
|
|
return badges, signInURLs
|
|
}
|
|
for _, opt := range options {
|
|
if opt.Cloud {
|
|
badges[opt.Name] = "Sign in required"
|
|
if signInURL != "" {
|
|
signInURLs[opt.Name] = signInURL
|
|
}
|
|
}
|
|
}
|
|
return badges, signInURLs
|
|
}
|
|
|
|
signedIn := user != nil && user.Name != ""
|
|
for _, opt := range options {
|
|
if !opt.Cloud {
|
|
continue
|
|
}
|
|
if !signedIn {
|
|
badges[opt.Name] = "Sign in required"
|
|
} else if opt.RequiredPlan != "" && !launch.PlanSatisfies(user.Plan, opt.RequiredPlan) {
|
|
badges[opt.Name] = "Upgrade required"
|
|
}
|
|
}
|
|
return badges, signInURLs
|
|
}
|
|
|
|
func agentRecommendationDescription(rec api.ModelRecommendation) string {
|
|
var parts []string
|
|
if description := strings.TrimSpace(rec.Description); description != "" {
|
|
parts = append(parts, description)
|
|
} else {
|
|
parts = append(parts, "cloud")
|
|
}
|
|
if rec.ContextLength > 0 {
|
|
parts = append(parts, format.HumanNumber(uint64(rec.ContextLength))+" ctx")
|
|
}
|
|
return strings.Join(parts, " - ")
|
|
}
|
|
|
|
func agentLocalModelDescription(model api.ListModelResponse) string {
|
|
desc := agentModelArchDescription(model)
|
|
if desc == "" {
|
|
return "local"
|
|
}
|
|
return "local - " + desc
|
|
}
|
|
|
|
func agentCloudModelDescription(model api.ListModelResponse) string {
|
|
return agentModelArchDescription(model)
|
|
}
|
|
|
|
func agentModelArchDescription(model api.ListModelResponse) string {
|
|
var details []string
|
|
if model.Details.Family != "" {
|
|
details = append(details, model.Details.Family)
|
|
}
|
|
if ps := humanizedParameterSize(model.Details.ParameterSize); ps != "" {
|
|
details = append(details, ps)
|
|
}
|
|
if model.Details.QuantizationLevel != "" {
|
|
details = append(details, model.Details.QuantizationLevel)
|
|
}
|
|
var parts []string
|
|
if len(details) > 0 {
|
|
parts = append(parts, strings.Join(details, " "))
|
|
}
|
|
if model.Details.ContextLength > 0 {
|
|
parts = append(parts, format.HumanNumber(uint64(model.Details.ContextLength))+" ctx")
|
|
}
|
|
return strings.Join(parts, " - ")
|
|
}
|
|
|
|
func humanizedParameterSize(s string) string {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
return ""
|
|
}
|
|
if f, err := strconv.ParseFloat(s, 64); err == nil {
|
|
return format.HumanNumber(uint64(f))
|
|
}
|
|
return s
|
|
}
|
|
|
|
func agentCloudStatusDisabled(ctx context.Context, client *api.Client) (disabled bool, known bool) {
|
|
if internalcloud.Disabled() {
|
|
return true, true
|
|
}
|
|
|
|
status, err := client.CloudStatusExperimental(ctx)
|
|
if err != nil {
|
|
var statusErr api.StatusError
|
|
if errors.As(err, &statusErr) && statusErr.StatusCode == http.StatusNotFound {
|
|
return false, false
|
|
}
|
|
return false, false
|
|
}
|
|
return status.Cloud.Disabled, true
|
|
}
|
|
|
|
func ensureCloudModelAccess(ctx context.Context, client *api.Client, modelName, requiredPlan string) error {
|
|
if client == nil {
|
|
return errors.New("no API client available")
|
|
}
|
|
if disabled, known := agentCloudStatusDisabled(ctx, client); known && disabled {
|
|
return errors.New("remote inference is unavailable")
|
|
}
|
|
user, err := client.Whoami(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if user != nil && user.Name != "" {
|
|
if requiredPlan != "" && !launch.PlanSatisfies(user.Plan, requiredPlan) {
|
|
return fmt.Errorf("plan upgrade required: %s needs plan %s, you have %s", modelName, requiredPlan, user.Plan)
|
|
}
|
|
return nil
|
|
}
|
|
return fmt.Errorf("%s requires sign in", modelName)
|
|
}
|