Qwen Code: settings.json Is the Client. The Modelfile Is the Runtime.

How Qwen Code and Ollama Must Agree Before a Local Agent Can Work

Most people configure a local agent by picking a model name.

That sounds like configuration.

The picker shows qwen3.8. The URL is localhost:11434. An API key field wants something, so they type ollama. The session starts.

Then the model is missing from the list. Or it is in the list and thinks the window is 128k while Ollama reserved 4k. Or the request dies during prefill. Or every shell command asks for approval again.

None of those bugs live in the prompt.

They live in two files that have to describe the same machine:

~/.qwen/settings.json     the client
Ollama Modelfile / env    the runtime

If those two disagree, the agent looks configured and is not.

This article is a walkthrough of that contract. It uses Qwen Code’s version-4 settings and Ollama on a 16 GB Windows workstation. The names can change. The split should not.


Two Layers, Two Jobs

Qwen Code does not load GGUF files.

It sends OpenAI-compatible HTTP to a base URL and expects a model id to exist on the other side.

Ollama does not read settings.json.

It loads weights, allocates a KV cache from num_ctx, and binds a port.

Qwen Code
  settings.json
    model.name
    modelProviders.openai[].id
    contextWindowSize
    timeouts / approval / env
        ↓
  POST /v1/chat/completions
        ↓
Ollama
  tag + Modelfile
    FROM <weights>
    PARAMETER num_ctx N
  OLLAMA_HOST / firewall

The client can declare a 128k window. The runtime can still have reserved 32k. The request will then truncate, slow down, or die, and the UI will blame the model.

Configuration is the act of making those numbers the same.


$version: 4 Is Not Cosmetic

Qwen Code 0.21 treats modelProviders.openai as a bare array.

This shape works:

{
  "$version": 4,
  "modelProviders": {
    "openai": [
      { "id": "qwen3.5-qwen:latest", "...": "..." }
    ]
  }
}

This shape is skipped:

"openai": {
  "protocol": "openai",
  "models": [ { "id": "..." } ]
}

The file still parses. The picker still exists. The models are gone.

That is the first class of local-agent bug: a silent schema miss. The UI does not throw. It just has nothing to select.

If you copy an older example from the internet, check the version and the array. Do not debug the model until the provider list is actually loaded.


Auth Is a Client Fiction

Ollama’s local API does not need a real secret.

Qwen Code’s OpenAI provider does. It wants an environment key and a selected auth type.

"env": {
  "OLLAMA_API_KEY": "ollama"
},
"security": {
  "auth": {
    "selectedType": "openai"
  }
}

ollama is a dummy string. The runtime ignores it. The client only needs a non-empty value so it will send Authorization and stop opening a cloud login.

selectedType: "openai" is the other half. Without it, Qwen Code still thinks it should talk to Qwen Cloud.

This is not security architecture. It is adapter architecture. The provider interface was built for remote APIs. Localhost has to pretend.


model.name Must Equal a Provider id

The default model is a string, not an object.

"model": {
  "name": "qwen3.6-claude:latest",
  "baseUrl": "http://127.0.0.1:11434/v1",
  "maxSessionTurns": -1
}

That string must match a provider entry exactly, including :latest if the tag has it.

wrong   qwen3.6-claude
right   qwen3.6-claude:latest

baseUrl belongs on both the default model and every provider row. The client should not have to guess which host a tag lives on.

maxSessionTurns: -1 is a loop policy, not a model property. A positive cap ends the run after N tool rounds. Local models already yield early. A turn cap makes that worse.


One Provider Row Is One Operating Point

A provider entry is not “the model.”

It is a named contract:

{
  "id": "qwen3.8-qwen-64k:latest",
  "name": "Qwen3.8 27B (Ollama, 64k)",
  "envKey": "OLLAMA_API_KEY",
  "baseUrl": "http://127.0.0.1:11434/v1",
  "generationConfig": {
    "timeout": 600000,
    "maxRetries": 2,
    "contextWindowSize": 65536,
    "samplingParams": {
      "max_tokens": 16384
    }
  }
}

