Expose gpt-oss:120b (llama-server) as a routed gateway target #3
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Part of the roster-setup epic coilyco-bridge/agentic-os-kai#693 and the local-model-stack mission (coilysiren/inbox#118), milestone M3.
Today gpt-oss:120b runs as a manual llama-server launch on the tower at :8080 (OpenAI-compatible), at the benchmarked --n-cpu-moe 24 split (34 tok/s - see agentic-os-hardware docs/llama-moe-offload.md). It is a second runtime next to Ollama on :11434 and is not auto-discovered or routed. The roster assigns aider and opencode to gpt-oss:120b, so both are blocked until the gateway can route to this endpoint.
Tasks:
Blocks - the aider and opencode setup tickets in coilyco-bridge/agentic-os-kai.
🔎 ward agent advisor
ward agent advisor --driver clauderan a one-shot standard research pass on this question:Exposing gpt-oss:120b (llama-server) as a routed backend
I cloned the repo.
mainis still the empty scaffold, so this is written against the phase-1 spec indocs/FEATURES.mdandREADME.md(the{backend_url, ollama_tag, num_ctx}registry, ollama/api/chat+options.num_ctxinjection, validate/retry/fallback/breaker). The short version: llama-server is not an ollama backend wearing a different URL. It speaks a different dialect on a different endpoint and returns a different response shape, so phase-1's ollama assumptions break in three concrete places. Here is what phase-2 has to touch.The core mismatch
Phase-1's
num_ctxfix works by POSTing to the backend's native ollama/api/chatwithoptions.num_ctxinjected per request (docs/FEATURES.md, num_ctx injection). llama-server does not implement/api/chat,/api/generate, or/api/tags. It exposes:/v1/chat/completions,/v1/completions,/v1/models(server README)./completion,/props,/health,/slots,/tokenize.And critically: context size is a launch flag (
-c/--ctx-size), not a per-request option. There is nonum_ctxequivalent in the request body. So for this backendnum_ctxstops being an injected value and becomes a guard-only value that must equal whatever-cthe server was launched with. Nothing to inject, everything to respect.1. Registry entry shape (
models.py)The
{backend_url, ollama_tag, num_ctx}triple can't describe a non-ollama backend. Add adialectdiscriminator and relax the ollama-specific fields:Concrete gpt-oss entry (slot it under whatever logical name the aider/opencode tickets expect. Given it is a large reasoning model with big context,
ctx-thinkfits the existingfast-think/fast/ctx-think/ctx/tunetaxonomy. If those slots are already assigned, add a dedicatedgpt-oss-120blogical name):backend_urlresolves from SSM at boot per AGENTS.md (tower FQDN is/coilysiren/kai-tower-3026/tailnet-fqdn). The:8080port collides with the proxy's own defaultproxy_port=8080inconfig.py, so on kai-server deployment either the proxy or the tower launch needs a distinct port. Flag that now.2. API-dialect handling (
upstream.py)This is the real work. The upstream client must branch on
dialect:/api/chatvs llama-server/v1/chat/completions.{model, messages, options:{num_ctx}, stream}vs OpenAI{model, messages, max_tokens, temperature, stream}. Dropoptions.num_ctxfor llama-server, there is nowhere to put it.--jinjaand the built-in gpt-oss chat template so Harmony tool calls parse into OpenAItool_calls, and set--reasoning-formatso chain-of-thought lands in areasoning_contentfield instead of pollutingcontent(gpt-oss + llama.cpp guide). Without--jinja, tool calls come back as raw Harmony text and every downstream tool-call validator fails.The clean design: normalize at the upstream boundary. Each dialect gets a request encoder and a response decoder that returns one canonical internal shape (normalized to OpenAI, since that is already the proxy's public surface). Then resilience.py and validation only ever see one shape. Do not let ollama-specific keys leak past
upstream.py.3. What breaks in
resilience.pyvalidationPhase-1 validation (non-empty completion, well-formed tool-call JSON, no degenerate repetition) will be written against the ollama
/api/chatresponse, which is a flat top-level object:resp["message"]["content"],resp["message"]["tool_calls"](args as an object), token counts atresp["prompt_eval_count"]/resp["eval_count"], terminalresp["done"].resp["choices"][0]["message"]["content"],resp["choices"][0]["message"]["tool_calls"](args as a JSON string), token counts atresp["usage"]["prompt_tokens"]/["completion_tokens"], terminal viachoices[0]["finish_reason"](which is"tool_calls"when a tool fires).Any validator that reaches for
resp["message"]orresp["prompt_eval_count"]willKeyErroron every llama-server response. Two things to check specifically:json.loads. If the validator assumes one, it rejects the other.message.contentwith a finaldone:true. OpenAI streams SSEdata: {...}frames withchoices[0].delta.contentand a terminaldata: [DONE]. Completely different framing. If the proxy normalizes ollama to OpenAI for its own output, a llama-server backend is already OpenAI-shaped and must skip that adaptation, not double-apply it.The fix follows from #2: if
upstream.pynormalizes both dialects to the canonical shape before anything else runs, resilience.py needs zero changes. That is the argument for putting the adapter at the upstream boundary rather than sprinklingdialectchecks through the resilience layer.4. Safe num_ctx
gpt-oss:120b is natively 128k-capable, but this instance runs CPU-MoE offloaded (
--n-cpu-moe 24, 34 tok/s per the hardware doc), so KV cache and prefill on large contexts are the constraint, not the model. Recommendation:num_ctx=32768, and launch llama-server with a matching-c 32768. This clears the aider/opencode working-context needs and keeps KV modest on an offloaded model.-c. Treat them as one config with two homes.49152or65536only after a benchmark at the offload split shows acceptable prefill latency. Note it in the hardware doc when you do.5. Lifecycle / supervision (the issue's open question)
Route it around the breaker's failure model. ollama is always-up and lazy-loads on demand, so a dead backend self-heals on next request. llama-server holds one model resident and does not lazy-load - if the process is down, the model is fully gone, no recovery without a restart. So:
llama-server -m gpt-oss-120b ... --n-cpu-moe 24 -c 32768 --jinja --host 0.0.0.0 --port 8080) withRestart=on-failure. This is what lets the gateway "rely on it being up." The manual launch cannot, so replace it./healthinto the breaker. llama-server returns 200 when the model is loaded and 503 while loading (which can be minutes for a 120B on cold start). The breaker's health probe must read/healthfor openai-dialect backends (not/api/tags) and treat 503 as "loading, hold" distinct from "dead."Touchpoint summary for phase-2
models.py- adddialect,chat_path,health_path,injects_num_ctx, relaxollama_tagtoupstream_model. Add the gpt-oss entry with SSM-resolved URL.upstream.py- dialect-branched request encoder + response decoder normalizing both to canonical OpenAI shape. Skipnum_ctxinjection wheninjects_num_ctx=False.resilience.py- no change if normalization lands inupstream.py. Audit it to confirm it never touchesresp["message"]/prompt_eval_countdirectly.:8080port collision with the proxy, add the systemd unit for the tower launch, keepnum_ctxand-clocked together.The one-line design principle: make llama-server a second upstream adapter that normalizes to the proxy's canonical shape, so the dialect difference dies at the
upstream.pyboundary and never reaches the resilience core.Sources: llama.cpp server README, gpt-oss with llama.cpp guide.
Researched and posted automatically by
ward agent advisor --driver claude(ward#179). This is one-shot research, not a carried change - verify before acting on it.— Claude (she/her), via
ward agent🔒 Reserved by
ward agent --driver codex— containerengineer-codex-agent-proxy-3on hostkais-macbook-pro-2.localis carrying this issue (reserved 2026-07-02T09:19:08Z). Concurrentward agentruns are blocked until it finishes or the reservation goes stale (2h0m0s TTL);--forceoverrides.— Codex, via
ward agentWARD-OUTCOME: done - routed gpt-oss:120b through the gateway via a llama-server/OpenAI adapter, added the stable gpt-oss-120b alias, and verified the full suite.\n\nThis felt straightforward once the dialect split was isolated in app/upstream.py. The only friction was the repo/tooling mismatch around ward exec and the missing pre-commit config, but the code path itself held together cleanly. Confidence is high. The main rough edge left is lifecycle outside this repo, since the tower process is still managed externally.