pf-core 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pf_core/__init__.py +20 -0
- pf_core/_extras.py +64 -0
- pf_core/alembic.py +72 -0
- pf_core/budget/__init__.py +147 -0
- pf_core/budget/_schema.py +122 -0
- pf_core/budget/audit.py +73 -0
- pf_core/budget/check.py +369 -0
- pf_core/budget/config.py +155 -0
- pf_core/budget/repo.py +386 -0
- pf_core/budget/scheduler.py +72 -0
- pf_core/budget/snapshot_job.py +54 -0
- pf_core/cache/__init__.py +0 -0
- pf_core/cache/redis.py +198 -0
- pf_core/cli/__init__.py +103 -0
- pf_core/cli/jobs.py +272 -0
- pf_core/cli/subcommands/__init__.py +31 -0
- pf_core/cli/subcommands/_render.py +174 -0
- pf_core/cli/subcommands/baseline.py +155 -0
- pf_core/cli/subcommands/invalidate.py +90 -0
- pf_core/clients/__init__.py +38 -0
- pf_core/clients/anthropic.py +333 -0
- pf_core/clients/brave.py +296 -0
- pf_core/clients/claude_code.py +429 -0
- pf_core/clients/openrouter.py +400 -0
- pf_core/clients/routing.py +167 -0
- pf_core/config.py +149 -0
- pf_core/db/__init__.py +79 -0
- pf_core/db/connection.py +184 -0
- pf_core/db/helpers.py +68 -0
- pf_core/db/json_compat.py +202 -0
- pf_core/db/models.py +79 -0
- pf_core/db/repository.py +66 -0
- pf_core/db/soft_delete.py +122 -0
- pf_core/db/upsert.py +175 -0
- pf_core/db/versioned_config.py +195 -0
- pf_core/docs/INSTALLATION.md +218 -0
- pf_core/docs/alembic.md +71 -0
- pf_core/docs/anthropic.md +172 -0
- pf_core/docs/anti-hallucination.md +123 -0
- pf_core/docs/article-fetch.md +166 -0
- pf_core/docs/brave.md +154 -0
- pf_core/docs/cache.md +178 -0
- pf_core/docs/claude-code.md +211 -0
- pf_core/docs/cli-subcommands.md +112 -0
- pf_core/docs/cli.md +95 -0
- pf_core/docs/config.md +113 -0
- pf_core/docs/cost-budget.md +218 -0
- pf_core/docs/database.md +292 -0
- pf_core/docs/dates.md +130 -0
- pf_core/docs/db-upsert.md +41 -0
- pf_core/docs/env.md +108 -0
- pf_core/docs/eval-harness.md +560 -0
- pf_core/docs/exceptions.md +143 -0
- pf_core/docs/export.md +92 -0
- pf_core/docs/guards.md +57 -0
- pf_core/docs/hashing.md +40 -0
- pf_core/docs/ids.md +95 -0
- pf_core/docs/io.md +83 -0
- pf_core/docs/jobs.md +381 -0
- pf_core/docs/json-recovery.md +94 -0
- pf_core/docs/json-utils.md +112 -0
- pf_core/docs/linting.md +128 -0
- pf_core/docs/llm-admin.md +168 -0
- pf_core/docs/llm-cache.md +276 -0
- pf_core/docs/llm-parse.md +96 -0
- pf_core/docs/llm-safe-apply.md +107 -0
- pf_core/docs/llm-schema-validation.md +445 -0
- pf_core/docs/llm-tracked.md +98 -0
- pf_core/docs/llm-tracking.md +405 -0
- pf_core/docs/llm-validation.md +94 -0
- pf_core/docs/logging.md +116 -0
- pf_core/docs/markdown.md +111 -0
- pf_core/docs/model-router.md +446 -0
- pf_core/docs/modules.md +176 -0
- pf_core/docs/openrouter.md +230 -0
- pf_core/docs/orchestrators.md +93 -0
- pf_core/docs/output.md +123 -0
- pf_core/docs/pagination.md +122 -0
- pf_core/docs/parallel.md +121 -0
- pf_core/docs/parsers.md +99 -0
- pf_core/docs/periods.md +84 -0
- pf_core/docs/phash.md +69 -0
- pf_core/docs/pipeline.md +287 -0
- pf_core/docs/pricing.md +43 -0
- pf_core/docs/project-portability.md +125 -0
- pf_core/docs/prompts.md +277 -0
- pf_core/docs/relative-dates.md +130 -0
- pf_core/docs/scaffold.md +33 -0
- pf_core/docs/services.md +84 -0
- pf_core/docs/similarity.md +102 -0
- pf_core/docs/soft-delete.md +105 -0
- pf_core/docs/test-migration.md +105 -0
- pf_core/docs/testing.md +143 -0
- pf_core/docs/throttle.md +33 -0
- pf_core/docs/urls.md +301 -0
- pf_core/docs/versioned-config.md +99 -0
- pf_core/docs/vocab.md +141 -0
- pf_core/docs/web.md +269 -0
- pf_core/eval/__init__.py +104 -0
- pf_core/eval/_compare.py +146 -0
- pf_core/eval/_config.py +142 -0
- pf_core/eval/_golden.py +205 -0
- pf_core/eval/_judge.py +146 -0
- pf_core/eval/_report.py +214 -0
- pf_core/eval/_runner.py +393 -0
- pf_core/exceptions.py +192 -0
- pf_core/export/__init__.py +16 -0
- pf_core/export/markdown.py +264 -0
- pf_core/guards/__init__.py +18 -0
- pf_core/guards/__main__.py +9 -0
- pf_core/guards/structure.py +174 -0
- pf_core/jobs/__init__.py +69 -0
- pf_core/jobs/_schema.py +156 -0
- pf_core/jobs/registry.py +254 -0
- pf_core/jobs/repo.py +775 -0
- pf_core/jobs/runtime.py +309 -0
- pf_core/llm/__init__.py +107 -0
- pf_core/llm/_router_config.py +161 -0
- pf_core/llm/_router_loader.py +118 -0
- pf_core/llm/_router_schema.py +142 -0
- pf_core/llm/cache/__init__.py +234 -0
- pf_core/llm/cache/_recorder.py +121 -0
- pf_core/llm/cache/_schema.py +111 -0
- pf_core/llm/cache/config.py +137 -0
- pf_core/llm/cache/exact.py +158 -0
- pf_core/llm/cache/invalidate.py +123 -0
- pf_core/llm/parse.py +152 -0
- pf_core/llm/prompts.py +237 -0
- pf_core/llm/router.py +331 -0
- pf_core/llm/safe_apply.py +193 -0
- pf_core/llm/tracked.py +286 -0
- pf_core/llm/tracking/__init__.py +114 -0
- pf_core/llm/tracking/_resolvers.py +275 -0
- pf_core/llm/tracking/decorator.py +198 -0
- pf_core/llm/tracking/purge.py +63 -0
- pf_core/llm/tracking/repo.py +332 -0
- pf_core/llm/tracking/schema.py +482 -0
- pf_core/llm/tracking/stats.py +207 -0
- pf_core/llm/tracking/subrepos.py +230 -0
- pf_core/llm/url_check.py +74 -0
- pf_core/llm/validate/__init__.py +80 -0
- pf_core/llm/validate/_cross_field.py +57 -0
- pf_core/llm/validate/_jsonschema.py +61 -0
- pf_core/llm/validate/_pipeline.py +263 -0
- pf_core/llm/validate/_pydantic.py +51 -0
- pf_core/llm/validate/_registry.py +135 -0
- pf_core/llm/validate/_semantic.py +353 -0
- pf_core/log.py +239 -0
- pf_core/orchestrators/__init__.py +5 -0
- pf_core/orchestrators/base.py +90 -0
- pf_core/output.py +108 -0
- pf_core/parallel.py +249 -0
- pf_core/parsers/__init__.py +48 -0
- pf_core/parsers/exceptions.py +36 -0
- pf_core/parsers/html.py +193 -0
- pf_core/parsers/types.py +32 -0
- pf_core/pipeline/__init__.py +15 -0
- pf_core/pipeline/baseline.py +245 -0
- pf_core/pipeline/baseline_diff.py +297 -0
- pf_core/pipeline/cache.py +159 -0
- pf_core/pipeline/resume.py +130 -0
- pf_core/pipeline/run_record.py +127 -0
- pf_core/pipeline/sequencer.py +161 -0
- pf_core/pricing/__init__.py +30 -0
- pf_core/pricing/_data.py +35 -0
- pf_core/pricing/_resolver.py +112 -0
- pf_core/pricing/_types.py +24 -0
- pf_core/py.typed +0 -0
- pf_core/services/__init__.py +5 -0
- pf_core/services/base.py +70 -0
- pf_core/testing/__init__.py +20 -0
- pf_core/testing/db_fixtures.py +197 -0
- pf_core/testing/fixtures.py +49 -0
- pf_core/utils/__init__.py +58 -0
- pf_core/utils/article_fetch.py +527 -0
- pf_core/utils/dates.py +186 -0
- pf_core/utils/env.py +157 -0
- pf_core/utils/hashing.py +52 -0
- pf_core/utils/ids.py +112 -0
- pf_core/utils/io.py +117 -0
- pf_core/utils/json.py +123 -0
- pf_core/utils/json_recovery.py +188 -0
- pf_core/utils/periods.py +197 -0
- pf_core/utils/phash.py +162 -0
- pf_core/utils/relative_dates.py +247 -0
- pf_core/utils/similarity.py +71 -0
- pf_core/utils/throttle.py +67 -0
- pf_core/utils/url_liveness.py +191 -0
- pf_core/utils/urls.py +518 -0
- pf_core/utils/vocab.py +135 -0
- pf_core/web/__init__.py +14 -0
- pf_core/web/app_factory.py +391 -0
- pf_core/web/health.py +112 -0
- pf_core/web/helpers.py +30 -0
- pf_core/web/json.py +55 -0
- pf_core/web/llm_admin/__init__.py +83 -0
- pf_core/web/llm_admin/api.py +161 -0
- pf_core/web/llm_admin/pages.py +174 -0
- pf_core/web/llm_admin/queries.py +526 -0
- pf_core/web/llm_admin/templates/base.html +60 -0
- pf_core/web/llm_admin/templates/budgets.html +46 -0
- pf_core/web/llm_admin/templates/cache.html +44 -0
- pf_core/web/llm_admin/templates/cost_by_agent.html +22 -0
- pf_core/web/llm_admin/templates/cost_by_model.html +24 -0
- pf_core/web/llm_admin/templates/dashboard.html +41 -0
- pf_core/web/llm_admin/templates/job_detail.html +60 -0
- pf_core/web/llm_admin/templates/jobs_list.html +47 -0
- pf_core/web/llm_admin/templates/macros.html +23 -0
- pf_core/web/llm_admin/templates/run_detail.html +114 -0
- pf_core/web/llm_admin/templates/runs_list.html +49 -0
- pf_core/web/markdown.py +208 -0
- pf_core/web/pagination.py +124 -0
- pf_core/web/rate_limit.py +113 -0
- pf_core/web/templates.py +59 -0
- pf_core-0.1.0.dist-info/METADATA +152 -0
- pf_core-0.1.0.dist-info/RECORD +220 -0
- pf_core-0.1.0.dist-info/WHEEL +5 -0
- pf_core-0.1.0.dist-info/entry_points.txt +5 -0
- pf_core-0.1.0.dist-info/licenses/LICENSE +21 -0
- pf_core-0.1.0.dist-info/top_level.txt +1 -0
pf_core/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""pf-core: a dependency-light Python foundation.
|
|
2
|
+
|
|
3
|
+
The base install (``pip install pf-core``) is the architectural foundation
|
|
4
|
+
only: structured logging, an exception hierarchy, config + env resolvers,
|
|
5
|
+
utils, and the ``Service`` base class — five small deps (structlog,
|
|
6
|
+
python-dotenv, pyyaml, nanoid, rich), no httpx/pydantic/LLM stack.
|
|
7
|
+
|
|
8
|
+
Everything else ships as opt-in, orthogonally-composable extras: anti-slop
|
|
9
|
+
output guards (``[validate]``), LLM clients (``[llm]`` ⊇ ``[validate]``), HTTP
|
|
10
|
+
utils (``[http]``), CLI scaffolding (``[cli]``), and the FastAPI + SQLAlchemy
|
|
11
|
+
app framework (``[db]``, ``[web]``, ``[jobs]``, ``[tracking]``, ``[eval]``,
|
|
12
|
+
``[admin]``). See ``docs/INSTALLATION.md`` for the extras matrix.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
__version__ = version("pf-core")
|
|
19
|
+
except PackageNotFoundError: # running from a source tree with no install
|
|
20
|
+
__version__ = "0.0.0+unknown"
|
pf_core/_extras.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Friendly errors for missing optional-dependency extras.
|
|
2
|
+
|
|
3
|
+
The foundation install (``pip install pf-core``) is dependency-light: it does
|
|
4
|
+
not ship httpx, pydantic, json-repair, tenacity, or typer. Modules that need
|
|
5
|
+
those live behind opt-in extras ([http], [llm], [cli], [jobs], ...). When such
|
|
6
|
+
a module is imported without its extra installed, the bare third-party
|
|
7
|
+
``ImportError`` ("No module named 'json_repair'") is opaque. This helper turns
|
|
8
|
+
it into a message that names the extra and the exact pip command.
|
|
9
|
+
|
|
10
|
+
Usage at the top of a gated leaf module::
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import httpx
|
|
14
|
+
except ImportError as e: # pragma: no cover - exercised by bare-install CI
|
|
15
|
+
from pf_core._extras import extra_import_error
|
|
16
|
+
|
|
17
|
+
raise extra_import_error(
|
|
18
|
+
"llm", "httpx", feature="pf_core.clients.openrouter"
|
|
19
|
+
) from e
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
# Extra name -> the pip target a user should install. Anything not listed
|
|
25
|
+
# falls back to ``pf-core[<extra>]``.
|
|
26
|
+
_INSTALL: dict[str, str] = {
|
|
27
|
+
"http": "pf-core[http]",
|
|
28
|
+
"cli": "pf-core[cli]",
|
|
29
|
+
"validate": "pf-core[validate]",
|
|
30
|
+
"llm": "pf-core[llm]",
|
|
31
|
+
"db": "pf-core[db]",
|
|
32
|
+
"web": "pf-core[web]",
|
|
33
|
+
"jobs": "pf-core[jobs]",
|
|
34
|
+
"tracking": "pf-core[tracking]",
|
|
35
|
+
"eval": "pf-core[eval]",
|
|
36
|
+
"admin": "pf-core[admin]",
|
|
37
|
+
"articles": "pf-core[articles]",
|
|
38
|
+
"jsonschema": "pf-core[jsonschema]",
|
|
39
|
+
"redis": "pf-core[redis]",
|
|
40
|
+
"ratelimit": "pf-core[ratelimit]",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def install_target(extra: str) -> str:
|
|
45
|
+
"""Return the ``pip install`` target for an extra (e.g. ``pf-core[llm]``)."""
|
|
46
|
+
return _INSTALL.get(extra, f"pf-core[{extra}]")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def extra_import_error(extra: str, package: str, *, feature: str) -> ImportError:
|
|
50
|
+
"""Build an ``ImportError`` that names the missing extra and pip command.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
extra: The optional-dependency extra that ships ``package`` (e.g. ``"llm"``).
|
|
54
|
+
package: The third-party import name that failed (e.g. ``"json_repair"``).
|
|
55
|
+
feature: The pf-core module or capability the caller was importing, used
|
|
56
|
+
in the message (e.g. ``"pf_core.llm.parse"``).
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
An ``ImportError`` to ``raise ... from`` the original failure.
|
|
60
|
+
"""
|
|
61
|
+
return ImportError(
|
|
62
|
+
f"{feature} requires the '{extra}' extra; '{package}' is not installed. "
|
|
63
|
+
f"Install it with: pip install {install_target(extra)}"
|
|
64
|
+
)
|
pf_core/alembic.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Alembic migration helper — shared env.py logic for all projects.
|
|
3
|
+
|
|
4
|
+
Supports SQLite (batch mode), MySQL/MariaDB, and PostgreSQL. Uses pf_core.db
|
|
5
|
+
for engine management so migrations share the same connection config as the app.
|
|
6
|
+
|
|
7
|
+
Usage in a project's ``alembic/env.py``::
|
|
8
|
+
|
|
9
|
+
from pf_core.alembic import run_migrations_online
|
|
10
|
+
|
|
11
|
+
run_migrations_online()
|
|
12
|
+
|
|
13
|
+
With SQLite fallback (e.g., an essay-grading app on SQLite)::
|
|
14
|
+
|
|
15
|
+
from pf_core.alembic import run_migrations_online
|
|
16
|
+
|
|
17
|
+
run_migrations_online(fallback_sqlite="grading.db")
|
|
18
|
+
|
|
19
|
+
With explicit metadata (for autogenerate)::
|
|
20
|
+
|
|
21
|
+
from myapp.models import Base
|
|
22
|
+
from pf_core.alembic import run_migrations_online
|
|
23
|
+
|
|
24
|
+
run_migrations_online(target_metadata=Base.metadata)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
from alembic import context
|
|
32
|
+
|
|
33
|
+
from pf_core.db import db_url, get_engine, is_sqlite
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def run_migrations_online(
|
|
37
|
+
*,
|
|
38
|
+
fallback_sqlite: str = "",
|
|
39
|
+
target_metadata: Any = None,
|
|
40
|
+
compare_type: bool = False,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Run Alembic migrations in online mode.
|
|
43
|
+
|
|
44
|
+
Call this from your project's ``alembic/env.py``. Handles:
|
|
45
|
+
- SQLite batch mode (``render_as_batch=True``)
|
|
46
|
+
- MySQL/MariaDB and PostgreSQL standard mode
|
|
47
|
+
- Engine reuse via ``pf_core.db.get_engine()``
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
fallback_sqlite: Path to SQLite file if DATABASE_URL is not set.
|
|
51
|
+
target_metadata: SQLAlchemy MetaData for autogenerate support.
|
|
52
|
+
Pass ``None`` (default) for raw-SQL migrations.
|
|
53
|
+
compare_type: Whether Alembic should detect column type changes.
|
|
54
|
+
"""
|
|
55
|
+
if context.is_offline_mode():
|
|
56
|
+
raise RuntimeError(
|
|
57
|
+
"Offline mode is not supported. Set DATABASE_URL and run in online mode."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
url = db_url(fallback_sqlite=fallback_sqlite)
|
|
61
|
+
sqlite = is_sqlite(url)
|
|
62
|
+
engine = get_engine(url)
|
|
63
|
+
|
|
64
|
+
with engine.connect() as connection:
|
|
65
|
+
context.configure(
|
|
66
|
+
connection=connection,
|
|
67
|
+
target_metadata=target_metadata,
|
|
68
|
+
compare_type=compare_type,
|
|
69
|
+
render_as_batch=sqlite,
|
|
70
|
+
)
|
|
71
|
+
with context.begin_transaction():
|
|
72
|
+
context.run_migrations()
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pre-call cost guardrails for LLM spending.
|
|
3
|
+
|
|
4
|
+
The kernel-safe surface (importable without ``pf-core[db]``) covers the
|
|
5
|
+
budget guard itself and YAML config loading:
|
|
6
|
+
|
|
7
|
+
from pf_core.budget import (
|
|
8
|
+
check_budget, project_cost, CostBudgetExceeded,
|
|
9
|
+
compute_period_start, compute_period_end,
|
|
10
|
+
load_yaml, clear_config_cache,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
The DB-backed surface (requires ``pf-core[db]``) is loaded lazily — the
|
|
14
|
+
import only triggers SQLAlchemy when an attribute is first accessed:
|
|
15
|
+
|
|
16
|
+
from pf_core.budget import (
|
|
17
|
+
BudgetRepo, BudgetSnapshotRepo, CostRateRepo, aggregate_spent,
|
|
18
|
+
sync_budgets_from_yaml,
|
|
19
|
+
refresh_snapshots, start_budget_refresh_loop,
|
|
20
|
+
record_blocked_run, record_override,
|
|
21
|
+
ALL_BUDGET_TABLES, llm_budgets, llm_budget_snapshots, llm_cost_rates,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
In a kernel-only install (no ``[db]`` extra), ``check_budget()`` and
|
|
25
|
+
``project_cost()`` still import and run, but ``project_cost()`` falls back
|
|
26
|
+
to ``0.0`` when no DB-backed cost rates are reachable, and any code path
|
|
27
|
+
that calls into the repos will surface ``ModuleNotFoundError: sqlalchemy``.
|
|
28
|
+
|
|
29
|
+
See ``docs/cost-budget.md``.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from typing import TYPE_CHECKING
|
|
35
|
+
|
|
36
|
+
# Eager imports — kernel-safe modules with no top-level sqlalchemy coupling.
|
|
37
|
+
# These remain importable in a base ``pip install pf-core`` (no extras).
|
|
38
|
+
from pf_core.budget.check import ( # noqa: F401
|
|
39
|
+
CostBudgetExceeded,
|
|
40
|
+
check_budget,
|
|
41
|
+
compute_period_end,
|
|
42
|
+
compute_period_start,
|
|
43
|
+
project_cost,
|
|
44
|
+
)
|
|
45
|
+
from pf_core.budget.config import ( # noqa: F401
|
|
46
|
+
clear_config_cache,
|
|
47
|
+
load_yaml,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Lazy imports — these submodules pull SQLAlchemy at module top, so we
|
|
52
|
+
# defer them via PEP 562 ``__getattr__`` to keep the kernel install clean.
|
|
53
|
+
# The first attribute access triggers the import; subsequent accesses are
|
|
54
|
+
# free because we cache into ``globals()``.
|
|
55
|
+
_LAZY: dict[str, str] = {
|
|
56
|
+
# Repos (DB-required)
|
|
57
|
+
"BudgetRepo": "pf_core.budget.repo",
|
|
58
|
+
"BudgetSnapshotRepo": "pf_core.budget.repo",
|
|
59
|
+
"CostRateRepo": "pf_core.budget.repo",
|
|
60
|
+
"aggregate_spent": "pf_core.budget.repo",
|
|
61
|
+
# Audit logging (DB-required)
|
|
62
|
+
"record_blocked_run": "pf_core.budget.audit",
|
|
63
|
+
"record_override": "pf_core.budget.audit",
|
|
64
|
+
# Snapshot / scheduler jobs (DB-required)
|
|
65
|
+
"refresh_snapshots": "pf_core.budget.snapshot_job",
|
|
66
|
+
"start_budget_refresh_loop": "pf_core.budget.scheduler",
|
|
67
|
+
# YAML→DB sync (DB-required path within the otherwise-kernel config module)
|
|
68
|
+
"sync_budgets_from_yaml": "pf_core.budget.config",
|
|
69
|
+
# Schema tables (DB-required — registers on shared MetaData on first access)
|
|
70
|
+
"ALL_BUDGET_TABLES": "pf_core.budget._schema",
|
|
71
|
+
"llm_budgets": "pf_core.budget._schema",
|
|
72
|
+
"llm_budget_snapshots": "pf_core.budget._schema",
|
|
73
|
+
"llm_cost_rates": "pf_core.budget._schema",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def __getattr__(name: str):
|
|
78
|
+
"""Lazy import for DB-required attributes (PEP 562)."""
|
|
79
|
+
target = _LAZY.get(name)
|
|
80
|
+
if target is None:
|
|
81
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
82
|
+
import importlib
|
|
83
|
+
|
|
84
|
+
mod = importlib.import_module(target)
|
|
85
|
+
value = getattr(mod, name)
|
|
86
|
+
globals()[name] = value # cache so subsequent access skips __getattr__
|
|
87
|
+
return value
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def __dir__() -> list[str]:
|
|
91
|
+
"""Expose lazy attributes to ``dir()`` and IDE autocomplete."""
|
|
92
|
+
return sorted(set(globals()) | set(_LAZY))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# Type-checker view of the lazy attributes — keeps ``mypy`` / IDEs happy
|
|
96
|
+
# without paying the import cost at runtime.
|
|
97
|
+
if TYPE_CHECKING:
|
|
98
|
+
from pf_core.budget._schema import ( # noqa: F401
|
|
99
|
+
ALL_BUDGET_TABLES,
|
|
100
|
+
llm_budget_snapshots,
|
|
101
|
+
llm_budgets,
|
|
102
|
+
llm_cost_rates,
|
|
103
|
+
)
|
|
104
|
+
from pf_core.budget.audit import ( # noqa: F401
|
|
105
|
+
record_blocked_run,
|
|
106
|
+
record_override,
|
|
107
|
+
)
|
|
108
|
+
from pf_core.budget.config import sync_budgets_from_yaml # noqa: F401
|
|
109
|
+
from pf_core.budget.repo import ( # noqa: F401
|
|
110
|
+
BudgetRepo,
|
|
111
|
+
BudgetSnapshotRepo,
|
|
112
|
+
CostRateRepo,
|
|
113
|
+
aggregate_spent,
|
|
114
|
+
)
|
|
115
|
+
from pf_core.budget.scheduler import start_budget_refresh_loop # noqa: F401
|
|
116
|
+
from pf_core.budget.snapshot_job import refresh_snapshots # noqa: F401
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
__all__ = [
|
|
120
|
+
# Guard (eager)
|
|
121
|
+
"check_budget",
|
|
122
|
+
"project_cost",
|
|
123
|
+
"CostBudgetExceeded",
|
|
124
|
+
"compute_period_start",
|
|
125
|
+
"compute_period_end",
|
|
126
|
+
# Config — kernel-safe (eager)
|
|
127
|
+
"load_yaml",
|
|
128
|
+
"clear_config_cache",
|
|
129
|
+
# Config — DB-backed (lazy)
|
|
130
|
+
"sync_budgets_from_yaml",
|
|
131
|
+
# Repos (lazy)
|
|
132
|
+
"BudgetRepo",
|
|
133
|
+
"BudgetSnapshotRepo",
|
|
134
|
+
"CostRateRepo",
|
|
135
|
+
"aggregate_spent",
|
|
136
|
+
# Snapshot / scheduler jobs (lazy)
|
|
137
|
+
"refresh_snapshots",
|
|
138
|
+
"start_budget_refresh_loop",
|
|
139
|
+
# Audit (lazy)
|
|
140
|
+
"record_blocked_run",
|
|
141
|
+
"record_override",
|
|
142
|
+
# Schema (lazy)
|
|
143
|
+
"ALL_BUDGET_TABLES",
|
|
144
|
+
"llm_budgets",
|
|
145
|
+
"llm_budget_snapshots",
|
|
146
|
+
"llm_cost_rates",
|
|
147
|
+
]
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SQLAlchemy schema for pf-core cost budgets.
|
|
3
|
+
|
|
4
|
+
Defines three framework-owned tables that enforce per-agent, per-job, and
|
|
5
|
+
per-tag cost caps on LLM calls:
|
|
6
|
+
|
|
7
|
+
- ``llm_budgets`` — authoritative budget definitions (one row per scope+period)
|
|
8
|
+
- ``llm_budget_snapshots`` — periodic aggregate cache for fast pre-call checks
|
|
9
|
+
- ``llm_cost_rates`` — per-model price list for projecting call cost
|
|
10
|
+
|
|
11
|
+
Shares the ``metadata`` object from ``pf_core.llm.tracking.schema`` so a
|
|
12
|
+
single ``metadata.create_all()`` creates tracking, jobs, cache, and budget
|
|
13
|
+
tables in one pass.
|
|
14
|
+
|
|
15
|
+
See ``docs/cost-budget.md`` for the full reference.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from sqlalchemy import (
|
|
21
|
+
Boolean,
|
|
22
|
+
Column,
|
|
23
|
+
Date,
|
|
24
|
+
ForeignKey,
|
|
25
|
+
Index,
|
|
26
|
+
Integer,
|
|
27
|
+
Numeric,
|
|
28
|
+
PrimaryKeyConstraint,
|
|
29
|
+
String,
|
|
30
|
+
Table,
|
|
31
|
+
UniqueConstraint,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
from pf_core.llm.tracking.schema import (
|
|
35
|
+
_JSON,
|
|
36
|
+
_PK_SMALL,
|
|
37
|
+
_FK_SMALL,
|
|
38
|
+
_TIMESTAMP_US,
|
|
39
|
+
_server_now,
|
|
40
|
+
metadata,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Tables
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
llm_budgets = Table(
|
|
48
|
+
"llm_budgets",
|
|
49
|
+
metadata,
|
|
50
|
+
Column("id", _PK_SMALL, primary_key=True, autoincrement=True),
|
|
51
|
+
Column("scope_kind", String(32), nullable=False),
|
|
52
|
+
Column("scope_value", String(128), nullable=True),
|
|
53
|
+
Column("period", String(16), nullable=False),
|
|
54
|
+
Column("limit_usd", Numeric(12, 4), nullable=False),
|
|
55
|
+
Column("soft_thresholds", _JSON, nullable=True),
|
|
56
|
+
Column("hard_cap", Boolean, nullable=False, server_default="1"),
|
|
57
|
+
Column("action", String(32), nullable=False, server_default="block"),
|
|
58
|
+
Column("enabled", Boolean, nullable=False, server_default="1"),
|
|
59
|
+
Column("created_at", _TIMESTAMP_US, nullable=False, server_default=_server_now()),
|
|
60
|
+
Column("updated_at", _TIMESTAMP_US, nullable=False, server_default=_server_now()),
|
|
61
|
+
UniqueConstraint(
|
|
62
|
+
"scope_kind", "scope_value", "period", name="uq_llm_budgets_scope_period"
|
|
63
|
+
),
|
|
64
|
+
Index("idx_llm_budgets_enabled", "enabled"),
|
|
65
|
+
)
|
|
66
|
+
"""Budget definitions. scope_kind ∈ {global, agent, job_kind, job_id, tag}."""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
llm_budget_snapshots = Table(
|
|
70
|
+
"llm_budget_snapshots",
|
|
71
|
+
metadata,
|
|
72
|
+
Column(
|
|
73
|
+
"budget_id",
|
|
74
|
+
_FK_SMALL,
|
|
75
|
+
ForeignKey("llm_budgets.id", ondelete="CASCADE"),
|
|
76
|
+
nullable=False,
|
|
77
|
+
),
|
|
78
|
+
Column("period_start", Date, nullable=False),
|
|
79
|
+
Column("spent_usd", Numeric(12, 4), nullable=False, server_default="0"),
|
|
80
|
+
Column("run_count", Integer, nullable=False, server_default="0"),
|
|
81
|
+
Column(
|
|
82
|
+
"last_updated", _TIMESTAMP_US, nullable=False, server_default=_server_now()
|
|
83
|
+
),
|
|
84
|
+
PrimaryKeyConstraint("budget_id", "period_start", name="pk_llm_budget_snapshots"),
|
|
85
|
+
Index("idx_llm_budget_snapshots_period", "period_start"),
|
|
86
|
+
)
|
|
87
|
+
"""Periodic aggregate cache. Not source of truth — rebuilt from llm_runs."""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
llm_cost_rates = Table(
|
|
91
|
+
"llm_cost_rates",
|
|
92
|
+
metadata,
|
|
93
|
+
Column(
|
|
94
|
+
"model_id",
|
|
95
|
+
_FK_SMALL,
|
|
96
|
+
ForeignKey("llm_models.id", ondelete="CASCADE"),
|
|
97
|
+
nullable=False,
|
|
98
|
+
),
|
|
99
|
+
Column("input_per_1k", Numeric(8, 6), nullable=False),
|
|
100
|
+
Column("output_per_1k", Numeric(8, 6), nullable=False),
|
|
101
|
+
Column("cache_read_per_1k", Numeric(8, 6), nullable=True),
|
|
102
|
+
Column("cache_write_per_1k", Numeric(8, 6), nullable=True),
|
|
103
|
+
Column("reasoning_per_1k", Numeric(8, 6), nullable=True),
|
|
104
|
+
Column("effective_from", Date, nullable=False),
|
|
105
|
+
Column("effective_to", Date, nullable=True),
|
|
106
|
+
PrimaryKeyConstraint(
|
|
107
|
+
"model_id", "effective_from", name="pk_llm_cost_rates"
|
|
108
|
+
),
|
|
109
|
+
Index("idx_llm_cost_rates_model_eff", "model_id", "effective_from"),
|
|
110
|
+
)
|
|
111
|
+
"""Per-model price list. Multiple rows per model allowed (versioned pricing)."""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
# Public table list (in dependency order)
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
ALL_BUDGET_TABLES = (
|
|
119
|
+
llm_budgets,
|
|
120
|
+
llm_budget_snapshots,
|
|
121
|
+
llm_cost_rates,
|
|
122
|
+
)
|
pf_core/budget/audit.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Audit helpers for blocked and override budget events.
|
|
3
|
+
|
|
4
|
+
``record_blocked_run()`` writes a zero-cost ``llm_runs`` row with
|
|
5
|
+
``status='budget_blocked'`` plus ``budget:blocked`` + ``budget:scope=...``
|
|
6
|
+
tags so analytics can answer "how many calls did the budget stop?".
|
|
7
|
+
|
|
8
|
+
``record_override()`` attaches a ``budget:override`` tag and an
|
|
9
|
+
``llm_run_outcomes`` row to an existing run.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pf_core.budget.check import CostBudgetExceeded
|
|
15
|
+
from pf_core.llm.tracking.repo import LlmRunRepo
|
|
16
|
+
from pf_core.llm.tracking.schema import llm_run_tags
|
|
17
|
+
from pf_core.llm.tracking.subrepos import LlmRunOutcomeRepo
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def record_blocked_run(
|
|
21
|
+
*,
|
|
22
|
+
agent_type: str,
|
|
23
|
+
model: str,
|
|
24
|
+
exc: CostBudgetExceeded,
|
|
25
|
+
job_id: int | None = None,
|
|
26
|
+
) -> int:
|
|
27
|
+
"""Insert a ``status='budget_blocked'`` run with scope tags.
|
|
28
|
+
|
|
29
|
+
Returns the new ``llm_runs.id``.
|
|
30
|
+
"""
|
|
31
|
+
descriptor = (
|
|
32
|
+
f"{exc.scope_kind}:{exc.scope_value}:{exc.period}"
|
|
33
|
+
if exc.scope_value
|
|
34
|
+
else f"{exc.scope_kind}:{exc.period}"
|
|
35
|
+
)
|
|
36
|
+
tags = ["budget:blocked", f"budget:scope={descriptor}"]
|
|
37
|
+
|
|
38
|
+
run_id = LlmRunRepo().record(
|
|
39
|
+
agent_type=agent_type,
|
|
40
|
+
model=model,
|
|
41
|
+
status="budget_blocked",
|
|
42
|
+
usage={
|
|
43
|
+
"cost_usd": 0.0,
|
|
44
|
+
"prompt_tokens": 0,
|
|
45
|
+
"completion_tokens": 0,
|
|
46
|
+
"duration_ms": 1,
|
|
47
|
+
},
|
|
48
|
+
error=str(exc),
|
|
49
|
+
error_class="CostBudgetExceeded",
|
|
50
|
+
job_id=job_id,
|
|
51
|
+
tags=tags,
|
|
52
|
+
)
|
|
53
|
+
return int(run_id)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def record_override(
|
|
57
|
+
*,
|
|
58
|
+
run_id: int,
|
|
59
|
+
reason: str,
|
|
60
|
+
operator: str | None = None,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Tag *run_id* as a budget override and write an outcome row."""
|
|
63
|
+
from pf_core.db.connection import transaction
|
|
64
|
+
|
|
65
|
+
with transaction() as conn:
|
|
66
|
+
conn.execute(
|
|
67
|
+
llm_run_tags.insert().values(llm_run_id=run_id, tag="budget:override")
|
|
68
|
+
)
|
|
69
|
+
LlmRunOutcomeRepo().record(
|
|
70
|
+
run_id,
|
|
71
|
+
outcome_kind="budget_override",
|
|
72
|
+
notes=reason if not operator else f"{reason} (operator: {operator})",
|
|
73
|
+
)
|