- 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>
7.0 KiB
Qwen Code CLI
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>"
}
envKeynames the environment variable Qwen Code reads the API key from — setOMNIROUTE_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
contextWindowSizeis the per-slot context, notLLAMA_CTX_SIZEitself (overview). - The classifier's
contextWindowSizeis its whole--ctx-size. It has a single slot, so there's no per-slot division (architecture → qwen-classifier). enable_thinking: falseon the classifier matters for parseability, thoughQwen3-4B-Instruct-2507is already architecturally non-thinking (seeresearch/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,imageModelfor other model roles — none are wired up in this stack; onlyfastModelis 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.