Files
Niko1221 acbaa4391b connect: the output cap grows with the window, and a rewrite keeps a cap the person set (#5006; #4528 checked, no change)
limit.output, CLAUDE_CODE_MAX_OUTPUT_TOKENS and maxTokens are max(4096, context/8): 16384 at 128K, so OpenCode's compaction summary has room (the tested 32K server keeps
4096). The input limit and compaction point stay as tested. Rewriting a config keeps an output cap other than the 4096 older versions wrote. #4528: the documented
provider block is still accepted by OpenCode's schema (deprecated in favour of 'providers'); no change. Idea as in PR #4230, written here.
2026-11-04 10:56:16 +01:00

355 lines
20 KiB
Python

"""`strata connect <harness>`: the setup for an app that talks to a running Strata server.
Every entry below comes from a config that was tested for real against a Strata server at 32K context (one simple prompt with the
answer text checked and one tool call with its arguments checked, plus streaming, thinking and compaction at about 15K): the files in
phaseA-tests/split/harness-configs/<harness>.md, one per app, with the versions tested. The generators here fill in the real address,
key, model name and context of the running server; the "limits" are those files' gotchas in plain words.
`tested` is the app version the config was tested with. A value of None would mean "not tested"; all eight entries are tested.
"""
from __future__ import annotations
import json
import os
import re
import shutil
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Callable
PLACEHOLDER_KEY = "strata-local" # the tested configs' placeholder when the server has no key (not a secret)
OUTPUT_CAP = 4096 # the output cap every tested config used
TESTED_CONTEXT = 32768 # the window every config below was tested on
COMPACT_AT = 15000 # and where those tested configs make the app compact its conversation
@dataclass
class Target:
"""The running server, as a config needs to know it."""
url: str # server root, no /v1
key: str # real key, or the placeholder
model: str
context: int = 32768
@property
def base(self) -> str:
return self.url.rstrip("/") + "/v1"
@property
def input_limit(self) -> int:
return max(1024, self.context - OUTPUT_CAP)
@property
def output_cap(self) -> int:
"""The longest reply a config lets the app ask for: the tested 4,096 up to the tested window, then an eighth of the window (16,384 at 128K).
Thinking counts against it, and an app asks its compaction summary for exactly this much: at 4,096 on a wide window the summary ended at the
limit and OpenCode failed the step ("Compaction summary reached the output token limit", #5006); a whole file written in one tool call was
cut off the same way (PR #4230). The input limit and the compaction point stay as tested (compaction fires long before the input limit, and the
server fits each request's cap to the room its prompt leaves)."""
return max(OUTPUT_CAP, self.context // 8)
def compact_at(t: Target) -> int:
"""Where the app should compact its conversation: the share of the input limit the tested configs used, so that the
summarising request (which carries the whole conversation) still fits the window.
A flat 15,000 summarises a wider server every 15,000 tokens - 6% of a 256K window - while the tested configs compact
a 32K server at 52% of its input limit. The share is what carries over, not the number: a 32K server keeps exactly
the value it was tested at."""
tested_limit = TESTED_CONTEXT - OUTPUT_CAP
return max(COMPACT_AT, int(t.input_limit * COMPACT_AT / tested_limit))
@dataclass
class FileSpec:
path: str # may start with ~
kind: str # json | toml | yaml | text
make: Callable[[Target], str]
merge: bool = False # json only: merge into an existing file instead of replacing it
@dataclass
class Harness:
id: str
title: str
speaks: str
tested: str = "" # "opencode 1.19.3": the version the config was tested with
files: list[FileSpec] = field(default_factory=list)
env: Callable[[Target], dict[str, str]] | None = None
limits: Callable[[Target], list[str]] | None = None
run: str = "" # one example command
install: str = ""
built_in: bool = False
aliases: tuple[str, ...] = ()
@property
def status(self) -> str:
if self.built_in:
return "built into the Strata app (tested)"
return f"tested ({self.tested})" if self.tested else "not tested yet"
def _j(obj) -> str:
return json.dumps(obj, indent=2)
# ------------------------------------------------------------------------------------------------ the eight
def opencode_file(t: Target) -> str:
return _j({"$schema": "https://opencode.ai/config.json", "model": f"strata/{t.model}", "small_model": f"strata/{t.model}",
"autoupdate": False,
"compaction": {"auto": True, "buffer": max(1024, t.input_limit - compact_at(t))},
"share": "disabled",
"provider": {"strata": {"npm": "@ai-sdk/openai-compatible", "name": "Strata (local)",
"options": {"baseURL": t.base, "apiKey": t.key},
"models": {t.model: {"name": "Qwen3.8-Flash-Next (local)", "tool_call": True, "reasoning": True,
"limit": {"context": t.context, "input": t.input_limit, "output": t.output_cap}}}}}})
def codex_file(t: Target) -> str:
return (f'model = "{t.model}"\nmodel_provider = "strata"\nmodel_context_window = {t.context}\n'
f"model_auto_compact_token_limit = {compact_at(t)}\nmodel_reasoning_summary = \"auto\"\napproval_policy = \"never\"\n"
'sandbox_mode = "workspace-write"\ncheck_for_update_on_startup = false\n\n[model_providers.strata]\n'
f'name = "Strata (local)"\nbase_url = "{t.base}"\nwire_api = "responses"\nenv_key = "STRATA_API_KEY"\n'
"stream_idle_timeout_ms = 600000\n")
def claude_env(t: Target) -> dict[str, str]:
return {"ANTHROPIC_BASE_URL": t.url.rstrip("/"), "ANTHROPIC_AUTH_TOKEN": t.key, "ANTHROPIC_MODEL": t.model,
"ANTHROPIC_SMALL_FAST_MODEL": t.model, "ANTHROPIC_DEFAULT_SONNET_MODEL": t.model, "ANTHROPIC_DEFAULT_OPUS_MODEL": t.model,
"ANTHROPIC_DEFAULT_HAIKU_MODEL": t.model, "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1", "DISABLE_AUTOUPDATER": "1",
"DISABLE_TELEMETRY": "1", "CLAUDE_CODE_MAX_CONTEXT_TOKENS": str(t.context),
"CLAUDE_CODE_MAX_OUTPUT_TOKENS": str(t.output_cap)} | ({"CLAUDE_CODE_SIMPLE": "1"} if t.context <= 40960 else {})
def claude_file(t: Target) -> str:
return _j({"env": claude_env(t)})
def omp_models(t: Target) -> str:
return ("providers:\n strata:\n baseUrl: " + t.base + "\n api: openai-completions\n auth: none\n compat:\n"
" supportsDeveloperRole: false\n models:\n - id: " + t.model + "\n name: Qwen3.8-Flash-Next (Strata, local)\n"
" reasoning: true\n input: [text]\n contextWindow: " + str(t.context) + "\n maxTokens: " + str(t.output_cap) +
"\n thinking:\n mode: effort\n efforts: [minimal, low, medium, high]\n defaultLevel: medium\n"
" cost: {input: 0, output: 0, cacheRead: 0, cacheWrite: 0}\n")
def omp_config(t: Target) -> str:
return ("modelRoles:\n default: strata/" + t.model + "\ndefaultThinkingLevel: medium\ncompaction:\n enabled: true\n thresholdTokens: "
+ str(compact_at(t)) + "\n keepRecentTokens: 4000\n methodOrder:\n - soft\n idleEnabled: false\nsetupVersion: 2\n")
def pi_models(t: Target) -> str:
return _j({"providers": {"strata": {"baseUrl": t.base, "api": "openai-completions", "apiKey": t.key,
"compat": {"supportsDeveloperRole": False, "supportsReasoningEffort": False},
"models": [{"id": t.model, "name": "Qwen3.8-Flash-Next (Strata, local)", "reasoning": True,
"input": ["text"], "contextWindow": t.context, "maxTokens": t.output_cap,
"cost": {"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0}}]}}})
def pi_settings(t: Target) -> str:
return _j({"defaultProvider": "strata", "defaultModel": t.model, "defaultThinkingLevel": "medium",
"compaction": {"enabled": True, "reserveTokens": max(1024, t.context - compact_at(t)), "keepRecentTokens": 4000}})
def hermes_file(t: Target) -> str:
return ("model:\n default: " + t.model + "\n provider: lmstudio\n base_url: " + t.base + "\n context_length: " + str(t.context) +
"\ncompression:\n enabled: true\n threshold_tokens: " + str(compact_at(t)) + "\nauxiliary:\n compression:\n context_length: 65536\n")
def openclaw_file(t: Target) -> str:
return _j({"models": {"mode": "merge", "providers": {"strata": {
"baseUrl": t.base, "apiKey": t.key, "api": "openai-completions",
"models": [{"id": t.model, "name": f"{t.model} (Strata)", "contextWindow": t.context,
"contextTokens": int(t.context * 23192 / 32768), "maxTokens": t.output_cap, "reasoning": True, "input": ["text"],
"cost": {"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0}}]}}},
"agents": {"defaults": {"model": {"primary": f"strata/{t.model}"},
"compaction": {"enabled": True, "notifyUser": True, "keepRecentTokens": 4000}}}})
def _limits_opencode(t: Target) -> list[str]:
return ["The setup needs limit.input (strata connect sets it to %d); without it OpenCode never compacts its conversation." % t.input_limit,
"Its first start downloads a plug-in from npm (internet needed once; the first run can take over a minute).",
"In a script or over ssh, run it as: opencode run \"...\" < /dev/null (it waits for typing otherwise).",
"Its system prompt and tools take about 7.4K tokens of the window."]
def _limits_codex(t: Target) -> list[str]:
return ["Codex prints \"Model metadata ... not found\" on every start: harmless, it does not know the model.",
"It will not send without STRATA_API_KEY set; any text works when the server has no key.",
"Where the sandbox cannot start (no bubblewrap), use --dangerously-bypass-approvals-and-sandbox only in a throwaway folder.",
"Its system prompt and tools take about 8.6K tokens of the window."]
def _limits_claude(t: Target) -> list[str]:
out = ["ANTHROPIC_BASE_URL is the server address %s without /v1: Claude Code adds /v1/messages itself (with /v1 in it the requests go to /v1/v1/messages)." % t.url,
"Set CLAUDE_CODE_MAX_CONTEXT_TOKENS to the served context (%d here) and CLAUDE_CODE_MAX_OUTPUT_TOKENS=%d: without them Claude Code "
"assumes a 100K window and never compacts, or compacts on every turn." % (t.context, t.output_cap),
"No Anthropic login or credential is used; the token is only checked by your own server.",
"The notice [claude-code:unrecognized_model] on stderr is harmless."]
if t.context <= 40960:
out.append("Its full prompt is 15.6K tokens (20 tools): with a 32K window use CLAUDE_CODE_SIMPLE=1 (3 tools, 1.25K tokens), as set here.")
return out
def _limits_omp(t: Target) -> list[str]:
return ["Its system prompt and tools take about 7.5K tokens of the window.",
"One small title/summary request may come back empty because the model spent its 512 tokens thinking; omp carries on."]
def _limits_pi(t: Target) -> list[str]:
return ["pi has the smallest prompt of the eight (1.5K tokens): the most room for the conversation.",
"It compacts after the turn that crosses the line, so one turn can reach about %dK tokens before it summarises."
% round(compact_at(t) / 1000)]
def _limits_hermes(t: Target) -> list[str]:
out = ["Hermes refuses a window under 64,000 tokens unless the provider is lmstudio with an explicit context_length: the config uses "
"that, and auxiliary.compression.context_length 65536 for its summariser (the same floor).",
"Its system prompt and 20 tools take about 12.9K tokens (40% of a 32K window): about 19K is left for the conversation.",
"Its own 64K minimum floors the compaction threshold, so it compacts later than the value above (around 20K on a 32K window); it still fits 32K."]
if t.context < 65536:
out.insert(0, "WARNING: Hermes is built for 64K or more. At %dK it works only through the workaround above." % (t.context // 1024))
return out
def _limits_openclaw(t: Target) -> list[str]:
return ["OpenClaw needs Node 24.16 or newer. On older Node, npm install -g openclaw quietly installs an empty 0.0.1 placeholder.",
"Its own token count runs 10-30% above the server's, so keep contextTokens (set here) below the window.",
"The embedded agent writes notes into its workspace folder: give it a scratch folder.",
"Harmless on exit: 'SQLite reclamation Worker failed' and 'semantic vector embeddings unavailable'."]
def _limits_dsh(t: Target) -> list[str]:
return ["DeepSeek Harness is Strata's own coding harness inside the Strata app: nothing to write here.",
"Its package default compaction headroom (64K) is larger than a 32K window; the app's bridge sets it for you."]
HARNESSES: list[Harness] = [
Harness("opencode", "OpenCode", "chat + tools", tested="opencode 1.19.3",
files=[FileSpec("~/.config/opencode/opencode.json", "json", opencode_file, merge=True)], limits=_limits_opencode,
run='opencode run "your task"', install="npm i -g opencode-ai"),
Harness("codex", "Codex CLI", "responses + tools", tested="codex-cli 0.163.0",
files=[FileSpec("~/.codex/config.toml", "toml", codex_file)], env=lambda t: {"STRATA_API_KEY": t.key}, limits=_limits_codex,
run='codex exec --skip-git-repo-check "your task"', install="npm i -g @openai/codex"),
Harness("claude-code", "Claude Code", "messages + tools", tested="Claude Code 2.1.302",
files=[FileSpec("~/.claude/settings.json", "json", claude_file, merge=True)], env=claude_env, limits=_limits_claude,
run='claude -p "your task"', install="npm i -g @anthropic-ai/claude-code", aliases=("claude",)),
Harness("omp", "oh-my-pi (omp)", "chat + tools", tested="omp 18.11.2",
files=[FileSpec("~/.omp/agent/models.yml", "yaml", omp_models), FileSpec("~/.omp/agent/config.yml", "yaml", omp_config)],
limits=_limits_omp, run='omp -p "your task"', install="curl -fsSL https://omp.sh/install | sh", aliases=("oh-my-pi",)),
Harness("pi", "pi (pi.dev)", "chat + tools", tested="pi 0.74.2",
files=[FileSpec("~/.pi/agent/models.json", "json", pi_models, merge=True),
FileSpec("~/.pi/agent/settings.json", "json", pi_settings, merge=True)],
env=lambda t: {"PI_SKIP_VERSION_CHECK": "1"}, limits=_limits_pi, run='pi -p "your task"',
install="npm install -g @earendil-works/pi-coding-agent"),
Harness("hermes", "Hermes Agent", "chat + tools", tested="Hermes Agent 2026.9.24",
files=[FileSpec("~/.hermes/config.yaml", "yaml", hermes_file)], env=lambda t: {"OPENAI_API_KEY": t.key},
limits=_limits_hermes, run='hermes chat -Q --yolo -q "your task"', aliases=("hermes-agent",)),
Harness("openclaw", "OpenClaw", "chat + tools", tested="OpenClaw 2026.10.4",
files=[FileSpec("~/.openclaw/openclaw.json", "json", openclaw_file, merge=True)], limits=_limits_openclaw,
run='openclaw agent --local --agent main --session-id demo --thinking low -m "your task"', install="npm install -g openclaw (Node 24.16+)"),
Harness("deepseek-harness", "DeepSeek Harness", "the Strata app", tested="DSH 0.2.1-alpha.3", built_in=True,
limits=_limits_dsh, aliases=("deepseek", "dsh")),
]
def find(name: str) -> Harness | None:
key = name.strip().lower()
for h in HARNESSES:
if key == h.id or key in h.aliases or key == h.title.lower():
return h
return None
def closest_name(name: str) -> str | None:
from strata_cli.launch.settings import closest
return closest(name, [h.id for h in HARNESSES])
# ------------------------------------------------------------------------------------------------ rendering
def server_root(url: str) -> str:
"""The server root: the address without a trailing /v1 (people paste the OpenAI-style base). Anthropic apps add /v1/messages themselves."""
root = url.strip().rstrip("/")
return root[:-3].rstrip("/") if root.lower().endswith("/v1") else root
def target(url: str, key: str | None, model: str, context: int | None = None) -> Target:
return Target(url=server_root(url), key=key or PLACEHOLDER_KEY, model=model or "strata", context=int(context or 32768))
def render(h: Harness, t: Target) -> dict:
"""What to show or write for this harness: files with their text, environment lines, the limits in plain words."""
files = [{"path": f.path, "kind": f.kind, "merge": f.merge, "text": f.make(t)} for f in h.files]
return {"harness": h.id, "title": h.title, "speaks": h.speaks, "status": h.status, "files": files,
"env": h.env(t) if h.env else {}, "limits": h.limits(t) if h.limits else [], "run": h.run, "install": h.install,
"tested": h.tested}
def expand(path: str) -> Path:
return Path(os.path.expanduser(path))
def write_file(spec: dict, *, backup: bool = True) -> dict:
"""Write one rendered file. An existing file is copied to `<name>.strata-bak-<time>` first; a JSON file with `merge` keeps the
keys it already had (the new ones win on the same key). Returns {path, backup, action}."""
path = expand(spec["path"])
path.parent.mkdir(parents=True, exist_ok=True)
existed = path.is_file()
bak = None
text = spec["text"]
if existed and backup:
bak = path.with_name(path.name + ".strata-bak-" + time.strftime("%Y%m%d-%H%M%S"))
shutil.copy2(path, bak)
if existed and spec.get("merge") and spec["kind"] == "json":
try:
old = json.loads(path.read_text(encoding="utf-8-sig"))
merged = _merge(old, json.loads(text))
_keep_output_cap(old, merged)
text = json.dumps(merged, indent=2)
except ValueError:
pass # an old file that is not JSON is replaced (a backup was made)
path.write_text(text if text.endswith("\n") else text + "\n", encoding="utf-8")
try:
os.chmod(path, 0o600) # it can hold the API key
except OSError:
pass
return {"path": str(path), "backup": str(bak) if bak else None,
"action": "merged" if existed and spec.get("merge") and spec["kind"] == "json" else ("replaced" if existed else "created")}
def _keep_output_cap(old, merged) -> None:
"""The output cap in a config being rewritten (in place on `merged`): a value the person set stays, so does a larger one from a newer Strata. The value
older Strata versions wrote on every window (OUTPUT_CAP, 4,096) is the generated default and is replaced, or the files written before #5006 would keep
the cap that breaks compaction for ever. OpenCode's limit.output and Claude Code's CLAUDE_CODE_MAX_OUTPUT_TOKENS."""
if not (isinstance(old, dict) and isinstance(merged, dict)):
return
models = ((old.get("provider") or {}).get("strata") or {}).get("models")
new_models = ((merged.get("provider") or {}).get("strata") or {}).get("models")
if isinstance(models, dict) and isinstance(new_models, dict):
for name, entry in models.items():
was = ((entry or {}).get("limit") or {}).get("output") if isinstance(entry, dict) else None
limit = (new_models.get(name) or {}).get("limit") if isinstance(new_models.get(name), dict) else None
if isinstance(was, int) and not isinstance(was, bool) and was != OUTPUT_CAP and isinstance(limit, dict):
limit["output"] = was
was_env = (old.get("env") or {}).get("CLAUDE_CODE_MAX_OUTPUT_TOKENS") if isinstance(old.get("env"), dict) else None
if isinstance(merged.get("env"), dict) and was_env is not None and str(was_env) != str(OUTPUT_CAP):
merged["env"]["CLAUDE_CODE_MAX_OUTPUT_TOKENS"] = was_env
def _merge(old, new):
if isinstance(old, dict) and isinstance(new, dict):
out = dict(old)
for k, v in new.items():
out[k] = _merge(old[k], v) if k in old else v
return out
return new
def mask(text: str, key: str) -> str:
return text.replace(key, key[:10] + "•" * 8) if key and key != PLACEHOLDER_KEY else text