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.
Usage records show a reasoning badge next to the model name for OpenAI and
Claude requests, but Gemini requests never got one. The extraction only read
the OpenAI/Claude shapes (`reasoning_effort`, `reasoning.effort`,
`output_config.effort`), while Gemini states its reasoning depth inside
`generationConfig.thinkingConfig` — so nothing was written to the usage
metadata and the list and detail views had no badge to render.
Read the Gemini shape too, as a fallback after the existing three so the
OpenAI and Claude paths are untouched:
- `thinkingLevel` / `thinking_level` wins when present, trimmed and
lowercased, with the protobuf enum prefix stripped so `THINKING_LEVEL_HIGH`
resolves like `high`.
- Otherwise `thinkingBudget` / `thinking_budget` goes through the existing
shared budget ladder, yielding the same `low|medium|high|xhigh` vocabulary
the badge already understands.
- Both camelCase and snake_case spellings are read, so a captured client body
and a converted provider body resolve to the same label.
- `includeThoughts` alone is a visibility flag, not a depth, and produces no
badge.
Two cases are handled explicitly rather than through the shared ladder:
- `thinkingBudget: 0` disables reasoning outright. The shared ladder maps
`0..=1664` to `low`, which would report an explicitly disabled request as a
shallow one, so it reports `none` instead.
- `THINKING_LEVEL_UNSPECIFIED` is the enum's "no explicit level" member, not a
depth; it is rejected rather than surfaced as an `unspecified` badge.
The frontend needs no change: `UsageModelDisplay` already renders the badge
whenever the fields are present, and keeps the `high -> xhigh` mapping format
when the requested and upstream efforts differ.
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.