add ability to disable cloud (#14221)

* add ability to disable cloud

Users can now easily opt-out of cloud inference and web search by
setting

```
"disable_ollama_cloud": true
```

in their `~/.ollama/server.json` settings file. After a setting update,
the server must be restarted.

Alternatively, setting the environment variable `OLLAMA_NO_CLOUD=1` will
also disable cloud features. While users previously were able to avoid
cloud models by not pulling or `ollama run`ing them, this gives them an
easy way to enforce that decision. Any attempt to run a cloud model when
cloud is disabled will fail.

The app's old "airplane mode" setting, which did a similar thing for
hiding cloud models within the app is now unified with this new cloud
disabled mode. That setting has been replaced with a "Cloud" toggle,
which behind the scenes edits `server.json` and then restarts the
server.

* gate cloud models across TUI and launch flows when cloud is disabled

Block cloud models from being selected, launched, or written to
integration configs when cloud mode is turned off:

- TUI main menu: open model picker instead of launching with a
  disabled cloud model
- cmd.go: add IsCloudModelDisabled checks for all Selection* paths
- LaunchCmd: filter cloud models from saved Editor configs before
  launch, fall through to picker if none remain
- Editor Run() methods (droid, opencode, openclaw): filter cloud
  models before calling Edit() and persist the cleaned list
- Export SaveIntegration, remove SaveIntegrationModel wrapper that
  was accumulating models instead of replacing them

* rename saveIntegration to SaveIntegration in config.go and tests

* cmd/config: add --model guarding and empty model list fixes

* Update docs/faq.mdx

Co-authored-by: Jeffrey Morgan <jmorganca@gmail.com>

* Update internal/cloud/policy.go

Co-authored-by: Jeffrey Morgan <jmorganca@gmail.com>

* Update internal/cloud/policy.go

Co-authored-by: Jeffrey Morgan <jmorganca@gmail.com>

* Update server/routes.go

Co-authored-by: Jeffrey Morgan <jmorganca@gmail.com>

* Revert "Update internal/cloud/policy.go"

This reverts commit 8bff8615f9.

Since this error shows up in other integrations, we want it to be
prefixed with Ollama

* rename cloud status

* more status renaming

* fix tests that weren't updated after rename

---------

Co-authored-by: ParthSareen <parth.sareen@ollama.com>
Co-authored-by: Jeffrey Morgan <jmorganca@gmail.com>
This commit is contained in:
Devon Rifkin
2026-02-12 15:47:00 -08:00
committed by GitHub
parent 598b74d42c
commit 948de6bbd2
56 changed files with 2026 additions and 155 deletions

View File

@@ -1,6 +1,8 @@
package envconfig
import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"math"
@@ -11,6 +13,7 @@ import (
"runtime"
"strconv"
"strings"
"sync"
"time"
)
@@ -206,6 +209,8 @@ var (
UseAuth = Bool("OLLAMA_AUTH")
// Enable Vulkan backend
EnableVulkan = Bool("OLLAMA_VULKAN")
// NoCloudEnv checks the OLLAMA_NO_CLOUD environment variable.
NoCloudEnv = Bool("OLLAMA_NO_CLOUD")
)
func String(s string) func() string {
@@ -285,6 +290,7 @@ func AsMap() map[string]EnvVar {
"OLLAMA_MAX_LOADED_MODELS": {"OLLAMA_MAX_LOADED_MODELS", MaxRunners(), "Maximum number of loaded models per GPU"},
"OLLAMA_MAX_QUEUE": {"OLLAMA_MAX_QUEUE", MaxQueue(), "Maximum number of queued requests"},
"OLLAMA_MODELS": {"OLLAMA_MODELS", Models(), "The path to the models directory"},
"OLLAMA_NO_CLOUD": {"OLLAMA_NO_CLOUD", NoCloud(), "Disable Ollama cloud features (remote inference and web search)"},
"OLLAMA_NOHISTORY": {"OLLAMA_NOHISTORY", NoHistory(), "Do not preserve readline history"},
"OLLAMA_NOPRUNE": {"OLLAMA_NOPRUNE", NoPrune(), "Do not prune model blobs on startup"},
"OLLAMA_NUM_PARALLEL": {"OLLAMA_NUM_PARALLEL", NumParallel(), "Maximum number of parallel requests"},
@@ -334,3 +340,91 @@ func Values() map[string]string {
func Var(key string) string {
return strings.Trim(strings.TrimSpace(os.Getenv(key)), "\"'")
}
// serverConfigData holds the parsed fields from ~/.ollama/server.json.
type serverConfigData struct {
DisableOllamaCloud bool `json:"disable_ollama_cloud,omitempty"`
}
var (
serverCfgMu sync.RWMutex
serverCfgLoaded bool
serverCfg serverConfigData
)
func loadServerConfig() {
serverCfgMu.RLock()
if serverCfgLoaded {
serverCfgMu.RUnlock()
return
}
serverCfgMu.RUnlock()
cfg := serverConfigData{}
home, err := os.UserHomeDir()
if err == nil {
path := filepath.Join(home, ".ollama", "server.json")
data, err := os.ReadFile(path)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
slog.Debug("envconfig: could not read server config", "error", err)
}
} else if err := json.Unmarshal(data, &cfg); err != nil {
slog.Debug("envconfig: could not parse server config", "error", err)
}
}
serverCfgMu.Lock()
defer serverCfgMu.Unlock()
if serverCfgLoaded {
return
}
serverCfg = cfg
serverCfgLoaded = true
}
func cachedServerConfig() serverConfigData {
serverCfgMu.RLock()
defer serverCfgMu.RUnlock()
return serverCfg
}
// ReloadServerConfig refreshes the cached ~/.ollama/server.json settings.
func ReloadServerConfig() {
serverCfgMu.Lock()
serverCfgLoaded = false
serverCfg = serverConfigData{}
serverCfgMu.Unlock()
loadServerConfig()
}
// NoCloud returns true if Ollama cloud features are disabled,
// checking both the OLLAMA_NO_CLOUD environment variable and
// the disable_ollama_cloud field in ~/.ollama/server.json.
func NoCloud() bool {
if NoCloudEnv() {
return true
}
loadServerConfig()
return cachedServerConfig().DisableOllamaCloud
}
// NoCloudSource returns the source of the cloud-disabled decision.
// Returns "none", "env", "config", or "both".
func NoCloudSource() string {
envDisabled := NoCloudEnv()
loadServerConfig()
configDisabled := cachedServerConfig().DisableOllamaCloud
switch {
case envDisabled && configDisabled:
return "both"
case envDisabled:
return "env"
case configDisabled:
return "config"
default:
return "none"
}
}

View File

@@ -3,6 +3,8 @@ package envconfig
import (
"log/slog"
"math"
"os"
"path/filepath"
"testing"
"time"
@@ -326,3 +328,81 @@ func TestLogLevel(t *testing.T) {
})
}
}
func TestNoCloud(t *testing.T) {
tests := []struct {
name string
envValue string
configContent string
wantDisabled bool
wantSource string
}{
{
name: "neither env nor config",
wantDisabled: false,
wantSource: "none",
},
{
name: "env only",
envValue: "1",
wantDisabled: true,
wantSource: "env",
},
{
name: "config only",
configContent: `{"disable_ollama_cloud": true}`,
wantDisabled: true,
wantSource: "config",
},
{
name: "both env and config",
envValue: "1",
configContent: `{"disable_ollama_cloud": true}`,
wantDisabled: true,
wantSource: "both",
},
{
name: "config false",
configContent: `{"disable_ollama_cloud": false}`,
wantDisabled: false,
wantSource: "none",
},
{
name: "invalid config ignored",
configContent: `{invalid json`,
wantDisabled: false,
wantSource: "none",
},
{
name: "no config file",
wantDisabled: false,
wantSource: "none",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
home := t.TempDir()
if tt.configContent != "" {
configDir := filepath.Join(home, ".ollama")
if err := os.MkdirAll(configDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(configDir, "server.json"), []byte(tt.configContent), 0o644); err != nil {
t.Fatal(err)
}
}
setTestHome(t, home)
t.Setenv("OLLAMA_NO_CLOUD", tt.envValue)
if got := NoCloud(); got != tt.wantDisabled {
t.Errorf("NoCloud() = %v, want %v", got, tt.wantDisabled)
}
if got := NoCloudSource(); got != tt.wantSource {
t.Errorf("NoCloudSource() = %q, want %q", got, tt.wantSource)
}
})
}
}

View File

@@ -0,0 +1,10 @@
package envconfig
import "testing"
func setTestHome(t *testing.T, home string) {
t.Helper()
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
ReloadServerConfig()
}