mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-18 12:39:28 +00:00
feat(learn): metered embeddings and the cache (decision-engine 6.2)
plane/models/openrouter-embedding.yaml: openai/text-embedding-3-small via OpenRouter (embeddings only), input_per_m 0.02, cache/output 0, egress openrouter.ai; `openrouter` joins the provider extensions. plane/budget.py MeteredEmbeddingClient: prompt_tokens priced from the Model manifest, usd/calls ceilings stop the next call, a priced call that reports no tokens raises UnmeteredUsage (a silent zero is an instrument failure). OpenRouterEmbeddingClient keeps one usage row per call (parse_embedding_payload); its api key is out of repr. learn/embeddings.py + `ousast learn memory embed`: excerpts embedded in batches into embeddings/<model-slug>/<excerpt_sha>.json (float32 base64), cached by content hash so a rerun makes no call; the declared dimension is checked; --dry-run estimates tokens and cost without a call; a paid run needs --budget-usd and exits 3 (unfinished) at its ceiling. Not run against the provider: the store embedding waits for its budget go-ahead. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7b81c9ac4e
commit
98fa4ec8cd
@@ -10,7 +10,7 @@ failure is `AccountError` (the task is `failed` and the run starts nothing furth
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
from typing import Any, Protocol
|
||||
|
||||
from ..model.endpoint import Prices
|
||||
from ..tool_hunter import ChatClient, ChatResponse
|
||||
@@ -160,4 +160,87 @@ def _tokens(value: object) -> int:
|
||||
return int(value) if isinstance(value, int | float) and not isinstance(value, bool) else 0
|
||||
|
||||
|
||||
__all__ = ["AccountError", "BudgetExhausted", "MeteredClient", "cost_of", "is_account_error", "prices_from"]
|
||||
class EmbeddingClient(Protocol):
|
||||
def embed(self, *, model: str, inputs: list[str], timeout_seconds: int = 60) -> list[list[float]]: ...
|
||||
|
||||
|
||||
class UnmeteredUsage(RuntimeError):
|
||||
"""A priced embedding call whose provider reported no tokens: a working-looking zero, never a free call."""
|
||||
|
||||
|
||||
class MeteredEmbeddingClient:
|
||||
"""An embedding client metered like :class:`MeteredClient`: the wrapped client records one usage dict per call
|
||||
in a ``usage`` list (``OpenRouterEmbeddingClient`` does), ``prompt_tokens`` are priced at the Model manifest's
|
||||
``input_per_m``, and the usd/calls ceilings stop the next call. A priced call that reports no tokens raises
|
||||
:class:`UnmeteredUsage` (the 2026-09 lesson: a silent zero is an instrument failure)."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
inner: EmbeddingClient,
|
||||
*,
|
||||
prices: Mapping[str, object] | Prices | None = None,
|
||||
budget_usd: float | None = None,
|
||||
budget_calls: int | None = None,
|
||||
require_usage: bool = True,
|
||||
) -> None:
|
||||
self.inner = inner
|
||||
self.prices = prices_from(prices)
|
||||
if budget_usd is not None and self.prices is None:
|
||||
raise ValueError("unpriced model cannot run under a usd budget")
|
||||
self.budget_usd, self.budget_calls, self.require_usage = budget_usd, budget_calls, require_usage
|
||||
self.calls = 0
|
||||
self.tokens = 0
|
||||
self._usd = 0.0
|
||||
self._seen_rows = len(self._inner_rows())
|
||||
|
||||
@property
|
||||
def usd(self) -> float | None:
|
||||
return self._usd if self.prices is not None else None
|
||||
|
||||
def check(self) -> None:
|
||||
if self.budget_usd is not None and self._usd >= self.budget_usd:
|
||||
raise BudgetExhausted(f"usd budget exhausted: spent {self._usd:.6f} of {self.budget_usd:.6f}", usd=self._usd, calls=self.calls)
|
||||
if self.budget_calls is not None and self.calls >= self.budget_calls:
|
||||
raise BudgetExhausted(f"calls budget exhausted: {self.calls} of {self.budget_calls}", usd=self.usd, calls=self.calls)
|
||||
|
||||
def embed(self, *, model: str, inputs: list[str], timeout_seconds: int = 60) -> list[list[float]]:
|
||||
self.check()
|
||||
try:
|
||||
vectors = self.inner.embed(model=model, inputs=inputs, timeout_seconds=timeout_seconds)
|
||||
except Exception as exc:
|
||||
self._absorb_rows()
|
||||
if is_account_error(exc):
|
||||
raise AccountError(str(exc)) from exc
|
||||
raise
|
||||
self.calls += 1
|
||||
tokens = self._absorb_rows()
|
||||
if self.require_usage and self.prices is not None and tokens == 0 and inputs:
|
||||
raise UnmeteredUsage(
|
||||
f"embedding call {self.calls} reported no prompt tokens for {len(inputs)} inputs: the meter cannot price it"
|
||||
)
|
||||
return vectors
|
||||
|
||||
def summary(self) -> dict[str, Any]:
|
||||
return {"usd": self.usd, "calls": self.calls, "usage": {"prompt_tokens": self.tokens}, "priced": self.prices is not None}
|
||||
|
||||
def _inner_rows(self) -> list[Any]:
|
||||
rows = getattr(self.inner, "usage", None)
|
||||
return rows if isinstance(rows, list) else []
|
||||
|
||||
def _absorb_rows(self) -> int:
|
||||
rows = self._inner_rows()
|
||||
tokens = 0
|
||||
for row in rows[self._seen_rows :]:
|
||||
if isinstance(row, Mapping):
|
||||
tokens += _tokens(row.get("prompt_tokens") or row.get("total_tokens"))
|
||||
self._seen_rows = len(rows)
|
||||
self.tokens += tokens
|
||||
if self.prices is not None:
|
||||
self._usd += tokens * self.prices.input_per_m / 1_000_000
|
||||
return tokens
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AccountError", "BudgetExhausted", "EmbeddingClient", "MeteredClient", "MeteredEmbeddingClient", "UnmeteredUsage", "cost_of",
|
||||
"is_account_error", "prices_from",
|
||||
] # fmt: skip
|
||||
|
||||
@@ -59,7 +59,7 @@ GIT_COMMITS_ANNOTATION = "openultrasast.io/git-commits"
|
||||
EGRESS_HOSTS_ANNOTATION = "openultrasast.io/egress-hosts" # the Model API hosts a bound task may reach
|
||||
_DNS_NAME = re.compile(r"^(?=.{1,253}$)[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$")
|
||||
AX_PROVIDERS = frozenset({"google", "anthropic"})
|
||||
EXTENSION_PROVIDERS = frozenset({"deepseek"})
|
||||
EXTENSION_PROVIDERS = frozenset({"deepseek", "openrouter"}) # openrouter: embeddings only (decision engine)
|
||||
|
||||
_API_VERSIONS = {"Task": AX_API_VERSION, "Workspace": AX_API_VERSION, "Model": AX_API_VERSION, "Run": RUN_API_VERSION}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user