Files
LLM-Server/docs/coding-cli-setup/qwen-code.md
T
haylanandClaude Bot a0e5e9704b docs: restructure around source-of-truth pages
- CONTEXT.md glossary
- README reduced to intro, quickstart and doc index
- new architecture.md, gateway.md (from proxy-key-onboarding.md),
  operations.md; network-access.md rewritten
- coding-cli-setup: stale OMNIROUTE_PORT:-4000 endpoint, terms and
  duplicated per-slot/classifier explanations fixed
- research/README.md index; proxy-request-priority.md moved there as
  superseded

Every doc names keys instead of restating values and links its source.

Refs #55 (#56, #57, #59, #60)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 21:51:01 +02:00

7.0 KiB

Qwen Code CLI

← back to overview

Source of truth: Qwen Code's repo for config keys; the overview for the gateway endpoint and context size. If this page disagrees with them, they win.

Qwen Code speaks plain OpenAI Chat Completions. Unlike the other CLIs, it needs two models: the main model, and the classifier model as its fastModel for Auto Mode's action classifier. Each has its own provider connection in the gateway, and both are reached through the same gateway URL.

Why a second model exists

Auto Mode's action classifier (permissions.autoMode) is qwen-code's safety gate for every tool call: it decides whether to auto-approve or block a shell command or tool call before it runs. It runs on its own backend, qwen-classifier. Why that backend exists and how it's sized: architecture → qwen-classifier.

~/.qwen/settings.json

{
  "modelProviders": {
    "openai": [
      {
        "id": "<main-model-provider-id-in-omniroute>",
        "name": "qwen3.8-27b-local",
        "envKey": "OMNIROUTE_API_KEY",
        "baseUrl": "http://<server>:<OMNIROUTE_API_PORT>/v1",
        "generationConfig": { "contextWindowSize": <LLAMA_CTX_SIZE / LLAMA_PARALLEL> }
      },
      {
        "id": "<classifier-provider-id-in-omniroute>",
        "name": "qwen3-4b-classifier",
        "envKey": "OMNIROUTE_API_KEY",
        "baseUrl": "http://<server>:<OMNIROUTE_API_PORT>/v1",
        "generationConfig": {
          "contextWindowSize": <qwen-classifier --ctx-size>,
          "extra_body": { "chat_template_kwargs": { "enable_thinking": false } }
        }
      }
    ]
  },
  "security": { "auth": { "selectedType": "openai" } },
  "model": {
    "name": "<main-model-provider-id-in-omniroute>",
    "baseUrl": "http://<server>:<OMNIROUTE_API_PORT>/v1"
  },
  "fastModel": "<classifier-provider-id-in-omniroute>"
}
  • envKey names the environment variable Qwen Code reads the API key from — set OMNIROUTE_API_KEY=<qwen-code-cli API key> before launching. Both providers can share one API key (as above); split it into two if you want separate usage tracking for chat vs. classifier calls.
  • The main model's contextWindowSize is the per-slot context, not LLAMA_CTX_SIZE itself (overview).
  • The classifier's contextWindowSize is its whole --ctx-size. It has a single slot, so there's no per-slot division (architecture → qwen-classifier).
  • enable_thinking: false on the classifier matters for parseability, though Qwen3-4B-Instruct-2507 is already architecturally non-thinking (see research/fast-model-choice.md §3) — this is belt-and-suspenders for any future fast-model swap that isn't.
  • Qwen Code also recognizes advisorModel, visionModel, compactionModel, imageModel for other model roles — none are wired up in this stack; only fastModel is required.

Web search via OmniRoute

Qwen Code's own built-in web search (tools.webSearch.enabled) has nothing to search with here — leave it false. Instead this stack's SearXNG-backed search (gateway → Web search) is exposed through a thin stdio MCP wrapper around OmniRoute's /v1/search REST endpoint (that endpoint isn't itself MCP — OmniRoute's real MCP surface is admin-only/LOCAL_ONLY-gated). Save this as e.g. ~/.qwen/mcp-servers/omniroute-search/index.mjs (needs @modelcontextprotocol/sdk and zod: npm init -y && npm i @modelcontextprotocol/sdk zod in that directory):

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const BASE_URL = process.env.OMNIROUTE_BASE_URL || "http://proxy-ai.home";
const API_KEY = process.env.OMNIROUTE_API_KEY;

if (!API_KEY) {
  console.error("OMNIROUTE_API_KEY is not set in the environment.");
  process.exit(1);
}

const server = new McpServer({ name: "omniroute-search", version: "1.0.0" });

server.registerTool(
  "search",
  {
    description: "Web/news search via OmniRoute's /v1/search endpoint.",
    inputSchema: { query: z.string().describe("Search query") },
  },
  async ({ query }) => {
    const res = await fetch(`${BASE_URL}/v1/search`, {
      method: "POST",
      headers: { "Content-Type": "application/json", Authorization: `Bearer ${API_KEY}` },
      body: JSON.stringify({ query }),
    });
    const text = await res.text();
    if (!res.ok) return { content: [{ type: "text", text: `HTTP ${res.status}: ${text}` }], isError: true };
    return { content: [{ type: "text", text }] };
  }
);

await server.connect(new StdioServerTransport());

Register it in ~/.qwen/settings.json:

{
  "mcpServers": {
    "omniroute-search": { "command": "node", "args": ["<path-to>/index.mjs"] }
  },
  "tools": { "webSearch": { "enabled": false } }
}

It reuses the same OMNIROUTE_API_KEY env var as the model providers above — the API key needs search permission in OmniRoute, not just chat-completions.

Non-interactive mode (qwen -p ...) needs this tool explicitly allow-listed. MCP tools require interactive confirmation by default; --approval-mode auto alone doesn't bypass that for a non-interactive run — pass --allowed-tools mcp__omniroute-search__search (or -y for full YOLO) alongside -p, or the search call never reaches the classifier at all and silently no-ops. Confirmed live: without the allow-list, only the tool calls the CLI's non-interactive gate lets through end up as classifier requests.

Auto Mode tuning

Auto Mode's action classifier calls the classifier model above. Even on its own GPU-resident backend, give it real timeout headroom rather than trusting OmniRoute's default. And since the gateway is a single trusted local endpoint, it's reasonable to pre-approve requests to it rather than confirm every call:

{
  "permissions": {
    "autoMode": {
      "classifier": { "timeouts": { "stage1Ms": 600000 } },
      "hints": { "allow": ["Requests to proxy-ai.home, my own local omniroute model proxy"] }
    }
  }
}

hints.allow entries are free-text descriptions the classifier matches against, not exact strings — capped at 150 entries/200 chars each.

Also set a generous per-connection timeout on the classifier's own OmniRoute provider connection (providerSpecificData.timeoutMs, dashboard or PATCH /api/providers/{id} — not a .env value, see gateway → Dashboard). 120000ms is comfortable for the current GPU-resident setup (real measured latency: well under a second for a short check, low seconds for the largest realistic transcript) — this isn't the 20-minute figure the main 27B connection needs, since the classifier isn't competing for a contended GPU slot the way the main model can.

Everything else in ~/.qwen/settings.json (hooks, security.auth's underlying tooling, editor prefs) is per-machine, not part of pointing at this stack — don't copy it wholesale between machines.