id is the Ollama tag.
name is only the picker label.
contextWindowSize is what the agent believes.
timeout is how long one HTTP request may live.
max_tokens is the completion budget, not the window.

I keep samplingParams to max_tokens only. Temperature and top_p stay with Ollama. If both sides set sampling, you will not know which one won when the output looks wrong.

contextWindowSize must equal the runtime num_ctx. That is the whole point of having several tags for one weight file.


Variants Belong in Ollama, Then in the Picker

Ollama does not need another 17 GB download to change the window.

It needs a Modelfile:

FROM qwen3.8:latest
PARAMETER num_ctx 65536
ollama create qwen3.8-qwen-64k -f Modelfile

Then a matching provider row.

The weights stay shared. Only the reserved KV cache changes.

A useful ladder on 16 GB looks like this:

small model, GPU-safe
  32k / 64k / 96k / 128k

20B-class
  64k default, 96k middle, 128k optional

27B-class
  32k practical minimum for this agent
  64k compromise
  128k luxury with offload

Do not put 16k in the picker for a coding agent unless you like a window that is already full. System prompt, tools, and memory already consume the mid-teens of thousands of tokens.

Do not invent a 128k row for a model whose native length is 32k. The client will stuff the prompt. The runtime cannot honor it.


Timeouts Are a Prefill Setting

Cloud APIs start dripping tokens quickly.

Local prefill does not.

A 42k prompt at about 680 tokens per second can stay silent for twenty to sixty seconds while the KV cache is built. Qwen Code’s defaults treat that silence as death:

timeout                 120s   per request
QWEN_STREAM_IDLE        240s   gap between chunks
QWEN_STREAM_MAX_LIFETIME 900s  total stream wait

Those numbers are not in generationConfig for the stream guards. They are environment variables. Writing streamIdleTimeoutMs into settings.json does nothing.

On this machine:

"env": {
  "QWEN_CODE_API_TIMEOUT_MS": "0",
  "QWEN_STREAM_IDLE_TIMEOUT_MS": "0",
  "QWEN_STREAM_MAX_LIFETIME_MS": "0"
}

and per model:

"timeout": 600000

0 means “do not abort because the stream is quiet.” That is the correct local default. The 500 you see in Ollama after cancel task is often the client hanging up during prefill, not a crashed runner.

QWEN_CODE_SUPPRESS_YOLO_WARNING only hides the stderr lecture once approval is yolo and there is no sandbox. It is not a timeout.


Approval Mode Is Client Policy

"tools": {
  "approvalMode": "yolo"
}

default asks before edits and shell.
auto still asks when the classifier is unsure.
yolo does not ask.

A local agent that stops after every dotnet build is not thinking. It is waiting.

YOLO is a trust decision about the workspace, not a model feature. I use it on my own machine. I would not use it on an untrusted clone.


What settings.env Cannot Fix

Qwen Code loads settings.env only for keys that are not already set.

ComSpec is always set on Windows. Putting ComSpec=pwsh.exe in settings.json does nothing.

OLLAMA_HOST is the same class of problem if the Ollama service already started with the default.

The loader also refuses variables that would hijack process startup. That is correct. It is also why “just add it to env” is not a complete operations guide.

Two consequences:

The Windows shell of the agent is cmd.exe unless you wrap the qwen launch so ComSpec points at pwsh only for that process, and you start Node directly. Pointing ComSpec at PowerShell globally breaks .cmd launchers.

LAN access is an Ollama setting, not a Qwen setting:

OLLAMA_HOST=0.0.0.0:11434

as a user environment variable, then restart the Ollama app. A firewall rule on port 11434 is required for other devices. Do not port-forward that port to the internet.

settings.json can still point baseUrl at http://192.168.x.x:11434/v1 on the remote machine. The bind address is decided before Qwen Code starts.


Ollama Configuration That Actually Matters

The runtime side is smaller than the client side. It is also easier to get wrong by leaving defaults in place.

Host and origins

OLLAMA_HOST=0.0.0.0:11434
OLLAMA_ORIGINS=*

only if you want other machines or browser UIs on the LAN. Default is localhost.

