* fix: normalize usage tokens + migration streaming/batching
- Migration: replace .fetchall() with yield_per streaming, replace per-message INSERT+SAVEPOINT with batched inserts (5k/batch) with fallback to row-by-row on error, add progress logging
- Write path: call normalize_usage() in upsert_message() before saving to ensure input_tokens/output_tokens always present
- Read path: analytics queries now COALESCE across input_tokens/prompt_tokens and output_tokens/completion_tokens so historical data with OpenAI-format keys is visible
* fix: restore defensive timestamp conversion in migration
Re-add try/except around int(float(timestamp)) that was accidentally dropped. Without this, a non-numeric timestamp string would cause a TypeError on the subsequent comparison, breaking the entire upgrade.
* revert: remove changes to chat_messages.py
The non-streaming response handler was saving assistant messages without
their usage/token data. While the streaming handler correctly extracted
and saved usage information, the non-streaming path discarded it entirely.
This caused assistant messages from non-streaming completions to have
NULL usage in the chat_message table, making them invisible to the
analytics token aggregation queries and contributing to the '0 tokens'
display in Admin Panel Analytics.
Extract and normalize the usage data from the API response and include
it in the database upsert, matching the pattern already used by the
streaming handler.
- HTML preview (iframe) no longer shows Edit/Save toolbar buttons
- Clicking Source toggle opens CodeMirror editor with syntax highlighting
- Save button appears only in source mode, using saveCodeFile()
- Ctrl+S saving supported via CodeMirror keybinding
* perf(models): batch-fetch function valves to eliminate N+1 queries
get_action_priority() called Functions.get_function_valves_by_id()
individually for every action on every model — an N+1 query pattern
that issued one DB round-trip per (action x model) pair.
Add Functions.get_function_valves_by_ids() that fetches all valves in
a single WHERE IN query, then look up each action's valves from the
pre-fetched dict inside get_action_priority().
No functional change — same priority resolution, same sort order.
* Update models.py
* Update models.py
Replace the recursive spread-based implementation with an iterative
push+reverse approach. The recursive version created a new array at
each level of recursion via spread, resulting in O(d^2) array copies
where d is the conversation depth. The iterative version walks from
the target message to the root, pushes each message, and reverses
once at the end for O(d) total work.
No behavioral change - same input produces the same output array.
- Add FileCodeEditor.svelte: CodeMirror wrapper with auto language
detection, dark mode, Ctrl+S save, reactive to value/filePath changes
- Replace Shiki read-only highlighting + textarea editing with
always-editable CodeMirror for code files in FileNav preview
- Show persistent Save button for code files in toolbar
- Non-code text files keep existing Edit/Save/Cancel textarea flow
- SVG retains Shiki highlighting for visual preview mode
removeAllDetails() uses replaceOutsideCode() which splits content on
triple-backtick code blocks before applying the details-removal regex.
When thinking/reasoning content inside a <details> block contained
code blocks (backticks survive html.escape), the <details> opening
and </details> closing tags ended up in different split segments,
making the regex unable to match either. This caused thinking content
to leak through to TTS playback.
Fix: add a direct <details> strip (without code-block splitting) as
the first step of getMessageContentParts(), which is the TTS-specific
entry point. This catches the edge case while keeping removeAllDetails
safe for copy-to-clipboard (where legitimate <details> inside code
blocks should be preserved).
Fixes#22197