* refactor(agent): unify runner request fitting
* fix(agent): fit every runner model request
* fix(agent): count resumed state during request fitting
* refactor(agent): consolidate request fitting state
* refactor(agent): defer transcript assembly to runner
Keep persisted history and the fresh turn as explicit inputs until the Runner assembles the provider transcript. Preserve ContextBuilder and direct AgentRunner compatibility while making the save boundary structural.
Refs NAN-81.
* fix(providers): preserve mixed adjacent user content
Mid-stream response.failed events from OpenAI carry
'server_error' (underscore), but the transient-error marker
list only had 'server error' (space). is_transient_response
missed it, so the retry loop gave up on the first attempt
instead of using its normal 1s/2s/4s backoff.
convert_messages() emitted reasoning items with ``content`` as a plain
string whenever preserve_reasoning was enabled (the DeepSeek spec).
DeepSeek's Responses gateway rejects that shape with a serde error
("input: invalid type: string ..., expected a sequence"), which surfaced
only after token consolidation cleared provider_state and forced the
full-history conversion path; replayed server items already carry list
content, which is why normal multi-turn requests never failed. Serialize
reasoning content as a list of output_text parts, matching the OpenAI
Responses schema and DeepSeek's accepted wire shape (verified live against
api.deepseek.com/responses).
The serde fallback classifier introduced in the previous commit remains as
a last-resort safeguard for any remaining wire incompatibility.
Tests: extend test_preserves_deepseek_reasoning_content to the array shape;
add a full-history regression with the observed failing item, a
replay/consolidation regression covering both replayed and converted
reasoning items, and provider-level request fixtures for both paths.
Full suite: 5773 passed, 22 skipped (only the known local-only
channels/sms packaging failure remains).
DeepSeek's new Responses endpoint (deepseek-v4-flash) intermittently rejects valid request bodies with serde deserialization errors such as 'input: invalid type: string ..., expected a sequence'. These were not classified as compatibility errors, so affected conversations died instead of falling back to Chat Completions.
The wire format is correct (input serializes as a list), so this is a server-side Responses compatibility issue; Chat Completions is strictly more permissive, making fallback safe. Extend the fallback classifier to recognize serde body-parsing markers. Repeated failures still trip the existing circuit breaker.
The live v1beta API rejects the legacy responseFormat.image block
(enum-based aspectRatio/imageSize fields) for gemini-3.1-flash-lite-image
with INVALID_ARGUMENT, even for documented plain-string values. Gemini
Flash image models accept plain-string hints under
generationConfig.imageConfig instead (e.g. aspectRatio 16:9, imageSize
1K), which the API accepts. Switch the flash path to imageConfig and
update the provider tests accordingly. Other providers (aihubmix,
ollama, imagen) are untouched.
Address review feedback that the capability checks were broader than the
documented per-model matrix:
- Drop the extreme aspect ratios (1:4, 4:1, 1:8, 8:1) from the Flash
allow-list. They are only documented for 3.1 Flash / Flash Lite, so the
global set could send an unsupported ratio to 2.5 Flash Image or 3.1 Pro
Image. Keep the ratios common to every Flash image model.
- Identify imageSize support positively via "gemini-3" instead of excluding
"2.5". The old predicate also matched gemini-2.0-flash-preview-image-
generation, which (with the default 1K size) altered that model's request
shape even though only Gemini 3+ image models accept a configurable size.
Add tests for the gemini-2.0 image-size drop and the extreme-ratio drop.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Gemini Flash image path (`generateContent`) dropped both `aspect_ratio`
and `image_size`: `generate()` never forwarded them and
`_generate_gemini_flash` did not accept them, so every request fell back to
1:1 / input-matched output. The Imagen path was unaffected.
Forward the hints and emit them under
`generationConfig.responseFormat.image` per the current Gemini API. Aspect
ratio is validated against the accepted set; `imageSize` is validated against
{512,1K,2K,4K} and only sent to Gemini 3+ image models, since
`gemini-2.5-flash-image` supports only `aspectRatio`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>