Fix gpt-oss empty turns: reasoning_effort=low via chat-template kwarg
Browse filesAt the harmony default (medium) effort, gpt-oss-20b spent the whole 512-token
budget in its analysis channel and never emitted assistantfinal, so the
normalizer dropped the output and Oona sat every turn out (trace health gate
measured 10/10 empty; she appeared in 1/10 attract frames). A plain
"Reasoning: low" line in the system message does nothing (the template routes
system content to the developer block), so the knob is passed server-side as
a chat-template kwarg, baked via TTW_REASONING_EFFORT into the engine image
(deployed; TypeError fallback for older vLLM). Live-verified: Oona returns
clean JSON with 4 valid offers; republished traces show 0/10 empty for
OpenAI (all four labs healthy). README deploy line updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- README.md +1 -1
- serve_council.py +23 -1
|
@@ -112,7 +112,7 @@ pip install -r requirements.txt
|
|
| 112 |
# No GPU, dummy agents (for trying the UI):
|
| 113 |
TTW_DUMMY=1 python app.py
|
| 114 |
# The multi-model council on Modal (deploy each engine, then enable the council):
|
| 115 |
-
TTW_APP_NAME=ttw-serve-gptoss TTW_MODEL=openai/gpt-oss-20b TTW_CUDA_DEVEL=1
|
| 116 |
TTW_APP_NAME=ttw-serve-minicpm TTW_MODEL=openbmb/MiniCPM3-4B TTW_CUDA_DEVEL=1 TTW_TRUST_REMOTE=1 python -m modal deploy serve_council.py
|
| 117 |
TTW_APP_NAME=ttw-serve-nemotron TTW_MODEL=nvidia/Nemotron-Mini-4B-Instruct TTW_CUDA_DEVEL=1 python -m modal deploy serve_council.py
|
| 118 |
TTW_APP_NAME=ttw-serve-qwen TTW_MODEL=AdmiralTaco/ttw-trader-0.5b TTW_CUDA_DEVEL=1 python -m modal deploy serve_council.py
|
|
|
|
| 112 |
# No GPU, dummy agents (for trying the UI):
|
| 113 |
TTW_DUMMY=1 python app.py
|
| 114 |
# The multi-model council on Modal (deploy each engine, then enable the council):
|
| 115 |
+
TTW_APP_NAME=ttw-serve-gptoss TTW_MODEL=openai/gpt-oss-20b TTW_CUDA_DEVEL=1 TTW_REASONING_EFFORT=low python -m modal deploy serve_council.py
|
| 116 |
TTW_APP_NAME=ttw-serve-minicpm TTW_MODEL=openbmb/MiniCPM3-4B TTW_CUDA_DEVEL=1 TTW_TRUST_REMOTE=1 python -m modal deploy serve_council.py
|
| 117 |
TTW_APP_NAME=ttw-serve-nemotron TTW_MODEL=nvidia/Nemotron-Mini-4B-Instruct TTW_CUDA_DEVEL=1 python -m modal deploy serve_council.py
|
| 118 |
TTW_APP_NAME=ttw-serve-qwen TTW_MODEL=AdmiralTaco/ttw-trader-0.5b TTW_CUDA_DEVEL=1 python -m modal deploy serve_council.py
|
|
@@ -39,6 +39,15 @@ CUDA_DEVEL = os.environ.get("TTW_CUDA_DEVEL", "0") == "1"
|
|
| 39 |
CUDA_TAG = os.environ.get("TTW_CUDA_TAG", "12.4.1-devel-ubuntu22.04")
|
| 40 |
# Some models ship custom modeling code (MiniCPM3); vLLM needs explicit opt-in.
|
| 41 |
TRUST_REMOTE = os.environ.get("TTW_TRUST_REMOTE", "0") == "1"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
app = modal.App(APP_NAME)
|
| 44 |
|
|
@@ -69,6 +78,7 @@ def build_image() -> modal.Image:
|
|
| 69 |
"TTW_MAX_LEN": str(MAX_LEN),
|
| 70 |
"TTW_DTYPE": DTYPE,
|
| 71 |
"TTW_TRUST_REMOTE": "1" if TRUST_REMOTE else "0",
|
|
|
|
| 72 |
}
|
| 73 |
)
|
| 74 |
)
|
|
@@ -113,7 +123,19 @@ class Engine:
|
|
| 113 |
params = self.SamplingParams(
|
| 114 |
temperature=temperature, top_p=0.9, max_tokens=max_tokens
|
| 115 |
)
|
| 116 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
return [o.outputs[0].text for o in outputs]
|
| 118 |
|
| 119 |
|
|
|
|
| 39 |
CUDA_TAG = os.environ.get("TTW_CUDA_TAG", "12.4.1-devel-ubuntu22.04")
|
| 40 |
# Some models ship custom modeling code (MiniCPM3); vLLM needs explicit opt-in.
|
| 41 |
TRUST_REMOTE = os.environ.get("TTW_TRUST_REMOTE", "0") == "1"
|
| 42 |
+
# Chat-template reasoning effort (gpt-oss harmony: low/medium/high; empty = leave
|
| 43 |
+
# the template default). At the default (medium) gpt-oss-20b spends the whole
|
| 44 |
+
# per-turn budget in its analysis channel and never emits `assistantfinal`, so
|
| 45 |
+
# the council's harmony normalizer drops the output and Oona sits every turn out
|
| 46 |
+
# (measured: 10/10 empty turns). Deploy the gptoss engine with
|
| 47 |
+
# TTW_REASONING_EFFORT=low to reach the final answer inside the budget. A plain
|
| 48 |
+
# "Reasoning: low" line in the system message does NOT work: the template routes
|
| 49 |
+
# system content to the developer block; the knob must be a template kwarg.
|
| 50 |
+
REASONING_EFFORT = os.environ.get("TTW_REASONING_EFFORT", "")
|
| 51 |
|
| 52 |
app = modal.App(APP_NAME)
|
| 53 |
|
|
|
|
| 78 |
"TTW_MAX_LEN": str(MAX_LEN),
|
| 79 |
"TTW_DTYPE": DTYPE,
|
| 80 |
"TTW_TRUST_REMOTE": "1" if TRUST_REMOTE else "0",
|
| 81 |
+
"TTW_REASONING_EFFORT": REASONING_EFFORT,
|
| 82 |
}
|
| 83 |
)
|
| 84 |
)
|
|
|
|
| 123 |
params = self.SamplingParams(
|
| 124 |
temperature=temperature, top_p=0.9, max_tokens=max_tokens
|
| 125 |
)
|
| 126 |
+
effort = os.environ.get("TTW_REASONING_EFFORT", "")
|
| 127 |
+
if effort:
|
| 128 |
+
try:
|
| 129 |
+
outputs = self.llm.chat(
|
| 130 |
+
conversations, params,
|
| 131 |
+
chat_template_kwargs={"reasoning_effort": effort},
|
| 132 |
+
)
|
| 133 |
+
except TypeError:
|
| 134 |
+
# Older vLLM without chat_template_kwargs: serve without the
|
| 135 |
+
# knob rather than fail the whole engine.
|
| 136 |
+
outputs = self.llm.chat(conversations, params)
|
| 137 |
+
else:
|
| 138 |
+
outputs = self.llm.chat(conversations, params)
|
| 139 |
return [o.outputs[0].text for o in outputs]
|
| 140 |
|
| 141 |
|