launch: add DeepSeek Harness integration (#17733)

This commit is contained in:
Parth Sareen 2026-08-13 15:19:02 -07:00 committed by GitHub
parent 7ce88bd686
commit 39df91c982
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1139 additions and 3 deletions

View file

@ -65,7 +65,7 @@ To launch a specific integration:
ollama launch claude
```
Supported integrations include [Claude Code](https://docs.ollama.com/integrations/claude-code), [Codex](https://docs.ollama.com/integrations/codex), [Copilot CLI](https://docs.ollama.com/integrations/copilot-cli), [Droid](https://docs.ollama.com/integrations/droid), and [OpenCode](https://docs.ollama.com/integrations/opencode).
Supported integrations include [Claude Code](https://docs.ollama.com/integrations/claude-code), [Codex](https://docs.ollama.com/integrations/codex), [Copilot CLI](https://docs.ollama.com/integrations/copilot-cli), [DeepSeek Harness](https://docs.ollama.com/integrations/deepseek-harness), [Droid](https://docs.ollama.com/integrations/droid), and [OpenCode](https://docs.ollama.com/integrations/opencode).
### AI assistant

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 50 50" fill="none">
<style>@media (prefers-color-scheme: dark) { path { fill: #fff; } }</style>
<path d="M48.8354 10.0479C48.3232 9.79199 48.1025 10.2798 47.8032 10.5278C46.7793 11.624 45.9048 12.1597 44.7622 12.0957C43.0923 12 41.666 12.5356 40.4058 13.8398C40.1377 12.2319 39.2476 11.272 37.8926 10.6558C36.4668 10.0156 35.9702 9.31982 35.356 7.72754C35.2456 7.3999 35.1353 7.06396 34.7651 7.00781C34.3633 6.94385 34.2056 7.2876 34.0479 7.57568C33.418 8.75195 33.1733 10.0479 33.1973 11.3599C33.2524 14.312 34.4736 16.6641 36.8999 18.3359C37.1758 18.5278 37.2466 18.7197 37.1597 19C36.9946 19.5757 36.7974 20.1357 36.624 20.7119C36.5137 21.0801 36.3486 21.1597 35.9624 21C32.4092 19.4878 30.0381 16.2319 27.2334 13.52C26.7764 13.1758 26.3193 12.856 25.8467 12.5518C23.8618 10.584 26.1069 8.96777 26.627 8.77588C27.1704 8.57568 26.8159 7.8877 25.0591 7.896C22.8691 7.90381 20.4507 9.06396 18.7095 9.58398C16.8501 9.22363 14.9199 9.14355 12.9033 9.37598C5.30859 10.2397 1.15674 16.4717 1.30664 27.2559C2.11768 31.9521 4.46582 35.8398 8.07373 38.8799C11.8159 42.0322 16.1255 43.5762 21.041 43.2803C24.0269 43.104 27.3516 42.6963 31.1016 39.4561C33.0396 40.1279 37.1758 40.208 38.1211 40.0078C39.6021 39.688 39.4995 38.2881 38.9639 38.0322C34.623 35.9678 35.5762 36.8081 34.71 36.1279C36.9155 33.4639 40.2402 30.6958 41.54 21.728C41.6426 21.0161 41.5557 20.5679 41.54 19.9917C41.5322 19.6396 41.6108 19.5039 42.0049 19.4639C46.6924 18.9116 49.064 15.9038 49.3315 11.2559C49.3711 10.7837 49.3237 10.2959 48.8354 10.0479ZM24.3262 37.8398C20.1196 34.4639 18.0791 33.3521 17.2358 33.3999C16.4482 33.4482 16.5898 34.3682 16.7632 34.9678C16.9443 35.5601 17.1812 35.9683 17.5117 36.4878C17.7402 36.832 17.8979 37.3442 17.2832 37.728C15.9282 38.584 13.5728 37.4399 13.4624 37.3838C7.97949 34.0879 4.48926 28.9282 4.19775 21.3677C4.1582 20.5757 4.38672 20.2959 5.15869 20.1519C11.8945 18.8799 17.165 22.0879 19.2529 25.7759C23.5381 30.104 25.335 35.1523 30.479 39.104C28.8643 39.2881 26.1699 39.3281 24.3262 37.8398ZM26.3433 24.6001C26.3433 24.248 26.6191 23.9678 26.9658 23.9678C27.3042 23.9678 27.5801 24.248 27.5801 24.6001C27.5801 24.9521 27.3042 25.2319 26.9575 25.2319C26.6108 25.2319 26.3433 24.9521 26.3433 24.6001ZM32.6064 27.8799C31.6372 28.2881 30.6289 28.3042 29.8096 27.688C28.6987 26.8555 28.6279 25.7759 28.7305 24.9199C28.8721 24.248 28.7144 23.8159 28.2495 23.4238C27.8716 23.104 27.3911 23.0161 26.8633 23.0161C26.666 23.0161 26.4849 22.9277 26.3511 22.856C25.8467 22.5762 25.9805 22.1758 26.5088 21.688C28.0996 20.7598 29.6362 21.9917 30.834 23.3281C31.6216 24.2559 32.8901 26.312 33.1104 26.9521C33.2446 27.3521 33.0713 27.6802 32.6064 27.8799Z" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -77,6 +77,14 @@ const LAUNCH_COMMANDS: LaunchCommand[] = [
description: "Factory's coding agent across terminal and IDEs",
icon: "/launch-icons/droid.svg",
},
{
id: "dsh",
name: "DeepSeek Harness",
command: "ollama launch dsh",
description: "DeepSeek's open-source agent harness",
icon: "/launch-icons/deepseek-harness.svg",
iconClassName: "h-7 w-7",
},
{
id: "pi",
name: "Pi",

View file

@ -0,0 +1,520 @@
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 (
deepSeekHarnessIntegrationName = "dsh"
deepSeekHarnessNpmPackage = "@deepseek-ai/dsh@latest"
deepSeekHarnessProvider = "ollama"
deepSeekHarnessAPIKeyEnv = "OLLAMA_LAUNCH_DSH_API_KEY"
deepSeekHarnessWebSettings = "web-search-deepseek"
)
var (
deepSeekHarnessLookPath = exec.LookPath
deepSeekHarnessCommand = exec.Command
deepSeekHarnessGOOS = runtime.GOOS
)
// DeepSeekHarness is the Ollama-managed DeepSeek Harness integration.
// It redirects only the settings provider for this invocation to an
// Ollama-owned document. The user's normal DSH_HOME, profiles, sessions,
// credentials, and patch layers remain available and untouched.
type DeepSeekHarness struct{}
func (d *DeepSeekHarness) String() string { return "DeepSeek Harness" }
func (d *DeepSeekHarness) Run(_ string, _ []LaunchModel, args []string) error {
if err := validateDeepSeekHarnessArgs(args); err != nil {
return err
}
bin, err := deepSeekHarnessLookPath("dsh")
if err != nil {
return fmt.Errorf("dsh is not installed: %w", err)
}
patchPath, err := deepSeekHarnessPatchPath()
if err != nil {
return err
}
cmd, err := deepSeekHarnessExecutableCommand(bin, deepSeekHarnessLaunchArgs(patchPath, args))
if err != nil {
return err
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = deepSeekHarnessLaunchEnv(os.Environ())
return cmd.Run()
}
func deepSeekHarnessLaunchArgs(patchPath string, args []string) []string {
launchArgs := []string{"web", "--patch", patchPath}
return append(launchArgs, args...)
}
func validateDeepSeekHarnessArgs(args []string) error {
for _, arg := range args {
if arg == "--patch" || strings.HasPrefix(arg, "--patch=") {
return fmt.Errorf("conflicting extra argument %q: ollama launch dsh manages --patch", arg)
}
}
return nil
}
func deepSeekHarnessLaunchEnv(env []string) []string {
return deepSeekHarnessUpsertEnv(env, deepSeekHarnessAPIKeyEnv, "ollama")
}
func deepSeekHarnessUpsertEnv(env []string, key, value string) []string {
prefix := key + "="
out := make([]string, 0, len(env)+1)
for _, entry := range env {
if strings.HasPrefix(entry, prefix) {
continue
}
out = append(out, entry)
}
return append(out, prefix+value)
}
func ensureDeepSeekHarnessInstalled() (string, error) {
if path, err := deepSeekHarnessLookPath("dsh"); err == nil {
return path, nil
}
npm, err := deepSeekHarnessLookPath("npm")
if err != nil {
return "", fmt.Errorf("dsh is not installed and npm (Node.js) is required\n\nInstall Node.js first:\n https://nodejs.org/\n\nThen re-run:\n ollama launch dsh")
}
ok, err := ConfirmPrompt("DeepSeek Harness is not installed. Install with npm?")
if err != nil {
return "", err
}
if !ok {
return "", fmt.Errorf("deepseek harness installation cancelled")
}
fmt.Fprintln(os.Stderr, "\nInstalling DeepSeek Harness...")
cmd, err := deepSeekHarnessNpmCommand(npm, []string{"install", "-g", deepSeekHarnessNpmPackage})
if err != nil {
return "", err
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("failed to install deepseek harness: %w", err)
}
path, err := deepSeekHarnessLookPath("dsh")
if err != nil {
return "", fmt.Errorf("deepseek harness was installed but dsh was not found on PATH\n\nYou may need to restart your shell")
}
fmt.Fprintf(os.Stderr, "%sDeepSeek Harness installed successfully%s\n\n", ansiGreen, ansiReset)
return path, nil
}
func deepSeekHarnessExecutableCommand(bin string, args []string) (*exec.Cmd, error) {
return deepSeekHarnessNodeShimCommand(bin, []string{"node_modules", "@deepseek-ai", "dsh", "lib", "bin.js"}, args)
}
func deepSeekHarnessNpmCommand(bin string, args []string) (*exec.Cmd, error) {
return deepSeekHarnessNodeShimCommand(bin, []string{"node_modules", "npm", "bin", "npm-cli.js"}, args)
}
// Windows npm binaries are .cmd shims, which cannot be passed safely to
// CreateProcess with an argv. Invoke their JavaScript entrypoints with Node so
// passthrough arguments remain data rather than cmd.exe syntax.
func deepSeekHarnessNodeShimCommand(shim string, entrypointParts, args []string) (*exec.Cmd, error) {
if deepSeekHarnessGOOS != "windows" || !deepSeekHarnessIsCommandShim(shim) {
return deepSeekHarnessCommand(shim, args...), nil
}
node, err := deepSeekHarnessLookPath("node")
if err != nil {
return nil, fmt.Errorf("node is required to run %s on Windows: %w", filepath.Base(shim), err)
}
entrypoint := filepath.Join(append([]string{filepath.Dir(shim)}, entrypointParts...)...)
if _, err := os.Stat(entrypoint); err != nil {
return nil, fmt.Errorf("resolve Windows entrypoint for %s: %w", filepath.Base(shim), err)
}
return deepSeekHarnessCommand(node, append([]string{entrypoint}, args...)...), nil
}
func deepSeekHarnessIsCommandShim(path string) bool {
ext := strings.ToLower(filepath.Ext(path))
return ext == ".cmd" || ext == ".bat"
}
func (d *DeepSeekHarness) Paths() []string {
settingsPath, settingsErr := deepSeekHarnessSettingsPath()
patchPath, patchErr := deepSeekHarnessPatchPath()
if settingsErr != nil || patchErr != nil {
return nil
}
return []string{settingsPath, patchPath}
}
func (d *DeepSeekHarness) Configure(modelName string) error {
return d.ConfigureWithModels(modelName, []LaunchModel{fallbackLaunchModel(modelName)})
}
func (d *DeepSeekHarness) ConfigureWithModels(primary string, models []LaunchModel) error {
if strings.TrimSpace(primary) == "" {
return nil
}
if len(models) == 0 {
models = []LaunchModel{fallbackLaunchModel(primary)}
}
if selected, ok := findLaunchModel(models, primary); ok {
primary = selected.Name
}
settingsPath, err := deepSeekHarnessSettingsPath()
if err != nil {
return err
}
settings, err := readDeepSeekHarnessYAMLDocument(settingsPath)
if err != nil {
return fmt.Errorf("parse deepseek harness launch settings: %w", err)
}
if err := applyDeepSeekHarnessSettings(settings, primary, models, shouldManageOllamaWebSearch()); err != nil {
return err
}
settingsData, err := yaml.Marshal(settings)
if err != nil {
return err
}
if err := writeDeepSeekHarnessFile(settingsPath, settingsData); err != nil {
return err
}
patchPath, err := deepSeekHarnessPatchPath()
if err != nil {
return err
}
patchData, err := yaml.Marshal([]map[string]any{
{
"id": "settings",
"config": map[string]any{
"path": settingsPath,
},
},
})
if err != nil {
return err
}
return writeDeepSeekHarnessFile(patchPath, patchData)
}
func readDeepSeekHarnessYAML(path string) (map[string]any, error) {
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return make(map[string]any), nil
}
return nil, err
}
settings := make(map[string]any)
if err := yaml.Unmarshal(data, &settings); err != nil {
return nil, err
}
if settings == nil {
settings = make(map[string]any)
}
return settings, nil
}
func readDeepSeekHarnessYAMLDocument(path string) (*yaml.Node, error) {
document := &yaml.Node{Kind: yaml.DocumentNode}
data, err := os.ReadFile(path)
if err != nil {
if !os.IsNotExist(err) {
return nil, err
}
document.Content = []*yaml.Node{{Kind: yaml.MappingNode, Tag: "!!map"}}
return document, nil
}
if err := yaml.Unmarshal(data, document); err != nil {
return nil, err
}
if len(document.Content) == 0 || document.Content[0].Kind == yaml.ScalarNode && document.Content[0].Tag == "!!null" {
document.Content = []*yaml.Node{{Kind: yaml.MappingNode, Tag: "!!map"}}
}
if document.Content[0].Kind != yaml.MappingNode {
return nil, fmt.Errorf("settings root must be a mapping")
}
return document, nil
}
func applyDeepSeekHarnessSettings(document *yaml.Node, primary string, models []LaunchModel, manageWebSearch bool) error {
settings := document.Content[0]
selected := deepSeekHarnessEnsureYAMLMapping(settings, "agent-default-model")
for key, value := range map[string]string{
"provider": deepSeekHarnessProvider,
"model": primary,
} {
if err := deepSeekHarnessSetYAMLValue(selected, key, value); err != nil {
return err
}
}
llm := deepSeekHarnessEnsureYAMLMapping(settings, "llm-pi-ai")
providers := deepSeekHarnessEnsureYAMLMapping(llm, "providers")
provider := deepSeekHarnessEnsureYAMLMapping(providers, deepSeekHarnessProvider)
for key, value := range map[string]any{
"displayName": "Ollama",
"apiKeyEnv": deepSeekHarnessAPIKeyEnv,
"api": "openai-completions",
"baseURL": deepSeekHarnessBaseURL(),
"models": deepSeekHarnessModelConfigs(primary, models),
} {
if err := deepSeekHarnessSetYAMLValue(provider, key, value); err != nil {
return err
}
}
if !manageWebSearch {
return nil
}
// Harness's bundled search provider appends /messages to this /v1 base and
// sends the Anthropic web_search server tool. This is separate from the main
// model provider above; Harness does not expose a configured way to send the
// native OpenAI Responses web_search tool.
web := deepSeekHarnessEnsureYAMLMapping(settings, deepSeekHarnessWebSettings)
for key, value := range map[string]string{
"apiKeyEnv": deepSeekHarnessAPIKeyEnv,
"baseURL": deepSeekHarnessBaseURL(),
"model": primary,
} {
if err := deepSeekHarnessSetYAMLValue(web, key, value); err != nil {
return err
}
}
return nil
}
func deepSeekHarnessEnsureYAMLMapping(mapping *yaml.Node, key string) *yaml.Node {
if value := deepSeekHarnessYAMLValue(mapping, key); value != nil && value.Kind == yaml.MappingNode {
return value
}
value := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
deepSeekHarnessSetYAMLNode(mapping, key, value)
return value
}
func deepSeekHarnessSetYAMLValue(mapping *yaml.Node, key string, value any) error {
node := &yaml.Node{}
if err := node.Encode(value); err != nil {
return err
}
deepSeekHarnessSetYAMLNode(mapping, key, node)
return nil
}
func deepSeekHarnessSetYAMLNode(mapping *yaml.Node, key string, value *yaml.Node) {
for i := 0; i+1 < len(mapping.Content); i += 2 {
if mapping.Content[i].Value == key {
mapping.Content[i+1] = value
return
}
}
mapping.Content = append(mapping.Content,
&yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: key},
value,
)
}
func deepSeekHarnessYAMLValue(mapping *yaml.Node, key string) *yaml.Node {
for i := 0; i+1 < len(mapping.Content); i += 2 {
if mapping.Content[i].Value == key {
return mapping.Content[i+1]
}
}
return nil
}
func deepSeekHarnessModelConfigs(primary string, models []LaunchModel) []any {
ordered := append([]LaunchModel(nil), models...)
if selected, ok := findLaunchModel(ordered, primary); ok {
ordered = append([]LaunchModel{selected}, removeLaunchModel(ordered, primary)...)
} else {
ordered = append([]LaunchModel{fallbackLaunchModel(primary)}, ordered...)
}
configs := make([]any, 0, len(ordered))
seen := make(map[string]bool, len(ordered))
for _, item := range ordered {
if item.Name == "" || seen[item.Name] {
continue
}
seen[item.Name] = true
entry := map[string]any{
"id": item.Name,
"name": item.Name,
"input": []string{"text"},
}
if slices.Contains(item.Capabilities, model.CapabilityVision) {
entry["input"] = []string{"text", "image"}
}
if item.ContextLength > 0 {
entry["contextWindow"] = item.ContextLength
}
if item.MaxOutputTokens > 0 {
entry["maxTokens"] = item.MaxOutputTokens
}
configs = append(configs, entry)
}
return configs
}
func (d *DeepSeekHarness) CurrentModel() string {
settingsPath, err := deepSeekHarnessSettingsPath()
if err != nil {
return ""
}
if !deepSeekHarnessPatchHealthy(settingsPath) {
return ""
}
settings, err := readDeepSeekHarnessYAML(settingsPath)
if err != nil {
return ""
}
selected, _ := settings["agent-default-model"].(map[string]any)
if selected == nil || selected["provider"] != deepSeekHarnessProvider {
return ""
}
modelName, _ := selected["model"].(string)
if modelName == "" {
return ""
}
llm, _ := settings["llm-pi-ai"].(map[string]any)
providers, _ := llm["providers"].(map[string]any)
provider, _ := providers[deepSeekHarnessProvider].(map[string]any)
if !deepSeekHarnessProviderHealthy(provider, modelName) {
return ""
}
if !shouldManageOllamaWebSearch() {
return modelName
}
web, _ := settings[deepSeekHarnessWebSettings].(map[string]any)
if !deepSeekHarnessWebProviderHealthy(web, modelName) {
return ""
}
return modelName
}
func deepSeekHarnessPatchHealthy(settingsPath string) bool {
patchPath, err := deepSeekHarnessPatchPath()
if err != nil {
return false
}
data, err := os.ReadFile(patchPath)
if err != nil {
return false
}
var patches []struct {
ID string `yaml:"id"`
Config struct {
Path string `yaml:"path"`
} `yaml:"config"`
}
if err := yaml.Unmarshal(data, &patches); err != nil || len(patches) != 1 {
return false
}
return patches[0].ID == "settings" && patches[0].Config.Path == settingsPath
}
func deepSeekHarnessProviderHealthy(provider map[string]any, modelName string) bool {
if provider == nil || provider["api"] != "openai-completions" || provider["apiKeyEnv"] != deepSeekHarnessAPIKeyEnv {
return false
}
baseURL, _ := provider["baseURL"].(string)
if strings.TrimRight(baseURL, "/") != strings.TrimRight(deepSeekHarnessBaseURL(), "/") {
return false
}
models, _ := provider["models"].([]any)
for _, raw := range models {
entry, _ := raw.(map[string]any)
if entry["id"] == modelName {
return true
}
}
return false
}
func deepSeekHarnessWebProviderHealthy(web map[string]any, modelName string) bool {
if web == nil || web["apiKeyEnv"] != deepSeekHarnessAPIKeyEnv || web["model"] != modelName {
return false
}
baseURL, _ := web["baseURL"].(string)
return strings.TrimRight(baseURL, "/") == strings.TrimRight(deepSeekHarnessBaseURL(), "/")
}
func (d *DeepSeekHarness) Onboard() error {
return config.MarkIntegrationOnboarded(deepSeekHarnessIntegrationName)
}
func (d *DeepSeekHarness) RequiresInteractiveOnboarding() bool { return false }
func deepSeekHarnessBaseURL() string {
return strings.TrimRight(envconfig.ConnectableHost().String(), "/") + "/v1"
}
func deepSeekHarnessConfigDir() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, ".ollama", "launch", "dsh"), nil
}
func deepSeekHarnessSettingsPath() (string, error) {
dir, err := deepSeekHarnessConfigDir()
if err != nil {
return "", err
}
return filepath.Join(dir, "settings.yaml"), nil
}
func deepSeekHarnessPatchPath() (string, error) {
dir, err := deepSeekHarnessConfigDir()
if err != nil {
return "", err
}
return filepath.Join(dir, "ollama.cordis.yml"), nil
}
func writeDeepSeekHarnessFile(path string, data []byte) error {
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0o700); err != nil {
return err
}
if err := os.Chmod(dir, 0o700); err != nil {
return err
}
if err := fileutil.WriteWithBackup(path, data, deepSeekHarnessIntegrationName); err != nil {
return err
}
return os.Chmod(path, 0o600)
}

View file

@ -0,0 +1,461 @@
package launch
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"runtime"
"slices"
"strings"
"testing"
"github.com/ollama/ollama/types/model"
"gopkg.in/yaml.v3"
)
func TestDeepSeekHarnessRegistry(t *testing.T) {
spec, err := LookupIntegrationSpec("deepseek-harness")
if err != nil {
t.Fatal(err)
}
if spec.Name != deepSeekHarnessIntegrationName {
t.Fatalf("canonical name = %q, want %q", spec.Name, deepSeekHarnessIntegrationName)
}
if spec.Runner.String() != "DeepSeek Harness" {
t.Fatalf("display name = %q", spec.Runner.String())
}
if got := strings.Join(spec.Install.Command, " "); got != "npm install -g @deepseek-ai/dsh@latest" {
t.Fatalf("install command = %q", got)
}
}
func TestDeepSeekHarnessConfigurePreservesSettingsAndIsIdempotent(t *testing.T) {
home := t.TempDir()
setTestHome(t, home)
t.Setenv("OLLAMA_HOST", "http://127.0.0.1:12345")
settingsPath, err := deepSeekHarnessSettingsPath()
if err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Dir(settingsPath), 0o700); err != nil {
t.Fatal(err)
}
existing := []byte("# keep-comment\ndefaults: &defaults\n mode: dark\ntheme: *defaults\nagent-default-model:\n # keep-reasoning-comment\n reasoningEffort: high\nllm-pi-ai:\n providers:\n custom:\n api: openai-completions\n baseURL: https://example.invalid/v1\n models:\n - id: custom-model\n ollama:\n # keep-retry-comment\n retryPolicy:\n maxAttempts: 2\nweb-search-deepseek:\n maxUses: 3\n")
if err := os.WriteFile(settingsPath, existing, 0o600); err != nil {
t.Fatal(err)
}
models := []LaunchModel{
{Name: "qwen3.5:latest", ContextLength: 262144, MaxOutputTokens: 32768, Capabilities: []model.Capability{model.CapabilityVision}},
{Name: "kimi-k2.6:cloud", ContextLength: 262144, MaxOutputTokens: 262144},
}
dsh := &DeepSeekHarness{}
if err := dsh.ConfigureWithModels("qwen3.5", models); err != nil {
t.Fatal(err)
}
firstSettings, err := os.ReadFile(settingsPath)
if err != nil {
t.Fatal(err)
}
patchPath, err := deepSeekHarnessPatchPath()
if err != nil {
t.Fatal(err)
}
firstPatch, err := os.ReadFile(patchPath)
if err != nil {
t.Fatal(err)
}
if err := dsh.ConfigureWithModels("qwen3.5", models); err != nil {
t.Fatal(err)
}
secondSettings, _ := os.ReadFile(settingsPath)
secondPatch, _ := os.ReadFile(patchPath)
if string(firstSettings) != string(secondSettings) || string(firstPatch) != string(secondPatch) {
t.Fatal("repeated configuration changed Ollama-managed files")
}
for _, preserved := range []string{"# keep-comment", "&defaults", "*defaults", "# keep-reasoning-comment", "# keep-retry-comment"} {
if !strings.Contains(string(firstSettings), preserved) {
t.Fatalf("settings did not preserve %q:\n%s", preserved, firstSettings)
}
}
var settings map[string]any
if err := yaml.Unmarshal(firstSettings, &settings); err != nil {
t.Fatal(err)
}
if theme, _ := settings["theme"].(map[string]any); theme["mode"] != "dark" {
t.Fatalf("unrelated settings were not preserved: %#v", settings["theme"])
}
selected, _ := settings["agent-default-model"].(map[string]any)
if selected["provider"] != deepSeekHarnessProvider || selected["model"] != "qwen3.5:latest" {
t.Fatalf("default model = %#v", selected)
}
if selected["reasoningEffort"] != "high" {
t.Fatalf("default model settings were not preserved: %#v", selected)
}
llm, _ := settings["llm-pi-ai"].(map[string]any)
providers, _ := llm["providers"].(map[string]any)
if providers["custom"] == nil {
t.Fatal("custom provider was removed")
}
provider, _ := providers[deepSeekHarnessProvider].(map[string]any)
if provider["baseURL"] != "http://127.0.0.1:12345/v1" || provider["apiKeyEnv"] != deepSeekHarnessAPIKeyEnv {
t.Fatalf("Ollama provider = %#v", provider)
}
retryPolicy, _ := provider["retryPolicy"].(map[string]any)
if retryPolicy["maxAttempts"] != 2 {
t.Fatalf("Ollama provider settings were not preserved: %#v", provider)
}
configuredModels, _ := provider["models"].([]any)
if len(configuredModels) != 2 {
t.Fatalf("configured models = %#v", configuredModels)
}
local, _ := configuredModels[0].(map[string]any)
if local["id"] != "qwen3.5:latest" || local["contextWindow"] != 262144 || local["maxTokens"] != 32768 {
t.Fatalf("local model = %#v", local)
}
if got, _ := local["input"].([]any); !slices.Equal(got, []any{"text", "image"}) {
t.Fatalf("local model input = %#v", local["input"])
}
cloud, _ := configuredModels[1].(map[string]any)
if cloud["id"] != "kimi-k2.6:cloud" || cloud["contextWindow"] != 262144 || cloud["maxTokens"] != 262144 {
t.Fatalf("cloud model = %#v", cloud)
}
web, _ := settings[deepSeekHarnessWebSettings].(map[string]any)
if web["baseURL"] != "http://127.0.0.1:12345/v1" || web["apiKeyEnv"] != deepSeekHarnessAPIKeyEnv || web["model"] != "qwen3.5:latest" {
t.Fatalf("Ollama web search provider = %#v", web)
}
if web["maxUses"] != 3 {
t.Fatalf("existing web search settings were not preserved: %#v", web)
}
var patches []map[string]any
if err := yaml.Unmarshal(firstPatch, &patches); err != nil {
t.Fatal(err)
}
if len(patches) != 1 || patches[0]["id"] != "settings" {
t.Fatalf("patches = %#v", patches)
}
patchConfig, _ := patches[0]["config"].(map[string]any)
if patchConfig["path"] != settingsPath {
t.Fatalf("settings patch path = %#v", patchConfig["path"])
}
if got := dsh.CurrentModel(); got != "qwen3.5:latest" {
t.Fatalf("CurrentModel() = %q", got)
}
}
func TestDeepSeekHarnessConfigureSkipsWebSearchWhenCloudDisabled(t *testing.T) {
home := t.TempDir()
setTestHome(t, home)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/status" {
fmt.Fprint(w, `{"cloud":{"disabled":true,"source":"config"}}`)
return
}
http.NotFound(w, r)
}))
t.Cleanup(srv.Close)
t.Setenv("OLLAMA_HOST", srv.URL)
settingsPath, err := deepSeekHarnessSettingsPath()
if err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Dir(settingsPath), 0o700); err != nil {
t.Fatal(err)
}
existing := []byte("web-search-deepseek:\n maxUses: 3\n")
if err := os.WriteFile(settingsPath, existing, 0o600); err != nil {
t.Fatal(err)
}
dsh := &DeepSeekHarness{}
if err := dsh.ConfigureWithModels("qwen3.5", []LaunchModel{{Name: "qwen3.5:latest"}}); err != nil {
t.Fatal(err)
}
settings, err := readDeepSeekHarnessYAML(settingsPath)
if err != nil {
t.Fatal(err)
}
web, _ := settings[deepSeekHarnessWebSettings].(map[string]any)
if len(web) != 1 || web["maxUses"] != 3 {
t.Fatalf("web search settings = %#v", web)
}
if got := dsh.CurrentModel(); got != "qwen3.5:latest" {
t.Fatalf("CurrentModel() = %q", got)
}
}
func TestDeepSeekHarnessCurrentModelRejectsDrift(t *testing.T) {
setTestHome(t, t.TempDir())
t.Setenv("OLLAMA_HOST", "http://127.0.0.1:11434")
dsh := &DeepSeekHarness{}
if err := dsh.Configure("qwen3.5"); err != nil {
t.Fatal(err)
}
t.Setenv("OLLAMA_HOST", "http://127.0.0.1:9999")
if got := dsh.CurrentModel(); got != "" {
t.Fatalf("CurrentModel() = %q for stale endpoint", got)
}
}
func TestDeepSeekHarnessCurrentModelRejectsPatchDrift(t *testing.T) {
setTestHome(t, t.TempDir())
t.Setenv("OLLAMA_HOST", "http://127.0.0.1:11434")
dsh := &DeepSeekHarness{}
if err := dsh.Configure("qwen3.5"); err != nil {
t.Fatal(err)
}
patchPath, err := deepSeekHarnessPatchPath()
if err != nil {
t.Fatal(err)
}
for name, data := range map[string][]byte{
"missing": nil,
"malformed": []byte("["),
"wrong path": []byte(`- id: settings
config:
path: /tmp/not-managed.yaml
`),
} {
t.Run(name, func(t *testing.T) {
if err := dsh.Configure("qwen3.5"); err != nil {
t.Fatal(err)
}
if data == nil {
if err := os.Remove(patchPath); err != nil {
t.Fatal(err)
}
} else if err := os.WriteFile(patchPath, data, 0o600); err != nil {
t.Fatal(err)
}
if got := dsh.CurrentModel(); got != "" {
t.Fatalf("CurrentModel() = %q", got)
}
})
}
}
func TestDeepSeekHarnessConfigureRejectsMalformedSettingsWithoutOverwrite(t *testing.T) {
setTestHome(t, t.TempDir())
settingsPath, err := deepSeekHarnessSettingsPath()
if err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Dir(settingsPath), 0o700); err != nil {
t.Fatal(err)
}
malformed := []byte("llm-pi-ai: [")
if err := os.WriteFile(settingsPath, malformed, 0o600); err != nil {
t.Fatal(err)
}
err = (&DeepSeekHarness{}).Configure("qwen3.5")
if err == nil || !strings.Contains(err.Error(), "parse deepseek harness launch settings") {
t.Fatalf("Configure() error = %v", err)
}
got, err := os.ReadFile(settingsPath)
if err != nil {
t.Fatal(err)
}
if !slices.Equal(got, malformed) {
t.Fatalf("malformed settings were overwritten: %q", got)
}
}
func TestDeepSeekHarnessConfigureAcceptsNullSettings(t *testing.T) {
setTestHome(t, t.TempDir())
settingsPath, err := deepSeekHarnessSettingsPath()
if err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Dir(settingsPath), 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(settingsPath, []byte("null\n"), 0o600); err != nil {
t.Fatal(err)
}
if err := (&DeepSeekHarness{}).Configure("qwen3.5"); err != nil {
t.Fatal(err)
}
}
func TestDeepSeekHarnessRunUsesManagedPatchAndCredential(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("uses a POSIX shell test binary")
}
home := t.TempDir()
setTestHome(t, home)
binDir := t.TempDir()
logPath := filepath.Join(home, "dsh-invocation")
script := "#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$DSH_TEST_LOG\"\nprintf '%s\\n' \"$OLLAMA_LAUNCH_DSH_API_KEY\" >> \"$DSH_TEST_LOG\"\n"
bin := filepath.Join(binDir, "dsh")
if err := os.WriteFile(bin, []byte(script), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", strings.Join([]string{binDir, "/bin", "/usr/bin"}, string(os.PathListSeparator)))
t.Setenv("DSH_TEST_LOG", logPath)
t.Setenv(deepSeekHarnessAPIKeyEnv, "do-not-keep")
dsh := &DeepSeekHarness{}
if err := dsh.Configure("qwen3.5"); err != nil {
t.Fatal(err)
}
if err := dsh.Run("qwen3.5", nil, []string{"--port", "0"}); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(logPath)
if err != nil {
t.Fatal(err)
}
patchPath, _ := deepSeekHarnessPatchPath()
want := "web\n--patch\n" + patchPath + "\n--port\n0\nollama\n"
if string(data) != want {
t.Fatalf("invocation = %q, want %q", data, want)
}
}
func TestDeepSeekHarnessRejectsManagedPatchArgument(t *testing.T) {
for _, args := range [][]string{{"--patch", "other.yml"}, {"--patch=other.yml"}} {
if err := (&DeepSeekHarness{}).Run("qwen3.5", nil, args); err == nil || !strings.Contains(err.Error(), "manages --patch") {
t.Fatalf("Run(%v) error = %v", args, err)
}
}
}
func TestEnsureDeepSeekHarnessInstalledUsesPublicNpmPackage(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("uses a POSIX shell test binary")
}
home := t.TempDir()
binDir := t.TempDir()
logPath := filepath.Join(home, "npm-invocation")
npm := filepath.Join(binDir, "npm")
script := "#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$DSH_NPM_LOG\"\nprintf '#!/bin/sh\\nexit 0\\n' > \"$DSH_INSTALLED_BIN\"\nchmod +x \"$DSH_INSTALLED_BIN\"\n"
if err := os.WriteFile(npm, []byte(script), 0o755); err != nil {
t.Fatal(err)
}
dshBin := filepath.Join(binDir, "dsh")
t.Setenv("PATH", strings.Join([]string{binDir, "/bin", "/usr/bin"}, string(os.PathListSeparator)))
t.Setenv("DSH_NPM_LOG", logPath)
t.Setenv("DSH_INSTALLED_BIN", dshBin)
restore := withLaunchConfirmPolicy(launchConfirmPolicy{yes: true})
defer restore()
path, err := ensureDeepSeekHarnessInstalled()
if err != nil {
t.Fatal(err)
}
if path != dshBin {
t.Fatalf("installed path = %q, want %q", path, dshBin)
}
data, err := os.ReadFile(logPath)
if err != nil {
t.Fatal(err)
}
if string(data) != "install\n-g\n@deepseek-ai/dsh@latest\n" {
t.Fatalf("npm invocation = %q", data)
}
}
func TestDeepSeekHarnessLaunchArgs(t *testing.T) {
got := deepSeekHarnessLaunchArgs("/tmp/ollama.cordis.yml", []string{"--port", "0"})
want := []string{"web", "--patch", "/tmp/ollama.cordis.yml", "--port", "0"}
if !slices.Equal(got, want) {
t.Fatalf("launch args = %v, want %v", got, want)
}
}
func TestDeepSeekHarnessWindowsNodeShims(t *testing.T) {
root := t.TempDir()
node := filepath.Join(root, "node.exe")
dsh := filepath.Join(root, "npm", "dsh.cmd")
npm := filepath.Join(root, "node", "npm.cmd")
dshEntrypoint := filepath.Join(filepath.Dir(dsh), "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js")
npmEntrypoint := filepath.Join(filepath.Dir(npm), "node_modules", "npm", "bin", "npm-cli.js")
for _, path := range []string{node, dsh, npm, dshEntrypoint, npmEntrypoint} {
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, nil, 0o755); err != nil {
t.Fatal(err)
}
}
originalGOOS := deepSeekHarnessGOOS
originalLookPath := deepSeekHarnessLookPath
deepSeekHarnessGOOS = "windows"
deepSeekHarnessLookPath = func(file string) (string, error) {
if file == "node" {
return node, nil
}
return "", exec.ErrNotFound
}
t.Cleanup(func() {
deepSeekHarnessGOOS = originalGOOS
deepSeekHarnessLookPath = originalLookPath
})
t.Run("dsh", func(t *testing.T) {
cmd, err := deepSeekHarnessExecutableCommand(dsh, []string{"web", "--port", "0", "a&b"})
if err != nil {
t.Fatal(err)
}
want := []string{node, dshEntrypoint, "web", "--port", "0", "a&b"}
if !slices.Equal(cmd.Args, want) {
t.Fatalf("command args = %v, want %v", cmd.Args, want)
}
})
t.Run("npm", func(t *testing.T) {
cmd, err := deepSeekHarnessNpmCommand(npm, []string{"install", "-g", deepSeekHarnessNpmPackage})
if err != nil {
t.Fatal(err)
}
want := []string{node, npmEntrypoint, "install", "-g", deepSeekHarnessNpmPackage}
if !slices.Equal(cmd.Args, want) {
t.Fatalf("command args = %v, want %v", cmd.Args, want)
}
})
}
func TestDeepSeekHarnessWindowsNodeShimRequiresEntrypoint(t *testing.T) {
originalGOOS := deepSeekHarnessGOOS
originalLookPath := deepSeekHarnessLookPath
deepSeekHarnessGOOS = "windows"
deepSeekHarnessLookPath = func(file string) (string, error) {
if file == "node" {
return filepath.Join(t.TempDir(), "node.exe"), nil
}
return "", exec.ErrNotFound
}
t.Cleanup(func() {
deepSeekHarnessGOOS = originalGOOS
deepSeekHarnessLookPath = originalLookPath
})
_, err := deepSeekHarnessExecutableCommand(filepath.Join(t.TempDir(), "dsh.cmd"), nil)
if err == nil || !strings.Contains(err.Error(), "resolve Windows entrypoint") {
t.Fatalf("error = %v", err)
}
}
func TestDeepSeekHarnessInstallDependencyError(t *testing.T) {
originalLookPath := deepSeekHarnessLookPath
deepSeekHarnessLookPath = func(file string) (string, error) { return "", exec.ErrNotFound }
t.Cleanup(func() { deepSeekHarnessLookPath = originalLookPath })
_, err := ensureDeepSeekHarnessInstalled()
if err == nil || !strings.Contains(err.Error(), "npm (Node.js) is required") {
t.Fatalf("error = %v", err)
}
}

View file

@ -67,6 +67,8 @@ func TestIntegrationLookup(t *testing.T) {
{"muse", "muse", true, "Muse Code"},
{"muse alias", "muse-code", true, "Muse Code"},
{"droid", "droid", true, "Droid"},
{"dsh", "dsh", true, "DeepSeek Harness"},
{"deepseek harness alias", "deepseek-harness", true, "DeepSeek Harness"},
{"opencode", "opencode", true, "OpenCode"},
{"omp", "omp", true, "OMP"},
{"pool", "pool", true, "Pool"},
@ -88,7 +90,7 @@ func TestIntegrationLookup(t *testing.T) {
}
func TestIntegrationRegistry(t *testing.T) {
expectedIntegrations := []string{"claude", "claude-desktop", "cline", "codex", "chatgpt", "kimi", "muse", "droid", "opencode", "omp", "hermes", "hermes-desktop", "pool", "qwen"}
expectedIntegrations := []string{"claude", "claude-desktop", "cline", "codex", "chatgpt", "kimi", "muse", "droid", "dsh", "opencode", "omp", "hermes", "hermes-desktop", "pool", "qwen"}
for _, name := range expectedIntegrations {
t.Run(name, func(t *testing.T) {
r, ok := integrations[name]

View file

@ -296,6 +296,7 @@ Supported integrations:
copilot Copilot CLI (aliases: copilot-cli)
omp OMP
droid Droid
dsh DeepSeek Harness (alias: deepseek-harness)
kimi Kimi Code CLI
muse Muse Code (aliases: muse-code)
pi Pi
@ -312,6 +313,7 @@ Examples:
ollama launch chatgpt --restore
ollama launch hermes
ollama launch hermes-desktop
ollama launch dsh
ollama launch droid --config (does not auto-launch)
ollama launch codex --restore
ollama launch codex -- --sandbox workspace-write`,

View file

@ -151,6 +151,21 @@ func (r *launcherManagedListRunner) ConfigureWithModels(primary string, models [
return r.Configure(primary)
}
type launcherCanonicalManagedListRunner struct {
launcherManagedListRunner
}
func (r *launcherCanonicalManagedListRunner) ConfigureWithModels(primary string, models []LaunchModel) error {
r.configuredModelLists = append(r.configuredModelLists, launchModelNames(models))
r.configured = append(r.configured, primary)
if selected, ok := findLaunchModel(models, primary); ok {
r.currentModel = selected.Name
} else {
r.currentModel = primary
}
return nil
}
type launcherManagedAutodiscoveryRunner struct {
launcherManagedRunner
autodiscoveryConfigures int
@ -1089,6 +1104,51 @@ func TestLaunchIntegration_ManagedSingleIntegrationCanConfigureWithModelList(t *
}
}
func TestLaunchIntegration_ManagedSingleIntegrationSavesCanonicalModel(t *testing.T) {
tmpDir := t.TempDir()
setLaunchTestHome(t, tmpDir)
withInteractiveSession(t, true)
withLauncherHooks(t)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/experimental/model-recommendations":
fmt.Fprint(w, `{"recommendations":[]}`)
case "/api/tags":
fmt.Fprint(w, `{"models":[{"name":"qwen3.5:latest"}]}`)
case "/api/show":
fmt.Fprint(w, `{"model_info":{"general.context_length":131072}}`)
default:
http.NotFound(w, r)
}
}))
defer srv.Close()
t.Setenv("OLLAMA_HOST", srv.URL)
runner := &launcherCanonicalManagedListRunner{}
withIntegrationOverride(t, "stubmanaged", runner)
request := IntegrationLaunchRequest{Name: "stubmanaged", ModelOverride: "qwen3.5"}
if err := LaunchIntegration(context.Background(), request); err != nil {
t.Fatalf("first LaunchIntegration returned error: %v", err)
}
saved, err := config.LoadIntegration("stubmanaged")
if err != nil {
t.Fatalf("failed to reload managed integration config: %v", err)
}
if diff := compareStrings(saved.Models, []string{"qwen3.5:latest"}); diff != "" {
t.Fatalf("saved models mismatch: %s", diff)
}
if err := LaunchIntegration(context.Background(), IntegrationLaunchRequest{Name: "stubmanaged"}); err != nil {
t.Fatalf("second LaunchIntegration returned error: %v", err)
}
if diff := compareStrings(runner.configured, []string{"qwen3.5"}); diff != "" {
t.Fatalf("expected second launch to skip configuration: %s", diff)
}
}
func TestLaunchIntegration_ManagedAutodiscoverySkipsModelPicker(t *testing.T) {
tmpDir := t.TempDir()
setLaunchTestHome(t, tmpDir)

View file

@ -323,6 +323,9 @@ func prepareManagedSingleIntegration(name string, managed ManagedSingleModel, mo
if err != nil {
return fmt.Errorf("setup failed: %w", err)
}
if current := managed.CurrentModel(); current != "" {
model = current
}
if err := config.SaveIntegration(name, []string{model}); err != nil {
return fmt.Errorf("failed to save: %w", err)
}

View file

@ -33,7 +33,7 @@ type IntegrationInfo struct {
Description string
}
var launcherIntegrationOrder = []string{"claude", "chatgpt", "hermes", "openclaw", "opencode", "hermes-desktop", "codex", "copilot", "omp", "cline", "droid", "pi", "pool", "qwen"}
var launcherIntegrationOrder = []string{"claude", "chatgpt", "hermes", "openclaw", "opencode", "hermes-desktop", "codex", "copilot", "omp", "cline", "droid", "dsh", "pi", "pool", "qwen"}
var integrationSpecs = []*IntegrationSpec{
{
@ -166,6 +166,24 @@ var integrationSpecs = []*IntegrationSpec{
URL: "https://docs.factory.ai/cli/getting-started/quickstart",
},
},
{
Name: deepSeekHarnessIntegrationName,
Runner: &DeepSeekHarness{},
Aliases: []string{"deepseek-harness"},
Description: "DeepSeek's open-source agent harness",
Install: IntegrationInstallSpec{
CheckInstalled: func() bool {
_, err := deepSeekHarnessLookPath("dsh")
return err == nil
},
EnsureInstalled: func() error {
_, err := ensureDeepSeekHarnessInstalled()
return err
},
URL: "https://github.com/deepseek-ai/deepseek-harness",
Command: []string{"npm", "install", "-g", deepSeekHarnessNpmPackage},
},
},
{
Name: "opencode",
Runner: &OpenCode{},

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 50 50" fill="none">
<style>@media (prefers-color-scheme: dark) { path { fill: #fff; } }</style>
<path d="M48.8354 10.0479C48.3232 9.79199 48.1025 10.2798 47.8032 10.5278C46.7793 11.624 45.9048 12.1597 44.7622 12.0957C43.0923 12 41.666 12.5356 40.4058 13.8398C40.1377 12.2319 39.2476 11.272 37.8926 10.6558C36.4668 10.0156 35.9702 9.31982 35.356 7.72754C35.2456 7.3999 35.1353 7.06396 34.7651 7.00781C34.3633 6.94385 34.2056 7.2876 34.0479 7.57568C33.418 8.75195 33.1733 10.0479 33.1973 11.3599C33.2524 14.312 34.4736 16.6641 36.8999 18.3359C37.1758 18.5278 37.2466 18.7197 37.1597 19C36.9946 19.5757 36.7974 20.1357 36.624 20.7119C36.5137 21.0801 36.3486 21.1597 35.9624 21C32.4092 19.4878 30.0381 16.2319 27.2334 13.52C26.7764 13.1758 26.3193 12.856 25.8467 12.5518C23.8618 10.584 26.1069 8.96777 26.627 8.77588C27.1704 8.57568 26.8159 7.8877 25.0591 7.896C22.8691 7.90381 20.4507 9.06396 18.7095 9.58398C16.8501 9.22363 14.9199 9.14355 12.9033 9.37598C5.30859 10.2397 1.15674 16.4717 1.30664 27.2559C2.11768 31.9521 4.46582 35.8398 8.07373 38.8799C11.8159 42.0322 16.1255 43.5762 21.041 43.2803C24.0269 43.104 27.3516 42.6963 31.1016 39.4561C33.0396 40.1279 37.1758 40.208 38.1211 40.0078C39.6021 39.688 39.4995 38.2881 38.9639 38.0322C34.623 35.9678 35.5762 36.8081 34.71 36.1279C36.9155 33.4639 40.2402 30.6958 41.54 21.728C41.6426 21.0161 41.5557 20.5679 41.54 19.9917C41.5322 19.6396 41.6108 19.5039 42.0049 19.4639C46.6924 18.9116 49.064 15.9038 49.3315 11.2559C49.3711 10.7837 49.3237 10.2959 48.8354 10.0479ZM24.3262 37.8398C20.1196 34.4639 18.0791 33.3521 17.2358 33.3999C16.4482 33.4482 16.5898 34.3682 16.7632 34.9678C16.9443 35.5601 17.1812 35.9683 17.5117 36.4878C17.7402 36.832 17.8979 37.3442 17.2832 37.728C15.9282 38.584 13.5728 37.4399 13.4624 37.3838C7.97949 34.0879 4.48926 28.9282 4.19775 21.3677C4.1582 20.5757 4.38672 20.2959 5.15869 20.1519C11.8945 18.8799 17.165 22.0879 19.2529 25.7759C23.5381 30.104 25.335 35.1523 30.479 39.104C28.8643 39.2881 26.1699 39.3281 24.3262 37.8398ZM26.3433 24.6001C26.3433 24.248 26.6191 23.9678 26.9658 23.9678C27.3042 23.9678 27.5801 24.248 27.5801 24.6001C27.5801 24.9521 27.3042 25.2319 26.9575 25.2319C26.6108 25.2319 26.3433 24.9521 26.3433 24.6001ZM32.6064 27.8799C31.6372 28.2881 30.6289 28.3042 29.8096 27.688C28.6987 26.8555 28.6279 25.7759 28.7305 24.9199C28.8721 24.248 28.7144 23.8159 28.2495 23.4238C27.8716 23.104 27.3911 23.0161 26.8633 23.0161C26.666 23.0161 26.4849 22.9277 26.3511 22.856C25.8467 22.5762 25.9805 22.1758 26.5088 21.688C28.0996 20.7598 29.6362 21.9917 30.834 23.3281C31.6216 24.2559 32.8901 26.312 33.1104 26.9521C33.2446 27.3521 33.0713 27.6802 32.6064 27.8799Z" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -0,0 +1,50 @@
---
title: DeepSeek Harness
---
[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) is an open-source coding agent.
<Note>DeepSeek Harness is currently a developer preview. Its upstream configuration may change between releases.</Note>
## Setup
```shell
ollama launch dsh
```
Ollama installs `@deepseek-ai/dsh` if needed. To choose a model:
```shell
ollama launch dsh --model qwen3.5
ollama launch dsh --model qwen3.5:cloud
```
To configure without starting:
```shell
ollama launch dsh --config
```
## Web search
Web search is enabled automatically. It requires Ollama cloud access and a model that supports tools. Run `ollama signin` if needed.
## Configuration
Ollama stores its settings in `~/.ollama/launch/dsh/settings.yaml`. These settings load last and set the model, provider, and web search connection. Repeated launches preserve other settings in this file. Ollama does not change `~/.dsh/settings.yaml`, profiles, sessions, or credentials.
Launch rejects additional `--patch` arguments. Pass other Harness arguments after `--`:
```shell
ollama launch dsh -- --port 3081
```
## Manual install
DeepSeek Harness requires Node.js. To install it manually:
```shell
npm install -g @deepseek-ai/dsh@latest
```
Then run `ollama launch dsh`. On Windows, install Node.js for Windows.

View file

@ -17,6 +17,10 @@ Run `ollama launch` to see the latest integrations you can run from the terminal
<Card title="OpenCode" icon="/images/launch-icons/opencode.svg" href="/integrations/opencode">
Open-source coding agent that edits, runs, and iterates on code.
</Card>
<Card title="DeepSeek Harness" icon="/images/launch-icons/deepseek-harness.svg" href="/integrations/deepseek-harness">
DeepSeek's open-source agent harness with subagents and web search.
</Card>
</CardGroup>
## Connect an assistant