diff --git a/CHANGELOG.md b/CHANGELOG.md index 2458c6c..13b4de4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,21 @@ All notable changes to this project are documented here. The format follows [Kee ## [Unreleased] +## [0.2.0] - 2026-09-25 + +### Added +- `await router.aroute(task, limits=None)`: async version of `route()` for asyncio apps (#18). +- `Router(timeout=60)`: configurable HTTP timeout in seconds for catalog downloads and routing calls (#13). + ### Fixed -- `request_json` now raises `RouterError` (with the original exception chained) for non-JSON responses, read timeouts and connection errors, so `except RouterError:` fallbacks work as documented. +- `request_json` now raises `RouterError` (with the original exception chained) for non-JSON responses, read timeouts and connection errors, so `except RouterError:` fallbacks work as documented (#5). +- A JSON response that isn't an object (e.g. `null` or a list), or a model list without `data`, now raises `RouterError` instead of `AttributeError`/`KeyError`. +- `Limits` rejects invalid values (`output_tokens < 1`, negative or NaN `max_cost_usd`, booleans) with `ValueError` (#9). +- `Router` rejects a non-positive or non-numeric `timeout` with `ValueError`. +- Duplicate model ids or providers are removed, keeping first-seen order, so Jev never sees the same model twice (#8). + +### Internal +- The examples run offline in CI with mocked network calls (#20). ## [0.1.0] - 2026-09-23 @@ -18,5 +31,6 @@ All notable changes to this project are documented here. The format follows [Kee - `router.api_key_for(model_id)` returns your key for the chosen model's provider. - Examples for basic routing and agents (plan-and-execute, multi-turn chat, fallback). -[Unreleased]: https://github.com/TheCoder30ec4/model_router_python/compare/v0.1.0...HEAD +[Unreleased]: https://github.com/TheCoder30ec4/model_router_python/compare/v0.2.0...HEAD +[0.2.0]: https://github.com/TheCoder30ec4/model_router_python/compare/v0.1.0...v0.2.0 [0.1.0]: https://github.com/TheCoder30ec4/model_router_python/releases/tag/v0.1.0 diff --git a/README.md b/README.md index 17f706b..3ba0933 100644 --- a/README.md +++ b/README.md @@ -174,10 +174,11 @@ These are real outputs from `examples/basic.py`. | Call | What it does | |---|---| -| `Router(*, jev_api_key=None, openrouter_api_key=None, providers=None, models=None, limits=Limits(), models_per_provider=None, timeout=60)` | All arguments are keyword-only. Loads live prices, context sizes and output limits (cached for 24h, no key needed). `timeout` is the HTTP timeout in seconds for catalog downloads and Jev/OpenRouter routing calls (e.g. `timeout=5`); it is passed to `urllib.request.urlopen`, not a total routing deadline. Raises `UnknownModelError` for unknown model ids or providers. | +| `Router(*, jev_api_key=None, openrouter_api_key=None, providers=None, models=None, limits=Limits(), models_per_provider=None, timeout=60)` | All arguments are keyword-only. Loads live prices, context sizes and output limits (cached for 24h, no key needed). `timeout` is the HTTP timeout in seconds for catalog downloads and Jev/OpenRouter routing calls (e.g. `timeout=5`); it is passed to `urllib.request.urlopen`, not a total routing deadline, and must be a finite number > 0 (`ValueError` otherwise). Raises `UnknownModelError` for unknown model ids or providers. | | `refresh_catalog()` | Clears the cached model list, so the next `Router` downloads fresh prices. | | `router.api_key_for(model_id) -> str \| None` | The key you passed in `providers={...}` for this model's provider. | | `router.route(task, limits=None) -> str` | Returns the best model id for `task`. | +| `await router.aroute(task, limits=None) -> str` | Async version of `route()` for asyncio apps. Runs `route()` in a worker thread, so the event loop isn't blocked; same results and errors. | | `router.fitting(task, limits=None) -> list[ModelInfo]` | Returns the models that pass the limits, without calling Jev (free). | | `Limits(output_tokens=1024, max_cost_usd=None)` | The output size you expect and an optional cost cap for each call. | @@ -188,7 +189,7 @@ Booleans and NaN are rejected. Invalid limits raise `ValueError` with the field Routing errors (all subclasses of `RouterError`): - `NoModelFitsError`: no model passes the limits. The message gives the reason for each model. - `UnknownModelError`: a model id isn't on OpenRouter. -- `RouterError`: no routing key, a network or HTTP failure, or an error returned by Jev (e.g. a rate limit). +- `RouterError`: no routing key, a network or HTTP failure, a timeout, a response that isn't valid JSON, or an error returned by Jev (e.g. a rate limit). ### Using it in an agent diff --git a/docs/index.html b/docs/index.html index f4ed00c..2222be3 100644 --- a/docs/index.html +++ b/docs/index.html @@ -616,8 +616,9 @@
| Call | What it does |
|---|---|
Router(*, jev_api_key=None, openrouter_api_key=None, providers=None, models=None, limits=Limits(), models_per_provider=None) | Keyword-only. Loads live model data (cached for 24h) and works out the candidate list. Raises UnknownModelError for unknown model ids or providers. |
Router(*, jev_api_key=None, openrouter_api_key=None, providers=None, models=None, limits=Limits(), models_per_provider=None, timeout=60) | Keyword-only. Loads live model data (cached for 24h) and works out the candidate list. timeout is the HTTP timeout in seconds for each request. Raises UnknownModelError for unknown model ids or providers. |
router.route(task, limits=None) → str | The best model id for task. |
await router.aroute(task, limits=None) → str | Async route() for asyncio apps; doesn't block the event loop. |
router.fitting(task, limits=None) → list[ModelInfo] | The models that pass the limits, without calling Jev (free). |
router.api_key_for(model_id) → str | None | The key you passed in providers={...} for that model's provider. |
Limits(output_tokens=1024, max_cost_usd=None) | The output size you expect and an optional cost cap for each call. |