mirror of
https://github.com/ollama/ollama.git
synced 2026-07-23 09:10:53 -05:00
launch: oh-my-pi (#16410)
This commit is contained in:
@@ -65,6 +65,7 @@ func TestIntegrationLookup(t *testing.T) {
|
||||
{"kimi", "kimi", true, "Kimi Code CLI"},
|
||||
{"droid", "droid", true, "Droid"},
|
||||
{"opencode", "opencode", true, "OpenCode"},
|
||||
{"omp", "omp", true, "OMP"},
|
||||
{"pool", "pool", true, "Pool"},
|
||||
{"unknown integration", "unknown", false, ""},
|
||||
{"empty string", "", false, ""},
|
||||
@@ -84,7 +85,7 @@ func TestIntegrationLookup(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIntegrationRegistry(t *testing.T) {
|
||||
expectedIntegrations := []string{"claude", "claude-desktop", "cline", "codex", "codex-app", "kimi", "droid", "opencode", "hermes", "hermes-desktop", "pool"}
|
||||
expectedIntegrations := []string{"claude", "claude-desktop", "cline", "codex", "codex-app", "kimi", "droid", "opencode", "omp", "hermes", "hermes-desktop", "pool", "qwen"}
|
||||
for _, name := range expectedIntegrations {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
r, ok := integrations[name]
|
||||
@@ -1847,9 +1848,9 @@ func TestListIntegrationInfos(t *testing.T) {
|
||||
for _, info := range infos {
|
||||
got = append(got, info.Name)
|
||||
}
|
||||
wantPrefix := []string{"claude", "codex-app", "hermes", "openclaw", "opencode", "hermes-desktop", "codex"}
|
||||
wantPrefix := []string{"claude", "codex-app", "hermes", "openclaw", "opencode", "hermes-desktop", "codex", "copilot", "omp"}
|
||||
if codexAppSupported() != nil {
|
||||
wantPrefix = []string{"claude", "hermes", "openclaw", "opencode", "hermes-desktop", "codex"}
|
||||
wantPrefix = []string{"claude", "hermes", "openclaw", "opencode", "hermes-desktop", "codex", "copilot", "omp"}
|
||||
}
|
||||
if len(got) < len(wantPrefix) {
|
||||
t.Fatalf("expected at least %d integrations, got %v", len(wantPrefix), got)
|
||||
@@ -1871,7 +1872,7 @@ func TestListIntegrationInfos(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("includes known integrations", func(t *testing.T) {
|
||||
known := map[string]bool{"claude": false, "cline": false, "codex": false, "opencode": false}
|
||||
known := map[string]bool{"claude": false, "cline": false, "codex": false, "opencode": false, "omp": false}
|
||||
if codexAppSupported() == nil {
|
||||
known["codex-app"] = false
|
||||
}
|
||||
@@ -2006,6 +2007,7 @@ func TestIntegration_Editor(t *testing.T) {
|
||||
{"claude", false},
|
||||
{"claude-desktop", false},
|
||||
{"codex", false},
|
||||
{"omp", false},
|
||||
{"nonexistent", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -2037,6 +2039,7 @@ func TestIntegration_AutoInstallable(t *testing.T) {
|
||||
{"claude-desktop", false},
|
||||
{"codex", false},
|
||||
{"opencode", false},
|
||||
{"omp", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -294,6 +294,7 @@ Supported integrations:
|
||||
codex Codex
|
||||
hermes-desktop Hermes Desktop
|
||||
copilot Copilot CLI (aliases: copilot-cli)
|
||||
omp OMP
|
||||
droid Droid
|
||||
kimi Kimi Code CLI
|
||||
pi Pi
|
||||
|
||||
454
cmd/launch/omp.go
Normal file
454
cmd/launch/omp.go
Normal file
@@ -0,0 +1,454 @@
|
||||
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 ""
|
||||
}
|
||||
if !ompProviderHealthy(provider) {
|
||||
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) {
|
||||
dir, err := ompAgentDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(dir, "models.yml"), nil
|
||||
}
|
||||
|
||||
func ompConfigPath() (string, error) {
|
||||
dir, err := ompAgentDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(dir, "config.yml"), nil
|
||||
}
|
||||
|
||||
func ompAgentDir() (string, error) {
|
||||
if dir := strings.TrimSpace(os.Getenv("PI_CODING_AGENT_DIR")); dir != "" {
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
configDir := strings.TrimSpace(os.Getenv("PI_CONFIG_DIR"))
|
||||
if configDir == "" {
|
||||
configDir = ".omp"
|
||||
}
|
||||
if filepath.IsAbs(configDir) {
|
||||
return filepath.Join(configDir, "agent"), nil
|
||||
}
|
||||
return filepath.Join(home, configDir, "agent"), 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 ompProviderHealthy(provider map[string]any) bool {
|
||||
baseURL, _ := provider["baseUrl"].(string)
|
||||
if strings.TrimRight(baseURL, "/") != strings.TrimRight(ompBaseURL(), "/") {
|
||||
return false
|
||||
}
|
||||
api, _ := provider["api"].(string)
|
||||
if api != "openai-responses" {
|
||||
return false
|
||||
}
|
||||
auth, _ := provider["auth"].(string)
|
||||
if auth != "none" {
|
||||
return false
|
||||
}
|
||||
discovery, _ := provider["discovery"].(map[string]any)
|
||||
if discovery == nil {
|
||||
return false
|
||||
}
|
||||
discoveryType, _ := discovery["type"].(string)
|
||||
return discoveryType == "ollama"
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
687
cmd/launch/omp_test.go
Normal file
687
cmd/launch/omp_test.go
Normal file
@@ -0,0 +1,687 @@
|
||||
package launch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
modelpkg "github.com/ollama/ollama/types/model"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if os.Getenv("OLLAMA_LAUNCH_OMP_TEST_HELPER") == "1" {
|
||||
runOMPTestHelper()
|
||||
return
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func runOMPTestHelper() {
|
||||
logPath := os.Getenv("OLLAMA_LAUNCH_OMP_TEST_LOG")
|
||||
if logPath != "" {
|
||||
f, err := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
||||
if err == nil {
|
||||
_, _ = fmt.Fprintln(f, strings.Join(os.Args[1:], " "))
|
||||
_ = f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
if len(os.Args) >= 3 && os.Args[1] == "plugin" && os.Args[2] == "list" {
|
||||
fmt.Print(os.Getenv("OLLAMA_LAUNCH_OMP_TEST_PLUGIN_LIST"))
|
||||
os.Exit(0)
|
||||
}
|
||||
if len(os.Args) >= 4 && os.Args[1] == "plugin" && os.Args[2] == "install" {
|
||||
if os.Getenv("OLLAMA_LAUNCH_OMP_TEST_FAIL_INSTALL") == "1" {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "install failed")
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func setOMPTestHome(t *testing.T, dir string) {
|
||||
t.Helper()
|
||||
setTestHome(t, dir)
|
||||
t.Setenv("PI_CONFIG_DIR", "")
|
||||
t.Setenv("PI_CODING_AGENT_DIR", "")
|
||||
}
|
||||
|
||||
func TestOMPIntegration(t *testing.T) {
|
||||
o := &OMP{}
|
||||
|
||||
t.Run("String", func(t *testing.T) {
|
||||
if got := o.String(); got != "OMP" {
|
||||
t.Errorf("String() = %q, want %q", got, "OMP")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("implements Runner", func(t *testing.T) {
|
||||
var _ Runner = o
|
||||
})
|
||||
|
||||
t.Run("implements ManagedSingleModel", func(t *testing.T) {
|
||||
var _ ManagedSingleModel = o
|
||||
})
|
||||
|
||||
t.Run("implements ManagedModelListConfigurer", func(t *testing.T) {
|
||||
var _ ManagedModelListConfigurer = o
|
||||
})
|
||||
|
||||
t.Run("does not require interactive onboarding", func(t *testing.T) {
|
||||
var _ ManagedInteractiveOnboarding = o
|
||||
if o.RequiresInteractiveOnboarding() {
|
||||
t.Fatal("OMP onboarding should not require an interactive terminal")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestOMPArgs(t *testing.T) {
|
||||
o := &OMP{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
model string
|
||||
args []string
|
||||
want []string
|
||||
}{
|
||||
{"with model", "gemma4", nil, []string{"--model", "ollama/gemma4"}},
|
||||
{"with cloud model", "kimi-k2.6:cloud", nil, []string{"--model", "ollama/kimi-k2.6:cloud"}},
|
||||
{"empty model", "", nil, nil},
|
||||
{"with model and extra", "gemma4", []string{"--help"}, []string{"--model", "ollama/gemma4", "--help"}},
|
||||
{"already qualified", "ollama/gemma4", nil, []string{"--model", "ollama/gemma4"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := o.args(tt.model, tt.args)
|
||||
if !slices.Equal(got, tt.want) {
|
||||
t.Errorf("args(%q, %v) = %v, want %v", tt.model, tt.args, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOMPRun_WebSearchPluginLifecycle(t *testing.T) {
|
||||
seedOMPHelperBinary := func(t *testing.T, dir string) {
|
||||
t.Helper()
|
||||
src, err := os.Executable()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dst := filepath.Join(dir, ompExecutableNames()[0])
|
||||
if err := os.WriteFile(dst, data, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
setCloudStatus := func(t *testing.T, disabled bool) {
|
||||
t.Helper()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/api/status" {
|
||||
fmt.Fprintf(w, `{"cloud":{"disabled":%t,"source":"config"}}`, disabled)
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
t.Setenv("OLLAMA_HOST", srv.URL)
|
||||
}
|
||||
|
||||
setup := func(t *testing.T, pluginList string, cloudDisabled bool) (string, *OMP) {
|
||||
t.Helper()
|
||||
tmpDir := t.TempDir()
|
||||
setOMPTestHome(t, tmpDir)
|
||||
t.Setenv("PATH", tmpDir)
|
||||
t.Setenv("OLLAMA_LAUNCH_OMP_TEST_HELPER", "1")
|
||||
t.Setenv("OLLAMA_LAUNCH_OMP_TEST_PLUGIN_LIST", pluginList)
|
||||
logPath := filepath.Join(tmpDir, "omp.log")
|
||||
t.Setenv("OLLAMA_LAUNCH_OMP_TEST_LOG", logPath)
|
||||
setCloudStatus(t, cloudDisabled)
|
||||
seedOMPHelperBinary(t, tmpDir)
|
||||
return logPath, &OMP{}
|
||||
}
|
||||
|
||||
t.Run("web search missing installs before launch", func(t *testing.T) {
|
||||
logPath, o := setup(t, "No plugins installed\n", false)
|
||||
|
||||
if err := o.Run("kimi-k2.6:cloud", nil, []string{"session"}); err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
|
||||
calls, err := os.ReadFile(logPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := string(calls)
|
||||
if !strings.Contains(got, "plugin list\n") {
|
||||
t.Fatalf("expected plugin list call, got:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, "plugin install "+ompWebSearchPlugin+"\n") {
|
||||
t.Fatalf("expected plugin install call, got:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, "--model ollama/kimi-k2.6:cloud session\n") {
|
||||
t.Fatalf("expected final omp launch call, got:\n%s", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("web search present refreshes before launch", func(t *testing.T) {
|
||||
logPath, o := setup(t, "npm Plugins:\n\n● "+ompWebSearchPlugin+"@0.0.5\n", false)
|
||||
|
||||
if err := o.Run("gemma4", nil, []string{"chat"}); err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
|
||||
calls, err := os.ReadFile(logPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := string(calls)
|
||||
if !strings.Contains(got, "plugin install "+ompWebSearchPlugin+"\n") {
|
||||
t.Fatalf("expected plugin refresh install call, got:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, "--model ollama/gemma4 chat\n") {
|
||||
t.Fatalf("expected final omp launch call, got:\n%s", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("web search install failure warns and continues", func(t *testing.T) {
|
||||
logPath, o := setup(t, "No plugins installed\n", false)
|
||||
t.Setenv("OLLAMA_LAUNCH_OMP_TEST_FAIL_INSTALL", "1")
|
||||
|
||||
stderr := captureStderr(t, func() {
|
||||
if err := o.Run("gemma4", nil, []string{"chat"}); err != nil {
|
||||
t.Fatalf("Run() should continue after plugin install failure, got %v", err)
|
||||
}
|
||||
})
|
||||
if !strings.Contains(stderr, "Warning: could not install "+ompWebSearchPlugin) {
|
||||
t.Fatalf("expected install warning, got:\n%s", stderr)
|
||||
}
|
||||
|
||||
calls, err := os.ReadFile(logPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(calls), "--model ollama/gemma4 chat\n") {
|
||||
t.Fatalf("expected final omp launch call, got:\n%s", calls)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cloud disabled skips web search plugin management", func(t *testing.T) {
|
||||
logPath, o := setup(t, "No plugins installed\n", true)
|
||||
|
||||
stderr := captureStderr(t, func() {
|
||||
if err := o.Run("gemma4", nil, []string{"chat"}); err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
})
|
||||
if !strings.Contains(stderr, "Cloud is disabled; skipping "+ompWebSearchPlugin+" setup.") {
|
||||
t.Fatalf("expected cloud-disabled skip message, got:\n%s", stderr)
|
||||
}
|
||||
|
||||
calls, err := os.ReadFile(logPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := string(calls)
|
||||
if strings.Contains(got, "plugin list\n") || strings.Contains(got, "plugin install "+ompWebSearchPlugin+"\n") {
|
||||
t.Fatalf("did not expect plugin management calls, got:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, "--model ollama/gemma4 chat\n") {
|
||||
t.Fatalf("expected final omp launch call, got:\n%s", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestOMPFindPath(t *testing.T) {
|
||||
o := &OMP{}
|
||||
|
||||
t.Run("finds omp in PATH", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
name := "omp"
|
||||
if runtime.GOOS == "windows" {
|
||||
name = "omp.exe"
|
||||
}
|
||||
fakeBin := filepath.Join(tmpDir, name)
|
||||
os.WriteFile(fakeBin, []byte("#!/bin/sh\n"), 0o755)
|
||||
t.Setenv("PATH", tmpDir)
|
||||
|
||||
got, err := o.findPath()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != fakeBin {
|
||||
t.Errorf("findPath() = %q, want %q", got, fakeBin)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("falls back to ~/.local/bin/omp", func(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
setOMPTestHome(t, home)
|
||||
t.Setenv("PATH", t.TempDir())
|
||||
|
||||
fallback := filepath.Join(home, ".local", "bin", ompExecutableNames()[0])
|
||||
os.MkdirAll(filepath.Dir(fallback), 0o755)
|
||||
os.WriteFile(fallback, []byte("#!/bin/sh\n"), 0o755)
|
||||
|
||||
got, err := o.findPath()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != fallback {
|
||||
t.Errorf("findPath() = %q, want %q", got, fallback)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("falls back to ~/.bun/bin/omp", func(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
setOMPTestHome(t, home)
|
||||
t.Setenv("PATH", t.TempDir())
|
||||
|
||||
fallback := filepath.Join(home, ".bun", "bin", ompExecutableNames()[0])
|
||||
os.MkdirAll(filepath.Dir(fallback), 0o755)
|
||||
os.WriteFile(fallback, []byte("#!/bin/sh\n"), 0o755)
|
||||
|
||||
got, err := o.findPath()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != fallback {
|
||||
t.Errorf("findPath() = %q, want %q", got, fallback)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns error when not found", func(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
setOMPTestHome(t, home)
|
||||
t.Setenv("PATH", t.TempDir())
|
||||
|
||||
if _, err := o.findPath(); err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestOMPConfigureWithModelsWritesModelsYML(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
setOMPTestHome(t, home)
|
||||
t.Setenv("OLLAMA_HOST", "http://0.0.0.0:11434")
|
||||
|
||||
o := &OMP{}
|
||||
models := []LaunchModel{
|
||||
{
|
||||
Name: "glm-5.1:cloud",
|
||||
ContextLength: 202_752,
|
||||
MaxOutputTokens: 131_072,
|
||||
},
|
||||
{
|
||||
Name: "qwen3.6",
|
||||
Capabilities: []modelpkg.Capability{modelpkg.CapabilityVision},
|
||||
},
|
||||
}
|
||||
if err := o.ConfigureWithModels("glm-5.1:cloud", models); err != nil {
|
||||
t.Fatalf("ConfigureWithModels returned error: %v", err)
|
||||
}
|
||||
|
||||
path := filepath.Join(home, ".omp", "agent", "models.yml")
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read models.yml: %v", err)
|
||||
}
|
||||
|
||||
cfg := parseOMPConfigYAML(t, data)
|
||||
provider := ompProviderFromYAML(t, cfg)
|
||||
if provider["baseUrl"] != "http://127.0.0.1:11434/v1" {
|
||||
t.Fatalf("baseUrl = %v, want connectable OpenAI-compatible host", provider["baseUrl"])
|
||||
}
|
||||
if provider["api"] != "openai-responses" {
|
||||
t.Fatalf("api = %v, want openai-responses", provider["api"])
|
||||
}
|
||||
if provider["auth"] != "none" {
|
||||
t.Fatalf("auth = %v, want none", provider["auth"])
|
||||
}
|
||||
discovery, _ := provider["discovery"].(map[string]any)
|
||||
if discovery["type"] != "ollama" {
|
||||
t.Fatalf("discovery = %v, want type ollama", discovery)
|
||||
}
|
||||
|
||||
entries := ompModelEntriesFromYAML(t, provider)
|
||||
if len(entries) != 2 {
|
||||
t.Fatalf("models length = %d, want 2", len(entries))
|
||||
}
|
||||
if entries[0]["id"] != "glm-5.1:cloud" {
|
||||
t.Fatalf("first model id = %v, want primary first", entries[0]["id"])
|
||||
}
|
||||
if got := numericYAMLValue(entries[0]["contextWindow"]); got != 202_752 {
|
||||
t.Fatalf("contextWindow = %d, want 202752", got)
|
||||
}
|
||||
if got := numericYAMLValue(entries[0]["maxTokens"]); got != 131_072 {
|
||||
t.Fatalf("maxTokens = %d, want 131072", got)
|
||||
}
|
||||
if input := stringSliceYAMLValue(entries[1]["input"]); !slices.Equal(input, []string{"text", "image"}) {
|
||||
t.Fatalf("vision input = %v, want [text image]", input)
|
||||
}
|
||||
if got := o.CurrentModel(); got != "glm-5.1:cloud" {
|
||||
t.Fatalf("CurrentModel = %q, want glm-5.1:cloud", got)
|
||||
}
|
||||
|
||||
configPath := filepath.Join(home, ".omp", "agent", "config.yml")
|
||||
configData, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read config.yml: %v", err)
|
||||
}
|
||||
config := parseOMPConfigYAML(t, configData)
|
||||
if got := numericYAMLValue(config["setupVersion"]); got != ompSetupVersion {
|
||||
t.Fatalf("setupVersion = %d, want %d", got, ompSetupVersion)
|
||||
}
|
||||
if paths := o.Paths(); !slices.Equal(paths, []string{path, configPath}) {
|
||||
t.Fatalf("Paths = %v, want [%s %s]", paths, path, configPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOMPConfigureWithModelsPreservesExistingConfig(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
setOMPTestHome(t, home)
|
||||
|
||||
modelsPath := filepath.Join(home, ".omp", "agent", "models.yml")
|
||||
if err := os.MkdirAll(filepath.Dir(modelsPath), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
existing := []byte(`
|
||||
providers:
|
||||
anthropic:
|
||||
baseUrl: https://example.com/anthropic
|
||||
ollama:
|
||||
baseUrl: http://old-host:11434
|
||||
api: openai-responses
|
||||
auth: none
|
||||
models:
|
||||
- id: old-model
|
||||
name: Old Model
|
||||
customField: keep-me
|
||||
`)
|
||||
if err := os.WriteFile(modelsPath, existing, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
configPath := filepath.Join(home, ".omp", "agent", "config.yml")
|
||||
existingConfig := []byte(`
|
||||
lastChangelogVersion: 15.7.6
|
||||
setupVersion: 0
|
||||
theme: monochrome
|
||||
`)
|
||||
if err := os.WriteFile(configPath, existingConfig, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
o := &OMP{}
|
||||
if err := o.ConfigureWithModels("new-model", []LaunchModel{{Name: "new-model"}, {Name: "old-model"}}); err != nil {
|
||||
t.Fatalf("ConfigureWithModels returned error: %v", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(modelsPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg := parseOMPConfigYAML(t, data)
|
||||
providers, _ := cfg["providers"].(map[string]any)
|
||||
if _, ok := providers["anthropic"]; !ok {
|
||||
t.Fatalf("expected non-Ollama provider to be preserved: %v", providers)
|
||||
}
|
||||
|
||||
provider := ompProviderFromYAML(t, cfg)
|
||||
if provider["baseUrl"] != "http://127.0.0.1:11434/v1" {
|
||||
t.Fatalf("baseUrl = %v, want repaired OpenAI-compatible host", provider["baseUrl"])
|
||||
}
|
||||
|
||||
entries := ompModelEntriesFromYAML(t, provider)
|
||||
if len(entries) != 2 {
|
||||
t.Fatalf("models length = %d, want 2", len(entries))
|
||||
}
|
||||
if entries[0]["id"] != "new-model" {
|
||||
t.Fatalf("first model id = %v, want new-model", entries[0]["id"])
|
||||
}
|
||||
if entries[1]["id"] != "old-model" {
|
||||
t.Fatalf("second model id = %v, want old-model", entries[1]["id"])
|
||||
}
|
||||
if entries[1]["customField"] != "keep-me" {
|
||||
t.Fatalf("custom field was not preserved: %v", entries[1])
|
||||
}
|
||||
|
||||
configData, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config := parseOMPConfigYAML(t, configData)
|
||||
if got := numericYAMLValue(config["setupVersion"]); got != ompSetupVersion {
|
||||
t.Fatalf("setupVersion = %d, want %d", got, ompSetupVersion)
|
||||
}
|
||||
if config["theme"] != "monochrome" {
|
||||
t.Fatalf("theme was not preserved: %v", config)
|
||||
}
|
||||
if config["lastChangelogVersion"] != "15.7.6" {
|
||||
t.Fatalf("lastChangelogVersion was not preserved: %v", config)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOMPConfigureWithModelsAlwaysMarksSetupComplete(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
setOMPTestHome(t, home)
|
||||
|
||||
configPath := filepath.Join(home, ".omp", "agent", "config.yml")
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(configPath, []byte("setupVersion: 2\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
o := &OMP{}
|
||||
if err := o.ConfigureWithModels("new-model", []LaunchModel{{Name: "new-model"}}); err != nil {
|
||||
t.Fatalf("ConfigureWithModels returned error: %v", err)
|
||||
}
|
||||
|
||||
configData, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config := parseOMPConfigYAML(t, configData)
|
||||
if got := numericYAMLValue(config["setupVersion"]); got != ompSetupVersion {
|
||||
t.Fatalf("setupVersion = %d, want %d", got, ompSetupVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOMPConfigureWithModelsRespectsPiConfigDir(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
setOMPTestHome(t, home)
|
||||
t.Setenv("PI_CONFIG_DIR", ".custom-omp")
|
||||
|
||||
o := &OMP{}
|
||||
if err := o.ConfigureWithModels("new-model", []LaunchModel{{Name: "new-model"}}); err != nil {
|
||||
t.Fatalf("ConfigureWithModels returned error: %v", err)
|
||||
}
|
||||
|
||||
modelsPath := filepath.Join(home, ".custom-omp", "agent", "models.yml")
|
||||
configPath := filepath.Join(home, ".custom-omp", "agent", "config.yml")
|
||||
for _, path := range []string{modelsPath, configPath} {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Fatalf("expected %s to be written: %v", path, err)
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(home, ".omp", "agent", "models.yml")); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected default OMP models path to be untouched, got err %v", err)
|
||||
}
|
||||
if paths := o.Paths(); !slices.Equal(paths, []string{modelsPath, configPath}) {
|
||||
t.Fatalf("Paths = %v, want [%s %s]", paths, modelsPath, configPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOMPConfigureWithModelsRespectsPiCodingAgentDir(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
setOMPTestHome(t, home)
|
||||
agentDir := filepath.Join(home, "agent-override")
|
||||
t.Setenv("PI_CONFIG_DIR", ".ignored-omp")
|
||||
t.Setenv("PI_CODING_AGENT_DIR", agentDir)
|
||||
|
||||
o := &OMP{}
|
||||
if err := o.ConfigureWithModels("new-model", []LaunchModel{{Name: "new-model"}}); err != nil {
|
||||
t.Fatalf("ConfigureWithModels returned error: %v", err)
|
||||
}
|
||||
|
||||
modelsPath := filepath.Join(agentDir, "models.yml")
|
||||
configPath := filepath.Join(agentDir, "config.yml")
|
||||
for _, path := range []string{modelsPath, configPath} {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Fatalf("expected %s to be written: %v", path, err)
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(home, ".ignored-omp", "agent", "models.yml")); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected PI_CONFIG_DIR path to be ignored when PI_CODING_AGENT_DIR is set, got err %v", err)
|
||||
}
|
||||
if got := o.CurrentModel(); got != "new-model" {
|
||||
t.Fatalf("CurrentModel = %q, want new-model", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOMPCurrentModelRequiresHealthyProvider(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
setOMPTestHome(t, home)
|
||||
t.Setenv("OLLAMA_HOST", "http://127.0.0.1:11434")
|
||||
|
||||
modelsPath := filepath.Join(home, ".omp", "agent", "models.yml")
|
||||
if err := os.MkdirAll(filepath.Dir(modelsPath), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
provider string
|
||||
}{
|
||||
{
|
||||
name: "wrong base url",
|
||||
provider: "" +
|
||||
" baseUrl: http://127.0.0.1:9999/v1\n" +
|
||||
" api: openai-responses\n" +
|
||||
" auth: none\n" +
|
||||
" discovery:\n" +
|
||||
" type: ollama\n",
|
||||
},
|
||||
{
|
||||
name: "wrong api",
|
||||
provider: "" +
|
||||
" baseUrl: http://127.0.0.1:11434/v1\n" +
|
||||
" api: openai-chat\n" +
|
||||
" auth: none\n" +
|
||||
" discovery:\n" +
|
||||
" type: ollama\n",
|
||||
},
|
||||
{
|
||||
name: "wrong auth",
|
||||
provider: "" +
|
||||
" baseUrl: http://127.0.0.1:11434/v1\n" +
|
||||
" api: openai-responses\n" +
|
||||
" auth: api-key\n" +
|
||||
" discovery:\n" +
|
||||
" type: ollama\n",
|
||||
},
|
||||
{
|
||||
name: "wrong discovery",
|
||||
provider: "" +
|
||||
" baseUrl: http://127.0.0.1:11434/v1\n" +
|
||||
" api: openai-responses\n" +
|
||||
" auth: none\n" +
|
||||
" discovery:\n" +
|
||||
" type: static\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := "providers:\n" +
|
||||
" ollama:\n" +
|
||||
tt.provider +
|
||||
" models:\n" +
|
||||
" - id: gemma4\n"
|
||||
if err := os.WriteFile(modelsPath, []byte(cfg), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := (&OMP{}).CurrentModel(); got != "" {
|
||||
t.Fatalf("expected stale config to return empty current model, got %q", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func parseOMPConfigYAML(t *testing.T, data []byte) map[string]any {
|
||||
t.Helper()
|
||||
var cfg map[string]any
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
t.Fatalf("generated YAML did not parse: %v\n%s", err, data)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func ompProviderFromYAML(t *testing.T, cfg map[string]any) map[string]any {
|
||||
t.Helper()
|
||||
providers, ok := cfg["providers"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("providers missing from config: %v", cfg)
|
||||
}
|
||||
provider, ok := providers["ollama"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("ollama provider missing from config: %v", providers)
|
||||
}
|
||||
return provider
|
||||
}
|
||||
|
||||
func ompModelEntriesFromYAML(t *testing.T, provider map[string]any) []map[string]any {
|
||||
t.Helper()
|
||||
rawModels, ok := provider["models"].([]any)
|
||||
if !ok {
|
||||
t.Fatalf("provider models missing: %v", provider)
|
||||
}
|
||||
models := make([]map[string]any, 0, len(rawModels))
|
||||
for _, raw := range rawModels {
|
||||
entry, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("model entry has unexpected type %T: %v", raw, raw)
|
||||
}
|
||||
models = append(models, entry)
|
||||
}
|
||||
return models
|
||||
}
|
||||
|
||||
func numericYAMLValue(value any) int {
|
||||
switch v := value.(type) {
|
||||
case int:
|
||||
return v
|
||||
case int64:
|
||||
return int(v)
|
||||
case float64:
|
||||
return int(v)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func stringSliceYAMLValue(value any) []string {
|
||||
raw, _ := value.([]any)
|
||||
out := make([]string, 0, len(raw))
|
||||
for _, item := range raw {
|
||||
if s, ok := item.(string); ok {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -351,7 +351,7 @@ func npmArgs(prefix string, args ...string) []string {
|
||||
}
|
||||
|
||||
func ensurePiWebSearchPackage(bin string) {
|
||||
if !shouldManagePiWebSearch() {
|
||||
if !shouldManageOllamaWebSearch() {
|
||||
fmt.Fprintf(os.Stderr, "%sCloud is disabled; skipping %s setup.%s\n", ansiGray, piWebSearchPkg, ansiReset)
|
||||
return
|
||||
}
|
||||
@@ -395,7 +395,7 @@ func ensurePiWebSearchPackage(bin string) {
|
||||
fmt.Fprintf(os.Stderr, "%s ✓ Updated %s%s\n", ansiGreen, piWebSearchPkg, ansiReset)
|
||||
}
|
||||
|
||||
func shouldManagePiWebSearch() bool {
|
||||
func shouldManageOllamaWebSearch() bool {
|
||||
client, err := api.ClientFromEnvironment()
|
||||
if err != nil {
|
||||
return true
|
||||
|
||||
@@ -33,7 +33,7 @@ type IntegrationInfo struct {
|
||||
Description string
|
||||
}
|
||||
|
||||
var launcherIntegrationOrder = []string{"claude", "codex-app", "hermes", "openclaw", "opencode", "hermes-desktop", "codex", "copilot", "cline", "droid", "pi", "pool", "qwen"}
|
||||
var launcherIntegrationOrder = []string{"claude", "codex-app", "hermes", "openclaw", "opencode", "hermes-desktop", "codex", "copilot", "omp", "cline", "droid", "pi", "pool", "qwen"}
|
||||
|
||||
var integrationSpecs = []*IntegrationSpec{
|
||||
{
|
||||
@@ -156,6 +156,18 @@ var integrationSpecs = []*IntegrationSpec{
|
||||
URL: "https://opencode.ai",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "omp",
|
||||
Runner: &OMP{},
|
||||
Description: "AI coding agent with IDE integration",
|
||||
Install: IntegrationInstallSpec{
|
||||
CheckInstalled: func() bool {
|
||||
_, err := (&OMP{}).findPath()
|
||||
return err == nil
|
||||
},
|
||||
URL: "https://omp.sh",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "openclaw",
|
||||
Runner: &Openclaw{},
|
||||
|
||||
@@ -61,6 +61,14 @@ func TestEditorRunsDoNotRewriteConfig(t *testing.T) {
|
||||
return filepath.Join(home, ".kimi", "config.toml")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "omp",
|
||||
binary: "omp",
|
||||
runner: &OMP{},
|
||||
checkPath: func(home string) string {
|
||||
return filepath.Join(home, ".omp", "agent", "models.yml")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
Reference in New Issue
Block a user