serve: "env_to_user" (opt-in) moves opencode's <env> block into the first user message, so a new folder or day reuses the cached system prompt (11.9 -> 1.1 s first request)

opencode puts its <env> block (working directory, today's date) near the start of the system message, before ~40K characters that never change. The prefix cache only matches a prompt's start, so each new folder or day read the whole system turn again. With the switch the block goes to the front of the first user message (string or content parts), for OpenAI and Anthropic requests. Off by default; serve/test_env_to_user.py; docs/DETAILS.md.
This commit is contained in:
zsariboga authored and Niko1221 committed 2026-10-13 00:48:16 +02:00
1 parent ac21812c1a
commit d8a49433e3
4 files changed
+116 -2

No files matched your search

+7
View File
@@ -834,6 +834,13 @@ print(r.choices[0].message.content)
second call's parameters merge into the first). Anything else in those forms stays the text it is. Measured on
1,462 agent turns (Qwen3.8 under Claude Code, from signalnine/q27's drift corpus): 98.3% read as intended with it,
92.9% without. Off by default, so what a client gets back is unchanged unless you turn it on.
- **opencode's environment block in the user turn (opt-in).** opencode writes an `<env>` block (working directory,
today's date) near the start of its system message, ahead of ~40K characters of instructions and tool definitions that
are the same in every session. The prefix cache only reuses a prompt's start, so every new folder and every new day
read ~14K tokens again. `"env_to_user": true` moves that block to the front of the first user message: the system
turn is then the same for every session and the disk prefix cache restores it. Measured (RTX 5070 Ti Laptop, Swift 1.5
IQ3_XXS, a new opencode session in a second folder): first request 14,073 tokens with 13,693 from the cache, 1.1 s,
against 14,753 read, 11.9 s, without it; the model still answered the folder and date correctly (3/3). Off by default.
- **Prefill progress in the stream (opt-in).** `"return_progress": true` puts that progress on the stream instead of
sending only the keep-alive, as one extra field on a chunk with an empty delta: `prompt_progress` with `total`,
`cache`, `processed` and `time_ms`. Those are llama.cpp's four fields and mean the same there (`time_ms` is the time
+38 -2
View File
@@ -380,6 +380,42 @@ def unmark_think_literals(prompt: str, tags: dict[str, str] = LITERAL_TAGS) -> t
return "".join(out), spans
# Config "env_to_user" (opt-in): opencode writes its <env> block (working directory, today's date) near the START of
# the system message, before ~40K characters of instructions and tool definitions that are the same in every session.
# The prefix cache only reuses a prompt's start, so a new folder or a new day read all of it again (~14K tokens, 8-11 s).
# Moved to the front of the first user message, the system turn (and the tools the template writes into it) is the same
# for every session and the disk prefix cache restores it. The model still sees the same lines, one turn later.
ENV_TO_USER = False
_ENV_BLOCK = re.compile(r"(?:[^\n]*environment you are running in:\n)?<env>\n.*?\n</env>\n?", re.S)
def _env_to_user(messages: list[dict]) -> list[dict]:
if not ENV_TO_USER or not messages or messages[0].get("role") != "system":
return messages
sys_text = messages[0].get("content")
if not isinstance(sys_text, str):
return messages
m = _ENV_BLOCK.search(sys_text)
if m is None:
return messages
for i in range(1, len(messages)):
if messages[i].get("role") != "user":
continue
block = m.group(0).rstrip("\n")
c = messages[i].get("content")
if isinstance(c, str):
nc = block + "\n\n" + c
elif isinstance(c, list):
nc = [{"type": "text", "text": block + "\n\n"}] + c
else:
return messages
out = list(messages)
out[0] = dict(messages[0], content=sys_text[:m.start()] + sys_text[m.end():])
out[i] = dict(messages[i], content=nc)
return out
return messages
def _late_system_to_user(messages: list[dict]) -> list[dict]:
"""The chat template takes a system message only at the start ("System message must be at the beginning").
Clients also send them mid-conversation - Claude Code's hook context as {"role": "system"} after the first user
@@ -517,7 +553,7 @@ def openai_to_messages(req: dict) -> tuple[list[dict], list[dict] | None, dict]:
kwargs = {"enable_thinking": False}
elif k == "reasoning_effort" and "enable_thinking" not in kwargs:
kwargs.update(effort_kwargs(v))
return _late_system_to_user(messages), tools, kwargs
return _env_to_user(_late_system_to_user(messages)), tools, kwargs
BILLING_HEADER = "x-anthropic-billing-header:"
@@ -613,7 +649,7 @@ def anthropic_to_messages(req: dict, think_unasked: bool = True) -> tuple[list[d
# A config's reasoning_effort still applies: Service.with_shared sets output_config before this runs. A
# request that gives its own reasoning_budget_tokens (#123) asks for thinking, so it thinks as before.
kwargs["enable_thinking"] = False
return _late_system_to_user(messages), tools, kwargs
return _env_to_user(_late_system_to_user(messages)), tools, kwargs
# ------------------------------------------------------------------------------------------------ output parser
+5
View File
@@ -6533,6 +6533,11 @@ def main() -> int:
if not isinstance(recovery, bool):
raise SystemExit(f"[strata] config \"tool_call_recovery\" must be true or false, not {recovery!r}")
svc.tool_call_recovery = recovery
etu = cfg.get("env_to_user", False) # opt-in: see frontend._env_to_user
if not isinstance(etu, bool):
raise SystemExit(f"[strata] config \"env_to_user\" must be true or false, not {etu!r}")
import serve.frontend as _frontend
_frontend.ENV_TO_USER = etu
rs = cfg.get("repeat_stop_tokens", REPEAT_STOP_TOKENS) # #606: opt-out with 0
if isinstance(rs, bool) or not isinstance(rs, int) or rs < 0:
raise SystemExit(f"[strata] config \"repeat_stop_tokens\" must be a whole number >= 0 (0 = off), not {rs!r}")
+66
View File
@@ -0,0 +1,66 @@
"""Config "env_to_user": opencode's <env> block (working directory, date) leaves the system message for the first user
message, so the system turn is the same in every session and the prefix cache keeps it. Off (the default), nothing moves."""
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import serve.frontend as fe # noqa: E402
SYS = ("You are opencode.\n"
"Here is some useful information about the environment you are running in:\n"
"<env>\n"
" Working directory: C:/a\n"
" Today's date: Mon Oct 12 2026\n"
"</env>\n"
"Instructions that never change.")
class EnvBlockToUser(unittest.TestCase):
def setUp(self):
self.addCleanup(setattr, fe, "ENV_TO_USER", fe.ENV_TO_USER)
def convert(self, on, system=SYS, user="hi"):
fe.ENV_TO_USER = on
msgs, _, _ = fe.openai_to_messages({"messages": [{"role": "system", "content": system},
{"role": "user", "content": user}]})
return msgs
def test_moved_to_the_first_user_message(self):
msgs = self.convert(True)
self.assertEqual(msgs[0]["content"], "You are opencode.\nInstructions that never change.")
self.assertTrue(msgs[1]["content"].startswith(
"Here is some useful information about the environment you are running in:\n<env>\n Working directory: C:/a"))
self.assertTrue(msgs[1]["content"].endswith("</env>\n\nhi"))
def test_two_folders_share_the_system_turn(self):
a = self.convert(True)[0]["content"]
b = self.convert(True, system=SYS.replace("C:/a", "D:/b"))[0]["content"]
self.assertEqual(a, b)
def test_content_parts_get_a_leading_text_part(self):
fe.ENV_TO_USER = True
out = fe._env_to_user([{"role": "system", "content": SYS},
{"role": "user", "content": [{"type": "image", "image": "x.png"}, {"type": "text", "text": "hi"}]}])
self.assertEqual(out[1]["content"][0]["type"], "text")
self.assertIn("<env>", out[1]["content"][0]["text"])
self.assertEqual(out[1]["content"][1:], [{"type": "image", "image": "x.png"}, {"type": "text", "text": "hi"}])
def test_text_parts_are_read_as_one_text(self):
msgs = self.convert(True, user=[{"type": "text", "text": "hi"}])
self.assertIn("<env>", msgs[1]["content"] if isinstance(msgs[1]["content"], str) else msgs[1]["content"][0]["text"])
def test_anthropic_requests_too(self):
fe.ENV_TO_USER = True
msgs, _, _ = fe.anthropic_to_messages({"system": SYS, "messages": [{"role": "user", "content": "hi"}]})
self.assertNotIn("<env>", msgs[0]["content"])
self.assertIn("<env>", msgs[1]["content"])
def test_off_and_no_block_change_nothing(self):
self.assertEqual(self.convert(False)[0]["content"], SYS)
self.assertEqual(self.convert(True, system="Plain system prompt.")[0]["content"], "Plain system prompt.")
if __name__ == "__main__":
unittest.main()