# SPDX-License-Identifier: AGPL-3.0-only # Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 """ Inference API routes for model loading and text generation. """ import os import sys import time import uuid from pathlib import Path from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi.responses import StreamingResponse, JSONResponse, Response from starlette.requests import ClientDisconnect from typing import Any, Callable, List, Literal, Optional, Union import json import httpx from loggers import get_logger import asyncio import threading import weakref from contextlib import ExitStack import re as _re # Model size extraction (shared with core/inference/llama_cpp.py) from utils.models import extract_model_size_b as _extract_model_size_b from utils.api_errors import openai_error_body, anthropic_error_body from utils.upload_limits import STT_AUDIO_B64_MAX_CHARS, STT_AUDIO_RAW_MAX_BYTES from hub.dependencies import get_hf_token from core.inference.orchestrator import GenStreamError, GenStreamErrorRaised from core.inference.llama_admission import ( LlamaAdmissionCancelled, LlamaAdmissionConfig, LlamaAdmissionLease, LlamaAdmissionQueueFull, LlamaAdmissionReservation, LlamaAdmissionTimeout, get_llama_admission_queue, llama_admission_config_from_env, ) def _positive_int_or_none(value: Any) -> Optional[int]: if isinstance(value, bool): return None try: value_int = int(value) except (TypeError, ValueError): return None return value_int if value_int > 0 else None def _nonnegative_int_or_none(value: Any) -> Optional[int]: if isinstance(value, bool): return None try: value_int = int(value) except (TypeError, ValueError): return None return value_int if value_int >= 0 else None _MLX_MPI_DISTRIBUTED_ENV_PAIRS = ( ("OMPI_COMM_WORLD_RANK", "OMPI_COMM_WORLD_SIZE"), ("PMI_RANK", "PMI_SIZE"), ("PMIX_RANK", "PMIX_SIZE"), ("MPI_RANK", "MPI_WORLD_SIZE"), ("MV2_COMM_WORLD_RANK", "MV2_COMM_WORLD_SIZE"), ) def _mlx_distributed_launch_detected() -> bool: if _nonnegative_int_or_none(os.environ.get("MLX_RANK")) is not None: world_size = _positive_int_or_none(os.environ.get("MLX_WORLD_SIZE")) if world_size is not None and world_size > 1: return True return bool( os.environ.get("MLX_HOSTFILE") or os.environ.get("MLX_IBV_DEVICES") or os.environ.get("MLX_JACCL_COORDINATOR") or (os.environ.get("NCCL_HOST_IP") and os.environ.get("NCCL_PORT")) ) return any( _nonnegative_int_or_none(os.environ.get(rank_env)) is not None and (_positive_int_or_none(os.environ.get(size_env)) or 0) > 1 for rank_env, size_env in _MLX_MPI_DISTRIBUTED_ENV_PAIRS ) def _install_httpcore_asyncgen_silencer() -> None: """Silence benign httpx/httpcore asyncgen GC noise on Python 3.13. When Unsloth proxies a llama-server stream via httpx, the innermost ``HTTP11ConnectionByteStream.__aiter__`` async generator is finalised by the asyncgen GC hook on a task different from the one that opened it. Its ``aclose`` calls ``anyio.Lock.acquire`` → ``cancel_shielded_checkpoint``, entering a ``CancelScope`` on the finaliser task; Python 3.13 flags the cross-task exit as ``"Attempted to exit cancel scope in a different task"`` and prints ``"async generator ignored GeneratorExit"`` as an unraisable warning. Known httpx + httpcore + anyio interaction (MCP SDK python-sdk#831, agno #3556, chainlit #2361, langchain-mcp-adapters #254). Benign: the 200 response is already delivered. The streaming pass-throughs (``/v1/chat/completions``, ``/v1/messages``, ``/v1/responses``, ``/v1/completions``) manage their httpx lifecycle in one task with explicit ``aclose()``; we don't hold a reference to the errant generator and can't close it ourselves. Install one process-wide unraisable hook that swallows only this interaction -- identified by (RuntimeError mentioning cancel scope / GeneratorExit) + (object repr referencing HTTP11ConnectionByteStream) -- and defers to the default hook otherwise. Idempotent. """ prior_hook = sys.unraisablehook if getattr(prior_hook, "_unsloth_httpcore_silencer", False): return def _hook(unraisable): exc_value = getattr(unraisable, "exc_value", None) obj = getattr(unraisable, "object", None) obj_repr = repr(obj) if obj is not None else "" if ( isinstance(exc_value, RuntimeError) and "HTTP11ConnectionByteStream" in obj_repr and ( "cancel scope" in str(exc_value) or "GeneratorExit" in str(exc_value) or "no running event loop" in str(exc_value) ) ): return prior_hook(unraisable) _hook._unsloth_httpcore_silencer = True # type: ignore[attr-defined] sys.unraisablehook = _hook _install_httpcore_asyncgen_silencer() def _loaded_chat_template() -> Optional[str]: """Chat template of the currently loaded GGUF model, if any.""" try: return get_llama_cpp_backend().chat_template except Exception: return None def _template_raise_message(error_text: str, chat_template: Optional[str]) -> Optional[str]: """A chat-template raise_exception message to surface, but only when it appears verbatim in chat_template (simple substring check), so we never leak arbitrary llama-server text. Anchors on llama.cpp's "Jinja Exception:" prefix.""" if not chat_template: return None marker = "Jinja Exception:" idx = error_text.find(marker) if idx == -1: return None candidate = error_text[idx + len(marker) :] # llama-server appends JSON after the message; cut at the first boundary. for stop in ('"', "\n"): cut = candidate.find(stop) if cut != -1: candidate = candidate[:cut] candidate = candidate.strip() return candidate if candidate and candidate in chat_template else None _LOST_CONNECTION_MSG = ( "Lost connection to the model server. It may have crashed -- try reloading the model." ) def _friendly_error(exc: Exception) -> str: """Extract a user-friendly message from known llama-server errors.""" if isinstance(exc, httpx.ReadTimeout): if "stopped producing tokens" in str(exc).lower(): return ( "The model stopped producing tokens before the response " "completed. Try stopping and retrying, or reduce max tokens." ) return ( "The model is still processing the prompt but did not produce a " "first token within 20 minutes. Try reducing context length, " "using more GPU offload, or loading a smaller model." ) if isinstance(exc, httpx.TimeoutException): return "Timed out communicating with the model server. Try again shortly." # httpx transport failures from the async pass-through helpers. Any # RequestError subclass (ConnectError, ReadError, RemoteProtocolError, # WriteError, PoolTimeout, ...) means the llama-server subprocess is # unreachable -- crashed or still coming up. if isinstance(exc, httpx.RequestError): return _LOST_CONNECTION_MSG msg = str(exc) m = _re.search( r"request \((\d+) tokens?\) exceeds the available context size \((\d+) tokens?\)", msg, ) if m: return ( f"Message too long: {m.group(1)} tokens exceeds the {m.group(2)}-token " f"context window. Try increasing the Context Length in Model settings, " f"or shorten the conversation." ) if "Lost connection to llama-server" in msg: return _LOST_CONNECTION_MSG template_msg = _template_raise_message(msg, _loaded_chat_template()) if template_msg: return f"An internal error occurred: {template_msg}" return "An internal error occurred" def _friendly_gen_stream_error(value) -> str: """Return a client-safe message for typed local generation errors.""" text = str(value) if getattr(value, "public", False): return text return safe_error_detail(RuntimeError(text), fallback = "An internal error occurred.") def _friendly_upstream_error(text: str) -> str: """Rewrite a raw llama-server error body into an actionable message where we can. The main case is a tool-calling grammar that llama-server can't compile ("failed to parse grammar" / "failed to initialize samplers"). This surfaces to coding agents as a hard 400 on every tool-bearing turn. It is a llama-server limitation with some model/quant + tool-schema combinations, and recent llama.cpp builds handle the common coding-agent tools, so point the user at updating Unsloth rather than the raw body. """ lowered = text.lower() if "failed to parse grammar" in lowered or "failed to initialize samplers" in lowered: return ( "The model couldn't compile a tool-calling grammar for this request. This is a " "llama-server limitation with some model/quant and tool-schema combinations. " "Update Unsloth (it installs the latest llama.cpp, which handles the common " "coding-agent tools) or try a different GGUF model." ) return f"llama-server error: {text}" def _clamp_finish_reason(value) -> str: """Coerce an upstream finish_reason into OpenAI's known chat values. Unknown values (including ``None``) become ``"stop"`` so local upstream quirks do not leak into the public API shape. """ return ( value if value in ( "stop", "length", "tool_calls", "content_filter", "function_call", ) else "stop" ) def _normalize_stop_sequences(raw): """Coerce an OpenAI/Anthropic ``stop`` value into the list-of-non-empty-strings shape llama-server expects, or ``None`` when absent. A bare string becomes a single-element list; empty strings are dropped (an empty stop sequence would terminate generation immediately at position 0).""" if isinstance(raw, str): return [raw] if raw else None if isinstance(raw, list): return [s for s in raw if isinstance(s, str) and s] or None return None def _effective_max_tokens(payload): """Resolve the generation cap, preferring OpenAI's replacement field. ``max_tokens`` is deprecated in favor of ``max_completion_tokens``; honor either for compatibility, but let the replacement field win when both are supplied. """ return ( payload.max_completion_tokens if payload.max_completion_tokens is not None else payload.max_tokens ) _OPENAI_COMPAT_STREAM_STALL_TIMEOUT_ENV = "UNSLOTH_OPENAI_COMPAT_STREAM_STALL_TIMEOUT" def _positive_float_env(env_name: str, default): """Parse a positive float from an env var. A parseable non-positive value returns ``None`` (0 disables the guarded feature); only unparseable or unset values fall back to ``default``.""" raw_value = os.environ.get(env_name) if raw_value is None or not raw_value.strip(): return default try: value = float(raw_value.strip()) except ValueError: return default return value if value > 0 else None def _effective_openai_max_tokens_from_values(max_tokens, max_completion_tokens = None): """Resolve the OpenAI-compatible generation cap from raw request values. Prefers ``max_completion_tokens`` over the deprecated ``max_tokens``, and returns ``None`` when both are omitted so callers keep their context-window default (OpenAI treats an omitted cap as bounded only by the context window). Explicit client caps pass through unchanged. """ def _validate_explicit(value, param: str): if value is None: return None if isinstance(value, bool) or not isinstance(value, int): raise HTTPException( status_code = 400, detail = openai_error_body( f"'{param}' must be an integer.", status = 400, code = "invalid_type", param = param, ), ) # The legacy completions spec declares ``minimum: 0`` for max_tokens, # so 0 is a valid (if degenerate) cap and only negatives are rejected. # The chat fields never reach here with 0 (pydantic enforces ge=1). if value < 0: raise HTTPException( status_code = 400, detail = openai_error_body( f"'{param}' must be at least 0.", status = 400, code = "invalid_value", param = param, ), ) return value max_tokens = _validate_explicit(max_tokens, "max_tokens") max_completion_tokens = _validate_explicit(max_completion_tokens, "max_completion_tokens") return max_completion_tokens if max_completion_tokens is not None else max_tokens def _effective_openai_max_tokens(payload): return _effective_openai_max_tokens_from_values( getattr(payload, "max_tokens", None), getattr(payload, "max_completion_tokens", None), ) def _wants_multiple_choices(payload) -> bool: return (payload.n or 1) > 1 def _has_openai_tool_history(messages) -> bool: for message in messages or []: if isinstance(message, dict): if message.get("role") == "tool" or message.get("tool_calls"): return True continue if getattr(message, "role", None) == "tool" or getattr(message, "tool_calls", None): return True return False def _raise_unsupported_openai_parameter(param: str, message: str) -> None: raise HTTPException( status_code = 400, detail = openai_error_body( message, status = 400, code = "unsupported_parameter", param = param, ), ) def _raise_unsupported_n(path_label: str) -> None: _raise_unsupported_openai_parameter("n", f"n > 1 is not supported for {path_label}.") def _sse_streaming_response(content) -> StreamingResponse: """A ``text/event-stream`` response with the standard SSE headers used by every streaming path here: no client/proxy caching, no proxy buffering, and a one-shot connection. Two callers build their response inline instead: the external-provider proxy omits ``Connection: close``, and the OpenAI passthrough returns an empty ``keep-alive`` stream when the request is cancelled before the upstream response starts. Built on ``_SameTaskStreamingResponse`` (not Starlette's stock ``StreamingResponse``) so the SSE generator runs in the request task. The legacy AnyIO task-group wrapper trips "Attempted to exit a cancel scope in a different task" on Python 3.13 + httpx, which surfaced as a mid-stream ``response.failed``. The streaming paths that take their response inline use ``_SameTaskStreamingResponse`` directly for the same reason.""" return _SameTaskStreamingResponse( content, media_type = "text/event-stream", headers = { "Cache-Control": "no-cache", "Connection": "close", "X-Accel-Buffering": "no", }, ) def _openai_stream_error_chunk(exc) -> dict: """Build an in-band OpenAI error chunk for a mid-stream failure. Once the stream's 200 headers are flushed the status can't change, so the error must ride in the SSE body. An upstream context-window overflow is mapped to code=context_length_exceeded so client compaction/trim loops can detect it (a code-less error hides it).""" _cls = _classify_llama_generation_error(exc) if _cls: return openai_error_body( _friendly_error(exc), status = 400, code = "context_length_exceeded", ) if _cls is False: return openai_error_body(_friendly_error(exc), status = 400) return openai_error_body(_friendly_error(exc), status = 500) def _openai_stream_error_sse(error: dict) -> str: return f"data: {json.dumps(error)}\n\ndata: [DONE]\n\n" def _openai_stream_error_sse_bytes(error: dict) -> bytes: return _openai_stream_error_sse(error).encode("utf-8") def _openai_passthrough_error(status_code, text) -> "HTTPException": """HTTPException for a non-200 upstream response on the OpenAI passthrough (tools / response_format). An over-context upstream error is mapped to a 400 with code="context_length_exceeded" so these paths deliver the same signal as the non-passthrough path; a tool-grammar compile failure gets the same actionable guidance as the Anthropic passthrough; any other upstream error stays verbatim.""" if _classify_llama_generation_error(Exception(text)): return HTTPException( status_code = 400, detail = openai_error_body( _friendly_error(Exception(text)), status = 400, code = "context_length_exceeded", param = "messages", ), ) return HTTPException( status_code = status_code, detail = _friendly_upstream_error(text[:500]), ) _OVERFLOW_TRUNCATE_MAX_RETRIES = 3 # Truncated-prompt share of the real window; the rest is generation headroom # so a near-full prompt cannot cut a tool call mid-JSON at the wall. _OVERFLOW_PROMPT_TARGET_FRACTION = 0.75 def _overflow_truncation_requested(payload) -> bool: """True when the request (or the UNSLOTH_CONTEXT_OVERFLOW server default, for clients that cannot send custom fields) opted into truncation.""" requested = getattr(payload, "context_overflow", None) if requested is not None: return requested == "truncate_middle" return os.environ.get("UNSLOTH_CONTEXT_OVERFLOW", "").strip().lower() == "truncate_middle" def _parse_overflow_counts(err_text: str): """(n_prompt_tokens, n_ctx) from an exceed_context_size_error body, or None. Tolerates \\" around keys (body may be a re-wrapped JSON string).""" m_prompt = _re.search(r'n_prompt_tokens\\?"?\s*:\s*(\d+)', err_text) m_ctx = _re.search(r'n_ctx\\?"?\s*:\s*(\d+)', err_text) if m_prompt and m_ctx: return int(m_prompt.group(1)), int(m_ctx.group(1)) return None def _estimate_message_tokens(msg: dict) -> int: try: return max(1, len(json.dumps(msg, ensure_ascii = False)) // 4) except Exception: return 1 def _truncate_middle_messages(messages: list, keep_ratio: float): """Drop whole turn-groups from the middle of an OpenAI message list. Always kept: leading system message(s), the first group (task anchor), and the trailing groups. A group is a user message, or an assistant message plus its following tool results, so surviving tool_calls stay paired with their results as chat templates require. Returns (new_messages, dropped_message_count). """ if not messages or keep_ratio >= 1.0: return messages, 0 head: list = [] idx = 0 while idx < len(messages) and messages[idx].get("role") in ("system", "developer"): head.append(messages[idx]) idx += 1 groups: list[list] = [] for msg in messages[idx:]: role = msg.get("role") if role == "tool" and groups: groups[-1].append(msg) elif role == "tool": groups.append([msg]) # orphan tool result; treat as its own group else: groups.append([msg]) # Anchor group plus the last 3 groups stay. protected_tail = min(3, max(1, len(groups) - 1)) if len(groups) <= 1 + protected_tail: return messages, 0 total_est = sum(_estimate_message_tokens(m) for m in messages) target_est = int(total_est * keep_ratio) anchor = groups[0] middle = groups[1:-protected_tail] tail = groups[-protected_tail:] current_est = total_est kept_middle: list[list] = list(middle) dropped = 0 # Drop oldest-first until the estimate fits the target. while kept_middle and current_est > target_est: victim = kept_middle.pop(0) dropped += len(victim) current_est -= sum(_estimate_message_tokens(m) for m in victim) if dropped == 0: return messages, 0 new_messages = head + anchor for grp in kept_middle: new_messages.extend(grp) for grp in tail: new_messages.extend(grp) return new_messages, dropped _CLIP_MARKER = "\n[... truncated by context_overflow=truncate_middle ...]\n" # Generous head+tail first; cut harder if the estimate still misses the target. _CLIP_KEEP_CHARS = (1500, 400) def _clip_long_contents(messages: list, target_est: int) -> int: """Clip oversized string contents middle-out until ``target_est`` is met. Tool results first, then earlier user turns, the final message last. Message count and roles never change, so tool pairing holds even when group-dropping could not free enough. Returns messages clipped. """ def _candidates(): tools = [m for m in messages if m.get("role") == "tool"] users = [m for m in messages[:-1] if m.get("role") == "user"] last = [messages[-1]] if messages else [] return tools + users + last clipped = 0 for keep in _CLIP_KEEP_CHARS: for msg in _candidates(): if sum(_estimate_message_tokens(m) for m in messages) <= target_est: return clipped content = msg.get("content") if not isinstance(content, str) or len(content) <= 2 * keep + len(_CLIP_MARKER): continue msg["content"] = content[:keep] + _CLIP_MARKER + content[-keep:] clipped += 1 return clipped def _apply_overflow_truncation(body: dict, err_text: str) -> bool: """Shrink a passthrough body after an upstream context overflow: drop middle turn-groups, clip still-oversized contents, clamp ``max_tokens`` to the generation headroom. Returns False when nothing could shrink.""" counts = _parse_overflow_counts(err_text) messages = body.get("messages") or [] total_est = sum(_estimate_message_tokens(m) for m in messages) if counts: n_prompt, n_ctx = counts keep_ratio = min(0.95, (_OVERFLOW_PROMPT_TARGET_FRACTION * n_ctx) / max(1, n_prompt)) else: n_ctx = None keep_ratio = 0.6 # no counts in the error; cut conservatively # Scale the server-token target into char-estimate units. target_est = int(total_est * keep_ratio) new_messages, dropped = _truncate_middle_messages(messages, keep_ratio) if dropped: body["messages"] = new_messages clipped = 0 if sum(_estimate_message_tokens(m) for m in body.get("messages") or []) > target_est: clipped = _clip_long_contents(body.get("messages") or [], target_est) if not dropped and not clipped: return False if n_ctx: headroom = max(1024, int(n_ctx * (1.0 - _OVERFLOW_PROMPT_TARGET_FRACTION))) cur_max = body.get("max_tokens") body["max_tokens"] = min(cur_max, headroom) if cur_max else headroom logger.warning( "context_overflow=truncate_middle: dropped %d middle messages, clipped " "%d contents (keep_ratio %.2f); retrying within the real window", dropped, clipped, keep_ratio, ) return True def _anthropic_stream_error_event(exc, *, force: bool = False): """Return an Anthropic in-band stream error event when one is useful.""" _cls = _classify_llama_generation_error(exc) if _cls is None and not force: return None status = 400 if _cls is not None else 500 return build_anthropic_sse_event( "error", anthropic_error_body(_friendly_error(exc), status = status), ) def _drop_parallel_tool_call_deltas(chunk) -> bool: """In-place: drop tool_call deltas whose index >= 1 from a parsed OpenAI streaming chunk so only the first tool call survives (parallel_tool_calls=false / disable_parallel_tool_use, best-effort). Returns True if anything changed.""" if not isinstance(chunk, dict): return False changed = False for ch in chunk.get("choices") or []: delta = ch.get("delta") or {} tcs = delta.get("tool_calls") if isinstance(tcs, list): kept = [tc for tc in tcs if isinstance(tc, dict) and (tc.get("index") or 0) == 0] if len(kept) != len(tcs): delta["tool_calls"] = kept changed = True return changed def _add_empty_content_to_reasoning_deltas(chunk: dict) -> bool: """Make reasoning-only deltas palatable to strict OpenAI adapters. Some clients built on OpenAI-compatible streams ignore or reject chunks whose delta only contains non-standard ``reasoning_content``. Preserve that field, but add an empty standard ``content`` member so the chunk is still a valid text-delta shape and downstream parsers keep the stream alive. """ changed = False choices = chunk.get("choices") if not isinstance(choices, list): return False for choice in choices: if not isinstance(choice, dict): continue delta = choice.get("delta") if not isinstance(delta, dict): continue if "reasoning_content" in delta and "content" not in delta: delta["content"] = "" changed = True return changed def _normalize_openai_passthrough_sse_line( raw_line: str, *, cap_parallel_tool_calls: bool = False ) -> str: """Normalize one passthrough OpenAI SSE ``data:`` line before relaying. The function is intentionally narrow: it leaves comments, blank events, ``[DONE]``, and unparseable upstream bytes untouched; parsed chunks are re-serialized only when a compatibility mutation is actually required. """ if not raw_line.startswith("data:"): return raw_line # Both mutations key off JSON object keys, so a line without either quoted # key can never change; skip the parse on the per-token common case. if '"reasoning_content"' not in raw_line and not ( cap_parallel_tool_calls and '"tool_calls"' in raw_line ): return raw_line payload = raw_line[len("data:") :].lstrip() if payload.strip() in ("", "[DONE]"): return raw_line try: obj = json.loads(payload) except Exception: return raw_line if not isinstance(obj, dict): return raw_line changed = _add_empty_content_to_reasoning_deltas(obj) if cap_parallel_tool_calls and _drop_parallel_tool_call_deltas(obj): changed = True if not changed: return raw_line return "data: " + json.dumps(obj, separators = (",", ":"), ensure_ascii = False) def _prompt_tokens_details(upstream): """Surface llama-server's real ``cached_tokens`` (KV-cache prompt hits) while keeping the full OpenAI ``prompt_tokens_details`` shape. Defaults to zero when the upstream usage doesn't carry it, so the field is always present.""" out = {"cached_tokens": 0, "audio_tokens": 0} if isinstance(upstream, dict): out.update({k: v for k, v in upstream.items() if v is not None}) return out def _wants_stream_usage(payload) -> bool: return bool((payload.stream_options or {}).get("include_usage")) _OPENAI_PASSTHROUGH_TERMINAL_GRACE_S = 2.0 _SSE_DONE_LINE = "data: [DONE]" def _openai_passthrough_sse_line_terminal_state(raw_line: str) -> Optional[str]: """Classify OpenAI-compatible chat stream terminal markers. Some llama-server builds can emit the logical final chunk (``finish_reason``) and optional usage chunk, then keep the HTTP stream open without sending the OpenAI ``data: [DONE]`` sentinel. Classifying those chunks lets Unsloth close the client stream promptly while preserving an optional trailing usage chunk. """ if not raw_line.startswith("data:"): return None data_str = raw_line[5:].lstrip() if data_str == "[DONE]": return "done" try: data = json.loads(data_str) except json.JSONDecodeError: return None return _openai_passthrough_terminal_state_from_data(data) def _openai_passthrough_terminal_state_from_data(data) -> Optional[str]: """Dict-level core of ``_openai_passthrough_sse_line_terminal_state`` for callers that already parsed the chunk (avoids a re-parse per relayed line).""" if not isinstance(data, dict): return None if _monitor_openai_error_message(data): return "error" choices = data.get("choices") if isinstance(choices, list): if not choices and isinstance(data.get("usage"), dict): return "usage" for choice in choices: if isinstance(choice, dict) and choice.get("finish_reason") is not None: return "finish" elif isinstance(data.get("usage"), dict): return "usage" return None def _openai_stream_usage_chunk( payload, completion_id, created, model_name, stream_usage, stream_timings ): """Build the final OpenAI-standard usage chunk (choices=[], usage populated) for a chat stream. Returns the SSE ``data:`` line, or None when the client did not opt in via ``stream_options.include_usage`` (or no usage exists).""" if not _wants_stream_usage(payload): return None if not (stream_usage or stream_timings): return None _usage = stream_usage or {} _prompt_tokens = _usage.get("prompt_tokens") or 0 _completion_tokens = _usage.get("completion_tokens") or 0 _total_tokens = _usage.get("total_tokens") or (_prompt_tokens + _completion_tokens) usage_chunk = ChatCompletionChunk( id = completion_id, created = created, model = model_name, choices = [], usage = CompletionUsage( prompt_tokens = _prompt_tokens, completion_tokens = _completion_tokens, total_tokens = _total_tokens, prompt_tokens_details = _prompt_tokens_details(_usage.get("prompt_tokens_details")), ), timings = stream_timings, ) return f"data: {usage_chunk.model_dump_json(exclude_none = True)}\n\n" def _chat_chunk_sse(completion_id, created, model_name, *, delta, finish_reason) -> str: """One ``ChatCompletionChunk`` as an SSE ``data:`` line. The role / content / final chunks every in-process streamer emits differ only in their ``delta`` and ``finish_reason``.""" chunk = ChatCompletionChunk( id = completion_id, created = created, model = model_name, choices = [ChunkChoice(delta = delta, finish_reason = finish_reason)], ) return f"data: {chunk.model_dump_json(exclude_none = True)}\n\n" def _chat_role_chunk(completion_id, created, model_name) -> str: """Opening assistant-role chunk for a chat stream.""" return _chat_chunk_sse( completion_id, created, model_name, delta = ChoiceDelta(role = "assistant"), finish_reason = None, ) def _chat_content_chunk(completion_id, created, model_name, text) -> str: """A content-delta chunk carrying ``text``.""" return _chat_chunk_sse( completion_id, created, model_name, delta = ChoiceDelta(content = text), finish_reason = None, ) def _chat_reasoning_chunk(completion_id, created, model_name, text) -> str: """Like ``_chat_content_chunk`` but on ``reasoning_content`` (renders the UI thinking block). Carries ``content: ""`` alongside, like the GGUF and passthrough paths, so strict OpenAI adapters don't drop the reasoning-only delta. """ return _chat_chunk_sse( completion_id, created, model_name, delta = ChoiceDelta(content = "", reasoning_content = text), finish_reason = None, ) def _chat_final_chunk(completion_id, created, model_name, finish_reason) -> str: """Terminal stop chunk (empty delta) carrying the finish reason.""" return _chat_chunk_sse( completion_id, created, model_name, delta = ChoiceDelta(), finish_reason = finish_reason, ) def _chat_tool_calls_chunk(completion_id, created, model_name, tool_calls) -> str: """Delta chunk carrying OpenAI tool-call deltas (sibling of ``_chat_content_chunk``).""" return _chat_chunk_sse( completion_id, created, model_name, delta = ChoiceDelta(tool_calls = tool_calls), finish_reason = None, ) def _sf_heal_events_to_sse( events, completion_id, created, model_name, state, parallel_tool_calls, monitor_id = None, ): """Serialize ``StreamToolCallHealer`` events into chat SSE lines. ``state["idx"]`` tracks the call index across ``feed``/``finalize``; ``parallel_tool_calls is False`` caps promotion to one call (GGUF parity). The monitor is fed from the same events the client receives, never the healed-away markup.""" lines = [] for kind, value in events: if kind == "text": if value: lines.append(_chat_content_chunk(completion_id, created, model_name, value)) api_monitor.append_reply(monitor_id, value) continue if parallel_tool_calls is False and state["idx"] >= 1: continue lines.append( _chat_tool_calls_chunk( completion_id, created, model_name, [ { "index": state["idx"], "id": value["id"], "type": "function", "function": value["function"], } ], ) ) _fn = value.get("function") or {} api_monitor.append_reply( monitor_id, ("[tool_calls] " if state["idx"] == 0 else "; ") + f"{_fn.get('name', '')}({_fn.get('arguments', '')})", ) state["idx"] += 1 return lines def _rewrite_cmpl_id(raw: bytes) -> bytes: """Rewrite llama-server's chat-style ``chatcmpl-`` ids to the ``cmpl-`` prefix OpenAI's legacy /v1/completions use. Anchored on the ``"id":`` key (both spacing variants) so the rest of the body stays byte-exact.""" return raw.replace(b'"id":"chatcmpl-', b'"id":"cmpl-').replace( b'"id": "chatcmpl-', b'"id": "cmpl-' ) def _cmpl_stream_event_out(event: bytes, include_usage: bool) -> Optional[bytes]: """Process one legacy /v1/completions SSE event (text between blank-line separators). Always rewrites the ``chatcmpl-`` -> ``cmpl-`` id prefix. When the client did NOT request ``stream_options.include_usage``, also removes the usage statistics so the stream matches OpenAI's contract. Shape note: on /v1/completions, llama-server attaches ``usage`` to the FINAL content chunk (the ``finish_reason`` chunk, which has a populated ``choices`` array) -- unlike the chat stream, which emits a standalone ``choices: []`` usage chunk. Both shapes are handled: a standalone usage-only chunk is dropped; an inline ``usage`` field is stripped from a content chunk while keeping ``choices``/``finish_reason`` intact. Returns the event bytes to emit, or ``None`` to drop the event. Only a usage-bearing event is re-serialized; every other event keeps exact bytes. """ if include_usage: return _rewrite_cmpl_id(event) lines = event.split(b"\n") changed = False for i, ln in enumerate(lines): if not ln.startswith(b"data:"): continue payload = ln[len(b"data:") :].strip() if not payload or payload == b"[DONE]": continue try: obj = json.loads(payload) except Exception: continue if not isinstance(obj, dict) or obj.get("usage") is None: continue # Standalone usage-only chunk (chat-style) -> drop the whole event. if obj.get("choices") == []: return None # Usage on a content/finish chunk (completions-style) -> strip it. obj.pop("usage", None) lines[i] = b"data: " + json.dumps(obj, separators = (",", ":")).encode("utf-8") changed = True return _rewrite_cmpl_id(b"\n".join(lines) if changed else event) def _classify_llama_generation_error(exc: Exception) -> Optional[bool]: """Classify an error raised while consuming the GGUF generator. Returns True for a context-window overflow, False for any other upstream 4xx (a client error), or None when it should stay a 500. Distinguishes a real client error from a genuine crash by the explicit "llama-server returned 4xx" marker, not a bare "tokens"/"exceed" substring. """ msg = str(exc) msg_l = msg.lower() if "n_ctx" in msg_l or ( "context" in msg_l and any(t in msg_l for t in ("exceed", "length", "window", "too long")) ): return True if _re.search(r"llama-server returned (4\d\d)", msg): return False return None # Add backend directory to path backend_path = Path(__file__).parent.parent.parent if str(backend_path) not in sys.path: sys.path.insert(0, str(backend_path)) try: from core.inference import get_inference_backend from core.inference.llama_cpp import ( LlamaCppBackend, _DEFAULT_FIRST_TOKEN_TIMEOUT_S, _DEFAULT_MAX_TOKENS_FLOOR, _DEFAULT_STREAM_STALL_TIMEOUT_S, _canonicalize_spec_mode, _extra_args_set_spec_type, _hf_offline_if_dns_dead, detect_reasoning_flags, ) from core.inference.llama_server_args import ( _effective_tensor_parallel, _tensor_parallel_matches_loaded, extra_args_disable_mmproj, parse_split_mode_override, resolve_tensor_parallel, strip_shadowing_flags, validate_extra_args, ) from core.inference.tensor_fallback import load_with_tensor_fallback from utils.models import ModelConfig from utils.inference import load_inference_config from utils.models.model_config import ( detect_mtp_file, load_model_defaults, ) from utils.native_path_leases import ( NativePathLeaseError, display_label_for_native_path, is_registered_native_path_label, redact_native_paths, verify_native_path_lease, ) except ImportError: parent_backend = backend_path.parent / "backend" if str(parent_backend) not in sys.path: sys.path.insert(0, str(parent_backend)) from core.inference import get_inference_backend from core.inference.llama_cpp import ( LlamaCppBackend, _DEFAULT_FIRST_TOKEN_TIMEOUT_S, _DEFAULT_MAX_TOKENS_FLOOR, _DEFAULT_STREAM_STALL_TIMEOUT_S, _canonicalize_spec_mode, _extra_args_set_spec_type, _hf_offline_if_dns_dead, detect_reasoning_flags, ) from core.inference.llama_server_args import ( _effective_tensor_parallel, _tensor_parallel_matches_loaded, extra_args_disable_mmproj, parse_split_mode_override, resolve_tensor_parallel, strip_shadowing_flags, validate_extra_args, ) from core.inference.tensor_fallback import load_with_tensor_fallback from utils.models import ModelConfig from utils.inference import load_inference_config from utils.models.model_config import ( detect_mtp_file, load_model_defaults, ) from utils.native_path_leases import ( NativePathLeaseError, display_label_for_native_path, is_registered_native_path_label, redact_native_paths, verify_native_path_lease, ) def _llama_non_streaming_generation_timeout() -> httpx.Timeout: return httpx.Timeout(_DEFAULT_FIRST_TOKEN_TIMEOUT_S) def _llama_streaming_generation_timeout() -> httpx.Timeout: return httpx.Timeout(_DEFAULT_FIRST_TOKEN_TIMEOUT_S) def _set_stream_response_read_timeout( response: httpx.Response, read_timeout_s: Optional[float] = _DEFAULT_STREAM_STALL_TIMEOUT_S ) -> None: # ``read_timeout_s = None`` clears httpx's read timeout (wait indefinitely), # used when the stall guard is disabled so a stale first-token deadline # can't keep timing out post-first-chunk gaps. try: timeout_ext = response.request.extensions.get("timeout") if isinstance(timeout_ext, dict): timeout_ext["read"] = read_timeout_s except Exception: pass _STREAM_DISCONNECT_POLL_TIMEOUT_S = 0.25 _OPENAI_PASSTHROUGH_PREHEADER_STATUS_WINDOW_S = 0.1 _OPENAI_PASSTHROUGH_PENDING_RESPONSE_KEEPALIVE_S = 5.0 _OPENAI_PASSTHROUGH_SSE_KEEPALIVE = ": keep-alive\n\n" _OPENAI_LLAMA_ADMISSION_POLL_S = 0.25 # Idle window before a local tool-loop stream emits an SSE keepalive comment # (e.g. prompt prefill between tool iterations). A second layer atop the # tool_stream_exec heartbeats, keeping proxies (Cloudflare drops idle at ~100s). _LOCAL_TOOL_STREAM_STALL_KEEPALIVE_S = 15.0 def _openai_llama_admission_capacity(request: Optional[Request], llama_backend = None) -> int: """Serving slots available for one local llama-server backend. The loaded backend is the source of truth because it may have reduced ``--parallel`` at load time to keep the model on GPU. The app state is a launch-intent fallback for tests and for the short window before a backend reports its committed runtime slots. """ slots = _positive_int_or_none(getattr(llama_backend, "effective_parallel_slots", None)) if slots is not None: return slots try: slots = getattr(request.app.state, "llama_parallel_slots", None) except Exception: slots = None return _positive_int_or_none(slots) or 1 def _openai_llama_admission_reserve( *, request: Optional[Request], llama_backend ) -> tuple[LlamaAdmissionReservation, LlamaAdmissionConfig]: config = llama_admission_config_from_env() capacity = _openai_llama_admission_capacity(request, llama_backend) key = str(getattr(llama_backend, "base_url", "llama-server")) reservation = get_llama_admission_queue(key).reserve( capacity = capacity, config = config, ) return reservation, config def _openai_admission_request_path(request: Optional[Request]) -> Optional[str]: try: return str(request.url.path) if request is not None else None except Exception: return None def _openai_admission_log( event: str, reservation: Optional[LlamaAdmissionReservation] = None, *, snapshot = None, request: Optional[Request], mode: str, wait_started_at: Optional[float] = None, completion_id: Optional[str] = None, level: str = "debug", ) -> None: if snapshot is None and reservation is not None: snapshot = reservation.snapshot_now() wait_ms = None if wait_started_at is not None: wait_ms = int(max(0.0, time.monotonic() - wait_started_at) * 1000) log = getattr(logger, level, logger.debug) log( "openai admission %s: mode=%s path=%s completion_id=%s capacity=%s active=%s queued=%s wait_ms=%s", event, mode, _openai_admission_request_path(request), completion_id, getattr(snapshot, "capacity", None), getattr(snapshot, "active", None), getattr(snapshot, "queued", None), wait_ms, ) def _openai_admission_error_body(exc: Exception, *, status_code: int) -> dict: snapshot = getattr(exc, "snapshot", None) message = str(exc) if snapshot is not None: message = ( f"{message} " f"(active={snapshot.active}, queued={snapshot.queued}, capacity={snapshot.capacity})" ) return openai_error_body(message, status = status_code) def _openai_admission_http_exception(exc: Exception, *, status_code: int) -> HTTPException: return HTTPException( status_code = status_code, detail = _openai_admission_error_body(exc, status_code = status_code), ) def _openai_admission_timeout_error( reservation: LlamaAdmissionReservation, ) -> LlamaAdmissionTimeout: return LlamaAdmissionTimeout( "Timed out waiting for an available local llama-server generation slot", snapshot = reservation.snapshot_now(), ) def _openai_admission_cancelled_error( reservation: LlamaAdmissionReservation, ) -> LlamaAdmissionCancelled: return LlamaAdmissionCancelled( "Client disconnected before an upstream llama-server generation slot was available", snapshot = reservation.snapshot_now(), ) async def _raise_if_openai_admission_cancelled( reservation: LlamaAdmissionReservation, *, request: Optional[Request], cancel_event ) -> None: if reservation.is_cancelled: raise _openai_admission_cancelled_error(reservation) if await _preheader_cancelled(cancel_event, request): reservation.cancel() raise _openai_admission_cancelled_error(reservation) async def _wait_for_openai_admission_non_streaming( reservation: LlamaAdmissionReservation, config: LlamaAdmissionConfig, *, request: Optional[Request], cancel_event, ) -> LlamaAdmissionLease: lease = reservation.lease_nowait() if lease is not None: try: await _raise_if_openai_admission_cancelled( reservation, request = request, cancel_event = cancel_event, ) except asyncio.CancelledError: lease.release() raise except LlamaAdmissionCancelled: lease.release() raise return lease await _raise_if_openai_admission_cancelled( reservation, request = request, cancel_event = cancel_event, ) deadline = None if config.queue_timeout_s is None else time.monotonic() + config.queue_timeout_s try: while True: await _raise_if_openai_admission_cancelled( reservation, request = request, cancel_event = cancel_event, ) lease = reservation.lease_nowait() if lease is not None: try: await _raise_if_openai_admission_cancelled( reservation, request = request, cancel_event = cancel_event, ) except asyncio.CancelledError: lease.release() raise except LlamaAdmissionCancelled: lease.release() raise return lease wait_s = _OPENAI_LLAMA_ADMISSION_POLL_S if deadline is not None: remaining_s = deadline - time.monotonic() if remaining_s <= 0: reservation.cancel() raise _openai_admission_timeout_error(reservation) wait_s = min(wait_s, max(remaining_s, 0.001)) try: lease = await reservation.wait(wait_s) except asyncio.TimeoutError: continue if lease is not None: return lease await _raise_if_openai_admission_cancelled( reservation, request = request, cancel_event = cancel_event, ) except asyncio.CancelledError: reservation.cancel() raise async def _openai_admission_wait_stream_chunks( reservation: LlamaAdmissionReservation, config: LlamaAdmissionConfig, *, request: Optional[Request], cancel_event, ): lease = reservation.lease_nowait() if lease is not None: yield lease return await _raise_if_openai_admission_cancelled( reservation, request = request, cancel_event = cancel_event, ) deadline = None if config.queue_timeout_s is None else time.monotonic() + config.queue_timeout_s keepalive_interval_s = max(0.001, config.keepalive_interval_s) next_keepalive_at = time.monotonic() + keepalive_interval_s try: while True: await _raise_if_openai_admission_cancelled( reservation, request = request, cancel_event = cancel_event, ) lease = reservation.lease_nowait() if lease is not None: yield lease return now = time.monotonic() wait_s = min(_OPENAI_LLAMA_ADMISSION_POLL_S, max(next_keepalive_at - now, 0.001)) if deadline is not None: remaining_s = deadline - now if remaining_s <= 0: reservation.cancel() raise _openai_admission_timeout_error(reservation) wait_s = min(wait_s, max(remaining_s, 0.001)) try: lease = await reservation.wait(wait_s) except asyncio.TimeoutError: lease = None if lease is not None: yield lease return await _raise_if_openai_admission_cancelled( reservation, request = request, cancel_event = cancel_event, ) now = time.monotonic() if now >= next_keepalive_at: next_keepalive_at = now + keepalive_interval_s yield _OPENAI_PASSTHROUGH_SSE_KEEPALIVE except asyncio.CancelledError: reservation.cancel() raise async def _close_openai_admitted_stream_iterator(iterator, *, cancelled: bool) -> None: if iterator is None: return if cancelled: athrow = getattr(iterator, "athrow", None) if athrow is not None: try: await athrow(asyncio.CancelledError()) except (asyncio.CancelledError, StopAsyncIteration, RuntimeError): return aclose = getattr(iterator, "aclose", None) if aclose is not None: await aclose() def _openai_compat_stream_stall_timeout(): """Max silent gap after an OpenAI passthrough stream has produced data. If the socket goes silent after valid SSE data, this bounds how long the client is kept open. Defaults to the backend-wide stall timeout so this path stalls out like every sibling stream; set the env var to tighten it for local serving, or to 0 to disable the guard. """ return _positive_float_env( _OPENAI_COMPAT_STREAM_STALL_TIMEOUT_ENV, _DEFAULT_STREAM_STALL_TIMEOUT_S, ) def _openai_passthrough_upstream_headers(*, llama_backend = None) -> dict: headers = {} auth_headers = getattr(llama_backend, "_auth_headers", None) if isinstance(auth_headers, dict): headers.update(auth_headers) headers["Connection"] = "close" return headers class _CompatSameTaskTimeout: """Same-task timeout fallback for Python versions before asyncio.timeout.""" def __init__(self, timeout_s: float): self.timeout_s = timeout_s self._task = None self._handle = None self._timed_out = False self._cancelling = 0 async def __aenter__(self): self._task = asyncio.current_task() if self._task is None: return self if hasattr(self._task, "cancelling"): self._cancelling = self._task.cancelling() loop = asyncio.get_running_loop() self._handle = loop.call_later(max(self.timeout_s, 0), self._cancel_task) return self async def __aexit__(self, exc_type, exc, tb): if self._handle is not None: self._handle.cancel() if exc_type is not None and issubclass(exc_type, asyncio.CancelledError): if self._timed_out: if self._task is not None and hasattr(self._task, "uncancel"): if self._task.uncancel() > self._cancelling: return None raise asyncio.TimeoutError from exc return None def _cancel_task(self) -> None: self._timed_out = True if self._task is not None: self._task.cancel() def _same_task_timeout(timeout_s: float): timeout_ctx = getattr(asyncio, "timeout", None) if timeout_ctx is not None: return timeout_ctx(timeout_s) return _CompatSameTaskTimeout(timeout_s) class _SameTaskStreamingResponse(StreamingResponse): """StreamingResponse without Starlette's legacy AnyIO task-group wrapper.""" def __init__( self, *args, unstarted_cleanup = None, **kwargs, ) -> None: super().__init__(*args, **kwargs) # Released when the client disconnects before the body iterator starts: # its try/finally never runs, so a stream that opens resources before the # first yield (the passthrough's upstream httpx stream) passes this. self._unstarted_cleanup = unstarted_cleanup async def __call__(self, scope, receive, send) -> None: # send() emits a body message only after the first chunk, so no body # message means the generator never entered its try/finally. body_started = False async def _tracking_send(message) -> None: nonlocal body_started if message.get("type") == "http.response.body": body_started = True await send(message) try: await self.stream_response(_tracking_send) except OSError: # client disconnected mid-send if body_started: # Generator is suspended in its try/finally: throw CancelledError # (not aclose's GeneratorExit) so its handler finishes the # api_monitor entry. Fall back to aclose() without athrow. athrow = getattr(self.body_iterator, "athrow", None) if athrow is not None: try: await athrow(asyncio.CancelledError()) except (asyncio.CancelledError, StopAsyncIteration, RuntimeError): pass else: aclose = getattr(self.body_iterator, "aclose", None) if aclose is not None: await aclose() else: # Generator never started; aclose()/athrow() are no-ops on it, so # release eager resources via the hook. getattr guards a response # built through __new__ without __init__ (tests, pickling). aclose = getattr(self.body_iterator, "aclose", None) if aclose is not None: await aclose() cleanup = getattr(self, "_unstarted_cleanup", None) if cleanup is not None: try: await cleanup() except Exception: pass raise ClientDisconnect() if self.background is not None: await self.background() def _tracked_cancel_unstarted_cleanup(tracker): """unstarted_cleanup that exits ``tracker`` on a pre-start disconnect, when the generator's finally (which normally exits it) never runs.""" async def _cleanup() -> None: tracker.__exit__(None, None, None) return _cleanup async def _aclose_stream_resources( *, watchers = (), iterator = None, resp = None, client = None, ) -> None: """Tear down an httpx streaming generator's resources in the required order: cancel + await each watcher task, then aclose() the byte/line iterator, the response, and the client. Each step swallows its own exceptions so teardown always completes; a close-time CancelledError is re-raised only after every step has run. See _anthropic_passthrough_stream for the ordering rationale.""" for watcher in watchers: if watcher is not None: watcher.cancel() try: await watcher except (asyncio.CancelledError, Exception): pass close_cancelled = False if iterator is not None: try: await iterator.aclose() except asyncio.CancelledError: close_cancelled = True except Exception: pass if resp is not None: try: await resp.aclose() except asyncio.CancelledError: close_cancelled = True except Exception: pass if client is not None: try: await client.aclose() except asyncio.CancelledError: close_cancelled = True except Exception: pass if close_cancelled: raise asyncio.CancelledError() async def _preheader_cancelled(cancel_event = None, request: Optional[Request] = None) -> bool: if cancel_event is not None and cancel_event.is_set(): return True if request is not None and await request.is_disconnected(): if cancel_event is not None: cancel_event.set() return True return False async def _wait_preheader_cancel(cancel_event = None, request: Optional[Request] = None) -> None: while not await _preheader_cancelled(cancel_event, request): await asyncio.sleep(0.05) async def _send_stream_with_preheader_cancel( client: httpx.AsyncClient, req: httpx.Request, cancel_event = None, request: Optional[Request] = None, mark_cancel_on_cancel: bool = True, ) -> Optional[httpx.Response]: if cancel_event is None and request is None: return await client.send(req, stream = True) if await _preheader_cancelled(cancel_event, request): return None send_task = asyncio.create_task(client.send(req, stream = True)) cancel_task = asyncio.create_task(_wait_preheader_cancel(cancel_event, request)) async def _stop_send_task() -> None: try: await client.aclose() except Exception: pass send_task.cancel() try: await send_task except (asyncio.CancelledError, Exception): pass try: done, _pending = await asyncio.wait( {send_task, cancel_task}, return_when = asyncio.FIRST_COMPLETED, ) if send_task in done: return await send_task await _stop_send_task() return None except asyncio.CancelledError: if mark_cancel_on_cancel and cancel_event is not None: cancel_event.set() await _stop_send_task() raise finally: cancel_task.cancel() try: await cancel_task except (asyncio.CancelledError, Exception): pass async def _aiter_llama_stream_items( async_iter, *, cancel_event = None, request: Optional[Request] = None, first_token_deadline: Optional[float] = None, response: Optional[httpx.Response] = None, post_first_item_read_timeout_s: Optional[ Union[float, Callable[[], Optional[float]]] ] = _DEFAULT_STREAM_STALL_TIMEOUT_S, ): if first_token_deadline is None: first_token_deadline = time.monotonic() + _DEFAULT_FIRST_TOKEN_TIMEOUT_S last_item_at: Optional[float] = None def _post_first_timeout_s() -> Optional[float]: if callable(post_first_item_read_timeout_s): return post_first_item_read_timeout_s() return post_first_item_read_timeout_s while True: if cancel_event is not None and cancel_event.is_set(): return if request is not None and await request.is_disconnected(): if cancel_event is not None: cancel_event.set() return waiting_first_item = last_item_at is None try: if waiting_first_item: remaining_s = first_token_deadline - time.monotonic() if remaining_s <= 0: raise httpx.ReadTimeout("The model did not produce a first token in time.") if response is not None: _set_stream_response_read_timeout(response, remaining_s) # Keep httpx/httpcore's AnyIO cancel scope in this task. # asyncio.wait_for would drive __anext__ in a child task. async with _same_task_timeout(remaining_s): item = await async_iter.__anext__() else: timeout_s = _post_first_timeout_s() if ( request is not None and response is not None and timeout_s is not None and last_item_at is not None ): stall_remaining_s = timeout_s - (time.monotonic() - last_item_at) if stall_remaining_s <= 0: raise httpx.ReadTimeout("The model stopped producing tokens mid-response.") _set_stream_response_read_timeout(response, stall_remaining_s) item = await async_iter.__anext__() except asyncio.TimeoutError as exc: if waiting_first_item: raise httpx.ReadTimeout("The model did not produce a first token in time.") from exc raise except StopAsyncIteration: return except httpx.ReadTimeout: now = time.monotonic() if last_item_at is None: if now >= first_token_deadline: raise continue timeout_s = _post_first_timeout_s() if request is not None and timeout_s is not None and now - last_item_at < timeout_s: continue raise httpx.ReadTimeout("The model stopped producing tokens mid-response.") if last_item_at is None and response is not None: # The first-token read deadline no longer applies once a chunk has # arrived: switch to the stall timeout, or clear the read timeout # entirely when the stall guard is disabled (callable returns None) # so a long gap can't trip the stale first-token deadline. _set_stream_response_read_timeout(response, _post_first_timeout_s()) last_item_at = time.monotonic() yield item from models.inference import ( LoadRequest, UnloadRequest, TranscribeRequest, SttLoadRequest, GenerateRequest, DiffusionLoadRequest, DiffusionGenerateRequest, DiffusionGenerateResponse, DiffusionGenerateProgressResponse, DiffusionStatusResponse, DiffusionDownloadPlanResponse, DiffusionInferenceInfoResponse, DiffusionLoadProgressResponse, GalleryImage, GalleryListResponse, ImageGenerationRequest, ImageGenerationData, ImageGenerationResponse, LoadResponse, LoadProgressResponse, UnloadResponse, InferenceStatusResponse, ChatCompletionRequest, ChatCompletionChunk, ChatCompletion, ToolConfirmRequest, ChatMessage, ChunkChoice, ChoiceDelta, CompletionChoice, CompletionMessage, CompletionUsage, ValidateModelRequest, ValidateModelResponse, TransformersUpgradeInfo, InstallLatestTransformersRequest, InstallLatestTransformersResponse, TextContentPart, ImageContentPart, ImageUrl, ResponsesRequest, ResponsesInputTextPart, ResponsesInputImagePart, ResponsesOutputTextPart, ResponsesUnknownInputItem, ResponsesFunctionCallInputItem, ResponsesFunctionCallOutputInputItem, ResponsesOutputTextContent, ResponsesOutputMessage, ResponsesOutputReasoning, ResponsesOutputReasoningContent, ResponsesOutputFunctionCall, ResponsesUsage, ResponsesResponse, AnthropicMessagesRequest, AnthropicMessagesResponse, AnthropicResponseTextBlock, AnthropicResponseToolUseBlock, AnthropicUsage, CreateOpenAIContainerBody, DeleteOpenAIContainerBody, ListOpenAIContainersResponse, OpenAIContainerRequest, OpenAIContainerSummary, ) from core.inference.anthropic_compat import ( anthropic_messages_to_openai, anthropic_tools_to_openai, anthropic_tool_choice_to_openai, openai_finish_to_anthropic_stop, anthropic_tool_use_id, build_anthropic_sse_event, AnthropicStreamEmitter, AnthropicPassthroughEmitter, ) from auth.authentication import get_current_subject from state.tool_approvals import resolve_tool_decision from core.inference.key_exchange import decrypt_api_key from core.inference.model_ids import public_model_id from core.inference.api_monitor import api_monitor from core.inference.llama_http import nonstreaming_client from core.inference.tool_call_parser import ( _strip_function_xml_calls, _strip_gemma_wrapperless_calls, _strip_glm_calls, _strip_mistral_closed_calls, ) from core.inference.tool_call_parser import TOOL_XML_SIGNALS as _PARSER_TOOL_SIGNALS from core.inference.passthrough_healing import ( StreamToolCallHealer, heal_gate, heal_openai_message, heal_openai_message_events, nudge_enabled, nudge_messages, nudge_should_retry, response_has_promotable_calls, ) from core.inference.providers import get_base_url from core.inference.external_provider import ExternalProviderClient from core.inference.chat_templates import resolve_effective_chat_template_override from storage import providers_db from utils.utils import is_hf_authentication_error, safe_error_detail, log_and_http_error import io import base64 import numpy as np from datetime import date as _date router = APIRouter() # Unsloth-only router (not mounted on /v1 OpenAI-compat). studio_router = APIRouter() # Packaged desktop runs at tauri://localhost (macOS/Linux) or http://tauri.localhost # (Windows WebView2); the web build is same-origin ('self'). The `tauri dev` shell, # however, serves the frontend from the Vite dev origin (http://localhost:5173), # so the packaged allowlist alone leaves the preview blocked in dev with an # "ancestor violates frame-ancestors" error. This shell exposes no server resource # (it only renders postMessage'd HTML in a no-same-origin sandbox), so also allowing # any localhost/127.0.0.1 dev origin to frame it is safe and unblocks the dev shell. _ARTIFACT_PREVIEW_FRAME_ANCESTORS = ( "'self' tauri://localhost http://tauri.localhost http://localhost:* http://127.0.0.1:*" ) _ARTIFACT_PREVIEW_FRAME_STRICT_CSP = ( "default-src 'none'; " "script-src 'unsafe-inline'; " "style-src 'unsafe-inline'; " "img-src data: blob:; " "font-src data:; " "media-src data: blob:; " "connect-src 'none'; " "object-src 'none'; " "base-uri 'none'; " "form-action 'none'; " f"frame-ancestors {_ARTIFACT_PREVIEW_FRAME_ANCESTORS}; " "sandbox allow-scripts" ) _ARTIFACT_PREVIEW_FRAME_NETWORK_CSP = ( "default-src http: https: data: blob:; " "script-src 'unsafe-inline' 'unsafe-eval' http: https: data: blob:; " "script-src-elem 'unsafe-inline' http: https: data: blob:; " "style-src 'unsafe-inline' http: https: data: blob:; " "style-src-elem 'unsafe-inline' http: https: data: blob:; " "img-src http: https: data: blob:; " "font-src http: https: data: blob:; " "media-src http: https: data: blob:; " "connect-src http: https: ws: wss: data: blob:; " "worker-src http: https: blob:; " "object-src 'none'; " "base-uri 'none'; " "form-action 'none'; " f"frame-ancestors {_ARTIFACT_PREVIEW_FRAME_ANCESTORS}; " "sandbox allow-scripts" ) _ARTIFACT_PREVIEW_FRAME_HTML = """
""" async def _authenticate_header_or_query(request: Request, token: Optional[str]) -> str: """Resolve the bearer token from the Authorization header or the ``?token=`` query param (needed for