Close local release-quality gaps and reconcile workplan status
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0e332-3365-77c0-8491-084e9ea33ac1
This commit is contained in:
tegwick 2026-09-27 17:36:27 +02:00
parent 37436bb562
commit 7cd633986e
56 changed files with 462 additions and 237 deletions

View file

@ -13,14 +13,6 @@ Quick start::
"""
from llm_connect.adapter import ErrorLLMAdapter, LLMAdapter, MockLLMAdapter
from llm_connect.claude_code import ClaudeCodeAdapter
from llm_connect.config import LLMConfig, load_config
from llm_connect.costs import CostEstimate, CostModel, estimate_cost
from llm_connect.fx import FxRate, resolve_fx_rate
from llm_connect.embedding_adapter import EmbeddingAdapter
from llm_connect.embedding_cache import EmbeddingCache
from llm_connect.embedding_factory import create_embedding_adapter
from llm_connect.embedding_openai import OpenAICompatibleEmbeddingAdapter
from llm_connect.balance import (
AccountBalance,
BalanceClientRegistry,
@ -30,6 +22,13 @@ from llm_connect.balance import (
get_account_balance,
resolve_balance_provider,
)
from llm_connect.claude_code import ClaudeCodeAdapter
from llm_connect.config import LLMConfig, load_config
from llm_connect.costs import CostEstimate, CostModel, estimate_cost
from llm_connect.embedding_adapter import EmbeddingAdapter
from llm_connect.embedding_cache import EmbeddingCache
from llm_connect.embedding_factory import create_embedding_adapter
from llm_connect.embedding_openai import OpenAICompatibleEmbeddingAdapter
from llm_connect.exceptions import (
LLMAPIError,
LLMBalanceUnsupportedError,
@ -41,6 +40,7 @@ from llm_connect.exceptions import (
LLMTimeoutError,
)
from llm_connect.factory import create_adapter
from llm_connect.fx import FxRate, resolve_fx_rate
from llm_connect.gemini import GeminiAdapter
from llm_connect.grading import (
BaselineGrader,

View file

@ -4,13 +4,13 @@ from __future__ import annotations
import copy
import json
from collections.abc import Iterator, Mapping
from contextlib import contextmanager
from contextvars import ContextVar
from dataclasses import dataclass, field
from typing import Any, Iterator, Mapping
from typing import Any
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
_SECRET_QUERY_KEYS = {"key", "api_key", "apikey", "access_token", "token"}
_SECRET_HEADER_TOKENS = ("authorization", "api-key", "apikey", "token", "secret", "key")

View file

@ -7,7 +7,7 @@ Translates HTTP errors into typed :mod:`markitect.llm.exceptions`.
import json
import urllib.error
import urllib.request
from typing import Any, Dict, Optional
from typing import Any, cast
from llm_connect._diagnostics import record_provider_request, record_provider_response
from llm_connect.exceptions import (
@ -19,10 +19,10 @@ from llm_connect.exceptions import (
def post_json(
url: str,
payload: Dict[str, Any],
headers: Optional[Dict[str, str]] = None,
payload: dict[str, Any],
headers: dict[str, str] | None = None,
timeout: int = 300,
) -> Dict[str, Any]:
) -> dict[str, Any]:
"""POST *payload* as JSON and return the parsed response body.
Raises:
@ -43,9 +43,9 @@ def post_json(
def get_json(
url: str,
headers: Optional[Dict[str, str]] = None,
headers: dict[str, str] | None = None,
timeout: int = 60,
) -> Dict[str, Any]:
) -> dict[str, Any]:
"""GET *url* and return the parsed JSON response body.
Raises:
@ -67,14 +67,14 @@ def _read_json_response(
req: urllib.request.Request,
*,
timeout: int,
) -> Dict[str, Any]:
) -> dict[str, Any]:
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
body = resp.read().decode()
try:
parsed = json.loads(body)
record_provider_response(status=resp.status, body=parsed)
return parsed
return cast(dict[str, Any], parsed)
except json.JSONDecodeError as exc:
record_provider_response(status=resp.status, body=body)
preview = body[:300].replace("\n", "\\n")

View file

@ -11,7 +11,6 @@ from llm_connect._diagnostics import (
record_adapter_transformation,
)
# OpenAI Chat Completions fields that map straight through from model_params.
# Anything not in this set is provider-specific and must be either translated
# or dropped. Blind merges are deliberately avoided because OpenAI-compatible

View file

@ -7,10 +7,9 @@ multiple providers (OpenAI, Anthropic, local models, etc.).
import asyncio
from abc import ABC, abstractmethod
from typing import Dict, Any
from llm_connect.models import RunConfig, LLMResponse, BudgetTracker
from llm_connect.exceptions import LLMBudgetExceededError
from llm_connect.models import LLMResponse, RunConfig
class LLMAdapter(ABC):
@ -131,8 +130,8 @@ class MockLLMAdapter(LLMAdapter):
"""
self.mock_response = mock_response
self.call_count = 0
self.last_prompt = None
self.last_config = None
self.last_prompt: str | None = None
self.last_config: RunConfig | None = None
def execute_prompt(
self,

View file

@ -140,7 +140,7 @@ class BalanceClientRegistry:
return factory()
@classmethod
def default(cls) -> "BalanceClientRegistry":
def default(cls) -> BalanceClientRegistry:
"""Built-in registry (OpenRouter first; more backends later)."""
return cls(
{

View file

@ -7,7 +7,6 @@ import json
import os
import subprocess
from pathlib import Path
from typing import Optional
from llm_connect._diagnostics import (
record_adapter_transformation,
@ -30,9 +29,9 @@ class ClaudeCodeAdapter(LLMAdapter):
def __init__(
self,
cli_path: Optional[str] = None,
model: Optional[str] = None,
config: Optional[LLMConfig] = None,
cli_path: str | None = None,
model: str | None = None,
config: LLMConfig | None = None,
):
self._config = config or LLMConfig(provider="claude-code")
self._cli_path = cli_path or self._resolve_cli_path()
@ -124,6 +123,7 @@ class ClaudeCodeAdapter(LLMAdapter):
status=proc.returncode,
body={"stdout": stdout, "stderr": stderr},
)
assert proc.returncode is not None # communicate() has reaped the process.
if proc.returncode != 0:
raise LLMSubprocessError(
f"claude CLI exited with code {proc.returncode}",

View file

@ -2,10 +2,10 @@
LLM configuration and API key resolution.
"""
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional, Dict, Any
import os
from typing import Any
@dataclass
@ -25,19 +25,19 @@ class LLMConfig:
provider: str = "openrouter"
model: str = "moonshotai/kimi-k3"
api_key: Optional[str] = None
api_key: str | None = None
api_base: str = "https://openrouter.ai/api/v1"
claude_cli_path: str = "claude"
timeout_seconds: int = 300
max_retries: int = 3
extra: Dict[str, Any] = field(default_factory=dict)
extra: dict[str, Any] = field(default_factory=dict)
def resolve_api_key(
explicit: Optional[str] = None,
explicit: str | None = None,
env_var: str = "OPENROUTER_API_KEY",
key_file_paths: Optional[list[Path]] = None,
) -> Optional[str]:
key_file_paths: list[Path] | None = None,
) -> str | None:
"""Return an API key from the first available source.
Resolution order:
@ -65,7 +65,7 @@ def resolve_api_key(
return None
def find_project_root(start: Optional[Path] = None) -> Optional[Path]:
def find_project_root(start: Path | None = None) -> Path | None:
"""Walk up from *start* (default CWD) looking for ``pyproject.toml``.
Returns the directory containing the marker file, or ``None``.
@ -79,8 +79,8 @@ def find_project_root(start: Optional[Path] = None) -> Optional[Path]:
def load_config(
provider: str = "openrouter",
model: Optional[str] = None,
api_key: Optional[str] = None,
model: str | None = None,
api_key: str | None = None,
**overrides: Any,
) -> LLMConfig:
"""Build an :class:`LLMConfig` with sensible defaults.
@ -99,7 +99,7 @@ def load_config(
key_file_paths=key_file_paths,
)
defaults: Dict[str, Any] = {
defaults: dict[str, Any] = {
"provider": provider,
"model": model or "moonshotai/kimi-k3",
"api_key": resolved_key,

View file

@ -8,7 +8,12 @@ automatically invalidated when entity content changes.
import json
from pathlib import Path
from typing import Optional
from typing import TypedDict
class _CacheEntry(TypedDict):
digest: str
vector: list[float]
class EmbeddingCache:
@ -24,12 +29,12 @@ class EmbeddingCache:
def __init__(self, cache_dir: Path):
self._path = cache_dir / "embeddings.json"
self._data: dict[str, dict] = {}
self._data: dict[str, _CacheEntry] = {}
self._hits = 0
self._misses = 0
self._load()
def get(self, slug: str, content_digest: str) -> Optional[list[float]]:
def get(self, slug: str, content_digest: str) -> list[float] | None:
"""Return the cached vector if *content_digest* matches, else ``None``."""
entry = self._data.get(slug)
if entry is not None and entry.get("digest") == content_digest:

View file

@ -2,7 +2,8 @@
Factory for creating embedding adapters by provider name.
"""
from typing import Optional, Any
from collections.abc import Callable
from typing import Any
from llm_connect.embedding_adapter import EmbeddingAdapter
from llm_connect.exceptions import LLMConfigurationError
@ -15,8 +16,8 @@ _EMBEDDING_PROVIDERS = {
def create_embedding_adapter(
provider: str = "openai",
model: Optional[str] = None,
api_key: Optional[str] = None,
model: str | None = None,
api_key: str | None = None,
**kwargs: Any,
) -> EmbeddingAdapter:
"""Instantiate an :class:`EmbeddingAdapter` for the given *provider*.
@ -45,6 +46,6 @@ def create_embedding_adapter(
module_path, class_name = fqn.rsplit(".", 1)
import importlib
mod = importlib.import_module(module_path)
cls = getattr(mod, class_name)
cls: Callable[..., EmbeddingAdapter] = getattr(mod, class_name)
return cls(model=model, api_key=api_key, provider=provider, **kwargs)

View file

@ -8,20 +8,20 @@ API key environment variable.
"""
import time
from typing import Optional, Dict, Any
from typing import Any
from llm_connect.embedding_adapter import EmbeddingAdapter
from llm_connect.config import resolve_api_key, find_project_root
from llm_connect._http import post_json
from llm_connect.config import find_project_root, resolve_api_key
from llm_connect.embedding_adapter import EmbeddingAdapter
from llm_connect.exceptions import (
LLMConfigurationError,
LLMAPIError,
LLMConfigurationError,
LLMRateLimitError,
)
_DEFAULT_MODEL = "text-embedding-3-small"
_PROVIDER_DEFAULTS: Dict[str, Dict[str, str]] = {
_PROVIDER_DEFAULTS: dict[str, dict[str, str]] = {
"openai": {
"api_base": "https://api.openai.com/v1",
"env_var": "OPENAI_API_KEY",
@ -42,9 +42,9 @@ class OpenAICompatibleEmbeddingAdapter(EmbeddingAdapter):
def __init__(
self,
model: Optional[str] = None,
api_key: Optional[str] = None,
api_base: Optional[str] = None,
model: str | None = None,
api_key: str | None = None,
api_base: str | None = None,
provider: str = "openai",
max_retries: int = 3,
):
@ -85,7 +85,7 @@ class OpenAICompatibleEmbeddingAdapter(EmbeddingAdapter):
)
url = f"{self._api_base}/embeddings"
payload: Dict[str, Any] = {
payload: dict[str, Any] = {
"model": self._model,
"input": texts,
}
@ -105,10 +105,10 @@ class OpenAICompatibleEmbeddingAdapter(EmbeddingAdapter):
def _post_with_retries(
self,
url: str,
payload: Dict[str, Any],
headers: Dict[str, str],
) -> Dict[str, Any]:
last_exc: Optional[Exception] = None
payload: dict[str, Any],
headers: dict[str, str],
) -> dict[str, Any]:
last_exc: Exception | None = None
for attempt in range(self._max_retries + 1):
try:
return post_json(url, payload, headers)

View file

@ -2,7 +2,7 @@
LLM-specific exceptions.
"""
from typing import Optional, Dict, Any
from typing import Any
class LLMError(Exception):
@ -11,8 +11,8 @@ class LLMError(Exception):
def __init__(
self,
message: str,
cause: Optional[Exception] = None,
context: Optional[Dict[str, Any]] = None,
cause: Exception | None = None,
context: dict[str, Any] | None = None,
):
super().__init__(message)
self.cause = cause
@ -46,8 +46,8 @@ class LLMAPIError(LLMError):
message: str,
status_code: int = 0,
response_body: str = "",
cause: Optional[Exception] = None,
context: Optional[Dict[str, Any]] = None,
cause: Exception | None = None,
context: dict[str, Any] | None = None,
):
super().__init__(message, cause=cause, context=context)
self.status_code = status_code
@ -79,8 +79,8 @@ class LLMBudgetExceededError(LLMError):
total: int = 0,
spent: int = 0,
requested: int = 0,
cause: Optional[Exception] = None,
context: Optional[Dict[str, Any]] = None,
cause: Exception | None = None,
context: dict[str, Any] | None = None,
):
if context is None:
context = {"total": total, "spent": spent, "requested": requested}
@ -102,9 +102,9 @@ class LLMBalanceUnsupportedError(LLMConfigurationError):
self,
message: str,
provider: str = "",
supported: Optional[list[str]] = None,
cause: Optional[Exception] = None,
context: Optional[Dict[str, Any]] = None,
supported: list[str] | None = None,
cause: Exception | None = None,
context: dict[str, Any] | None = None,
):
supported_list = list(supported or [])
if context is None:
@ -127,8 +127,8 @@ class LLMSubprocessError(LLMError):
message: str,
return_code: int = 1,
stderr: str = "",
cause: Optional[Exception] = None,
context: Optional[Dict[str, Any]] = None,
cause: Exception | None = None,
context: dict[str, Any] | None = None,
):
super().__init__(message, cause=cause, context=context)
self.return_code = return_code

View file

@ -2,14 +2,15 @@
Factory for creating LLM adapters by provider name.
"""
import os
from typing import Optional, Dict, Any
import os
from collections.abc import Callable
from typing import Any
from llm_connect.adapter import LLMAdapter
from llm_connect.exceptions import LLMConfigurationError
# Lazy imports to avoid pulling in every adapter at module load time.
_PROVIDERS: Dict[str, str] = {
_PROVIDERS: dict[str, str] = {
"openrouter": "llm_connect.openrouter.OpenRouterAdapter",
"claude-code": "llm_connect.claude_code.ClaudeCodeAdapter",
"gemini": "llm_connect.gemini.GeminiAdapter",
@ -20,9 +21,9 @@ _PROVIDERS: Dict[str, str] = {
def create_adapter(
provider: str = "openrouter",
model: Optional[str] = None,
api_key: Optional[str] = None,
system_prompt: Optional[str] = None,
model: str | None = None,
api_key: str | None = None,
system_prompt: str | None = None,
**kwargs: Any,
) -> LLMAdapter:
"""Instantiate an :class:`LLMAdapter` for the given *provider*.
@ -52,7 +53,7 @@ def create_adapter(
module_path, class_name = fqn.rsplit(".", 1)
import importlib
mod = importlib.import_module(module_path)
cls = getattr(mod, class_name)
cls: Callable[..., LLMAdapter] = getattr(mod, class_name)
if provider in ("openrouter", "gemini", "openai"):
return cls(model=model, api_key=api_key, system_prompt=system_prompt, **kwargs)

View file

@ -10,7 +10,6 @@ import os
from dataclasses import dataclass
from typing import Any
# Snapshot: euros per one US dollar. Operator can override via env.
DEFAULT_EUR_PER_USD = 0.92
DEFAULT_FX_CAPTURED_AT = "2026-08-03"

View file

@ -3,14 +3,14 @@ Google Gemini adapter — calls the Generative Language REST API directly.
"""
import time
from typing import Optional, Dict, Any
from typing import Any
from llm_connect.adapter import LLMAdapter
from llm_connect.models import RunConfig, LLMResponse
from llm_connect.config import resolve_api_key, find_project_root
from llm_connect._http import post_json
from llm_connect._payload import merge_gemini_model_params
from llm_connect.adapter import LLMAdapter
from llm_connect.config import find_project_root, resolve_api_key
from llm_connect.exceptions import LLMConfigurationError
from llm_connect.models import LLMResponse, RunConfig
_DEFAULT_MODEL = "gemini-2.5-flash"
_API_BASE = "https://generativelanguage.googleapis.com/v1beta"
@ -24,9 +24,9 @@ class GeminiAdapter(LLMAdapter):
def __init__(
self,
model: Optional[str] = None,
api_key: Optional[str] = None,
system_prompt: Optional[str] = None,
model: str | None = None,
api_key: str | None = None,
system_prompt: str | None = None,
**_kwargs: Any,
):
self._model = model or _DEFAULT_MODEL
@ -53,7 +53,7 @@ class GeminiAdapter(LLMAdapter):
model = self._model
# Build Gemini request
contents: list[Dict[str, Any]] = []
contents: list[dict[str, Any]] = []
if self._system_prompt:
contents.append({
"role": "user",
@ -68,7 +68,7 @@ class GeminiAdapter(LLMAdapter):
"parts": [{"text": prompt}],
})
payload: Dict[str, Any] = {
payload: dict[str, Any] = {
"contents": contents,
"generationConfig": {
"temperature": config.temperature,

View file

@ -18,7 +18,7 @@ from llm_connect.models import LLMResponse, RunConfig
from llm_connect.similarity import cosine_similarity
def _validate_score(value: float) -> float:
def _validate_score(value: object) -> float:
if not isinstance(value, (int, float)):
raise ValueError("quality_score must be a number between 0 and 1")
score = float(value)

View file

@ -7,7 +7,7 @@ markitect.prompts.execution.models for backward compatibility.
import threading
from dataclasses import dataclass, field
from typing import Dict, Any, Optional
from typing import Any, Optional
from llm_connect.exceptions import LLMBudgetExceededError
@ -70,13 +70,13 @@ class RunConfig:
model_name: str = "gpt-4"
temperature: float = 0.7
max_tokens: int = 2000
model_params: Dict[str, Any] = field(default_factory=dict)
model_params: dict[str, Any] = field(default_factory=dict)
max_depth: int = 3
skip_if_exists: bool = True
timeout_seconds: int = 300
budget_tracker: Optional["BudgetTracker"] = field(default=None, repr=False)
def to_dict(self) -> Dict[str, Any]:
def to_dict(self) -> dict[str, Any]:
"""Convert to dictionary. ``budget_tracker`` is excluded (runtime object)."""
return {
"model_name": self.model_name,
@ -89,7 +89,7 @@ class RunConfig:
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "RunConfig":
def from_dict(cls, data: dict[str, Any]) -> "RunConfig":
"""Create from dictionary."""
return cls(
model_name=data.get("model_name", "gpt-4"),
@ -116,11 +116,11 @@ class LLMResponse:
"""
content: str
model: str
usage: Dict[str, int] = field(default_factory=dict)
usage: dict[str, int] = field(default_factory=dict)
finish_reason: str = "stop"
metadata: Dict[str, Any] = field(default_factory=dict)
metadata: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
def to_dict(self) -> dict[str, Any]:
"""Convert to dictionary."""
return {
"content": self.content,

View file

@ -3,18 +3,18 @@ OpenAI (ChatGPT) adapter — calls the OpenAI chat completions API.
"""
import time
from typing import Optional, Dict, Any
from typing import Any
from llm_connect.adapter import LLMAdapter
from llm_connect.models import RunConfig, LLMResponse
from llm_connect.config import resolve_api_key, find_project_root
from llm_connect._http import post_json
from llm_connect._payload import merge_openai_chat_model_params
from llm_connect.adapter import LLMAdapter
from llm_connect.config import find_project_root, resolve_api_key
from llm_connect.exceptions import (
LLMConfigurationError,
LLMAPIError,
LLMConfigurationError,
LLMRateLimitError,
)
from llm_connect.models import LLMResponse, RunConfig
_DEFAULT_MODEL = "gpt-4.1-mini"
_API_BASE = "https://api.openai.com/v1"
@ -25,9 +25,9 @@ class OpenAIAdapter(LLMAdapter):
def __init__(
self,
model: Optional[str] = None,
api_key: Optional[str] = None,
system_prompt: Optional[str] = None,
model: str | None = None,
api_key: str | None = None,
system_prompt: str | None = None,
max_retries: int = 3,
**_kwargs: Any,
):
@ -55,12 +55,12 @@ class OpenAIAdapter(LLMAdapter):
self._preflight_budget(config)
model = self._model
messages: list[Dict[str, str]] = []
messages: list[dict[str, str]] = []
if self._system_prompt:
messages.append({"role": "system", "content": self._system_prompt})
messages.append({"role": "user", "content": prompt})
payload: Dict[str, Any] = {
payload: dict[str, Any] = {
"model": model,
"messages": messages,
"temperature": config.temperature,
@ -114,11 +114,11 @@ class OpenAIAdapter(LLMAdapter):
def _post_with_retries(
self,
url: str,
payload: Dict[str, Any],
headers: Dict[str, str],
payload: dict[str, Any],
headers: dict[str, str],
timeout: int,
) -> Dict[str, Any]:
last_exc: Optional[Exception] = None
) -> dict[str, Any]:
last_exc: Exception | None = None
for attempt in range(self._max_retries + 1):
try:
return post_json(url, payload, headers, timeout=timeout)

View file

@ -3,7 +3,7 @@ OpenRouter adapter - calls the OpenAI-compatible chat completions API.
"""
import time
from typing import Any, Dict, Optional
from typing import Any
from llm_connect._http import post_json
from llm_connect._payload import merge_openai_chat_model_params
@ -25,13 +25,13 @@ class OpenRouterAdapter(LLMAdapter):
def __init__(
self,
model: Optional[str] = None,
api_key: Optional[str] = None,
api_base: Optional[str] = None,
config: Optional[LLMConfig] = None,
system_prompt: Optional[str] = None,
extra_headers: Optional[Dict[str, str]] = None,
max_retries: Optional[int] = None,
model: str | None = None,
api_key: str | None = None,
api_base: str | None = None,
config: LLMConfig | None = None,
system_prompt: str | None = None,
extra_headers: dict[str, str] | None = None,
max_retries: int | None = None,
):
self._config = config or LLMConfig()
# Track whether the model was explicitly supplied (constructor or
@ -69,12 +69,12 @@ class OpenRouterAdapter(LLMAdapter):
else:
model = config.model_name or self._model
messages: list[Dict[str, str]] = []
messages: list[dict[str, str]] = []
if self._system_prompt:
messages.append({"role": "system", "content": self._system_prompt})
messages.append({"role": "user", "content": prompt})
payload: Dict[str, Any] = {
payload: dict[str, Any] = {
"model": model,
"messages": messages,
"temperature": config.temperature,
@ -137,11 +137,11 @@ class OpenRouterAdapter(LLMAdapter):
def _post_with_retries(
self,
url: str,
payload: Dict[str, Any],
headers: Dict[str, str],
payload: dict[str, Any],
headers: dict[str, str],
timeout: int,
) -> Dict[str, Any]:
last_exc: Optional[Exception] = None
) -> dict[str, Any]:
last_exc: Exception | None = None
for attempt in range(self._max_retries + 1):
try:
return post_json(url, payload, headers, timeout=timeout)
@ -158,6 +158,6 @@ class OpenRouterAdapter(LLMAdapter):
raise last_exc # type: ignore[misc]
def _uses_json_schema_response_format(payload: Dict[str, Any]) -> bool:
def _uses_json_schema_response_format(payload: dict[str, Any]) -> bool:
response_format = payload.get("response_format")
return isinstance(response_format, dict) and response_format.get("type") == "json_schema"

View file

@ -6,7 +6,6 @@ from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from typing import Any, Protocol
DEFAULT_WORDS_PER_TOKEN = 0.75
@ -66,7 +65,7 @@ class ProblemClass(Protocol):
observations: Sequence[Any],
*,
min_observations: int = 3,
) -> "ProblemClass":
) -> ProblemClass:
"""Return an estimator with params adapted from observed token use."""
...
@ -99,7 +98,7 @@ class ProblemClassRegistry:
self._classes[name] = problem_class
@classmethod
def default(cls) -> "ProblemClassRegistry":
def default(cls) -> ProblemClassRegistry:
"""Return the built-in problem-class registry."""
return cls(
[

View file

@ -5,9 +5,10 @@ from __future__ import annotations
import json
import os
import threading
from collections.abc import Callable, Mapping
from dataclasses import dataclass, field, replace
from pathlib import Path
from typing import Any, Callable, Mapping
from typing import Any
from llm_connect.adapter import LLMAdapter
from llm_connect.exceptions import LLMConfigurationError

View file

@ -8,13 +8,14 @@ from __future__ import annotations
import json
import os
import sys
import threading
from collections.abc import Iterator
from contextlib import contextmanager
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Iterator, TextIO
from typing import Any, Literal, TextIO
_PATH_LOCKS: dict[Path, threading.Lock] = {}
_PATH_LOCKS_GUARD = threading.Lock()
@ -62,7 +63,7 @@ def _path_lock(path: Path) -> threading.Lock:
def _lock_file(handle: TextIO) -> None:
if os.name == "nt":
if sys.platform == "win32":
import msvcrt
msvcrt.locking(handle.fileno(), msvcrt.LK_LOCK, 1)
@ -73,7 +74,7 @@ def _lock_file(handle: TextIO) -> None:
def _unlock_file(handle: TextIO) -> None:
if os.name == "nt":
if sys.platform == "win32":
import msvcrt
msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
@ -84,7 +85,7 @@ def _unlock_file(handle: TextIO) -> None:
@contextmanager
def _locked_file(path: Path, mode: str) -> Iterator[TextIO]:
def _locked_file(path: Path, mode: Literal["a", "a+", "r"]) -> Iterator[TextIO]:
path.parent.mkdir(parents=True, exist_ok=True)
local_lock = _path_lock(path)
with local_lock:
@ -157,7 +158,7 @@ class QualityObservation:
}
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "QualityObservation":
def from_dict(cls, data: dict[str, Any]) -> QualityObservation:
"""Create an observation from a JSON-decoded dictionary."""
return cls(
task_type=data["task_type"],

View file

@ -7,7 +7,6 @@ from dataclasses import dataclass
from pathlib import Path
from typing import Any
DEFAULT_RATE_SOURCE_URL = "https://openrouter.ai/models"
DEFAULT_RATE_CAPTURED_AT = "2026-05-17"
DEFAULT_RATE_CURRENCY = "USD"
@ -60,12 +59,12 @@ class ModelRateRegistry:
return dict(self._rates)
@classmethod
def default(cls) -> "ModelRateRegistry":
def default(cls) -> ModelRateRegistry:
"""Return the bundled OpenRouter list-price snapshot."""
return cls(_default_rate_payload())
@classmethod
def from_yaml(cls, path: Path | str) -> "ModelRateRegistry":
def from_yaml(cls, path: Path | str) -> ModelRateRegistry:
"""Load rates from a YAML file.
The expected shape matches the historic infospace-bench table::
@ -84,7 +83,7 @@ class ModelRateRegistry:
payload = _load_yaml_mapping(Path(path))
return cls(_rates_from_payload(payload))
def merged_with(self, override: "ModelRateRegistry") -> "ModelRateRegistry":
def merged_with(self, override: ModelRateRegistry) -> ModelRateRegistry:
"""Return a new registry where *override* entries win by model id."""
merged = self.all()
merged.update(override.all())
@ -111,9 +110,9 @@ def _default_rate_payload() -> dict[str, ModelRate]:
rates: dict[str, ModelRate] = {}
for model_id, values in _DEFAULT_RATES.items():
if len(values) == 3:
prompt_rate, completion_rate, captured_at = values # type: ignore[misc]
prompt_rate, completion_rate, captured_at = values
else:
prompt_rate, completion_rate = values # type: ignore[misc]
prompt_rate, completion_rate = values
captured_at = DEFAULT_RATE_CAPTURED_AT
rates[model_id] = ModelRate(
model_id=model_id,

View file

@ -5,7 +5,7 @@ from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
from typing import Any, cast
from llm_connect.claude_code import _unwrap_cli_json_envelope
from llm_connect.models import RunConfig
@ -51,7 +51,7 @@ def _parse_provider_response(provider: str | None, body: Any, config: RunConfig)
if provider in {"openai", "openrouter"}:
if isinstance(body, dict):
choice = (body.get("choices") or [{}])[0]
return choice.get("message", {}).get("content", "")
return cast(str, choice.get("message", {}).get("content", ""))
return ""
if provider == "gemini":

View file

@ -4,9 +4,9 @@ RoutingPolicy — task-type-aware adapter selection (FR-2).
Maps task types to preferred adapters with optional cost-cap fallback.
"""
from collections.abc import Mapping
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from typing import List, Mapping, Optional
from llm_connect.adapter import LLMAdapter
from llm_connect.quality import QualityLedger, QualityObservation
@ -27,8 +27,8 @@ class RoutingRule:
task_type: str
prefer: LLMAdapter
max_cost_per_1k: Optional[float] = None
fallback: Optional[LLMAdapter] = None
max_cost_per_1k: float | None = None
fallback: LLMAdapter | None = None
@dataclass
@ -50,13 +50,13 @@ class RoutingPolicy:
adapter = policy.resolve("triage")
"""
rules: List[RoutingRule] = field(default_factory=list)
default: Optional[LLMAdapter] = None
rules: list[RoutingRule] = field(default_factory=list)
default: LLMAdapter | None = None
def resolve(
self,
task_type: str,
estimated_cost_per_1k: Optional[float] = None,
estimated_cost_per_1k: float | None = None,
) -> LLMAdapter:
"""Return the adapter for *task_type*.
@ -111,11 +111,11 @@ class AdaptiveRoutingPolicy(RoutingPolicy):
caller can use the same policy on day zero and after observations accrue.
"""
ledger: Optional[QualityLedger] = None
ledger: QualityLedger | None = None
adapters_by_id: Mapping[str, LLMAdapter] = field(default_factory=dict)
window_size: int = 20
min_observations: int = 1
max_age: Optional[timedelta] = None
max_age: timedelta | None = None
def __post_init__(self) -> None:
if self.window_size <= 0:
@ -128,9 +128,9 @@ class AdaptiveRoutingPolicy(RoutingPolicy):
def resolve(
self,
task_type: str,
estimated_cost_per_1k: Optional[float] = None,
estimated_cost_per_1k: float | None = None,
*,
quality_floor: Optional[float] = None,
quality_floor: float | None = None,
) -> LLMAdapter:
"""Return the adaptive adapter for *task_type*.

View file

@ -30,14 +30,14 @@ import time
import uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Optional
from typing import Any
from urllib.parse import parse_qs, urlsplit
from llm_connect._diagnostics import capture_diagnostics
from llm_connect.adapter import LLMAdapter
from llm_connect.exceptions import (
LLMBudgetExceededError,
LLMAPIError,
LLMBudgetExceededError,
LLMConfigurationError,
LLMError,
LLMRateLimitError,
@ -48,15 +48,21 @@ from llm_connect.profiles import ProfiledLLMAdapter, default_runtime_profiles
from llm_connect.usage import maybe_record_usage, suppress_auto_usage_record
class _AdapterHTTPServer(ThreadingHTTPServer):
adapter: LLMAdapter
class _Handler(BaseHTTPRequestHandler):
"""Request handler — adapter injected via server.adapter."""
def log_message(self, format, *args): # suppress default access log
server: _AdapterHTTPServer
def log_message(self, format: str, *args: Any) -> None: # suppress default access log
pass
# ── GET ────────────────────────────────────────────────────────
def do_GET(self):
def do_GET(self) -> None:
parsed = urlsplit(self.path)
if parsed.path == "/health":
self._respond(200, {"status": "ok"})
@ -65,7 +71,7 @@ class _Handler(BaseHTTPRequestHandler):
# ── POST ───────────────────────────────────────────────────────
def do_POST(self):
def do_POST(self) -> None:
parsed = urlsplit(self.path)
if parsed.path != "/execute":
self._respond(404, {"error": "not found"})
@ -96,7 +102,7 @@ class _Handler(BaseHTTPRequestHandler):
diagnostics_enabled = debug_enabled or bool(audit_dir)
try:
with capture_diagnostics(diagnostics_enabled) as diagnostics:
adapter = self.server.adapter # type: ignore[attr-defined]
adapter = self.server.adapter
if not adapter.validate_config(config):
raise LLMConfigurationError(
"Adapter rejected RunConfig",
@ -152,9 +158,9 @@ class LLMServer:
host: str = "127.0.0.1",
port: int = 8080,
) -> None:
self._httpd = ThreadingHTTPServer((host, port), _Handler)
self._httpd.adapter = adapter # type: ignore[attr-defined]
self._thread: Optional[threading.Thread] = None
self._httpd = _AdapterHTTPServer((host, port), _Handler)
self._httpd.adapter = adapter
self._thread: threading.Thread | None = None
@property
def port(self) -> int:
@ -163,7 +169,7 @@ class LLMServer:
@property
def host(self) -> str:
return self._httpd.server_address[0]
return str(self._httpd.server_address[0])
def start(self) -> None:
"""Start serving in a daemon background thread."""
@ -185,7 +191,7 @@ class LLMServer:
def _build_adapter(
provider: str,
model: Optional[str],
model: str | None,
*,
enable_profiles: bool = True,
strict_profiles: bool = False,
@ -240,7 +246,7 @@ def _error_response(exc: Exception) -> tuple[int, dict]:
def _error_body(code: str, exc: Exception) -> dict:
body = {
body: dict[str, Any] = {
"error": code,
"message": _sanitize_text(_message(exc)),
"type": exc.__class__.__name__,
@ -260,7 +266,7 @@ def _message(exc: Exception) -> str:
def _safe_context(context: dict) -> dict:
safe = {}
safe: dict[str, Any] = {}
for key, value in context.items():
lowered = str(key).lower()
if any(secret_word in lowered for secret_word in ("key", "secret", "token", "password")):
@ -321,7 +327,7 @@ def _safe_filename(value: str) -> str:
return re.sub(r"[^A-Za-z0-9_.-]+", "-", value).strip("-") or "response"
def main(argv=None) -> None:
def main(argv: list[str] | None = None) -> None:
parser = argparse.ArgumentParser(
prog="python -m llm_connect.server",
description="Start llm_connect HTTP serve mode.",

View file

@ -5,9 +5,10 @@ from __future__ import annotations
import asyncio
import random
import threading
from collections.abc import Callable, Mapping
from concurrent.futures import Future, ThreadPoolExecutor
from dataclasses import dataclass, field, replace
from typing import Any, Callable, Mapping
from typing import Any
from llm_connect.adapter import LLMAdapter
from llm_connect.grading import BaselineGrader

View file

@ -18,7 +18,6 @@ Resolution order (highest → lowest):
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
import toml
@ -55,8 +54,8 @@ def _dir_config_name(app_name: str) -> str:
@dataclass
class LLMLayer:
"""One layer of provider/model configuration (may be partial)."""
provider: Optional[str] = None
model: Optional[str] = None
provider: str | None = None
model: str | None = None
@dataclass
@ -129,7 +128,7 @@ def _clear_llm_section(path: Path, section: str) -> bool:
# ── Directory config path helper ─────────────────────────────────────────
def _dir_config_path(app_name: str = "markitect") -> Optional[Path]:
def _dir_config_path(app_name: str = "markitect") -> Path | None:
root = find_project_root()
if root is None:
return None
@ -139,8 +138,8 @@ def _dir_config_path(app_name: str = "markitect") -> Optional[Path]:
# ── Resolution ───────────────────────────────────────────────────────────
def resolve_llm(
cli_provider: Optional[str] = None,
cli_model: Optional[str] = None,
cli_provider: str | None = None,
cli_model: str | None = None,
app_name: str = "markitect",
) -> ResolvedLLM:
"""Walk the 7-level priority chain and return a fully resolved config.

View file

@ -9,12 +9,14 @@ from __future__ import annotations
import contextvars
import json
import os
import sys
import threading
from collections.abc import Iterator
from contextlib import contextmanager
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Iterator, TextIO
from typing import Any, Literal, TextIO
from zoneinfo import ZoneInfo
from llm_connect.costs import CostEstimate, estimate_cost
@ -22,7 +24,6 @@ from llm_connect.fx import FxRate
from llm_connect.models import LLMResponse
from llm_connect.rates import ModelRateRegistry
ENV_USAGE_LEDGER = "LLM_CONNECT_USAGE_LEDGER"
ENV_TZ = "LLM_CONNECT_TZ"
DEFAULT_TZ = "Europe/Berlin"
@ -82,7 +83,7 @@ def _path_lock(path: Path) -> threading.Lock:
def _lock_file(handle: TextIO) -> None:
if os.name == "nt":
if sys.platform == "win32":
import msvcrt
msvcrt.locking(handle.fileno(), msvcrt.LK_LOCK, 1)
@ -93,7 +94,7 @@ def _lock_file(handle: TextIO) -> None:
def _unlock_file(handle: TextIO) -> None:
if os.name == "nt":
if sys.platform == "win32":
import msvcrt
msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
@ -104,7 +105,7 @@ def _unlock_file(handle: TextIO) -> None:
@contextmanager
def _locked_file(path: Path, mode: str) -> Iterator[TextIO]:
def _locked_file(path: Path, mode: Literal["a", "a+", "r"]) -> Iterator[TextIO]:
path.parent.mkdir(parents=True, exist_ok=True)
local_lock = _path_lock(path)
with local_lock:
@ -201,7 +202,7 @@ class UsageEvent:
}
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "UsageEvent":
def from_dict(cls, data: dict[str, Any]) -> UsageEvent:
"""Create an event from a JSON-decoded dictionary."""
return cls(
provider=data["provider"],