ax 23.0.16 — three DeepSeek/thinking fixes, with exact locations
All line numbers are from the generated Go in
github.com/ax-llm/ax/packages/go@v0.0.0-20260817012209-94681b903c12/axllm.go.
Per axir-provenance.json that file is emitted ("target": "go", "enforced": true), so the changes belong in the IR/source that generates it —
the Go references are for locating the logic, not for patching directly.
Verified empirically against api.deepseek.com (model deepseek-v4-flash) and
against Together AI serving the same weights as
deepseek-ai/DeepSeek-V4-Flash-0731.
Fix 1 — thinking support is gated on an exact model-name match
Highest leverage: this one fix cascades and also resolves the tool_choice
problem in Fix 3 for DeepSeek-family models.
Location — axllm.go:12762-12766
v_is_flash = _core_eq(v_model, "deepseek-v4-flash")
v_is_pro = _core_eq(v_model, "deepseek-v4-pro")
v_supports_thinking = _core_or(v_is_flash, v_is_pro)
v_is_reasoner = _core_eq(v_model, "deepseek-reasoner")
v_unsupported_tool_choice_left = _core_or(v_supports_thinking, v_is_reasoner)
Problem
The test is exact string equality. Every third-party host namespaces the model
id, so none of them match:
| host |
model id |
v_supports_thinking |
| api.deepseek.com |
deepseek-v4-flash |
true |
| Together AI |
deepseek-ai/DeepSeek-V4-Flash-0731 |
false |
| OpenRouter |
deepseek/deepseek-v4-flash |
false |
When it is false, two things silently go wrong further down:
- The
thinking block is never emitted (the branch at 12767 is skipped), so a
reasoning model runs with reasoning off.
v_unsupported_tool_choice_left is also false, so the strip at
12804-12813 never runs and a forced tool_choice survives — which
independently suppresses reasoning (see Fix 3).
Suggested change
Normalize before comparing: lowercase, drop any vendor prefix before /, and
drop a trailing date/version suffix, then match on the resulting base name.
normalize("deepseek-ai/DeepSeek-V4-Flash-0731") -> "deepseek-v4-flash"
normalize("deepseek/deepseek-v4-flash") -> "deepseek-v4-flash"
normalize("deepseek-v4-flash") -> "deepseek-v4-flash"
A caller-declared override (e.g. model_config.supportsThinking) would also
work, and is more robust for hosts with unpredictable naming.
Verification
With GJ_AGENT_MODEL=deepseek-ai/DeepSeek-V4-Flash-0731 and a thinking budget
set, the outgoing payload should contain "thinking": {"type": "enabled"} and
must not contain tool_choice.
Fix 2 — low and medium collapse to high
Location — axllm.go:12782-12791
v_is_xhigh = _core_eq(v_reasoning, "xhigh")
v_is_max = _core_eq(v_reasoning, "max")
v_reasoning_is_max = _core_or(v_is_xhigh, v_is_max)
v_budget_is_highest = _core_eq(v_budget, "highest")
v_is_max_effort = _core_or(v_reasoning_is_max, v_budget_is_highest)
if coreTruthy(v_is_max_effort) {
coreSet(v_payload, "reasoning_effort", "max")
} else {
coreSet(v_payload, "reasoning_effort", "high") // every non-max level
}
Problem
Both branches below max write "high", so the effective ladder is
off / high / max with no middle. Measured on 23.0.16, one row per run:
| configured |
on the wire |
high |
reasoning_effort: "high" |
medium |
reasoning_effort: "high" |
low |
reasoning_effort: "high" |
Suggested change
Mirror the Grok adapter, which already implements this correctly —
_provider_apply_grok_chat_quirks, axllm.go:13031-13045:
if none -> "none"
if lowish -> "low"
if medium -> "medium"
if highish -> "high"
Apply the same shape in the DeepSeek adapter, keeping the existing
xhigh|max|highest -> "max" rung on top.
Why it matters
At high, one 8-step agent episode averages 9.2 minutes and 141k tokens.
Across a 339-episode benchmark that is roughly a day of wall clock. A real
medium is the difference between a suite we can run nightly and one we can't.
Fix 3 — the forced __axOutput call defeats thinking
Location — axllm.go:20630-20634
v_no_user_functions = _core_eq(v_fn_count, 0)
if coreTruthy(v_no_user_functions) {
v_forced_function = Object()
coreSet(v_forced_function, "name", "__axOutput")
v_mode = v_forced_function // becomes request "function_call"
}
Problem
When a program registers no user functions — the normal case for callers that
run their own tool protocol — ax forces the synthetic output call. On hosts
where thinking and forced tool selection conflict, forcing silently disables
reasoning. Measured directly against Together, same prompt each time
(correct answer 17:41):
| request |
reasoning tokens |
answer |
plain, reasoning_effort: max |
527 |
17:41 ✓ |
unforced tools + reasoning_effort: max |
1469 |
17:41 ✓ |
forced via tool_choice |
none |
18:35 ✗ |
response_format: json_object / json_schema |
none |
wrong |
Note the model called the function on every attempt without being forced,
and reasoned more when unforced — so forcing buys nothing here and costs the
reasoning.
Suggested change
Leave function_call as auto when the target model supports thinking, rather
than pinning it to __axOutput. ax already computes exactly this condition one
adapter over (v_unsupported_tool_choice_left, axllm.go:12766); the synthetic
output path just doesn't consult it.
For the DeepSeek family specifically, Fix 1 makes the existing strip at
axllm.go:12804-12813 handle this automatically. Fix 3 is the general case for
every other provider where the same conflict exists.
Reproduction
KEY=<together key>
M=deepseek-ai/DeepSeek-V4-Flash-0731
P='A train leaves at 14:20 at 87 km/h. Another leaves the same station at 15:05 at 112 km/h on the same track. At what clock time does the second catch the first?'
# reasons correctly: reasoning_tokens > 0, answer 17:41
curl -s https://api.together.ai/v1/chat/completions \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d "{\"model\":\"$M\",\"messages\":[{\"role\":\"user\",\"content\":\"$P\"}],\"reasoning_effort\":\"max\"}"
# no reasoning at all: reasoning_tokens absent, answer wrong
curl -s https://api.together.ai/v1/chat/completions \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d "{\"model\":\"$M\",\"messages\":[{\"role\":\"user\",\"content\":\"$P\"}],\"reasoning_effort\":\"max\",\"response_format\":{\"type\":\"json_object\"}}"
Check usage.completion_tokens_details.reasoning_tokens and the presence of
choices[0].message.reasoning.
Why this matters downstream
We published a public benchmark row for DeepSeek V4 Flash at 0.177 recall that
was, unknown to us at the time, produced with thinking silently off. It read as
a weak model when it was a misconfiguration. Fixes 1 and 3 are both cases where
reasoning disappears without any error, warning, or field in the response to
indicate it — that silence is the expensive part.
The 23.0.16 config-propagation fix already resolved the earlier half of this
(a caller-supplied thinkingTokenBudget now reaches the request), and the
thinking-replay fix cut our per-episode cost from ~118k to ~71k tokens. Both
are much appreciated.
ax 23.0.16 — three DeepSeek/thinking fixes, with exact locations
All line numbers are from the generated Go in
github.com/ax-llm/ax/packages/go@v0.0.0-20260817012209-94681b903c12/axllm.go.Per
axir-provenance.jsonthat file is emitted ("target": "go", "enforced": true), so the changes belong in the IR/source that generates it —the Go references are for locating the logic, not for patching directly.
Verified empirically against
api.deepseek.com(modeldeepseek-v4-flash) andagainst Together AI serving the same weights as
deepseek-ai/DeepSeek-V4-Flash-0731.Fix 1 — thinking support is gated on an exact model-name match
Highest leverage: this one fix cascades and also resolves the tool_choice
problem in Fix 3 for DeepSeek-family models.
Location —
axllm.go:12762-12766Problem
The test is exact string equality. Every third-party host namespaces the model
id, so none of them match:
v_supports_thinkingdeepseek-v4-flashdeepseek-ai/DeepSeek-V4-Flash-0731deepseek/deepseek-v4-flashWhen it is false, two things silently go wrong further down:
thinkingblock is never emitted (the branch at 12767 is skipped), so areasoning model runs with reasoning off.
v_unsupported_tool_choice_leftis also false, so the strip at12804-12813 never runs and a forced
tool_choicesurvives — whichindependently suppresses reasoning (see Fix 3).
Suggested change
Normalize before comparing: lowercase, drop any vendor prefix before
/, anddrop a trailing date/version suffix, then match on the resulting base name.
A caller-declared override (e.g.
model_config.supportsThinking) would alsowork, and is more robust for hosts with unpredictable naming.
Verification
With
GJ_AGENT_MODEL=deepseek-ai/DeepSeek-V4-Flash-0731and a thinking budgetset, the outgoing payload should contain
"thinking": {"type": "enabled"}andmust not contain
tool_choice.Fix 2 —
lowandmediumcollapse tohighLocation —
axllm.go:12782-12791Problem
Both branches below
maxwrite"high", so the effective ladder isoff / high / max with no middle. Measured on 23.0.16, one row per run:
highreasoning_effort: "high"mediumreasoning_effort: "high"lowreasoning_effort: "high"Suggested change
Mirror the Grok adapter, which already implements this correctly —
_provider_apply_grok_chat_quirks,axllm.go:13031-13045:Apply the same shape in the DeepSeek adapter, keeping the existing
xhigh|max|highest -> "max"rung on top.Why it matters
At
high, one 8-step agent episode averages 9.2 minutes and 141k tokens.Across a 339-episode benchmark that is roughly a day of wall clock. A real
mediumis the difference between a suite we can run nightly and one we can't.Fix 3 — the forced
__axOutputcall defeats thinkingLocation —
axllm.go:20630-20634Problem
When a program registers no user functions — the normal case for callers that
run their own tool protocol — ax forces the synthetic output call. On hosts
where thinking and forced tool selection conflict, forcing silently disables
reasoning. Measured directly against Together, same prompt each time
(correct answer 17:41):
reasoning_effort: maxtools+reasoning_effort: maxtool_choiceresponse_format: json_object/json_schemaNote the model called the function on every attempt without being forced,
and reasoned more when unforced — so forcing buys nothing here and costs the
reasoning.
Suggested change
Leave
function_callasautowhen the target model supports thinking, ratherthan pinning it to
__axOutput. ax already computes exactly this condition oneadapter over (
v_unsupported_tool_choice_left,axllm.go:12766); the syntheticoutput path just doesn't consult it.
For the DeepSeek family specifically, Fix 1 makes the existing strip at
axllm.go:12804-12813handle this automatically. Fix 3 is the general case forevery other provider where the same conflict exists.
Reproduction
Check
usage.completion_tokens_details.reasoning_tokensand the presence ofchoices[0].message.reasoning.Why this matters downstream
We published a public benchmark row for DeepSeek V4 Flash at 0.177 recall that
was, unknown to us at the time, produced with thinking silently off. It read as
a weak model when it was a misconfiguration. Fixes 1 and 3 are both cases where
reasoning disappears without any error, warning, or field in the response to
indicate it — that silence is the expensive part.
The 23.0.16 config-propagation fix already resolved the earlier half of this
(a caller-supplied
thinkingTokenBudgetnow reaches the request), and thethinking-replay fix cut our per-episode cost from ~118k to ~71k tokens. Both
are much appreciated.