Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ Certified routers can be served with CUDA graphs and fused kernels for RMSNorm,

The package installs as `microtensor`, the customer facing name. The network tooling installs as `microtensor_subnet` (distribution `microtensor-subnet`), so both can live in one environment.

## Layout

| Module | Holds |
|---|---|
| `microtensor/engine.py` | `Engine`, plus `catalogue()`, `served_tasks()` and `frontier_models()` |
| `microtensor/catalogue.py` | `Catalogue` and `Served`: the live task list and name resolution |
| `microtensor/backends/hosted.py` | The hosted backend: the HTTP client for the API |
| `microtensor/result.py` | `Result`, built from a chat completion |
| `microtensor/budget.py` | `Budget`: the daily escalation allowance |
| `microtensor/errors.py` | `EngineError` and `TaskUnavailable` |
| `microtensor/constants.py` | Base URL, `mt/auto`, default floor and timeout |

The local backend joins `backends/` with the Enterprise preview.

## Development

pip install -e ".[dev]"
Expand Down
23 changes: 11 additions & 12 deletions microtensor/__init__.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
from microtensor.engine import (
Budget,
Catalogue,
Engine,
EngineError,
Result,
Served,
TaskUnavailable,
catalogue,
frontier_models,
served_tasks,
)
from microtensor.backends.hosted import Hosted
from microtensor.budget import Budget
from microtensor.catalogue import Catalogue, Served
from microtensor.constants import AUTO, DEFAULT_BASE_URL, DEFAULT_FLOOR
from microtensor.engine import Engine, catalogue, frontier_models, served_tasks
from microtensor.errors import EngineError, TaskUnavailable
from microtensor.result import Result

