fix(ollama): discover cloud models (#217)
- commit
- 076e2bbda00480443b8aa0671ffa3b6580c21baf
- author
- bnema · · 2026-09-19 13:28 UTC
- committer
- GitHub · · 2026-09-19 13:28 UTC
- parent
- d9fd43a
- signature
- GPG Unverified
B5690EEEBB952194The key is unknown - refs
- tag: v0.40.0
5 files · +141 -2
M packages/agent/build.go +1 -1
| @@ -833,7 +833,7 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||
| 833 | 833 | // Explicit --base-url and model-level baseUrl (applied above) | |
| 834 | 834 | // both win over this default. | |
| 835 | 835 | if hasRealOllamaCred { | |
| 836 | - | args.BaseURL = "https://ollama.com/v1" | |
| 836 | + | args.BaseURL = provider.OllamaCloudBaseURL | |
| 837 | 837 | } else { | |
| 838 | 838 | args.BaseURL = "http://localhost:11434" | |
| 839 | 839 | } | |
| @@ -833,7 +833,7 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||||
| 833 | // Explicit --base-url and model-level baseUrl (applied above) | 833 | // Explicit --base-url and model-level baseUrl (applied above) | ||
| 834 | // both win over this default. | 834 | // both win over this default. | ||
| 835 | if hasRealOllamaCred { | 835 | if hasRealOllamaCred { | ||
| 836 | - | args.BaseURL = "https://ollama.com/v1" | 836 | + | args.BaseURL = provider.OllamaCloudBaseURL |
| 837 | } else { | 837 | } else { | ||
| 838 | args.BaseURL = "http://localhost:11434" | 838 | args.BaseURL = "http://localhost:11434" | ||
| 839 | } | 839 | } | ||
M packages/agent/modelsync.go +27 -1
| @@ -16,6 +16,7 @@ import ( | |||
| 16 | 16 | ||
| 17 | 17 | var ( | |
| 18 | 18 | discoverOpenCodeGoFn = provider.DiscoverOpenCodeGo | |
| 19 | + | discoverOllamaFn = provider.DiscoverOllama | |
| 19 | 20 | modelCatalogMu sync.Mutex | |
| 20 | 21 | ) | |
| 21 | 22 | ||
| @@ -115,6 +116,9 @@ func currentModelProviderScopesForBaseURL(openCodeGoBaseURL string) map[string]s | |||
| 115 | 116 | if key, method, _, err := resolveCredentialFull(context.Background(), provider.ProviderOpenCodeGo, "", apiKeyCommandSkip); err == nil && method == "apikey" && key != "" { | |
| 116 | 117 | scopes[provider.ProviderOpenCodeGo] = credentialScopeForEndpoint(key, openCodeGoBaseURL) | |
| 117 | 118 | } | |
| 119 | + | if key, method, _, err := resolveCredentialFull(context.Background(), provider.ProviderOllama, "", apiKeyCommandSkip); err == nil && method == "apikey" && key != "" { | |
| 120 | + | scopes[provider.ProviderOllama] = credentialScopeForEndpoint(key, provider.OllamaCloudBaseURL) | |
| 121 | + | } | |
| 118 | 122 | return scopes | |
| 119 | 123 | } | |
| 120 | 124 | ||
| @@ -140,7 +144,10 @@ func modelProviderScopes(explicitProvider, explicitAPIKey, explicitBaseURL strin | |||
| 140 | 144 | } | |
| 141 | 145 | scopes := currentModelProviderScopesForBaseURL(openCodeGoBaseURL) | |
| 142 | 146 | if explicitProvider == provider.ProviderOpenCodeGo && explicitAPIKey != "" { | |
| 143 | - | scopes[provider.ProviderOpenCodeGo] = credentialScopeForEndpoint(explicitAPIKey, explicitBaseURL) | |
| 147 | + | scopes[provider.ProviderOpenCodeGo] = credentialScopeForEndpoint(explicitAPIKey, firstNonEmpty(explicitBaseURL, provider.OpenCodeGoDefaultBaseURL)) | |
| 148 | + | } | |
| 149 | + | if explicitProvider == provider.ProviderOllama && explicitAPIKey != "" { | |
| 150 | + | scopes[provider.ProviderOllama] = credentialScopeForEndpoint(explicitAPIKey, firstNonEmpty(explicitBaseURL, provider.OllamaCloudBaseURL)) | |
| 144 | 151 | } | |
| 145 | 152 | return scopes | |
| 146 | 153 | } | |
| @@ -498,6 +505,25 @@ func refreshModelsWithContext(parent context.Context, explicitProvider, explicit | |||
| 498 | 505 | } | |
| 499 | 506 | } | |
| 500 | 507 | } | |
| 508 | + | if onlyProvider == "" { | |
| 509 | + | var ollamaKey string | |
| 510 | + | if explicitProvider == provider.ProviderOllama && explicitAPIKey != "" { | |
| 511 | + | ollamaKey = explicitAPIKey | |
| 512 | + | } else if cred, method, err := resolveCredentialForCatalog(ctx, provider.ProviderOllama, commandMode); err == nil && method == "apikey" { | |
| 513 | + | ollamaKey = cred | |
| 514 | + | } | |
| 515 | + | if ollamaKey != "" { | |
| 516 | + | markEligible(provider.ProviderOllama) | |
| 517 | + | baseURL := provider.OllamaCloudBaseURL | |
| 518 | + | if explicitProvider == provider.ProviderOllama && explicitBaseURL != "" { | |
| 519 | + | baseURL = explicitBaseURL | |
| 520 | + | } | |
| 521 | + | if live, err := discoverOllamaFn(ctx, ollamaKey, baseURL); err == nil { | |
| 522 | + | scope := credentialScopeForEndpoint(ollamaKey, baseURL) | |
| 523 | + | recordDiscovery(provider.ProviderOllama, live, false, scope) | |
| 524 | + | } | |
| 525 | + | } | |
| 526 | + | } | |
| 501 | 527 | if onlyProvider == "" || onlyProvider == provider.ProviderOpenAICodex { | |
| 502 | 528 | if cred, method, accountID, err := resolveCredentialFull(ctx, provider.ProviderOpenAICodex, "", commandMode); err == nil && method == "oauth" { | |
| 503 | 529 | markEligible(provider.ProviderOpenAICodex) | |
| @@ -16,6 +16,7 @@ import ( | |||||
| 16 | 16 | ||||
| 17 | var ( | 17 | var ( | ||
| 18 | discoverOpenCodeGoFn = provider.DiscoverOpenCodeGo | 18 | discoverOpenCodeGoFn = provider.DiscoverOpenCodeGo | ||
| 19 | + | discoverOllamaFn = provider.DiscoverOllama | |||
| 19 | modelCatalogMu sync.Mutex | 20 | modelCatalogMu sync.Mutex | ||
| 20 | ) | 21 | ) | ||
| 21 | 22 | ||||
| @@ -115,6 +116,9 @@ func currentModelProviderScopesForBaseURL(openCodeGoBaseURL string) map[string]s | |||||
| 115 | if key, method, _, err := resolveCredentialFull(context.Background(), provider.ProviderOpenCodeGo, "", apiKeyCommandSkip); err == nil && method == "apikey" && key != "" { | 116 | if key, method, _, err := resolveCredentialFull(context.Background(), provider.ProviderOpenCodeGo, "", apiKeyCommandSkip); err == nil && method == "apikey" && key != "" { | ||
| 116 | scopes[provider.ProviderOpenCodeGo] = credentialScopeForEndpoint(key, openCodeGoBaseURL) | 117 | scopes[provider.ProviderOpenCodeGo] = credentialScopeForEndpoint(key, openCodeGoBaseURL) | ||
| 117 | } | 118 | } | ||
| 119 | + | if key, method, _, err := resolveCredentialFull(context.Background(), provider.ProviderOllama, "", apiKeyCommandSkip); err == nil && method == "apikey" && key != "" { | |||
| 120 | + | scopes[provider.ProviderOllama] = credentialScopeForEndpoint(key, provider.OllamaCloudBaseURL) | |||
| 121 | + | } | |||
| 118 | return scopes | 122 | return scopes | ||
| 119 | } | 123 | } | ||
| 120 | 124 | ||||
| @@ -140,7 +144,10 @@ func modelProviderScopes(explicitProvider, explicitAPIKey, explicitBaseURL strin | |||||
| 140 | } | 144 | } | ||
| 141 | scopes := currentModelProviderScopesForBaseURL(openCodeGoBaseURL) | 145 | scopes := currentModelProviderScopesForBaseURL(openCodeGoBaseURL) | ||
| 142 | if explicitProvider == provider.ProviderOpenCodeGo && explicitAPIKey != "" { | 146 | if explicitProvider == provider.ProviderOpenCodeGo && explicitAPIKey != "" { | ||
| 143 | - | scopes[provider.ProviderOpenCodeGo] = credentialScopeForEndpoint(explicitAPIKey, explicitBaseURL) | 147 | + | scopes[provider.ProviderOpenCodeGo] = credentialScopeForEndpoint(explicitAPIKey, firstNonEmpty(explicitBaseURL, provider.OpenCodeGoDefaultBaseURL)) |
| 148 | + | } | |||
| 149 | + | if explicitProvider == provider.ProviderOllama && explicitAPIKey != "" { | |||
| 150 | + | scopes[provider.ProviderOllama] = credentialScopeForEndpoint(explicitAPIKey, firstNonEmpty(explicitBaseURL, provider.OllamaCloudBaseURL)) | |||
| 144 | } | 151 | } | ||
| 145 | return scopes | 152 | return scopes | ||
| 146 | } | 153 | } | ||
| @@ -498,6 +505,25 @@ func refreshModelsWithContext(parent context.Context, explicitProvider, explicit | |||||
| 498 | } | 505 | } | ||
| 499 | } | 506 | } | ||
| 500 | } | 507 | } | ||
| 508 | + | if onlyProvider == "" { | |||
| 509 | + | var ollamaKey string | |||
| 510 | + | if explicitProvider == provider.ProviderOllama && explicitAPIKey != "" { | |||
| 511 | + | ollamaKey = explicitAPIKey | |||
| 512 | + | } else if cred, method, err := resolveCredentialForCatalog(ctx, provider.ProviderOllama, commandMode); err == nil && method == "apikey" { | |||
| 513 | + | ollamaKey = cred | |||
| 514 | + | } | |||
| 515 | + | if ollamaKey != "" { | |||
| 516 | + | markEligible(provider.ProviderOllama) | |||
| 517 | + | baseURL := provider.OllamaCloudBaseURL | |||
| 518 | + | if explicitProvider == provider.ProviderOllama && explicitBaseURL != "" { | |||
| 519 | + | baseURL = explicitBaseURL | |||
| 520 | + | } | |||
| 521 | + | if live, err := discoverOllamaFn(ctx, ollamaKey, baseURL); err == nil { | |||
| 522 | + | scope := credentialScopeForEndpoint(ollamaKey, baseURL) | |||
| 523 | + | recordDiscovery(provider.ProviderOllama, live, false, scope) | |||
| 524 | + | } | |||
| 525 | + | } | |||
| 526 | + | } | |||
| 501 | if onlyProvider == "" || onlyProvider == provider.ProviderOpenAICodex { | 527 | if onlyProvider == "" || onlyProvider == provider.ProviderOpenAICodex { | ||
| 502 | if cred, method, accountID, err := resolveCredentialFull(ctx, provider.ProviderOpenAICodex, "", commandMode); err == nil && method == "oauth" { | 528 | if cred, method, accountID, err := resolveCredentialFull(ctx, provider.ProviderOpenAICodex, "", commandMode); err == nil && method == "oauth" { | ||
| 503 | markEligible(provider.ProviderOpenAICodex) | 529 | markEligible(provider.ProviderOpenAICodex) | ||
M packages/agent/modelsync_test.go +37 -0
| @@ -139,6 +139,43 @@ func TestFilterCacheByProviderScopesDropsLegacyAuthoritativeCodex(t *testing.T) | |||
| 139 | 139 | } | |
| 140 | 140 | } | |
| 141 | 141 | ||
| 142 | + | func TestRefreshModelsDiscoversOllamaWithExplicitKeyAndEndpointScope(t *testing.T) { | |
| 143 | + | home := t.TempDir() | |
| 144 | + | t.Setenv("ZUT_HOME", home) | |
| 145 | + | t.Setenv("OLLAMA_API_KEY", "") | |
| 146 | + | provider.SetLiveModels(nil) | |
| 147 | + | t.Cleanup(func() { provider.SetLiveModels(nil) }) | |
| 148 | + | ||
| 149 | + | previous := discoverOllamaFn | |
| 150 | + | t.Cleanup(func() { discoverOllamaFn = previous }) | |
| 151 | + | var gotKey, gotBaseURL string | |
| 152 | + | discoverOllamaFn = func(_ context.Context, key, baseURL string) ([]provider.Model, error) { | |
| 153 | + | gotKey, gotBaseURL = key, baseURL | |
| 154 | + | return []provider.Model{{Provider: provider.ProviderOllama, ID: "cloud-model", Source: "live"}}, nil | |
| 155 | + | } | |
| 156 | + | ||
| 157 | + | baseURL := "https://private.example/v4" | |
| 158 | + | refreshModels(provider.ProviderOllama, "explicit-key", baseURL, "") | |
| 159 | + | if gotKey != "explicit-key" || gotBaseURL != baseURL { | |
| 160 | + | t.Fatalf("discovery key/baseURL = %q, %q", gotKey, gotBaseURL) | |
| 161 | + | } | |
| 162 | + | model, err := provider.FindModel(provider.ProviderOllama, "cloud-model") | |
| 163 | + | if err != nil { | |
| 164 | + | t.Fatal(err) | |
| 165 | + | } | |
| 166 | + | if model.Provider != provider.ProviderOllama { | |
| 167 | + | t.Fatalf("model = %+v", model) | |
| 168 | + | } | |
| 169 | + | cache, err := provider.LoadCache(filepath.Join(home, "models-cache.json")) | |
| 170 | + | if err != nil { | |
| 171 | + | t.Fatal(err) | |
| 172 | + | } | |
| 173 | + | wantScope := credentialScopeForEndpoint("explicit-key", baseURL) | |
| 174 | + | if cache.ProviderScopes[provider.ProviderOllama] != wantScope { | |
| 175 | + | t.Fatalf("ollama scope = %q, want %q", cache.ProviderScopes[provider.ProviderOllama], wantScope) | |
| 176 | + | } | |
| 177 | + | } | |
| 178 | + | ||
| 142 | 179 | func TestRefreshLlamaCPPModelsAddsOnlyLoadedModels(t *testing.T) { | |
| 143 | 180 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 144 | 181 | if r.URL.Path != "/models" { | |
| @@ -139,6 +139,43 @@ func TestFilterCacheByProviderScopesDropsLegacyAuthoritativeCodex(t *testing.T) | |||||
| 139 | } | 139 | } | ||
| 140 | } | 140 | } | ||
| 141 | 141 | ||||
| 142 | + | func TestRefreshModelsDiscoversOllamaWithExplicitKeyAndEndpointScope(t *testing.T) { | |||
| 143 | + | home := t.TempDir() | |||
| 144 | + | t.Setenv("ZUT_HOME", home) | |||
| 145 | + | t.Setenv("OLLAMA_API_KEY", "") | |||
| 146 | + | provider.SetLiveModels(nil) | |||
| 147 | + | t.Cleanup(func() { provider.SetLiveModels(nil) }) | |||
| 148 | + | ||||
| 149 | + | previous := discoverOllamaFn | |||
| 150 | + | t.Cleanup(func() { discoverOllamaFn = previous }) | |||
| 151 | + | var gotKey, gotBaseURL string | |||
| 152 | + | discoverOllamaFn = func(_ context.Context, key, baseURL string) ([]provider.Model, error) { | |||
| 153 | + | gotKey, gotBaseURL = key, baseURL | |||
| 154 | + | return []provider.Model{{Provider: provider.ProviderOllama, ID: "cloud-model", Source: "live"}}, nil | |||
| 155 | + | } | |||
| 156 | + | ||||
| 157 | + | baseURL := "https://private.example/v4" | |||
| 158 | + | refreshModels(provider.ProviderOllama, "explicit-key", baseURL, "") | |||
| 159 | + | if gotKey != "explicit-key" || gotBaseURL != baseURL { | |||
| 160 | + | t.Fatalf("discovery key/baseURL = %q, %q", gotKey, gotBaseURL) | |||
| 161 | + | } | |||
| 162 | + | model, err := provider.FindModel(provider.ProviderOllama, "cloud-model") | |||
| 163 | + | if err != nil { | |||
| 164 | + | t.Fatal(err) | |||
| 165 | + | } | |||
| 166 | + | if model.Provider != provider.ProviderOllama { | |||
| 167 | + | t.Fatalf("model = %+v", model) | |||
| 168 | + | } | |||
| 169 | + | cache, err := provider.LoadCache(filepath.Join(home, "models-cache.json")) | |||
| 170 | + | if err != nil { | |||
| 171 | + | t.Fatal(err) | |||
| 172 | + | } | |||
| 173 | + | wantScope := credentialScopeForEndpoint("explicit-key", baseURL) | |||
| 174 | + | if cache.ProviderScopes[provider.ProviderOllama] != wantScope { | |||
| 175 | + | t.Fatalf("ollama scope = %q, want %q", cache.ProviderScopes[provider.ProviderOllama], wantScope) | |||
| 176 | + | } | |||
| 177 | + | } | |||
| 178 | + | ||||
| 142 | func TestRefreshLlamaCPPModelsAddsOnlyLoadedModels(t *testing.T) { | 179 | func TestRefreshLlamaCPPModelsAddsOnlyLoadedModels(t *testing.T) { | ||
| 143 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | 180 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| 144 | if r.URL.Path != "/models" { | 181 | if r.URL.Path != "/models" { | ||
M packages/provider/discover.go +41 -0
| @@ -114,6 +114,47 @@ func DiscoverOpenAI(ctx context.Context, apiKey, baseURL string) ([]Model, error | |||
| 114 | 114 | return out, nil | |
| 115 | 115 | } | |
| 116 | 116 | ||
| 117 | + | const OllamaCloudBaseURL = "https://ollama.com/v1" | |
| 118 | + | ||
| 119 | + | // DiscoverOllama lists the public Ollama Cloud catalog. Ollama model IDs do | |
| 120 | + | // not follow OpenAI's gpt-/o* naming convention, so discovery keeps every ID. | |
| 121 | + | func DiscoverOllama(ctx context.Context, apiKey, baseURL string) ([]Model, error) { | |
| 122 | + | if baseURL == "" { | |
| 123 | + | baseURL = OllamaCloudBaseURL | |
| 124 | + | } | |
| 125 | + | client := &http.Client{Timeout: 15 * time.Second} | |
| 126 | + | baseURL = strings.TrimRight(baseURL, "/") | |
| 127 | + | url := baseURL + "/v1/models" | |
| 128 | + | if versionSegmentSuffix.MatchString(baseURL) { | |
| 129 | + | url = baseURL + "/models" | |
| 130 | + | } | |
| 131 | + | body, err := fetchDiscoveryJSON(ctx, client, url, "Bearer "+apiKey) | |
| 132 | + | if err != nil { | |
| 133 | + | return nil, fmt.Errorf("ollama discover: %w", err) | |
| 134 | + | } | |
| 135 | + | var page struct { | |
| 136 | + | Data []struct { | |
| 137 | + | ID string `json:"id"` | |
| 138 | + | } `json:"data"` | |
| 139 | + | } | |
| 140 | + | if err := json.Unmarshal(body, &page); err != nil { | |
| 141 | + | return nil, fmt.Errorf("ollama discover parse: %w", err) | |
| 142 | + | } | |
| 143 | + | out := make([]Model, 0, len(page.Data)) | |
| 144 | + | for _, item := range page.Data { | |
| 145 | + | if item.ID == "" { | |
| 146 | + | continue | |
| 147 | + | } | |
| 148 | + | out = append(out, Model{ | |
| 149 | + | Provider: "ollama", | |
| 150 | + | ID: item.ID, | |
| 151 | + | DisplayName: item.ID, | |
| 152 | + | Source: "live", | |
| 153 | + | }) | |
| 154 | + | } | |
| 155 | + | return out, nil | |
| 156 | + | } | |
| 157 | + | ||
| 117 | 158 | const ( | |
| 118 | 159 | modelsDevAPIURL = "https://models.dev/api.json" | |
| 119 | 160 | OpenCodeGoDefaultBaseURL = "https://opencode.ai/zen/go/v1" | |
| @@ -114,6 +114,47 @@ func DiscoverOpenAI(ctx context.Context, apiKey, baseURL string) ([]Model, error | |||||
| 114 | return out, nil | 114 | return out, nil | ||
| 115 | } | 115 | } | ||
| 116 | 116 | ||||
| 117 | + | const OllamaCloudBaseURL = "https://ollama.com/v1" | |||
| 118 | + | ||||
| 119 | + | // DiscoverOllama lists the public Ollama Cloud catalog. Ollama model IDs do | |||
| 120 | + | // not follow OpenAI's gpt-/o* naming convention, so discovery keeps every ID. | |||
| 121 | + | func DiscoverOllama(ctx context.Context, apiKey, baseURL string) ([]Model, error) { | |||
| 122 | + | if baseURL == "" { | |||
| 123 | + | baseURL = OllamaCloudBaseURL | |||
| 124 | + | } | |||
| 125 | + | client := &http.Client{Timeout: 15 * time.Second} | |||
| 126 | + | baseURL = strings.TrimRight(baseURL, "/") | |||
| 127 | + | url := baseURL + "/v1/models" | |||
| 128 | + | if versionSegmentSuffix.MatchString(baseURL) { | |||
| 129 | + | url = baseURL + "/models" | |||
| 130 | + | } | |||
| 131 | + | body, err := fetchDiscoveryJSON(ctx, client, url, "Bearer "+apiKey) | |||
| 132 | + | if err != nil { | |||
| 133 | + | return nil, fmt.Errorf("ollama discover: %w", err) | |||
| 134 | + | } | |||
| 135 | + | var page struct { | |||
| 136 | + | Data []struct { | |||
| 137 | + | ID string `json:"id"` | |||
| 138 | + | } `json:"data"` | |||
| 139 | + | } | |||
| 140 | + | if err := json.Unmarshal(body, &page); err != nil { | |||
| 141 | + | return nil, fmt.Errorf("ollama discover parse: %w", err) | |||
| 142 | + | } | |||
| 143 | + | out := make([]Model, 0, len(page.Data)) | |||
| 144 | + | for _, item := range page.Data { | |||
| 145 | + | if item.ID == "" { | |||
| 146 | + | continue | |||
| 147 | + | } | |||
| 148 | + | out = append(out, Model{ | |||
| 149 | + | Provider: "ollama", | |||
| 150 | + | ID: item.ID, | |||
| 151 | + | DisplayName: item.ID, | |||
| 152 | + | Source: "live", | |||
| 153 | + | }) | |||
| 154 | + | } | |||
| 155 | + | return out, nil | |||
| 156 | + | } | |||
| 157 | + | ||||
| 117 | const ( | 158 | const ( | ||
| 118 | modelsDevAPIURL = "https://models.dev/api.json" | 159 | modelsDevAPIURL = "https://models.dev/api.json" | ||
| 119 | OpenCodeGoDefaultBaseURL = "https://opencode.ai/zen/go/v1" | 160 | OpenCodeGoDefaultBaseURL = "https://opencode.ai/zen/go/v1" | ||
M packages/provider/discover_test.go +35 -0
| @@ -53,6 +53,41 @@ func TestDiscoverOpenAICodex(t *testing.T) { | |||
| 53 | 53 | } | |
| 54 | 54 | } | |
| 55 | 55 | ||
| 56 | + | func TestDiscoverOllamaKeepsCloudModelIDs(t *testing.T) { | |
| 57 | + | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 58 | + | if r.URL.Path != "/v1/models" { | |
| 59 | + | t.Errorf("path = %q, want /v1/models", r.URL.Path) | |
| 60 | + | } | |
| 61 | + | if got := r.Header.Get("authorization"); got != "Bearer ollama-key" { | |
| 62 | + | t.Errorf("authorization = %q", got) | |
| 63 | + | } | |
| 64 | + | _, _ = io.WriteString(w, `{"data":[{"id":"qwen3.5:397b-cloud"},{"id":"gemma3:27b"}]}`) | |
| 65 | + | })) | |
| 66 | + | defer srv.Close() | |
| 67 | + | ||
| 68 | + | got, err := DiscoverOllama(context.Background(), "ollama-key", srv.URL) | |
| 69 | + | if err != nil { | |
| 70 | + | t.Fatal(err) | |
| 71 | + | } | |
| 72 | + | if len(got) != 2 || got[0].Provider != "ollama" || got[0].ID != "qwen3.5:397b-cloud" || got[1].ID != "gemma3:27b" { | |
| 73 | + | t.Fatalf("models = %+v", got) | |
| 74 | + | } | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | func TestDiscoverOllamaAcceptsVersionedBaseURL(t *testing.T) { | |
| 78 | + | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 79 | + | if r.URL.Path != "/v1/models" { | |
| 80 | + | t.Errorf("path = %q, want /v1/models", r.URL.Path) | |
| 81 | + | } | |
| 82 | + | _, _ = io.WriteString(w, `{"data":[]}`) | |
| 83 | + | })) | |
| 84 | + | defer srv.Close() | |
| 85 | + | ||
| 86 | + | if _, err := DiscoverOllama(context.Background(), "ollama-key", srv.URL+"/v1"); err != nil { | |
| 87 | + | t.Fatal(err) | |
| 88 | + | } | |
| 89 | + | } | |
| 90 | + | ||
| 56 | 91 | func TestDiscoverOpenAICodexRejectsHTTPError(t *testing.T) { | |
| 57 | 92 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 58 | 93 | http.Error(w, "denied", http.StatusUnauthorized) | |
| @@ -53,6 +53,41 @@ func TestDiscoverOpenAICodex(t *testing.T) { | |||||
| 53 | } | 53 | } | ||
| 54 | } | 54 | } | ||
| 55 | 55 | ||||
| 56 | + | func TestDiscoverOllamaKeepsCloudModelIDs(t *testing.T) { | |||
| 57 | + | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
| 58 | + | if r.URL.Path != "/v1/models" { | |||
| 59 | + | t.Errorf("path = %q, want /v1/models", r.URL.Path) | |||
| 60 | + | } | |||
| 61 | + | if got := r.Header.Get("authorization"); got != "Bearer ollama-key" { | |||
| 62 | + | t.Errorf("authorization = %q", got) | |||
| 63 | + | } | |||
| 64 | + | _, _ = io.WriteString(w, `{"data":[{"id":"qwen3.5:397b-cloud"},{"id":"gemma3:27b"}]}`) | |||
| 65 | + | })) | |||
| 66 | + | defer srv.Close() | |||
| 67 | + | ||||
| 68 | + | got, err := DiscoverOllama(context.Background(), "ollama-key", srv.URL) | |||
| 69 | + | if err != nil { | |||
| 70 | + | t.Fatal(err) | |||
| 71 | + | } | |||
| 72 | + | if len(got) != 2 || got[0].Provider != "ollama" || got[0].ID != "qwen3.5:397b-cloud" || got[1].ID != "gemma3:27b" { | |||
| 73 | + | t.Fatalf("models = %+v", got) | |||
| 74 | + | } | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | func TestDiscoverOllamaAcceptsVersionedBaseURL(t *testing.T) { | |||
| 78 | + | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
| 79 | + | if r.URL.Path != "/v1/models" { | |||
| 80 | + | t.Errorf("path = %q, want /v1/models", r.URL.Path) | |||
| 81 | + | } | |||
| 82 | + | _, _ = io.WriteString(w, `{"data":[]}`) | |||
| 83 | + | })) | |||
| 84 | + | defer srv.Close() | |||
| 85 | + | ||||
| 86 | + | if _, err := DiscoverOllama(context.Background(), "ollama-key", srv.URL+"/v1"); err != nil { | |||
| 87 | + | t.Fatal(err) | |||
| 88 | + | } | |||
| 89 | + | } | |||
| 90 | + | ||||
| 56 | func TestDiscoverOpenAICodexRejectsHTTPError(t *testing.T) { | 91 | func TestDiscoverOpenAICodexRejectsHTTPError(t *testing.T) { | ||
| 57 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | 92 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| 58 | http.Error(w, "denied", http.StatusUnauthorized) | 93 | http.Error(w, "denied", http.StatusUnauthorized) | ||