Close local release-quality gaps and reconcile workplan status
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0e332-3365-77c0-8491-084e9ea33ac1
This commit is contained in:
parent
37436bb562
commit
7cd633986e
56 changed files with 462 additions and 237 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
[
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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*.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue