# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0

"""
Async HTTP client proxying chat completions to external LLM providers.

Most providers use OpenAI-compatible /v1/chat/completions; Anthropic uses
the native Messages API, translated in this client.
"""

import base64
import json as _json
import mimetypes
import re
import time
from typing import Any, AsyncGenerator, Literal, NamedTuple, Optional, Union
from urllib.parse import urlparse

import httpx
import structlog

# structlog so INFO diagnostics reach the backend's JSON log stream (the
# stdlib root logger defaults to WARNING with no handlers). It accepts the
# existing printf-style positional args.
logger = structlog.get_logger(__name__)


# Claude 4.7 (Opus/Sonnet/Haiku) removed temperature/top_p/top_k — the API
# 400s "<param> is deprecated for this model" on a non-default value. 3.x and
# 4.5/4.6 still accept them, so match the 4-7 line strictly. Ref:
#   https://platform.claude.com/docs/en/about-claude/models/whats-new-claude-4-7
def _is_openai_family_cloud(base_url: Optional[str]) -> bool:
    """True iff ``base_url`` points at OpenAI cloud or Azure OpenAI Foundry.

    Anchored to the URL host so a path/subdomain like
    ``https://api.openai.com.attacker.com/v1`` can't bypass it (CodeQL
    py/incomplete-url-substring-sanitization). Scopes cloud-only Responses-API
    extensions that 400 on non-cloud OAI-compat servers (ollama/llama.cpp/vLLM).
    Azure Foundry resources live at ``<resource>.openai.azure.com``; the leading
    dot on `endswith` stops `openai.azure.com` apex from matching.
    """
    if not base_url:
        return False
    try:
        host = (urlparse(base_url).hostname or "").lower()
    except Exception:
        return False
    if not host:
        return False
    return host == "api.openai.com" or host.endswith(".openai.azure.com")


_ANTHROPIC_4_7_SAMPLING_REMOVED = re.compile(r"^claude-(?:opus|sonnet|haiku)-4-7(?:[-.]|$)")
_OPENAI_REASONING_SUMMARY_UNSUPPORTED = re.compile(r"^o3(?:[-.]|$)")
_OPENAI_REASONING_STATUSES = {"in_progress", "completed", "incomplete"}


def _openai_image_replay_requires_reasoning(model: str) -> bool:
    normalized = model.strip().lower()
    return normalized.startswith("gpt-5") or normalized.startswith("o")


def _sanitize_openai_reasoning_replay_item(item: Any) -> Optional[dict[str, Any]]:
    """Return a Responses input-safe reasoning item, if ``item`` is one.

    OpenAI image-generation docs allow follow-up edits via the previous
    ``image_generation_call`` id. Reasoning models can also require the
    paired ``reasoning`` output item in manually managed context, so keep
    only the public replay fields and drop everything else.
    """
    if not isinstance(item, dict) or item.get("type") != "reasoning":
        return None
    item_id = item.get("id")
    if not isinstance(item_id, str) or not item_id:
        return None
    summary_parts: list[dict[str, str]] = []
    summary = item.get("summary")
    if isinstance(summary, list):
        for part in summary:
            if not isinstance(part, dict):
                continue
            if part.get("type") != "summary_text":
                continue
            text = part.get("text")
            if isinstance(text, str):
                summary_parts.append({"type": "summary_text", "text": text})
    replay_item: dict[str, Any] = {
        "type": "reasoning",
        "id": item_id,
        "summary": summary_parts,
    }
    status = item.get("status")
    if isinstance(status, str) and status in _OPENAI_REASONING_STATUSES:
        replay_item["status"] = status
    return replay_item


# OpenAI Responses inline citation markers: `citeSOURCE_ID[id2...][LOCATOR]`
# using private-use codepoints (see
# https://developers.openai.com/api/docs/guides/citation-formatting).
# Group 1 holds delim-separated tokens; each resolvable token expands to
# `[[N]](URL)`, unresolved tokens (locators, unknown ids) drop silently so
# no garbled glyph reaches the renderer.
_OPENAI_CITE_OPEN = "cite"
_OPENAI_CITE_STOP = ""
_OPENAI_CITE_DELIM = ""
_OPENAI_CITATION_MARKER = re.compile(
    f"{_OPENAI_CITE_OPEN}([^{_OPENAI_CITE_STOP}]+){_OPENAI_CITE_STOP}"
)


def _build_citation_lookup(url_citations: list[dict[str, Any]]) -> dict[str, tuple[int, str]]:
    """Map every known ``source_id`` alias to ``(citation_index, url)``.

    Accepts singular ``source_id`` and plural ``source_ids``. First-seen
    wins on collision so an earlier citation keeps its number.
    """
    by_source: dict[str, tuple[int, str]] = {}
    for idx, cit in enumerate(url_citations, start = 1):
        url = cit.get("url")
        if not isinstance(url, str) or not url:
            continue
        aliases: list[str] = []
        sid = cit.get("source_id")
        if isinstance(sid, str) and sid:
            aliases.append(sid)
        sids = cit.get("source_ids")
        if isinstance(sids, list):
            aliases.extend(s for s in sids if isinstance(s, str) and s)
        for alias in aliases:
            by_source.setdefault(alias, (idx, url))
    return by_source


def _replace_openai_citation_markers(text: str, url_citations: list[dict[str, Any]]) -> str:
    """Rewrite `\\ue200cite\\ue202SOURCE_ID[\\ue202LOCATOR]\\ue201` markers into
    `[[N]](URL)` per resolvable id. Multi-source markers expand to one link
    per id; unresolved tokens drop. Idempotent on text without private-use
    codepoints.
    """
    if not text or _OPENAI_CITE_STOP not in text:
        return text
    by_source = _build_citation_lookup(url_citations)

    def _sub(match: re.Match[str]) -> str:
        # Try every delim-split token; unresolved tokens drop. Handles
        # multi-source (all resolve) and source+locator (only id resolves,
        # locator drops). Empty result strips the marker.
        rendered: list[str] = []
        for tok in match.group(1).split(_OPENAI_CITE_DELIM):
            if not tok:
                continue
            hit = by_source.get(tok)
            if hit is None:
                continue
            idx, url = hit
            rendered.append(f"[[{idx}]]({url})")
        return "".join(rendered)

    return _OPENAI_CITATION_MARKER.sub(_sub, text)


def _rewrite_citation_markers_partial(
    text: str, url_citations: list[dict[str, Any]]
) -> tuple[str, bool]:
    """Like ``_replace_openai_citation_markers`` but also reports whether
    any marker referenced a source_id not yet in ``url_citations``.

    A url_citation's ``annotation.added`` event typically arrives AFTER the
    delta carrying the marker that references it. Callers buffer the segment
    until a later event records the annotation; unresolved markers are left
    verbatim so a follow-up pass still parses cleanly.
    """
    if not text or _OPENAI_CITE_STOP not in text:
        return text, False
    by_source = _build_citation_lookup(url_citations)
    has_unresolved = False

    def _sub(match: re.Match[str]) -> str:
        nonlocal has_unresolved
        tokens = [t for t in match.group(1).split(_OPENAI_CITE_DELIM) if t]
        rendered: list[str] = []
        any_unresolved = False
        for tok in tokens:
            hit = by_source.get(tok)
            if hit is None:
                any_unresolved = True
                continue
            idx, url = hit
            rendered.append(f"[[{idx}]]({url})")
        # Leave the whole marker verbatim if any token is unresolved so the
        # caller can re-run once the late annotation lands; partial emission
        # would lose unresolved ids once the source text is dropped.
        if any_unresolved:
            has_unresolved = True
            return match.group(0)
        return "".join(rendered)

    return _OPENAI_CITATION_MARKER.sub(_sub, text), has_unresolved


def _split_pending_citation_tail(text: str) -> tuple[str, str]:
    """Split ``text`` into ``(head, pending_tail)`` for streamed deltas.

    A citation marker can straddle two SSE deltas (e.g. delta-1 ends with
    ``\\ue200citetu`` and delta-2 starts with ``rn0view0\\ue201``); the
    unterminated tail is buffered and prepended onto the next delta so the
    rewriter sees a complete marker. ``pending_tail`` is the longest suffix
    starting with ``\\ue200`` and lacking ``\\ue201``; ``head`` is safe to
    emit. Empty tail when ``text`` has no open or a fully closed marker.
    """
    if not text:
        return text, ""
    last_open = text.rfind("")
    if last_open == -1:
        return text, ""
    # A stop byte after the last open byte means the marker closed here.
    if _OPENAI_CITE_STOP in text[last_open:]:
        return text, ""
    return text[:last_open], text[last_open:]


class _AnthropicThinkingSpec(NamedTuple):
    prefixes: tuple[str, ...]
    kind: Literal["adaptive", "manual"]
    efforts: tuple[str, ...]


_ANTHROPIC_THINKING_SPECS = (
    _AnthropicThinkingSpec(
        prefixes = ("claude-opus-4-7",),
        kind = "adaptive",
        efforts = ("none", "low", "medium", "high", "xhigh", "max"),
    ),
    _AnthropicThinkingSpec(
        prefixes = ("claude-opus-4-6", "claude-sonnet-4-6"),
        kind = "adaptive",
        efforts = ("none", "low", "medium", "high", "xhigh", "max"),
    ),
    _AnthropicThinkingSpec(
        prefixes = ("claude-opus-4-5", "claude-sonnet-4-5", "claude-haiku-4-5"),
        kind = "manual",
        efforts = ("none", "low", "medium", "high"),
    ),
)


def _anthropic_thinking_spec(model: str) -> Optional[_AnthropicThinkingSpec]:
    for spec in _ANTHROPIC_THINKING_SPECS:
        if model.startswith(spec.prefixes):
            return spec
    return None


# Anthropic ships date-pinned tool versions per model family: the newer
# `_20260209`/`_20260120` variants only run on recent models (400 "tool not
# supported" elsewhere), and old versions on a new model miss dynamic
# filtering and free-with-search pricing. Pick the newest combo the model
# accepts, else the GA `_20250305`/`_20250910`/`_20250825` defaults. Ref:
# https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-reference
_ANTHROPIC_NEW_WEB_PREFIXES = (
    "claude-opus-4-7",
    "claude-opus-4-6",
    "claude-sonnet-4-6",
)
_ANTHROPIC_NEW_CODE_EXEC_PREFIXES = (
    "claude-opus-4-7",
    "claude-opus-4-6",
    "claude-sonnet-4-6",
    "claude-opus-4-5",
    "claude-sonnet-4-5",
)


def _anthropic_web_search_version(model: str) -> str:
    return (
        "web_search_20260209"
        if model.startswith(_ANTHROPIC_NEW_WEB_PREFIXES)
        else "web_search_20250305"
    )


def _anthropic_web_fetch_version(model: str) -> str:
    return (
        "web_fetch_20260209"
        if model.startswith(_ANTHROPIC_NEW_WEB_PREFIXES)
        else "web_fetch_20250910"
    )


def _anthropic_code_execution_version(model: str) -> str:
    return (
        "code_execution_20260120"
        if model.startswith(_ANTHROPIC_NEW_CODE_EXEC_PREFIXES)
        else "code_execution_20250825"
    )


# Anthropic's beta-header flag for code execution does NOT change with the
# tool version -- both `_20250825` and `_20260120` are unlocked by the same
# `code-execution-2025-08-25` header per the upstream docs.
_ANTHROPIC_CODE_EXECUTION_BETA = "code-execution-2025-08-25"


# Anthropic server-side context compaction (beta compact-2026-01-12), supported
# on Opus 4.6/4.7, Sonnet 4.6 and Mythos Preview. Same beta header for all; the
# dated `compact_20260112` type lives in body `context_management.edits`. Models
# outside the prefix list are silently ignored so we don't 400 upstream.
_ANTHROPIC_COMPACTION_PREFIXES = (
    "claude-opus-4-7",
    "claude-opus-4-6",
    "claude-sonnet-4-6",
    "claude-mythos-preview",
)
_ANTHROPIC_COMPACTION_BETA = "compact-2026-01-12"
_ANTHROPIC_COMPACTION_TYPE = "compact_20260112"
# The threshold must be >= 50K tokens; lower 400s. Clamp on the way out so
# a UI slider can't underflow.
_ANTHROPIC_COMPACTION_MIN = 50_000


# Anthropic fast-mode beta (Opus 4.6 / 4.7 only, per
# https://platform.claude.com/docs/en/build-with-claude/fast-mode).
# Mutually exclusive with the Priority service tier.
_ANTHROPIC_FAST_MODE_BETA = "fast-mode-2026-02-01"
_ANTHROPIC_FAST_MODE_PREFIXES = (
    "claude-opus-4-7",
    "claude-opus-4-6",
)


def _anthropic_supports_compaction(model: str) -> bool:
    return model.startswith(_ANTHROPIC_COMPACTION_PREFIXES)


def _anthropic_supports_fast_mode(model: str) -> bool:
    # Require a family boundary ("" or "-") after the prefix so IDs like
    # "claude-opus-4-70" / "claude-opus-4-7b" don't match.
    return any(model == p or model.startswith(f"{p}-") for p in _ANTHROPIC_FAST_MODE_PREFIXES)


# Cap on ``cited_text`` forwarded in document_citations tool_events; bounds
# SSE bytes on multi-KB cited spans (frontend trims to 240 chars anyway).
_CITED_TEXT_MAX_LEN = 512


def _anthropic_citation_key(citation: dict[str, Any]) -> tuple:
    """Stable dedup key for an Anthropic ``citations_delta.citation``.

    Anchor fields vary per type (char_location, page_location,
    content_block_location, search_result_location); both start AND
    exclusive end indices are in the key so same-start / different-end pairs
    stay distinct. search_result_location keys on ``search_result_index`` +
    ``source`` instead of document_index so distinct results with the same
    source don't collapse. Unknown shapes fall back to a stringified copy
    (more entries, never collisions). See
    https://platform.claude.com/docs/en/build-with-claude/citations
    and https://platform.claude.com/docs/en/build-with-claude/search-results.
    """
    ctype = citation.get("type")
    doc = citation.get("document_index")
    title = citation.get("document_title") or ""
    if ctype == "char_location":
        return (
            ctype,
            doc,
            title,
            citation.get("start_char_index"),
            citation.get("end_char_index"),
        )
    if ctype == "page_location":
        return (
            ctype,
            doc,
            title,
            citation.get("start_page_number"),
            citation.get("end_page_number"),
        )
    if ctype == "content_block_location":
        return (
            ctype,
            doc,
            title,
            citation.get("start_block_index"),
            citation.get("end_block_index"),
        )
    if ctype == "search_result_location":
        return (
            ctype,
            citation.get("search_result_index"),
            citation.get("source"),
            citation.get("title") or "",
            citation.get("start_block_index"),
            citation.get("end_block_index"),
        )
    return (ctype, _json.dumps(citation, sort_keys = True))


class _MistralThinkingSpec(NamedTuple):
    models: tuple[str, ...]
    style: Literal["prompt_mode", "reasoning_effort", "disabled"]
    efforts: tuple[str, ...] = ()


_MISTRAL_THINKING_SPECS = (
    _MistralThinkingSpec(
        models = ("magistral-medium-latest",),
        style = "prompt_mode",
    ),
    _MistralThinkingSpec(
        models = ("mistral-small-latest", "mistral-vibe-cli-latest"),
        style = "reasoning_effort",
        efforts = ("none", "high"),
    ),
)

_OPENROUTER_MANDATORY_REASONING_MODELS = frozenset(
    {
        "~google/gemini-pro-latest",
        "baidu/cobuddy:free",
        "inclusionai/ring-2.6-1t:free",
        "deepseek/deepseek-r1",
    }
)


def _mistral_thinking_spec(model: str) -> _MistralThinkingSpec:
    for spec in _MISTRAL_THINKING_SPECS:
        if model in spec.models:
            return spec
    return _MistralThinkingSpec(models = (), style = "disabled")


def _apply_mistral_reasoning_controls(
    body: dict[str, Any],
    model: str,
    enable_thinking: Optional[bool],
    reasoning_effort: Optional[str],
) -> None:
    """
    Translate generic reasoning controls into Mistral's model-specific shape.

    Contract:
      - magistral-medium-latest: baseline (no extra field) or
        `prompt_mode="reasoning"` for explicit reasoning mode.
      - mistral-small-latest / mistral-vibe-cli-latest:
        `reasoning_effort` in {"none", "high"}.
      - all other tested Mistral models: no reasoning/thinking params.
    """
    model_for_matching = model.rsplit("/", 1)[-1].strip().lower()
    spec = _mistral_thinking_spec(model_for_matching)
    body.pop("prompt_mode", None)
    body.pop("reasoning_effort", None)

    if spec.style == "prompt_mode":
        # Magistral baseline is already reasoning-capable; the explicit
        # prompt_mode path is only for the "high" UI selection.
        if enable_thinking is True or reasoning_effort == "high":
            body["prompt_mode"] = "reasoning"
        return

    if spec.style == "reasoning_effort":
        if reasoning_effort in spec.efforts:
            body["reasoning_effort"] = reasoning_effort
        elif enable_thinking is False:
            body["reasoning_effort"] = "none"
        elif enable_thinking is True:
            body["reasoning_effort"] = "high"


# Shared client reused across all requests for HTTP connection pooling.
# Auth headers and timeouts are passed per-request, so a single client
# handles every provider without storing credentials.
def _create_shared_http_client() -> httpx.AsyncClient:
    # Unsupported env proxy schemes (socks:// etc) raise at construction and
    # would crash Studio startup (#6090); retry ignoring env proxies instead.
    try:
        return httpx.AsyncClient()
    except (ImportError, ValueError) as exc:
        exc_str = str(exc)
        if "Unknown scheme for proxy URL" not in exc_str and "socksio" not in exc_str:
            raise
        logger.warning(
            "Ignoring unsupported environment proxy for the shared HTTP client: %s", exc_str
        )
        return httpx.AsyncClient(trust_env = False)


_http_client = _create_shared_http_client()


# Cap per-image fetch well below Gemini's ~20 MB total request budget.
_GEMINI_REMOTE_IMAGE_MAX_BYTES = 10 * 1024 * 1024
_GEMINI_REMOTE_IMAGE_TIMEOUT_S = 15.0


def _safe_fetch_image_for_gemini_sync(
    url: str,
    fallback_mime: str,
    max_bytes: int = _GEMINI_REMOTE_IMAGE_MAX_BYTES,
) -> Optional[tuple[str, str]]:
    """Synchronous IP-pinned HTTPS image fetch with SSRF guards.

    Uses the same pinned-IP + SNI pattern as `tools._fetch_page_text` so DNS
    rebinding between validation and the connection cannot redirect us to a
    private/metadata address. Follows up to 4 hops, re-validating each
    redirect target. Returns (mime, base64) or None.

    `max_bytes` is clamped to the per-image cap and also lets the caller pass
    the remaining per-request budget, so an over-budget URL is rejected via
    Content-Length (or read short-circuit) instead of being fully downloaded
    then discarded.
    """
    import urllib.error
    import urllib.request
    from urllib.parse import urljoin, urlunparse

    # Refuse upfront if the per-request budget is already spent.
    _byte_limit = min(max(0, int(max_bytes)), _GEMINI_REMOTE_IMAGE_MAX_BYTES)
    if _byte_limit <= 0:
        return None

    # Reuse tools.py's pinned-IP hardening: validate-once-then-pin.
    from .tools import (
        _NoRedirect,
        _SNIHTTPSHandler,
        _validate_and_resolve_host,
    )

    def _safe_parse_https(raw_url: str) -> Optional[tuple[Any, str, int]]:
        """Validate https + hostname + port. Returns (parsed, host, port) or
        None. Handles malformed-port and malformed-bracketed-IPv6 URLs that
        would else raise ValueError mid-build.
        """
        try:
            parsed_url = urlparse(raw_url)
            host_value = parsed_url.hostname
            port_value = parsed_url.port or 443
        except (ValueError, UnicodeError) as _err:
            logger.info(
                "Gemini image fetch: refusing malformed url err=%s",
                type(_err).__name__,
            )
            return None
        scheme_value = (parsed_url.scheme or "").lower()
        if scheme_value != "https":
            logger.info(
                "Gemini image fetch: refusing non-https scheme=%s",
                scheme_value,
            )
            return None
        if not host_value:
            logger.info("Gemini image fetch: refusing url with no hostname")
            return None
        return parsed_url, host_value, port_value

    parsed_info = _safe_parse_https(url)
    if parsed_info is None:
        return None
    parsed, current_host, current_port = parsed_info
    current_url = url
    ok, reason, pinned_ip = _validate_and_resolve_host(current_host, current_port)
    if not ok:
        logger.warning(
            "Gemini image fetch: refusing host=%s reason=%s",
            current_host,
            reason,
        )
        return None

    for _hop in range(4):
        # Pin to validated IP; SNI + cert still use the hostname via _SNIHTTPSHandler.
        cp_info = _safe_parse_https(current_url)
        if cp_info is None:
            return None
        cp, _cp_host, _cp_port = cp_info
        ip_str = f"[{pinned_ip}]" if ":" in pinned_ip else pinned_ip
        ip_netloc = f"{ip_str}:{cp.port}" if cp.port else ip_str
        pinned_url = urlunparse(cp._replace(netloc = ip_netloc))

        opener = urllib.request.build_opener(
            _NoRedirect,
            _SNIHTTPSHandler(current_host),
        )
        req = urllib.request.Request(
            pinned_url,
            headers = {"Host": current_host},
            method = "GET",
        )

        try:
            resp = opener.open(req, timeout = _GEMINI_REMOTE_IMAGE_TIMEOUT_S)
        except urllib.error.HTTPError as e:
            if e.code not in (301, 302, 303, 307, 308):
                logger.info(
                    "Gemini image fetch: status=%d host=%s",
                    e.code,
                    current_host,
                )
                return None
            location = e.headers.get("Location")
            if not location:
                return None
            try:
                current_url = urljoin(current_url, location)
            except (ValueError, UnicodeError) as _err:
                logger.info(
                    "Gemini image fetch: refusing malformed redirect err=%s",
                    type(_err).__name__,
                )
                return None
            rp_info = _safe_parse_https(current_url)
            if rp_info is None:
                return None
            _rp, current_host, current_port = rp_info
            ok2, reason2, pinned_ip = _validate_and_resolve_host(current_host, current_port)
            if not ok2:
                logger.warning(
                    "Gemini image fetch: refusing redirect host=%s reason=%s",
                    current_host,
                    reason2,
                )
                return None
            continue
        except (urllib.error.URLError, OSError) as _err:
            logger.warning(
                "Gemini image fetch failed host=%s err=%s",
                current_host,
                type(_err).__name__,
            )
            return None

        with resp:
            status = getattr(resp, "status", None) or resp.getcode()
            if status != 200:
                logger.info("Gemini image fetch: status=%s host=%s", status, current_host)
                return None
            _hdr_mime = (resp.headers.get("content-type") or "").split(";")[0].strip().lower()
            # Declared non-image MIME is refused; missing MIME uses the caller's.
            if _hdr_mime and not _hdr_mime.startswith("image/"):
                logger.info(
                    "Gemini image fetch: non-image content-type=%s host=%s",
                    _hdr_mime,
                    current_host,
                )
                return None
            _final_mime_pre = _hdr_mime if _hdr_mime else fallback_mime
            if not isinstance(_final_mime_pre, str) or not _final_mime_pre.startswith("image/"):
                logger.info(
                    "Gemini image fetch: missing content-type and no image fallback host=%s",
                    current_host,
                )
                return None
            _hdr_len = resp.headers.get("content-length")
            if _hdr_len and _hdr_len.isdigit() and int(_hdr_len) > _byte_limit:
                logger.info(
                    "Gemini image fetch: declared %s bytes exceeds cap=%s host=%s",
                    _hdr_len,
                    _byte_limit,
                    current_host,
                )
                return None
            # Read cap+1 to detect oversize without buffering unbounded data.
            raw = resp.read(_byte_limit + 1)
            if len(raw) > _byte_limit:
                logger.info(
                    "Gemini image fetch: streamed bytes exceed cap=%s host=%s",
                    _byte_limit,
                    current_host,
                )
                return None
            return _final_mime_pre, base64.b64encode(raw).decode("ascii")

    logger.info("Gemini image fetch: too many redirects host=%s", current_host)
    return None


async def _safe_fetch_image_for_gemini(
    url: str,
    fallback_mime: str,
    max_bytes: int = _GEMINI_REMOTE_IMAGE_MAX_BYTES,
) -> Optional[tuple[str, str]]:
    """Async wrapper running the IP-pinned fetch on a worker thread.

    SSRF guards (https only, pinned IP, per-hop redirect re-check, size cap,
    image/* content-type) live in the sync helper. `max_bytes` carries the
    remaining per-request budget so over-budget URLs are rejected up front.
    """
    import asyncio
    return await asyncio.to_thread(_safe_fetch_image_for_gemini_sync, url, fallback_mime, max_bytes)


# Synthetic-tool names stamped onto outbound _toolEvent.arguments so the
# frontend can tell provider-side cards from real user-declared tools of the
# same name. Mirrored on the TS side.
_SERVER_SIDE_BUILTIN_TOOL_NAMES = frozenset(
    {"web_search", "web_fetch", "code_execution", "image_generation"}
)


def _stamp_server_tool_marker(payload: dict[str, Any]) -> None:
    """Tag synthetic provider-side tool events so the frontend can tell them
    from real user-declared / local function tools of the same name. The
    marker rides on `arguments._server_tool` and is only added for known
    server-side builtin names; user-supplied tool calls echoed back through
    these helpers (e.g. Kimi `$web_search`) keep their shape because this
    stays scoped to the canonical builtin names.
    """
    if not isinstance(payload, dict):
        return
    if payload.get("type") != "tool_start":
        return
    name = payload.get("tool_name")
    if not isinstance(name, str) or name not in _SERVER_SIDE_BUILTIN_TOOL_NAMES:
        return
    args = payload.get("arguments")
    if not isinstance(args, dict):
        args = {}
        payload["arguments"] = args
    args["_server_tool"] = True


def _build_kimi_tool_end(
    synthetic_chunk_fn: Any, tool_call_id: str, citations: list[dict[str, str]]
) -> str:
    """Format Kimi web_search citations into the tool_end payload.

    Same shape the frontend's parseSourcesFromResult expects for the other
    built-in web_search providers: `Title: ...\\nURL: ...\\n
    Snippet: ...\\n---\\n...`. If no citations were emitted, fall back to a
    generic "(search complete)" string so the UI still transitions the tool
    card to completed.
    """
    blocks: list[str] = []
    for cit in citations:
        line = f"Title: {cit['title']}\nURL: {cit['url']}"
        if cit.get("snippet"):
            line += f"\nSnippet: {cit['snippet']}"
        blocks.append(line)
    return synthetic_chunk_fn(
        {
            "type": "tool_end",
            "tool_call_id": tool_call_id,
            "result": "\n---\n".join(blocks) if blocks else "(search complete)",
        }
    )


