Spaces:
Running
Running
deploy: feature/audiobook@f1c0d35 (fix batch β models/providers, TTS $0, grants, DB admins, delete-hardening)
833ba13 verified | """``/tools/*`` β read-only registry view. | |
| Two endpoints: | |
| - ``GET /tools?category=`` β list, optionally filtered to one category. | |
| - ``GET /tools/{name}`` β single tool detail. | |
| Both wrap :func:`src.lib.tools.repo.load_registry`. No paid calls. | |
| """ | |
| from __future__ import annotations | |
| from fastapi import APIRouter, Depends, Path, Query | |
| from src.api.deps import forbid_readers | |
| from src.api.dto.tools import ( | |
| ToolCategoryEnum, | |
| ToolDTO, | |
| ToolPricingDTO, | |
| ToolStatusEnum, | |
| ) | |
| from src.api.errors import ApiError | |
| from src.lib.auth.models import User | |
| from src.lib.llm_keys.store import KNOWN_PROVIDERS, resolve_provider_key | |
| from src.lib.tools.models import Tool | |
| from src.lib.tools.repo import get_tool, load_registry | |
| router = APIRouter(prefix="/tools", tags=["tools"]) | |
| class ToolNotFound(ApiError): | |
| """404 for ``GET /tools/{name}`` when the registry has no such tool. | |
| Local to this router β every other 404 is on the canonical list in | |
| ``src/api/errors.py``. We don't add this to the central module | |
| because the tools registry is a single YAML file and an "unknown | |
| tool" lookup is more of a programming error than a real user-facing | |
| state. | |
| """ | |
| code = "TOOL_NOT_FOUND" | |
| status_code = 404 | |
| def _to_dto(tool: Tool) -> ToolDTO: | |
| pricing = None | |
| if tool.pricing is not None: | |
| # tools_registry.yaml uses YAML date literals; PyYAML parses them to | |
| # ``datetime.date`` objects. The wire shape is a string (ISO date) β | |
| # the FE renders it as-is in the "last verified" column. | |
| last_updated_raw = tool.pricing.last_updated | |
| last_updated = str(last_updated_raw) if last_updated_raw is not None else None | |
| pricing = ToolPricingDTO( | |
| input_per_million_usd=tool.pricing.input_per_million_usd, | |
| output_per_million_usd=tool.pricing.output_per_million_usd, | |
| image_input_tokens_per_image=tool.pricing.image_input_tokens_per_image, | |
| per_request_usd=tool.pricing.per_request_usd, | |
| last_updated=last_updated, | |
| verified=tool.pricing.verified, | |
| ) | |
| # Task A1b: for llm tools, surface whether the provider's key is | |
| # resolvable (store or env) so the FE picker can disable unconfigured | |
| # providers. Only meaningful for the 5 managed LLM providers; None for | |
| # everything else. Never exposes the key value. | |
| active: bool | None = None | |
| if tool.category == "llm" and tool.provider in KNOWN_PROVIDERS: | |
| active = resolve_provider_key(tool.provider) is not None | |
| return ToolDTO( | |
| name=tool.name, | |
| category=ToolCategoryEnum(tool.category), | |
| status=ToolStatusEnum(tool.status), | |
| used_by=list(tool.used_by), | |
| compatible_with=list(tool.compatible_with), | |
| source_file=tool.source_file, | |
| provider=tool.provider, | |
| pricing=pricing, | |
| notes=tool.notes, | |
| active=active, | |
| ) | |
| def list_tools( | |
| _: User = Depends(forbid_readers), | |
| category: ToolCategoryEnum | None = Query(default=None), | |
| ) -> list[ToolDTO]: | |
| """All tools, optionally filtered to one category. | |
| Active tools sort first, then stubbed, then planned (matches the | |
| Operations Tools page's column order). | |
| """ | |
| rank = {"active": 0, "stubbed": 1, "planned": 2} | |
| tools = list(load_registry()) | |
| if category is not None: | |
| tools = [t for t in tools if t.category == category.value] | |
| tools.sort(key=lambda t: (rank[t.status], t.name)) | |
| return [_to_dto(t) for t in tools] | |
| def get_tool_endpoint( | |
| name: str = Path(...), | |
| _: User = Depends(forbid_readers), | |
| ) -> ToolDTO: | |
| tool = get_tool(name) | |
| if tool is None: | |
| raise ToolNotFound(f"No tool named {name!r} in tools_registry.yaml.") | |
| return _to_dto(tool) | |