Source code for cadrumo.core.config_runtime_fields

"""LLM provider endpoint and runtime tuning settings layered on top of timeout config.

Defines :class:`CadrumoRuntimeSettings`, which adds the OpenAI/Gemini/Ollama endpoint
URLs and model-runtime tuning fields (context window, per-request timeouts) consumed by
the LLM-backed invoice-read pipeline.
"""

from __future__ import annotations

from pydantic import Field

from .config_timeouts import CadrumoTimeoutSettings
from .model_catalogue import ModelRole, ModelRuntime, default_model_runtime_id


[docs] class CadrumoRuntimeSettings(CadrumoTimeoutSettings): """LLM provider endpoints and runtime tuning fields layered on top of timeouts.""" cadrumo_llm_openai_chat_completions_url: str = Field( default="https://api.openai.com/v1/chat/completions", description="OpenAI Chat Completions endpoint; override for OpenAI-compatible proxies", ) cadrumo_llm_gemini_generate_content_template: str = Field( default="https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent", description="Google Gemini generateContent endpoint template (``{model}`` is substituted)", ) cadrumo_llm_ollama_chat_url: str = Field( default="http://127.0.0.1:11434/api/chat", description="Local Ollama /api/chat endpoint; override for non-localhost Ollama deployments", ) cadrumo_llm_ollama_num_ctx: int = Field( default=8192, gt=0, description=( "Ollama context window (num_ctx) for local requests. The vision read sends " "the full registry allow-list prompt plus the encoded invoice image, which " "exceeds Ollama's 4096 default; 8192 fits the prompt + image + output with " "headroom and still runs on consumer hardware" ), ) cadrumo_llm_vision_read_timeout_s: int = Field( default=300, gt=0, description=( "Per-request timeout for the on-host local vision read; larger than the " "general LLM timeout because a local vision model on consumer hardware " "(CPU or a modest GPU) can take one to several minutes to read an invoice" ), ) cadrumo_llm_ollama_vision_model: str = Field( default=default_model_runtime_id(ModelRole.VISION_TRANSCRIPTION), description=( "Local Ollama vision model used to read scanned/image evidence on-host " "(the default, gestor-allowed posture); must be a multimodal model pulled " "into the local Ollama runtime. The default is the weakest catalogued " "candidate that clears the capability bars under a COMMERCIAL licence " "posture -- qwen3-vl:2b, Apache-2.0, ~1.9 GB. It replaces qwen2.5vl:3b, " "which is measured-equivalent at this discipline but carries the Qwen " "Research licence and therefore bars commercial use. Every accepted value " "and its licence are declared in the core model catalogue; overriding to a " "research-licensed candidate is permitted and surfaces a licence advisory" ), ) cadrumo_llm_ollama_text_model: str = Field( default=default_model_runtime_id(ModelRole.TEXT_EXTRACTION), description=( "Local Ollama TEXT model used to classify a text-layer document on-host. " "Chosen under the same constraints that bind the vision default rather " "than beside it: qwen3:1.7b is Apache-2.0 and ~1.4 GB, comfortably under " "the declared memory floor, so a machine provisioned for the vision " "default is provisioned for this one. It replaces qwen2.5:3b, which is one " "of the two Qwen2.5 sizes released under the Qwen Research licence rather " "than Apache-2.0 -- easy to miss, and the reason the licence flag is " "declared per candidate. This model reads only the extracted text and " "selects from the registry allow-list; it never emits a regulated number" ), ) cadrumo_llm_ollama_mapping_model: str = Field( default=default_model_runtime_id(ModelRole.COLUMN_ROLE_MAPPING), description=( "Local Ollama model used to name what each column of a delimited table " "holds, from its header strings alone. Deliberately its own setting rather " "than sharing the text one: column-role mapping is strictly EASIER than the " "text read -- a selection over a short closed vocabulary given a handful of " "short headers, not a document read -- so it must be sizeable DOWN " "independently. Sharing the field would silently drag it upward the next " "time the text role needs a larger model. It defaults to the same qwen3:1.7b " "(Apache-2.0, ~1.4 GB), so it introduces no new hardware floor and no second " "model to pull. This model never emits a regulated number; it selects from " "the closed column-role vocabulary" ), ) cadrumo_llm_cloud_vision_model: str = Field( default=default_model_runtime_id(ModelRole.VISION_TRANSCRIPTION, ModelRuntime.CLOUD_ANTHROPIC), description=( "Hosted model used to read scanned/image evidence when a request is " "routed off-host. The cloud counterpart of " "cadrumo_llm_ollama_vision_model, and it exists for the same reason the " "local roles do: without a role-named cloud setting every off-host read " "falls through to the single global cadrumo_llm_model, so each new cloud " "consumer silently re-inherits whatever tier that happens to name. " "Defaults to the lowest-bound capable current Claude model" ), ) cadrumo_llm_cloud_text_model: str = Field( default=default_model_runtime_id(ModelRole.TEXT_EXTRACTION, ModelRuntime.CLOUD_ANTHROPIC), description=( "Hosted model used to classify an already-extracted text layer off-host. " "The cloud counterpart of cadrumo_llm_ollama_text_model. This model reads " "only extracted text and selects from the registry allow-list; it never " "emits a regulated number" ), ) cadrumo_llm_cloud_mapping_model: str = Field( default=default_model_runtime_id(ModelRole.COLUMN_ROLE_MAPPING, ModelRuntime.CLOUD_ANTHROPIC), description=( "Hosted model used to name what each column of a delimited table holds. " "The cloud counterpart of cadrumo_llm_ollama_mapping_model, kept separate " "for the same reason: column-role mapping is the easiest job in the " "product and must be sizeable DOWN independently rather than inheriting a " "harder role's model" ), ) cadrumo_llm_model_runtime_memory_floor_bytes: int = Field( default=8 * 1024**3, gt=0, description=( "Minimum total system memory the local model runtime needs before a " "vision read is worth attempting. Sized for the default vision model " "(qwen3-vl:2b, ~1.9 GB of weights) plus the 8192-token context window " "and normal OS residency, which is why the floor sits well above the " "weight size alone. Below it the runtime does not refuse -- it loads " "and thrashes, or is killed mid-read, which reaches the operator as " "an unexplained timeout rather than as a hardware shortfall. Tunable " "because the floor tracks the configured model: raise it when " "overriding upward to a larger catalogued candidate" ), ) cadrumo_llm_contention_safety_margin_bytes: int = Field( default=1024**3, ge=0, description=( "Headroom kept free ABOVE a model's declared memory requirement before " "a load is admitted. A model does not occupy exactly its weight size: " "the KV cache grows with the context window, the runtime allocates " "scratch buffers, and the display server keeps taking frames while the " "read runs. Admitting a load that fits with zero margin is how a " "measured-safe load still ends as an OOM kill mid-read. 1 GiB covers " "the default 8192-token context plus normal desktop churn; raise it on " "a machine that also drives a display, lower it on a headless box" ), ) cadrumo_llm_contention_check_override: bool = Field( default=False, description=( "Admit a local model load even when free headroom could not be " "measured. Default false: an unreadable free figure fails CLOSED at " "the act, because 'could not tell' is not evidence of headroom. Set " "true only on a machine known to have capacity that this build cannot " "read (a non-NVIDIA accelerator, or NVML absent because cadrumo[llm] " "is not installed). It does not override a MEASURED shortfall -- a " "load whose requirement exceeds measured free memory is refused " "regardless of this setting" ), ) cadrumo_llm_local_inference_concurrency: int = Field( default=1, ge=1, description=( "How many on-host inference requests this process may run at once. " "Default one, because two concurrent reads on consumer hardware are " "the same out-of-memory kill the contention check exists to prevent, " "reached by another route: each load individually fits the measured " "headroom, and together they do not. A request arriving while the " "arena is full is REFUSED rather than queued -- a queued request " "would wait holding its decoded pages in the memory under pressure, " "and would run against headroom measured before it waited. Raise it " "only on a machine with headroom for a second concurrent model load; " "it does not bound off-host dispatch, which occupies none of this " "machine's device memory" ), ) cadrumo_llm_default_max_tokens: int = Field( default=1024, gt=0, description="Default maximum output tokens when an LLM request omits ``max_tokens``", ) cadrumo_llm_default_temperature: float = Field( default=0.0, ge=0.0, # Capped where the REQUEST model caps it, not where a provider API # elsewhere allows. This value is fed straight into an LlmRequest whose # own temperature is bounded at one, so a configured 1.5 validated here # as settings and then failed at request construction -- a configuration # the app called valid and could never use. le=1.0, description="Default sampling temperature when an LLM request omits ``temperature``", ) cadrumo_browser_locale: str = Field( default="es-ES", min_length=2, description="Default browser locale passed to Playwright context (BCP-47 tag)", ) cadrumo_browser_timezone: str = Field( default="Europe/Madrid", min_length=1, description="Default IANA timezone string passed to Playwright context", ) cadrumo_browser_viewport_width: int = Field( default=1366, gt=0, description="Default Playwright viewport width (px) for AEAT sede sessions", ) cadrumo_browser_viewport_height: int = Field( default=900, gt=0, description="Default Playwright viewport height (px) for AEAT sede sessions", ) cadrumo_file_lock_timeout_s: float = Field( default=30.0, gt=0, description="Default exclusive file-lock acquisition timeout (seconds)", ) cadrumo_file_lock_retry_backoff_s: float = Field( default=0.05, gt=0, description="Sleep interval (seconds) between non-blocking file-lock acquire attempts", ) cadrumo_bucket_lock_poll_interval_s: float = Field( default=0.1, gt=0, description="Polling interval (seconds) for bucket lockfile acquisition retries", ) cadrumo_bucket_default_idle_lock_minutes: int = Field( default=15, gt=0, description="Fallback idle-lock window (minutes) when a bucket manifest omits the value", ) cadrumo_bucket_default_session_absolute_minutes: int = Field( default=240, gt=0, ge=60, le=720, description=( "Fallback absolute session-lifetime cap (minutes) when a bucket manifest omits " "session_absolute_minutes; fixed at login and never refreshed, so a touched-forever " "session still seals at this cap (default 4 h, 12 h hard ceiling)" ), ) cadrumo_auth_clave_movil_lock_buffer_s: int = Field( default=90, gt=0, description="Headroom (seconds) added to ``cadrumo_clave_movil_timeout_ms`` for the acquisition lock TTL", ) cadrumo_auth_certificate_lock_ttl_s: int = Field( default=180, gt=0, description="Acquisition lock TTL (seconds) for certificate-backed AEAT auth flows", ) cadrumo_log_stderr_level: str = Field( default="ERROR", description="Log level for the stderr handler installed by ``cadrumo.core.logging``", ) cadrumo_log_file_level: str = Field( default="DEBUG", description="Log level for the file handler installed by ``cadrumo.core.logging``", ) cadrumo_log_file_max_bytes: int = Field( default=10 * 1024 * 1024, ge=1, description=("Size cap (bytes) for cadrumo.log before the rotating file handler rolls over; default 10 MiB"), ) cadrumo_log_file_backup_count: int = Field( default=5, ge=0, description="Number of rotated cadrumo.log backups retained by the rotating file handler", ) cadrumo_log_root_level: str = Field( default="DEBUG", description="Root logger level installed by ``cadrumo.core.logging``", ) cadrumo_manuals_http_timeout_s: float = Field( default=60.0, gt=0, description="HTTP timeout (seconds) for AEAT manual PDF downloads", )