mirror of
https://github.com/ollama/ollama.git
synced 2026-06-03 05:53:55 -04:00
Add omp (oh my pi) as a launch integration. This an opinionated extension of pi. It is a coding agent.
412 lines
9.5 KiB
Go
412 lines
9.5 KiB
Go
package launch
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/ollama/ollama/cmd/config"
|
|
"github.com/ollama/ollama/cmd/internal/fileutil"
|
|
"github.com/ollama/ollama/envconfig"
|
|
"github.com/ollama/ollama/types/model"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
const (
|
|
ompIntegrationName = "omp"
|
|
ompProviderName = "ollama"
|
|
ompSetupVersion = 1
|
|
ompWebSearchPlugin = "@ollama/pi-web-search"
|
|
)
|
|
|
|
// OMP implements Runner for the OMP coding-agent integration.
|
|
type OMP struct{}
|
|
|
|
func (o *OMP) String() string { return "OMP" }
|
|
|
|
func (o *OMP) Paths() []string {
|
|
var paths []string
|
|
for _, pathFn := range []func() (string, error){ompModelsPath, ompConfigPath} {
|
|
path, err := pathFn()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if _, err := os.Stat(path); err == nil {
|
|
paths = append(paths, path)
|
|
}
|
|
}
|
|
return paths
|
|
}
|
|
|
|
func (o *OMP) Configure(model string) error {
|
|
return o.ConfigureWithModels(model, []LaunchModel{fallbackLaunchModel(model)})
|
|
}
|
|
|
|
func (o *OMP) ConfigureWithModels(primary string, models []LaunchModel) error {
|
|
if primary == "" {
|
|
return nil
|
|
}
|
|
if len(models) == 0 {
|
|
models = []LaunchModel{fallbackLaunchModel(primary)}
|
|
}
|
|
if err := writeOMPModelsConfig(primary, models); err != nil {
|
|
return err
|
|
}
|
|
return writeOMPAgentConfig()
|
|
}
|
|
|
|
func (o *OMP) CurrentModel() string {
|
|
cfg, err := readOMPModelsConfig()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
provider, ok := ompProvider(cfg)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
models, _ := provider["models"].([]any)
|
|
for _, raw := range models {
|
|
entry, ok := raw.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if id, _ := entry["id"].(string); id != "" {
|
|
return id
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (o *OMP) Onboard() error {
|
|
return config.MarkIntegrationOnboarded(ompIntegrationName)
|
|
}
|
|
|
|
func (o *OMP) RequiresInteractiveOnboarding() bool { return false }
|
|
|
|
func (o *OMP) args(model string, extra []string) []string {
|
|
var args []string
|
|
if model != "" {
|
|
args = append(args, "--model", ompModelName(model))
|
|
}
|
|
args = append(args, extra...)
|
|
return args
|
|
}
|
|
|
|
func ompModelName(model string) string {
|
|
if strings.HasPrefix(model, "ollama/") {
|
|
return model
|
|
}
|
|
return "ollama/" + model
|
|
}
|
|
|
|
func (o *OMP) findPath() (string, error) {
|
|
if p, err := exec.LookPath("omp"); err == nil {
|
|
return p, nil
|
|
}
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for _, dir := range []string{
|
|
filepath.Join(home, ".local", "bin"),
|
|
filepath.Join(home, ".bun", "bin"),
|
|
} {
|
|
for _, name := range ompExecutableNames() {
|
|
fallback := filepath.Join(dir, name)
|
|
if _, err := os.Stat(fallback); err == nil {
|
|
return fallback, nil
|
|
}
|
|
}
|
|
}
|
|
return "", exec.ErrNotFound
|
|
}
|
|
|
|
func ompExecutableNames() []string {
|
|
if runtime.GOOS == "windows" {
|
|
return []string{"omp.exe", "omp.cmd", "omp.bat"}
|
|
}
|
|
return []string{"omp"}
|
|
}
|
|
|
|
func (o *OMP) Run(model string, _ []LaunchModel, args []string) error {
|
|
ompPath, err := o.findPath()
|
|
if err != nil {
|
|
return fmt.Errorf("omp is not installed, install from https://omp.sh")
|
|
}
|
|
|
|
ensureOMPWebSearchPlugin(ompPath)
|
|
|
|
cmd := exec.Command(ompPath, o.args(model, args)...)
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
cmd.Env = os.Environ()
|
|
return cmd.Run()
|
|
}
|
|
|
|
func ensureOMPWebSearchPlugin(bin string) {
|
|
if !shouldManageOllamaWebSearch() {
|
|
fmt.Fprintf(os.Stderr, "%sCloud is disabled; skipping %s setup.%s\n", ansiGray, ompWebSearchPlugin, ansiReset)
|
|
return
|
|
}
|
|
|
|
fmt.Fprintf(os.Stderr, "%sChecking OMP web search plugin...%s\n", ansiGray, ansiReset)
|
|
|
|
installed, err := ompPluginInstalled(bin, ompWebSearchPlugin)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "%s Warning: could not check %s installation: %v%s\n", ansiYellow, ompWebSearchPlugin, err, ansiReset)
|
|
return
|
|
}
|
|
|
|
verb := "Installing"
|
|
warnVerb := "install"
|
|
doneVerb := "Installed"
|
|
if installed {
|
|
verb = "Updating"
|
|
warnVerb = "update"
|
|
doneVerb = "Updated"
|
|
}
|
|
|
|
fmt.Fprintf(os.Stderr, "%s%s %s...%s\n", ansiGray, verb, ompWebSearchPlugin, ansiReset)
|
|
cmd := exec.Command(bin, "plugin", "install", ompWebSearchPlugin)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Run(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "%s Warning: could not %s %s: %v%s\n", ansiYellow, warnVerb, ompWebSearchPlugin, err, ansiReset)
|
|
return
|
|
}
|
|
|
|
fmt.Fprintf(os.Stderr, "%s ✓ %s %s%s\n", ansiGreen, doneVerb, ompWebSearchPlugin, ansiReset)
|
|
}
|
|
|
|
func ompPluginInstalled(bin, plugin string) (bool, error) {
|
|
cmd := exec.Command(bin, "plugin", "list")
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
msg := strings.TrimSpace(string(out))
|
|
if msg == "" {
|
|
return false, err
|
|
}
|
|
return false, fmt.Errorf("%w: %s", err, msg)
|
|
}
|
|
|
|
versioned := plugin + "@"
|
|
for _, line := range strings.Split(string(out), "\n") {
|
|
trimmed := strings.TrimSpace(line)
|
|
if strings.Contains(trimmed, versioned) || trimmed == plugin {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func ompModelsPath() (string, error) {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(home, ".omp", "agent", "models.yml"), nil
|
|
}
|
|
|
|
func ompConfigPath() (string, error) {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(home, ".omp", "agent", "config.yml"), nil
|
|
}
|
|
|
|
func readOMPModelsConfig() (map[string]any, error) {
|
|
path, err := ompModelsPath()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var cfg map[string]any
|
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
if cfg == nil {
|
|
cfg = make(map[string]any)
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func writeOMPModelsConfig(primary string, models []LaunchModel) error {
|
|
path, err := ompModelsPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return err
|
|
}
|
|
|
|
cfg := make(map[string]any)
|
|
if existing, err := readOMPModelsConfig(); err == nil {
|
|
cfg = existing
|
|
}
|
|
|
|
provider := ensureOMPProvider(cfg)
|
|
existingByID := ompModelEntriesByID(provider)
|
|
ordered := append([]LaunchModel(nil), models...)
|
|
if model, ok := findLaunchModel(ordered, primary); ok {
|
|
ordered = append([]LaunchModel{model}, removeLaunchModel(ordered, primary)...)
|
|
} else {
|
|
ordered = append([]LaunchModel{fallbackLaunchModel(primary)}, ordered...)
|
|
}
|
|
|
|
var merged []any
|
|
seen := make(map[string]bool, len(ordered))
|
|
for _, model := range ordered {
|
|
if model.Name == "" || seen[model.Name] {
|
|
continue
|
|
}
|
|
seen[model.Name] = true
|
|
entry := ompModelConfig(model)
|
|
if existing, ok := existingByID[model.Name]; ok {
|
|
for key, value := range existing {
|
|
if _, overridden := entry[key]; !overridden {
|
|
entry[key] = value
|
|
}
|
|
}
|
|
}
|
|
merged = append(merged, entry)
|
|
}
|
|
|
|
for _, raw := range ompProviderModels(provider) {
|
|
entry, ok := raw.(map[string]any)
|
|
if !ok {
|
|
merged = append(merged, raw)
|
|
continue
|
|
}
|
|
id, _ := entry["id"].(string)
|
|
if id == "" || seen[id] {
|
|
continue
|
|
}
|
|
merged = append(merged, entry)
|
|
}
|
|
provider["models"] = merged
|
|
|
|
data, err := yaml.Marshal(cfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return fileutil.WriteWithBackup(path, data, ompIntegrationName)
|
|
}
|
|
|
|
func writeOMPAgentConfig() error {
|
|
path, err := ompConfigPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return err
|
|
}
|
|
|
|
cfg := make(map[string]any)
|
|
if data, err := os.ReadFile(path); err == nil {
|
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
|
return err
|
|
}
|
|
if cfg == nil {
|
|
cfg = make(map[string]any)
|
|
}
|
|
}
|
|
cfg["setupVersion"] = ompSetupVersion
|
|
|
|
data, err := yaml.Marshal(cfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return fileutil.WriteWithBackup(path, data, ompIntegrationName)
|
|
}
|
|
|
|
func ensureOMPProvider(cfg map[string]any) map[string]any {
|
|
providers, _ := cfg["providers"].(map[string]any)
|
|
if providers == nil {
|
|
providers = make(map[string]any)
|
|
cfg["providers"] = providers
|
|
}
|
|
provider, _ := providers[ompProviderName].(map[string]any)
|
|
if provider == nil {
|
|
provider = make(map[string]any)
|
|
providers[ompProviderName] = provider
|
|
}
|
|
|
|
provider["baseUrl"] = ompBaseURL()
|
|
provider["api"] = "openai-responses"
|
|
provider["auth"] = "none"
|
|
provider["discovery"] = map[string]any{"type": "ollama"}
|
|
return provider
|
|
}
|
|
|
|
func ompBaseURL() string {
|
|
return strings.TrimRight(envconfig.ConnectableHost().String(), "/") + "/v1"
|
|
}
|
|
|
|
func ompProvider(cfg map[string]any) (map[string]any, bool) {
|
|
providers, ok := cfg["providers"].(map[string]any)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
provider, ok := providers[ompProviderName].(map[string]any)
|
|
return provider, ok
|
|
}
|
|
|
|
func ompProviderModels(provider map[string]any) []any {
|
|
models, _ := provider["models"].([]any)
|
|
return models
|
|
}
|
|
|
|
func ompModelEntriesByID(provider map[string]any) map[string]map[string]any {
|
|
out := make(map[string]map[string]any)
|
|
for _, raw := range ompProviderModels(provider) {
|
|
entry, ok := raw.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if id, _ := entry["id"].(string); id != "" {
|
|
out[id] = entry
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func ompModelConfig(modelInfo LaunchModel) map[string]any {
|
|
entry := map[string]any{
|
|
"id": modelInfo.Name,
|
|
"name": modelInfo.Name,
|
|
}
|
|
input := []string{"text"}
|
|
if slices.Contains(modelInfo.Capabilities, model.CapabilityVision) {
|
|
input = append(input, "image")
|
|
}
|
|
entry["input"] = input
|
|
|
|
if modelInfo.ContextLength > 0 {
|
|
entry["contextWindow"] = modelInfo.ContextLength
|
|
}
|
|
if modelInfo.MaxOutputTokens > 0 {
|
|
entry["maxTokens"] = modelInfo.MaxOutputTokens
|
|
}
|
|
return entry
|
|
}
|
|
|
|
func removeLaunchModel(models []LaunchModel, name string) []LaunchModel {
|
|
out := make([]LaunchModel, 0, len(models))
|
|
for _, model := range models {
|
|
if launchModelMatches(model.Name, name) || launchModelMatches(name, model.Name) {
|
|
continue
|
|
}
|
|
out = append(out, model)
|
|
}
|
|
return out
|
|
}
|