__all__ = [
"AUTO",
"DEFAULT_BASE_URL",
"DEFAULT_FLOOR",
"Budget",
"Catalogue",
"Engine",
"EngineError",
"Hosted",
"Result",
"Served",
"TaskUnavailable",
Expand Down
3 changes: 3 additions & 0 deletions microtensor/backends/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from microtensor.backends.hosted import Hosted

__all__ = ["Hosted"]
82 changes: 82 additions & 0 deletions microtensor/backends/hosted.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from __future__ import annotations

import os
from collections.abc import Mapping, Sequence
from typing import Any, Final

import httpx

from microtensor.catalogue import Catalogue
from microtensor.constants import TIMEOUT_SECONDS
from microtensor.errors import EngineError

NO_KEY: Final[str] = "set MICROTENSOR_API_KEY or pass api_key"


def messages(question: str, context: str) -> list[dict[str, str]]:
found: list[dict[str, str]] = []
if context.strip():
found.append({"role": "system", "content": context})
found.append({"role": "user", "content": question})
return found


class Hosted:
def __init__(
self, api_key: str, base_url: str, *, transport: httpx.BaseTransport | None = None
) -> None:
if not api_key:
raise EngineError(NO_KEY)
self._http = httpx.Client(
base_url=base_url.rstrip("/"),
headers={"authorization": f"Bearer {api_key}", "accept": "application/json"},
timeout=TIMEOUT_SECONDS,
transport=transport,
)

@classmethod
def connect(
cls,
api_key: str | None,
base_url: str,
transport: httpx.BaseTransport | None = None,
) -> Hosted:
return cls(
api_key or os.environ.get("MICROTENSOR_API_KEY", ""), base_url, transport=transport
)

def close(self) -> None:
self._http.close()

def _body(self, response: httpx.Response) -> dict[str, Any]:
try:
body = response.json()
except ValueError as exc:
raise EngineError("the API answered with something that is not JSON") from exc
if response.status_code >= 400:
error = body.get("error") if isinstance(body, dict) else {}
error = error if isinstance(error, dict) else {}
raise EngineError(
f"{error.get('code', 'error')}: {error.get('message', response.text[:200])}"
)
if not isinstance(body, dict):
raise EngineError("unexpected answer from the API")
return body

def models(self) -> list[dict[str, Any]]:
body = self._body(self._http.get("/models"))
return [dict(m) for m in body.get("data", [])]

def catalogue(self) -> Catalogue:
return Catalogue.from_models(self.models())

def chat(
self, *, model: str, messages: Sequence[Mapping[str, str]], quality_floor: float
) -> dict[str, Any]:
payload = {
"model": model,
"messages": [dict(m) for m in messages],
"quality_floor": quality_floor,
"stream": False,
}
return self._body(self._http.post("/chat/completions", json=payload))
29 changes: 29 additions & 0 deletions microtensor/budget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from __future__ import annotations

import datetime as dt
from dataclasses import dataclass, field


def today() -> dt.date:
return dt.datetime.now(dt.timezone.utc).date()


@dataclass(slots=True)
class Budget:
usd_per_day: float | None = None
spent: float = 0.0
day: dt.date = field(default_factory=today)

def allows(self, cost: float) -> bool:
self._roll()
return self.usd_per_day is None or self.spent + cost <= self.usd_per_day

def charge(self, cost: float) -> None:
self._roll()
self.spent += cost

def _roll(self) -> None:
now = today()
if now != self.day:
self.day = now
self.spent = 0.0
89 changes: 89 additions & 0 deletions microtensor/catalogue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from __future__ import annotations

from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from typing import Any

from microtensor.constants import AUTO


def normal(name: str) -> str:
return name.strip().lower().replace(" ", "-")


@dataclass(frozen=True, slots=True)
class Served:
task: str
model: str
title: str
aliases: tuple[str, ...]
live: bool

@property
def names(self) -> tuple[str, ...]:
raw = [
self.task,
self.task.replace("_", "-"),
self.model,
self.model.removeprefix("mt/"),
*self.aliases,
]
return tuple(dict.fromkeys(normal(n) for n in raw if n))

@property
def label(self) -> str:
return self.aliases[0] if self.aliases else self.task


@dataclass(frozen=True, slots=True)
class Catalogue:
served: tuple[Served, ...]
frontier: tuple[str, ...]

@classmethod
def from_models(cls, models: Sequence[Mapping[str, Any]]) -> Catalogue:
entries = [dict(m) for m in models]
picked: dict[str, str] = {}
live: dict[str, bool] = {}
for entry in entries:
if entry.get("id") != AUTO:
continue
for item in entry.get("tasks") or []:
task = str(item.get("task", ""))
if task:
picked[task] = str(item.get("model", ""))
live[task] = bool(item.get("live", False))
for entry in entries:
task = str(entry.get("task", ""))
if task and entry.get("id") != AUTO and task not in picked:
picked[task] = str(entry.get("id", ""))
live[task] = bool((entry.get("capacity") or {}).get("live", False))
by_id = {str(e.get("id", "")): e for e in entries}
served = tuple(
Served(
task=task,
model=model,
title=str(by_id.get(model, {}).get("title", "")),
aliases=tuple(str(a) for a in by_id.get(model, {}).get("aliases") or []),
live=live.get(task, False),
)
for task, model in sorted(picked.items())
)
frontier = tuple(
str(e["id"]) for e in entries if e.get("kind") == "frontier" and e.get("id")
)
return cls(served=served, frontier=frontier)

def resolve(self, name: str) -> Served | None:
wanted = normal(name)
for entry in self.served:
if wanted in entry.names:
return entry
return None

def describe(self) -> str:
parts: list[str] = []
for entry in self.served:
extra = f" ({', '.join(entry.aliases)})" if entry.aliases else ""
parts.append(f"{entry.task}{extra}")
return ", ".join(parts) or "none"
6 changes: 6 additions & 0 deletions microtensor/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from typing import Final

DEFAULT_BASE_URL: Final[str] = "https://api.microtensor.cloud/v1"
AUTO: Final[str] = "mt/auto"
DEFAULT_FLOOR: Final[float] = 0.9
TIMEOUT_SECONDS: Final[float] = 180.0
Loading
Loading