class ExternalProviderClient:
    """Async proxy for OpenAI-compatible external LLM APIs."""

    def __init__(
        self,
        provider_type: str,
        base_url: str,
        api_key: str,
        timeout: float = 120.0,
    ):
        self.provider_type = provider_type
        self.base_url = base_url.rstrip("/")
        # Strip a legacy `/openai` suffix from Google-hosted bases so configs
        # saved before the native switch still route correctly. Custom proxy
        # paths ending in `/openai` are left untouched.
        if self.provider_type == "gemini":
            _parsed_base = urlparse(self.base_url)
            if (
                _parsed_base.hostname or ""
            ).lower() == "generativelanguage.googleapis.com" and _parsed_base.path.rstrip(
                "/"
            ) == "/v1beta/openai":
                self.base_url = self.base_url[: -len("/openai")]
        self.api_key = api_key
        self._timeout = httpx.Timeout(timeout, connect = 10.0)
        # Generous per-byte read timeout: reasoning models pause tens of seconds
        # between bytes, but a dead upstream must eventually error, not hang forever.
        self._stream_timeout = httpx.Timeout(timeout, connect = 10.0, read = 300.0)

    def _auth_headers(self) -> dict[str, str]:
        """Build authentication headers using the provider's registry config."""
        from core.inference.providers import get_provider_info

        provider_info = get_provider_info(self.provider_type) or {}
        auth_header = provider_info.get("auth_header", "Authorization")
        auth_prefix = provider_info.get("auth_prefix", "Bearer ")

        # Non-Google Gemini bases (LiteLLM, custom gateways) use OAI-compat
        # Bearer auth, not Google's x-goog-api-key. Override the default.
        if self.provider_type == "gemini":
            _host = (urlparse(self.base_url).hostname or "").lower()
            if _host != "generativelanguage.googleapis.com":
                auth_header = "Authorization"
                auth_prefix = "Bearer "

        headers = {"Content-Type": "application/json"}
        # Skip auth header when api_key is empty (optional for local providers);
        # httpx rejects an empty `Bearer ` value as "Illegal header value".
        if self.api_key:
            headers[auth_header] = f"{auth_prefix}{self.api_key}"
        # Merge provider-specific extra headers (anthropic-version, OpenRouter attribution).
        headers.update(provider_info.get("extra_headers", {}))
        return headers

    def _is_openai_compatible(self) -> bool:
        """Return False for providers needing request/response translation (e.g. Anthropic)."""
        from core.inference.providers import get_provider_info

        info = get_provider_info(self.provider_type) or {}
        # Google-hosted Gemini uses the native translator; non-Google bases
        # stay on OAI-compat so LiteLLM / custom proxies still work.
        if self.provider_type == "gemini":
            _host = (urlparse(self.base_url).hostname or "").lower()
            if _host != "generativelanguage.googleapis.com":
                return True
        return info.get("openai_compatible", True)

    async def stream_chat_completion(
        self,
        messages: list[dict[str, Any]],
        model: str,
        temperature: float = 0.7,
        top_p: float = 0.95,
        max_tokens: Optional[int] = None,
        presence_penalty: float = 0.0,
        top_k: Optional[int] = None,
        enable_thinking: Optional[bool] = None,
        reasoning_effort: Optional[str] = None,
        enabled_tools: Optional[list[str]] = None,
        enable_prompt_caching: Optional[Union[bool, str]] = None,
        openai_code_exec_container_id: Optional[str] = None,
        anthropic_code_exec_container_id: Optional[str] = None,
        prompt_cache_ttl: Optional[str] = None,
        compaction_threshold: Optional[int] = None,
        tools: Optional[list[dict[str, Any]]] = None,
        tool_choice: Optional[Any] = None,
        fast_mode: Optional[bool] = None,
        stream: bool = True,
    ) -> AsyncGenerator[str, None]:
        """
        Yield OpenAI-format SSE lines from the external provider.

        OpenAI-compatible providers forward lines verbatim. For Anthropic, the
        native Messages API SSE is translated to OpenAI format.

        ``top_k`` and ``presence_penalty`` are forwarded only when the caller
        supplies a value the provider accepts; the frontend's
        provider-capability map already filters these per provider, so they're
        opt-in here.

        ``fast_mode`` only applies to Anthropic Opus 4.6 / 4.7 (silently
        dropped elsewhere); adds the beta header and ``speed: "fast"``.
        """
        # tool_choice="none" hard-disables hosted/builtin tools across every
        # provider so enabled_tools can't accidentally bill or leak.
        tool_choice_disabled = (
            isinstance(tool_choice, str) and tool_choice.strip().lower() == "none"
        )

        if not self._is_openai_compatible():
            # Gemini speaks its own native REST shape (contents/parts);
            # `_stream_gemini` translates request/response into the OpenAI
            # Chat Completions chunk format the rest of Studio expects.
            # API ref: https://ai.google.dev/gemini-api/docs
            if self.provider_type == "gemini":
                async for line in self._stream_gemini(
                    messages,
                    model,
                    temperature,
                    top_p,
                    max_tokens,
                    top_k,
                    presence_penalty,
                    enabled_tools,
                    enable_prompt_caching,
                    enable_thinking,
                    reasoning_effort,
                    tools,
                    tool_choice,
                ):
                    yield line
                return
            async for line in self._stream_anthropic(
                messages,
                model,
                temperature,
                top_p,
                max_tokens,
                top_k,
                enable_thinking,
                reasoning_effort,
                enabled_tools,
                enable_prompt_caching,
                anthropic_code_exec_container_id,
                prompt_cache_ttl,
                compaction_threshold,
                tool_choice,
                fast_mode = fast_mode,
            ):
                yield line
            return

        # OpenAI moved flagship models (gpt-5.x) off /v1/chat/completions —
        # those endpoints return 404 "This is not a chat model" for the new
        # families. Route all OpenAI traffic through /v1/responses instead;
        # we translate the Responses SSE back into Chat Completions chunks so
        # the frontend stays endpoint-agnostic.
        if self.provider_type == "openai":
            async for line in self._stream_openai_responses(
                messages,
                model,
                temperature,
                top_p,
                max_tokens,
                enable_thinking,
                reasoning_effort,
                enabled_tools,
                enable_prompt_caching,
                openai_code_exec_container_id,
                compaction_threshold,
                tools,
                tool_choice,
            ):
                yield line
            return

        # Kimi $web_search needs a 2-call round-trip + thinking off; route to
        # a helper. Forced-function tool_choice suppresses it.
        # https://platform.kimi.ai/docs/guide/use-web-search
        _kimi_tool_choice_forced_function = (
            isinstance(tool_choice, dict)
            and tool_choice.get("type") == "function"
            and isinstance(tool_choice.get("function"), dict)
            and bool(tool_choice["function"].get("name"))
        )
        if (
            self.provider_type == "kimi"
            and not tool_choice_disabled
            and not _kimi_tool_choice_forced_function
            and enabled_tools
            and "web_search" in enabled_tools
        ):
            async for line in self._stream_kimi_web_search(
                messages,
                model,
                max_tokens,
            ):
                yield line
            return

        body: dict[str, Any] = {
            "model": model,
            "messages": messages,
            "stream": stream,
            "temperature": temperature,
            "top_p": top_p,
            "presence_penalty": presence_penalty,
        }
        if max_tokens is not None:
            # Newer OpenAI models (gpt-4o, gpt-5.x) reject max_tokens
            if self.provider_type == "openai":
                body["max_completion_tokens"] = max_tokens
            else:
                body["max_tokens"] = max_tokens

        # Drop fields the registry flags as unusable so reasoning-class models
        # with fixed defaults (Kimi k2.6 etc) don't 400 on pydantic defaults
        # the route layer still fills in.
        from core.inference.providers import get_provider_info

        provider_info = get_provider_info(self.provider_type) or {}
        for field in provider_info.get("body_omit", ()):
            body.pop(field, None)

        # Kimi thinking is a top-level body field. kimi-k2-thinking is always
        # on (ignore the toggle); kimi-k2.6 defaults on, can be disabled.
        # `keep: all` preserves every chunk for the UI panel.
        if self.provider_type == "kimi" and enable_thinking is not None:
            if model == "kimi-k2-thinking":
                # Always on; ignore client toggle to avoid an API-level reject.
                pass
            elif enable_thinking:
                body["thinking"] = {"type": "enabled", "keep": "all"}
            else:
                body["thinking"] = {"type": "disabled"}
        elif self.provider_type == "mistral":
            _apply_mistral_reasoning_controls(body, model, enable_thinking, reasoning_effort)
        elif self.provider_type == "vllm" and enable_thinking is not None:
            # vLLM gates thinking via chat_template_kwargs.enable_thinking.
            tpl_kw = body.get("chat_template_kwargs")
            if not isinstance(tpl_kw, dict):
                tpl_kw = {}
            tpl_kw["enable_thinking"] = bool(enable_thinking)
            body["chat_template_kwargs"] = tpl_kw

        # OpenRouter's unified `reasoning` field gates per-model thinking.
        # Some routes (`*_MANDATORY_REASONING_MODELS`) 400 on explicit off.
        # https://openrouter.ai/docs/guides/best-practices/reasoning-tokens
        if self.provider_type == "openrouter":
            normalized_or_model = model.strip().lower()
            if reasoning_effort in ("low", "medium", "high"):
                body["reasoning"] = {"effort": reasoning_effort}
            elif enable_thinking is True:
                body["reasoning"] = {"enabled": True}
            elif enable_thinking is False:
                if normalized_or_model in _OPENROUTER_MANDATORY_REASONING_MODELS:
                    body.pop("reasoning", None)
                else:
                    body["reasoning"] = {"enabled": False}

            # OpenRouter web plugin works on every model id including
            # meta-routers (unlike `:online`). Forced-function tool_choice
            # suppresses it, matching Gemini/Anthropic.
            # https://openrouter.ai/docs/guides/features/plugins/web-search
            _or_tool_choice_forced_function = (
                isinstance(tool_choice, dict)
                and tool_choice.get("type") == "function"
                and isinstance(tool_choice.get("function"), dict)
                and bool(tool_choice["function"].get("name"))
            )
            if (
                not tool_choice_disabled
                and not _or_tool_choice_forced_function
                and enabled_tools
                and "web_search" in enabled_tools
            ):
                plugins = list(body.get("plugins") or [])
                if not any(isinstance(p, dict) and p.get("id") == "web" for p in plugins):
                    plugins.append({"id": "web"})
                body["plugins"] = plugins
                logger.info(
                    "OpenRouter web_search: attached plugins=[{id: 'web'}] (model=%s)",
                    body.get("model"),
                )

        # Forward OpenAI-style function tools / tool_choice on every OAI-compat
        # route (incl. custom Gemini OpenAI proxies like LiteLLM). Without
        # this, callers wiring user-defined tools silently lose
        # function-calling on non-native providers.
        if tools:
            body["tools"] = tools
        if tool_choice is not None:
            body["tool_choice"] = tool_choice

        url = f"{self.base_url}/chat/completions"
        logger.info(
            "Proxying chat completion to %s (provider=%s, model=%s)",
            url,
            self.provider_type,
            model,
        )

        try:
            async with _http_client.stream(
                "POST",
                url,
                json = body,
                headers = self._auth_headers(),
                timeout = self._stream_timeout,
            ) as response:
                if response.status_code != 200:
                    error_body = await response.aread()
                    error_text = error_body.decode("utf-8", errors = "replace")
                    error_text = _friendly_provider_error_text(
                        self.provider_type,
                        response.status_code,
                        error_text,
                        model = model,
                    )
                    logger.error(
                        "External provider returned %d: %s",
                        response.status_code,
                        error_text[:500],
                    )
                    yield _error_sse_line(response.status_code, error_text, self.provider_type)
                    return

                # Manual __anext__ (not `async for`) so we can close the
                # response BEFORE lines_gen, avoiding the httpcore 1.0
                # GeneratorExit -> RuntimeError path on Python 3.13.
                lines_gen = response.aiter_lines().__aiter__()
                # Diagnostic counters for the OAI-compat path; surface
                # OpenRouter mid-stream errors otherwise invisible server-side.
                event_counts: dict[str, int] = {}
                chosen_model: Optional[str] = None
                # OpenRouter has no web_search_call events — citations arrive
                # as url_citation annotations. Synthesise a tool_start/tool_end
                # pair to match the OpenAI/Anthropic UX.
                web_search_active = (
                    self.provider_type == "openrouter"
                    and not tool_choice_disabled
                    and not _or_tool_choice_forced_function
                    and bool(enabled_tools)
                    and "web_search" in (enabled_tools or [])
                )
                web_search_tool_id = "openrouter_web_search"
                web_search_citations: list[dict[str, str]] = []
                web_search_tool_started = False
                web_search_tool_ended = False

                def _emit_synthetic_tool_event(payload: dict[str, Any]) -> str:
                    _stamp_server_tool_marker(payload)
                    chunk = {
                        "id": f"chatcmpl-{self.provider_type}-synthetic",
                        "object": "chat.completion.chunk",
                        "choices": [
                            {
                                "index": 0,
                                "delta": {},
                                "finish_reason": None,
                            }
                        ],
                        "_toolEvent": payload,
                    }
                    return f"data: {_json.dumps(chunk)}"

                def _record_or_url_citation(payload: Any) -> None:
                    if not isinstance(payload, dict):
                        return
                    if payload.get("type") != "url_citation":
                        return
                    # OpenRouter (and OpenAI Chat Completions web_search) nest
                    # the citation under url_citation; some variants ship the
                    # fields flat on the annotation itself. Accept both.
                    cit = payload.get("url_citation")
                    if not isinstance(cit, dict):
                        cit = payload
                    url = cit.get("url", "") if isinstance(cit, dict) else ""
                    if not url or not isinstance(url, str):
                        return
                    if any(c["url"] == url for c in web_search_citations):
                        return
                    title = cit.get("title") or url
                    snippet = cit.get("content") or cit.get("snippet") or ""
                    web_search_citations.append(
                        {
                            "url": url,
                            "title": title,
                            "snippet": snippet if isinstance(snippet, str) else "",
                        }
                    )

                def _build_web_search_tool_end() -> str:
                    blocks: list[str] = []
                    for cit in web_search_citations:
                        line = f"Title: {cit['title']}\nURL: {cit['url']}"
                        if cit.get("snippet"):
                            line += f"\nSnippet: {cit['snippet']}"
                        blocks.append(line)
                    return _emit_synthetic_tool_event(
                        {
                            "type": "tool_end",
                            "tool_call_id": web_search_tool_id,
                            "result": ("\n---\n".join(blocks) if blocks else "(search complete)"),
                        }
                    )

                if web_search_active:
                    yield _emit_synthetic_tool_event(
                        {
                            "type": "tool_start",
                            "tool_name": "web_search",
                            "tool_call_id": web_search_tool_id,
                            "arguments": {},
                        }
                    )
                    web_search_tool_started = True

                try:
                    while True:
                        try:
                            line = await lines_gen.__anext__()
                        except StopAsyncIteration:
                            break
                        if not line.strip():
                            continue
                        if line.startswith("data:"):
                            data_str = line[len("data:") :].strip()
                            if data_str == "[DONE]":
                                event_counts["done"] = event_counts.get("done", 0) + 1
                                # Emit synthetic tool_end with collected
                                # citations BEFORE forwarding [DONE], so the
                                # tool-card transitions to "complete" before
                                # the stream closes.
                                if (
                                    web_search_active
                                    and web_search_tool_started
                                    and not web_search_tool_ended
                                ):
                                    yield _build_web_search_tool_end()
                                    web_search_tool_ended = True
                            elif data_str:
                                try:
                                    parsed = _json.loads(data_str)
                                except Exception:
                                    parsed = None
                                if isinstance(parsed, dict):
                                    # Mid-stream provider error event. OpenRouter
                                    # in particular returns 200 then surfaces the
                                    # failure as an SSE error event.
                                    if "error" in parsed:
                                        event_counts["error"] = event_counts.get("error", 0) + 1
                                        logger.warning(
                                            "%s SSE error event: %s",
                                            self.provider_type,
                                            parsed.get("error"),
                                        )
                                    else:
                                        event_counts["delta"] = event_counts.get("delta", 0) + 1
                                    # OpenRouter (and most OAI-compat providers)
                                    # report the handling model in every chunk's
                                    # `model` field. Latch the first non-empty
                                    # value so the router-picked model surfaces
                                    # in logs and reaches the proxy caller.
                                    if chosen_model is None and isinstance(
                                        parsed.get("model"), str
                                    ):
                                        chosen_model = parsed["model"]
                                    # With web_search on, scan every chunk's
                                    # delta and message objects for url_citation
                                    # annotations. Different OpenRouter upstreams
                                    # place them in different spots.
                                    if web_search_active:
                                        choices = parsed.get("choices") or []
                                        if isinstance(choices, list):
                                            for choice in choices:
                                                if not isinstance(choice, dict):
                                                    continue
                                                for envelope in (
                                                    choice.get("delta"),
                                                    choice.get("message"),
                                                ):
                                                    if not isinstance(envelope, dict):
                                                        continue
                                                    for ann in envelope.get("annotations") or []:
                                                        _record_or_url_citation(ann)
                        yield line
                    # Stream ended without [DONE] (some upstreams just close
                    # the connection). Emit tool_end so the card doesn't stay
                    # in "running" forever.
                    if web_search_active and web_search_tool_started and not web_search_tool_ended:
                        yield _build_web_search_tool_end()
                        web_search_tool_ended = True
                except GeneratorExit:
                    await response.aclose()  # set PoolByteStream._closed=True FIRST
                    await lines_gen.aclose()  # now safe — aclose() is a no-op
                    raise
                finally:
                    logger.info(
                        "%s stream complete (model=%s, chosen=%s, "
                        "web_search_requested=%s, citations=%s, events=%s)",
                        self.provider_type,
                        model,
                        chosen_model,
                        web_search_active,
                        len(web_search_citations),
                        event_counts,
                    )
                    await response.aclose()
                    await lines_gen.aclose()

        except httpx.ConnectError as exc:
            logger.error("Connection error to %s: %s", self.provider_type, exc)
            yield _error_sse_line(
                502,
                f"Failed to connect to {self.provider_type}: {exc}",
                self.provider_type,
            )
        except httpx.ReadTimeout as exc:
            logger.error("Read timeout from %s: %s", self.provider_type, exc)
            yield _error_sse_line(
                504,
                f"Timeout waiting for {self.provider_type} response",
                self.provider_type,
            )
        except httpx.HTTPError as exc:
            logger.error("HTTP error from %s: %s", self.provider_type, exc)
            yield _error_sse_line(
                502,
                f"Error communicating with {self.provider_type}: {exc}",
                self.provider_type,
            )

    async def _stream_kimi_web_search(
        self, messages: list[dict[str, Any]], model: str, max_tokens: Optional[int]
    ) -> AsyncGenerator[str, None]:
        """
        Kimi $web_search round-trip.

        Wire flow (per https://platform.kimi.ai/docs/guide/use-web-search):
          1. POST messages with tools=[{type: "builtin_function",
             function: {name: "$web_search"}}] and thinking=disabled.
          2. Stream the first response — accumulate function.arguments across
             tool_call deltas until finish_reason="tool_calls". Do NOT forward
             those tool_call chunks to the client (internal protocol step, not
             user-visible output).
          3. Build a second request: original messages + the assistant message
             carrying the tool_calls + a role=tool message echoing the same
             arguments verbatim (per Kimi docs, the caller "just needs to
             submit tool_call.function.arguments to Kimi as they are" — the
             server actually runs the search).
          4. Stream the second response — the final answer the user sees, with
             search results already incorporated.

        We synthesize tool_start (with the parsed query) when step (2)
        completes, and tool_end (with any url_citation annotations the second
        stream emits) before [DONE], so the chat UI shows the same web-search
        tool card as other providers.
        """
        url = f"{self.base_url}/chat/completions"
        body: dict[str, Any] = {
            "model": model,
            "messages": messages,
            "stream": True,
            # $web_search forbids thinking; sending the toggle would make the
            # server reject the request with 400.
            "thinking": {"type": "disabled"},
            "tools": [{"type": "builtin_function", "function": {"name": "$web_search"}}],
        }
        if max_tokens is not None:
            body["max_tokens"] = max_tokens

        # Strip body fields the Kimi registry declares unusable
        # (temperature/top_p — see body_omit in providers.py).
        from core.inference.providers import get_provider_info

        provider_info = get_provider_info(self.provider_type) or {}
        for field in provider_info.get("body_omit", ()):
            body.pop(field, None)

        tool_call_id = "kimi_web_search"
        synthetic_id = f"chatcmpl-{self.provider_type}-synthetic"

        def _synthetic_chunk(payload: dict[str, Any]) -> str:
            _stamp_server_tool_marker(payload)
            chunk = {
                "id": synthetic_id,
                "object": "chat.completion.chunk",
                "choices": [{"index": 0, "delta": {}, "finish_reason": None}],
                "_toolEvent": payload,
            }
            return f"data: {_json.dumps(chunk)}"

        logger.info(
            "Kimi $web_search round-trip starting (model=%s, url=%s)",
            model,
            url,
        )

        # ---- First call: collect the model's $web_search tool_call ----
        tool_calls_acc: dict[int, dict[str, Any]] = {}
        try:
            async with _http_client.stream(
                "POST",
                url,
                json = body,
                headers = self._auth_headers(),
                timeout = self._stream_timeout,
            ) as response:
                if response.status_code != 200:
                    error_body = await response.aread()
                    error_text = error_body.decode("utf-8", errors = "replace")
                    logger.error(
                        "Kimi first-call returned %d: %s",
                        response.status_code,
                        error_text[:500],
                    )
                    yield _error_sse_line(response.status_code, error_text, self.provider_type)
                    return

                lines_gen = response.aiter_lines().__aiter__()
                try:
                    while True:
                        try:
                            line = await lines_gen.__anext__()
                        except StopAsyncIteration:
                            break
                        if not line.strip() or not line.startswith("data:"):
                            continue
                        data_str = line[len("data:") :].strip()
                        if data_str == "[DONE]":
                            break
                        try:
                            parsed = _json.loads(data_str)
                        except Exception:
                            continue
                        for choice in parsed.get("choices") or []:
                            if not isinstance(choice, dict):
                                continue
                            delta = choice.get("delta") or {}
                            for tc in delta.get("tool_calls") or []:
                                if not isinstance(tc, dict):
                                    continue
                                idx = tc.get("index", 0)
                                slot = tool_calls_acc.setdefault(
                                    idx,
                                    {
                                        "id": tc.get("id") or f"call_{idx}",
                                        "type": "function",
                                        "function": {"name": "", "arguments": ""},
                                    },
                                )
                                if tc.get("id"):
                                    slot["id"] = tc["id"]
                                fn = tc.get("function") or {}
                                if fn.get("name"):
                                    slot["function"]["name"] = fn["name"]
                                if fn.get("arguments"):
                                    slot["function"]["arguments"] += fn["arguments"]
                            if choice.get("finish_reason") == "tool_calls":
                                break
                except GeneratorExit:
                    await response.aclose()
                    await lines_gen.aclose()
                    raise
                finally:
                    await response.aclose()
                    await lines_gen.aclose()
        except httpx.HTTPError as exc:
            logger.error("Kimi first-call HTTP error: %s", exc)
            yield _error_sse_line(
                502,
                f"Error communicating with kimi: {exc}",
                self.provider_type,
            )
            return

        # If the model decided not to search, fall back to a plain streaming
        # call without the builtin tool. Mirrors the UX of every other
        # provider when web_search is on but the model didn't need it.
        search_calls = [
            tc for tc in tool_calls_acc.values() if tc["function"]["name"] == "$web_search"
        ]
        if not search_calls:
            logger.info(
                "Kimi $web_search: model did not invoke search; falling back to plain stream"
            )
            fallback_body = dict(body)
            fallback_body.pop("tools", None)
            try:
                async with _http_client.stream(
                    "POST",
                    url,
                    json = fallback_body,
                    headers = self._auth_headers(),
                    timeout = self._stream_timeout,
                ) as response:
                    if response.status_code != 200:
                        error_body = await response.aread()
                        error_text = error_body.decode("utf-8", errors = "replace")
                        logger.error(
                            "Kimi fallback returned %d: %s",
                            response.status_code,
                            error_text[:500],
                        )
                        yield _error_sse_line(response.status_code, error_text, self.provider_type)
                        return
                    # Manual __anext__ loop instead of `async for` — see the
                    # stream_chat_completion comment for the Python 3.13 +
                    # httpcore 1.0.x GeneratorExit interaction this avoids.
                    lines_gen = response.aiter_lines().__aiter__()
                    try:
                        while True:
                            try:
                                line = await lines_gen.__anext__()
                            except StopAsyncIteration:
                                break
                            if line.strip():
                                yield line
                    except GeneratorExit:
                        await response.aclose()
                        await lines_gen.aclose()
                        raise
                    finally:
                        await response.aclose()
                        await lines_gen.aclose()
            except httpx.HTTPError as exc:
                logger.error("Kimi fallback HTTP error: %s", exc)
                yield _error_sse_line(
                    502,
                    f"Error communicating with kimi: {exc}",
                    self.provider_type,
                )
            return

        # Synthesize tool_start with the parsed search query so the chat UI's
        # web-search card shows "Searching for: ...".
        first_args_raw = search_calls[0]["function"]["arguments"] or "{}"
        try:
            first_args = _json.loads(first_args_raw)
        except Exception:
            first_args = {}
        # Args are an opaque receipt (`{"search_result":..., "usage":{"total_tokens":N}}`),
        # not a query — Kimi runs the search server-side and bakes results into context.
        logger.info(
            "Kimi $web_search: %d tool_call(s), args[0]=%s",
            len(search_calls),
            first_args_raw[:500],
        )
        first_args_search_tokens: Optional[int] = None
        if isinstance(first_args, dict):
            usage_block = first_args.get("usage")
            if isinstance(usage_block, dict):
                tok = usage_block.get("total_tokens")
                if isinstance(tok, int):
                    first_args_search_tokens = tok
        yield _synthetic_chunk(
            {
                "type": "tool_start",
                "tool_name": "web_search",
                "tool_call_id": tool_call_id,
                "arguments": first_args if isinstance(first_args, dict) else {},
            }
        )
        # The search already ran server-side, so emit tool_end now — otherwise the
        # UI card sits in "running" through the whole second-call answer.
        yield _build_kimi_tool_end(_synthetic_chunk, tool_call_id, [])

        # ---- Second call: echo the tool_calls back and stream answer ----
        assistant_msg = {
            "role": "assistant",
            "content": "",
            "tool_calls": list(tool_calls_acc.values()),
        }
        tool_msgs = [
            {
                "role": "tool",
                "tool_call_id": tc["id"],
                "name": tc["function"]["name"],
                "content": tc["function"]["arguments"],
            }
            for tc in tool_calls_acc.values()
        ]
        followup_body = dict(body)
        followup_body["messages"] = list(messages) + [assistant_msg] + tool_msgs
        # Request a final `usage` block (OAI-compat streams omit it otherwise) so
        # we can see prompt_tokens jump when search context is injected.
        followup_body["stream_options"] = {"include_usage": True}
        # Keep the tool on the second call so the model can search again mid-turn.

        try:
            async with _http_client.stream(
                "POST",
                url,
                json = followup_body,
                headers = self._auth_headers(),
                timeout = self._stream_timeout,
            ) as response:
                if response.status_code != 200:
                    error_body = await response.aread()
                    error_text = error_body.decode("utf-8", errors = "replace")
                    logger.error(
                        "Kimi second-call returned %d: %s",
                        response.status_code,
                        error_text[:500],
                    )
                    yield _error_sse_line(response.status_code, error_text, self.provider_type)
                    return

                lines_gen = response.aiter_lines().__aiter__()
                # Latch final usage; a big prompt_tokens is evidence the server
                # injected search results into context.
                last_usage: Optional[dict[str, Any]] = None
                annotation_shapes: set[str] = set()
                try:
                    while True:
                        try:
                            line = await lines_gen.__anext__()
                        except StopAsyncIteration:
                            break
                        if not line.strip():
                            continue
                        if line.startswith("data:"):
                            data_str = line[len("data:") :].strip()
                            if data_str and data_str != "[DONE]":
                                try:
                                    parsed = _json.loads(data_str)
                                except Exception:
                                    parsed = None
                                if isinstance(parsed, dict):
                                    usage = parsed.get("usage")
                                    if isinstance(usage, dict):
                                        last_usage = usage
                                    # Scan annotations for diagnostics only; Kimi
                                    # doesn't emit url_citation today, but a future
                                    # version's type name would show in the log.
                                    for choice in parsed.get("choices") or []:
                                        if not isinstance(choice, dict):
                                            continue
                                        for envelope in (
                                            choice.get("delta"),
                                            choice.get("message"),
                                        ):
                                            if not isinstance(envelope, dict):
                                                continue
                                            for ann in envelope.get("annotations") or []:
                                                if isinstance(ann, dict):
                                                    annotation_shapes.add(
                                                        str(ann.get("type") or "?")
                                                    )
                        yield line
                except GeneratorExit:
                    await response.aclose()
                    await lines_gen.aclose()
                    raise
                finally:
                    logger.info(
                        "Kimi $web_search complete (model=%s, "
                        "search_ctx_tokens=%s, annotation_types=%s, "
                        "prompt_tokens=%s, completion_tokens=%s)",
                        model,
                        first_args_search_tokens,
                        sorted(annotation_shapes) or None,
                        (last_usage or {}).get("prompt_tokens"),
                        (last_usage or {}).get("completion_tokens"),
                    )
                    await response.aclose()
                    await lines_gen.aclose()
        except httpx.HTTPError as exc:
            logger.error("Kimi second-call HTTP error: %s", exc)
            yield _error_sse_line(
                502,
                f"Error communicating with kimi: {exc}",
                self.provider_type,
            )

    async def _stream_anthropic(
        self,
        messages: list[dict[str, Any]],
        model: str,
        temperature: float,
        top_p: float,
        max_tokens: Optional[int],
        top_k: Optional[int] = None,
        enable_thinking: Optional[bool] = None,
        reasoning_effort: Optional[str] = None,
        enabled_tools: Optional[list[str]] = None,
        enable_prompt_caching: Optional[bool] = None,
        anthropic_code_exec_container_id: Optional[str] = None,
        prompt_cache_ttl: Optional[str] = None,
        compaction_threshold: Optional[int] = None,
        tool_choice: Optional[Any] = None,
        *,
        fast_mode: Optional[bool] = None,
    ) -> AsyncGenerator[str, None]:
        """
        Call the Anthropic Messages API and translate its SSE to OpenAI format.

        Anthropic SSE event types:
          content_block_delta  → OpenAI chunk with delta.content
          message_delta        → OpenAI chunk with finish_reason
          message_stop         → data: [DONE]
          (all others skipped)
        """
        import json as _json

        # Extract system prompt; translate image_url parts to Anthropic format
        system: Optional[str] = None
        filtered: list[dict[str, Any]] = []
        for msg in messages:
            if msg.get("role") == "system":
                content = msg.get("content", "")
                system = (
                    content
                    if isinstance(content, str)
                    else "\n".join(p["text"] for p in content if p.get("type") == "text")
                )
                continue

            content = msg.get("content")
            # OpenAI role="tool" with list content -> Anthropic native
            # tool_result block on a user message. Translating only in the
            # string-content branch (below) would forward the list-content form
            # as an invalid `role:"tool"` message Anthropic rejects. Handle both
            # upfront.
            if msg.get("role") == "tool":
                _tr_id = msg.get("tool_call_id") or ""
                if isinstance(content, list):
                    _flat_parts: list[str] = []
                    for part in content:
                        if (
                            isinstance(part, dict)
                            and part.get("type") == "text"
                            and part.get("text")
                        ):
                            _flat_parts.append(str(part["text"]))
                    _flat_result = "".join(_flat_parts)
                elif content is None:
                    _flat_result = ""
                elif isinstance(content, str):
                    _flat_result = content
                else:
                    _flat_result = _json.dumps(content)
                filtered.append(
                    {
                        "role": "user",
                        "content": [
                            {
                                "type": "tool_result",
                                "tool_use_id": _tr_id,
                                "content": _flat_result,
                            }
                        ],
                    }
                )
                continue
            if isinstance(content, list):
                # Translate OpenAI multimodal parts -> Anthropic native shapes.
                # - `image_url`     -> `{type:"image", source:...}`
                # - `input_document` -> `{type:"document", source:...}`
                #   (Studio extension; mirrors Anthropic's document block,
                #   which supports PDFs as base64 or URL per
                #   https://platform.claude.com/docs/en/build-with-claude/vision)
                anthropic_parts: list[dict[str, Any]] = []
                for part in content:
                    if part.get("type") == "text":
                        anthropic_parts.append({"type": "text", "text": part["text"]})
                    elif part.get("type") == "compaction":
                        # Round-trip a prior turn's compaction block back onto this
                        # assistant message so Anthropic skips re-compaction. Ref:
                        #   https://platform.claude.com/docs/en/build-with-claude/compaction
                        summary = part.get("content") or ""
                        if isinstance(summary, str) and summary:
                            anthropic_parts.append({"type": "compaction", "content": summary})
                    elif part.get("type") == "image_url":
                        url = part.get("image_url", {}).get("url", "")
                        if url.startswith("data:"):
                            # data:image/png;base64,<DATA> -> split header and data
                            header, _, b64data = url.partition(",")
                            media_type = header.split(";")[0].replace("data:", "") or "image/jpeg"
                            anthropic_parts.append(
                                {
                                    "type": "image",
                                    "source": {
                                        "type": "base64",
                                        "media_type": media_type,
                                        "data": b64data,
                                    },
                                }
                            )
                        else:
                            # Remote URL -- Anthropic supports the url source type natively.
                            # See: https://docs.anthropic.com/en/docs/build-with-claude/vision#url-based-images
                            anthropic_parts.append(
                                {
                                    "type": "image",
                                    "source": {
                                        "type": "url",
                                        "url": url,
                                    },
                                }
                            )
                    elif part.get("type") == "input_document":
                        # Studio's normalised PDF/doc type (file_data data-URI or
                        # file_url) -> Anthropic's native `document` block.
                        url = part.get("file_url") or ""
                        data_uri = part.get("file_data") or ""
                        title = part.get("filename")
                        # Treat a "data:" URI with no base64 payload as missing so
                        # the file_url branch can take over (matches OpenAI side).
                        data_uri_valid = False
                        b64data = ""
                        header = ""
                        if data_uri.startswith("data:"):
                            header, _, b64data = data_uri.partition(",")
                            data_uri_valid = bool(b64data.strip())
                        if data_uri_valid:
                            media_type = (
                                part.get("media_type")
                                or header.split(";")[0].replace("data:", "")
                                or "application/pdf"
                            )
                            doc_block: dict[str, Any] = {
                                "type": "document",
                                "source": {
                                    "type": "base64",
                                    "media_type": media_type,
                                    "data": b64data,
                                },
                                # Opt into Anthropic's natural-citation
                                # pipeline; without this no citations_delta
                                # events fire. See
                                # https://platform.claude.com/docs/en/build-with-claude/citations
                                "citations": {"enabled": True},
                            }
                            if title:
                                doc_block["title"] = title
                            anthropic_parts.append(doc_block)
                        elif url:
                            doc_block = {
                                "type": "document",
                                "source": {
                                    "type": "url",
                                    "url": url,
                                },
                                "citations": {"enabled": True},
                            }
                            if title:
                                doc_block["title"] = title
                            anthropic_parts.append(doc_block)
                # Assistant tool_calls -> Anthropic tool_use blocks appended to
                # the same message. The native Messages API doesn't accept
                # OpenAI's top-level `tool_calls` field; the call lives inside a
                # content block `{type:"tool_use", id, name, input}`.
                if msg.get("role") == "assistant" and isinstance(msg.get("tool_calls"), list):
                    for _tc in msg["tool_calls"]:
                        if not isinstance(_tc, dict):
                            continue
                        _fn = _tc.get("function") or {}
                        if not isinstance(_fn, dict) or not _fn.get("name"):
                            continue
                        _raw = _fn.get("arguments") or "{}"
                        try:
                            _input = _json.loads(_raw) if isinstance(_raw, str) else _raw
                        except Exception:
                            _input = {"_raw": _raw}
                        if not isinstance(_input, dict):
                            _input = {"value": _input}
                        anthropic_parts.append(
                            {
                                "type": "tool_use",
                                "id": _tc.get("id") or f"toolu_{time.time_ns()}",
                                "name": _fn["name"],
                                "input": _input,
                            }
                        )
                # Skip whole-message append when nothing usable survived. An
                # empty content array (e.g. user dropped only an unparseable
                # `input_document`) would 400 with "messages.N.content: at
                # least one block is required".
                if anthropic_parts:
                    filtered.append({"role": msg["role"], "content": anthropic_parts})
            else:
                # role="tool" follow-up -> Anthropic native tool_result block
                # on a `user` message. The OpenAI shape (role=tool,
                # content=string, tool_call_id) is not a valid Anthropic role.
                if msg.get("role") == "tool":
                    _tr_id = msg.get("tool_call_id") or ""
                    _tr_content = msg.get("content")
                    if _tr_content is None:
                        _tr_content = ""
                    filtered.append(
                        {
                            "role": "user",
                            "content": [
                                {
                                    "type": "tool_result",
                                    "tool_use_id": _tr_id,
                                    "content": (
                                        _tr_content
                                        if isinstance(_tr_content, str)
                                        else _json.dumps(_tr_content)
                                    ),
                                }
                            ],
                        }
                    )
                    continue
                # Assistant turn whose content is a plain string but also
                # carries OpenAI `tool_calls`: convert into a content-array
                # message with a text block + tool_use blocks. Without this,
                # the top-level tool_calls leaks through unchanged.
                if (
                    msg.get("role") == "assistant"
                    and isinstance(msg.get("tool_calls"), list)
                    and msg["tool_calls"]
                ):
                    _text_content = msg.get("content")
                    _blocks: list[dict[str, Any]] = []
                    if isinstance(_text_content, str) and _text_content:
                        _blocks.append({"type": "text", "text": _text_content})
                    for _tc in msg["tool_calls"]:
                        if not isinstance(_tc, dict):
                            continue
                        _fn = _tc.get("function") or {}
                        if not isinstance(_fn, dict) or not _fn.get("name"):
                            continue
                        _raw = _fn.get("arguments") or "{}"
                        try:
                            _input = _json.loads(_raw) if isinstance(_raw, str) else _raw
                        except Exception:
                            _input = {"_raw": _raw}
                        if not isinstance(_input, dict):
                            _input = {"value": _input}
                        _blocks.append(
                            {
                                "type": "tool_use",
                                "id": _tc.get("id") or f"toolu_{time.time_ns()}",
                                "name": _fn["name"],
                                "input": _input,
                            }
                        )
                    if _blocks:
                        filtered.append({"role": "assistant", "content": _blocks})
                    continue
                filtered.append(msg)

        # Claude 4.7 removed temperature/top_p/top_k entirely (400 "deprecated
        # for this model"). Latch the match and reuse it wherever those are set,
        # including the thinking-mode override below that used to force temp=1.
        sampling_removed = bool(_ANTHROPIC_4_7_SAMPLING_REMOVED.match(model))

        body: dict[str, Any] = {
            "model": model,
            "messages": filtered,
            "max_tokens": max_tokens or 1024,  # required by Anthropic
            "stream": True,
        }
        if not sampling_removed:
            body["temperature"] = temperature
        if top_k is not None and top_k > 0 and not sampling_removed:
            body["top_k"] = top_k
        # Anthropic caches a prefix only with a cache_control marker. Treat None
        # as True (frontend default); pass False to opt out.
        prompt_caching_enabled = enable_prompt_caching is not False
        # Optional 1h cache TTL is GA (no beta header). 1h writes cost 2x vs 5m's
        # 1.25x but reads are 0.1x for both, so 1h wins after one extra hit.
        cache_marker: dict[str, Any] = {"type": "ephemeral"}
        if prompt_cache_ttl in ("5m", "1h"):
            cache_marker["ttl"] = prompt_cache_ttl

        if system:
            if prompt_caching_enabled:
                # System is the most stable cross-turn prefix; own breakpoint.
                body["system"] = [
                    {
                        "type": "text",
                        "text": system,
                        "cache_control": dict(cache_marker),
                    }
                ]
            else:
                body["system"] = system

        if prompt_caching_enabled and filtered:
            # Second breakpoint on the latest message so turn N+1 rehydrates
            # through turn N from cache. Covers the case where the system prompt
            # is below Anthropic's ~1024-token cache floor. (Max 4 breakpoints;
            # we use 2: system + tail.)
            last_msg = filtered[-1]
            content = last_msg.get("content")
            if isinstance(content, str):
                last_msg["content"] = [
                    {
                        "type": "text",
                        "text": content,
                        "cache_control": dict(cache_marker),
                    }
                ]
            elif isinstance(content, list) and content:
                # Rebuild the tail (don't mutate the caller's list) with
                # cache_control on the final block.
                head = list(content[:-1])
                tail = content[-1]
                if isinstance(tail, dict):
                    head.append({**tail, "cache_control": dict(cache_marker)})
                else:
                    head.append(tail)
                last_msg["content"] = head
        thinking_spec = _anthropic_thinking_spec(model)
        allowed_efforts = (
            thinking_spec.efforts if thinking_spec else ("none", "low", "medium", "high")
        )
        effort = reasoning_effort if reasoning_effort in allowed_efforts else None
        # Claude 4.6 takes top-tier adaptive effort as "max" only ("xhigh" is
        # 4.7-only), so map "xhigh" -> "max" for 4.6 outbound requests.
        if effort == "xhigh" and model.startswith(("claude-opus-4-6", "claude-sonnet-4-6")):
            effort = "max"
        if effort is None:
            if enable_thinking is False:
                effort = "none"
            elif enable_thinking is True:
                effort = "medium"
        # Normalize one semantic Thinking control into Anthropic's two model-era
        # APIs: adaptive effort on Claude 4.6/4.7, manual budget_tokens on 4.5.
        if effort and effort != "none":
            # Anthropic rejects top_k whenever thinking is enabled.
            body.pop("top_k", None)
            # 4.5/4.6 require temperature=1 with thinking and forbid top_p in the
            # same request; 4.7 removed temperature entirely (any value 400s), so
            # skip the override there.
            if not sampling_removed:
                body["temperature"] = 1
            body.pop("top_p", None)
            if thinking_spec and thinking_spec.kind == "adaptive":
                # Force display="summarized": it defaults to "omitted" on Opus 4.7,
                # which emits an empty thinking block and leaves the panel blank.
                # Harmless no-op on 4.6.
                body["thinking"] = {"type": "adaptive", "display": "summarized"}
                # Adaptive effort lives under `output_config.effort`, not top-level
                # (top-level 400s "Extra inputs are not permitted"). Allowed:
                # low|medium|high|xhigh|max. See
                # https://platform.claude.com/docs/en/api/messages
                body["output_config"] = {"effort": effort}
            elif thinking_spec and thinking_spec.kind == "manual":
                budget_tokens = {"low": 1024, "medium": 2048, "high": 4096}[effort]
                body["thinking"] = {
                    "type": "enabled",
                    "budget_tokens": budget_tokens,
                }
                # Anthropic requires max_tokens to be strictly greater than
                # thinking.budget_tokens on the manual-thinking path.
                if body.get("max_tokens", 0) <= budget_tokens:
                    body["max_tokens"] = budget_tokens + 1024

        # tool_choice="none" or pinned-function suppresses hosted tools so a
        # stale UI toggle can't fire server-side search/code-exec.
        _anthropic_tool_choice_disabled = (
            isinstance(tool_choice, str) and tool_choice.strip().lower() == "none"
        )
        _anthropic_tool_choice_forced_function = (
            isinstance(tool_choice, dict)
            and tool_choice.get("type") == "function"
            and isinstance(tool_choice.get("function"), dict)
            and bool(tool_choice["function"].get("name"))
        )
        _anthropic_hosted_builtins_allowed = (
            not _anthropic_tool_choice_disabled and not _anthropic_tool_choice_forced_function
        )

        # Anthropic web_search (date-pinned per model family).
        # https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool
        if _anthropic_hosted_builtins_allowed and enabled_tools and "web_search" in enabled_tools:
            anthropic_tools = list(body.get("tools") or [])
            anthropic_tools.append(
                {
                    "type": _anthropic_web_search_version(model),
                    "name": "web_search",
                    "max_uses": 5,
                }
            )
            body["tools"] = anthropic_tools

        # Anthropic web_fetch: only URLs already in conversation. Date-pinned.
        # https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-fetch-tool
        web_fetch_enabled = bool(
            _anthropic_hosted_builtins_allowed and enabled_tools and "web_fetch" in enabled_tools
        )
        if web_fetch_enabled:
            anthropic_tools = list(body.get("tools") or [])
            anthropic_tools.append(
                {
                    "type": _anthropic_web_fetch_version(model),
                    "name": "web_fetch",
                    "max_uses": 5,
                }
            )
            body["tools"] = anthropic_tools

        # Anthropic server-side code execution (date-pinned type per model, both
        # unlocked by the same beta header set below). See
        # https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool
        code_execution_enabled = bool(
            _anthropic_hosted_builtins_allowed
            and enabled_tools
            and "code_execution" in enabled_tools
        )
        if code_execution_enabled:
            anthropic_tools = list(body.get("tools") or [])
            anthropic_tools.append(
                {
                    "type": _anthropic_code_execution_version(model),
                    "name": "code_execution",
                }
            )
            body["tools"] = anthropic_tools
            # Reuse the thread's prior container so filesystem state persists.
            # Stale ids 4xx and clear via container_invalidated.
            if anthropic_code_exec_container_id:
                body["container"] = anthropic_code_exec_container_id

        # Server-side compaction (beta `compact-2026-01-12`). Clamps below-min
        # thresholds to 50K so the request doesn't 400.
        # https://platform.claude.com/docs/en/build-with-claude/compaction
        compaction_active = (
            compaction_threshold is not None
            and compaction_threshold > 0
            and _anthropic_supports_compaction(model)
        )
        if compaction_active and compaction_threshold is not None:
            trigger_value = max(
                int(compaction_threshold),
                _ANTHROPIC_COMPACTION_MIN,
            )
            body["context_management"] = {
                "edits": [
                    {
                        "type": _ANTHROPIC_COMPACTION_TYPE,
                        "trigger": {
                            "type": "input_tokens",
                            "value": trigger_value,
                        },
                    }
                ]
            }

        # fast_mode is Opus 4.6/4.7 only; silently drop elsewhere. Incompatible
        # with the Priority service_tier (frontend gate prevents both at once;
        # backend lets Anthropic 400 if combined).
        fast_mode_active = bool(fast_mode) and _anthropic_supports_fast_mode(model)
        if fast_mode_active:
            body["speed"] = "fast"

        url = f"{self.base_url}/messages"
        completion_id = f"chatcmpl-anthropic-{model.replace('/', '-')}"

        # Log outgoing config keys (not messages) to prove which thinking /
        # effort fields reached the wire.
        logger.info(
            "Anthropic request shape (model=%s, has_thinking=%s, thinking=%s, "
            "output_config=%s, temperature=%s, has_top_p=%s, has_top_k=%s, "
            "max_tokens=%s)",
            model,
            "thinking" in body,
            body.get("thinking"),
            body.get("output_config"),
            body.get("temperature"),
            "top_p" in body,
            "top_k" in body,
            body.get("max_tokens"),
        )

        # Anthropic stop_reason -> OpenAI finish_reason. `pause_turn` maps to
        # None so the UI doesn't treat a paused server-tool turn as final.
        # `refusal` -> "content_filter" (closest match).
        # https://platform.claude.com/docs/en/api/messages#response-stop-reason
        _finish_reason_map: dict[str, Optional[str]] = {
            "end_turn": "stop",
            "max_tokens": "length",
            "stop_sequence": "stop",
            "tool_use": "tool_calls",
            "refusal": "content_filter",
            "pause_turn": None,
        }

        logger.info("Proxying Anthropic Messages API to %s (model=%s)", url, model)

        request_headers = self._auth_headers()
        # Merge new beta flags onto whatever the registry contributed.
        existing_beta = request_headers.get("anthropic-beta", "").strip()
        beta_parts = (
            [p.strip() for p in existing_beta.split(",") if p.strip()] if existing_beta else []
        )
        if code_execution_enabled and _ANTHROPIC_CODE_EXECUTION_BETA not in beta_parts:
            beta_parts.append(_ANTHROPIC_CODE_EXECUTION_BETA)
        if compaction_active and _ANTHROPIC_COMPACTION_BETA not in beta_parts:
            beta_parts.append(_ANTHROPIC_COMPACTION_BETA)
        if fast_mode_active and _ANTHROPIC_FAST_MODE_BETA not in beta_parts:
            beta_parts.append(_ANTHROPIC_FAST_MODE_BETA)
        if beta_parts:
            request_headers["anthropic-beta"] = ",".join(beta_parts)

        try:
            async with _http_client.stream(
                "POST",
                url,
                json = body,
                headers = request_headers,
                timeout = self._stream_timeout,
            ) as response:
                if response.status_code != 200:
                    error_body = await response.aread()
                    error_text = error_body.decode("utf-8", errors = "replace")
                    logger.error(
                        "Anthropic returned %d: %s",
                        response.status_code,
                        error_text[:500],
                    )
                    # Stale container detection (mirrors the OpenAI path). When
                    # we sent a `container` field and the response is 4xx hinting
                    # the id is expired / missing, emit container_invalidated so
                    # the chat adapter clears the stored id and the next turn
                    # falls back to auto-create.
                    if anthropic_code_exec_container_id and 400 <= response.status_code < 500:
                        lowered = error_text.lower()
                        if "container" in lowered and (
                            "expired" in lowered
                            or "not_found" in lowered
                            or "not found" in lowered
                            or "no such container" in lowered
                            or "invalid" in lowered
                        ):
                            yield (
                                f"data: "
                                f"{_json.dumps({'id': completion_id, 'object': 'chat.completion.chunk', 'choices': [{'index': 0, 'delta': {}, 'finish_reason': None}], '_toolEvent': {'type': 'container_invalidated'}})}"
                            )
                    yield _error_sse_line(response.status_code, error_text, self.provider_type)
                    return

                # NOTE: same manual __anext__ loop as stream_chat_completion — see comment there.
                lines_gen = response.aiter_lines().__aiter__()
                thinking_open = False
                # Diagnostic counters for "no thinking content" reports —
                # distinguish "Anthropic never sent thinking_delta" from
                # "frontend didn't render the chunks".
                event_counts: dict[str, int] = {}
                # web_search state. Query streams via input_json_delta on a
                # server_tool_use block; results land in a separate
                # web_search_tool_result block. Per-call citations.
                current_server_tool_use: Optional[dict[str, Any]] = None
                current_result_block: Optional[dict[str, Any]] = None
                web_search_calls: dict[str, dict[str, Any]] = {}
                # code_execution state (bash / text_editor sub-tools); kept
                # parallel to web_search so concurrent pills don't collide.
                current_code_exec_use: Optional[dict[str, Any]] = None
                current_code_exec_result: Optional[dict[str, Any]] = None
                code_execution_calls: dict[str, dict[str, Any]] = {}
                # web_fetch state. Same server_tool_use → *_tool_result block
                # shape as web_search but server_tool_use carries
                # name="web_fetch" and the result block is
                # `web_fetch_tool_result` with content.type=`web_fetch_result`
                # (success) or `web_fetch_tool_error` (failure). Kept separate
                # from web_search state so a turn using both doesn't collide.
                current_web_fetch_use: Optional[dict[str, Any]] = None
                current_web_fetch_result: Optional[dict[str, Any]] = None
                web_fetch_calls: dict[str, dict[str, Any]] = {}
                # Compaction state. Server-side compaction emits a
                # `{type:"compaction", content:"..."}` content block whenever it
                # runs. The summary text can land on the start event AND/OR via
                # text_delta events on the same block (Anthropic's wire format
                # is permissive). Accumulate in `current_compaction["content"]`
                # and emit on content_block_stop so the chat-adapter can persist
                # it onto the assistant message for next-turn round-tripping.
                current_compaction: Optional[dict[str, Any]] = None
                compaction_blocks_seen = 0
                # Document citations from ``citations_delta`` events. Deduped by
                # type-specific anchor key; inline [N] is injected after each
                # cited run, and the full list is forwarded as a synthetic
                # document_citations tool_event on message_stop for the Sources
                # panel.
                document_citations: list[dict[str, Any]] = []
                # Surfaced in the final log line. generated_files_count tracks
                # file_id entries on bash_code_execution_result.content that v1
                # drops, to gauge how often the future Files API PR would matter.
                code_execution_generated_files = 0
                # Container id captured from `message_start.message.container.id`
                # when code_execution is enabled. Emit a `container_ready`
                # _toolEvent on first sight so the chat adapter persists it
                # on the thread record. Only emitted when the value differs
                # from the inbound id — no churn on reuse.
                latched_container_id: Optional[str] = None
                container_id_emitted = False
                # Cache usage from message_start (cache_creation/read_input_tokens)
                # and message_delta (output_tokens), logged on stream complete so
                # caching is verifiable per-request without the dashboard.
                last_usage: dict[str, Any] = {}

                def _content_chunk(text: str) -> str:
                    chunk = {
                        "id": completion_id,
                        "object": "chat.completion.chunk",
                        "choices": [
                            {
                                "index": 0,
                                "delta": {"content": text},
                                "finish_reason": None,
                            }
                        ],
                    }
                    return f"data: {_json.dumps(chunk)}"

                def _emit_tool_event(payload: dict[str, Any]) -> str:
                    _stamp_server_tool_marker(payload)
                    chunk = {
                        "id": completion_id,
                        "object": "chat.completion.chunk",
                        "choices": [
                            {
                                "index": 0,
                                "delta": {},
                                "finish_reason": None,
                            }
                        ],
                        "_toolEvent": payload,
                    }
                    return f"data: {_json.dumps(chunk)}"

                def _format_web_search_results(results: list[Any]) -> str:
                    blocks: list[str] = []
                    for r in results:
                        if not isinstance(r, dict):
                            continue
                        if r.get("type") != "web_search_result":
                            continue
                        url = r.get("url", "")
                        title = r.get("title") or url
                        if not url:
                            continue
                        blocks.append(f"Title: {title}\nURL: {url}")
                    return "\n---\n".join(blocks)

                def _format_web_fetch_result(inner: dict[str, Any]) -> str:
                    """Render a `web_fetch_tool_result.content` payload as the
                    Title / URL / snippet block CodeExecutionToolUI and
                    parseSourcesFromResult expect from the web_search path.

                    Success shape (text):
                        {type: web_fetch_result, url, retrieved_at,
                         content: {type: document, source: {type: text,
                                   media_type, data}, title?}}
                    Success shape (pdf): source.type=base64 + media_type=
                        application/pdf. We don't surface the base64 bytes; the
                        title + url is enough for the source pill, and the model
                        still sees the document contents.
                    Error shape: {type: web_fetch_tool_error, error_code}.
                    """
                    inner_type = inner.get("type") or ""
                    if inner_type == "web_fetch_tool_error":
                        return f"Error: {inner.get('error_code', 'unknown')}"
                    url = inner.get("url", "")
                    document = inner.get("content") or {}
                    title = ""
                    snippet = ""
                    if isinstance(document, dict):
                        title = document.get("title") or ""
                        source = document.get("source") or {}
                        if isinstance(source, dict):
                            media_type = source.get("media_type") or ""
                            data = source.get("data") or ""
                            # Inline a short text preview so the source pill
                            # carries usable context; skip for PDFs (body is
                            # base64-encoded).
                            if media_type.startswith("text/") and isinstance(data, str) and data:
                                snippet = data[:240].strip()
                    # Frontend parseSourcesFromResult only emits a source pill
                    # when both `Title:` and `URL:` are present, so fall back to
                    # the URL when Anthropic omits the document title (matches
                    # the web_search formatter).
                    if not title and url:
                        title = url
                    parts: list[str] = []
                    if title:
                        parts.append(f"Title: {title}")
                    if url:
                        parts.append(f"URL: {url}")
                    if snippet:
                        parts.append(f"Snippet: {snippet}")
                    return "\n".join(parts) if parts else "(fetch complete)"

                def _format_code_execution_result(inner: dict[str, Any]) -> str:
                    """Render an Anthropic code-execution result block as the
                    preformatted text payload the frontend's
                    CodeExecutionToolUI displays inside a <pre>. Handles bash,
                    text_editor (view/create/str_replace), and the matching
                    error variants.
                    """
                    inner_type = inner.get("type") or ""
                    if inner_type.endswith("_error"):
                        return f"Error: {inner.get('error_code', 'unknown')}"
                    if inner_type == "bash_code_execution_result":
                        stdout = inner.get("stdout") or ""
                        stderr = inner.get("stderr") or ""
                        return_code = inner.get("return_code")
                        parts: list[str] = []
                        if stdout:
                            parts.append(stdout)
                        if stderr:
                            parts.append(f"--- stderr ---\n{stderr}")
                        if isinstance(return_code, int) and return_code != 0:
                            parts.append(f"return_code: {return_code}")
                        return "\n".join(parts) if parts else "(no output)"
                    if inner_type == "text_editor_code_execution_result":
                        # view: file content; create: is_file_update flag;
                        # str_replace: diff `lines` list. The matching
                        # server_tool_use carries the command + path, already
                        # encoded into tool_start arguments — here we only
                        # format the result body.
                        if "lines" in inner and isinstance(inner.get("lines"), list):
                            return "\n".join(str(line) for line in inner["lines"])
                        if "is_file_update" in inner:
                            return "Updated" if inner.get("is_file_update") else "Created"
                        content_field = inner.get("content")
                        if isinstance(content_field, str):
                            return content_field
                        return "(file operation complete)"
                    return "(code execution complete)"

                try:
                    while True:
                        try:
                            line = await lines_gen.__anext__()
                        except StopAsyncIteration:
                            break
                        if not line or line.startswith("event:"):
                            continue
                        if not line.startswith("data:"):
                            continue

                        data_str = line[len("data:") :].strip()
                        if not data_str:
                            continue

                        try:
                            event = _json.loads(data_str)
                        except _json.JSONDecodeError:
                            continue

                        event_type = event.get("type")
                        if event_type == "content_block_delta":
                            delta_kind = (event.get("delta") or {}).get("type")
                            key = f"{event_type}:{delta_kind}"
                        else:
                            key = event_type or "<unknown>"
                        event_counts[key] = event_counts.get(key, 0) + 1

                        # Merge input-side usage from message_start with
                        # message_delta's output_tokens into last_usage.
                        if event_type == "message_start":
                            start_usage = (event.get("message") or {}).get("usage")
                            if isinstance(start_usage, dict):
                                last_usage.update(start_usage)

                        if event_type == "content_block_start":
                            content_block = event.get("content_block") or {}
                            block_type = content_block.get("type")
                            block_name = content_block.get("name")
                            if block_type == "server_tool_use" and block_name == "web_search":
                                tool_use_id = content_block.get("id", "") or (
                                    f"ws_{len(web_search_calls)}"
                                )
                                current_server_tool_use = {
                                    "id": tool_use_id,
                                    "buffer": "",
                                }
                                web_search_calls[tool_use_id] = {
                                    "query": "",
                                    "results": [],
                                }
                            elif block_type == "web_search_tool_result":
                                tool_use_id = content_block.get("tool_use_id", "")
                                # Anthropic sometimes ships the full results list
                                # on the start event; sometimes deltas follow.
                                # Capture whatever is present and finalize on
                                # content_block_stop.
                                content = content_block.get("content") or []
                                current_result_block = {
                                    "tool_use_id": tool_use_id,
                                    "results": list(content) if isinstance(content, list) else [],
                                }
                            elif block_type == "server_tool_use" and block_name == "web_fetch":
                                tool_use_id = content_block.get("id", "") or (
                                    f"wf_{len(web_fetch_calls)}"
                                )
                                current_web_fetch_use = {
                                    "id": tool_use_id,
                                    "buffer": "",
                                }
                                web_fetch_calls[tool_use_id] = {
                                    "url": "",
                                    "result": None,
                                }
                            elif block_type == "web_fetch_tool_result":
                                tool_use_id = content_block.get("tool_use_id", "")
                                inner = content_block.get("content") or {}
                                current_web_fetch_result = {
                                    "tool_use_id": tool_use_id,
                                    "inner": inner if isinstance(inner, dict) else {},
                                }
                            elif block_type == "server_tool_use" and block_name in (
                                "bash_code_execution",
                                "text_editor_code_execution",
                            ):
                                tool_use_id = content_block.get("id", "") or (
                                    f"ce_{len(code_execution_calls)}"
                                )
                                kind = (
                                    "bash" if block_name == "bash_code_execution" else "text_editor"
                                )
                                current_code_exec_use = {
                                    "id": tool_use_id,
                                    "kind": kind,
                                    "buffer": "",
                                }
                                code_execution_calls[tool_use_id] = {
                                    "kind": kind,
                                    "arguments": {},
                                    "result": None,
                                }
                            elif block_type in (
                                "bash_code_execution_tool_result",
                                "text_editor_code_execution_tool_result",
                            ):
                                # Code-exec result content arrives whole on the
                                # start event; finalize on content_block_stop to
                                # match the web_search ordering.
                                tool_use_id = content_block.get("tool_use_id", "")
                                inner = content_block.get("content") or {}
                                current_code_exec_result = {
                                    "tool_use_id": tool_use_id,
                                    "inner": inner if isinstance(inner, dict) else {},
                                }
                            elif block_type == "compaction":
                                # Summary may arrive on start AND/OR via
                                # text_delta. Capture both; emit on stop.
                                seed = content_block.get("content") or ""
                                current_compaction = {
                                    "content": seed if isinstance(seed, str) else "",
                                }

                        elif event_type == "content_block_delta":
                            delta = event.get("delta", {})
                            delta_type = delta.get("type")
                            if delta_type == "thinking_delta":
                                # Wrap as <think>...</think> for parseAssistantContent.
                                thinking_text = delta.get("thinking", "")
                                if thinking_text:
                                    if not thinking_open:
                                        thinking_text = f"<think>{thinking_text}"
                                        thinking_open = True
                                    yield _content_chunk(thinking_text)
                            elif delta_type == "text_delta":
                                text = delta.get("text", "")
                                # text_deltas inside a compaction block carry
                                # summary chunks; route them into the compaction
                                # buffer and DON'T yield them to the
                                # user-visible stream -- the summary is opaque
                                # internal state, not assistant prose.
                                if current_compaction is not None:
                                    if text:
                                        current_compaction["content"] += text
                                else:
                                    # First text after a thinking block closes the
                                    # <think> tag opened above. Anthropic emits a
                                    # content_block_stop between blocks, but
                                    # closing on the text_delta transition is more
                                    # forgiving if events arrive out of order.
                                    if thinking_open:
                                        yield _content_chunk("</think>")
                                        thinking_open = False
                                    if text:
                                        yield _content_chunk(text)
                                    # web_search citations: web_search_tool_result.
                                    # User-doc citations: citations_delta below.
                            elif delta_type == "citations_delta":
                                # One citation per event; collapse onto a numbered
                                # footnote list and inject [N] inline. See
                                # https://platform.claude.com/docs/en/build-with-claude/citations
                                cit = delta.get("citation")
                                if isinstance(cit, dict):
                                    key = _anthropic_citation_key(cit)
                                    idx_for_marker: Optional[int] = None
                                    for idx, existing in enumerate(document_citations, start = 1):
                                        if existing.get("_key") == key:
                                            idx_for_marker = idx
                                            break
                                    if idx_for_marker is None:
                                        document_citations.append({**cit, "_key": key})
                                        idx_for_marker = len(document_citations)
                                    yield _content_chunk(f"[{idx_for_marker}]")
                            elif delta_type == "input_json_delta":
                                # partial_json carrying tool inputs (web_search
                                # query, code-exec command, etc.); route to
                                # whichever buffer is open.
                                partial = delta.get("partial_json", "")
                                if current_server_tool_use is not None:
                                    current_server_tool_use["buffer"] += partial
                                elif current_code_exec_use is not None:
                                    current_code_exec_use["buffer"] += partial
                                elif current_web_fetch_use is not None:
                                    current_web_fetch_use["buffer"] += partial
                            # signature_delta and other delta types are skipped
                            # — they carry trust / verification metadata, not
                            # user-visible content.

                        elif event_type == "content_block_stop":
                            if current_server_tool_use is not None:
                                # End of the server_tool_use block — parse the
                                # accumulated input_json into a query and emit
                                # tool_start. The matching tool_end fires later
                                # when the web_search_tool_result block closes
                                # with the actual results.
                                buffer = current_server_tool_use["buffer"]
                                query = ""
                                if buffer:
                                    try:
                                        parsed = _json.loads(buffer)
                                        if isinstance(parsed, dict):
                                            q = parsed.get("query", "")
                                            if isinstance(q, str):
                                                query = q
                                    except Exception:
                                        query = ""
                                tool_use_id = current_server_tool_use["id"]
                                if tool_use_id in web_search_calls:
                                    web_search_calls[tool_use_id]["query"] = query
                                yield _emit_tool_event(
                                    {
                                        "type": "tool_start",
                                        "tool_name": "web_search",
                                        "tool_call_id": tool_use_id,
                                        "arguments": ({"query": query} if query else {}),
                                    }
                                )
                                current_server_tool_use = None
                            elif current_result_block is not None:
                                # End of a web_search_tool_result — emit tool_end
                                # carrying the search results as Title:/URL:
                                # blocks. The frontend's parseSourcesFromResult
                                # lifts these into source pills at message tail.
                                tool_use_id = current_result_block["tool_use_id"]
                                results = current_result_block["results"]
                                if tool_use_id in web_search_calls:
                                    web_search_calls[tool_use_id]["results"] = results
                                result_text = _format_web_search_results(results)
                                yield _emit_tool_event(
                                    {
                                        "type": "tool_end",
                                        "tool_call_id": tool_use_id,
                                        "result": (result_text or "(search complete)"),
                                    }
                                )
                                current_result_block = None
                            elif current_code_exec_use is not None:
                                # End of a code-execution server_tool_use — parse
                                # the buffered input_json into a {command, path,
                                # ...} dict and emit tool_start. The matching
                                # tool_end fires on the result block's
                                # content_block_stop.
                                buffer = current_code_exec_use["buffer"]
                                parsed_args: dict[str, Any] = {}
                                if buffer:
                                    try:
                                        parsed_obj = _json.loads(buffer)
                                        if isinstance(parsed_obj, dict):
                                            parsed_args = parsed_obj
                                    except Exception:
                                        parsed_args = {}
                                tool_use_id = current_code_exec_use["id"]
                                kind = current_code_exec_use["kind"]
                                emit_args = {"kind": kind, **parsed_args}
                                if tool_use_id in code_execution_calls:
                                    code_execution_calls[tool_use_id]["arguments"] = emit_args
                                yield _emit_tool_event(
                                    {
                                        "type": "tool_start",
                                        "tool_name": "code_execution",
                                        "tool_call_id": tool_use_id,
                                        "arguments": emit_args,
                                    }
                                )
                                current_code_exec_use = None
                            elif current_compaction is not None:
                                # End of a compaction block: emit a synthetic
                                # tool_event so the chat-adapter persists it onto
                                # the assistant message for next-turn round-trip.
                                compaction_blocks_seen += 1
                                yield _emit_tool_event(
                                    {
                                        "type": "compaction_block",
                                        "content": current_compaction["content"],
                                    }
                                )
                                current_compaction = None
                            elif current_code_exec_result is not None:
                                # End of a code-execution result block — format
                                # the inner result into the text payload
                                # CodeExecutionToolUI renders.
                                tool_use_id = current_code_exec_result["tool_use_id"]
                                inner = current_code_exec_result["inner"]
                                # Track generated-file count for the follow-up
                                # Files API PR. v1 drops them.
                                if isinstance(inner, dict):
                                    file_blocks = inner.get("content")
                                    if isinstance(file_blocks, list):
                                        for entry in file_blocks:
                                            if isinstance(entry, dict) and entry.get("file_id"):
                                                code_execution_generated_files += 1
                                result_text = _format_code_execution_result(
                                    inner if isinstance(inner, dict) else {}
                                )
                                if tool_use_id in code_execution_calls:
                                    code_execution_calls[tool_use_id]["result"] = result_text
                                yield _emit_tool_event(
                                    {
                                        "type": "tool_end",
                                        "tool_call_id": tool_use_id,
                                        "result": result_text,
                                    }
                                )
                                current_code_exec_result = None
                            elif current_web_fetch_use is not None:
                                # End of the web_fetch server_tool_use — parse the
                                # buffered input_json into the URL the model asked
                                # Anthropic to fetch and emit tool_start. The
                                # matching tool_end fires on the result block's
                                # content_block_stop just below.
                                buffer = current_web_fetch_use["buffer"]
                                url = ""
                                if buffer:
                                    try:
                                        parsed = _json.loads(buffer)
                                        if isinstance(parsed, dict):
                                            probe = parsed.get("url", "")
                                            if isinstance(probe, str):
                                                url = probe
                                    except Exception:
                                        logger.debug(
                                            "Failed to parse web_fetch input_json",
                                            buffer = buffer,
                                        )
                                        url = ""
                                tool_use_id = current_web_fetch_use["id"]
                                if tool_use_id in web_fetch_calls:
                                    web_fetch_calls[tool_use_id]["url"] = url
                                yield _emit_tool_event(
                                    {
                                        "type": "tool_start",
                                        "tool_name": "web_fetch",
                                        "tool_call_id": tool_use_id,
                                        "arguments": ({"url": url} if url else {}),
                                    }
                                )
                                current_web_fetch_use = None
                            elif current_web_fetch_result is not None:
                                # End of the web_fetch_tool_result — format the
                                # source pill and emit tool_end.
                                tool_use_id = current_web_fetch_result["tool_use_id"]
                                result_text = _format_web_fetch_result(
                                    current_web_fetch_result["inner"]
                                )
                                if tool_use_id in web_fetch_calls:
                                    web_fetch_calls[tool_use_id]["result"] = result_text
                                yield _emit_tool_event(
                                    {
                                        "type": "tool_end",
                                        "tool_call_id": tool_use_id,
                                        "result": result_text,
                                    }
                                )
                                current_web_fetch_result = None
                            elif thinking_open:
                                # Close the <think> tag when the thinking block
                                # ends, in case no text_delta follows (e.g.
                                # display=omitted on Claude 4.7, or
                                # thinking-only turns).
                                yield _content_chunk("</think>")
                                thinking_open = False

                        elif event_type == "message_delta":
                            delta_usage = event.get("usage")
                            if isinstance(delta_usage, dict):
                                last_usage.update(delta_usage)
                                # Compaction iterations aren't in top-level
                                # input/output_tokens; fold them into
                                # compaction_{input,output}_tokens for billing.
                                iterations = delta_usage.get("iterations")
                                if isinstance(iterations, list):
                                    c_in = 0
                                    c_out = 0
                                    for it in iterations:
                                        if isinstance(it, dict) and it.get("type") == "compaction":
                                            c_in += int(it.get("input_tokens") or 0)
                                            c_out += int(it.get("output_tokens") or 0)
                                    if c_in or c_out:
                                        last_usage["compaction_input_tokens"] = c_in
                                        last_usage["compaction_output_tokens"] = c_out
                            # Container id is on message_delta.delta.container
                            # (not message_start; not provisioned yet there).
                            # Emit container_ready only when it differs from the
                            # inbound id so reuse doesn't re-write it every turn.
                            delta_obj = event.get("delta") or {}
                            container_obj = delta_obj.get("container")
                            if isinstance(container_obj, dict) and latched_container_id is None:
                                probe = container_obj.get("id")
                                if isinstance(probe, str) and probe:
                                    latched_container_id = probe
                            if (
                                latched_container_id
                                and not container_id_emitted
                                and latched_container_id != anthropic_code_exec_container_id
                            ):
                                yield _emit_tool_event(
                                    {
                                        "type": "container_ready",
                                        "container_id": latched_container_id,
                                    }
                                )
                                container_id_emitted = True
                            stop_reason = event.get("delta", {}).get("stop_reason")
                            if stop_reason:
                                if thinking_open:
                                    yield _content_chunk("</think>")
                                    thinking_open = False
                                # `pause_turn` is in-progress, not terminal: the
                                # SSE stream still ends with [DONE] via
                                # message_stop but we skip emitting a
                                # finish_reason="stop" chunk that would truncate
                                # the rendered message in the UI.
                                mapped = _finish_reason_map.get(stop_reason, "stop")
                                # Streaming refusal: emit a visible notice plus an
                                # out-of-band _toolEvent so the frontend can prune
                                # the refused turn. The mapped finish_reason is
                                # "content_filter" per OpenAI spec.
                                # https://platform.claude.com/docs/en/test-and-evaluate/strengthen-guardrails/handle-streaming-refusals
                                if stop_reason == "refusal":
                                    logger.warning(
                                        "Anthropic refusal stop_reason (model=%s)",
                                        model,
                                    )
                                    # Drop signal rides _toolEvent (not text) so
                                    # assistant content can't spoof a context
                                    # reset.
                                    yield _content_chunk(
                                        "\n\n_The response was stopped by "
                                        "Anthropic's safety classifier. Edit "
                                        "or remove the previous turn and try "
                                        "again._"
                                    )
                                    yield _emit_tool_event({"type": "anthropic_refusal"})
                                if mapped is not None:
                                    chunk = {
                                        "id": completion_id,
                                        "object": "chat.completion.chunk",
                                        "choices": [
                                            {
                                                "index": 0,
                                                "delta": {},
                                                "finish_reason": mapped,
                                            }
                                        ],
                                    }
                                    yield f"data: {_json.dumps(chunk)}"

                        elif event_type == "message_stop":
                            if thinking_open:
                                yield _content_chunk("</think>")
                                thinking_open = False
                            # Forward document_citations so the Sources panel
                            # can render the inline [N] footnotes. ``cited_text``
                            # is truncated server-side to keep SSE bytes bounded
                            # on long spans.
                            if document_citations:
                                clean_cits = []
                                for c in document_citations:
                                    entry = {k: v for k, v in c.items() if k != "_key"}
                                    cited = entry.get("cited_text")
                                    if isinstance(cited, str) and len(cited) > _CITED_TEXT_MAX_LEN:
                                        entry["cited_text"] = cited[:_CITED_TEXT_MAX_LEN] + "…"
                                    clean_cits.append(entry)
                                yield _emit_tool_event(
                                    {
                                        "type": "document_citations",
                                        "citations": clean_cits,
                                    }
                                )
                            # Final include_usage-style chunk so callers see
                            # cache_creation / cache_read without scraping the
                            # server log.
                            usage_line = _build_usage_chunk(
                                completion_id,
                                "anthropic",
                                last_usage,
                            )
                            if usage_line:
                                yield usage_line
                            yield "data: [DONE]"
                            await response.aclose()  # set PoolByteStream._closed=True FIRST
                            break
                except GeneratorExit:
                    await response.aclose()  # set PoolByteStream._closed=True FIRST
                    await lines_gen.aclose()  # now safe — aclose() is a no-op
                    raise
                finally:
                    # Per-event-type counts + web_search summary for triage.
                    web_search_requested = bool(enabled_tools and "web_search" in enabled_tools)
                    web_search_invocations = len(web_search_calls)
                    total_results = sum(
                        len(sc.get("results") or []) for sc in web_search_calls.values()
                    )
                    queries = [sc["query"] for sc in web_search_calls.values() if sc.get("query")]
                    # cache_read_input_tokens > 0 proves the cache_control marker
                    # works (turn 1 shows cache_creation instead).
                    code_execution_invocations = len(code_execution_calls)
                    code_execution_results = sum(
                        1 for c in code_execution_calls.values() if c.get("result") is not None
                    )
                    web_fetch_requested = web_fetch_enabled
                    web_fetch_invocations = len(web_fetch_calls)
                    web_fetch_urls = [wf["url"] for wf in web_fetch_calls.values() if wf.get("url")]
                    logger.info(
                        "Anthropic stream complete (model=%s, "
                        "web_search_requested=%s, web_search_invocations=%s, "
                        "results=%s, queries=%s, "
                        "web_fetch_requested=%s, web_fetch_invocations=%s, "
                        "web_fetch_urls=%s, "
                        "code_execution_requested=%s, "
                        "code_execution_invocations=%s, "
                        "code_execution_results=%s, "
                        "code_execution_generated_files=%s, "
                        "container_id_in=%s, container_id_out=%s, "
                        "input_tokens=%s, output_tokens=%s, "
                        "cache_creation_input_tokens=%s, "
                        "cache_read_input_tokens=%s, "
                        "compaction_input_tokens=%s, "
                        "compaction_output_tokens=%s, "
                        "compaction_blocks_seen=%s, events=%s)",
                        model,
                        web_search_requested,
                        web_search_invocations,
                        total_results,
                        queries,
                        web_fetch_requested,
                        web_fetch_invocations,
                        web_fetch_urls,
                        code_execution_enabled,
                        code_execution_invocations,
                        code_execution_results,
                        code_execution_generated_files,
                        anthropic_code_exec_container_id,
                        latched_container_id,
                        last_usage.get("input_tokens"),
                        last_usage.get("output_tokens"),
                        last_usage.get("cache_creation_input_tokens"),
                        last_usage.get("cache_read_input_tokens"),
                        last_usage.get("compaction_input_tokens"),
                        last_usage.get("compaction_output_tokens"),
                        compaction_blocks_seen,
                        event_counts,
                    )
                    await response.aclose()
                    await lines_gen.aclose()

        except httpx.ConnectError as exc:
            logger.error("Connection error to %s: %s", self.provider_type, exc)
            yield _error_sse_line(
                502,
                f"Failed to connect to {self.provider_type}: {exc}",
                self.provider_type,
            )
        except httpx.ReadTimeout as exc:
            logger.error("Read timeout from %s: %s", self.provider_type, exc)
            yield _error_sse_line(
                504,
                f"Timeout waiting for {self.provider_type} response",
                self.provider_type,
            )
        except httpx.HTTPError as exc:
            logger.error("HTTP error from %s: %s", self.provider_type, exc)
            yield _error_sse_line(
                502,
                f"Error communicating with {self.provider_type}: {exc}",
                self.provider_type,
            )

    async def _stream_gemini(
        self,
        messages: list[dict[str, Any]],
        model: str,
        temperature: float,
        top_p: float,
        max_tokens: Optional[int],
        top_k: Optional[int] = None,
        presence_penalty: float = 0.0,
        enabled_tools: Optional[list[str]] = None,
        enable_prompt_caching: Optional[Any] = None,
        enable_thinking: Optional[bool] = None,
        reasoning_effort: Optional[str] = None,
        tools: Optional[list[dict[str, Any]]] = None,
        tool_choice: Optional[Any] = None,
    ) -> AsyncGenerator[str, None]:
        """
        Call Google's native Gemini API and translate its streaming
        ``streamGenerateContent`` response into OpenAI Chat Completions chunks.

        Gemini does NOT speak the OpenAI Chat Completions contract on its
        primary endpoint. The wire shape is:

          POST /v1beta/models/{model}:streamGenerateContent?alt=sse
          {
            "contents": [{"role": "user|model", "parts": [{"text": "..."}]}],
            "systemInstruction": {"parts": [{"text": "..."}]},
            "generationConfig": {"temperature": 0.7, "topP": 0.95, "topK": 40,
                                  "maxOutputTokens": 1024},
            "tools": [{"googleSearch": {}}, {"codeExecution": {}}],
            "cachedContent": "<cache name>"  // optional, see caching docs
          }

        Streamed responses are SSE frames carrying partial
        ``GenerateContentResponse`` objects:

          {"candidates": [{"content": {"parts": [{"text": "Hello"}]},
                            "finishReason": "STOP"}],
           "usageMetadata": {"promptTokenCount": 7, "candidatesTokenCount": 3}}

        Image generation uses the same endpoint with model
        ``gemini-2.5-flash-image`` (Nano Banana); the response carries an
        ``inlineData`` part with base64 PNG bytes and a ``mimeType``. We surface
        that through the same ``tool_start`` / ``tool_end`` ``image_b64``
        envelope the OpenAI image_generation path uses, so the chat UI renders
        the image inline with no extra plumbing.

        References:
          - https://ai.google.dev/gemini-api/docs/text-generation
          - https://ai.google.dev/gemini-api/docs/function-calling
          - https://ai.google.dev/gemini-api/docs/grounding
          - https://ai.google.dev/gemini-api/docs/caching
          - https://ai.google.dev/gemini-api/docs/image-generation
        """
        import json as _json

        # Validate the user-controlled model id first: `../cachedContents/x` is
        # path traversal, and rejecting early avoids attacker-triggered outbound
        # image fetches on a doomed request. Catalog ids match `[A-Za-z0-9._-]+`.
        if not re.fullmatch(r"[A-Za-z0-9._-]+", model):
            yield _error_sse_line(
                400,
                f"Invalid Gemini model id: {model!r}",
                self.provider_type,
            )
            return

        # Translate OpenAI messages -> Gemini contents. system role promotes to
        # top-level systemInstruction.
        system_text_parts: list[str] = []
        contents: list[dict[str, Any]] = []
        # OpenAI may drop `name` from role="tool" follow-ups. Remember prior
        # function names so functionResponse isn't sent name-less (Gemini 400s
        # on empty names).
        tool_call_names: dict[str, str] = {}
        # tool_call_ids whose assistant card was dropped (synthetic builtin) or
        # already replayed as native parts. Their role="tool" follow-up must be
        # skipped to avoid orphan/duplicate responses.
        _gemini_skip_tool_result_ids: set[str] = set()
        # Per-request image caps. The byte cap counts DECODED bytes; set to
        # ~14 MB because base64 expansion + prompt overhead must fit Gemini's
        # ~20 MB request limit.
        _GEMINI_REMOTE_IMAGE_MAX_COUNT = 8
        _GEMINI_REMOTE_IMAGE_MAX_TOTAL_BYTES = 14 * 1024 * 1024
        _remote_image_count = 0
        _remote_image_total_bytes = 0
        for msg in messages:
            role = msg.get("role")
            content = msg.get("content", "")
            if role == "system":
                if isinstance(content, str):
                    if content:
                        system_text_parts.append(content)
                elif isinstance(content, list):
                    for part in content:
                        if (
                            isinstance(part, dict)
                            and part.get("type") == "text"
                            and part.get("text")
                        ):
                            system_text_parts.append(part["text"])
                continue
            # Map OpenAI roles to Gemini's two-role contract.
            gemini_role = "model" if role == "assistant" else "user"
            parts: list[dict[str, Any]] = []
            if isinstance(content, str):
                if content:
                    parts.append({"text": content})
            elif isinstance(content, list):
                for part in content:
                    if not isinstance(part, dict):
                        continue
                    ptype = part.get("type")
                    if ptype == "text":
                        text = part.get("text", "")
                        if text:
                            parts.append({"text": text})
                    elif ptype == "image_url":
                        url = part.get("image_url", {}).get("url", "")
                        if url.startswith("data:"):
                            header, _, b64data = url.partition(",")
                            media_type = (
                                header.split(";")[0].replace("data:", "").strip().lower()
                                or "image/jpeg"
                            )
                            # Reject non-image data URLs (e.g. data:text/html);
                            # they'd 400 the request as inlineData. Mirrors the
                            # remote-fetch path's Content-Type check.
                            if not media_type.startswith("image/"):
                                logger.info(
                                    "Gemini inlineData: refusing non-image data URL media_type=%s",
                                    media_type,
                                )
                            elif b64data:
                                # data: URLs share the same caps as fetched
                                # URLs so inline payloads don't bypass them.
                                _data_approx_bytes = (len(b64data) * 3) // 4
                                if _remote_image_count >= _GEMINI_REMOTE_IMAGE_MAX_COUNT:
                                    logger.info(
                                        "Gemini inlineData: per-request count cap %d reached, dropping image",
                                        _GEMINI_REMOTE_IMAGE_MAX_COUNT,
                                    )
                                elif (
                                    _remote_image_total_bytes + _data_approx_bytes
                                    > _GEMINI_REMOTE_IMAGE_MAX_TOTAL_BYTES
                                ):
                                    logger.info(
                                        "Gemini inlineData: per-request byte cap reached, dropping image",
                                    )
                                else:
                                    _remote_image_count += 1
                                    _remote_image_total_bytes += _data_approx_bytes
                                    parts.append(
                                        {
                                            "inlineData": {
                                                "mimeType": media_type,
                                                "data": b64data,
                                            }
                                        }
                                    )
                        elif url:
                            # fileData.fileUri only accepts Files-API URIs and
                            # YouTube; everything else is downloaded and inlined.
                            # Parse host explicitly so e.g.
                            # https://evil.com/youtube.com/x isn't mis-detected.
                            try:
                                _parsed_image_url = urlparse(url)
                            except (ValueError, UnicodeError):
                                _parsed_image_url = None
                            if _parsed_image_url is None:
                                _img_scheme = ""
                                _img_host = ""
                                _img_path = ""
                            else:
                                _img_scheme = (_parsed_image_url.scheme or "").lower()
                                _img_host = (_parsed_image_url.hostname or "").lower()
                                _img_path = _parsed_image_url.path or ""
                            _is_native_uri = (
                                _img_scheme == "https"
                                and _img_host == "generativelanguage.googleapis.com"
                                and _img_path.startswith("/v1beta/files/")
                            )
                            _is_youtube = _img_scheme == "https" and (
                                _img_host == "youtu.be"
                                or _img_host == "youtube.com"
                                or _img_host.endswith(".youtube.com")
                            )
                            _guessed, _ = mimetypes.guess_type(_img_path)
                            _media_type = (
                                _guessed
                                if isinstance(_guessed, str) and _guessed.startswith("image/")
                                else "image/jpeg"
                            )
                            if _is_youtube:
                                # YouTube URIs must use video/mp4; the
                                # default image/jpeg yields a 400.
                                parts.append(
                                    {
                                        "fileData": {
                                            "fileUri": url,
                                            "mimeType": "video/mp4",
                                        }
                                    }
                                )
                            elif _is_native_uri:
                                parts.append(
                                    {
                                        "fileData": {
                                            "fileUri": url,
                                            "mimeType": _media_type,
                                        }
                                    }
                                )
                            elif _remote_image_count >= _GEMINI_REMOTE_IMAGE_MAX_COUNT:
                                logger.info(
                                    "Gemini image fetch: per-request count cap %d reached, dropping image",
                                    _GEMINI_REMOTE_IMAGE_MAX_COUNT,
                                )
                            else:
                                # Refuse pre-fetch when the per-request byte
                                # budget is spent; pass the remainder so
                                # over-budget URLs reject on Content-Length.
                                _remaining_bytes = (
                                    _GEMINI_REMOTE_IMAGE_MAX_TOTAL_BYTES - _remote_image_total_bytes
                                )
                                if _remaining_bytes <= 0:
                                    logger.info(
                                        "Gemini image fetch: per-request byte cap already reached, dropping image",
                                    )
                                else:
                                    # Count attempts before awaiting so slow URLs
                                    # don't each burn the timeout.
                                    _remote_image_count += 1
                                    _fetched = await _safe_fetch_image_for_gemini(
                                        url,
                                        _media_type,
                                        max_bytes = _remaining_bytes,
                                    )
                                    if _fetched is not None:
                                        _final_mime, _b64 = _fetched
                                        # base64 expands ~4/3 — recover bytes from len(_b64).
                                        _approx_bytes = (len(_b64) * 3) // 4
                                        if (
                                            _remote_image_total_bytes + _approx_bytes
                                            > _GEMINI_REMOTE_IMAGE_MAX_TOTAL_BYTES
                                        ):
                                            logger.info(
                                                "Gemini image fetch: per-request byte cap reached, dropping image",
                                            )
                                        else:
                                            _remote_image_total_bytes += _approx_bytes
                                            parts.append(
                                                {
                                                    "inlineData": {
                                                        "mimeType": _final_mime,
                                                        "data": _b64,
                                                    }
                                                }
                                            )
            # Gemini 3 strict function-calling requires text-part
            # thoughtSignatures to be replayed on history; the frontend stows
            # the latest one as extra_content.google.thought_signature on the
            # assistant message and we pin it onto the last text part here.
            if role == "assistant" and parts:
                _msg_extra = msg.get("extra_content") if isinstance(msg, dict) else None
                if isinstance(_msg_extra, dict):
                    _msg_g = _msg_extra.get("google") or {}
                    if isinstance(_msg_g, dict):
                        _msg_sig = _msg_g.get("thought_signature") or _msg_g.get("thoughtSignature")
                        if isinstance(_msg_sig, str) and _msg_sig:
                            for _idx in range(len(parts) - 1, -1, -1):
                                if "text" in parts[_idx]:
                                    parts[_idx] = {
                                        **parts[_idx],
                                        "thoughtSignature": _msg_sig,
                                    }
                                    break
            # Translate OpenAI tool_calls into Gemini functionCall parts.
            # code_execution / image_generation replay their native parts
            # (executableCode / codeExecutionResult / inlineData) stowed on
            # extra_content.google.native_part.
            tool_calls = msg.get("tool_calls") if isinstance(msg, dict) else None
            if isinstance(tool_calls, list):
                for tc in tool_calls:
                    if not isinstance(tc, dict):
                        continue
                    fn = tc.get("function") or {}
                    if not isinstance(fn, dict):
                        continue
                    args_raw = fn.get("arguments") or "{}"
                    if isinstance(args_raw, str):
                        try:
                            args = _json.loads(args_raw)
                        except Exception:
                            args = {"_raw": args_raw}
                    elif isinstance(args_raw, dict):
                        args = args_raw
                    else:
                        args = {}
                    fn_name = fn.get("name", "")
                    tc_id = tc.get("id")
                    if fn_name and isinstance(tc_id, str) and tc_id:
                        tool_call_names[tc_id] = fn_name

                    # Replay native Gemini code_execution / image_generation
                    # parts from extra_content.google.native_part, falling back
                    # to args.google.native_part for OAI-compat round-trips.
                    _extra = tc.get("extra_content")
                    _native_part = None
                    _google_extra: dict[str, Any] = {}
                    if isinstance(_extra, dict):
                        _ge = _extra.get("google") or {}
                        if isinstance(_ge, dict):
                            _google_extra = _ge
                            _native_part = _ge.get("native_part")
                    if _native_part is None and isinstance(args, dict):
                        _args_google = args.get("google")
                        if isinstance(_args_google, dict):
                            _args_np = _args_google.get("native_part")
                            if isinstance(_args_np, dict):
                                _native_part = _args_np
                                if not _google_extra:
                                    _google_extra = _args_google

                    # Synthetic builtin cards (web_search/web_fetch) must not
                    # become fake functionCalls; drop them. Native
                    # code_execution / image_generation replay below.
                    _name_lc = fn_name.lower() if isinstance(fn_name, str) else ""
                    _is_synthetic_server_builtin = (
                        _name_lc
                        in (
                            "web_search",
                            "web_fetch",
                            "code_execution",
                            "image_generation",
                        )
                        and isinstance(args, dict)
                        and (
                            args.get("_server_tool") is True
                            or isinstance((args.get("google") or {}).get("native_part"), dict)
                        )
                    )
                    if _is_synthetic_server_builtin and not (
                        _name_lc in ("code_execution", "image_generation")
                        and isinstance(_native_part, dict)
                    ):
                        # No replayable Gemini native part -- skip entirely
                        # rather than send a fake functionCall. Also remember
                        # this tool_call_id so a matching role="tool" follow-up
                        # doesn't become an orphan functionResponse below.
                        if isinstance(tc_id, str) and tc_id:
                            _gemini_skip_tool_result_ids.add(tc_id)
                            tool_call_names.pop(tc_id, None)
                        continue
                    if fn_name in ("code_execution", "image_generation") and isinstance(
                        _native_part, dict
                    ):
                        # code_execution/image_generation history is replayed as
                        # native parts; the matching role="tool" must be skipped
                        # or Gemini sees a functionResponse with no declared
                        # function name and 400s the turn.
                        if isinstance(tc_id, str) and tc_id:
                            _gemini_skip_tool_result_ids.add(tc_id)
                        # New shape: `native_part.parts` is an ordered list of
                        # full part wrappers, each carrying its own
                        # `thoughtSignature`. Preserves Gemini 3's strict
                        # per-part replay requirement when the frontend merged
                        # executableCode + codeExecutionResult + inlineData into
                        # the same tool-call card.
                        _native_parts_list = _native_part.get("parts")
                        if isinstance(_native_parts_list, list):
                            for _entry in _native_parts_list:
                                if isinstance(_entry, dict):
                                    parts.append(_entry)
                            continue
                        # Legacy single-object native_part: fan the shared
                        # thoughtSignature only when one subpart exists; for
                        # code+result, prefer executableCode and drop the
                        # signature elsewhere.
                        _legacy_sig = _native_part.get("thoughtSignature") or _native_part.get(
                            "thought_signature"
                        )
                        _legacy_subparts = [
                            _k
                            for _k in (
                                "executableCode",
                                "codeExecutionResult",
                                "inlineData",
                            )
                            if isinstance(_native_part.get(_k), dict)
                        ]
                        for _native_key in (
                            "executableCode",
                            "codeExecutionResult",
                            "inlineData",
                        ):
                            _sub = _native_part.get(_native_key)
                            if not isinstance(_sub, dict):
                                continue
                            _replay_part: dict[str, Any] = {_native_key: _sub}
                            if isinstance(_legacy_sig, str) and _legacy_sig:
                                if len(_legacy_subparts) == 1:
                                    _replay_part["thoughtSignature"] = _legacy_sig
                                elif _native_key == "executableCode":
                                    _replay_part["thoughtSignature"] = _legacy_sig
                            parts.append(_replay_part)
                        continue

                    # Forward the OpenAI tool_call id into Gemini's
                    # functionCall.id so a follow-up turn issuing multiple calls
                    # to the same function (different args, same name) can be
                    # disambiguated on the response side. Gemini accepts the
                    # field per
                    # https://ai.google.dev/gemini-api/docs/function-calling.
                    function_call_part: dict[str, Any] = {
                        "name": fn_name,
                        "args": args,
                    }
                    if isinstance(tc_id, str) and tc_id:
                        function_call_part["id"] = tc_id
                    # Gemini 3 function-calling requires the prior
                    # thoughtSignature echoed back as a sibling of the
                    # functionCall part. The translator stows it on the
                    # assistant tool_call via
                    # `extra_content.google.thought_signature` (see the inbound
                    # emit below).
                    fc_part: dict[str, Any] = {"functionCall": function_call_part}
                    sig = _google_extra.get("thought_signature") or _google_extra.get(
                        "thoughtSignature"
                    )
                    if isinstance(sig, str) and sig:
                        fc_part["thoughtSignature"] = sig
                    parts.append(fc_part)
            if role == "tool":
                # Drop the follow-up if its assistant-side tool_call was dropped
                # or already replayed as native parts; else it would be an
                # orphan/duplicate functionResponse.
                _tc_id_for_skip = msg.get("tool_call_id")
                if (
                    isinstance(_tc_id_for_skip, str)
                    and _tc_id_for_skip in _gemini_skip_tool_result_ids
                ):
                    continue
                # OpenAI's role="tool" follow-up carries the function result.
                # Gemini's matching shape is a role="user" turn with a
                # functionResponse part. When the caller dropped ``name``,
                # recover it from the matching assistant tool_call so Gemini
                # doesn't 400 on an empty name.
                tool_name = msg.get("name") or msg.get("tool_name") or ""
                if not tool_name:
                    tc_id = msg.get("tool_call_id")
                    if isinstance(tc_id, str) and tc_id in tool_call_names:
                        tool_name = tool_call_names[tc_id]
                response_payload: Any
                if isinstance(content, list):
                    # Flatten list-form tool content to text so the
                    # functionResponse result matches the string-content path.
                    _flat_parts: list[str] = []
                    for _cpart in content:
                        if (
                            isinstance(_cpart, dict)
                            and _cpart.get("type") == "text"
                            and isinstance(_cpart.get("text"), str)
                        ):
                            _flat_parts.append(_cpart["text"])
                    _flat_text = "".join(_flat_parts)
                    try:
                        response_payload = _json.loads(_flat_text)
                    except Exception:
                        response_payload = {"result": _flat_text}
                elif isinstance(content, str):
                    try:
                        response_payload = _json.loads(content)
                    except Exception:
                        response_payload = {"result": content}
                else:
                    response_payload = content or {}
                function_response_part: dict[str, Any] = {
                    "name": tool_name,
                    "response": (
                        response_payload
                        if isinstance(response_payload, dict)
                        else {"result": response_payload}
                    ),
                }
                # Mirror tool_call_id onto functionResponse.id so Gemini can
                # match the result to the originating functionCall when multiple
                # parallel calls were made.
                tc_id = msg.get("tool_call_id")
                if isinstance(tc_id, str) and tc_id:
                    function_response_part["id"] = tc_id
                parts = [{"functionResponse": function_response_part}]
                gemini_role = "user"
            if parts:
                # Merge consecutive functionResponse-only user blocks: Gemini
                # wants parallel tool responses grouped into one user turn.
                if (
                    role == "tool"
                    and contents
                    and contents[-1].get("role") == "user"
                    and all(
                        isinstance(p, dict) and "functionResponse" in p
                        for p in (contents[-1].get("parts") or [])
                    )
                ):
                    contents[-1]["parts"].extend(parts)
                else:
                    contents.append({"role": gemini_role, "parts": parts})

        body: dict[str, Any] = {"contents": contents}
        if system_text_parts:
            body["systemInstruction"] = {"parts": [{"text": "\n\n".join(system_text_parts)}]}

        # Generation config -- temperature / topP / topK / maxOutputTokens map
        # straight across. The frontend capability matrix restricts the sliders
        # the UI exposes for Gemini to this set.
        gen_config: dict[str, Any] = {}
        if temperature is not None:
            gen_config["temperature"] = temperature
        if top_p is not None:
            gen_config["topP"] = top_p
        if top_k is not None and top_k > 0:
            gen_config["topK"] = top_k
        # Gemini accepts ``presencePenalty`` on generationConfig with the same
        # sign convention as the OpenAI knob (positive discourages repetition).
        # Forward when the caller sets it.
        if presence_penalty:
            gen_config["presencePenalty"] = presence_penalty
        if max_tokens is not None:
            gen_config["maxOutputTokens"] = max_tokens

        # Nano Banana image generation: only image-capable models (id contains
        # `-image`/`nano-banana`) accept responseModalities=["TEXT","IMAGE"];
        # text models 400 on it, so a stale image_generation pill is ignored.
        # https://ai.google.dev/gemini-api/docs/image-generation
        model_lc = model.lower()
        is_image_picker_model = "-image" in model_lc or "nano-banana" in model_lc
        # tool_choice="none"/forced-function also suppresses implicit image
        # generation, else an explicit opt-out still bills for image output.
        _tool_choice_disabled = (
            isinstance(tool_choice, str) and tool_choice.strip().lower() == "none"
        )
        _tool_choice_forced_function = (
            isinstance(tool_choice, dict)
            and tool_choice.get("type") == "function"
            and isinstance(tool_choice.get("function"), dict)
            and bool(tool_choice["function"].get("name"))
        )
        _hosted_builtins_allowed = not _tool_choice_disabled and not _tool_choice_forced_function
        # Image-tier models reject text-only tools and thinkingConfig regardless
        # of the pill (model-level constraint); the pill only controls image
        # output. Decouple the two so Images-off + Code/Search-on doesn't 400.
        image_tool_requested = bool(
            _hosted_builtins_allowed and enabled_tools and "image_generation" in enabled_tools
        )
        # Strict tool / thinking strip uses the model-id check.
        is_image_model_strict = is_image_picker_model
        # The actual modality flip only happens when the user opted in.
        is_image_model = is_image_picker_model and image_tool_requested
        if is_image_model:
            gen_config["responseModalities"] = ["TEXT", "IMAGE"]
        elif is_image_picker_model:
            # Force TEXT-only so an image-capable model with Images OFF doesn't
            # still bill for image output.
            gen_config["responseModalities"] = ["TEXT"]

        # Thinking control. Gemini 3 uses thinkingLevel (str), 2.5 uses
        # thinkingBudget (int). Gemini 3 has no full-off; minimum is
        # "minimal" on Flash, "low" on Pro.
        # https://ai.google.dev/gemini-api/docs/thinking
        _GEMINI3_THINKING_PREFIXES = (
            "gemini-3.5-",
            "gemini-3.1-",
            "gemini-3-",
            "gemini-pro-latest",
            "gemini-flash-latest",
            "gemini-flash-lite-latest",
        )
        _GEMINI3_PRO_PREFIXES = (
            "gemini-3.5-pro",
            "gemini-3.1-pro",
            "gemini-3-pro",
            "gemini-pro-latest",
        )
        _PRO_THINKING_PREFIXES = ("gemini-2.5-pro",)
        is_gemini3_thinking = any(model_lc.startswith(p) for p in _GEMINI3_THINKING_PREFIXES)
        is_gemini3_pro = any(model_lc.startswith(p) for p in _GEMINI3_PRO_PREFIXES)
        _is_pro_thinking_only = any(
            model_lc == p or model_lc.startswith(p + "-") for p in _PRO_THINKING_PREFIXES
        )
        effort_lc = (reasoning_effort or "").strip().lower()
        if not is_image_model_strict and is_gemini3_thinking:
            # Gemini 3.x thinkingLevel matrix:
            #   3.1+ Pro:    low/medium/high
            #   3 Pro:       low/high (deprecated 2026-03-09)
            #   3.x Flash*:  minimal/low/medium/high
            # Coerce minimal->low on Pro; medium->high on legacy 3-Pro.
            _G3_LEVELS = {"minimal", "low", "medium", "high"}
            level: Optional[str] = None
            if effort_lc in ("none", "off"):
                level = "low" if is_gemini3_pro else "minimal"
            elif effort_lc == "max":
                level = "high"
            elif effort_lc in _G3_LEVELS:
                # Coerce legacy 3-Pro (low/high only) inputs.
                _is_legacy_gemini3_pro = model_lc.startswith(
                    ("gemini-3-pro-preview", "gemini-3-pro")
                ) and not model_lc.startswith(("gemini-3.1-pro", "gemini-3.5-pro"))
                if is_gemini3_pro and effort_lc == "minimal":
                    level = "low"
                elif _is_legacy_gemini3_pro and effort_lc == "medium":
                    level = "high"
                else:
                    level = effort_lc
            elif enable_thinking is True:
                level = "high"
            elif enable_thinking is False:
                level = "low" if is_gemini3_pro else "minimal"
            if level is not None:
                gen_config["thinkingConfig"] = {"thinkingLevel": level}
        elif not is_image_model_strict:
            # Gemini 2.5 / older: thinkingBudget int. Effort -> budget mirrors
            # the OpenAI minimal/low/medium/high ladder so the frontend picker
            # maps cleanly.
            # NOTE: gemini-2.5-flash-lite rejects positive budgets below 512
            # with HTTP 400, so minimal=512 sits at that floor.
            _EFFORT_TO_BUDGET: dict[str, int] = {
                "minimal": 512,
                "low": 2048,
                "medium": 8192,
                "high": 24576,
                "xhigh": -1,
                "max": -1,
            }
            thinking_budget: Optional[int] = None
            if effort_lc == "none" or enable_thinking is False:
                # Pro-tier 2.5 rejects budget=0 (400 "only works in thinking
                # mode"), so coerce to a small positive value.
                thinking_budget = 128 if _is_pro_thinking_only else 0
            elif effort_lc in _EFFORT_TO_BUDGET:
                thinking_budget = _EFFORT_TO_BUDGET[effort_lc]
            elif enable_thinking is True:
                thinking_budget = -1
            if thinking_budget is not None:
                gen_config["thinkingConfig"] = {
                    "thinkingBudget": thinking_budget,
                }

        if gen_config:
            body["generationConfig"] = gen_config

        # Hosted tools: googleSearch (grounding) and codeExecution.
        # Image-mode rejects codeExecution; only Gemini 3 image models
        # accept googleSearch.
        # https://ai.google.dev/gemini-api/docs/grounding
        # https://ai.google.dev/gemini-api/docs/code-execution
        def _gemini_image_model_allows_google_search(_m: str) -> bool:
            return (
                _m.startswith("gemini-3-pro-image")
                or _m.startswith("gemini-3.1-flash-image")
                or _m.startswith("nano-banana-pro")
                or _m.startswith("nano-banana-2")
            )

        google_search_allowed = (
            not is_image_model_strict or _gemini_image_model_allows_google_search(model_lc)
        )
        code_execution_allowed = not is_image_model_strict
        text_tools_allowed = not is_image_model_strict
        # tool_choice="none" / forced-function suppresses hosted builtins too,
        # matching the Anthropic / OpenRouter gates.
        tools_array: list[dict[str, Any]] = []
        if (
            _hosted_builtins_allowed
            and enabled_tools
            and "web_search" in enabled_tools
            and google_search_allowed
        ):
            tools_array.append({"googleSearch": {}})
        if (
            _hosted_builtins_allowed
            and enabled_tools
            and "code_execution" in enabled_tools
            and code_execution_allowed
        ):
            tools_array.append({"codeExecution": {}})
        # OpenAI function declarations -> Gemini functionDeclarations
        # (https://ai.google.dev/gemini-api/docs/function-calling#step_1). Gemini's
        # Schema accepts only the OpenAPI 3.0 subset; OpenAI strict tools include
        # keys (additionalProperties, $schema, $defs, ...) that 400 as
        # INVALID_ARGUMENT, so strip recursively. Ref:
        # https://ai.google.dev/api/caching#Schema
        _GEMINI_ALLOWED_SCHEMA_KEYS = frozenset(
            {
                "type",
                "format",
                "title",
                "description",
                "nullable",
                "enum",
                "maxItems",
                "minItems",
                "properties",
                "required",
                "minProperties",
                "maxProperties",
                "items",
                "minimum",
                "maximum",
                "minLength",
                "maxLength",
                "pattern",
                "default",
                "anyOf",
                "propertyOrdering",
            }
        )

        def _resolve_local_schema_ref(root: Optional[dict[str, Any]], ref: str) -> Optional[Any]:
            # Walk a `#/foo/bar` JSON pointer against the schema root. Returns
            # None if the pointer doesn't resolve to a dict, so the caller can
            # fall back to the unresolved node.
            if not isinstance(root, dict) or not isinstance(ref, str):
                return None
            if not ref.startswith("#/"):
                return None
            node: Any = root
            for raw_part in ref[2:].split("/"):
                if not raw_part:
                    continue
                part = raw_part.replace("~1", "/").replace("~0", "~")
                if not isinstance(node, dict) or part not in node:
                    return None
                node = node[part]
            return node

        def _sanitize_gemini_schema(
            node: Any,
            root: Optional[dict[str, Any]] = None,
            _seen_refs: Optional[frozenset[str]] = None,
        ) -> Any:
            # Recursively filter to Gemini's OpenAPI 3.0 subset (drop non-allowlist
            # keys) and translate JSON Schema `type:[X,"null"]` into
            # `type:X` + `nullable:true`.
            if root is None and isinstance(node, dict):
                root = node
            if _seen_refs is None:
                _seen_refs = frozenset()
            if isinstance(node, dict):
                # Inline `$ref` targets (Gemini's subset has no $ref), with local
                # siblings overriding and a cycle guard.
                _ref = node.get("$ref")
                if isinstance(_ref, str):
                    if _ref in _seen_refs:
                        return {}
                    _target = _resolve_local_schema_ref(root, _ref)
                    if isinstance(_target, dict):
                        _merged = {
                            **_target,
                            **{k: v for k, v in node.items() if k != "$ref"},
                        }
                        return _sanitize_gemini_schema(_merged, root, _seen_refs | {_ref})
                cleaned: dict[str, Any] = {}
                _nullable_from_union = False
                _flattened_type: Optional[str] = None
                _union_any_of: Optional[list[dict[str, Any]]] = None
                _raw_type = node.get("type")
                if isinstance(_raw_type, list):
                    _non_null = [t for t in _raw_type if t != "null"]
                    if len(_non_null) < len(_raw_type):
                        _nullable_from_union = True
                    if len(_non_null) == 1:
                        _flattened_type = _non_null[0]
                    elif len(_non_null) > 1:
                        # Preserve multi-type unions as anyOf; flattening to the
                        # first non-null type silently drops the other branches
                        # and changes the tool contract.
                        _union_any_of = [{"type": _t} for _t in _non_null if isinstance(_t, str)]
                for _k, _v in node.items():
                    if _k == "type" and isinstance(_v, list):
                        # Handled below via _flattened_type.
                        continue
                    if _k not in _GEMINI_ALLOWED_SCHEMA_KEYS:
                        continue
                    if _k == "properties" and isinstance(_v, dict):
                        cleaned[_k] = {
                            _name: _sanitize_gemini_schema(_subschema, root, _seen_refs)
                            for _name, _subschema in _v.items()
                        }
                    elif _k == "items":
                        cleaned[_k] = _sanitize_gemini_schema(_v, root, _seen_refs)
                    elif _k == "anyOf" and isinstance(_v, list):
                        # Optional[X] / Union[A, B, None]: Pydantic emits
                        # `anyOf: [..., {"type":"null"}]`. Gemini's OpenAPI
                        # subset rejects `"type": "null"` inside anyOf, so drop
                        # the null variant and surface it via `nullable: true`.
                        # If exactly one non-null branch remains, collapse it
                        # inline; otherwise keep the slim anyOf and mark the
                        # field nullable.
                        _saw_null = any(
                            isinstance(_entry, dict) and _entry.get("type") == "null"
                            for _entry in _v
                        )
                        _non_null_entries = [
                            _entry
                            for _entry in _v
                            if not (isinstance(_entry, dict) and _entry.get("type") == "null")
                        ]
                        if len(_non_null_entries) == 1 and _saw_null:
                            _inner = _sanitize_gemini_schema(_non_null_entries[0], root, _seen_refs)
                            if isinstance(_inner, dict):
                                for _ik, _iv in _inner.items():
                                    cleaned.setdefault(_ik, _iv)
                                cleaned.setdefault("nullable", True)
                        else:
                            cleaned[_k] = [
                                _sanitize_gemini_schema(_entry, root, _seen_refs)
                                for _entry in _non_null_entries
                            ]
                            if _saw_null:
                                cleaned.setdefault("nullable", True)
                    elif _k in ("required", "enum", "propertyOrdering"):
                        # Lists of plain strings; copy verbatim.
                        cleaned[_k] = _v
                    else:
                        cleaned[_k] = _v
                if _union_any_of is not None and "anyOf" not in cleaned:
                    cleaned["anyOf"] = [
                        _sanitize_gemini_schema(_s, root, _seen_refs) for _s in _union_any_of
                    ]
                elif _flattened_type is not None:
                    cleaned["type"] = _flattened_type
                if _nullable_from_union and "nullable" not in cleaned:
                    cleaned["nullable"] = True
                return cleaned
            return node

        function_declarations: list[dict[str, Any]] = []
        if tools and text_tools_allowed and not _tool_choice_disabled:
            for _tool in tools:
                if not isinstance(_tool, dict) or _tool.get("type") != "function":
                    continue
                _fn = _tool.get("function")
                if not isinstance(_fn, dict) or not _fn.get("name"):
                    continue
                _decl: dict[str, Any] = {
                    "name": _fn["name"],
                    "description": _fn.get("description") or "",
                }
                _params = _fn.get("parameters")
                if isinstance(_params, dict):
                    _decl["parameters"] = _sanitize_gemini_schema(_params)
                function_declarations.append(_decl)
        if function_declarations:
            tools_array.append({"functionDeclarations": function_declarations})
        if tools_array:
            body["tools"] = tools_array
        # Tool-choice mapping: OpenAI "auto"/"none"/"required"/{name=...} ->
        # Gemini toolConfig.functionCallingConfig.mode + allowedFunctionNames.
        if tool_choice is not None and function_declarations and text_tools_allowed:
            _mode: Optional[str] = None
            _allowed: Optional[list[str]] = None
            if isinstance(tool_choice, str):
                _tc_lc = tool_choice.strip().lower()
                if _tc_lc == "auto":
                    _mode = "AUTO"
                elif _tc_lc == "none":
                    _mode = "NONE"
                elif _tc_lc in ("required", "any"):
                    _mode = "ANY"
            elif isinstance(tool_choice, dict) and tool_choice.get("type") == "function":
                _fn_pick = tool_choice.get("function") or {}
                _name = _fn_pick.get("name") if isinstance(_fn_pick, dict) else None
                if isinstance(_name, str) and _name:
                    _mode = "ANY"
                    _allowed = [_name]
            if _mode is not None:
                _fcc: dict[str, Any] = {"mode": _mode}
                if _allowed:
                    _fcc["allowedFunctionNames"] = _allowed
                body["toolConfig"] = {"functionCallingConfig": _fcc}

        # Prompt caching. The Gemini contract is "create a CachedContent
        # resource, then pass its name on `cachedContent`". The cache is created
        # out of band by the caller via POST /cachedContents; here we forward an
        # explicit cache id when the dispatcher hands us one (a string value on
        # enable_prompt_caching means "use this cache name").
        # https://ai.google.dev/gemini-api/docs/caching
        if isinstance(enable_prompt_caching, str) and enable_prompt_caching:
            body["cachedContent"] = enable_prompt_caching

        # Model id is already validated at the top of _stream_gemini so we never
        # reach a path-traversed URL segment here.
        url = f"{self.base_url}/models/{model}:streamGenerateContent?alt=sse"
        completion_id = f"chatcmpl-gemini-{model.replace('/', '-')}"

        logger.info(
            "Proxying Gemini streamGenerateContent to %s (model=%s, tools=%s, image=%s)",
            url,
            model,
            [list(t.keys())[0] for t in tools_array] if tools_array else [],
            is_image_model,
        )

        def _emit_tool_event(payload: dict[str, Any]) -> str:
            _stamp_server_tool_marker(payload)
            chunk = {
                "id": completion_id,
                "object": "chat.completion.chunk",
                "choices": [
                    {
                        "index": 0,
                        "delta": {},
                        "finish_reason": None,
                    }
                ],
                "_toolEvent": payload,
            }
            return f"data: {_json.dumps(chunk)}"

        def _text_chunk(text: str, extra_content: Optional[dict[str, Any]] = None) -> str:
            delta: dict[str, Any] = {"content": text}
            if extra_content:
                delta["extra_content"] = extra_content
            chunk = {
                "id": completion_id,
                "object": "chat.completion.chunk",
                "choices": [
                    {
                        "index": 0,
                        "delta": delta,
                        "finish_reason": None,
                    }
                ],
            }
            return f"data: {_json.dumps(chunk)}"

        def _gemini_part_extra(part: dict[str, Any]) -> Optional[dict[str, Any]]:
            """Return ``{"google": {"thought_signature": ...}}`` when the Gemini
            stream part carries a `thoughtSignature` we must replay on a
            follow-up turn (Gemini 3 image editing + tool contexts both require
            an exact signature echo)."""
            sig = part.get("thoughtSignature") or part.get("thought_signature")
            if isinstance(sig, str) and sig:
                return {"google": {"thought_signature": sig}}
            return None

        # Gemini finish reasons -> OpenAI vocabulary. Reference:
        # https://ai.google.dev/api/rest/v1beta/Candidate#FinishReason
        _finish_reason_map: dict[str, Optional[str]] = {
            "STOP": "stop",
            "MAX_TOKENS": "length",
            "SAFETY": "content_filter",
            "RECITATION": "content_filter",
            "PROHIBITED_CONTENT": "content_filter",
            "BLOCKLIST": "content_filter",
            "MALFORMED_FUNCTION_CALL": "stop",
            "OTHER": "stop",
            "FINISH_REASON_UNSPECIFIED": None,
        }

        last_usage: Optional[dict[str, Any]] = None
        emitted_function_call_ids: set[str] = set()
        # True once any Gemini functionCall part has been emitted so the final
        # finish_reason swaps STOP -> tool_calls (matches the OpenAI Chat
        # Completions contract; an OAI client that sees a tool_calls delta
        # followed by finish_reason="stop" never executes the tool).
        emitted_any_function_call = False
        # Keyed on whether `googleSearch` was actually forwarded (not caller
        # intent) so image-mode turns don't show a phantom "search complete".
        web_search_active = any("googleSearch" in t for t in tools_array)
        web_search_tool_id = "gemini_web_search"
        web_search_tool_started = False
        web_search_tool_ended = False
        web_search_citations: list[dict[str, str]] = []
        # tool_call_id minted on the most recent executableCode part so the
        # matching codeExecutionResult closes out the same envelope. None
        # between rounds.
        gemini_code_exec_pending_id: Optional[str] = None
        # The most recently emitted code_execution id + result text. Kept *after*
        # the tool_end so a following inline image (matplotlib plot from
        # codeExecution) can attach to the same card via a `__IMAGES__:` marker
        # instead of spawning a separate image_generation event.
        last_code_exec_tool_id: Optional[str] = None
        last_code_exec_result_text: str = ""

        try:
            async with _http_client.stream(
                "POST",
                url,
                json = body,
                headers = self._auth_headers(),
                timeout = self._stream_timeout,
            ) as response:
                if response.status_code != 200:
                    error_body = await response.aread()
                    error_text = error_body.decode("utf-8", errors = "replace")
                    logger.error(
                        "Gemini returned %d: %s",
                        response.status_code,
                        error_text[:500],
                    )
                    yield _error_sse_line(response.status_code, error_text, self.provider_type)
                    return

                if web_search_active:
                    yield _emit_tool_event(
                        {
                            "type": "tool_start",
                            "tool_name": "web_search",
                            "tool_call_id": web_search_tool_id,
                            "arguments": {},
                        }
                    )
                    web_search_tool_started = True

                # NOTE: same manual __anext__ loop as the other streaming
                # helpers (see stream_chat_completion for the Python 3.13 +
                # httpcore 1.0.x GeneratorExit ordering).
                lines_gen = response.aiter_lines().__aiter__()
                final_finish_reason: Optional[str] = None
                try:
                    while True:
                        try:
                            line = await lines_gen.__anext__()
                        except StopAsyncIteration:
                            break
                        if not line.strip():
                            continue
                        if not line.startswith("data:"):
                            continue
                        data_str = line[len("data:") :].strip()
                        if not data_str or data_str == "[DONE]":
                            continue
                        try:
                            event = _json.loads(data_str)
                        except Exception:
                            logger.warning(
                                "Gemini: failed to parse SSE chunk: %s",
                                data_str[:200],
                            )
                            continue
                        if not isinstance(event, dict):
                            continue

                        # Latch usageMetadata across deltas -- the final fragment
                        # carries the complete totals.
                        usage_meta = event.get("usageMetadata")
                        if isinstance(usage_meta, dict):
                            last_usage = usage_meta

                        # Prompt-level safety block (zero candidates +
                        # promptFeedback.blockReason): surface as an error so the
                        # client doesn't see an empty successful response.
                        prompt_feedback = event.get("promptFeedback")
                        if isinstance(prompt_feedback, dict) and prompt_feedback.get("blockReason"):
                            block_reason = str(prompt_feedback.get("blockReason"))
                            # Close out the synthetic web_search start so the UI
                            # doesn't show a spinner stuck on "searching..."
                            # after the error toast lands.
                            if (
                                web_search_active
                                and web_search_tool_started
                                and not web_search_tool_ended
                            ):
                                yield _emit_tool_event(
                                    {
                                        "type": "tool_end",
                                        "tool_call_id": web_search_tool_id,
                                        "result": (
                                            "(search aborted: Gemini blocked "
                                            f"prompt: {block_reason})"
                                        ),
                                    }
                                )
                                web_search_tool_ended = True
                            yield _error_sse_line(
                                400,
                                f"Gemini blocked prompt: {block_reason}",
                                self.provider_type,
                            )
                            return

                        candidates = event.get("candidates") or []
                        if not isinstance(candidates, list):
                            continue
                        for cand in candidates:
                            if not isinstance(cand, dict):
                                continue
                            # Citations / grounding metadata.
                            # `groundingMetadata.groundingChunks[].web` carries
                            # `uri` + `title`. Collect for the tool_end emission
                            # at stream close.
                            gm = cand.get("groundingMetadata")
                            if isinstance(gm, dict) and web_search_active:
                                chunks_list = gm.get("groundingChunks") or []
                                if isinstance(chunks_list, list):
                                    for ch in chunks_list:
                                        if not isinstance(ch, dict):
                                            continue
                                        web = ch.get("web") or {}
                                        if not isinstance(web, dict):
                                            continue
                                        u = web.get("uri") or ""
                                        if not u or not isinstance(u, str):
                                            continue
                                        if any(c["url"] == u for c in web_search_citations):
                                            continue
                                        web_search_citations.append(
                                            {
                                                "url": u,
                                                "title": (web.get("title") or u),
                                                "snippet": "",
                                            }
                                        )

                            content_obj = cand.get("content") or {}
                            parts = (
                                content_obj.get("parts") if isinstance(content_obj, dict) else None
                            )
                            if isinstance(parts, list):
                                for part in parts:
                                    if not isinstance(part, dict):
                                        continue
                                    # Text delta. Stow part-level
                                    # `thoughtSignature` on the delta so Gemini 3
                                    # turns needing an exact signature echo
                                    # round-trip cleanly.
                                    text = part.get("text")
                                    _part_extra = _gemini_part_extra(part)
                                    if isinstance(text, str) and text:
                                        yield _text_chunk(
                                            text,
                                            extra_content = _part_extra,
                                        )
                                    elif _part_extra is not None and not any(
                                        k in part
                                        for k in (
                                            "functionCall",
                                            "executableCode",
                                            "codeExecutionResult",
                                            "inlineData",
                                        )
                                    ):
                                        # Empty-content part carrying a
                                        # thoughtSignature: emit an empty delta to
                                        # preserve the signature.
                                        yield _text_chunk(
                                            "",
                                            extra_content = _part_extra,
                                        )
                                    # functionCall -> OpenAI tool_calls
                                    # delta envelope.
                                    fc = part.get("functionCall")
                                    if isinstance(fc, dict):
                                        fc_name = fc.get("name") or ""
                                        fc_args = fc.get("args") or {}
                                        fc_id = fc.get("id") or f"call_{fc_name}_{time.time_ns()}"
                                        if fc_id in emitted_function_call_ids:
                                            continue
                                        emitted_function_call_ids.add(fc_id)
                                        # Each functionCall needs its own
                                        # tool_calls[*].index, else index-based
                                        # consumers collapse parallel calls.
                                        tc_index = len(emitted_function_call_ids) - 1
                                        tool_call_delta: dict[str, Any] = {
                                            "index": tc_index,
                                            "id": fc_id,
                                            "type": "function",
                                            "function": {
                                                "name": fc_name,
                                                "arguments": _json.dumps(fc_args),
                                            },
                                        }
                                        # Gemini 3 requires the part-level
                                        # thoughtSignature echoed next turn; stow
                                        # it on extra_content.google for replay.
                                        thought_sig = part.get("thoughtSignature") or part.get(
                                            "thought_signature"
                                        )
                                        if isinstance(thought_sig, str) and thought_sig:
                                            tool_call_delta["extra_content"] = {
                                                "google": {
                                                    "thought_signature": thought_sig,
                                                }
                                            }
                                        emitted_any_function_call = True
                                        tool_chunk = {
                                            "id": completion_id,
                                            "object": "chat.completion.chunk",
                                            "choices": [
                                                {
                                                    "index": 0,
                                                    "delta": {"tool_calls": [tool_call_delta]},
                                                    "finish_reason": None,
                                                }
                                            ],
                                        }
                                        yield f"data: {_json.dumps(tool_chunk)}"
                                    # executableCode + codeExecutionResult parts
                                    # surface as the standard code_execution
                                    # tool_start/tool_end envelope (same shape
                                    # OpenAI and Anthropic emit) so the chat
                                    # adapter renders Gemini sandbox output
                                    # through CodeExecutionToolUI.
                                    # https://ai.google.dev/gemini-api/docs/code-execution
                                    exec_code = part.get("executableCode")
                                    if isinstance(exec_code, dict):
                                        code_str = exec_code.get("code") or ""
                                        if code_str:
                                            code_tool_id = (
                                                exec_code.get("id")
                                                or f"gemini_code_exec_{time.time_ns()}"
                                            )
                                            gemini_code_exec_pending_id = code_tool_id
                                            # Stow the raw Gemini part so
                                            # follow-up turns can replay the
                                            # native `executableCode` (Gemini
                                            # rejects a generic functionCall echo
                                            # for code execution history).
                                            _exec_thought_sig = part.get(
                                                "thoughtSignature"
                                            ) or part.get("thought_signature")
                                            # Per-part thoughtSignature stays
                                            # bound to its own part (Gemini 3
                                            # rejects shared signatures).
                                            _exec_part_entry: dict[str, Any] = {
                                                "executableCode": exec_code,
                                            }
                                            if (
                                                isinstance(_exec_thought_sig, str)
                                                and _exec_thought_sig
                                            ):
                                                _exec_part_entry["thoughtSignature"] = (
                                                    _exec_thought_sig
                                                )
                                            _exec_native: dict[str, Any] = {
                                                "parts": [_exec_part_entry],
                                            }
                                            yield _emit_tool_event(
                                                {
                                                    "type": "tool_start",
                                                    "tool_name": "code_execution",
                                                    "tool_call_id": code_tool_id,
                                                    "arguments": {
                                                        "kind": "code_execution",
                                                        "language": (
                                                            (
                                                                exec_code.get("language")
                                                                or "PYTHON"
                                                            ).lower()
                                                        ),
                                                        "code": code_str,
                                                        "google": {
                                                            "native_part": _exec_native,
                                                        },
                                                    },
                                                }
                                            )
                                    exec_result = part.get("codeExecutionResult")
                                    if isinstance(exec_result, dict):
                                        outcome = exec_result.get("outcome") or ""
                                        output = exec_result.get("output") or ""
                                        # Gemini returns OUTCOME_OK /
                                        # OUTCOME_FAILED /
                                        # OUTCOME_DEADLINE_EXCEEDED. Treat non-OK
                                        # outcomes as stderr so the UI surfaces
                                        # the error.
                                        if outcome and outcome != "OUTCOME_OK":
                                            result_text = f"[{outcome}]\n{output}".rstrip()
                                        else:
                                            result_text = output
                                        # Pair tool_end with the most recent
                                        # executableCode tool_start; else
                                        # exec_result.id, then a fresh id.
                                        pair_id = (
                                            gemini_code_exec_pending_id
                                            or exec_result.get("id")
                                            or f"gemini_code_exec_{time.time_ns()}"
                                        )
                                        if gemini_code_exec_pending_id is None:
                                            yield _emit_tool_event(
                                                {
                                                    "type": "tool_start",
                                                    "tool_name": "code_execution",
                                                    "tool_call_id": pair_id,
                                                    "arguments": {
                                                        "kind": "code_execution",
                                                        "code": "",
                                                    },
                                                }
                                            )
                                        _result_thought_sig = part.get(
                                            "thoughtSignature"
                                        ) or part.get("thought_signature")
                                        _result_part_entry: dict[str, Any] = {
                                            "codeExecutionResult": exec_result,
                                        }
                                        if (
                                            isinstance(_result_thought_sig, str)
                                            and _result_thought_sig
                                        ):
                                            _result_part_entry["thoughtSignature"] = (
                                                _result_thought_sig
                                            )
                                        _result_native: dict[str, Any] = {
                                            "parts": [_result_part_entry],
                                        }
                                        yield _emit_tool_event(
                                            {
                                                "type": "tool_end",
                                                "tool_call_id": pair_id,
                                                "result": result_text,
                                                "google": {
                                                    "native_part": _result_native,
                                                },
                                            }
                                        )
                                        last_code_exec_tool_id = pair_id
                                        last_code_exec_result_text = result_text
                                        gemini_code_exec_pending_id = None
                                    # inlineData: either a Nano Banana generation
                                    # (own card) or a sandbox plot attached to
                                    # the code_execution card via the __IMAGES__:
                                    # marker.
                                    inline = part.get("inlineData")
                                    if isinstance(inline, dict):
                                        b64 = inline.get("data") or ""
                                        mime = inline.get("mimeType") or "image/png"
                                        if b64:
                                            image_uri = f"data:{mime};base64,{b64}"
                                            attached_to_code_exec = (
                                                not is_image_model
                                                and last_code_exec_tool_id is not None
                                                and bool(enabled_tools)
                                                and "code_execution" in (enabled_tools or [])
                                            )
                                            if attached_to_code_exec:
                                                updated_result = (
                                                    last_code_exec_result_text
                                                    + "\n__IMAGES__:"
                                                    + _json.dumps([image_uri])
                                                )
                                                # Stow inlineData so a follow-up
                                                # turn replays the plot with its
                                                # per-part thoughtSignature.
                                                _plot_thought_sig = part.get(
                                                    "thoughtSignature"
                                                ) or part.get("thought_signature")
                                                _plot_part_entry: dict[str, Any] = {
                                                    "inlineData": {
                                                        "mimeType": mime,
                                                        "data": b64,
                                                    },
                                                }
                                                if (
                                                    isinstance(_plot_thought_sig, str)
                                                    and _plot_thought_sig
                                                ):
                                                    _plot_part_entry["thoughtSignature"] = (
                                                        _plot_thought_sig
                                                    )
                                                yield _emit_tool_event(
                                                    {
                                                        "type": "tool_end",
                                                        "tool_call_id": (last_code_exec_tool_id),
                                                        "result": updated_result,
                                                        "google": {
                                                            "native_part": {
                                                                "parts": [_plot_part_entry],
                                                            },
                                                        },
                                                    }
                                                )
                                                last_code_exec_result_text = updated_result
                                            else:
                                                img_id = f"img_{time.time_ns()}"
                                                yield _emit_tool_event(
                                                    {
                                                        "type": "tool_start",
                                                        "tool_name": "image_generation",
                                                        "tool_call_id": img_id,
                                                        "arguments": {
                                                            "kind": "image",
                                                            "prompt": "",
                                                        },
                                                    }
                                                )
                                                # Gemini 3 image edit needs
                                                # the prior thoughtSignature
                                                # echoed on the inline image part.
                                                _img_thought_sig = part.get(
                                                    "thoughtSignature"
                                                ) or part.get("thought_signature")
                                                _img_tool_end: dict[str, Any] = {
                                                    "type": "tool_end",
                                                    "tool_call_id": img_id,
                                                    "result": "",
                                                    "image_b64": b64,
                                                    "image_mime": mime,
                                                }
                                                # Stow inlineData so multi-turn
                                                # edits replay the original
                                                # image as native history.
                                                _img_part_entry: dict[str, Any] = {
                                                    "inlineData": {
                                                        "mimeType": mime,
                                                        "data": b64,
                                                    },
                                                }
                                                if (
                                                    isinstance(_img_thought_sig, str)
                                                    and _img_thought_sig
                                                ):
                                                    _img_part_entry["thoughtSignature"] = (
                                                        _img_thought_sig
                                                    )
                                                _img_native: dict[str, Any] = {
                                                    "parts": [_img_part_entry],
                                                }
                                                _img_google: dict[str, Any] = {
                                                    "native_part": _img_native,
                                                }
                                                if (
                                                    isinstance(_img_thought_sig, str)
                                                    and _img_thought_sig
                                                ):
                                                    _img_google["thought_signature"] = (
                                                        _img_thought_sig
                                                    )
                                                _img_tool_end["google"] = _img_google
                                                yield _emit_tool_event(_img_tool_end)
                            finish_reason = cand.get("finishReason")
                            if isinstance(finish_reason, str):
                                mapped = _finish_reason_map.get(finish_reason, "stop")
                                if mapped is not None:
                                    final_finish_reason = mapped

                    # End-of-stream order: web_search tool_end -> finish_reason ->
                    # usage -> [DONE], matching the Anthropic/OpenAI helpers.
                    if web_search_active and web_search_tool_started and not web_search_tool_ended:
                        blocks: list[str] = []
                        for cit in web_search_citations:
                            line_out = f"Title: {cit['title']}\nURL: {cit['url']}"
                            if cit.get("snippet"):
                                line_out += f"\nSnippet: {cit['snippet']}"
                            blocks.append(line_out)
                        yield _emit_tool_event(
                            {
                                "type": "tool_end",
                                "tool_call_id": web_search_tool_id,
                                "result": (
                                    "\n---\n".join(blocks) if blocks else "(search complete)"
                                ),
                            }
                        )
                        web_search_tool_ended = True

                    if final_finish_reason:
                        # Gemini emits "STOP" even for a pure functionCall turn;
                        # override to "tool_calls" so OAI clients run the tool.
                        if emitted_any_function_call and final_finish_reason == "stop":
                            final_finish_reason = "tool_calls"
                        finish_chunk = {
                            "id": completion_id,
                            "object": "chat.completion.chunk",
                            "choices": [
                                {
                                    "index": 0,
                                    "delta": {},
                                    "finish_reason": final_finish_reason,
                                }
                            ],
                        }
                        yield f"data: {_json.dumps(finish_chunk)}"

                    # Map Gemini usageMetadata onto OpenAI include_usage.
                    # thoughtsTokenCount is billed output too — fold it in so
                    # cost calculators don't undercount.
                    if isinstance(last_usage, dict):
                        thought_tokens = last_usage.get("thoughtsTokenCount") or 0
                        candidate_tokens = last_usage.get("candidatesTokenCount") or 0
                        prompt_tokens = last_usage.get("promptTokenCount") or 0
                        # Gemini bills tool-call prompt slices separately via
                        # `toolUsePromptTokenCount`. Fold into input so
                        # total_tokens doesn't undercount tool turns.
                        tool_use_prompt_tokens = last_usage.get("toolUsePromptTokenCount") or 0
                        translated_usage = {
                            "input_tokens": prompt_tokens + tool_use_prompt_tokens,
                            "output_tokens": candidate_tokens + thought_tokens,
                            "input_tokens_details": {
                                "cached_tokens": (last_usage.get("cachedContentTokenCount") or 0),
                                "tool_use_prompt_tokens": tool_use_prompt_tokens,
                            },
                            "output_tokens_details": {
                                "reasoning_tokens": thought_tokens,
                            },
                        }
                        usage_line = _build_usage_chunk(completion_id, "openai", translated_usage)
                        if usage_line:
                            yield usage_line

                    yield "data: [DONE]"
                finally:
                    # Close response first so lines_gen.aclose() is a no-op.
                    await response.aclose()
                    await lines_gen.aclose()

        except httpx.ConnectError as exc:
            logger.error("Connection error to %s: %s", self.provider_type, exc)
            if web_search_tool_started and not web_search_tool_ended:
                yield _emit_tool_event(
                    {
                        "type": "tool_end",
                        "tool_call_id": web_search_tool_id,
                        "result": f"(search aborted: connection error: {exc})",
                    }
                )
                web_search_tool_ended = True
            yield _error_sse_line(
                502,
                f"Failed to connect to {self.provider_type}: {exc}",
                self.provider_type,
            )
        except httpx.ReadTimeout as exc:
            logger.error("Read timeout from %s: %s", self.provider_type, exc)
            if web_search_tool_started and not web_search_tool_ended:
                yield _emit_tool_event(
                    {
                        "type": "tool_end",
                        "tool_call_id": web_search_tool_id,
                        "result": "(search aborted: read timeout)",
                    }
                )
                web_search_tool_ended = True
            yield _error_sse_line(
                504,
                f"Timeout waiting for {self.provider_type} response",
                self.provider_type,
            )
        except httpx.HTTPError as exc:
            logger.error("HTTP error from %s: %s", self.provider_type, exc)
            if web_search_tool_started and not web_search_tool_ended:
                yield _emit_tool_event(
                    {
                        "type": "tool_end",
                        "tool_call_id": web_search_tool_id,
                        "result": f"(search aborted: transport error: {exc})",
                    }
                )
                web_search_tool_ended = True
            yield _error_sse_line(
                502,
                f"Error communicating with {self.provider_type}: {exc}",
                self.provider_type,
            )

    async def _stream_openai_responses(
        self,
        messages: list[dict[str, Any]],
        model: str,
        temperature: float,
        top_p: float,
        max_tokens: Optional[int],
        enable_thinking: Optional[bool],
        reasoning_effort: Optional[str],
        enabled_tools: Optional[list[str]] = None,
        enable_prompt_caching: Optional[bool] = None,
        openai_code_exec_container_id: Optional[str] = None,
        compaction_threshold: Optional[int] = None,
        tools: Optional[list[dict[str, Any]]] = None,
        tool_choice: Optional[Any] = None,
    ) -> AsyncGenerator[str, None]:
        """
        Call OpenAI's /v1/responses endpoint and translate its SSE stream back
        into OpenAI Chat Completions chunk format.

        The Responses API uses a different request shape (``input`` not
        ``messages``, ``instructions`` for system prompts, ``max_output_tokens``
        for the budget) and emits event-typed SSE frames (e.g.
        ``response.output_text.delta``) rather than chat-completion chunks.
        ``presence_penalty`` / ``top_k`` aren't part of the Responses contract
        and are dropped here.
        """
        import json as _json

        is_openai_cloud = _is_openai_family_cloud(self.base_url)
        image_generation_requested = bool(
            enabled_tools and "image_generation" in enabled_tools and is_openai_cloud
        )

        # Split system messages into a single `instructions` string and
        # translate user/assistant messages into the Responses input shape.
        instructions_parts: list[str] = []
        input_items: list[dict[str, Any]] = []
        # When we drop a server-side builtin `function_call` here, the matching
        # `role="tool"` follow-up must also be dropped -- otherwise the outbound
        # body has an orphan `function_call_output` with no matching
        # `function_call`, which OpenAI Responses can reject or mis-associate.
        skipped_server_builtin_call_ids: set[str] = set()
        openai_replay_items: list[dict[str, Any]] = []
        previous_response_id: Optional[str] = None
        for msg in messages:
            role = msg.get("role")
            content = msg.get("content", "")

            if role == "system":
                if isinstance(content, str):
                    if content:
                        instructions_parts.append(content)
                elif isinstance(content, list):
                    for part in content:
                        if part.get("type") == "text" and part.get("text"):
                            instructions_parts.append(part["text"])
                continue

            # Responses uses item-shape history: each assistant call is a
            # `function_call` item and each role="tool" follow-up a
            # `function_call_output` keyed by call_id (Chat Completions shape 400s).
            if role == "tool":
                _call_id = msg.get("tool_call_id") or ""
                # If the matching assistant `function_call` was a server-side
                # builtin we already dropped, drop the follow-up too to avoid
                # an orphan `function_call_output`.
                if _call_id and _call_id in skipped_server_builtin_call_ids:
                    continue
                if isinstance(content, list):
                    _flat_parts: list[str] = []
                    for part in content:
                        if part.get("type") == "text" and part.get("text"):
                            _flat_parts.append(part["text"])
                    _output_text = "".join(_flat_parts)
                else:
                    _output_text = content if isinstance(content, str) else ""
                if _call_id:
                    input_items.append(
                        {
                            "type": "function_call_output",
                            "call_id": _call_id,
                            "output": _output_text,
                        }
                    )
                continue

            # Translate assistant tool_calls into `function_call` items, skipping
            # server-side builtin cards (builtin name + `_server_tool` marker).
            _tool_calls = msg.get("tool_calls") if isinstance(msg, dict) else None
            if role == "assistant" and isinstance(_tool_calls, list):
                # Emit assistant text before its function_call items to preserve
                # the original response.output ordering.
                if isinstance(content, str) and content:
                    input_items.append({"role": "assistant", "content": content})
                elif isinstance(content, list):
                    _asst_parts: list[dict[str, Any]] = []
                    for _part in content:
                        if not isinstance(_part, dict):
                            continue
                        _pt = _part.get("type")
                        if _pt == "text" and _part.get("text"):
                            _asst_parts.append(
                                {
                                    "type": "input_text",
                                    "text": _part.get("text", ""),
                                }
                            )
                        elif _pt == "image_url":
                            _u = _part.get("image_url", {}).get("url", "")
                            if _u:
                                _asst_parts.append({"type": "input_image", "image_url": _u})
                    if _asst_parts:
                        input_items.append({"role": "assistant", "content": _asst_parts})

                for _tc in _tool_calls:
                    if not isinstance(_tc, dict):
                        continue
                    _fn = _tc.get("function") or {}
                    if not isinstance(_fn, dict) or not _fn.get("name"):
                        continue
                    _args_raw = _fn.get("arguments") or ""
                    if not isinstance(_args_raw, str):
                        try:
                            _args_raw = _json.dumps(_args_raw)
                        except Exception:
                            _args_raw = ""
                    _fn_name_lc = (_fn.get("name") or "").lower()
                    _is_server_builtin = False
                    if _fn_name_lc in _SERVER_SIDE_BUILTIN_TOOL_NAMES:
                        try:
                            _args_obj = _json.loads(_args_raw) if _args_raw else {}
                        except Exception:
                            _args_obj = None
                        if isinstance(_args_obj, dict):
                            if _args_obj.get("_server_tool") is True:
                                _is_server_builtin = True
                            else:
                                _g = _args_obj.get("google")
                                if isinstance(_g, dict) and isinstance(_g.get("native_part"), dict):
                                    _is_server_builtin = True
                    _call_id_out = _tc.get("id") or f"call_{time.time_ns()}"
                    if _is_server_builtin:
                        skipped_server_builtin_call_ids.add(_call_id_out)
                        continue
                    input_items.append(
                        {
                            "type": "function_call",
                            "call_id": _call_id_out,
                            "name": _fn["name"],
                            "arguments": _args_raw,
                        }
                    )
                # Assistant text already emitted above (in order) so we don't
                # fall through to the generic content branches.
                continue

            if isinstance(content, str):
                input_items.append({"role": role, "content": content})
                continue

            if isinstance(content, list):
                translated_parts: list[dict[str, Any]] = []
                used_previous_response_id = False
                for part in content:
                    part_type = part.get("type")
                    if part_type == "text":
                        translated_parts.append(
                            {"type": "input_text", "text": part.get("text", "")}
                        )
                    elif part_type == "image_url":
                        url = part.get("image_url", {}).get("url", "")
                        if url:
                            # Responses takes image_url as a flat string (both
                            # https:// URLs and data: URLs are accepted).
                            translated_parts.append({"type": "input_image", "image_url": url})
                    elif (
                        part_type == "reasoning"
                        and role == "assistant"
                        and image_generation_requested
                    ):
                        replay_item = _sanitize_openai_reasoning_replay_item(part)
                        if replay_item:
                            openai_replay_items.append(replay_item)
                    elif (
                        part_type == "image_generation_call"
                        and role == "assistant"
                        and image_generation_requested
                    ):
                        response_id = (
                            part.get("response_id")
                            or part.get("openai_response_id")
                            or part.get("previous_response_id")
                        )
                        call_id = part.get("id") or part.get("image_generation_call_id")
                        if isinstance(call_id, str) and call_id:
                            if isinstance(response_id, str) and response_id:
                                previous_response_id = response_id
                                input_items = []
                                translated_parts = []
                                used_previous_response_id = True
                            else:
                                previous_response_id = None
                            openai_replay_items.append(
                                {"type": "image_generation_call", "id": call_id}
                            )
                    elif part_type == "input_document":
                        # Map Studio's `input_document` onto Responses' `input_file`.
                        # https://developers.openai.com/api/docs/guides/images-vision
                        file_url = part.get("file_url")
                        file_data = part.get("file_data")
                        filename = part.get("filename")
                        # Treat a "data:" URI with no base64 payload as missing
                        # (else file_data="" 400s) and fall back to file_url.
                        file_data_valid = bool(
                            isinstance(file_data, str)
                            and file_data
                            and (
                                not file_data.startswith("data:")
                                or file_data.partition(",")[2].strip()
                            )
                        )
                        block: dict[str, Any] = {"type": "input_file"}
                        if file_data_valid:
                            block["file_data"] = file_data
                        elif file_url:
                            block["file_url"] = file_url
                        else:
                            continue
                        if filename:
                            block["filename"] = filename
                        translated_parts.append(block)
                if translated_parts and not used_previous_response_id:
                    input_items.append({"role": role, "content": translated_parts})

        if previous_response_id:
            # OpenAI's documented multi-turn image generation path can use
            # `previous_response_id` to carry the prior generated image and
            # paired reasoning state. Prefer that over manual item replay when we
            # captured the response id; replay below is a fallback for older
            # stored turns that only have an image_generation_call id.
            openai_replay_items = []
        elif (
            _openai_image_replay_requires_reasoning(model)
            and reasoning_effort != "none"
            and enable_thinking is not False
        ):
            filtered_replay_items: list[dict[str, Any]] = []
            has_reasoning_replay = False
            dropped_image_replay_without_reasoning = False
            for item in openai_replay_items:
                if item.get("type") == "reasoning":
                    has_reasoning_replay = True
                    filtered_replay_items.append(item)
                elif item.get("type") == "image_generation_call":
                    if has_reasoning_replay:
                        filtered_replay_items.append(item)
                    else:
                        dropped_image_replay_without_reasoning = True
                else:
                    filtered_replay_items.append(item)
            openai_replay_items = filtered_replay_items
            if dropped_image_replay_without_reasoning:
                yield _error_sse_line(
                    400,
                    "OpenAI image edit reference is missing paired reasoning state. "
                    "Regenerate the image, then retry the edit.",
                    self.provider_type,
                )
                return
        image_generation_has_reference = bool(
            previous_response_id
            or any(
                isinstance(item, dict) and item.get("type") == "image_generation_call"
                for item in openai_replay_items
            )
        )
        if openai_replay_items:
            insert_at = len(input_items)
            for index in range(len(input_items) - 1, -1, -1):
                if input_items[index].get("role") == "user":
                    insert_at = index
                    break
            input_items[insert_at:insert_at] = openai_replay_items

        # gpt-5.x / o3 / gpt-4.5 reject temperature/top_p (400 "Unsupported
        # parameter"); the openai allowlist scopes the picker to these families,
        # so never forward sampling knobs.
        del temperature, top_p  # accepted for API symmetry, not forwarded.

        body: dict[str, Any] = {
            "model": model,
            "input": input_items,
            "stream": True,
        }
        if previous_response_id:
            body["previous_response_id"] = previous_response_id
        # `summary: "auto"` is what makes /v1/responses emit reasoning summary
        # events; without it the reasoning panel stays blank. Pair it with any
        # explicit effort except "none".
        summary_unsupported = bool(
            _OPENAI_REASONING_SUMMARY_UNSUPPORTED.match(model.strip().lower())
        )
        if reasoning_effort in (
            "minimal",
            "low",
            "medium",
            "high",
            "max",
            "xhigh",
        ):
            body["reasoning"] = {"effort": reasoning_effort}
            if not summary_unsupported:
                body["reasoning"]["summary"] = "auto"
        elif reasoning_effort == "none" or enable_thinking is False:
            body["reasoning"] = {"effort": "none"}
        elif enable_thinking is True:
            body["reasoning"] = {"effort": "medium"}
            if not summary_unsupported:
                body["reasoning"]["summary"] = "auto"
        if instructions_parts:
            body["instructions"] = "\n\n".join(instructions_parts)
        if max_tokens is not None:
            body["max_output_tokens"] = max_tokens

        # Opt into 24h prompt-cache retention (free, vs the default ~5-10 min).
        # Gated on the OpenAI cloud host because ollama / llama.cpp / "custom"
        # presets reach this path too and would 400 on the unknown field.
        if is_openai_cloud and enable_prompt_caching is not False:
            body["prompt_cache_retention"] = "24h"

        # Server-side context compaction (OpenAI cloud only).
        # https://developers.openai.com/api/docs/guides/compaction
        if is_openai_cloud and compaction_threshold is not None and compaction_threshold > 0:
            body["context_management"] = [
                {
                    "type": "compaction",
                    "compact_threshold": int(compaction_threshold),
                }
            ]

        # Map enabled_tools onto Responses-API server tools (cloud only;
        # local OAI-compat backends 400 on these).
        # https://developers.openai.com/api/docs/guides/tools
        code_execution_enabled_openai = bool(
            enabled_tools and "code_execution" in enabled_tools and is_openai_cloud
        )
        image_generation_enabled_openai = bool(
            enabled_tools and "image_generation" in enabled_tools and is_openai_cloud
        )

        def _openai_image_generation_tool() -> dict[str, Any]:
            tool: dict[str, Any] = {"type": "image_generation"}
            if image_generation_has_reference:
                # Force edit mode so the prior call id is used as context.
                tool["action"] = "edit"
            return tool

        # Translate Chat-Completions function tools into the Responses
        # function-tool shape (flat name/description/parameters).
        responses_user_function_tools: list[dict[str, Any]] = []
        if tools:
            for _tool in tools:
                if not isinstance(_tool, dict) or _tool.get("type") != "function":
                    continue
                _fn = _tool.get("function")
                if not isinstance(_fn, dict) or not _fn.get("name"):
                    continue
                _entry: dict[str, Any] = {
                    "type": "function",
                    "name": _fn["name"],
                }
                if _fn.get("description"):
                    _entry["description"] = _fn["description"]
                if isinstance(_fn.get("parameters"), dict):
                    _entry["parameters"] = _fn["parameters"]
                responses_user_function_tools.append(_entry)

        # Translate tool_choice into the Responses shape.
        _responses_tc_string: Optional[str] = None
        if isinstance(tool_choice, str):
            _tc_lc = tool_choice.strip().lower()
            if _tc_lc in ("auto", "none", "required"):
                _responses_tc_string = _tc_lc
        responses_tool_choice: Optional[Any] = None
        _has_responses_tools = bool(enabled_tools or responses_user_function_tools)
        if _responses_tc_string is not None and _has_responses_tools:
            responses_tool_choice = _responses_tc_string
        elif (
            tool_choice is not None
            and responses_user_function_tools
            and isinstance(tool_choice, dict)
            and tool_choice.get("type") == "function"
        ):
            _fn_pick = tool_choice.get("function") or {}
            _name = _fn_pick.get("name") if isinstance(_fn_pick, dict) else None
            if isinstance(_name, str) and _name:
                responses_tool_choice = {"type": "function", "name": _name}

        _responses_tool_choice_none = _responses_tc_string == "none"
        # A pinned user function suppresses hosted builtins (privacy +
        # billing), matching the Gemini / Anthropic / OpenRouter gates.
        _responses_tool_choice_forced_function = (
            isinstance(tool_choice, dict)
            and tool_choice.get("type") == "function"
            and isinstance(tool_choice.get("function"), dict)
            and bool(tool_choice["function"].get("name"))
        )
        _responses_hosted_builtins_allowed = (
            not _responses_tool_choice_none and not _responses_tool_choice_forced_function
        )

        if (enabled_tools or responses_user_function_tools) and not _responses_tool_choice_none:
            tools_array: list[dict[str, Any]] = list(responses_user_function_tools)
            if (
                _responses_hosted_builtins_allowed
                and enabled_tools
                and "web_search" in enabled_tools
            ):
                tools_array.append({"type": "web_search"})
            if _responses_hosted_builtins_allowed and code_execution_enabled_openai:
                # Reuse the thread's container so filesystem state persists;
                # auto-create when there isn't one yet. Stale ids 400 and are
                # cleared via container_invalidated.
                shell_env: dict[str, Any]
                if openai_code_exec_container_id:
                    shell_env = {
                        "type": "container_reference",
                        "container_id": openai_code_exec_container_id,
                    }
                else:
                    shell_env = {"type": "container_auto"}
                tools_array.append({"type": "shell", "environment": shell_env})
            if _responses_hosted_builtins_allowed and image_generation_enabled_openai:
                tools_array.append(_openai_image_generation_tool())
            if tools_array:
                body["tools"] = tools_array
        if responses_tool_choice is not None:
            body["tool_choice"] = responses_tool_choice

        url = f"{self.base_url}/responses"
        completion_id = f"chatcmpl-openai-{model.replace('/', '-')}"

        logger.info("Proxying OpenAI Responses API to %s (model=%s)", url, model)

        def _build_body(container_id_for_this_attempt: Optional[str]) -> dict[str, Any]:
            """Snapshot of the request body. Called once for the initial attempt
            and again with ``None`` for the post-expiry retry. Returns a fresh
            dict so the retry doesn't share state with the first attempt.
            """
            attempt_body = dict(body)
            if (enabled_tools or responses_user_function_tools) and not _responses_tool_choice_none:
                tools_array_attempt: list[dict[str, Any]] = list(responses_user_function_tools)
                if (
                    _responses_hosted_builtins_allowed
                    and enabled_tools
                    and "web_search" in enabled_tools
                ):
                    tools_array_attempt.append({"type": "web_search"})
                if _responses_hosted_builtins_allowed and code_execution_enabled_openai:
                    if container_id_for_this_attempt:
                        env_attempt: dict[str, Any] = {
                            "type": "container_reference",
                            "container_id": container_id_for_this_attempt,
                        }
                    else:
                        env_attempt = {"type": "container_auto"}
                    tools_array_attempt.append({"type": "shell", "environment": env_attempt})
                if _responses_hosted_builtins_allowed and image_generation_enabled_openai:
                    tools_array_attempt.append(_openai_image_generation_tool())
                if tools_array_attempt:
                    attempt_body["tools"] = tools_array_attempt
                else:
                    attempt_body.pop("tools", None)
            if responses_tool_choice is not None:
                attempt_body["tool_choice"] = responses_tool_choice
            return attempt_body

        def _is_openai_container_expired_error(error_text: str) -> bool:
            """Substring-match OpenAI's expired/missing code-exec container errors
            (no official error code exists)."""
            lowered = error_text.lower()
            if "container" not in lowered:
                return False
            return (
                "expired" in lowered
                or "not_found" in lowered
                or "not found" in lowered
                or "no such container" in lowered
            )

        try:
            retried = False
            attempt_container_id = openai_code_exec_container_id
            while True:
                attempt_body = _build_body(attempt_container_id)
                async with _http_client.stream(
                    "POST",
                    url,
                    json = attempt_body,
                    headers = self._auth_headers(),
                    timeout = self._stream_timeout,
                ) as response:
                    if response.status_code != 200:
                        error_body = await response.aread()
                        error_text = error_body.decode("utf-8", errors = "replace")
                        logger.error(
                            "OpenAI Responses returned %d: %s",
                            response.status_code,
                            error_text[:500],
                        )
                        expired_container_4xx = (
                            attempt_container_id
                            and 400 <= response.status_code < 500
                            and _is_openai_container_expired_error(error_text)
                        )
                        if expired_container_4xx and not retried:
                            yield (
                                f"data: "
                                f"{_json.dumps({'id': completion_id, 'object': 'chat.completion.chunk', 'choices': [{'index': 0, 'delta': {}, 'finish_reason': None}], '_toolEvent': {'type': 'container_invalidated'}})}"
                            )
                            retried = True
                            attempt_container_id = None
                            continue
                        yield _error_sse_line(response.status_code, error_text, self.provider_type)
                        return

                    # NOTE: same manual __anext__ loop as stream_chat_completion —
                    # see comment there for the GeneratorExit / aclose ordering.
                    lines_gen = response.aiter_lines().__aiter__()
                    done_emitted = False
                    reasoning_open = False
                    reasoning_emitted = False
                    # Per-call function-tool indexing; distinct slots so
                    # parallel calls don't collide on delta.tool_calls[].index.
                    saw_function_call = False
                    function_call_index = 0
                    # Latched from response.completed/incomplete; surfaces
                    # input_tokens_details.cached_tokens to prove cache hits.
                    last_usage: Optional[dict[str, Any]] = None
                    # web_search state. Citations are emitted on text deltas (not
                    # per call), so the aggregate list is shared and applied to
                    # the LAST web_search tool_end (parseSourcesFromResult
                    # flatmaps every call, one non-empty is enough).
                    web_search_calls: dict[str, dict[str, Any]] = {}
                    all_url_citations: list[dict[str, Any]] = []
                    # shell_calls (code execution): {call_id -> {commands, output}}.
                    # shell_call <-> shell_call_output match by call_id; emit
                    # tool_start/tool_end like the Anthropic UX.
                    shell_calls: dict[str, dict[str, Any]] = {}
                    # Container id latched from response.container_id or
                    # item.environment.container_id; emit container_ready when it
                    # differs from the inbound id.
                    latched_container_id: Optional[str] = None
                    container_id_emitted = False
                    current_openai_response_id: Optional[str] = None
                    last_openai_reasoning_replay_item: Optional[dict[str, Any]] = None
                    openai_reasoning_replay_items: dict[str, dict[str, Any]] = {}
                    image_generation_calls_started: set[str] = set()
                    # Buffer for a citation marker straddling two delta events;
                    # prepended onto the next delta. See _split_pending_citation_tail.
                    pending_marker_tail: str = ""
                    # Segments deferred while their markers reference unseen
                    # source_ids; held in arrival order so output never leapfrogs
                    # an earlier deferred segment. Flushed on annotation events
                    # and force-flushed at end-of-stream with leftover
                    # private-use codepoints stripped.
                    pending_citation_segments: list[str] = []

                    def _record_openai_response_id(payload: dict[str, Any]) -> None:
                        nonlocal current_openai_response_id
                        response_obj = payload.get("response")
                        candidates: list[Any] = []
                        if isinstance(response_obj, dict):
                            candidates.append(response_obj.get("id"))
                        candidates.append(payload.get("response_id"))
                        for candidate in candidates:
                            if isinstance(candidate, str) and candidate:
                                current_openai_response_id = candidate
                                return

                    def _drain_pending_segments(force: bool) -> str:
                        """Re-attempt resolution on buffered segments in order.
                        Stops at the first still-unresolved segment unless
                        ``force`` (end-of-stream), where lingering markers drop."""
                        out: list[str] = []
                        while pending_citation_segments:
                            seg = pending_citation_segments[0]
                            rewritten, unresolved = _rewrite_citation_markers_partial(
                                seg,
                                all_url_citations,
                            )
                            if unresolved and not force:
                                pending_citation_segments[0] = rewritten
                                break
                            if unresolved and force:
                                rewritten = _replace_openai_citation_markers(
                                    rewritten,
                                    all_url_citations,
                                )
                            pending_citation_segments.pop(0)
                            if rewritten:
                                out.append(rewritten)
                        return "".join(out)

                    def _flush_pending_marker_tail(tail: str) -> str:
                        """Render any leftover citation tail at end-of-stream.

                        Unterminated tails drop (no annotation to bind to). If the
                        close byte arrived concatenated, rewrite then scrub any
                        residual private-use bytes and any orphan ``cite<sid>``
                        literal so the renderer never sees raw markup. url_citations
                        are aggregated separately and applied to web_search tool_end.
                        """
                        if not tail:
                            return ""
                        if _OPENAI_CITE_STOP not in tail:
                            # Unterminated: drop the whole tail, else the residual
                            # ``cite<sid>`` would leak as plain text.
                            return ""
                        rendered = _replace_openai_citation_markers(tail, all_url_citations)
                        # Scrub residual private-use bytes (e.g. a partial opener).
                        for ch in ("", "", ""):
                            rendered = rendered.replace(ch, "")
                        # Drop any orphan ``cite<sid>`` literal -- meaningless
                        # without its closing byte and matching url_citation.
                        rendered = re.sub(r"^cite\S*", "", rendered)
                        return rendered

                    def _emit_tool_event(payload: dict[str, Any]) -> str:
                        _stamp_server_tool_marker(payload)
                        chunk = {
                            "id": completion_id,
                            "object": "chat.completion.chunk",
                            "choices": [
                                {
                                    "index": 0,
                                    "delta": {},
                                    "finish_reason": None,
                                }
                            ],
                            "_toolEvent": payload,
                        }
                        return f"data: {_json.dumps(chunk)}"

                    def _format_shell_output(output: Any) -> str:
                        """Render `shell_call_output.output` (stdout/stderr/outcome
                        per entry) as the preformatted text CodeExecutionToolUI
                        shows; append return_code/(timeout) only when informative."""
                        if not isinstance(output, list):
                            return ""
                        parts: list[str] = []
                        for entry in output:
                            if not isinstance(entry, dict):
                                continue
                            stdout = entry.get("stdout") or ""
                            stderr = entry.get("stderr") or ""
                            outcome = entry.get("outcome") or {}
                            chunk_parts: list[str] = []
                            if stdout:
                                chunk_parts.append(stdout)
                            if stderr:
                                chunk_parts.append(f"--- stderr ---\n{stderr}")
                            if isinstance(outcome, dict):
                                outcome_type = outcome.get("type")
                                if outcome_type == "exit":
                                    exit_code = outcome.get("exit_code")
                                    if isinstance(exit_code, int) and exit_code != 0:
                                        chunk_parts.append(f"return_code: {exit_code}")
                                elif outcome_type == "timeout":
                                    chunk_parts.append("(timeout)")
                            if chunk_parts:
                                parts.append("\n".join(chunk_parts))
                        return "\n--- next command ---\n".join(parts) if parts else "(no output)"

                    def _record_url_citation(payload: dict[str, Any]) -> None:
                        """Append a url_citation, deduped by URL: collect every
                        source_id alias onto the entry's ``source_ids`` so the
                        rewriter can resolve any alias. The id lives under
                        source_id/id/locator across API revisions."""
                        if payload.get("type") != "url_citation":
                            return
                        url = payload.get("url", "")
                        if not url:
                            return
                        source_id = (
                            payload.get("source_id")
                            or payload.get("id")
                            or payload.get("locator")
                            or ""
                        )
                        # Single pass: either backfill aliases onto an existing
                        # URL entry (and return) or fall through to append a
                        # fresh one.
                        for c in all_url_citations:
                            if c["url"] != url:
                                continue
                            if source_id:
                                aliases = c.setdefault("source_ids", [])
                                if source_id not in aliases:
                                    aliases.append(source_id)
                            return
                        title = payload.get("title") or url
                        snippet = payload.get("snippet") or payload.get("quote") or ""
                        all_url_citations.append(
                            {
                                "url": url,
                                "title": title,
                                "snippet": snippet,
                                "source_ids": [source_id] if source_id else [],
                            }
                        )

                    def _record_openai_reasoning_replay_item(
                        payload: Any,
                    ) -> Optional[dict[str, Any]]:
                        if not isinstance(payload, dict):
                            return None
                        item_id = payload.get("id") or payload.get("item_id")
                        if not isinstance(item_id, str) or not item_id:
                            return None
                        existing = openai_reasoning_replay_items.setdefault(
                            item_id,
                            {
                                "type": "reasoning",
                                "id": item_id,
                                "summary": [],
                                "status": "completed",
                            },
                        )
                        if payload.get("type") == "reasoning":
                            sanitized = _sanitize_openai_reasoning_replay_item(payload)
                            if sanitized:
                                existing.update(sanitized)
                                return existing
                        summary_text = ""
                        part = payload.get("part")
                        if isinstance(part, dict) and part.get("type") == "summary_text":
                            text = part.get("text")
                            if isinstance(text, str):
                                summary_text = text
                        elif payload.get("type") == "response.reasoning_summary_text.done":
                            text = payload.get("text")
                            if isinstance(text, str):
                                summary_text = text
                        if summary_text:
                            summary_index = payload.get("summary_index")
                            summary = existing.setdefault("summary", [])
                            if isinstance(summary, list):
                                summary_part = {
                                    "type": "summary_text",
                                    "text": summary_text,
                                }
                                if isinstance(summary_index, int) and summary_index >= 0:
                                    while len(summary) <= summary_index:
                                        summary.append({"type": "summary_text", "text": ""})
                                    summary[summary_index] = summary_part
                                else:
                                    summary.append(summary_part)
                        return existing

                    def _image_generation_arguments(
                        prompt: str, raw_item_id: Any
                    ) -> dict[str, Any]:
                        arguments: dict[str, Any] = {"kind": "image", "prompt": prompt}
                        if isinstance(raw_item_id, str) and raw_item_id:
                            arguments["openai_image_generation_call_id"] = raw_item_id
                        if current_openai_response_id:
                            arguments["openai_response_id"] = current_openai_response_id
                        if last_openai_reasoning_replay_item:
                            arguments["openai_reasoning_item"] = last_openai_reasoning_replay_item
                        return arguments

                    def _extract_reasoning_text(payload: Any) -> str:
                        if payload is None:
                            return ""
                        if isinstance(payload, str):
                            return payload
                        if isinstance(payload, list):
                            out: list[str] = []
                            for item in payload:
                                text = _extract_reasoning_text(item)
                                if text:
                                    out.append(text)
                            return "".join(out)
                        if isinstance(payload, dict):
                            # OpenAI responses carry reasoning summaries in
                            # different envelope fields across event variants.
                            for key in ("text", "delta", "content", "summary"):
                                if key in payload:
                                    text = _extract_reasoning_text(payload.get(key))
                                    if text:
                                        return text
                            if payload.get("type") == "summary_text":
                                return _extract_reasoning_text(payload.get("text"))
                        return ""

                    def _chunk_with_text(text: str) -> str:
                        chunk = {
                            "id": completion_id,
                            "object": "chat.completion.chunk",
                            "choices": [
                                {
                                    "index": 0,
                                    "delta": {"content": text},
                                    "finish_reason": None,
                                }
                            ],
                        }
                        return f"data: {_json.dumps(chunk)}"

                    try:
                        while True:
                            try:
                                line = await lines_gen.__anext__()
                            except StopAsyncIteration:
                                break
                            if not line or line.startswith("event:"):
                                continue
                            if not line.startswith("data:"):
                                continue

                            data_str = line[len("data:") :].strip()
                            if not data_str:
                                continue
                            if data_str == "[DONE]":
                                # Flush any held-over partial marker; strip
                                # private-use bytes so garbled glyphs don't leak.
                                if pending_marker_tail:
                                    flushed = _flush_pending_marker_tail(pending_marker_tail)
                                    pending_marker_tail = ""
                                    if flushed:
                                        if reasoning_open:
                                            yield _chunk_with_text("</think>")
                                            reasoning_open = False
                                        yield _chunk_with_text(flushed)
                                # Force-drain any segment still awaiting an
                                # annotation; lingering codepoints drop.
                                tail_flushed = _drain_pending_segments(
                                    force = True,
                                )
                                if tail_flushed:
                                    if reasoning_open:
                                        yield _chunk_with_text("</think>")
                                        reasoning_open = False
                                    yield _chunk_with_text(tail_flushed)
                                if not done_emitted:
                                    yield "data: [DONE]"
                                    done_emitted = True
                                break

                            try:
                                event = _json.loads(data_str)
                            except _json.JSONDecodeError:
                                continue

                            event_type = event.get("type")
                            _record_openai_response_id(event)

                            if event_type == "response.output_text.delta":
                                delta_text = event.get("delta", "")
                                # Process inline annotations first so source_ids
                                # referenced by same-delta markers are in the
                                # lookup before the rewriter runs. Some API
                                # versions inline url citations on the delta event.
                                for ann in event.get("annotations") or []:
                                    if isinstance(ann, dict):
                                        _record_url_citation(ann)
                                if delta_text or pending_marker_tail:
                                    # Prepend any held-over tail so a marker
                                    # straddling two SSE events resolves cleanly.
                                    combined = pending_marker_tail + delta_text
                                    head, pending_marker_tail = _split_pending_citation_tail(
                                        combined
                                    )
                                    if head:
                                        if reasoning_open:
                                            yield _chunk_with_text("</think>")
                                            reasoning_open = False
                                        # Re-attempt earlier deferred segments
                                        # first so output stays in order; the
                                        # needed annotation may have arrived
                                        # inline above.
                                        flushed = _drain_pending_segments(
                                            force = False,
                                        )
                                        if flushed:
                                            yield _chunk_with_text(flushed)
                                        head_rewritten, has_unresolved = (
                                            _rewrite_citation_markers_partial(
                                                head,
                                                all_url_citations,
                                            )
                                        )
                                        if has_unresolved or pending_citation_segments:
                                            pending_citation_segments.append(head_rewritten)
                                        elif head_rewritten:
                                            yield _chunk_with_text(head_rewritten)

                            elif event_type == "response.output_text.annotation.added":
                                ann = event.get("annotation")
                                if isinstance(ann, dict):
                                    _record_url_citation(ann)
                                flushed = _drain_pending_segments(
                                    force = False,
                                )
                                if flushed:
                                    if reasoning_open:
                                        yield _chunk_with_text("</think>")
                                        reasoning_open = False
                                    yield _chunk_with_text(flushed)

                            elif event_type == "response.output_item.added":
                                item = event.get("item", {})
                                if isinstance(item, dict) and item.get("type") == "web_search_call":
                                    item_id = item.get("id", "") or (f"ws_{len(web_search_calls)}")
                                    web_search_calls.setdefault(item_id, {"query": ""})
                                # Register shell_call eagerly so out-of-order
                                # output links back. Probe env.container_id to
                                # emit container_ready before response.completed.
                                if isinstance(item, dict) and item.get("type") == "shell_call":
                                    item_id = item.get("id", "") or (f"sc_{len(shell_calls)}")
                                    shell_calls.setdefault(
                                        item_id,
                                        {"commands": [], "output": None},
                                    )
                                    env = item.get("environment")
                                    if isinstance(env, dict):
                                        probe = env.get("container_id") or env.get("id")
                                        if (
                                            isinstance(probe, str)
                                            and probe.startswith("cntr_")
                                            and latched_container_id is None
                                        ):
                                            latched_container_id = probe
                                if (
                                    isinstance(item, dict)
                                    and item.get("type") == "image_generation_call"
                                ):
                                    raw_item_id = item.get("id")
                                    if isinstance(raw_item_id, str) and raw_item_id:
                                        arguments = _image_generation_arguments(
                                            "",
                                            raw_item_id,
                                        )
                                        image_generation_calls_started.add(raw_item_id)
                                        yield _emit_tool_event(
                                            {
                                                "type": "tool_start",
                                                "tool_name": "image_generation",
                                                "tool_call_id": raw_item_id,
                                                "arguments": arguments,
                                            }
                                        )

                            elif event_type == "response.output_item.done":
                                item = event.get("item", {})
                                if not isinstance(item, dict):
                                    continue
                                if item.get("type") == "reasoning":
                                    last_openai_reasoning_replay_item = (
                                        _record_openai_reasoning_replay_item(item)
                                    )
                                    summary_text = _extract_reasoning_text(item.get("summary"))
                                    if summary_text and not reasoning_emitted:
                                        if not reasoning_open:
                                            summary_text = f"<think>{summary_text}"
                                            reasoning_open = True
                                        yield _chunk_with_text(summary_text)
                                        reasoning_emitted = True
                                elif item.get("type") == "web_search_call":
                                    # done carries the query; emit tool_start +
                                    # tool_end here. Citations are aggregated and
                                    # the last call's result is overwritten at
                                    # response.completed.
                                    item_id = item.get("id", "") or (f"ws_{len(web_search_calls)}")
                                    action = item.get("action")
                                    query = (
                                        action.get("query", "") if isinstance(action, dict) else ""
                                    )
                                    web_search_calls[item_id] = {"query": query}
                                    yield _emit_tool_event(
                                        {
                                            "type": "tool_start",
                                            "tool_name": "web_search",
                                            "tool_call_id": item_id,
                                            "arguments": ({"query": query} if query else {}),
                                        }
                                    )
                                    # Per-card text; last call gets overwritten
                                    # with citations at response.completed.
                                    per_call_result = f"Searching: {query}" if query else ""
                                    yield _emit_tool_event(
                                        {
                                            "type": "tool_end",
                                            "tool_call_id": item_id,
                                            "result": per_call_result,
                                        }
                                    )
                                elif item.get("type") == "shell_call":
                                    # Join the action.commands array into one
                                    # newline-separated string (the card renderer,
                                    # shared with Anthropic bash, wants a single
                                    # `command`).
                                    item_id = item.get("id", "") or (f"sc_{len(shell_calls)}")
                                    action = item.get("action") or {}
                                    commands = (
                                        action.get("commands") if isinstance(action, dict) else None
                                    ) or []
                                    joined_command = (
                                        "\n".join(str(c) for c in commands)
                                        if isinstance(commands, list)
                                        else ""
                                    )
                                    shell_calls.setdefault(
                                        item_id,
                                        {
                                            "commands": [],
                                            "output": None,
                                            "tool_end_emitted": False,
                                        },
                                    )
                                    shell_calls[item_id]["commands"] = (
                                        list(commands) if isinstance(commands, list) else []
                                    )
                                    yield _emit_tool_event(
                                        {
                                            "type": "tool_start",
                                            "tool_name": "code_execution",
                                            "tool_call_id": item_id,
                                            "arguments": {
                                                "kind": "bash",
                                                "command": joined_command,
                                            },
                                        }
                                    )
                                    # Fallback: output may be bundled on the
                                    # shell_call done event itself.
                                    embedded_output = item.get("output")
                                    if isinstance(embedded_output, list) and embedded_output:
                                        shell_calls[item_id]["output"] = embedded_output
                                        shell_calls[item_id]["tool_end_emitted"] = True
                                        yield _emit_tool_event(
                                            {
                                                "type": "tool_end",
                                                "tool_call_id": item_id,
                                                "result": _format_shell_output(embedded_output),
                                            }
                                        )
                                elif item.get("type") == "shell_call_output":
                                    # `call_id` links back to the shell_call's
                                    # `id`, used as the tool_call_id on
                                    # tool_start. Match on call_id when present so
                                    # the matching card transitions to complete.
                                    call_id = item.get("call_id") or item.get("id") or ""
                                    output = item.get("output") or []
                                    # Skip if bundled-output path already
                                    # finalised this card.
                                    if shell_calls.get(call_id, {}).get("tool_end_emitted"):
                                        continue
                                    if call_id in shell_calls:
                                        shell_calls[call_id]["output"] = output
                                        shell_calls[call_id]["tool_end_emitted"] = True
                                    result_text = _format_shell_output(output)
                                    yield _emit_tool_event(
                                        {
                                            "type": "tool_end",
                                            "tool_call_id": call_id,
                                            "result": result_text,
                                        }
                                    )
                                elif item.get("type") == "image_generation_call":
                                    # Base64 image on `result` (or `b64_json`),
                                    # `revised_prompt` for the rewritten prompt.
                                    # ns-resolution id so concurrent gens are unique.
                                    raw_item_id = item.get("id")
                                    item_id = raw_item_id or f"img_{time.time_ns()}"
                                    prompt_in = (
                                        item.get("revised_prompt") or item.get("prompt") or ""
                                    )
                                    done_arguments = _image_generation_arguments(
                                        prompt_in,
                                        raw_item_id,
                                    )
                                    if item_id not in image_generation_calls_started:
                                        yield _emit_tool_event(
                                            {
                                                "type": "tool_start",
                                                "tool_name": "image_generation",
                                                "tool_call_id": item_id,
                                                "arguments": done_arguments,
                                            }
                                        )
                                    b64 = item.get("result") or item.get("b64_json") or ""
                                    output_format = item.get("output_format") or "png"
                                    yield _emit_tool_event(
                                        {
                                            "type": "tool_end",
                                            "tool_call_id": item_id,
                                            "result": "",
                                            "arguments": done_arguments,
                                            "image_b64": b64,
                                            "image_mime": (f"image/{output_format}"),
                                            "size": item.get("size"),
                                            "quality": item.get("quality"),
                                            "background": item.get("background"),
                                            "prompt": prompt_in,
                                        }
                                    )
                                elif item.get("type") == "function_call":
                                    # Translate to Chat-Completions delta.tool_calls.
                                    # https://platform.openai.com/docs/guides/function-calling?api-mode=responses
                                    fn_call_id = (
                                        item.get("call_id")
                                        or item.get("id")
                                        or f"call_{time.time_ns()}"
                                    )
                                    fn_name = item.get("name") or ""
                                    fn_args = item.get("arguments") or ""
                                    if not isinstance(fn_args, str):
                                        try:
                                            fn_args = _json.dumps(fn_args)
                                        except Exception:
                                            fn_args = ""
                                    _tc_index = function_call_index
                                    function_call_index += 1
                                    yield (
                                        "data: "
                                        + _json.dumps(
                                            {
                                                "id": completion_id,
                                                "object": "chat.completion.chunk",
                                                "choices": [
                                                    {
                                                        "index": 0,
                                                        "delta": {
                                                            "tool_calls": [
                                                                {
                                                                    "index": _tc_index,
                                                                    "id": fn_call_id,
                                                                    "type": "function",
                                                                    "function": {
                                                                        "name": fn_name,
                                                                        "arguments": (fn_args),
                                                                    },
                                                                }
                                                            ],
                                                        },
                                                        "finish_reason": None,
                                                    }
                                                ],
                                            }
                                        )
                                    )
                                    saw_function_call = True

                            elif isinstance(event_type, str) and "reasoning" in event_type:
                                recorded_reasoning = _record_openai_reasoning_replay_item(event)
                                if recorded_reasoning:
                                    last_openai_reasoning_replay_item = recorded_reasoning
                                reasoning_delta = _extract_reasoning_text(event)
                                if reasoning_delta:
                                    if not reasoning_open:
                                        reasoning_delta = f"<think>{reasoning_delta}"
                                        reasoning_open = True
                                    yield _chunk_with_text(reasoning_delta)
                                    reasoning_emitted = True

                            elif event_type == "response.completed":
                                completed_usage = (event.get("response") or {}).get("usage")
                                if isinstance(completed_usage, dict):
                                    last_usage = completed_usage
                                # Flush any unterminated citation tail; by now all
                                # annotations are recorded, else private-use bytes
                                # are stripped.
                                if pending_marker_tail:
                                    flushed = _flush_pending_marker_tail(pending_marker_tail)
                                    pending_marker_tail = ""
                                    if flushed:
                                        if reasoning_open:
                                            yield _chunk_with_text("</think>")
                                            reasoning_open = False
                                        yield _chunk_with_text(flushed)
                                # Force-drain segments still awaiting an annotation.
                                tail_flushed = _drain_pending_segments(
                                    force = True,
                                )
                                if tail_flushed:
                                    if reasoning_open:
                                        yield _chunk_with_text("</think>")
                                        reasoning_open = False
                                    yield _chunk_with_text(tail_flushed)
                                if reasoning_open:
                                    yield _chunk_with_text("</think>")
                                    reasoning_open = False
                                # Scan response.container_id and response.container.id
                                # (docs don't pin the field); emit container_ready
                                # only when it differs from the inbound id.
                                response_obj = event.get("response") or {}
                                if isinstance(response_obj, dict):
                                    probe_id = response_obj.get("container_id")
                                    if not probe_id:
                                        container_field = response_obj.get("container")
                                        if isinstance(container_field, dict):
                                            probe_id = container_field.get("id")
                                    if (
                                        isinstance(probe_id, str)
                                        and probe_id.startswith("cntr_")
                                        and latched_container_id is None
                                    ):
                                        latched_container_id = probe_id
                                if (
                                    latched_container_id
                                    and not container_id_emitted
                                    and latched_container_id != openai_code_exec_container_id
                                ):
                                    yield _emit_tool_event(
                                        {
                                            "type": "container_ready",
                                            "container_id": latched_container_id,
                                        }
                                    )
                                    container_id_emitted = True
                                # Overwrite the last web_search card with the
                                # citation list (the extractor flatMaps cards).
                                if web_search_calls and all_url_citations:
                                    last_id = list(web_search_calls.keys())[-1]
                                    blocks: list[str] = []
                                    for cit in all_url_citations:
                                        line = f"Title: {cit['title']}\nURL: {cit['url']}"
                                        if cit.get("snippet"):
                                            line += f"\nSnippet: {cit['snippet']}"
                                        blocks.append(line)
                                    yield _emit_tool_event(
                                        {
                                            "type": "tool_end",
                                            "tool_call_id": last_id,
                                            "result": "\n---\n".join(blocks),
                                        }
                                    )
                                # Final flush: finalise any orphan shell_call
                                # so the card stops spinning.
                                for sc_id, sc_state in shell_calls.items():
                                    if sc_state.get("tool_end_emitted"):
                                        continue
                                    yield _emit_tool_event(
                                        {
                                            "type": "tool_end",
                                            "tool_call_id": sc_id,
                                            "result": _format_shell_output(
                                                sc_state.get("output") or []
                                            ),
                                        }
                                    )
                                    sc_state["tool_end_emitted"] = True
                                chunk = {
                                    "id": completion_id,
                                    "object": "chat.completion.chunk",
                                    "choices": [
                                        {
                                            "index": 0,
                                            "delta": {},
                                            "finish_reason": (
                                                "tool_calls" if saw_function_call else "stop"
                                            ),
                                        }
                                    ],
                                }
                                yield f"data: {_json.dumps(chunk)}"
                                # Emit include_usage-style chunk after the
                                # finish_reason so callers can surface
                                # cached_tokens in their UI.
                                usage_line = _build_usage_chunk(
                                    completion_id,
                                    "openai",
                                    last_usage,
                                )
                                if usage_line:
                                    yield usage_line

                            elif event_type == "response.incomplete":
                                incomplete_usage = (event.get("response") or {}).get("usage")
                                if isinstance(incomplete_usage, dict):
                                    last_usage = incomplete_usage
                                # Same flush as response.completed -- truncated
                                # streams can leave a half-marker in the buffer.
                                if pending_marker_tail:
                                    flushed = _flush_pending_marker_tail(pending_marker_tail)
                                    pending_marker_tail = ""
                                    if flushed:
                                        if reasoning_open:
                                            yield _chunk_with_text("</think>")
                                            reasoning_open = False
                                        yield _chunk_with_text(flushed)
                                # Force-drain any segment still awaiting an
                                # annotation; lingering codepoints drop.
                                tail_flushed = _drain_pending_segments(
                                    force = True,
                                )
                                if tail_flushed:
                                    if reasoning_open:
                                        yield _chunk_with_text("</think>")
                                        reasoning_open = False
                                    yield _chunk_with_text(tail_flushed)
                                if reasoning_open:
                                    yield _chunk_with_text("</think>")
                                    reasoning_open = False
                                # Same citation backfill as response.completed.
                                if web_search_calls and all_url_citations:
                                    last_id = list(web_search_calls.keys())[-1]
                                    blocks = []
                                    for cit in all_url_citations:
                                        line = f"Title: {cit['title']}\nURL: {cit['url']}"
                                        if cit.get("snippet"):
                                            line += f"\nSnippet: {cit['snippet']}"
                                        blocks.append(line)
                                    yield _emit_tool_event(
                                        {
                                            "type": "tool_end",
                                            "tool_call_id": last_id,
                                            "result": "\n---\n".join(blocks),
                                        }
                                    )
                                # Mirror the response.completed flush so
                                # truncated streams also finalise orphan
                                # shell_calls.
                                for sc_id, sc_state in shell_calls.items():
                                    if sc_state.get("tool_end_emitted"):
                                        continue
                                    yield _emit_tool_event(
                                        {
                                            "type": "tool_end",
                                            "tool_call_id": sc_id,
                                            "result": _format_shell_output(
                                                sc_state.get("output") or []
                                            ),
                                        }
                                    )
                                    sc_state["tool_end_emitted"] = True
                                chunk = {
                                    "id": completion_id,
                                    "object": "chat.completion.chunk",
                                    "choices": [
                                        {
                                            "index": 0,
                                            "delta": {},
                                            "finish_reason": "length",
                                        }
                                    ],
                                }
                                yield f"data: {_json.dumps(chunk)}"
                                # Emit include_usage-style chunk after the
                                # length-truncated finish_reason too, so
                                # incomplete responses still report cached_tokens.
                                usage_line = _build_usage_chunk(
                                    completion_id,
                                    "openai",
                                    last_usage,
                                )
                                if usage_line:
                                    yield usage_line

                            elif event_type in ("response.failed", "error"):
                                # Surface the failure to the client; the outer
                                # route emits [DONE] as part of its cleanup.
                                error_payload = event.get("response", {}).get("error", {}) or {
                                    "message": event.get("message", "Unknown error"),
                                    "code": event.get("code"),
                                }
                                yield _error_sse_line(
                                    502,
                                    _json.dumps(error_payload),
                                    self.provider_type,
                                )
                                break
                    except GeneratorExit:
                        await response.aclose()
                        await lines_gen.aclose()
                        raise
                    finally:
                        # Per-turn tool summary for triage.
                        web_search_requested = bool(enabled_tools and "web_search" in enabled_tools)
                        web_search_invocations = len(web_search_calls)
                        total_citations = len(all_url_citations)
                        queries = [
                            sc["query"] for sc in web_search_calls.values() if sc.get("query")
                        ]
                        # On /v1/responses cached tokens live at
                        # usage.input_tokens_details.cached_tokens (not
                        # prompt_tokens_details, the chat/completions shape).
                        cached_input_tokens = None
                        if isinstance(last_usage, dict):
                            details = last_usage.get("input_tokens_details")
                            if isinstance(details, dict):
                                cached_input_tokens = details.get("cached_tokens")
                        code_execution_requested = code_execution_enabled_openai
                        code_execution_invocations = len(shell_calls)
                        code_execution_results = sum(
                            1 for sc in shell_calls.values() if sc.get("output") is not None
                        )
                        logger.info(
                            "OpenAI Responses stream complete (model=%s, "
                            "web_search_requested=%s, web_search_invocations=%s, "
                            "citations=%s, queries=%s, reasoning_emitted=%s, "
                            "code_execution_requested=%s, "
                            "code_execution_invocations=%s, "
                            "code_execution_results=%s, "
                            "container_id_in=%s, container_id_out=%s, "
                            "input_tokens=%s, output_tokens=%s, "
                            "cached_input_tokens=%s)",
                            model,
                            web_search_requested,
                            web_search_invocations,
                            total_citations,
                            queries,
                            reasoning_emitted,
                            code_execution_requested,
                            code_execution_invocations,
                            code_execution_results,
                            openai_code_exec_container_id,
                            latched_container_id,
                            (last_usage or {}).get("input_tokens"),
                            (last_usage or {}).get("output_tokens"),
                            cached_input_tokens,
                        )
                        await response.aclose()
                        await lines_gen.aclose()
                    return

        except httpx.ConnectError as exc:
            logger.error("Connection error to %s: %s", self.provider_type, exc)
            yield _error_sse_line(
                502,
                f"Failed to connect to {self.provider_type}: {exc}",
                self.provider_type,
            )
        except httpx.ReadTimeout as exc:
            logger.error("Read timeout from %s: %s", self.provider_type, exc)
            yield _error_sse_line(
                504,
                f"Timeout waiting for {self.provider_type} response",
                self.provider_type,
            )
        except httpx.HTTPError as exc:
            logger.error("HTTP error from %s: %s", self.provider_type, exc)
            yield _error_sse_line(
                502,
                f"Error communicating with {self.provider_type}: {exc}",
                self.provider_type,
            )

    async def chat_completion(
        self,
        messages: list[dict[str, Any]],
        model: str,
        temperature: float = 0.7,
        top_p: float = 0.95,
        max_tokens: Optional[int] = None,
        presence_penalty: float = 0.0,
    ) -> dict[str, Any]:
        """Non-streaming chat completion. Returns the full response dict.

        Only valid for OpenAI-compatible providers. Anthropic requires its own
        Messages API; use stream_chat_completion (with stream=False) if a
        non-streaming Anthropic path is needed later.
        """
        body: dict[str, Any] = {
            "model": model,
            "messages": messages,
            "stream": False,
            "temperature": temperature,
            "top_p": top_p,
            "presence_penalty": presence_penalty,
        }
        if max_tokens is not None:
            if self.provider_type == "openai":
                body["max_completion_tokens"] = max_tokens
            else:
                body["max_tokens"] = max_tokens

        response = await _http_client.post(
            f"{self.base_url}/chat/completions",
            json = body,
            headers = self._auth_headers(),
            timeout = self._timeout,
        )
        response.raise_for_status()
        return response.json()

    async def list_models(self) -> list[dict[str, Any]]:
        """GET /models to discover available models.

        Returns dicts with at least 'id'. All providers expose /models with the
        OpenAI {"data": [...]} shape (Anthropic included:
        https://api.anthropic.com/v1/models).
        """
        try:
            response = await _http_client.get(
                f"{self.base_url}/models",
                headers = self._auth_headers(),
                timeout = self._timeout,
            )
            response.raise_for_status()
            data = response.json()
            # Some local servers (Ollama with no models) return data: null.
            models: list[dict[str, Any]] = []
            if isinstance(data, dict):
                raw_models = data.get("data") or []
                if isinstance(raw_models, list):
                    models = [model for model in raw_models if isinstance(model, dict)]
            if not models and self.provider_type == "ollama":
                models = await self._list_ollama_native_models()
            # Gemini's native /v1beta/models uses a different shape; repackage
            # into the OpenAI-compatible one Studio expects.
            if not models and self.provider_type == "gemini":
                models = self._parse_gemini_models(data)
            return models
        except httpx.HTTPError as exc:
            logger.error("Failed to list models from %s: %s", self.provider_type, exc)
            raise

    @staticmethod
    def _parse_gemini_models(payload: Any) -> list[dict[str, Any]]:
        """Translate Gemini's native /v1beta/models payload to OpenAI shape,
        keeping only entries advertising generateContent / streamGenerateContent
        so embedding-only models don't reach the chat picker.
        """
        if not isinstance(payload, dict):
            return []
        entries = payload.get("models") or []
        if not isinstance(entries, list):
            return []
        out: list[dict[str, Any]] = []
        for entry in entries:
            if not isinstance(entry, dict):
                continue
            methods = entry.get("supportedGenerationMethods") or []
            if (
                isinstance(methods, list)
                and methods
                and not any(m in methods for m in ("generateContent", "streamGenerateContent"))
            ):
                continue
            base_id = entry.get("baseModelId")
            name = entry.get("name") or ""
            # ``name`` arrives as ``"models/gemini-2.5-flash"``; the chat path
            # uses the bare id.
            short_id = (
                base_id
                if isinstance(base_id, str) and base_id
                else (name.split("/", 1)[1] if "/" in name else name)
            )
            if not short_id:
                continue
            out.append(
                {
                    "id": short_id,
                    "owned_by": "google",
                    "display_name": entry.get("displayName") or short_id,
                }
            )
        return out

    async def _list_ollama_native_models(self) -> list[dict[str, Any]]:
        """Fallback when Ollama's /v1/models returns an empty or null catalog."""
        root = self.base_url.removesuffix("/v1").rstrip("/")
        response = await _http_client.get(
            f"{root}/api/tags",
            headers = self._auth_headers(),
            timeout = self._timeout,
        )
        response.raise_for_status()
        payload = response.json()
        if not isinstance(payload, dict):
            return []
        raw_models = payload.get("models") or []
        if not isinstance(raw_models, list):
            return []
        return [
            {"id": entry.get("name", "").strip(), "owned_by": "ollama"}
            for entry in raw_models
            if isinstance(entry, dict) and entry.get("name", "").strip()
        ]

    async def verify_models_endpoint_lightweight(self) -> None:
        """
        Confirm GET /models returns 200 without buffering the full response body.

        Used for providers with enormous catalogs (e.g. OpenRouter, Hugging Face
        router) where downloading the full JSON would be prohibitive.
        """
        url = f"{self.base_url}/models"
        try:
            async with _http_client.stream(
                "GET",
                url,
                headers = self._auth_headers(),
                timeout = self._timeout,
            ) as response:
                if response.status_code != 200:
                    response.raise_for_status()
                async for _chunk in response.aiter_bytes(chunk_size = 2048):
                    break
        except httpx.HTTPError as exc:
            logger.error(
                "Lightweight /models check failed for %s: %s",
                self.provider_type,
                exc,
            )
            raise

    def _container_headers(self) -> dict[str, str]:
        """Auth headers plus the required ``OpenAI-Beta: containers=v1`` opt-in;
        without it DELETE silently no-ops (returns deleted:true but keeps the
        container, verified 2026-05-15)."""
        headers = self._auth_headers()
        headers["OpenAI-Beta"] = "containers=v1"
        return headers

    async def list_openai_containers(self) -> list[dict[str, Any]]:
        """GET /v1/containers; returns raw container records (the route reshapes
        them). Only valid against api.openai.com (caller guards is_openai_cloud).
        """
        response = await _http_client.get(
            f"{self.base_url}/containers",
            headers = self._container_headers(),
            timeout = self._timeout,
        )
        response.raise_for_status()
        data = response.json()
        containers = data.get("data") if isinstance(data, dict) else None
        result = list(containers) if isinstance(containers, list) else []
        logger.info(
            "openai_container_list.response count=%s items=%s",
            len(result),
            [{"id": c.get("id"), "status": c.get("status")} for c in result if isinstance(c, dict)],
        )
        return result

    async def create_openai_container(self, name: str, ttl_minutes: int) -> dict[str, Any]:
        """
        POST /v1/containers with ``expires_after.anchor="last_active_at"``.
        ``ttl_minutes`` is the idle timeout — every API call touching the
        container resets the timer.
        """
        body = {
            "name": name,
            "expires_after": {
                "anchor": "last_active_at",
                "minutes": ttl_minutes,
            },
        }
        response = await _http_client.post(
            f"{self.base_url}/containers",
            json = body,
            headers = self._container_headers(),
            timeout = self._timeout,
        )
        response.raise_for_status()
        return response.json()

    async def delete_openai_container(self, container_id: str) -> None:
        """DELETE /v1/containers/{id}. 404s surface as HTTPError.

        Uses a fresh httpx client (shared-pool DELETEs returned deleted:true but
        left the container alive). Also verifies the body reports deleted:true,
        since OpenAI 2xx-returns that even when silently rejecting the request.
        """
        url = f"{self.base_url}/containers/{container_id}"
        headers = self._container_headers()
        logger.info(
            "openai_container_delete.outbound url=%s has_auth=%s openai_beta=%s",
            url,
            "Authorization" in headers,
            headers.get("OpenAI-Beta"),
        )
        async with httpx.AsyncClient(timeout = self._timeout) as fresh_client:
            response = await fresh_client.delete(url, headers = headers)
        logger.info(
            "openai_container_delete.response status=%s cf_ray=%s "
            "request_id=%s organization=%s project=%s processing_ms=%s body=%s",
            response.status_code,
            response.headers.get("cf-ray"),
            response.headers.get("x-request-id"),
            response.headers.get("openai-organization"),
            response.headers.get("openai-project"),
            response.headers.get("openai-processing-ms"),
            response.text[:300],
        )
        response.raise_for_status()
        try:
            payload = response.json()
        except ValueError:
            payload = None
        if not (isinstance(payload, dict) and payload.get("deleted") is True):
            raise httpx.HTTPError(
                f"OpenAI did not confirm container deletion: {response.text[:200]}"
            )

    async def close(self) -> None:
        """No-op — the underlying client is shared across requests."""


