diff --git a/.custodian-brief.md b/.custodian-brief.md index bfddaaf..0902046 100644 --- a/.custodian-brief.md +++ b/.custodian-brief.md @@ -2,7 +2,7 @@ # Custodian Brief — llm-connect **Domain:** agents -**Last synced:** 2026-09-09 19:47 UTC +**Last synced:** 2026-09-27 14:15 UTC **State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)* ## Active Workstreams @@ -12,7 +12,7 @@ Progress: 3/4 done | workplan_id: `d396a090-c2ed-5042-aaea-b75fd1d3a471` **Open tasks:** - ! Integrate the admitted owner route and prove production confinement `98a38d75` - *(wait: Requires trusted owner hosting and actual lease/token delivery, provider custody and direct-route denial under HFACT T03/T04; accepted tariff/FX and G0 remain HFACT T01.)* + *(wait: Local Unix hosting, worker lease/token lifecycle and bwrap confinement proved; requires admitted credential-to-owner bootstrap, matched protected artifact and Railiance custody/placement under HFACT T03/T04; live tariff/FX and G0 remain HFACT T01.)* --- ## MCP Orientation (when available) diff --git a/Makefile b/Makefile index fb737d6..ea11d90 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ -include .env export -.PHONY: install test lint typecheck start help +.PHONY: install test lint typecheck check start help UV ?= $(shell command -v uv 2>/dev/null || if [ -x "$$HOME/.local/bin/uv" ]; then printf "%s" "$$HOME/.local/bin/uv"; else printf "%s" "uv"; fi) @@ -23,6 +23,8 @@ lint: ## Run ruff linter typecheck: ## Run mypy type checker $(UV) run mypy llm_connect +check: lint typecheck test ## Run all release quality checks + start: ## Start llm-connect HTTP server (reads .env and LLM_CONNECT_* overrides) $(UV) run python -m llm_connect.server \ --host $(LLM_CONNECT_HOST) \ @@ -34,4 +36,4 @@ start: ## Start llm-connect HTTP server (reads .env and LLM_CONNECT_* overrides help: ## Show this help message @grep -Eh '^[a-zA-Z_-]+:.*?##' $(MAKEFILE_LIST) | \ awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-16s\033[0m %s\n", $$1, $$2}' | \ - sort \ No newline at end of file + sort diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 4424ad4..ea8a921 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -12,11 +12,11 @@ | workplan | LLM-WP-0006 | finished | — | workplans/LLM-WP-0006-activity-core-always-on-endpoint.md | | workplan | LLM-WP-0007 | finished | — | workplans/LLM-WP-0007-kimi-k3-default-and-spend-reporting.md | | workplan | LLM-WP-0008 | finished | — | workplans/LLM-WP-0008-provider-account-balance-cli.md | -| workplan | LLM-WP-0009 | active | — | workplans/LLM-WP-0009-owner-metered-messages-transport.md | -| workplan | LLM-WP-0001 | completed | — | workplans/llm-connect-WP-0001-foundation-gaaf-baseline.md | -| workplan | LLM-WP-0002 | completed | — | workplans/llm-connect-WP-0002-core-extensions.md | -| workplan | LLM-WP-0003 | completed | — | workplans/llm-connect-WP-0003-functional-extensions.md | -| workplan | LLM-WP-0004 | completed | — | workplans/llm-connect-WP-0004-adaptive-cost-quality-routing.md | +| workplan | LLM-WP-0009 | blocked | — | workplans/LLM-WP-0009-owner-metered-messages-transport.md | +| workplan | LLM-WP-0001 | finished | — | workplans/llm-connect-WP-0001-foundation-gaaf-baseline.md | +| workplan | LLM-WP-0002 | finished | — | workplans/llm-connect-WP-0002-core-extensions.md | +| workplan | LLM-WP-0003 | finished | — | workplans/llm-connect-WP-0003-functional-extensions.md | +| workplan | LLM-WP-0004 | finished | — | workplans/llm-connect-WP-0004-adaptive-cost-quality-routing.md | | workplan | LLM-WP-0005 | finished | — | workplans/llm-connect-WP-0005-cost-model-and-problem-class-estimators.md | | task | LLM-WP-ADHOC-2026-06-02-T01 | done | — | workplans/ADHOC-2026-06-02.md | | task | LLM-WP-ADHOC-2026-06-02-T02 | done | — | workplans/ADHOC-2026-06-02.md | diff --git a/contracts/functional/messages-admission.md b/contracts/functional/messages-admission.md index 5d06866..196acce 100644 --- a/contracts/functional/messages-admission.md +++ b/contracts/functional/messages-admission.md @@ -8,12 +8,14 @@ REINAH-WP-0003-T05/T06. ## Contract The owner constructs immutable `MessagesPolicy` with an exact model, tariff -reference, maximum admitted context/output, integer micro-USD per-token upper +reference, input liability context/output ceiling, integer micro-USD per-token upper rates, explicit beta allowlist, body limit and request timeout. There is no built-in live price, FX source, token estimate or default beta grant. Input rates must conservatively cover input, both cache-write lifetimes, cache reads and all accepted multipliers. Accepted provider limits and tariff validity remain an -operator policy responsibility; a fixture policy cannot establish them. +operator policy responsibility; a fixture policy cannot establish them. The input +context is a reservation assumption, not an enforced token cap: it must cover +the provider's maximum accepted context for the admitted model/features. `RequestMeter.reserve_request(token, policy_sha256, liability_microusd)` must atomically check authority and remaining parent capacity, persist the hold and @@ -57,14 +59,18 @@ the parent already binds worker, definition, project, target, grant and runtime digests. Route tokens are random, stored only as hashes, cannot be rebound or renewed by the workload, and revoke on parent terminal observation. -The next integration must host the listener in the trusted owner boundary, keep -provider credentials and ledger inaccessible to the sandbox, deliver only its -run token/base URL, bind actual lease loss to route revocation, and prove direct -provider and alternate-route denial. This module does not install a listener, -configure a sandbox, resolve credentials, or promote a profile. The installed -CLI fixture demonstrates transport/ledger behavior in a fake-provider namespace; -it does **not** prove secret or network separation between real owner/workload -processes. LLM-WP-0009-T03 retains this owner integration return. +Rein's `MessagesOwner` now hosts the private listener, binds the accepted queue +lease, delivers only the run token/base URL, and revokes on lease loss or exit. +Real local bwrap tests prove owner/workload state separation and direct-route +denial using a fake provider. The factory's 2026-09-10 placement receipt also +records synthetic checks of a pinned protected artifact on Railiance. The +2026-09-27 return additionally records the approved corrected owner/Secrets Engine +installation and a synthetic two-request tool/result proof on runtime `b6e4e8a4`, +profile `harness.agent-dev-local@1.1.1`. The inactive EUR 10 proposal and remaining +native approvals are recorded in LLM-WP-0009-T03; no paid proof is claimed. +This module does not resolve credentials or promote a profile. Native credential +delivery, service/profile admission and recovery, accepted live provider policy, +and admitted model/queue evidence remain open under LLM-WP-0009-T03. ## Verification and primary protocol references @@ -91,6 +97,6 @@ At most 16 request handlers are active; idle header reads time out. TCP host/por selection cannot coexist with Unix mode. The protocol and durable meter interface are unchanged. Rein's MessagesOwner supplies the private listener, accepted lease and cancellation hooks; sand-boxer mounts only the socket and enforces sole routing. -The provider key and ledger remain outside the workload. This source/library path -is tested with a fake provider; accepted custody, protected bootstrap/artifact and -Railiance placement are still required by LLM-WP-0009-T03. +The provider key and ledger remain outside the workload. The source/library path +and installed bootstrap/artifact were tested with a fake provider; accepted +native custody and service admission remain required by LLM-WP-0009-T03. diff --git a/docs/evidence/2026-09-27-request-admission-quality.json b/docs/evidence/2026-09-27-request-admission-quality.json new file mode 100644 index 0000000..f0204d4 --- /dev/null +++ b/docs/evidence/2026-09-27-request-admission-quality.json @@ -0,0 +1,92 @@ +{ + "date": "2026-09-27", + "base_revision": "37436bb562a3f20abc3b88defc5bae6508c32e7e", + "scope": "Source release-quality checks and repository loose-end review; no installed artifact or production acceptance", + "before": { + "ruff_errors": 177, + "mypy_errors": 36 + }, + "after": { + "command": "make check", + "exit_code": 0, + "ruff_errors": 0, + "mypy_errors": 0, + "typechecked_source_files": 35, + "tests_passed": 264, + "tests_skipped": 0 + }, + "changes_sha256": { + "Makefile": "8c2dc0248a53808782ab9258755fe15971a6061ae0b1c2a2f3c0cd57ad7daf11", + "contracts/functional/messages-admission.md": "e1385f99f7929f18594fbf9cc90ed9b87448cbe3b7fae6388e8546c5ba55bfd8", + "examples/adaptive_routing_fixture_batch.py": "f23c58ff662bb38c852e6c70b6ee31f8aa0331ec9311e07821b81bf21e5c1c55", + "llm_connect/__init__.py": "e921043aa3fbc3a59bbce637dc38abe9b0e1ab30493bb0b27d8e0c9463b23a13", + "llm_connect/_diagnostics.py": "51ce22556ab2c7d7b4c7d7ffd4741d6e3c231d073366d9cbeb0b89b7ad2c85ae", + "llm_connect/_http.py": "df309e5dbcb4c163bfe34314e37c5ab8f1c54a4ab55da4102cd143c2940a2356", + "llm_connect/_payload.py": "a598e73d5dc325eda01510ced71c3ee02fb98f296e79189401b998d07bade7e3", + "llm_connect/adapter.py": "7ba82b35306526cc58deebd3b4e5f3ec13d177d0794312f734d652ff9033cf5d", + "llm_connect/balance.py": "2ddc3e5f60bd5e685c59baa191ed8e6e9d5730fe509ffe96a1b1d38c0e865ca7", + "llm_connect/claude_code.py": "5fcf33936d007d892d9eefb4720559e36b6d010a5d6b3830b5273fd6266c5b26", + "llm_connect/config.py": "2da9511caa44cc0fa9c8f0797d8b28c9b964f0d06d48f63b317aaf3586dbe835", + "llm_connect/embedding_cache.py": "00e7b2c891c1666c7799a70270416819382bed5d5e7776384fa10c906b52f08c", + "llm_connect/embedding_factory.py": "ca510a1148d63a4adde76b19a41478b795175be70c2d66afc4d8381b01303849", + "llm_connect/embedding_openai.py": "9a03832ca703d90cc222df300c7d485da406200f8d3c35446e210a4a53dddaf2", + "llm_connect/exceptions.py": "c459934f1877ae1f44ac50c5cf2630a2b35e4a3525b131b2a0bfd08ac05357f8", + "llm_connect/factory.py": "3cd9f74ce49cb076c71496018b3a8525de98d5e9951513a7e4ade27e26273a80", + "llm_connect/fx.py": "455f32b28c01371f48102dccc391b0c8536c0ba4f6a18357dc4b22620336b0ac", + "llm_connect/gemini.py": "1a5f7f2c69879a95946c6a74b8a0ef05d5fdb070656d973d7a9d513050203b8f", + "llm_connect/grading.py": "eb1ec18b6442d45c043da41aae74732e510281ff90c045c3088841d288703380", + "llm_connect/models.py": "edc10e34a6f3f45b9d5fccb06c0b66752381bbb360976aa3446ce862d9082eab", + "llm_connect/openai.py": "1793730584d4d6f5da3be1de5aca9a04af03548734a688576c997c9336699256", + "llm_connect/openrouter.py": "6b2e19dd3ab87ab028295d1e36688fff3b4e18055620f1eace3ca66af1ae6aff", + "llm_connect/problem_classes.py": "4a046f3ca544a40dd712f3c9b0d1ecd9eb2e087cc6348ecf468c420a6089001a", + "llm_connect/profiles.py": "ab3f6af59f28861c05ec320321e636f08a8eac140c726c36c7677c9985f8055d", + "llm_connect/quality.py": "7ff616287d788e86ae8a0ed069d1a7e0b89e319b7f42d476941e15c91ba72ccc", + "llm_connect/rates.py": "71186fb944065707c0f873cd0923fd2b24125a8013851d54e2610fcdbf0c08c4", + "llm_connect/replay.py": "c0d7d33d95167c09d940c04e01969ea6670436831f723b66b96f41bad612ded0", + "llm_connect/routing.py": "35e28e2e06b637b506ddd4f37dd79fad60fe785c412a4843e3117417168acc3f", + "llm_connect/server.py": "33ccff82b52b9181d40629e90500be5ce75d4dd08afe5113d75d9d6b9cf83579", + "llm_connect/shadowing.py": "c9f649bda3a4306d506554befd702e70fb3f32761502273cc338a147e60238e1", + "llm_connect/toml_config.py": "5d14c35e6e6aca6006eed60c9caa63d1dbd9632fb8d9fc6d3e62c99dcb75c965", + "llm_connect/usage.py": "ba1c93d11dc98b55c9f87090c4255319f42b413bb7d36426fb46d2ad773f7b37", + "pyproject.toml": "e430482f51667b4de47cf23311fb9a8d6e85673b58a659b31d63547798b6684d", + "tests/conftest.py": "b93789db1467f2c60cb98ce638f17d78070678159eab19a652932e58c0099006", + "tests/test_activity_core_smoke.py": "10d2b607c98114dca4d5143bb487673d15a6a3fd6449a3a0ace8f4d07e94c6d8", + "tests/test_adapter.py": "cbbac15d76fedc580287e39e78f69c7e91f534d051c238cc668ae0f65a321023", + "tests/test_async.py": "a6a96bcf4e0396484aa3dd81f33aed282d0ce44f58f5e0da18ee5446f01e4fb5", + "tests/test_budget.py": "ef6277464b66a56a7a87c6caa672d6bd854683a163dd9262073daea517dddcf7", + "tests/test_costs.py": "860d3246e3e8667f7aebc5b9035da9513752db49b5c5c7c789b61c90cb0d2963", + "tests/test_exceptions.py": "3063039d2370fc4fcbbfe324e1c8f7bada8ed2c92f90261e4f745a32816ebc0b", + "tests/test_factory.py": "cd5870f3133eb9ca8bf06bbc27d030088dc1ac9ba65a388c65331491d8d228cf", + "tests/test_models.py": "987dd2f8cc2af8f855c92ad10cdec585ad76249fc225ac35c06b3429f4113f2f", + "tests/test_payload.py": "520fb17bfa1955241f832a189657311a4c7406fc4f2fda3772b6b49361726928", + "tests/test_problem_classes.py": "0764bf6798c983c62e23872da188d795e31025acb749082cdaccea6b2e02284f", + "tests/test_replay.py": "dd33a2c7ca0c825cc25f2da51d333d9b34c2cd76fca9e3fe8a572ff10a927e84", + "tests/test_routing.py": "6c29238900c04935c95715aba5dc011876e669ed763fe3e2a4053af1d7b8271b", + "tests/test_server.py": "05fb1a5f739d6f9ad5a07ad2c5b755fae0e50c2a70ea3e7da00f0831868708d7", + "tests/test_structured_output_smoke.py": "c23648eaa0df948a5e2b8272a9883fd3047593b1ea9bec4129706efcd56bdd72", + "workplans/LLM-WP-0009-owner-metered-messages-transport.md": "773129d4ff62c375aa5d8065aee212cd0a18812a37444b66e280fede0bb9cf1e" + }, + "limitations": [ + "Protected Railiance runtime b6e4e8a4 remains unchanged; these source quality changes are not deployed in it", + "Windows lock branch not executed by Linux tests", + "T03 native delivery, service admission, accepted live policy and live proof remain open" + ], + "workplan_review": { + "source_workplans": 10, + "finished": 9, + "blocked": [ + "LLM-WP-0009" + ], + "remaining_tasks": [ + "LLM-WP-0009-T03" + ], + "new_tasks": 0, + "new_workplans": 0, + "normalized_to_finished": [ + "LLM-WP-0001", + "LLM-WP-0002", + "LLM-WP-0003", + "LLM-WP-0004" + ] + } +} diff --git a/examples/adaptive_routing_fixture_batch.py b/examples/adaptive_routing_fixture_batch.py index 13dc61c..7b0575b 100644 --- a/examples/adaptive_routing_fixture_batch.py +++ b/examples/adaptive_routing_fixture_batch.py @@ -4,6 +4,9 @@ from __future__ import annotations import argparse + +# Imports follow the source-checkout path bootstrap below. +# ruff: noqa: E402 import sys from dataclasses import dataclass from pathlib import Path diff --git a/llm_connect/__init__.py b/llm_connect/__init__.py index 0cc3d6b..66b1cd9 100644 --- a/llm_connect/__init__.py +++ b/llm_connect/__init__.py @@ -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, diff --git a/llm_connect/_diagnostics.py b/llm_connect/_diagnostics.py index 3215e87..54b58d7 100644 --- a/llm_connect/_diagnostics.py +++ b/llm_connect/_diagnostics.py @@ -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") diff --git a/llm_connect/_http.py b/llm_connect/_http.py index 4cfce98..04f5273 100644 --- a/llm_connect/_http.py +++ b/llm_connect/_http.py @@ -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") diff --git a/llm_connect/_payload.py b/llm_connect/_payload.py index 895625e..c8966d4 100644 --- a/llm_connect/_payload.py +++ b/llm_connect/_payload.py @@ -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 diff --git a/llm_connect/adapter.py b/llm_connect/adapter.py index c429861..50838fa 100644 --- a/llm_connect/adapter.py +++ b/llm_connect/adapter.py @@ -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, diff --git a/llm_connect/balance.py b/llm_connect/balance.py index e5837ea..3148e1d 100644 --- a/llm_connect/balance.py +++ b/llm_connect/balance.py @@ -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( { diff --git a/llm_connect/claude_code.py b/llm_connect/claude_code.py index 95e383c..012dc2f 100644 --- a/llm_connect/claude_code.py +++ b/llm_connect/claude_code.py @@ -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}", diff --git a/llm_connect/config.py b/llm_connect/config.py index 9505581..61a40c3 100644 --- a/llm_connect/config.py +++ b/llm_connect/config.py @@ -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, diff --git a/llm_connect/embedding_cache.py b/llm_connect/embedding_cache.py index 5273b86..47cb6e2 100644 --- a/llm_connect/embedding_cache.py +++ b/llm_connect/embedding_cache.py @@ -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: diff --git a/llm_connect/embedding_factory.py b/llm_connect/embedding_factory.py index f81f07b..92fffd6 100644 --- a/llm_connect/embedding_factory.py +++ b/llm_connect/embedding_factory.py @@ -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) diff --git a/llm_connect/embedding_openai.py b/llm_connect/embedding_openai.py index 06d646b..b6c5117 100644 --- a/llm_connect/embedding_openai.py +++ b/llm_connect/embedding_openai.py @@ -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) diff --git a/llm_connect/exceptions.py b/llm_connect/exceptions.py index 23bc3bd..bdc64e1 100644 --- a/llm_connect/exceptions.py +++ b/llm_connect/exceptions.py @@ -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 diff --git a/llm_connect/factory.py b/llm_connect/factory.py index 6231ca6..089627f 100644 --- a/llm_connect/factory.py +++ b/llm_connect/factory.py @@ -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) diff --git a/llm_connect/fx.py b/llm_connect/fx.py index 44532f6..94f67c6 100644 --- a/llm_connect/fx.py +++ b/llm_connect/fx.py @@ -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" diff --git a/llm_connect/gemini.py b/llm_connect/gemini.py index c6e674c..1a2e427 100644 --- a/llm_connect/gemini.py +++ b/llm_connect/gemini.py @@ -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, diff --git a/llm_connect/grading.py b/llm_connect/grading.py index 1cebbd1..478c196 100644 --- a/llm_connect/grading.py +++ b/llm_connect/grading.py @@ -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) diff --git a/llm_connect/models.py b/llm_connect/models.py index cd40a6a..b4fffe4 100644 --- a/llm_connect/models.py +++ b/llm_connect/models.py @@ -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, diff --git a/llm_connect/openai.py b/llm_connect/openai.py index 14e8e6f..9a65141 100644 --- a/llm_connect/openai.py +++ b/llm_connect/openai.py @@ -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) diff --git a/llm_connect/openrouter.py b/llm_connect/openrouter.py index 721eb99..1eb12ba 100644 --- a/llm_connect/openrouter.py +++ b/llm_connect/openrouter.py @@ -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" diff --git a/llm_connect/problem_classes.py b/llm_connect/problem_classes.py index 03f9042..6fd448b 100644 --- a/llm_connect/problem_classes.py +++ b/llm_connect/problem_classes.py @@ -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( [ diff --git a/llm_connect/profiles.py b/llm_connect/profiles.py index 946fd24..1e370b7 100644 --- a/llm_connect/profiles.py +++ b/llm_connect/profiles.py @@ -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 diff --git a/llm_connect/quality.py b/llm_connect/quality.py index 63497e1..c9e3f17 100644 --- a/llm_connect/quality.py +++ b/llm_connect/quality.py @@ -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"], diff --git a/llm_connect/rates.py b/llm_connect/rates.py index 3a56973..a30c66c 100644 --- a/llm_connect/rates.py +++ b/llm_connect/rates.py @@ -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, diff --git a/llm_connect/replay.py b/llm_connect/replay.py index 082c122..bacb3aa 100644 --- a/llm_connect/replay.py +++ b/llm_connect/replay.py @@ -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": diff --git a/llm_connect/routing.py b/llm_connect/routing.py index 9ffda1a..e8b45d8 100644 --- a/llm_connect/routing.py +++ b/llm_connect/routing.py @@ -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*. diff --git a/llm_connect/server.py b/llm_connect/server.py index e53f19b..31061d5 100644 --- a/llm_connect/server.py +++ b/llm_connect/server.py @@ -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.", diff --git a/llm_connect/shadowing.py b/llm_connect/shadowing.py index 058a0eb..b7fe6db 100644 --- a/llm_connect/shadowing.py +++ b/llm_connect/shadowing.py @@ -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 diff --git a/llm_connect/toml_config.py b/llm_connect/toml_config.py index e2d1401..dc9b299 100644 --- a/llm_connect/toml_config.py +++ b/llm_connect/toml_config.py @@ -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. diff --git a/llm_connect/usage.py b/llm_connect/usage.py index 9356dde..17dc38e 100644 --- a/llm_connect/usage.py +++ b/llm_connect/usage.py @@ -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"], diff --git a/pyproject.toml b/pyproject.toml index 6ea7d4d..b500eaf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ dev = [ [tool.pytest.ini_options] testpaths = ["tests"] +pythonpath = ["."] addopts = "-v" [tool.ruff] diff --git a/tests/conftest.py b/tests/conftest.py index 62e5714..00edfd9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,8 +4,8 @@ Shared pytest fixtures for llm-connect tests. import pytest -from llm_connect.models import RunConfig, LLMResponse from llm_connect.adapter import MockLLMAdapter +from llm_connect.models import LLMResponse, RunConfig @pytest.fixture diff --git a/tests/test_activity_core_smoke.py b/tests/test_activity_core_smoke.py index 515f3b1..f2e2b80 100644 --- a/tests/test_activity_core_smoke.py +++ b/tests/test_activity_core_smoke.py @@ -7,7 +7,6 @@ from llm_connect.models import RunConfig from llm_connect.profiles import CUSTODIAN_TRIAGE_BALANCED, ProfiledLLMAdapter, RuntimeProfile from llm_connect.server import LLMServer - ROOT = Path(__file__).resolve().parents[1] SCRIPT = ROOT / "scripts" / "smoke_activity_core_endpoint.py" FIXTURE_DIR = ROOT / "fixtures" / "activity_core" diff --git a/tests/test_adapter.py b/tests/test_adapter.py index 39b42f5..2f1b44c 100644 --- a/tests/test_adapter.py +++ b/tests/test_adapter.py @@ -3,8 +3,9 @@ Tests for MockLLMAdapter and ErrorLLMAdapter (Core adapter utilities). """ import pytest -from llm_connect.adapter import MockLLMAdapter, ErrorLLMAdapter -from llm_connect.models import RunConfig, LLMResponse + +from llm_connect.adapter import ErrorLLMAdapter, MockLLMAdapter +from llm_connect.models import LLMResponse class TestMockLLMAdapter: diff --git a/tests/test_async.py b/tests/test_async.py index 4f2dae3..dfd904d 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -3,11 +3,12 @@ Tests for async_execute_prompt (FR-3). """ import asyncio + import pytest -from llm_connect.models import RunConfig, BudgetTracker from llm_connect.adapter import MockLLMAdapter from llm_connect.exceptions import LLMBudgetExceededError +from llm_connect.models import BudgetTracker, RunConfig class TestAsyncExecutePrompt: @@ -46,7 +47,6 @@ class TestAsyncExecutePrompt: def test_concurrent_faster_than_sequential(self): """Gathering N async calls should not be N× slower than one call.""" - import time adapter = MockLLMAdapter() config = RunConfig() diff --git a/tests/test_budget.py b/tests/test_budget.py index 9458161..f93c6cd 100644 --- a/tests/test_budget.py +++ b/tests/test_budget.py @@ -3,11 +3,12 @@ Tests for BudgetTracker (FR-4) and LLMBudgetExceededError. """ import threading + import pytest -from llm_connect.models import BudgetTracker, RunConfig from llm_connect.adapter import MockLLMAdapter from llm_connect.exceptions import LLMBudgetExceededError, LLMError +from llm_connect.models import BudgetTracker, RunConfig class TestBudgetTracker: diff --git a/tests/test_costs.py b/tests/test_costs.py index 99a788c..2357f7d 100644 --- a/tests/test_costs.py +++ b/tests/test_costs.py @@ -1,6 +1,6 @@ import pytest -from llm_connect.costs import CostEstimate, CostModel, estimate_cost +from llm_connect.costs import CostModel, estimate_cost from llm_connect.rates import ModelRate, ModelRateRegistry diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index b0a5cd0..80476ef 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -3,13 +3,14 @@ Tests for the LLMError exception hierarchy (Core). """ import pytest + from llm_connect.exceptions import ( - LLMError, - LLMConfigurationError, LLMAPIError, + LLMConfigurationError, + LLMError, LLMRateLimitError, - LLMTimeoutError, LLMSubprocessError, + LLMTimeoutError, ) diff --git a/tests/test_factory.py b/tests/test_factory.py index d34b543..43a92e7 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -3,16 +3,17 @@ Tests for create_adapter() and create_embedding_adapter() factories. """ import pytest -from llm_connect.factory import create_adapter -from llm_connect.embedding_factory import create_embedding_adapter -from llm_connect.exceptions import LLMConfigurationError + from llm_connect.adapter import LLMAdapter -from llm_connect.embedding_adapter import EmbeddingAdapter -from llm_connect.openrouter import OpenRouterAdapter from llm_connect.claude_code import ClaudeCodeAdapter -from llm_connect.openai import OpenAIAdapter -from llm_connect.gemini import GeminiAdapter +from llm_connect.embedding_adapter import EmbeddingAdapter +from llm_connect.embedding_factory import create_embedding_adapter from llm_connect.embedding_openai import OpenAICompatibleEmbeddingAdapter +from llm_connect.exceptions import LLMConfigurationError +from llm_connect.factory import create_adapter +from llm_connect.gemini import GeminiAdapter +from llm_connect.openai import OpenAIAdapter +from llm_connect.openrouter import OpenRouterAdapter class TestCreateAdapter: diff --git a/tests/test_models.py b/tests/test_models.py index 7f4bc3e..803e209 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -2,8 +2,7 @@ Tests for RunConfig and LLMResponse (Core models). """ -import pytest -from llm_connect.models import RunConfig, LLMResponse +from llm_connect.models import LLMResponse, RunConfig class TestRunConfig: diff --git a/tests/test_payload.py b/tests/test_payload.py index 9ff4c34..8f2ff58 100644 --- a/tests/test_payload.py +++ b/tests/test_payload.py @@ -1,6 +1,5 @@ from llm_connect._payload import merge_gemini_model_params, merge_openai_chat_model_params - STRUCTURED_SCHEMA = { "type": "object", "properties": { diff --git a/tests/test_problem_classes.py b/tests/test_problem_classes.py index b31c628..004eec9 100644 --- a/tests/test_problem_classes.py +++ b/tests/test_problem_classes.py @@ -10,7 +10,6 @@ from llm_connect.problem_classes import ( ) from llm_connect.quality import QualityObservation - DIMENSIONS_BY_CLASS = { "chunk-summarization": [ {"chunk_words": 900, "template_words": 150}, diff --git a/tests/test_replay.py b/tests/test_replay.py index a907f46..eb6931a 100644 --- a/tests/test_replay.py +++ b/tests/test_replay.py @@ -1,6 +1,5 @@ from llm_connect.replay import parse_audit_record - STRUCTURED_SCHEMA = { "type": "object", "properties": { diff --git a/tests/test_routing.py b/tests/test_routing.py index f81903d..88d65b6 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -4,8 +4,8 @@ Tests for RoutingPolicy (FR-2). import pytest -from llm_connect.routing import RoutingPolicy, RoutingRule from llm_connect.adapter import MockLLMAdapter +from llm_connect.routing import RoutingPolicy, RoutingRule class TestRoutingPolicy: diff --git a/tests/test_server.py b/tests/test_server.py index b36385a..408f20c 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -2,12 +2,12 @@ Tests for LLMServer HTTP serve mode (FR-1). """ +import json import threading import time -from concurrent.futures import ThreadPoolExecutor -import json import urllib.error import urllib.request +from concurrent.futures import ThreadPoolExecutor import pytest @@ -16,7 +16,7 @@ from llm_connect._diagnostics import ( record_provider_request, record_provider_response, ) -from llm_connect.adapter import MockLLMAdapter, ErrorLLMAdapter +from llm_connect.adapter import ErrorLLMAdapter, MockLLMAdapter from llm_connect.exceptions import LLMAPIError, LLMConfigurationError, LLMTimeoutError from llm_connect.models import LLMResponse, RunConfig from llm_connect.profiles import CUSTODIAN_TRIAGE_BALANCED, ProfiledLLMAdapter, RuntimeProfile @@ -136,6 +136,7 @@ class TestExecute: except urllib.error.HTTPError as exc: status, body = exc.code, json.loads(exc.read()) assert status == 400 + assert body == {"error": "invalid JSON body"} def test_unknown_post_path_returns_404(self, server): status, body = _post( diff --git a/tests/test_structured_output_smoke.py b/tests/test_structured_output_smoke.py index df1a93c..e922811 100644 --- a/tests/test_structured_output_smoke.py +++ b/tests/test_structured_output_smoke.py @@ -5,7 +5,6 @@ from llm_connect.models import RunConfig from llm_connect.openai import OpenAIAdapter from llm_connect.openrouter import OpenRouterAdapter - STRUCTURED_SCHEMA = { "type": "object", "properties": { diff --git a/workplans/LLM-WP-0009-owner-metered-messages-transport.md b/workplans/LLM-WP-0009-owner-metered-messages-transport.md index 477be93..401c8c6 100644 --- a/workplans/LLM-WP-0009-owner-metered-messages-transport.md +++ b/workplans/LLM-WP-0009-owner-metered-messages-transport.md @@ -4,12 +4,12 @@ type: workplan title: "Owner-metered Messages transport for bounded factory execution" domain: agents repo: llm-connect -status: active +status: blocked flavor: implementation owner: codex topic_slug: llm-connect created: "2026-09-09" -updated: "2026-09-09" +updated: "2026-09-27" related: - HFACT-WP-0001 - REINAH-WP-0003 @@ -58,7 +58,7 @@ No actual inference, provider credential or live price/FX policy is involved. id: LLM-WP-0009-T03 status: wait priority: high -blocking_reason: "Local Unix hosting, worker lease/token lifecycle and bwrap confinement proved; requires admitted credential-to-owner bootstrap, matched protected artifact and Railiance custody/placement under HFACT T03/T04; live tariff/FX and G0 remain HFACT T01." +blocking_reason: "Corrected Railiance owner/code installation and synthetic two-request tool proof are complete. Remaining: accepted native spend grant/window with FX/tariffs and accounting continuity, regenerated recipient pins and attended per-lane delivery approvals, then real provider and natural queue/tool/commit/recovery evidence under SECRETS-WP-0009-T03, HFACT-WP-0001-T01/T03/T04/T05 and REINAH-WP-0003-T05/T06." state_hub_task_id: "98a38d75-73ab-5f37-b710-5df9d9681e49" ``` @@ -111,6 +111,113 @@ admission, live provider compatibility and accepted bounds/tariffs/FX, then G0 a natural model/queue evidence. No protected runtime was installed or promoted, no existing CCR changed, no secret read or paid execution took place. +### Release quality and dependency reconciliation — 2026-09-27 + +Resolved the reproduced 177 Ruff findings and 36 mypy errors without disabling +repository-wide rules. Modernized annotations/imports, typed lazy adapter +constructors and cache entries, narrowed text-file modes and platform branches, +and declared the HTTP server's injected adapter. JSON boundary casts retain the +existing response contracts. The example's source-path bootstrap has an explicit +E402 exception; invalid-JSON server coverage now checks the returned error body. +Pytest explicitly includes the repository root so the example integration test +collects under both the pytest executable and module invocation. `make check` +now runs lint, typecheck and tests together: lint clean, all 35 source files type +check, **264 tests passed**. Evidence: +`docs/evidence/2026-09-27-request-admission-quality.json`. + +Consumed newer owner evidence from +`../prj-helixforge-factory/operations/owner-bootstrap-admission.md` and +`../prj-helixforge-factory/evidence/2026-09-10-runtime-placement.json`. +The one-cycle bootstrap and protected artifact +`5371156d2027dde6e8f140cc0a1833c4e90b8c75b3bec862e05f06a957f5fd34` +were already installed and synthetically proved on workstation and Railiance. +These receipts supersede the earlier statement that no protected artifact had +been installed. They do not establish native service or credential admission. +Today's source quality changes are not installed in that immutable artifact; +any replacement needs a new source pin/build and the same artifact checks. + +T03 remains `wait` and the workplan is `blocked` on these existing owner tasks: + +| Remaining acceptance | Existing owner record | +| --- | --- | +| Exact recipient configuration and native provider-key delivery | SECRETS-WP-0009-T03 / HFACT-WP-0001-T03 | +| Service/profile/consumer admission, isolated queue and private-state recovery | HFACT-WP-0001-T04 / REINAH-WP-0003-T05 | +| Accepted live provider compatibility, bounds, maximum tariffs, validity, FX and G0 | HFACT-WP-0001-T01 | +| Admitted real model and natural queue proof | REINAH-WP-0003-T06 / HFACT-WP-0001-T05 | + +No accepted live policy or native delivery receipt was found in the owning +records. Fixture inputs cannot substitute for them. Closing T03 would contradict +its explicit acceptance condition. No credential retrieval, paid request, +production mutation or profile activation was performed in this session. + +### Direct cross-repository follow-up — 2026-09-27 + +Followed the user's instruction into secrets-engine, rein-aharness and the +factory project. The prior summary was stale: the dedicated metered worker +identity, private owner state and newer b6e4e8a4 runtime already exist. +The installed policy nevertheless reserved 200k tokens against Sonnet 5's 1M +context, refused the CLI's actual 64k output request and omitted its additional +primary-request beta. Its prior runtime proof always selected profile 1.0.0. + +Corrected rein's proof to require the exact profile/model and record request +shape metadata. The real Railiance artifact now passes a synthetic Sonnet 5 / +profile 1.1.1 first response, zero-forward exhausted-capacity refusal, private +state exclusion, read-only unchanged artifact and cleanup. Prepared the corrected +owner config and source catalog pin, plus six exact unapproved action requests +in `../secrets-engine/docs/proposals/glas-metered-20260927/README.md`. +Secrets Engine's full suite passes 498 tests. The installed old policy is +explicitly refused by the new offline review; the candidate passes. + +Maximum request hold is USD 4.64. Only one fits the existing USD 5.74 allowance; +a successful tool loop must not be promised under the current EUR 5 cap. The +candidate preserves all spend limits and the ledger. Source changes and owner +records are updated in their actual repositories. Corrected production config +installation and native action approvals remain pending; no secret read, paid +request or production owner mutation occurred. The broader useful-change G0 is +separate from this disposable Glas proof. Factory receipt: +`../prj-helixforge-factory/evidence/2026-09-27-metered-owner-followup.json`. + +### Approved installation and tool-session proposal — 2026-09-27 + +The user approved the configuration/code update and separately requested preparation +of the €10 tool-session proposal. Installed Secrets Engine `11cc0d5` and corrected +owner `e0d3fb84` on Railiance; exact path/hash checks, substituted-command refusal, +standalone companion refusal and backend-free owner check pass. Standing worker, +spend limits and existing ledger are unchanged; no credentials read or paid calls. +Deployment receipt: `../secrets-engine/docs/evidence/2026-09-27-metered-owner-deployment.json`. + +Actual pinned CLI/profile 1.1.1/runtime b6e4e8a4 passes a synthetic two-request Bash +tool/result exchange and zero-forward underfunded refusal, with teardown and +unchanged artifact. Receipt: `../rein-aharness/docs/evidence/2026-09-27-sonnet5-tool-session-proof.json`. +The inactive proposal is `../prj-helixforge-factory/operations/metered-tool-session-proposal.md`: +EUR 10 run cap, USD 10 liability, EUR/USD 1.00 treatment, existing native USD 5 +threshold/daily EUR 20/total EUR 500 retained. Two USD 4.64 holds fit. Installed +0.87 FX is below the latest observed 0.876962 reference; fresh validity/FX acceptance +is required. No new grant or paid execution is authorized by proposal preparation. + +Remaining owner tasks retain their waiting status: fresh spend grant/window, +accounting continuity and replacement recipient pins; attended native per-lane +approval/delivery/revocation; then natural queue/model/tool/commit/recovery proof. +This return supersedes earlier installation-pending statements, not those gates. + + +### Repository loose-end review — 2026-09-27 + +Reviewed all ten source workplans and their task blocks, including the four +legacy `completed` plans. Only T03 remains unfinished; there are no locally +ready, active or proposed tasks to implement. LLM-WP-0001 through LLM-WP-0008 +and the historical ad-hoc plan have all tasks done. Normalized LLM-WP-0001–0004 +to the canonical `finished` state without changing identities or task history. + +Completed the local release-quality repairs described above and included them +in the repository commit: lint/type fixes, typed server/factory/cache boundaries, +pytest example imports, and the combined `make check` target. Removed the stale +installation-pending blocker from T03's structured record. Current synthetic +and deployment receipts establish the local completion; they do not satisfy +T03's explicit live acceptance. Retain `status: blocked` / task `wait` for the +existing native-spend, delivery and natural-run owner dependencies. No new task +or workplan was opened, and no paid or production action is part of this review. + ## Repair historical source identities blocking primary synchronization ```task diff --git a/workplans/llm-connect-WP-0001-foundation-gaaf-baseline.md b/workplans/llm-connect-WP-0001-foundation-gaaf-baseline.md index b709a35..0640790 100644 --- a/workplans/llm-connect-WP-0001-foundation-gaaf-baseline.md +++ b/workplans/llm-connect-WP-0001-foundation-gaaf-baseline.md @@ -3,13 +3,14 @@ id: LLM-WP-0001 type: workplan title: llm-connect — Foundation & GAAF Baseline domain: agents -status: completed +status: finished owner: llm-connect created: 2026-04-01 repo: llm-connect planning_priority: high planning_order: 1 state_hub_workstream_id: f7f08327-753f-4175-8591-ffa1c3188ebc +updated: "2026-09-27" --- # LLM-WP-0001 — Foundation & GAAF Baseline diff --git a/workplans/llm-connect-WP-0002-core-extensions.md b/workplans/llm-connect-WP-0002-core-extensions.md index 5b8977b..0e978d0 100644 --- a/workplans/llm-connect-WP-0002-core-extensions.md +++ b/workplans/llm-connect-WP-0002-core-extensions.md @@ -3,13 +3,14 @@ id: LLM-WP-0002 type: workplan title: llm-connect — Core Extensions (FR-4 BudgetTracker + FR-3 async) domain: agents -status: completed +status: finished owner: llm-connect created: 2026-04-01 repo: llm-connect planning_priority: high planning_order: 2 state_hub_workstream_id: 448fa379-eb9e-4808-b3fa-0078f1e4eaba +updated: "2026-09-27" --- # LLM-WP-0002 — Core Extensions (FR-4 + FR-3) diff --git a/workplans/llm-connect-WP-0003-functional-extensions.md b/workplans/llm-connect-WP-0003-functional-extensions.md index 766ce42..df62b9a 100644 --- a/workplans/llm-connect-WP-0003-functional-extensions.md +++ b/workplans/llm-connect-WP-0003-functional-extensions.md @@ -3,13 +3,14 @@ id: LLM-WP-0003 type: workplan title: llm-connect — Functional Extensions (FR-2 RoutingPolicy + FR-1 HTTP server) domain: agents -status: completed +status: finished owner: llm-connect created: 2026-04-01 repo: llm-connect planning_priority: high planning_order: 3 state_hub_workstream_id: 7b463cdc-40a2-4cc5-8b55-b59cc5ae3443 +updated: "2026-09-27" --- # LLM-WP-0003 — Functional Extensions (FR-2 + FR-1) diff --git a/workplans/llm-connect-WP-0004-adaptive-cost-quality-routing.md b/workplans/llm-connect-WP-0004-adaptive-cost-quality-routing.md index ffb35bb..e9a827f 100644 --- a/workplans/llm-connect-WP-0004-adaptive-cost-quality-routing.md +++ b/workplans/llm-connect-WP-0004-adaptive-cost-quality-routing.md @@ -3,13 +3,14 @@ id: LLM-WP-0004 type: workplan title: Adaptive Cost-Quality Routing domain: agents -status: completed +status: finished owner: llm-connect created: 2026-05-17 repo: llm-connect planning_priority: high planning_order: 4 state_hub_workstream_id: e1807fab-e29e-4517-b362-95737a96582d +updated: "2026-09-27" --- # LLM-WP-0004 — Adaptive Cost-Quality Routing