mirror of
https://github.com/ollama/ollama.git
synced 2026-03-11 17:34:04 -05:00
* 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>
409 lines
9.7 KiB
Go
409 lines
9.7 KiB
Go
package envconfig
|
|
|
|
import (
|
|
"log/slog"
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/ollama/ollama/logutil"
|
|
)
|
|
|
|
func TestHost(t *testing.T) {
|
|
cases := map[string]struct {
|
|
value string
|
|
expect string
|
|
}{
|
|
"empty": {"", "http://127.0.0.1:11434"},
|
|
"only address": {"1.2.3.4", "http://1.2.3.4:11434"},
|
|
"only port": {":1234", "http://:1234"},
|
|
"address and port": {"1.2.3.4:1234", "http://1.2.3.4:1234"},
|
|
"hostname": {"example.com", "http://example.com:11434"},
|
|
"hostname and port": {"example.com:1234", "http://example.com:1234"},
|
|
"zero port": {":0", "http://:0"},
|
|
"too large port": {":66000", "http://:11434"},
|
|
"too small port": {":-1", "http://:11434"},
|
|
"ipv6 localhost": {"[::1]", "http://[::1]:11434"},
|
|
"ipv6 world open": {"[::]", "http://[::]:11434"},
|
|
"ipv6 no brackets": {"::1", "http://[::1]:11434"},
|
|
"ipv6 + port": {"[::1]:1337", "http://[::1]:1337"},
|
|
"extra space": {" 1.2.3.4 ", "http://1.2.3.4:11434"},
|
|
"extra quotes": {"\"1.2.3.4\"", "http://1.2.3.4:11434"},
|
|
"extra space+quotes": {" \" 1.2.3.4 \" ", "http://1.2.3.4:11434"},
|
|
"extra single quotes": {"'1.2.3.4'", "http://1.2.3.4:11434"},
|
|
"http": {"http://1.2.3.4", "http://1.2.3.4:80"},
|
|
"http port": {"http://1.2.3.4:4321", "http://1.2.3.4:4321"},
|
|
"https": {"https://1.2.3.4", "https://1.2.3.4:443"},
|
|
"https port": {"https://1.2.3.4:4321", "https://1.2.3.4:4321"},
|
|
"proxy path": {"https://example.com/ollama", "https://example.com:443/ollama"},
|
|
"ollama.com": {"ollama.com", "https://ollama.com:443"},
|
|
}
|
|
|
|
for name, tt := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Setenv("OLLAMA_HOST", tt.value)
|
|
if host := Host(); host.String() != tt.expect {
|
|
t.Errorf("%s: expected %s, got %s", name, tt.expect, host.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOrigins(t *testing.T) {
|
|
cases := []struct {
|
|
value string
|
|
expect []string
|
|
}{
|
|
{"", []string{
|
|
"http://localhost",
|
|
"https://localhost",
|
|
"http://localhost:*",
|
|
"https://localhost:*",
|
|
"http://127.0.0.1",
|
|
"https://127.0.0.1",
|
|
"http://127.0.0.1:*",
|
|
"https://127.0.0.1:*",
|
|
"http://0.0.0.0",
|
|
"https://0.0.0.0",
|
|
"http://0.0.0.0:*",
|
|
"https://0.0.0.0:*",
|
|
"app://*",
|
|
"file://*",
|
|
"tauri://*",
|
|
"vscode-webview://*",
|
|
"vscode-file://*",
|
|
}},
|
|
{"http://10.0.0.1", []string{
|
|
"http://10.0.0.1",
|
|
"http://localhost",
|
|
"https://localhost",
|
|
"http://localhost:*",
|
|
"https://localhost:*",
|
|
"http://127.0.0.1",
|
|
"https://127.0.0.1",
|
|
"http://127.0.0.1:*",
|
|
"https://127.0.0.1:*",
|
|
"http://0.0.0.0",
|
|
"https://0.0.0.0",
|
|
"http://0.0.0.0:*",
|
|
"https://0.0.0.0:*",
|
|
"app://*",
|
|
"file://*",
|
|
"tauri://*",
|
|
"vscode-webview://*",
|
|
"vscode-file://*",
|
|
}},
|
|
{"http://172.16.0.1,https://192.168.0.1", []string{
|
|
"http://172.16.0.1",
|
|
"https://192.168.0.1",
|
|
"http://localhost",
|
|
"https://localhost",
|
|
"http://localhost:*",
|
|
"https://localhost:*",
|
|
"http://127.0.0.1",
|
|
"https://127.0.0.1",
|
|
"http://127.0.0.1:*",
|
|
"https://127.0.0.1:*",
|
|
"http://0.0.0.0",
|
|
"https://0.0.0.0",
|
|
"http://0.0.0.0:*",
|
|
"https://0.0.0.0:*",
|
|
"app://*",
|
|
"file://*",
|
|
"tauri://*",
|
|
"vscode-webview://*",
|
|
"vscode-file://*",
|
|
}},
|
|
{"http://totally.safe,http://definitely.legit", []string{
|
|
"http://totally.safe",
|
|
"http://definitely.legit",
|
|
"http://localhost",
|
|
"https://localhost",
|
|
"http://localhost:*",
|
|
"https://localhost:*",
|
|
"http://127.0.0.1",
|
|
"https://127.0.0.1",
|
|
"http://127.0.0.1:*",
|
|
"https://127.0.0.1:*",
|
|
"http://0.0.0.0",
|
|
"https://0.0.0.0",
|
|
"http://0.0.0.0:*",
|
|
"https://0.0.0.0:*",
|
|
"app://*",
|
|
"file://*",
|
|
"tauri://*",
|
|
"vscode-webview://*",
|
|
"vscode-file://*",
|
|
}},
|
|
}
|
|
for _, tt := range cases {
|
|
t.Run(tt.value, func(t *testing.T) {
|
|
t.Setenv("OLLAMA_ORIGINS", tt.value)
|
|
|
|
if diff := cmp.Diff(AllowedOrigins(), tt.expect); diff != "" {
|
|
t.Errorf("%s: mismatch (-want +got):\n%s", tt.value, diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBool(t *testing.T) {
|
|
cases := map[string]bool{
|
|
"": false,
|
|
"true": true,
|
|
"false": false,
|
|
"1": true,
|
|
"0": false,
|
|
// invalid values
|
|
"random": true,
|
|
"something": true,
|
|
}
|
|
|
|
for k, v := range cases {
|
|
t.Run(k, func(t *testing.T) {
|
|
t.Setenv("OLLAMA_BOOL", k)
|
|
if b := Bool("OLLAMA_BOOL")(); b != v {
|
|
t.Errorf("%s: expected %t, got %t", k, v, b)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUint(t *testing.T) {
|
|
cases := map[string]uint{
|
|
"0": 0,
|
|
"1": 1,
|
|
"1337": 1337,
|
|
// default values
|
|
"": 11434,
|
|
"-1": 11434,
|
|
"0o10": 11434,
|
|
"0x10": 11434,
|
|
"string": 11434,
|
|
}
|
|
|
|
for k, v := range cases {
|
|
t.Run(k, func(t *testing.T) {
|
|
t.Setenv("OLLAMA_UINT", k)
|
|
if i := Uint("OLLAMA_UINT", 11434)(); i != v {
|
|
t.Errorf("%s: expected %d, got %d", k, v, i)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestKeepAlive(t *testing.T) {
|
|
cases := map[string]time.Duration{
|
|
"": 5 * time.Minute,
|
|
"1s": time.Second,
|
|
"1m": time.Minute,
|
|
"1h": time.Hour,
|
|
"5m0s": 5 * time.Minute,
|
|
"1h2m3s": 1*time.Hour + 2*time.Minute + 3*time.Second,
|
|
"0": time.Duration(0),
|
|
"60": 60 * time.Second,
|
|
"120": 2 * time.Minute,
|
|
"3600": time.Hour,
|
|
"-0": time.Duration(0),
|
|
"-1": time.Duration(math.MaxInt64),
|
|
"-1m": time.Duration(math.MaxInt64),
|
|
// invalid values
|
|
" ": 5 * time.Minute,
|
|
"???": 5 * time.Minute,
|
|
"1d": 5 * time.Minute,
|
|
"1y": 5 * time.Minute,
|
|
"1w": 5 * time.Minute,
|
|
}
|
|
|
|
for tt, expect := range cases {
|
|
t.Run(tt, func(t *testing.T) {
|
|
t.Setenv("OLLAMA_KEEP_ALIVE", tt)
|
|
if actual := KeepAlive(); actual != expect {
|
|
t.Errorf("%s: expected %s, got %s", tt, expect, actual)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoadTimeout(t *testing.T) {
|
|
defaultTimeout := 5 * time.Minute
|
|
cases := map[string]time.Duration{
|
|
"": defaultTimeout,
|
|
"1s": time.Second,
|
|
"1m": time.Minute,
|
|
"1h": time.Hour,
|
|
"5m0s": defaultTimeout,
|
|
"1h2m3s": 1*time.Hour + 2*time.Minute + 3*time.Second,
|
|
"0": time.Duration(math.MaxInt64),
|
|
"60": 60 * time.Second,
|
|
"120": 2 * time.Minute,
|
|
"3600": time.Hour,
|
|
"-0": time.Duration(math.MaxInt64),
|
|
"-1": time.Duration(math.MaxInt64),
|
|
"-1m": time.Duration(math.MaxInt64),
|
|
// invalid values
|
|
" ": defaultTimeout,
|
|
"???": defaultTimeout,
|
|
"1d": defaultTimeout,
|
|
"1y": defaultTimeout,
|
|
"1w": defaultTimeout,
|
|
}
|
|
|
|
for tt, expect := range cases {
|
|
t.Run(tt, func(t *testing.T) {
|
|
t.Setenv("OLLAMA_LOAD_TIMEOUT", tt)
|
|
if actual := LoadTimeout(); actual != expect {
|
|
t.Errorf("%s: expected %s, got %s", tt, expect, actual)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVar(t *testing.T) {
|
|
cases := map[string]string{
|
|
"value": "value",
|
|
" value ": "value",
|
|
" 'value' ": "value",
|
|
` "value" `: "value",
|
|
" ' value ' ": " value ",
|
|
` " value " `: " value ",
|
|
}
|
|
|
|
for k, v := range cases {
|
|
t.Run(k, func(t *testing.T) {
|
|
t.Setenv("OLLAMA_VAR", k)
|
|
if s := Var("OLLAMA_VAR"); s != v {
|
|
t.Errorf("%s: expected %q, got %q", k, v, s)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestContextLength(t *testing.T) {
|
|
cases := map[string]uint{
|
|
"": 0,
|
|
"2048": 2048,
|
|
}
|
|
|
|
for k, v := range cases {
|
|
t.Run(k, func(t *testing.T) {
|
|
t.Setenv("OLLAMA_CONTEXT_LENGTH", k)
|
|
if i := ContextLength(); i != v {
|
|
t.Errorf("%s: expected %d, got %d", k, v, i)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLogLevel(t *testing.T) {
|
|
cases := map[string]slog.Level{
|
|
// Default to INFO
|
|
"": slog.LevelInfo,
|
|
"false": slog.LevelInfo,
|
|
"f": slog.LevelInfo,
|
|
"0": slog.LevelInfo,
|
|
|
|
// True values enable Debug
|
|
"true": slog.LevelDebug,
|
|
"t": slog.LevelDebug,
|
|
|
|
// Positive values increase verbosity
|
|
"1": slog.LevelDebug,
|
|
"2": logutil.LevelTrace,
|
|
|
|
// Negative values decrease verbosity
|
|
"-1": slog.LevelWarn,
|
|
"-2": slog.LevelError,
|
|
}
|
|
|
|
for k, v := range cases {
|
|
t.Run(k, func(t *testing.T) {
|
|
t.Setenv("OLLAMA_DEBUG", k)
|
|
if i := LogLevel(); i != v {
|
|
t.Errorf("%s: expected %d, got %d", k, v, i)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|