mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 06:15:58 -10:00
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:
1 parent
ac21812c1a
commit
d8a49433e3
4 files changed
+116
-2
No files matched your search
@@ -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
@@ -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
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user