def _provider_display_name(provider_type: str) -> str:
    from core.inference.providers import get_provider_info
    info = get_provider_info(provider_type) or {}
    return str(info.get("display_name") or provider_type)


def _friendly_provider_error_text(
    provider_type: str,
    status_code: int,
    raw_message: str,
    *,
    model: str | None = None,
) -> str:
    """Rewrite common provider errors into actionable Studio copy."""
    if status_code == 404 and model:
        lowered = raw_message.lower()
        if "not found" in lowered or "not_found" in lowered:
            if provider_type == "ollama":
                label = _provider_display_name(provider_type)
                return (
                    f"Model '{model}' is not installed in {label}. "
                    f"Run `ollama pull {model}` in a terminal, then retry."
                )
            if provider_type in ("vllm", "llama_cpp"):
                label = _provider_display_name(provider_type)
                return (
                    f"Model '{model}' is not available on the {label} server. "
                    "Check that the server is running and the model is loaded, "
                    "then retry."
                )
    return raw_message


def _error_sse_line(status_code: int, message: str, provider_type: str) -> str:
    """Format an error as an SSE data line in OpenAI error format."""
    import json

    error_obj = {
        "error": {
            "message": message,
            "type": "provider_error",
            "code": str(status_code),
            "provider": provider_type,
        }
    }
    return f"data: {json.dumps(error_obj)}"


