Raw chain-of-thought was written to both `content` (`reasoning_text`) and
`summary` (`summary_text`), and the stream emitter sent the same delta on
`response.reasoning_text.delta` *and* `response.reasoning_summary_text.delta`.
Clients that render both channels therefore printed every thinking chunk
twice — most visibly the Codex CLI, whose thinking panel repeated itself.
OpenAI keeps the two channels distinct: `content` carries the raw CoT while
`summary` is the summarised view. Emit the thinking on `content` only:
- `openai_responses_reasoning_text_fields` becomes
`openai_responses_reasoning_text_parts`, returning just the `content` array;
reasoning items keep `summary: []` (or a provider-supplied summary).
- The Responses stream emitter emits `response.reasoning_text.delta` /
`.done` and no longer mirrors them onto the summary events. The reasoning
`output_item.added` no longer announces a `reasoning_summary_part`.
- The provider-state reasoning reader accepts `content` (`reasoning_text`)
first and falls back to `summary`, so it also understands items produced by
older Aether versions; its state field is renamed accordingly.
- Non-streaming builders (Chat -> Responses, manual Responses response, Grok
gateway) place the thinking on `content` and leave `summary` empty.
Tests cover the raw thinking appearing exactly once in the emitted stream.
Show already checked / already associated models first in the 关联模型
list so they are easy to find and uncheck. Keep that order inside the
current search results.
Let admins select one or more models in a provider's model list and
delete them together, using the existing single-model delete API and
the same confirm-danger pattern as global model batch delete.
OpenAI Responses treats reasoning.content as the raw chain-of-thought
and summary as a skim view. Aether was dumping thinking into summary
and leaving content null, which hid the thinking panel in desktop UIs.
Put reasoning_content / equivalent text into reasoning_text content
parts, and copy the same text into summary_text so CLI clients still
work. Stream emitters now send both reasoning_text and summary events.
The cleanup loop break condition used rows_affected() from the UPDATE
statement, but for rows that only had blob/audit refs (no inline
compressed body data), the UPDATE reported 0 affected rows. This caused
the loop to exit after the first batch, skipping the majority of
candidates.
Change the break condition in all 4 cleanup functions from:
if cleaned == 0 || cleaned < batch_size
to:
if rows.len() < batch_size
This ensures the loop continues as long as SELECT returns a full batch,
regardless of how many rows the UPDATE actually modified.
Affected functions:
- cleanup_usage_raw_body_fields
- cleanup_usage_compressed_body_fields
- cleanup_usage_header_fields
- cleanup_usage_stale_body_fields
The ON CONFLICT update guard for provider_id, provider_endpoint_id, and
provider_api_key_id previously required billing_status = 'pending'. Once a
usage row left pending state with these fields still NULL, subsequent upserts
could never fill them. Add an OR IS NULL clause so missing provider links are
always recoverable regardless of billing status.