Copper API (Mark's self-hosted LLM endpoint)¶
URL: https://mkdocs.justinsforge.com/memory/general/reference_copper_api_mark/
Friend-hosted OpenAI-compatible chat completions endpoint from Mark. Received 2026-07-01.
Connection¶
| Item | Value |
|---|---|
| Base URL | https://copper-api.markcodes.org |
| Endpoint | POST /chat/completions (OpenAI chat completions shape) |
| Auth | Authorization: Bearer <key> |
| Secret file | ~/.forge-secrets/markaiapi.env (chmod 600; file contains the bare key only, no VAR= prefix; read with pathlib.Path(...).read_text().strip()) |
| Model | cyankiwi/Qwen3.6-27B-AWQ-BF16-INT4 (single model; model param ignored, may omit) |
| Context window | 150k tokens |
| Backend | vLLM 0.23.x, tp2 |
Behavior and gotchas¶
- Reasoning model, thinks a lot. Disable with
<|think_off|>anywhere in system or user prompt for non-reasoning tasks. <|think_off|>response quirk (verified 2026-07-01): with thinking disabled, vLLM's reasoning parser misroutes the answer.message.contentcomes backnulland the actual reply is inmessage.reasoning. Client code must fall back:content = msg["content"] or msg["reasoning"]. With thinking ON,contentis populated normally andreasoningholds the think trace.- Runaway loop bug: rarely emits infinite
!!!!!repetition. Always setmax_tokensand a client timeout; kill stuck calls. - Speed: Mark quotes 15-20 TPS sustained; smoke test measured ~60 TPS on a short burst (198 completion tokens in 3.3s). Expect slow long generations; coding use doubles back and second-guesses but converges.
Usage limits (host courtesy, treat as hard)¶
- Thermal-limited server: keep sessions to ~20 minutes max, do not hammer with parallel or batch traffic.
- Not a fleet workhorse. Good for: offline experiments, second-opinion evals, cheap bulk drafting where latency is fine. Anthropic models remain the default for all forge automation.
Chat surface: markai.justinsforge.com (built 2026-07-01)¶
Claude-style chat UI over this endpoint.
| Item | Value |
|---|---|
| URL | https://markai.justinsforge.com (Cloudflare Access "Justin Secure Only", app id ec10850c-5e43-4178-b449-0becc3a15f09) |
| App | scripts/forge_markai_chat.py, Flask, port 8102, bind 0.0.0.0 (Console) |
| Service | forge-markai.service (user unit, enabled); venv ~/.forge-venvs/markai (flask, requests, ddgs), NOT system python |
| Routing | media-server tunnel ingress markai.justinsforge.com -> http://192.168.86.50:8102 |
| DB | data/markai/markai_chat.db (SQLite WAL; conversations + messages, reasoning stored separately) |
| Features | conversation sidebar (rename via double-click, delete), SSE streaming, collapsible Thinking block, per-message think toggle (off by default, injects <|think_off|>), runaway-repetition guard, mobile off-canvas sidebar, home pill |
| Quirk handling | in no-think mode the server remaps delta.reasoning to the content channel (upstream misroute, see above) |
| Homepage | tile in Surfaces section of justinsforge.com landing |
Verified end-to-end 2026-07-01: no-think and think modes both streamed and persisted; edge serves Access login to unauthenticated requests.
Second surface: openwebui.justinsforge.com (built 2026-07-02)¶
Off-the-shelf Open WebUI frontend over the same Copper endpoint. Additive; the markai Flask UI stays running.
| Item | Value |
|---|---|
| URL | https://openwebui.justinsforge.com (Cloudflare Access app "Open WebUI Secure", id 5f32ef44-4823-4c47-a8f0-b75cc23ae68c, same two-email allow policy as markai) |
| Host | LXC 113 forge-openwebui on Finn, 192.168.86.88, Debian 13, nesting=1. Docker container open-webui (ghcr.io/open-webui/open-webui:main), port 8080, restart unless-stopped, volume open-webui. |
| Backend | OPENAI_API_BASE_URL=https://copper-api.markcodes.org + key from ~/.forge-secrets/markaiapi.env. Copper implements GET /models, so the Qwen model auto-populates. ENABLE_OLLAMA_API=false. |
| Auth | WEBUI_AUTH=false (no Open WebUI login); CF Access is the sole gate. Add more OpenAI-compatible backends from Admin > Settings > Connections. |
| Perf tuning (2026-07-02) | Copper is a single thermal-limited GPU serving one request at a time at ~15-20 TPS. Open WebUI's default background tasks (title/tags/follow-up/autocomplete/arena) each fire an extra Copper call per message and queue behind the real reply, making the UI crawl. Disabled via env: ENABLE_TITLE_GENERATION, ENABLE_TAGS_GENERATION, ENABLE_AUTOCOMPLETE_GENERATION, ENABLE_FOLLOW_UP_GENERATION, ENABLE_RETRIEVAL_QUERY_GENERATION, ENABLE_SEARCH_QUERY_GENERATION, ENABLE_EVALUATION_ARENA_MODELS all =false, TASK_MODEL_EXTERNAL="". Result: one user message = exactly one Copper call. Slowness that remains is inherent (model reasons verbosely at 15-20 TPS). |
| Routing | media-server tunnel ingress openwebui.justinsforge.com -> http://192.168.86.88:8080 (added via forge_cloudflare_cf.py add). |
| Homepage | tile in Surfaces section of justinsforge.com landing. |
Note: the <|think_off|> content/reasoning misroute (gotcha #2) is NOT remapped by Open WebUI (it's a stock client). Leave thinking on for coherent replies, or expect blank content in no-think mode. Verified 2026-07-02: container healthy (/health 200), Copper model listed, edge returns 302 Access login to unauthenticated requests.
Web search on Open WebUI = NATIVE tool calling (added 2026-07-05)¶
Enabled Open WebUI's built-in Web Search over the Copper model. In 0.10.2
this is genuine native tool calling, NOT prompt-based RAG injection: Open
WebUI passes a search_web (+ fetch_url) function spec to the endpoint, the
Qwen model emits a real tool_calls response, Open WebUI executes the search
and feeds results back, loops until the model answers with citations.
Why native works here (0.10.2 flipped the default; middleware.py:2762):
function_calling != 'legacy' is the DEFAULT, so tools go to the endpoint as
real function specs. The Copper/vLLM backend returns proper tool_calls +
finish_reason: "tool_calls" (verified directly). No function_calling param
needs setting; model params are {} and global models.default_params is
{}, both resolve to native.
| Item | Value |
|---|---|
| Engine | duckduckgo (keyless, matches prior ddgs usage); ddgs_backend=auto |
Config keys (SQLite config, key/value in 0.10.2) |
web.search.enable=true, web.search.engine="duckduckgo", web.search.result_count=3 |
Builtin-tool gates (tools.py:582) |
web.search.enable (set) AND model capabilities.web_search=true (already set) AND capabilities.builtin_tools=true AND function_calling!='legacy' AND request features.web_search=true (the Web Search toggle) AND user perm (admin passes; WEBUI_AUTH=false user IS admin) |
| DB path | /app/backend/data/webui.db in container (volume open-webui); one row per dotted key, not a JSON blob |
| How to change config | edit config rows then docker restart open-webui (config loads at startup; env vars are PersistentConfig so DB wins once seeded). Config API needs a bearer token even with WEBUI_AUTH=false; mint one with create_token({"id": <admin_uuid>}) signed by WEBUI_SECRET_KEY (32-char, read from /proc/1/environ). |
| Usage in UI | click the Web Search toggle in the message input to arm the tool; the model then decides whether to call it. Toggle is the features.web_search gate. |
Verified 2026-07-05 end-to-end: (a) Copper returns proper tool_calls when
handed a tools array; (b) with Web Search armed, Open WebUI injects search_web
and Copper emits a real tool_call (streamed tool_call deltas observed over
/api/chat/completions); (c) full manual loop against Copper (call -> DDG
search -> feed back -> answer) produced a correct Austin-weather answer,
finish_reason: stop. Note: the execute-and-feed-back loop runs in Open WebUI's
websocket UI flow (needs chat_id+session_id); a bare HTTP stream only shows
the tool_call passthrough, not the execution.
markai Flask app tools (removed 2026-07-01)¶
The custom forge_markai_chat.py app briefly ran a native OpenAI tool-calling
agent loop (web_search via ddgs, fetch_page, plus CreatorTrack capture
verbs, cap MAX_TOOL_ROUNDS=4, chips persisted in messages.tool_trace).
This was removed 2026-07-01 (see forge_markai_chat.py:597); markai now
sends raw prompts with no tools. Old chats still render their historical tool
chips read-only. For web search over Copper, use the Open WebUI surface above.
Smoke test¶
import json, pathlib, urllib.request
key = pathlib.Path("/home/justinwieb/.forge-secrets/markaiapi.env").read_text().strip()
req = urllib.request.Request(
"https://copper-api.markcodes.org/chat/completions",
data=json.dumps({"messages": [{"role": "user", "content": "<|think_off|> ping"}], "max_tokens": 30}).encode(),
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"})
msg = json.loads(urllib.request.urlopen(req, timeout=120).read())["choices"][0]["message"]
print(msg["content"] or msg["reasoning"])
Verified working 2026-07-01 (both thinking on and off).
[Claude Code, 2026-07-01]