def _build_usage_chunk(
    completion_id: str, provider: Literal["anthropic", "openai"], last_usage: Optional[dict]
) -> Optional[str]:
    """Build an OpenAI ``include_usage``-style SSE chunk carrying upstream
    prompt-cache accounting back to the client.

    Emits the standard chunk shape (``choices: []`` + ``usage`` block) so
    ``stream_options={"include_usage": true}`` clients keep working, plus the
    Anthropic-native counts as extra keys:
        usage.prompt_tokens_details.cached_tokens  (both providers)
        usage.cache_creation_input_tokens          (Anthropic-only)
        usage.cache_read_input_tokens              (Anthropic-only)

    Anthropic's ``input_tokens`` excludes the cache buckets, so prompt_tokens
    sums all three (OpenAI Responses already folds cached tokens in).

    Returns ``None`` when there are no usage numbers to report.
    """
    if not isinstance(last_usage, dict):
        return None

    completion_tokens = last_usage.get("output_tokens") or 0

    if provider == "anthropic":
        uncached_input = last_usage.get("input_tokens") or 0
        cache_creation = last_usage.get("cache_creation_input_tokens") or 0
        cache_read = last_usage.get("cache_read_input_tokens") or 0
        prompt_tokens = uncached_input + cache_creation + cache_read
        if not (prompt_tokens or completion_tokens):
            return None
        usage_block: dict[str, Any] = {
            "prompt_tokens": prompt_tokens,
            "completion_tokens": completion_tokens,
            "total_tokens": prompt_tokens + completion_tokens,
            "prompt_tokens_details": {"cached_tokens": cache_read},
            "cache_creation_input_tokens": cache_creation,
            "cache_read_input_tokens": cache_read,
        }
        # Forward 5m/1h cache-write breakdown so cost calc applies the 2x 1h
        # premium instead of defaulting to 5m on chat-style.
        cc_breakdown = last_usage.get("cache_creation")
        if isinstance(cc_breakdown, dict) and cc_breakdown:
            usage_block["cache_creation"] = cc_breakdown
        # Propagate fast-mode `usage.speed` so the cost ledger applies the 6x
        # multiplier without re-derivation (Anthropic falls back to "standard"
        # when fast-mode is unsupported or rate-limited).
        speed = last_usage.get("speed")
        if speed in ("fast", "standard"):
            usage_block["speed"] = speed
    else:
        prompt_tokens = last_usage.get("input_tokens") or 0
        cached = 0
        details = last_usage.get("input_tokens_details")
        if isinstance(details, dict):
            cached = details.get("cached_tokens") or 0
        if not (prompt_tokens or completion_tokens or cached):
            return None
        usage_block = {
            "prompt_tokens": prompt_tokens,
            "completion_tokens": completion_tokens,
            "total_tokens": prompt_tokens + completion_tokens,
            "prompt_tokens_details": {"cached_tokens": cached},
        }
        # Surface OpenAI Responses / Gemini reasoning-token detail. The caller
        # pre-populates last_usage["output_tokens_details"] with at least
        # {"reasoning_tokens": ...}; mirror it into the OAI
        # `completion_tokens_details` shape so SDKs can render the
        # hidden-thoughts slice.
        out_details = last_usage.get("output_tokens_details")
        if isinstance(out_details, dict) and out_details:
            usage_block["completion_tokens_details"] = {
                "reasoning_tokens": out_details.get("reasoning_tokens") or 0,
            }
            usage_block["output_tokens_details"] = out_details

    chunk = {
        "id": completion_id,
        "object": "chat.completion.chunk",
        "choices": [],
        "usage": usage_block,
    }
    return f"data: {_json.dumps(chunk)}"