Keep-alive

A loaded 23 GB model with a 128k slot occupies the card. If you bounce between tags, the next prefill pays the load cost again. For agent work, keep the daily model resident. Unload it when you switch families.

num_ctx is the KV reservation

ollama show <tag> is the source of truth. If it says num_ctx 65536, the client row must say 65536. The native context length on the card is a maximum, not the current allocation.

ollama ps is the health check

100% GPU     the operating point you configured is real
any CPU%     offload has already started

A beautiful settings file cannot fix a 17 GB weight file on a 16 GB card. It can only stop you from also reserving 128k of cache on top.

Do not set sampling in two places

If the Modelfile pins temperature and the client later sends another temperature, you own both knobs. I leave sampling in Ollama and send only max_tokens from Qwen Code.


Comments Are Allowed. Saving May Delete Them.

Qwen Code reads settings.json as JSONC.

// comments survive a human edit. They may vanish if the CLI writes the file — a model switch, a UI flag, an auth nudge.

That is why the comments belong in the file anyway. The next time you open it, you need to remember why openai is an array, why timeouts are zero, and why LFM uses 128000 instead of 131072.

If the app strips comments, put the same notes back. Do not treat the file as generated-only.

ui.autoModeAcknowledged and feedbackLastShownTimestamp are flags the app writes. Leave them. Deleting them brings the dialogs back.


A Minimal Valid Skeleton

You do not need thirty tags to start.

You need one tag that exists in Ollama, one matching id, and timeouts that survive prefill.

{
  "$version": 4,
  "env": {
    "OLLAMA_API_KEY": "ollama",
    "QWEN_CODE_API_TIMEOUT_MS": "0",
    "QWEN_STREAM_IDLE_TIMEOUT_MS": "0",
    "QWEN_STREAM_MAX_LIFETIME_MS": "0"
  },
  "security": {
    "auth": { "selectedType": "openai" }
  },
  "model": {
    "name": "qwen3.5-qwen:latest",
    "baseUrl": "http://127.0.0.1:11434/v1",
    "maxSessionTurns": -1
  },
  "tools": {
    "approvalMode": "yolo"
  },
  "modelProviders": {
    "openai": [
      {
        "id": "qwen3.5-qwen:latest",
        "name": "Qwen3.5 9.7B (Ollama, 128k)",
        "envKey": "OLLAMA_API_KEY",
        "baseUrl": "http://127.0.0.1:11434/v1",
        "generationConfig": {
          "timeout": 600000,
          "maxRetries": 2,
          "contextWindowSize": 131072,
          "samplingParams": {
            "max_tokens": 16384
          }
        }
      }
    ]
  }
}

Then create the Ollama tag with the same num_ctx.

Then run:

ollama show qwen3.5-qwen
ollama ps
qwen --model qwen3.5-qwen:latest -p "/context"

If /context reports a different window than num_ctx, the two layers still disagree.


Common Mismatches

The provider list is wrapped, so version 4 ignores it.

The default model.name does not match any id.

contextWindowSize is 131072 and num_ctx is 4096 or 32768.

samplingParams is missing, and the client invents a max_tokens the runtime does not expect — or the opposite, a huge max_tokens on a 16k window.

Timeouts stay at cloud defaults, and long prefills return 500.

baseUrl points at 127.0.0.1 while you are on another machine, or at the LAN IP while Ollama still binds localhost.

ComSpec is still cmd.exe, and the model writes head and grep.

Each of those is a contract bug. None of them is fixed by changing the system prompt.


Final Takeaway

Qwen Code settings are the client contract.

Ollama’s Modelfile and environment are the runtime contract.

The agent works when the id, the tag, the window, and the timeouts describe one operating point.

$version: 4 wants a bare OpenAI array. The API key is a dummy. Variants are num_ctx, not downloads. Stream idle timeouts must tolerate silent prefill. Approval mode and turn caps belong to the client. Bind address and GPU offload belong to Ollama.

If those sentences are explicit, settings.json stops being a pile of copied keys.

It becomes the file that tells the truth about the machine.

Von admin