mirror of
https://github.com/ollama/ollama.git
synced 2026-04-23 17:29:54 +02:00
Compare commits
3 Commits
jessegross
...
v0.21.2-rc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
160660e572 | ||
|
|
3b43b9bc4b | ||
|
|
21883571b7 |
@@ -301,7 +301,7 @@ func TestParseArgs(t *testing.T) {
|
|||||||
func TestIsCloudModel(t *testing.T) {
|
func TestIsCloudModel(t *testing.T) {
|
||||||
// isCloudModel now only uses Show API, so nil client always returns false
|
// isCloudModel now only uses Show API, so nil client always returns false
|
||||||
t.Run("nil client returns false", func(t *testing.T) {
|
t.Run("nil client returns false", func(t *testing.T) {
|
||||||
models := []string{"glm-5.1:cloud", "kimi-k2.5:cloud", "local-model"}
|
models := []string{"glm-5.1:cloud", "kimi-k2.6:cloud", "local-model"}
|
||||||
for _, model := range models {
|
for _, model := range models {
|
||||||
if isCloudModel(context.Background(), nil, model) {
|
if isCloudModel(context.Background(), nil, model) {
|
||||||
t.Errorf("isCloudModel(%q) with nil client should return false", model)
|
t.Errorf("isCloudModel(%q) with nil client should return false", model)
|
||||||
@@ -321,7 +321,7 @@ func names(items []ModelItem) []string {
|
|||||||
func TestBuildModelList_NoExistingModels(t *testing.T) {
|
func TestBuildModelList_NoExistingModels(t *testing.T) {
|
||||||
items, _, _, _ := buildModelList(nil, nil, "")
|
items, _, _, _ := buildModelList(nil, nil, "")
|
||||||
|
|
||||||
want := []string{"kimi-k2.5:cloud", "qwen3.5:cloud", "glm-5.1:cloud", "minimax-m2.7:cloud", "gemma4", "qwen3.5"}
|
want := []string{"kimi-k2.6:cloud", "qwen3.5:cloud", "glm-5.1:cloud", "minimax-m2.7:cloud", "gemma4", "qwen3.5"}
|
||||||
if diff := cmp.Diff(want, names(items)); diff != "" {
|
if diff := cmp.Diff(want, names(items)); diff != "" {
|
||||||
t.Errorf("with no existing models, items should be recommended in order (-want +got):\n%s", diff)
|
t.Errorf("with no existing models, items should be recommended in order (-want +got):\n%s", diff)
|
||||||
}
|
}
|
||||||
@@ -350,7 +350,7 @@ func TestBuildModelList_OnlyLocalModels_CloudRecsStillFirst(t *testing.T) {
|
|||||||
|
|
||||||
// Cloud recs always come first among recommended, regardless of installed inventory.
|
// Cloud recs always come first among recommended, regardless of installed inventory.
|
||||||
// Cloud disablement is handled upstream in loadSelectableModels via filterCloudItems.
|
// Cloud disablement is handled upstream in loadSelectableModels via filterCloudItems.
|
||||||
want := []string{"kimi-k2.5:cloud", "qwen3.5:cloud", "glm-5.1:cloud", "minimax-m2.7:cloud", "gemma4", "qwen3.5", "llama3.2", "qwen2.5"}
|
want := []string{"kimi-k2.6:cloud", "qwen3.5:cloud", "glm-5.1:cloud", "minimax-m2.7:cloud", "gemma4", "qwen3.5", "llama3.2", "qwen2.5"}
|
||||||
if diff := cmp.Diff(want, got); diff != "" {
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
t.Errorf("cloud recs pinned first even when no cloud models installed (-want +got):\n%s", diff)
|
t.Errorf("cloud recs pinned first even when no cloud models installed (-want +got):\n%s", diff)
|
||||||
}
|
}
|
||||||
@@ -366,7 +366,7 @@ func TestBuildModelList_BothCloudAndLocal_RegularSort(t *testing.T) {
|
|||||||
got := names(items)
|
got := names(items)
|
||||||
|
|
||||||
// All recs pinned at top (cloud before local in mixed case), then non-recs
|
// All recs pinned at top (cloud before local in mixed case), then non-recs
|
||||||
want := []string{"kimi-k2.5:cloud", "qwen3.5:cloud", "glm-5.1:cloud", "minimax-m2.7:cloud", "gemma4", "qwen3.5", "llama3.2"}
|
want := []string{"kimi-k2.6:cloud", "qwen3.5:cloud", "glm-5.1:cloud", "minimax-m2.7:cloud", "gemma4", "qwen3.5", "llama3.2"}
|
||||||
if diff := cmp.Diff(want, got); diff != "" {
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
t.Errorf("recs pinned at top, cloud recs first in mixed case (-want +got):\n%s", diff)
|
t.Errorf("recs pinned at top, cloud recs first in mixed case (-want +got):\n%s", diff)
|
||||||
}
|
}
|
||||||
@@ -437,7 +437,7 @@ func TestBuildModelList_ExistingRecommendedMarked(t *testing.T) {
|
|||||||
if !strings.HasSuffix(item.Description, "(not downloaded)") {
|
if !strings.HasSuffix(item.Description, "(not downloaded)") {
|
||||||
t.Errorf("non-installed recommended %q should have '(not downloaded)' suffix, got %q", item.Name, item.Description)
|
t.Errorf("non-installed recommended %q should have '(not downloaded)' suffix, got %q", item.Name, item.Description)
|
||||||
}
|
}
|
||||||
case "minimax-m2.7:cloud", "kimi-k2.5:cloud", "qwen3.5:cloud":
|
case "minimax-m2.7:cloud", "kimi-k2.6:cloud", "qwen3.5:cloud":
|
||||||
if strings.HasSuffix(item.Description, "(not downloaded)") {
|
if strings.HasSuffix(item.Description, "(not downloaded)") {
|
||||||
t.Errorf("cloud model %q should not have '(not downloaded)' suffix, got %q", item.Name, item.Description)
|
t.Errorf("cloud model %q should not have '(not downloaded)' suffix, got %q", item.Name, item.Description)
|
||||||
}
|
}
|
||||||
@@ -455,9 +455,9 @@ func TestBuildModelList_ExistingCloudModelsNotPushedToBottom(t *testing.T) {
|
|||||||
got := names(items)
|
got := names(items)
|
||||||
|
|
||||||
// gemma4 and glm-5.1:cloud are installed so they sort normally;
|
// gemma4 and glm-5.1:cloud are installed so they sort normally;
|
||||||
// kimi-k2.5:cloud, qwen3.5:cloud, and qwen3.5 are not installed so they go to the bottom
|
// qwen3.5:cloud and qwen3.5 are not installed so they go to the bottom
|
||||||
// All recs: cloud first in mixed case, then local, in rec order within each
|
// All recs: cloud first in mixed case, then local, in rec order within each
|
||||||
want := []string{"kimi-k2.5:cloud", "qwen3.5:cloud", "glm-5.1:cloud", "minimax-m2.7:cloud", "gemma4", "qwen3.5"}
|
want := []string{"kimi-k2.6:cloud", "qwen3.5:cloud", "glm-5.1:cloud", "minimax-m2.7:cloud", "gemma4", "qwen3.5"}
|
||||||
if diff := cmp.Diff(want, got); diff != "" {
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
t.Errorf("all recs, cloud first in mixed case (-want +got):\n%s", diff)
|
t.Errorf("all recs, cloud first in mixed case (-want +got):\n%s", diff)
|
||||||
}
|
}
|
||||||
@@ -466,23 +466,23 @@ func TestBuildModelList_ExistingCloudModelsNotPushedToBottom(t *testing.T) {
|
|||||||
func TestBuildModelList_HasRecommendedCloudModel_OnlyNonInstalledAtBottom(t *testing.T) {
|
func TestBuildModelList_HasRecommendedCloudModel_OnlyNonInstalledAtBottom(t *testing.T) {
|
||||||
existing := []modelInfo{
|
existing := []modelInfo{
|
||||||
{Name: "llama3.2:latest", Remote: false},
|
{Name: "llama3.2:latest", Remote: false},
|
||||||
{Name: "kimi-k2.5:cloud", Remote: true},
|
{Name: "kimi-k2.6:cloud", Remote: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
items, _, _, _ := buildModelList(existing, nil, "")
|
items, _, _, _ := buildModelList(existing, nil, "")
|
||||||
got := names(items)
|
got := names(items)
|
||||||
|
|
||||||
// kimi-k2.5:cloud is installed so it sorts normally;
|
// kimi-k2.6:cloud is installed so it sorts normally;
|
||||||
// the rest of the recommendations are not installed so they go to the bottom
|
// the rest of the recommendations are not installed so they go to the bottom
|
||||||
// All recs pinned at top (cloud first in mixed case), then non-recs
|
// All recs pinned at top (cloud first in mixed case), then non-recs
|
||||||
want := []string{"kimi-k2.5:cloud", "qwen3.5:cloud", "glm-5.1:cloud", "minimax-m2.7:cloud", "gemma4", "qwen3.5", "llama3.2"}
|
want := []string{"kimi-k2.6:cloud", "qwen3.5:cloud", "glm-5.1:cloud", "minimax-m2.7:cloud", "gemma4", "qwen3.5", "llama3.2"}
|
||||||
if diff := cmp.Diff(want, got); diff != "" {
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
t.Errorf("recs pinned at top, cloud first in mixed case (-want +got):\n%s", diff)
|
t.Errorf("recs pinned at top, cloud first in mixed case (-want +got):\n%s", diff)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
isCloud := strings.HasSuffix(item.Name, ":cloud")
|
isCloud := strings.HasSuffix(item.Name, ":cloud")
|
||||||
isInstalled := slices.Contains([]string{"kimi-k2.5:cloud", "llama3.2"}, item.Name)
|
isInstalled := slices.Contains([]string{"kimi-k2.6:cloud", "llama3.2"}, item.Name)
|
||||||
if isInstalled || isCloud {
|
if isInstalled || isCloud {
|
||||||
if strings.HasSuffix(item.Description, "(not downloaded)") {
|
if strings.HasSuffix(item.Description, "(not downloaded)") {
|
||||||
t.Errorf("installed or cloud model %q should not have '(not downloaded)' suffix, got %q", item.Name, item.Description)
|
t.Errorf("installed or cloud model %q should not have '(not downloaded)' suffix, got %q", item.Name, item.Description)
|
||||||
@@ -549,8 +549,8 @@ func TestBuildModelList_ReturnsExistingAndCloudMaps(t *testing.T) {
|
|||||||
if !cloudModels["glm-5.1:cloud"] {
|
if !cloudModels["glm-5.1:cloud"] {
|
||||||
t.Error("glm-5.1:cloud should be in cloudModels")
|
t.Error("glm-5.1:cloud should be in cloudModels")
|
||||||
}
|
}
|
||||||
if !cloudModels["kimi-k2.5:cloud"] {
|
if !cloudModels["kimi-k2.6:cloud"] {
|
||||||
t.Error("kimi-k2.5:cloud should be in cloudModels (recommended cloud)")
|
t.Error("kimi-k2.6:cloud should be in cloudModels (recommended cloud)")
|
||||||
}
|
}
|
||||||
if !cloudModels["qwen3.5:cloud"] {
|
if !cloudModels["qwen3.5:cloud"] {
|
||||||
t.Error("qwen3.5:cloud should be in cloudModels (recommended cloud)")
|
t.Error("qwen3.5:cloud should be in cloudModels (recommended cloud)")
|
||||||
@@ -570,7 +570,7 @@ func TestBuildModelList_RecommendedFieldSet(t *testing.T) {
|
|||||||
|
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
switch item.Name {
|
switch item.Name {
|
||||||
case "gemma4", "qwen3.5", "glm-5.1:cloud", "kimi-k2.5:cloud", "qwen3.5:cloud":
|
case "gemma4", "qwen3.5", "glm-5.1:cloud", "kimi-k2.6:cloud", "qwen3.5:cloud":
|
||||||
if !item.Recommended {
|
if !item.Recommended {
|
||||||
t.Errorf("%q should have Recommended=true", item.Name)
|
t.Errorf("%q should have Recommended=true", item.Name)
|
||||||
}
|
}
|
||||||
@@ -628,7 +628,7 @@ func TestBuildModelList_RecsAboveNonRecs(t *testing.T) {
|
|||||||
lastRecIdx := -1
|
lastRecIdx := -1
|
||||||
firstNonRecIdx := len(got)
|
firstNonRecIdx := len(got)
|
||||||
for i, name := range got {
|
for i, name := range got {
|
||||||
isRec := name == "gemma4" || name == "qwen3.5" || name == "minimax-m2.7:cloud" || name == "glm-5.1:cloud" || name == "kimi-k2.5:cloud" || name == "qwen3.5:cloud"
|
isRec := name == "gemma4" || name == "qwen3.5" || name == "minimax-m2.7:cloud" || name == "glm-5.1:cloud" || name == "kimi-k2.6:cloud" || name == "qwen3.5:cloud"
|
||||||
if isRec && i > lastRecIdx {
|
if isRec && i > lastRecIdx {
|
||||||
lastRecIdx = i
|
lastRecIdx = i
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var recommendedModels = []ModelItem{
|
var recommendedModels = []ModelItem{
|
||||||
{Name: "kimi-k2.5:cloud", Description: "Multimodal reasoning with subagents", Recommended: true},
|
{Name: "kimi-k2.6:cloud", Description: "State-of-the-art coding, long-horizon execution, and multimodal agent swarm capability", Recommended: true},
|
||||||
{Name: "qwen3.5:cloud", Description: "Reasoning, coding, and agentic tool use with vision", Recommended: true},
|
{Name: "qwen3.5:cloud", Description: "Reasoning, coding, and agentic tool use with vision", Recommended: true},
|
||||||
{Name: "glm-5.1:cloud", Description: "Reasoning and code generation", Recommended: true},
|
{Name: "glm-5.1:cloud", Description: "Reasoning and code generation", Recommended: true},
|
||||||
{Name: "minimax-m2.7:cloud", Description: "Fast, efficient coding and real-world productivity", Recommended: true},
|
{Name: "minimax-m2.7:cloud", Description: "Fast, efficient coding and real-world productivity", Recommended: true},
|
||||||
@@ -56,6 +56,7 @@ var cloudModelLimits = map[string]cloudModelLimit{
|
|||||||
"gpt-oss:20b": {Context: 131_072, Output: 131_072},
|
"gpt-oss:20b": {Context: 131_072, Output: 131_072},
|
||||||
"kimi-k2:1t": {Context: 262_144, Output: 262_144},
|
"kimi-k2:1t": {Context: 262_144, Output: 262_144},
|
||||||
"kimi-k2.5": {Context: 262_144, Output: 262_144},
|
"kimi-k2.5": {Context: 262_144, Output: 262_144},
|
||||||
|
"kimi-k2.6": {Context: 262_144, Output: 262_144},
|
||||||
"kimi-k2-thinking": {Context: 262_144, Output: 262_144},
|
"kimi-k2-thinking": {Context: 262_144, Output: 262_144},
|
||||||
"nemotron-3-nano:30b": {Context: 1_048_576, Output: 131_072},
|
"nemotron-3-nano:30b": {Context: 1_048_576, Output: 131_072},
|
||||||
"qwen3-coder:480b": {Context: 262_144, Output: 65_536},
|
"qwen3-coder:480b": {Context: 262_144, Output: 65_536},
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/mod/semver"
|
|
||||||
|
|
||||||
"github.com/ollama/ollama/api"
|
"github.com/ollama/ollama/api"
|
||||||
"github.com/ollama/ollama/cmd/internal/fileutil"
|
"github.com/ollama/ollama/cmd/internal/fileutil"
|
||||||
"github.com/ollama/ollama/envconfig"
|
"github.com/ollama/ollama/envconfig"
|
||||||
@@ -98,9 +96,7 @@ func (c *Openclaw) Run(model string, args []string) error {
|
|||||||
patchDeviceScopes()
|
patchDeviceScopes()
|
||||||
}
|
}
|
||||||
|
|
||||||
if ensureWebSearchPlugin() {
|
configureOllamaWebSearch()
|
||||||
registerWebSearchPlugin()
|
|
||||||
}
|
|
||||||
|
|
||||||
// When extra args are passed through, run exactly what the user asked for
|
// When extra args are passed through, run exactly what the user asked for
|
||||||
// after setup and skip the built-in gateway+TUI convenience flow.
|
// after setup and skip the built-in gateway+TUI convenience flow.
|
||||||
@@ -738,89 +734,13 @@ func clearSessionModelOverride(primary string) {
|
|||||||
_ = os.WriteFile(path, out, 0o600)
|
_ = os.WriteFile(path, out, 0o600)
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
// configureOllamaWebSearch keeps launch-managed OpenClaw installs on the
|
||||||
webSearchNpmPackage = "@ollama/openclaw-web-search"
|
// bundled Ollama web_search provider. Older launch builds installed an
|
||||||
webSearchMinVersion = "0.2.1"
|
// external openclaw-web-search plugin that added custom ollama_web_search and
|
||||||
)
|
// ollama_web_fetch tools. Current OpenClaw versions ship Ollama web_search as
|
||||||
|
// the bundled "ollama" plugin instead, so we migrate stale config and ensure
|
||||||
// ensureWebSearchPlugin installs the openclaw-web-search extension into the
|
// fresh installs select the bundled provider.
|
||||||
// user-level extensions directory (~/.openclaw/extensions/) if it isn't already
|
func configureOllamaWebSearch() {
|
||||||
// present, or re-installs if the installed version is older than webSearchMinVersion.
|
|
||||||
// Returns true if the extension is available.
|
|
||||||
func ensureWebSearchPlugin() bool {
|
|
||||||
home, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
pluginDir := filepath.Join(home, ".openclaw", "extensions", "openclaw-web-search")
|
|
||||||
if webSearchPluginUpToDate(pluginDir) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
npmBin, err := exec.LookPath("npm")
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.MkdirAll(pluginDir, 0o755); err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download the tarball via `npm pack`, extract it flat into the plugin dir.
|
|
||||||
pack := exec.Command(npmBin, "pack", webSearchNpmPackage, "--pack-destination", pluginDir)
|
|
||||||
out, err := pack.Output()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "%s Warning: could not download web search plugin: %v%s\n", ansiYellow, err, ansiReset)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
tgzName := strings.TrimSpace(string(out))
|
|
||||||
tgzPath := filepath.Join(pluginDir, tgzName)
|
|
||||||
defer os.Remove(tgzPath)
|
|
||||||
|
|
||||||
tar := exec.Command("tar", "xzf", tgzPath, "--strip-components=1", "-C", pluginDir)
|
|
||||||
if err := tar.Run(); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "%s Warning: could not extract web search plugin: %v%s\n", ansiYellow, err, ansiReset)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(os.Stderr, "%s ✓ Installed Ollama web search %s\n", ansiGreen, ansiReset)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// webSearchPluginUpToDate returns true if the plugin is installed and its
|
|
||||||
// package.json version is >= webSearchMinVersion.
|
|
||||||
func webSearchPluginUpToDate(pluginDir string) bool {
|
|
||||||
data, err := os.ReadFile(filepath.Join(pluginDir, "package.json"))
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
var pkg struct {
|
|
||||||
Version string `json:"version"`
|
|
||||||
}
|
|
||||||
if json.Unmarshal(data, &pkg) != nil || pkg.Version == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return !versionLessThan(pkg.Version, webSearchMinVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
// versionLessThan compares two semver version strings (major.minor.patch).
|
|
||||||
// Inputs may omit the "v" prefix; it is added automatically for semver.Compare.
|
|
||||||
func versionLessThan(a, b string) bool {
|
|
||||||
if !strings.HasPrefix(a, "v") {
|
|
||||||
a = "v" + a
|
|
||||||
}
|
|
||||||
if !strings.HasPrefix(b, "v") {
|
|
||||||
b = "v" + b
|
|
||||||
}
|
|
||||||
return semver.Compare(a, b) < 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// registerWebSearchPlugin adds plugins.entries.openclaw-web-search to the OpenClaw
|
|
||||||
// config so the gateway activates it on next start. Best-effort; silently returns
|
|
||||||
// on any error.
|
|
||||||
func registerWebSearchPlugin() {
|
|
||||||
home, err := os.UserHomeDir()
|
home, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -835,6 +755,8 @@ func registerWebSearchPlugin() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stalePluginConfigured := false
|
||||||
|
|
||||||
plugins, _ := config["plugins"].(map[string]any)
|
plugins, _ := config["plugins"].(map[string]any)
|
||||||
if plugins == nil {
|
if plugins == nil {
|
||||||
plugins = make(map[string]any)
|
plugins = make(map[string]any)
|
||||||
@@ -843,68 +765,100 @@ func registerWebSearchPlugin() {
|
|||||||
if entries == nil {
|
if entries == nil {
|
||||||
entries = make(map[string]any)
|
entries = make(map[string]any)
|
||||||
}
|
}
|
||||||
entries["openclaw-web-search"] = map[string]any{"enabled": true}
|
|
||||||
plugins["entries"] = entries
|
|
||||||
|
|
||||||
// Pin trust so the gateway doesn't warn about untracked plugins.
|
|
||||||
allow, _ := plugins["allow"].([]any)
|
|
||||||
hasAllow := false
|
|
||||||
for _, v := range allow {
|
|
||||||
if s, ok := v.(string); ok && s == "openclaw-web-search" {
|
|
||||||
hasAllow = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !hasAllow {
|
|
||||||
allow = append(allow, "openclaw-web-search")
|
|
||||||
}
|
|
||||||
plugins["allow"] = allow
|
|
||||||
|
|
||||||
// Record install provenance so the loader can verify the plugin origin.
|
|
||||||
installs, _ := plugins["installs"].(map[string]any)
|
|
||||||
if installs == nil {
|
|
||||||
installs = make(map[string]any)
|
|
||||||
}
|
|
||||||
pluginDir := filepath.Join(home, ".openclaw", "extensions", "openclaw-web-search")
|
|
||||||
installs["openclaw-web-search"] = map[string]any{
|
|
||||||
"source": "npm",
|
|
||||||
"spec": webSearchNpmPackage,
|
|
||||||
"installPath": pluginDir,
|
|
||||||
}
|
|
||||||
plugins["installs"] = installs
|
|
||||||
|
|
||||||
config["plugins"] = plugins
|
|
||||||
|
|
||||||
// Add plugin tools to tools.alsoAllow so they survive the coding profile's
|
|
||||||
// policy pipeline (which has an explicit allow list of core tools only).
|
|
||||||
tools, _ := config["tools"].(map[string]any)
|
tools, _ := config["tools"].(map[string]any)
|
||||||
if tools == nil {
|
if tools == nil {
|
||||||
tools = make(map[string]any)
|
tools = make(map[string]any)
|
||||||
}
|
}
|
||||||
|
|
||||||
alsoAllow, _ := tools["alsoAllow"].([]any)
|
|
||||||
needed := []string{"ollama_web_search", "ollama_web_fetch"}
|
|
||||||
have := make(map[string]bool, len(alsoAllow))
|
|
||||||
for _, v := range alsoAllow {
|
|
||||||
if s, ok := v.(string); ok {
|
|
||||||
have[s] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, name := range needed {
|
|
||||||
if !have[name] {
|
|
||||||
alsoAllow = append(alsoAllow, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tools["alsoAllow"] = alsoAllow
|
|
||||||
|
|
||||||
// Disable built-in web search/fetch since our plugin replaces them.
|
|
||||||
web, _ := tools["web"].(map[string]any)
|
web, _ := tools["web"].(map[string]any)
|
||||||
if web == nil {
|
if web == nil {
|
||||||
web = make(map[string]any)
|
web = make(map[string]any)
|
||||||
}
|
}
|
||||||
web["search"] = map[string]any{"enabled": false}
|
search, _ := web["search"].(map[string]any)
|
||||||
web["fetch"] = map[string]any{"enabled": false}
|
if search == nil {
|
||||||
|
search = make(map[string]any)
|
||||||
|
}
|
||||||
|
fetch, _ := web["fetch"].(map[string]any)
|
||||||
|
if fetch == nil {
|
||||||
|
fetch = make(map[string]any)
|
||||||
|
}
|
||||||
|
|
||||||
|
alsoAllow, _ := tools["alsoAllow"].([]any)
|
||||||
|
var filteredAlsoAllow []any
|
||||||
|
for _, v := range alsoAllow {
|
||||||
|
s, ok := v.(string)
|
||||||
|
if !ok {
|
||||||
|
filteredAlsoAllow = append(filteredAlsoAllow, v)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if s == "ollama_web_search" || s == "ollama_web_fetch" {
|
||||||
|
stalePluginConfigured = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filteredAlsoAllow = append(filteredAlsoAllow, v)
|
||||||
|
}
|
||||||
|
if len(filteredAlsoAllow) > 0 {
|
||||||
|
tools["alsoAllow"] = filteredAlsoAllow
|
||||||
|
} else {
|
||||||
|
delete(tools, "alsoAllow")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := entries["openclaw-web-search"]; ok {
|
||||||
|
delete(entries, "openclaw-web-search")
|
||||||
|
stalePluginConfigured = true
|
||||||
|
}
|
||||||
|
ollamaEntry, _ := entries["ollama"].(map[string]any)
|
||||||
|
if ollamaEntry == nil {
|
||||||
|
ollamaEntry = make(map[string]any)
|
||||||
|
}
|
||||||
|
ollamaEntry["enabled"] = true
|
||||||
|
entries["ollama"] = ollamaEntry
|
||||||
|
plugins["entries"] = entries
|
||||||
|
|
||||||
|
if allow, ok := plugins["allow"].([]any); ok {
|
||||||
|
var nextAllow []any
|
||||||
|
hasOllama := false
|
||||||
|
for _, v := range allow {
|
||||||
|
s, ok := v.(string)
|
||||||
|
if ok && s == "openclaw-web-search" {
|
||||||
|
stalePluginConfigured = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ok && s == "ollama" {
|
||||||
|
hasOllama = true
|
||||||
|
}
|
||||||
|
nextAllow = append(nextAllow, v)
|
||||||
|
}
|
||||||
|
if !hasOllama {
|
||||||
|
nextAllow = append(nextAllow, "ollama")
|
||||||
|
}
|
||||||
|
plugins["allow"] = nextAllow
|
||||||
|
}
|
||||||
|
|
||||||
|
if installs, ok := plugins["installs"].(map[string]any); ok {
|
||||||
|
if _, exists := installs["openclaw-web-search"]; exists {
|
||||||
|
delete(installs, "openclaw-web-search")
|
||||||
|
stalePluginConfigured = true
|
||||||
|
}
|
||||||
|
if len(installs) > 0 {
|
||||||
|
plugins["installs"] = installs
|
||||||
|
} else {
|
||||||
|
delete(plugins, "installs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if stalePluginConfigured || search["provider"] == nil {
|
||||||
|
search["provider"] = "ollama"
|
||||||
|
}
|
||||||
|
if stalePluginConfigured {
|
||||||
|
fetch["enabled"] = true
|
||||||
|
}
|
||||||
|
search["enabled"] = true
|
||||||
|
web["search"] = search
|
||||||
|
if len(fetch) > 0 {
|
||||||
|
web["fetch"] = fetch
|
||||||
|
}
|
||||||
tools["web"] = web
|
tools["web"] = web
|
||||||
|
config["plugins"] = plugins
|
||||||
config["tools"] = tools
|
config["tools"] = tools
|
||||||
|
|
||||||
out, err := json.MarshalIndent(config, "", " ")
|
out, err := json.MarshalIndent(config, "", " ")
|
||||||
|
|||||||
@@ -2242,95 +2242,7 @@ func TestIntegrationOnboarded(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestVersionLessThan(t *testing.T) {
|
func TestConfigureOllamaWebSearch(t *testing.T) {
|
||||||
tests := []struct {
|
|
||||||
a, b string
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{"0.1.7", "0.2.1", true},
|
|
||||||
{"0.2.0", "0.2.1", true},
|
|
||||||
{"0.2.1", "0.2.1", false},
|
|
||||||
{"0.2.2", "0.2.1", false},
|
|
||||||
{"1.0.0", "0.2.1", false},
|
|
||||||
{"0.2.1", "1.0.0", true},
|
|
||||||
{"v0.1.7", "0.2.1", true},
|
|
||||||
{"0.2.1", "v0.2.1", false},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.a+"_vs_"+tt.b, func(t *testing.T) {
|
|
||||||
if got := versionLessThan(tt.a, tt.b); got != tt.want {
|
|
||||||
t.Errorf("versionLessThan(%q, %q) = %v, want %v", tt.a, tt.b, got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWebSearchPluginUpToDate(t *testing.T) {
|
|
||||||
t.Run("missing directory", func(t *testing.T) {
|
|
||||||
if webSearchPluginUpToDate(filepath.Join(t.TempDir(), "nonexistent")) {
|
|
||||||
t.Error("expected false for missing directory")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("missing package.json", func(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
if webSearchPluginUpToDate(dir) {
|
|
||||||
t.Error("expected false for missing package.json")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("old version", func(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(`{"version":"0.1.7"}`), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if webSearchPluginUpToDate(dir) {
|
|
||||||
t.Error("expected false for old version 0.1.7")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("exact minimum version", func(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(`{"version":"0.2.1"}`), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !webSearchPluginUpToDate(dir) {
|
|
||||||
t.Error("expected true for exact minimum version 0.2.1")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("newer version", func(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(`{"version":"1.0.0"}`), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !webSearchPluginUpToDate(dir) {
|
|
||||||
t.Error("expected true for newer version 1.0.0")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("invalid json", func(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(`not json`), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if webSearchPluginUpToDate(dir) {
|
|
||||||
t.Error("expected false for invalid json")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("empty version", func(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(`{"version":""}`), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if webSearchPluginUpToDate(dir) {
|
|
||||||
t.Error("expected false for empty version")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRegisterWebSearchPlugin(t *testing.T) {
|
|
||||||
home := t.TempDir()
|
home := t.TempDir()
|
||||||
setTestHome(t, home)
|
setTestHome(t, home)
|
||||||
|
|
||||||
@@ -2345,7 +2257,7 @@ func TestRegisterWebSearchPlugin(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
registerWebSearchPlugin()
|
configureOllamaWebSearch()
|
||||||
|
|
||||||
data, err := os.ReadFile(configPath)
|
data, err := os.ReadFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2361,40 +2273,30 @@ func TestRegisterWebSearchPlugin(t *testing.T) {
|
|||||||
t.Fatal("plugins section missing")
|
t.Fatal("plugins section missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check entries
|
|
||||||
entries, _ := plugins["entries"].(map[string]any)
|
entries, _ := plugins["entries"].(map[string]any)
|
||||||
entry, _ := entries["openclaw-web-search"].(map[string]any)
|
entry, _ := entries["ollama"].(map[string]any)
|
||||||
if enabled, _ := entry["enabled"].(bool); !enabled {
|
if enabled, _ := entry["enabled"].(bool); !enabled {
|
||||||
t.Error("expected entries.openclaw-web-search.enabled = true")
|
t.Error("expected entries.ollama.enabled = true")
|
||||||
|
}
|
||||||
|
if _, ok := entries["openclaw-web-search"]; ok {
|
||||||
|
t.Error("expected stale openclaw-web-search entry to be absent")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check allow list
|
if _, ok := plugins["allow"]; ok {
|
||||||
allow, _ := plugins["allow"].([]any)
|
t.Error("did not expect plugins.allow to be created when no allowlist exists")
|
||||||
found := false
|
|
||||||
for _, v := range allow {
|
|
||||||
if s, ok := v.(string); ok && s == "openclaw-web-search" {
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !found {
|
if _, ok := plugins["installs"]; ok {
|
||||||
t.Error("expected plugins.allow to contain openclaw-web-search")
|
t.Error("did not expect plugins.installs to be created")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check install provenance
|
tools, _ := config["tools"].(map[string]any)
|
||||||
installs, _ := plugins["installs"].(map[string]any)
|
web, _ := tools["web"].(map[string]any)
|
||||||
record, _ := installs["openclaw-web-search"].(map[string]any)
|
search, _ := web["search"].(map[string]any)
|
||||||
if record == nil {
|
if got, _ := search["provider"].(string); got != "ollama" {
|
||||||
t.Fatal("expected plugins.installs.openclaw-web-search")
|
t.Errorf("search provider = %q, want %q", got, "ollama")
|
||||||
}
|
}
|
||||||
if source, _ := record["source"].(string); source != "npm" {
|
if enabled, _ := search["enabled"].(bool); !enabled {
|
||||||
t.Errorf("install source = %q, want %q", source, "npm")
|
t.Error("expected tools.web.search.enabled = true")
|
||||||
}
|
|
||||||
if spec, _ := record["spec"].(string); spec != webSearchNpmPackage {
|
|
||||||
t.Errorf("install spec = %q, want %q", spec, webSearchNpmPackage)
|
|
||||||
}
|
|
||||||
expectedPath := filepath.Join(home, ".openclaw", "extensions", "openclaw-web-search")
|
|
||||||
if installPath, _ := record["installPath"].(string); installPath != expectedPath {
|
|
||||||
t.Errorf("installPath = %q, want %q", installPath, expectedPath)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -2403,8 +2305,8 @@ func TestRegisterWebSearchPlugin(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
registerWebSearchPlugin()
|
configureOllamaWebSearch()
|
||||||
registerWebSearchPlugin()
|
configureOllamaWebSearch()
|
||||||
|
|
||||||
data, err := os.ReadFile(configPath)
|
data, err := os.ReadFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2416,30 +2318,39 @@ func TestRegisterWebSearchPlugin(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
plugins, _ := config["plugins"].(map[string]any)
|
plugins, _ := config["plugins"].(map[string]any)
|
||||||
allow, _ := plugins["allow"].([]any)
|
entries, _ := plugins["entries"].(map[string]any)
|
||||||
count := 0
|
if len(entries) != 1 {
|
||||||
for _, v := range allow {
|
t.Fatalf("expected only bundled ollama entry, got %v", entries)
|
||||||
if s, ok := v.(string); ok && s == "openclaw-web-search" {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if count != 1 {
|
if _, ok := entries["ollama"]; !ok {
|
||||||
t.Errorf("expected exactly 1 openclaw-web-search in allow, got %d", count)
|
t.Fatalf("expected entries.ollama to exist, got %v", entries)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("preserves existing config", func(t *testing.T) {
|
t.Run("migrates stale plugin config and preserves unrelated settings", func(t *testing.T) {
|
||||||
initial := map[string]any{
|
initial := map[string]any{
|
||||||
"plugins": map[string]any{
|
"plugins": map[string]any{
|
||||||
"allow": []any{"some-other-plugin"},
|
"allow": []any{"some-other-plugin", "openclaw-web-search"},
|
||||||
"entries": map[string]any{
|
"entries": map[string]any{
|
||||||
"some-other-plugin": map[string]any{"enabled": true},
|
"some-other-plugin": map[string]any{"enabled": true},
|
||||||
|
"openclaw-web-search": map[string]any{"enabled": true},
|
||||||
},
|
},
|
||||||
"installs": map[string]any{
|
"installs": map[string]any{
|
||||||
"some-other-plugin": map[string]any{
|
"some-other-plugin": map[string]any{
|
||||||
"source": "npm",
|
"source": "npm",
|
||||||
"installPath": "/some/path",
|
"installPath": "/some/path",
|
||||||
},
|
},
|
||||||
|
"openclaw-web-search": map[string]any{
|
||||||
|
"source": "npm",
|
||||||
|
"installPath": "/old/path",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"tools": map[string]any{
|
||||||
|
"alsoAllow": []any{"ollama_web_search", "ollama_web_fetch", "browser"},
|
||||||
|
"web": map[string]any{
|
||||||
|
"search": map[string]any{"enabled": false},
|
||||||
|
"fetch": map[string]any{"enabled": false},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"customField": "preserved",
|
"customField": "preserved",
|
||||||
@@ -2449,7 +2360,7 @@ func TestRegisterWebSearchPlugin(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
registerWebSearchPlugin()
|
configureOllamaWebSearch()
|
||||||
|
|
||||||
out, err := os.ReadFile(configPath)
|
out, err := os.ReadFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2469,28 +2380,61 @@ func TestRegisterWebSearchPlugin(t *testing.T) {
|
|||||||
if entries["some-other-plugin"] == nil {
|
if entries["some-other-plugin"] == nil {
|
||||||
t.Error("existing plugin entry was lost")
|
t.Error("existing plugin entry was lost")
|
||||||
}
|
}
|
||||||
|
if entries["openclaw-web-search"] != nil {
|
||||||
|
t.Error("stale openclaw-web-search entry should be removed")
|
||||||
|
}
|
||||||
|
if ollamaEntry, _ := entries["ollama"].(map[string]any); ollamaEntry == nil {
|
||||||
|
t.Fatal("expected bundled ollama entry to be enabled")
|
||||||
|
}
|
||||||
|
|
||||||
installs, _ := plugins["installs"].(map[string]any)
|
installs, _ := plugins["installs"].(map[string]any)
|
||||||
if installs["some-other-plugin"] == nil {
|
if installs["some-other-plugin"] == nil {
|
||||||
t.Error("existing install record was lost")
|
t.Error("existing install record was lost")
|
||||||
}
|
}
|
||||||
|
if installs["openclaw-web-search"] != nil {
|
||||||
|
t.Error("stale openclaw-web-search install record should be removed")
|
||||||
|
}
|
||||||
|
|
||||||
allow, _ := plugins["allow"].([]any)
|
allow, _ := plugins["allow"].([]any)
|
||||||
hasOther, hasWebSearch := false, false
|
hasOther, hasStalePlugin, hasOllama := false, false, false
|
||||||
for _, v := range allow {
|
for _, v := range allow {
|
||||||
s, _ := v.(string)
|
s, _ := v.(string)
|
||||||
if s == "some-other-plugin" {
|
if s == "some-other-plugin" {
|
||||||
hasOther = true
|
hasOther = true
|
||||||
}
|
}
|
||||||
if s == "openclaw-web-search" {
|
if s == "openclaw-web-search" {
|
||||||
hasWebSearch = true
|
hasStalePlugin = true
|
||||||
|
}
|
||||||
|
if s == "ollama" {
|
||||||
|
hasOllama = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !hasOther {
|
if !hasOther {
|
||||||
t.Error("existing allow entry was lost")
|
t.Error("existing allow entry was lost")
|
||||||
}
|
}
|
||||||
if !hasWebSearch {
|
if hasStalePlugin {
|
||||||
t.Error("openclaw-web-search not added to allow")
|
t.Error("stale openclaw-web-search allow entry should be removed")
|
||||||
|
}
|
||||||
|
if !hasOllama {
|
||||||
|
t.Error("expected plugins.allow to contain bundled ollama plugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
tools, _ := config["tools"].(map[string]any)
|
||||||
|
alsoAllow, _ := tools["alsoAllow"].([]any)
|
||||||
|
if len(alsoAllow) != 1 || alsoAllow[0] != "browser" {
|
||||||
|
t.Errorf("expected stale custom web tools to be removed, got %v", alsoAllow)
|
||||||
|
}
|
||||||
|
web, _ := tools["web"].(map[string]any)
|
||||||
|
search, _ := web["search"].(map[string]any)
|
||||||
|
fetch, _ := web["fetch"].(map[string]any)
|
||||||
|
if got, _ := search["provider"].(string); got != "ollama" {
|
||||||
|
t.Errorf("search provider = %q, want %q", got, "ollama")
|
||||||
|
}
|
||||||
|
if enabled, _ := search["enabled"].(bool); !enabled {
|
||||||
|
t.Error("expected migrated tools.web.search.enabled = true")
|
||||||
|
}
|
||||||
|
if enabled, _ := fetch["enabled"].(bool); !enabled {
|
||||||
|
t.Error("expected migrated tools.web.fetch.enabled = true")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
title: Structured Outputs
|
title: Structured Outputs
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Ollama's Cloud currently does not support structured outputs.
|
||||||
|
</Note>
|
||||||
|
|
||||||
Structured outputs let you enforce a JSON schema on model responses so you can reliably extract structured data, describe images, or keep every reply consistent.
|
Structured outputs let you enforce a JSON schema on model responses so you can reliably extract structured data, describe images, or keep every reply consistent.
|
||||||
|
|
||||||
## Generating structured JSON
|
## Generating structured JSON
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Ollama handles everything automatically:
|
|||||||
1. **Install** — If OpenClaw isn't installed, Ollama prompts to install it via npm
|
1. **Install** — If OpenClaw isn't installed, Ollama prompts to install it via npm
|
||||||
2. **Security** — On the first launch, a security notice explains the risks of tool access
|
2. **Security** — On the first launch, a security notice explains the risks of tool access
|
||||||
3. **Model** — Pick a model from the selector (local or cloud)
|
3. **Model** — Pick a model from the selector (local or cloud)
|
||||||
4. **Onboarding** — Ollama configures the provider, installs the gateway daemon, sets your model as the primary, and installs the web search and fetch plugin
|
4. **Onboarding** — Ollama configures the provider, installs the gateway daemon, sets your model as the primary, and enables OpenClaw's bundled Ollama web search
|
||||||
5. **Gateway** — Starts in the background and opens the OpenClaw TUI
|
5. **Gateway** — Starts in the background and opens the OpenClaw TUI
|
||||||
|
|
||||||
<Note>OpenClaw requires a larger context window. It is recommended to use a context window of at least 64k tokens if using local models. See [Context length](/context-length) for more information.</Note>
|
<Note>OpenClaw requires a larger context window. It is recommended to use a context window of at least 64k tokens if using local models. See [Context length](/context-length) for more information.</Note>
|
||||||
@@ -24,19 +24,19 @@ Ollama handles everything automatically:
|
|||||||
|
|
||||||
## Web search and fetch
|
## Web search and fetch
|
||||||
|
|
||||||
OpenClaw ships with a web search and fetch plugin that gives local or cloud models the ability to search the web and extract readable page content.
|
OpenClaw ships with a bundled Ollama `web_search` provider that lets local or cloud-backed Ollama setups search the web through the configured Ollama host.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ollama launch openclaw
|
ollama launch openclaw
|
||||||
```
|
```
|
||||||
|
|
||||||
Web search and fetch is enabled automatically when launching OpenClaw through Ollama. To install the plugin directly:
|
Ollama web search is enabled automatically when launching OpenClaw through Ollama. To configure it manually:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openclaw plugins install @ollama/openclaw-web-search
|
openclaw configure --section web
|
||||||
```
|
```
|
||||||
|
|
||||||
<Note>Web search for local models requires `ollama signin`.</Note>
|
<Note>Ollama web search for local models requires `ollama signin`.</Note>
|
||||||
|
|
||||||
## Configure without launching
|
## Configure without launching
|
||||||
|
|
||||||
@@ -93,4 +93,3 @@ Link WhatsApp, Telegram, Slack, Discord, or iMessage to chat with your local mod
|
|||||||
```bash
|
```bash
|
||||||
openclaw gateway stop
|
openclaw gateway stop
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user