Commit Graph

4 Commits

  • feat(proxy): Gemini Native API proxy integration (#1918)
    * refactor(proxy): extract take_sse_block helper with CRLF delimiter support
    
    Replace inline `buffer.find("\n\n")` SSE splitting logic across streaming,
    streaming_responses, response_handler, and response_processor with a shared
    `take_sse_block` function that handles both `\n\n` and `\r\n\r\n` delimiters.
    
    * feat(proxy): add Gemini Native URL builder and full-URL resolver
    
    Introduce gemini_url module that normalizes legacy Gemini/OpenAI-compatible
    base URLs into canonical models/*:generateContent endpoints. Supports both
    structured Gemini URLs (auto-normalized) and opaque relay URLs (pass-through
    with query params only).
    
    * feat(proxy): add Gemini Native schema, shadow store, transform, and streaming
    
    - gemini_schema: Gemini generateContent request/response type definitions
    - gemini_shadow: session-scoped shadow store for thinking signature and
      tool-call state replay across streaming chunks
    - transform_gemini: bidirectional Anthropic Messages ↔ Gemini Native
      request/response conversion with thinking block and tool-use support
    - streaming_gemini: Gemini SSE → Anthropic SSE streaming adapter with
      incremental thinking/text/tool_use delta emission
    
    * feat(proxy): wire Gemini Native format into proxy core and Claude adapter
    
    Integrate gemini_native api_format throughout the proxy pipeline:
    - ClaudeAdapter: detect Gemini provider type, Google/GoogleOAuth auth
      strategies, and suppress Anthropic-specific headers for Gemini targets
    - Forwarder: Gemini URL resolution, shadow store threading, endpoint
      rewriting to models/*:generateContent with stream/non-stream variants
    - Handlers: route Gemini streaming through streaming_gemini adapter and
      non-streaming through transform_gemini converter
    - Server/State: add GeminiShadowStore to shared ProxyState
    - StreamCheck: support gemini_native health check with proper auth headers
    
    * feat(ui): add Gemini Native provider preset and api format option
    
    - Add gemini_native to ClaudeApiFormat type and ProviderMeta.apiFormat
    - Add "Gemini Native" provider preset with default Google AI endpoints
    - Show Gemini-specific endpoint hints and full-URL mode guidance
    - Add gemini_native option to API format selector in ClaudeFormFields
    - Add i18n strings for zh/en/ja
    
    * feat(proxy): add Gemini Native tool argument rectification
    
    * feat(proxy): update Gemini streaming and transformation logic
    
    * fix(proxy): align shadow turns to tail on client history truncation
    
    * fix: revert unrelated cache_key change in claude proxy transform
    
    Restore .unwrap_or(&provider.id) fallback for cache_key to match main
    branch behavior. Only gemini_native related changes should be in this branch.
    
    * Prevent Gemini review regressions in streaming and tool rectification
    
    PR #1918 review feedback exposed two correctness issues in the Gemini Native adapter path. Gemini SSE buffering was still using lossy UTF-8 decoding, which could corrupt split multibyte payloads and drop streamed output. Tool arg rectification also removed top-level parameters eagerly, which broke tools that legitimately define a parameters field.
    
    This change moves Gemini SSE buffering onto the existing append_utf8_safe path and makes parameters flattening conditional on the schema actually expecting nested extraction. The old Skill rectification path stays intact, and new regression tests cover both the preserved parameters case and UTF-8-split JSON payloads.
    
    Constraint: Existing PR #1918 review feedback must be fixed without staging unrelated local docs and artifact files
    Rejected: Keep String::from_utf8_lossy in Gemini SSE buffering | corrupts split multibyte payloads and can drop JSON chunks
    Rejected: Always preserve the parameters wrapper | regresses the existing nested-parameters rectification path for Skill-style tools
    Confidence: high
    Scope-risk: narrow
    Reversibility: clean
    Directive: Keep Gemini SSE buffering on the UTF-8-safe accumulator path and only unwrap parameters when the target schema does not declare it as a legitimate field
    Tested: cargo fmt --manifest-path src-tauri/Cargo.toml --all; cargo test --manifest-path src-tauri/Cargo.toml preserves_utf8_boundaries_when_json_payload_spans_chunks; cargo test --manifest-path src-tauri/Cargo.toml gemini_to_anthropic_rectifies_tool_args_from_schema_hints; cargo test --manifest-path src-tauri/Cargo.toml rectifies_streamed_skill_args_from_nested_parameters; cargo test --manifest-path src-tauri/Cargo.toml gemini_to_anthropic_preserves_legitimate_parameters_arg
    Not-tested: Full src-tauri test suite; live end-to-end Gemini relay traffic against upstream services
    
    * Keep Gemini tool replay stable across Claude request boundaries
    
    Claude Code follow-up requests were still falling back to locally reconstructed functionCall parts, which dropped Gemini thought signatures and triggered INVALID_ARGUMENT errors from the official Gemini API. The replay path needed to survive real Claude request boundaries, not just idealized in-process test flows.
    
    This change makes Claude requests reuse X-Claude-Code-Session-Id as the shadow session key, records streamed Gemini tool turns before tool_use events are fully drained, and matches assistant tool_use turns to shadow state by tool_use id and normalized tool name before positional fallback. Together these fixes keep thoughtSignature-bearing Gemini tool calls available for the next request in the loop.
    
    Constraint: Claude Code sends a stable X-Claude-Code-Session-Id header while metadata.session_id may be absent on follow-up requests
    Rejected: Rely on metadata-only Claude session extraction | generated fresh session ids and broke cross-request shadow replay
    Rejected: Record Gemini shadow only after streaming completes | loses the race when the client sends the next request immediately after tool_use
    Confidence: high
    Scope-risk: narrow
    Reversibility: clean
    Directive: Preserve Gemini shadow continuity across requests by keying Claude sessions from the header first and persisting tool-call shadow before yielding tool_use events downstream
    Tested: cargo fmt --manifest-path src-tauri/Cargo.toml --all; cargo test --manifest-path src-tauri/Cargo.toml test_extract_session_from_claude_header; cargo test --manifest-path src-tauri/Cargo.toml test_extract_session_from_claude_header_precedes_metadata; cargo test --manifest-path src-tauri/Cargo.toml stores_tool_shadow_before_tool_use_events_are_fully_drained; cargo test --manifest-path src-tauri/Cargo.toml shadow_replay_matches_tool_use_turn_by_id_when_position_drifts; cargo test --manifest-path src-tauri/Cargo.toml shadow_replay_aligns_to_latest_turns_after_client_truncation
    Not-tested: Full src-tauri test suite without test filters; live end-to-end Gemini relay after this exact commit hash
    
    * style: apply cargo fmt to pass Backend Checks CI
    
    Wrap prompt_cache_key chained call across lines per rustfmt default
    formatting. Pure formatting change, no behavior difference.
    
    * fix(proxy/gemini): synthesize unique ids for no-id tool calls + enforce object params schema
    
    P1 — Parallel tool calls without Gemini-assigned ids no longer collapse.
    Gemini 2.x native parallel `functionCall` entries may omit the `id` field.
    The previous `merge_tool_call_snapshots` fell back to matching by `name`,
    which silently merged two parallel calls to the same function into one
    entry — dropping the first call's args. The non-streaming path and shadow
    store further bottlenecked on empty-string ids: multiple `tool_use` blocks
    shared the same id, and `tool_name_by_id.get("")` could only return one
    mapping, causing later `tool_result` round-trips to fail with
    `Unable to resolve Gemini functionResponse.name` or bind to the wrong tool.
    
    Fix: introduce `synthesize_tool_call_id()` producing `gemini_synth_<uuid>`.
    Both streaming and non-streaming response paths now guarantee every
    Anthropic-visible tool_use carries a unique id. `merge_tool_call_snapshots`
    matches by id first, falling back to the `parts` array position (for the
    cumulative-streaming case) while preserving the synthesized id across
    chunks. `convert_message_content_to_parts` detects the synthetic prefix
    and strips the id from outbound `functionCall`/`functionResponse` so the
    internal identifier never leaks upstream. `shadow_parts` performs the
    same strip when replaying a recorded assistant turn.
    
    P2 — Vertex AI rejects empty `parameters` schemas. When an Anthropic tool
    arrives with missing or empty `input_schema`, the proxy used to emit
    `"parameters": {}` (no `type`), which fails Vertex AI validation with
    `functionDeclaration parameters schema should be of type OBJECT`.
    Contrary to the automated-review suggestion, the fix is not to omit
    `parameters` (that too is rejected) but to normalize to the canonical
    empty-object form `{type: "object", properties: {}}`.
    Refs: google-gemini/generative-ai-python#423, BerriAI/litellm#5055.
    
    Fix: new `ensure_object_schema` helper in `gemini_schema` promotes
    missing `type` to `"object"` and adds empty `properties` when absent,
    while leaving atomic (non-object) schemas untouched.
    
    Tests: seven new regressions covering parallel no-id calls, cumulative
    chunk id reuse, synthetic-id round-trip both directions, shadow replay
    id stripping, and the three Vertex-AI schema shapes.
    
    The two existing wrapper functions (`gemini_to_anthropic` and
    `gemini_to_anthropic_with_shadow`) gain `#[allow(dead_code)]` to clear
    a pre-existing clippy -D warnings failure — they are part of the public
    transform API surface and intentionally kept for future callers.
    
    Addresses Codex review P1/P2 on #1918.
    
    * fix(proxy/gemini): narrow URL normalization + guard empty OAuth access_token
    
    P2a — Preserve opaque relay URLs that contain `/v1/models/` prefixes.
    
    `should_normalize_gemini_full_url` previously flagged any full URL whose
    path merely contained `/v1beta/models/` or `/v1/models/` as a structured
    Gemini endpoint, forcing rewrite to `.../v1beta/models/{model}:method`.
    This silently dropped legitimate relay route segments (e.g.
    `https://relay.example/v1/models/invoke` → `.../v1beta/models/...:generateContent`,
    losing `/invoke`) and sent traffic to the wrong upstream path.
    
    Replace the bare `contains(...)` checks with
    `matches_structured_gemini_models_path`, which requires the
    `/models/` segment to be followed by a canonical Gemini method call
    (`*:generateContent` or `*:streamGenerateContent`). The
    `matches_bare_gemini_models_path` helper is generalized (and renamed) to
    handle both `/v1beta/models/` and `/v1/models/` alongside the original
    bare `/models/` shape.
    
    P2b — Reject empty Gemini OAuth access_tokens before they reach the
    bearer header.
    
    `GeminiAdapter::parse_oauth_credentials` accepts refresh-token-only JSON
    (and surfaces `{"access_token": "", ...}` for expired credentials) with
    `access_token` defaulting to `""`. The Claude adapter's GeminiCli branch
    then called `AuthInfo::with_access_token(key, creds.access_token)`
    unconditionally, so the bearer-header builder at
    `AuthStrategy::GoogleOAuth` resolved to `Authorization: Bearer ` — a
    deterministic 401 from upstream.
    
    CC Switch does not currently exchange the refresh_token for a fresh
    access_token (`OAuthCredentials::needs_refresh` / `can_refresh` are
    annotated `#[allow(dead_code)]`). Until that exists, only attach
    `access_token` when it is non-empty; fall back to plain GoogleOAuth
    strategy with the raw key and log a warn pointing users at
    `~/.gemini/oauth_creds.json` so the failure mode is observable.
    
    Tests:
    - gemini_url.rs: three new regressions — opaque `/v1/models/invoke`,
      opaque `/v1beta/models/route`, and the positive counter-case where a
      structured `/v1/models/...:generateContent` path still normalizes.
    - claude.rs: three new `test_extract_auth_gemini_cli_*` tests covering
      refresh-only JSON, empty-string access_token JSON, and the valid-JSON
      pass-through.
    
    All 839 lib tests pass; cargo fmt + clippy -D warnings clean.
    
    Addresses Codex review P2 findings on #1918.
    
    * fix(proxy/gemini): treat empty-string functionCall id as missing in streaming path
    
    Follow-up to the earlier P1 fix: some Gemini relays serialize an absent
    functionCall id as `"id": ""` instead of omitting the field. The
    non-streaming `extract_tool_call_meta` already filters these via
    `.filter(|s| !s.is_empty())`, but the streaming counterpart
    `extract_tool_calls` passed the empty string straight through
    `function_call.get("id").and_then(|v| v.as_str())` into
    `GeminiToolCallMeta::new`, producing a `Some("")` id.
    
    Downstream, `merge_tool_call_snapshots` would then match two parallel
    no-id calls against each other on their shared empty-string id,
    collapsing them into a single snapshot (silent data loss for the first
    call) and emitting an Anthropic `tool_use.id: ""` that breaks tool_result
    correlation on the Claude Code client.
    
    Fix:
    - `extract_tool_calls`: apply the same `filter(|s| !s.is_empty())` guard
      used in the non-streaming path so empty strings become `None` before
      reaching the shadow meta.
    - `merge_tool_call_snapshots`: defensively collapse any incoming
      `Some("")` to `None` up front — keeps the "missing vs present" invariant
      local to the merge step for future callers that might build
      `GeminiToolCallMeta` by hand.
    
    Tests (2 new, both in streaming_gemini):
    - `parallel_empty_string_id_calls_are_treated_as_missing_and_preserved`
      covers two parallel calls with explicit `"id": ""` — asserts both
      surface, no empty tool_use id leaks, and each gets a unique
      `gemini_synth_` id.
    - `single_empty_string_id_tool_call_gets_synthesized_id` covers the
      non-parallel degraded-relay case.
    
    All 841 lib tests pass; cargo fmt + clippy -D warnings clean.
    
    Addresses Codex follow-up P1 on #1918.
    
    * fix(proxy/gemini): gate generic REST path suffixes behind Google host whitelist
    
    `should_normalize_gemini_full_url` previously treated any full URL whose
    path ends with `/v1`, `/v1/models`, `/models`, `/v1/openai`, or `/openai`
    as a structured Gemini endpoint and rewrote it to
    `/v1beta/models/{model}:generateContent`. These are ubiquitous REST
    conventions — opaque relays such as `https://relay.example/custom/v1`
    legitimately use them for fixed endpoints — so the rewrite silently
    routed traffic to the wrong upstream path.
    
    Split the predicate into two layers:
    
    - **Unconditional**: `matches_structured_gemini_models_path` (i.e. a
      `/models/...:generateContent` method call anywhere in the path), the
      Google-specific `/v1beta*` family, and the deep OpenAI-compat paths
      (`/v1beta/openai/chat/completions`, `/openai/chat/completions`, and
      their `responses` siblings). These remain host-agnostic because the
      path grammar itself is Gemini-specific.
    - **Google-host gated**: `/v1`, `/v1/models`, `/models`, `/v1/openai`,
      `/openai`. Only normalized when the host is one of
      `generativelanguage.googleapis.com`, `aiplatform.googleapis.com`, or a
      real `*-aiplatform.googleapis.com` Vertex regional endpoint. The match
      is exact/suffix (not `contains`), so lookalike hosts like
      `aiplatform.example.com` are correctly treated as opaque relays.
    
    Tests (8 new in `gemini_url::tests`):
    - Four opaque-relay cases: `/custom/v1`, `/custom/models`,
      `/custom/v1/models`, `/custom/openai` — all preserved as-is.
    - Three Google-host counter-cases: `/v1`, `/models`, and
      `us-central1-aiplatform.googleapis.com/v1` still normalize.
    - One lookalike safety case: `aiplatform.example.com/v1` is NOT
      treated as Google.
    
    All 849 lib tests pass; cargo fmt + clippy -D warnings clean.
    
    Addresses Codex review P2 on #1918.
    
    * fix(proxy/gemini): align shadow id with client-visible id in non-streaming path
    
    When Gemini returns a `functionCall` without an id (common in 2.x
    parallel calls), `gemini_to_anthropic_with_shadow_and_hints` previously
    generated TWO independent synthesized UUIDs:
    
      1. Line 186-197 — synthesized id `A` used for the Anthropic-visible
         `content[tool_use].id` returned to the client.
      2. Line 850-881 — `extract_tool_call_meta` independently synthesized
         id `B ≠ A`, which populated `shadow_turn.tool_calls[i].id`.
    
    `shadow_content` (line 225-228, cloned from `rectified_parts`) retained
    the original missing/empty id. Result: the client sees id `A`, the
    shadow store holds id `B`.
    
    On the next turn, `convert_messages_to_contents` builds
    `tool_name_by_id` from `build_tool_name_map_from_shadow_turns`, which
    uses `tool_calls[i].id` — so the map contains `B → name` but not
    `A → name`. When the client sends back `tool_result(tool_use_id=A)`,
    resolution fails with:
    
      Unable to resolve Gemini functionResponse.name for tool_use_id `A`
    
    This affects both truncated histories (client sends only the
    tool_result) and full histories (shadow-replay branch at line 342-354
    skips `convert_message_content_to_parts`, so the assistant tool_use
    block never registers id `A` itself).
    
    Fix: make `rectified_parts` the single source of truth. After
    `rectify_tool_call_parts`, run a pre-pass that writes
    `synthesize_tool_call_id()` back into any `functionCall` that lacks a
    non-empty id. All three readers — the content builder (186-197), the
    shadow_content clone (225-228), and `extract_tool_call_meta` — then
    observe the same id. `shadow_parts()` already strips synthesized ids on
    replay (line 616-628), so the internal identifier never leaks to
    Gemini upstream.
    
    This mirrors the streaming path, which already has single-source-of-
    truth semantics via `tool_call_snapshots` in `streaming_gemini.rs` —
    no change needed there.
    
    Tests (5 new in `transform_gemini::tests`):
    - `non_stream_shadow_id_matches_client_visible_id`: asserts
      `response.content[0].id == shadow.tool_calls[0].id ==
      shadow.assistant_content.parts[0].functionCall.id`.
    - `non_stream_missing_id_scenario_a_truncated_history_resolves`: turn 2
      sends only `[tool_result(id=A)]`; resolution must succeed.
    - `non_stream_missing_id_scenario_b_full_history_replay_resolves`: turn 2
      sends `[assistant(tool_use=A), tool_result(A)]`; shadow-replay branch
      strips the synth id from outgoing `functionCall` while still
      resolving the subsequent `tool_result`.
    - `non_stream_preserves_original_gemini_id_when_present`: regression —
      genuine Gemini ids flow through unchanged.
    - `non_stream_synthesized_id_not_leaked_to_gemini_via_shadow_replay`:
      defensive — shadow-replay path must strip synth ids from both
      `functionCall.id` and `functionResponse.id`.
    
    All 854 lib tests pass; cargo fmt + clippy -D warnings clean.
    
    Addresses Codex follow-up P1 on #1918.
    
    * refactor(proxy/gemini): share build_anthropic_usage between stream and non-stream paths
    
    `streaming_gemini::anthropic_usage_from_gemini` and
    `transform_gemini::build_anthropic_usage` were byte-for-byte identical
    (32 lines each) — both converting Gemini `usageMetadata` into the
    Anthropic `usage` shape including `cache_read_input_tokens` mapping.
    
    Promote the non-streaming version to `pub(crate)` and reuse it from the
    streaming SSE converter. Removes ~30 lines of duplication and guarantees
    the two paths cannot drift apart.
    
    No behavioral change; all 854 lib tests pass; cargo fmt + clippy -D
    warnings clean.
    
    * fix(proxy/gemini): gate /v1beta behind Google host + normalize models/ model id prefix
    
    Two related P2 corrections to the Gemini Native URL surface, both
    folding into the existing Google-host-whitelist architecture.
    
    ## P2a — `/v1beta` suffix should not unconditionally trigger rewrite
    
    `should_normalize_gemini_full_url` placed `/v1beta` and `/v1beta/models`
    in the unconditional layer on the reasoning that `/v1beta` is
    Google-specific. In practice an opaque relay fronting a non-Gemini
    service at `https://relay.example/custom/v1beta` would still be
    silently rewritten to `/v1beta/models/{model}:generateContent`,
    breaking the deployment.
    
    Move `/v1beta`, `/v1beta/models`, and `/v1beta/openai` into the
    Google-host gated layer alongside `/v1`, `/models`, and friends. The
    unconditional layer now only accepts paths whose grammar is
    intrinsically Gemini — `/models/...:generateContent` method calls and
    the deep OpenAI-compat endpoints like `/openai/chat/completions` and
    `/openai/responses`. Pasted AI-Studio URLs such as
    `https://generativelanguage.googleapis.com/v1beta` still normalize
    because the host matches the whitelist.
    
    ## P2b — `model: "models/gemini-2.5-pro"` produced doubled path prefix
    
    Gemini SDKs (and the official `list_models` response) commonly surface
    model ids in resource-name form `models/gemini-2.5-pro`. Raw
    interpolation into `format!("/v1beta/models/{model}:...")` produced
    `/v1beta/models/models/gemini-2.5-pro:streamGenerateContent` which
    upstream rejects — yielding false-negative health checks for otherwise
    valid provider configs.
    
    Introduce `normalize_gemini_model_id(&str) -> &str` in `gemini_url`
    as the single source of truth: strips an optional leading `/` then an
    optional `models/` prefix, leaving bare ids untouched. Apply in the
    three call sites that build a Gemini method URL:
    - `services/stream_check.rs::resolve_claude_stream_url` (unified path)
    - `services/stream_check.rs::check_gemini_stream` (Gemini-only path)
    - `proxy/forwarder.rs::rewrite_claude_transform_endpoint` (production)
    
    Tests (9 new):
    - `gemini_url`: 3 regressions for opaque vs Google-host `/v1beta*`
      handling + 5 unit tests pinning `normalize_gemini_model_id` behavior
      (strip prefix, leave bare id, preserve nested slashes past the one
      stripped prefix, tolerate leading slash, pass through empty input).
    - `stream_check`: one end-to-end regression confirming
      `models/gemini-2.5-pro` collapses to the expected single-prefix URL.
    - `forwarder`: one end-to-end regression on the production rewrite
      path.
    
    All 864 lib tests pass; cargo fmt + clippy -D warnings clean.
    
    Addresses Codex P2 feedback on #1918.
    
    * fix(proxy/gemini): trim API key before provider-type detection and OAuth parsing
    
    Leading whitespace on a copied oauth_creds.json (e.g. trailing newline
    when the user copies the file content as-is) would slip past the
    `starts_with("ya29.") || starts_with('{')` prefix check in
    `ClaudeAdapter::provider_type`, causing the provider to be misclassified
    as raw-API-key Gemini and fall back to `x-goog-api-key` with the raw
    JSON as the key — which upstream rejects with 401.
    
    The frontend's `handleApiKeyChange` already trims on keystrokes but
    deep-link imports, the JSON editor, and live-config backfill all bypass
    that path. Trim at every backend extraction point so the coverage is
    uniform:
    
    - `ClaudeAdapter::extract_key` (5 env / fallback branches) gets
      `.map(str::trim)` before `.filter(|s| !s.is_empty())` so that
      whitespace-only values are also treated as missing.
    - `GeminiAdapter::extract_key_raw` gets the same chain (including
      the `.filter` it was missing before).
    - `GeminiAdapter::parse_oauth_credentials` gets a defensive
      `let key = key.trim();` at the entry as a belt-and-suspenders guard.
    
    Adds two regression tests covering JSON and bare `ya29.` keys with
    leading newline/space.
    
    * fix(proxy/gemini): gate generic REST suffix stripping behind Google host in non-full-URL mode
    
    `build_gemini_native_url` unconditionally stripped `/v1`, `/v1beta`,
    `/models`, and `/openai` suffixes from the base path regardless of
    host. This worked for Google's own endpoints but silently rewrote
    third-party relay URLs like `https://relay.example/custom/v1` to
    `.../custom/v1beta/models/...`, breaking any relay that mounts its
    Gemini-compatible namespace under a versioned prefix.
    
    The result was also asymmetric with the previously-fixed full-URL
    branch: toggling the "full URL" switch changed the outbound URL for
    the same base_url, which is exactly the kind of invisible behavior
    that makes debugging proxy deployments painful.
    
    Align `normalize_gemini_base_path` with
    `should_normalize_gemini_full_url`'s layered model:
    
    - Unconditional: `/models/...:method` structured paths and deep
      OpenAI-compat endpoints (`/openai/chat/completions`,
      `/openai/responses` and their versioned variants) — these are
      unambiguous Gemini-specific grammar on any host.
    - Google-host gated: generic `/v1`, `/v1beta`, `/models`, `/openai`
      suffixes only get stripped on `generativelanguage.googleapis.com`,
      `aiplatform.googleapis.com`, or `*-aiplatform.googleapis.com`.
      Other hosts preserve the prefix verbatim so relays keep their
      intended routing.
    
    Adds seven regression tests for the non-full-URL flow: opaque relay
    preservation (v1 / v1beta / models / openai suffix variants), Google
    host normalization (counter-case), and boundary cases (structured
    method path and deep OpenAI-compat endpoint stripped regardless of
    host).
    
    Test count: 864 -> 873.
    
    * Revert "fix(proxy/gemini): gate generic REST suffix stripping behind Google host in non-full-URL mode"
    
    This reverts commit d19ff09cb7.
    
    * test(proxy/gemini): pin non-full-URL versioned relay base stripping
    
    Adds two regression tests that lock in the intentional asymmetry
    between full-URL and non-full-URL modes:
    
    - Full-URL mode: opaque base path (e.g. `https://relay.example/custom/v1beta`)
      is preserved verbatim. Already covered by
      `preserves_opaque_full_url_with_bare_v1beta_suffix`.
    - Non-full-URL mode: base path MUST strip `/v1`, `/v1beta`, etc. so the
      standard `/v1beta/models/{model}:method` endpoint can be appended
      without producing a doubled `/v1beta/v1beta/models/...` path.
    
    The non-full-URL contract is "base URL + cc-switch appends the
    canonical Gemini endpoint". A user who needs a relay's custom
    namespace (e.g. `/v1/models/...`) must use full-URL mode and paste
    the complete method path. This commit adds regression coverage so a
    future attempt to mirror full-URL's host-whitelist gating into
    `normalize_gemini_base_path` will fail the test suite immediately.
    
    * chore(lint): address clippy 1.95 findings in existing modules
    
    CI upgraded to Rust 1.95 and flagged ten pre-existing warnings that
    older toolchains did not enforce. None relate to the Gemini proxy
    integration PR itself but they block CI on the feature branch, so
    clean them up here as a separate commit for easy review:
    
    collapsible_match:
    - proxy/providers/gemini_schema.rs: `"items" if value.is_object()`
      match guard instead of nested if.
    - proxy/providers/transform_responses.rs: fold
      `map_responses_stop_reason`'s `"completed"` / `"incomplete"` arms
      into match guards, relying on the existing `_ => "end_turn"` fall-
      through for non-matching guard conditions (semantics preserved).
    - services/session_usage_codex.rs: fold
      `"session_meta" if state.session_id.is_none()` guard, relying on
      the existing `_ => {}` fall-through.
    
    unnecessary_sort_by:
    - services/provider/endpoints.rs: `sort_by_key(|ep| Reverse(ep.added_at))`.
    - services/skill.rs (backup list): same Reverse idiom on `created_at`.
    - services/skill.rs (skill listings x2): `sort_by_key(|s| s.name.to_lowercase())`.
    
    useless_conversion:
    - services/skill.rs: drop the explicit `.into_iter()` on `zip`'s argument.
    
    while_let_loop:
    - services/webdav_auto_sync.rs: `while let Some(wait_for) = ...`
      instead of `loop { let Some(...) = ... else { break }; ... }`.
    
    All changes are mechanical and preserve behavior. `cargo test --lib`
    remains green (868 passed).
    
    * fix(proxy/gemini): reconcile synthesized tool-call ids with later real ids + preserve thoughtSignature
    
    Three related findings on `streaming_gemini.rs` for Gemini's cumulative
    `streamGenerateContent` stream, all centered on `merge_tool_call_snapshots`:
    
    1. (P1) Match upgraded tool-call IDs by position.
       When Gemini delivers a `functionCall` without an id on chunk 1
       (cc-switch synthesizes `gemini_synth_*`) and then upgrades it to a
       real id on chunk 2, the `Some(incoming_id)` branch only matched by
       id and missed the existing synthesized snapshot. A second entry
       would be pushed, yielding duplicate `tool_use` content blocks at
       stream end — one with the synthesized id, one with the real id —
       which could trigger duplicate tool execution and break tool_result
       correlation. Add a positional fallback: when no id match exists but
       the same-position slot holds a synthesized id, merge into it.
       `or(preserved_id)` already lets the real id win the merge.
    
    2. (P2) Preserve prior thoughtSignature when merging snapshots.
       `tool_call_snapshots[index] = tool_call` overwrote the slot
       entirely, dropping any `thoughtSignature` captured on an earlier
       chunk if the current cumulative snapshot omitted it. Since
       `build_shadow_assistant_parts` writes `thoughtSignature` into the
       shadow turn from `tool_call.thought_signature`, a dropped signature
       would cause later replay requests to Gemini to be rejected with
       invalid-signature errors. Preserve the existing signature when the
       incoming chunk does not carry one.
    
    3. (P2) Document the part-order streaming trade-off.
       All `tool_use` content blocks are emitted after the final text
       `content_block_stop`, so interleaved [text, functionCall, text,
       functionCall] parts arrive at the Anthropic client as [text(concat),
       tool_use, tool_use] — different from the non-streaming transformer,
       which preserves part order. This is intentional given the cumulative
       snapshot model and the consumers we target (claude-code-like clients
       don't depend on strict interleaving for tool execution correctness).
       Add a block comment at the flush site describing the trade-off and
       what a strict-order fix would entail, so this isn't rediscovered as
       a bug later.
    
    Regression tests:
    - upgraded_real_id_merges_into_existing_synthesized_snapshot
    - thought_signature_preserved_when_later_chunk_omits_it
    
    Test count: 868 -> 870. clippy 1.95 clean. fmt clean.
    
    * fix(proxy/gemini): prefer exact tool-call id over normalized-name fallback
    
    The shadow-turn matcher used a three-branch `||` chain (id / full name /
    normalized name). When two tools share a suffix (e.g. `server_a:search`
    and `server_b:search`), the normalized-name clause could short-circuit
    on an earlier turn whose id is actually wrong for the incoming tool_use,
    mis-routing replay state (functionCall id / thoughtSignature) for later
    tool_result resolution.
    
    Split matching into two layers: when the incoming message carries any
    tool_use ids, run id-based lookup first and return on the earliest hit.
    Only fall back to full-name / normalized-name matching when the incoming
    ids are absent or none of them resolve.
    
    Add two regressions:
    
    - shadow_replay_prefers_exact_id_match_over_normalized_name_collision
      Two shadow turns with colliding normalized names and two assistant
      messages whose ids cross the positional order; asserts each message
      replays the id-correct shadow turn (including thoughtSignature).
    
    - shadow_replay_falls_back_to_name_when_ids_absent
      Shadow turn with no id and incoming tool_use with an empty id;
      asserts the name fallback still populates the replayed part.
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • fix(proxy): reduce unnecessary Copilot premium interaction consumption
    - Fix request classification: treat messages containing tool_result as
      agent continuation instead of user-initiated, preventing false premium
      charges on every tool call
    - Add subagent detection via __SUBAGENT_MARKER__ and metadata._agent_
      fallback, setting x-interaction-type=conversation-subagent
    - Add deterministic x-interaction-id derived from session ID to group
      requests into a single billing interaction
    - Add orphan tool_result sanitization to prevent upstream API errors
      that could cause retries and duplicate billing
    - Reorder pipeline: classify (on original body) → sanitize → merge →
      warmup, ensuring classification sees raw tool_result semantics
    - Enable warmup downgrade by default with gpt-5-mini model
    - Enhance session ID extraction priority chain for Copilot cache keys
    - Detect infinite whitespace bug in streaming tool call arguments
  • Feat/usage improvements (#508)
    * i18n: update cache terminology across all languages
    
    - Change 'Cache Read' to 'Cache Hit' in all languages
    - Change 'Cache Write' to 'Cache Creation' in all languages
    - Update zh: 缓存读取 → 缓存命中, 缓存写入 → 缓存创建
    - Update en: Cache Read → Cache Hit, Cache Write → Cache Creation
    - Update ja: キャッシュ読取 → キャッシュヒット, キャッシュ書込 → キャッシュ作成
    
    Affected keys: cacheReadTokens, cacheCreationTokens, cacheReadCost,
    cacheWriteCost, cacheRead, cacheWrite
    
    * feat(usage): add cache metrics to trend chart
    
    - Add cache creation tokens visualization (orange line)
    - Add cache hit tokens visualization (purple line)
    - Add gradient definitions for new cache metrics
    - Include cache data in hourly aggregation
    - Display cache metrics alongside input/output tokens
    
    This provides better visibility into cache usage patterns over time.
    
    * fix(usage): fix timezone handling in datetime picker
    
    - Add timestampToLocalDatetime() to convert Unix timestamp to local datetime
    - Add localDatetimeToTimestamp() with validation for incomplete input
    - Fix issue where typing hours/minutes would jump to previous day
    - Validate datetime format completeness before conversion
    - Use local timezone instead of UTC for datetime-local input
    
    This resolves the issue where users couldn't fine-tune time selection
    and the input would jump unexpectedly when editing hours or minutes.
    
    * feat(usage): add auto-refresh for usage statistics
    
    - Add 30-second auto-refresh interval for all usage queries
    - Disable background refresh to save resources
    - Apply to: summary, trends, provider stats, model stats, request logs
    - Queries automatically update when tab is active
    - Pause refresh when user switches to another tab
    
    This keeps usage data fresh without manual refresh.
    
    * fix(proxy): improve usage logging and cache token parsing
    
    - Log requests even when usage parsing fails (with default values)
    - Add detailed debug logging for usage metrics
    - Support cache_read_input_tokens field in Codex responses
    - Fallback to input_tokens_details.cached_tokens if needed
    - Add test case for cached_tokens in input_tokens_details
    - Ensure all requests are tracked in database for analytics
    
    This fixes missing request logs when API responses lack usage data
    and improves cache token detection across different response formats.
    
    * style(rust): use inline format args in format! macros
    
    - Replace format!("...", var) with format!("...{var}")
    - Update universal provider ID formatting
    - Update error message formatting
    - Update config.toml generation in Codex provider
    
    Fixes clippy::uninlined_format_args warnings.
    
    * feat(proxy): enhance provider router logging
    
    - Add debug logs for failover queue provider count
    - Log circuit breaker state for each provider check
    - Add logs for missing current provider scenarios
    - Log when no current provider is configured
    - Use inline format args for better readability
    
    This improves debugging of provider selection and failover behavior.
    
    * feat(database): update model pricing data
    
    - Update Claude models to full version format (e.g. claude-opus-4-5-20251101)
    - Add GPT-5.2 series model pricing (10 models)
    - Add GPT-5.1 series model pricing (10 models)
    - Add GPT-5 series model pricing (12 models)
    - Add Gemini 3 series model pricing (2 models)
    - Update Gemini 2.5 series model ID format (use dot separator)
    - Unify display names by removing thinking level suffixes
    
    * fix(usage): correct Gemini output token calculation
    
    Fix Gemini API output token parsing to use totalTokenCount - promptTokenCount
    instead of candidatesTokenCount alone. This ensures thoughtsTokenCount is
    included in output statistics.
    
    - Update from_gemini_response to calculate output from total - input
    - Update from_gemini_stream_chunks with same logic for consistency
    - Fix from_codex_stream_events to use adjusted token calculation
    - Add test case for responses with thoughtsTokenCount
    - Update existing tests to match new calculation logic
    
    * fix(usage): correct cache token billing and add Codex format auto-detection
    
    - Avoid double-billing cache tokens by subtracting from input before calculation
    - Add smart Codex parser that auto-detects OpenAI vs Codex API format
    - Extract model name from Codex responses for accurate tracking
    
    * fix(proxy): improve takeover detection with live config check
    
    - Add live config takeover detection for hot-switch decision
    - Rebuild takeover when backup is missing or placeholder remains
    - Make detect_takeover_in_live_config_for_app public
    - Fix is_takeover_active to use actual takeover status
    
    * refactor(usage): simplify model pricing lookup by removing suffix fallback
    
    Replace complex suffix-stripping fallback with direct prefix/suffix cleanup.
    Model IDs are now cleaned by removing vendor prefix (before /) and colon
    suffix (after :), then matched exactly against pricing table.
    
    * feat(database): add Chinese AI model pricing data
    
    Add pricing for domestic AI models (CNY/1M tokens):
    - Doubao-Seed-Code (ByteDance)
    - DeepSeek V3/V3.1/V3.2
    - Kimi K2/K2-Thinking/K2-Turbo (Moonshot)
    - MiniMax M2/M2.1/M2.1-Lightning
    - GLM-4.6/4.7 (Zhipu)
    - Mimo V2 Flash (Xiaomi)
    
    Also fix test case to use correct model ID and remove invalid currency column.
    
    * refactor(proxy): improve header forwarding with blacklist approach
    
    Change from whitelist to blacklist mode for request header forwarding.
    Only skip headers that will be overridden (auth, host, content-length).
    This preserves client's original headers and improves compatibility.
    
    * fix(proxy): bypass timeout and retry configs when failover is disabled
    
    When auto_failover_enabled is false, timeout and retry configurations
    should not affect normal request flow. This change ensures:
    
    - create_forwarder: passes 0 for all timeout/retry params when failover
      is disabled, effectively bypassing these checks
    - streaming_timeout_config: returns 0 for both first_byte_timeout and
      idle_timeout when failover is disabled
    
    This prevents unnecessary timeout errors and retry attempts when users
    have explicitly disabled the failover feature.
    
    * fix(proxy): handle zero value input in failover config fields
    
    * refactor(proxy): remove retry logic and add enabled check for failover
    
    * refactor(proxy): distinguish circuit-open from no-provider errors
    
    * Align usage stats to sliding windows
    
    * feat(proxy): add body and header filtering for upstream requests
    
    * feat(proxy): enable transparent passthrough for headers
    
    - Passthrough anthropic-beta header as-is from client
    - Passthrough anthropic-version header from client
    - Passthrough client IP headers (x-forwarded-for, x-real-ip) by default
    - Filter private params (underscore-prefixed fields) from request body
    - No database changes required
    
    * feat(proxy): extract session ID from client requests for logging
    
    - Add SessionIdExtractor to parse session ID from Claude/Codex requests
    - Support extraction from metadata.user_id, headers, previous_response_id
    - Pass session_id through RequestContext to usage logger
    - Enable request correlation by session in proxy_request_logs
  • Feat/proxy server (#355)
    * feat(proxy): implement local HTTP proxy server with multi-provider failover
    
    Add a complete HTTP proxy server implementation built on Axum framework,
    enabling local API request forwarding with automatic provider failover
    and load balancing capabilities.
    
    Backend Implementation (Rust):
    - Add proxy server module with 7 core components:
      * server.rs: Axum HTTP server lifecycle management (start/stop/status)
      * router.rs: API routing configuration for Claude/OpenAI/Gemini endpoints
      * handlers.rs: Request/response handling and transformation
      * forwarder.rs: Upstream forwarding logic with retry mechanism (652 lines)
      * error.rs: Comprehensive error handling and HTTP status mapping
      * types.rs: Shared types (ProxyConfig, ProxyStatus, ProxyServerInfo)
      * health.rs: Provider health check infrastructure
    
    Service Layer:
    - Add ProxyService (services/proxy.rs, 157 lines):
      * Manage proxy server lifecycle
      * Handle configuration updates
      * Track runtime status and metrics
    
    Database Layer:
    - Add proxy configuration DAO (dao/proxy.rs, 242 lines):
      * Persist proxy settings (listen address, port, timeout)
      * Store provider priority and availability flags
    - Update schema with proxy_config table (schema.rs):
      * Support runtime configuration persistence
    
    Tauri Commands:
    - Add 6 command endpoints (commands/proxy.rs):
      * start_proxy_server: Launch proxy server
      * stop_proxy_server: Gracefully shutdown server
      * get_proxy_status: Query runtime status
      * get_proxy_config: Retrieve current configuration
      * update_proxy_config: Modify settings without restart
      * is_proxy_running: Check server state
    
    Frontend Implementation (React + TypeScript):
    - Add ProxyPanel component (222 lines):
      * Real-time server status display
      * Start/stop controls
      * Provider availability monitoring
    - Add ProxySettingsDialog component (420 lines):
      * Configuration editor (address, port, timeout)
      * Provider priority management
      * Settings validation
    - Add React hooks:
      * useProxyConfig: Manage proxy configuration state
      * useProxyStatus: Poll and display server status
    - Add TypeScript types (types/proxy.ts):
      * Define ProxyConfig, ProxyStatus interfaces
    
    Provider Integration:
    - Extend Provider model with availability field (providers.rs):
      * Track provider health for failover logic
    - Update ProviderCard UI to display proxy status
    - Integrate proxy controls in Settings page
    
    Dependencies:
    - Add Axum 0.7 (async web framework)
    - Add Tower 0.4 (middleware and service abstractions)
    - Add Tower-HTTP (CORS layer)
    - Add Tokio sync primitives (oneshot, RwLock)
    
    Technical Details:
    - Graceful shutdown via oneshot channel
    - Shared state with Arc<RwLock<T>> for thread-safe config updates
    - CORS enabled for cross-origin frontend access
    - Request/response streaming support
    - Automatic retry with exponential backoff (forwarder)
    - API key extraction from multiple config formats (Claude/Codex/Gemini)
    
    File Statistics:
    - 41 files changed
    - 3491 insertions(+), 41 deletions(-)
    - Core modules: 1393 lines (server + forwarder + handlers)
    - Frontend UI: 642 lines (ProxyPanel + ProxySettingsDialog)
    - Database/DAO: 326 lines
    
    This implementation provides the foundation for advanced features like:
    - Multi-provider load balancing
    - Automatic failover on provider errors
    - Request logging and analytics
    - Usage tracking and cost monitoring
    
    * fix(proxy): resolve UI/UX issues and database constraint error
    
    Simplify proxy control interface and fix database persistence issues:
    
    Backend Fixes:
    - Fix NOT NULL constraint error in proxy_config.created_at field
      * Use COALESCE to preserve created_at on updates
      * Ensure proper INSERT OR REPLACE behavior
    - Remove redundant enabled field validation on startup
      * Auto-enable when user clicks start button
      * Persist enabled state after successful start
    - Preserve enabled state during config updates
      * Prevent accidental service shutdown on config save
    
    Frontend Improvements:
    - Remove duplicate proxy enable switch from settings dialog
      * Keep only runtime toggle in ProxyPanel
      * Simplify user experience with single control point
    - Hide proxy target button when proxy service is stopped
      * Add isProxyRunning prop to ProviderCard
      * Conditionally render proxy controls based on service status
    - Update form schema to omit enabled field
      * Managed automatically by backend
    
    Files: 5 changed, 81 insertions(+), 94 deletions(-)
    
    * fix(proxy): improve URL building and Gemini request handling
    
    - Refactor URL construction with version path deduplication (/v1, /v1beta)
    - Preserve query parameters for Gemini API requests
    - Support GOOGLE_GEMINI_API_KEY field name (with fallback)
    - Change default proxy port from 5000 to 15721
    - Fix test: use Option type for is_proxy_target field
    
    * refactor(proxy): remove unused request handlers and routes
    
    - Remove unused GET/DELETE request forwarding methods
    - Remove count_tokens, get/delete response handlers
    - Simplify router by removing unused endpoints
    - Keep only essential routes: /v1/messages, /v1/responses, /v1beta/*
    
    * Merge branch 'main' into feat/proxy-server
    
    * fix(proxy): resolve clippy warnings for dead code and uninlined format args
    
    - Add #[allow(dead_code)] to unused ProviderUnhealthy variant
    - Inline format string arguments in handlers.rs and codex.rs log macros
    - Refactor error response handling to properly pass through upstream errors
    - Add URL deduplication logic for /v1/v1 paths in CodexAdapter
    
    * feat(proxy): implement provider adapter pattern with OpenRouter support
    
    This major refactoring introduces a modular provider adapter architecture
    to support format transformation between different AI API formats.
    
    New features:
    - Add ProviderAdapter trait for unified provider abstraction
    - Implement Claude, Codex, and Gemini adapters with specific logic
    - Add Anthropic ↔ OpenAI format transformation for OpenRouter compatibility
    - Support model mapping from provider configuration (ANTHROPIC_MODEL, etc.)
    - Add OpenRouter preset to Claude provider presets
    
    Refactoring:
    - Extract authentication logic into auth.rs with AuthInfo and AuthStrategy
    - Move URL building and request transformation to individual adapters
    - Simplify ProviderRouter to only use proxy target providers
    - Refactor RequestForwarder to use adapter-based request/response handling
    - Use whitelist mode for header forwarding (only pass necessary headers)
    
    Architecture:
    - providers/adapter.rs: ProviderAdapter trait definition
    - providers/auth.rs: AuthInfo, AuthStrategy types
    - providers/claude.rs: Claude adapter with OpenRouter detection
    - providers/codex.rs: Codex (OpenAI) adapter
    - providers/gemini.rs: Gemini (Google) adapter
    - providers/models/: Anthropic and OpenAI API data models
    - providers/transform.rs: Bidirectional format transformation
    
    * feat(proxy): add streaming SSE transform and thinking parameter support
    
    New features:
    - Add OpenAI → Anthropic SSE streaming response transformation
    - Support thinking parameter detection for reasoning model selection
    - Add ANTHROPIC_REASONING_MODEL config option for extended thinking
    
    Changes:
    - streaming.rs: Implement SSE event parsing and Anthropic format conversion
    - transform.rs: Add thinking detection logic and reasoning model mapping
    - handlers.rs: Integrate streaming transform for OpenRouter compatibility
    - Cargo.toml: Add async-stream and bytes dependencies
    
    * feat(db): add usage tracking schema and types
    
    Add database tables for proxy request logs and model pricing.
    Extend Provider and error types to support usage statistics.
    
    * feat(proxy): implement usage tracking subsystem
    
    Add request logger with automatic cost calculation.
    Implement token parser for Claude/OpenAI/Gemini responses.
    Add cost calculator based on model pricing configuration.
    
    * feat(proxy): integrate usage logging into request handlers
    
    Add usage logging to forwarder and streaming handlers.
    Track token usage and costs for each proxy request.
    
    * feat(commands): add usage statistics Tauri commands
    
    Register usage commands for summary, trends, logs, and pricing.
    Expose usage stats service through Tauri command layer.
    
    * feat(api): add frontend usage API and query hooks
    
    Add TypeScript types for usage statistics.
    Implement usage API with Tauri invoke calls.
    Add TanStack Query hooks for usage data fetching.
    
    * feat(ui): add usage dashboard components
    
    Add UsageDashboard with summary cards, trend chart, and data tables.
    Implement model pricing configuration panel.
    Add request log viewer with filtering and detail panel.
    
    * fix(ui): integrate usage dashboard and fix type errors
    
    Add usage dashboard tab to settings page.
    Fix UsageScriptModal TypeScript type annotations.
    
    * deps: add recharts for charts and rust_decimal/uuid for usage tracking
    
    - recharts: Chart visualization for usage trends
    - rust_decimal: Precise cost calculations
    - uuid: Request ID generation
    
    * feat(proxy): add ProviderType enum for fine-grained provider detection
    
    Introduce ProviderType enum to distinguish between different provider
    implementations (Claude, ClaudeAuth, Codex, Gemini, GeminiCli, OpenRouter).
    This enables proper authentication handling and request transformation
    based on the actual provider type rather than just AppType.
    
    - Add ProviderType enum with detection logic from config
    - Enhance Claude adapter with OpenRouter detection
    - Enhance Gemini adapter with CLI mode detection
    - Add helper methods for provider type inference
    
    * feat(database): extend schema with streaming and timing fields
    
    Add new columns to proxy_request_logs table for enhanced usage tracking:
    - first_token_ms and duration_ms for performance metrics
    - provider_type and is_streaming for request classification
    - cost_multiplier for flexible pricing
    
    Update model pricing with accurate rates for Claude/GPT/Gemini models.
    Add ensure_model_pricing_seeded() call on database initialization.
    Add test for model pricing auto-seeding verification.
    
    * feat(proxy/usage): enhance token parser and logger for multi-format support
    
    Parser enhancements:
    - Add OpenAI Chat Completions format parsing (prompt_tokens/completion_tokens)
    - Add model field to TokenUsage for actual model name extraction
    - Add from_codex_response_adjusted() for proper cache token handling
    - Add debug logging for better stream event tracing
    
    Logger enhancements:
    - Add first_token_ms, provider_type, is_streaming, cost_multiplier fields
    - Extend RequestLog struct with full metadata tracking
    - Update log_with_calculation() signature for new fields
    
    Calculator: Update tests with model field in TokenUsage.
    
    * feat(proxy): enhance proxy server with session tracking and OpenAI route
    
    Error handling:
    - Add StreamIdleTimeout and AuthError variants for better error classification
    
    Module exports:
    - Export ResponseType, StreamHandler, NonStreamHandler from response_handler
    - Export ProxySession, ClientFormat from session module
    
    Server routing:
    - Add /v1/chat/completions route for OpenAI Chat Completions API
    
    Handlers:
    - Add log_usage_with_session() for enhanced usage tracking with session context
    - Add first_token_ms timing measurement for streaming responses
    - Use SseUsageCollector with start_time for accurate latency calculation
    - Track is_streaming flag in usage logs
    
    * feat(services): add pagination and enhanced filtering for request logs
    
    Usage stats service:
    - Change get_request_logs() from limit/offset to page/page_size pagination
    - Return PaginatedLogs with total count, page, and page_size
    - Add appType and providerName filters with LIKE search
    - Add is_streaming, first_token_ms, duration_ms to RequestLogDetail
    - Join with providers table for provider name lookup
    
    Commands:
    - Update get_request_logs command signature for pagination params
    
    Module exports:
    - Export PaginatedLogs struct
    
    * feat(frontend): update usage types and API for pagination support
    
    Types (usage.ts):
    - Add isStreaming, firstTokenMs, durationMs to RequestLog
    - Add PaginatedLogs interface with data, total, page, pageSize
    - Change LogFilters: providerId -> appType + providerName
    
    API (usage.ts):
    - Change getRequestLogs params from limit/offset to page/pageSize
    - Return PaginatedLogs instead of RequestLog[]
    - Pass filters object directly to backend
    
    Query (usage.ts):
    - Update usageKeys.logs key generation for pagination
    - Update useRequestLogs hook signature
    
    * refactor(ui): enhance RequestLogTable with filtering and pagination
    
    UI improvements:
    - Add filter bar with app type, provider name, model, status selectors
    - Add date range picker (startDate/endDate)
    - Add search/reset/refresh buttons
    
    Pagination:
    - Implement proper page-based pagination with page info display
    - Show total count and current page range
    - Add prev/next navigation buttons
    
    Features:
    - Default to last 24 hours filter
    - Streamlined table columns layout
    - Query invalidation on refresh
    
    * style(config): format mcpPresets code style
    
    Apply consistent formatting to createNpxCommand function and
    sequential-thinking server configuration.
    
    * fix(ui): update SettingsPage tab styles for improved appearance (#342)
    
    * feat(model-test): add provider model availability testing
    
    Implement standalone model testing feature to verify provider API connectivity:
    - Add ModelTestService for Claude/Codex/Gemini endpoint testing
    - Create model_test_logs table for test result persistence
    - Add test button to ProviderCard with loading state
    - Include ModelTestConfigPanel for customizing test parameters
    
    * fix(proxy): resolve token parsing for OpenRouter streaming responses
    
    Problem:
    - OpenRouter and similar third-party services return streaming responses
      where input_tokens appear in message_delta instead of message_start
    - The previous implementation only extracted input_tokens from message_start,
      causing input_tokens to be recorded as 0 for these providers
    
    Changes:
    - streaming.rs: Add prompt_tokens field to Usage struct and include
      input_tokens in the transformed message_delta event when converting
      OpenAI format to Anthropic format
    - parser.rs: Update from_claude_stream_events() to handle input_tokens
      from both message_start (native Claude API) and message_delta (OpenRouter)
      - Use if-let pattern instead of direct unwrap for safer parsing
      - Only update input_tokens from message_delta if not already set
    - logger.rs: Adjust test parameters to match updated function signature
    
    Tests:
    - Add test_openrouter_stream_parsing() for OpenRouter format validation
    - Add test_native_claude_stream_parsing() for native Claude API validation
    
    * fix(pricing): standardize model ID format for pricing lookup
    
    Normalize model IDs by removing vendor prefixes and converting dots to hyphens to ensure consistent pricing lookups across different API response formats.
    
    Changes:
    - Update seed data to use hyphen format (e.g., gpt-5-1, gemini-2-5-pro)
    - Add normalize_model_id() function to strip vendor prefixes (anthropic/, openai/)
    - Convert dots to hyphens in model IDs (claude-haiku-4.5 → claude-haiku-4-5)
    - Try both original and normalized IDs for exact matching
    - Use normalized ID for suffix-based fallback matching
    - Add comprehensive test cases for prefix and dot handling
    - Add warning log when no pricing found
    
    This ensures pricing lookups work correctly for:
    - Models with vendor prefixes: anthropic/claude-haiku-4.5
    - Models with dots in version: claude-sonnet-4.5
    - Models with date suffixes: claude-haiku-4-5-20240229
    
    * style(rust): apply clippy formatting suggestions
    
    Apply automatic clippy fixes for uninlined_format_args warnings across Rust codebase. Replace format string placeholders with inline variable syntax for improved readability.
    
    Changes:
    - Convert format!("{}", var) to format!("{var}")
    - Apply to model_test.rs, parser.rs, and usage_stats.rs
    - Fix line length issues by breaking long function calls
    - Improve code formatting consistency
    
    All changes are automatic formatting with no functional impact.
    
    * fix(ui): restore card borders in usage statistics panels
    
    Restore proper card styling for ModelTestConfigPanel and PricingConfigPanel by adding back border and rounded-lg classes. The transparent background styling was causing visual inconsistency.
    
    Changes:
    - Replace border-none bg-transparent shadow-none with border rounded-lg
    - Apply to both loading and error states for consistency
    - Format TypeScript code for better readability
    - Break long function signatures across multiple lines
    
    This ensures the usage statistics panels have consistent visual appearance with proper borders and rounded corners.
    
    * feat(pricing): add GPT-5 Codex model pricing presets
    
    Add pricing configuration for GPT-5 Codex variants to support cost tracking for Codex-specific models.
    
    Changes:
    - Add gpt-5-codex model with standard GPT-5 pricing
    - Add gpt-5-1-codex model with standard GPT-5.1 pricing
    - Input: $1.25/M tokens, Output: $10/M tokens
    - Cache read: $0.125/M tokens, Cache creation: $0
    
    This ensures accurate cost calculation for Codex API requests using GPT-5 Codex models.