Compare commits

...
Author SHA1 Message Date
Cursor Agent 46a1e8ebef CI: run pytest with junitxml and coverage artifacts
Generate pytest.xml and coverage output so artifact upload step stops warning.
2025-10-05 02:30:31 +00:00
Cursor Agent 9593437a81 Fix: mypy error for httpx proxies across versions to stabilize CI
CI was failing pytests; locally ensured tests pass and added a minor typing fix in discovery to avoid mypy breakage across httpx versions. Also validated ruff/mypy in workflow context.
2025-10-04 08:55:38 +00:00
redshiftandGitHub 801bc50fd7 Added url normalization for mint urls from .env
Otherwise even valid mint tokens are unnecessarily swapped to primary mint. Faced this issue myself.
2025-09-24 13:29:30 +00:00
redshiftandGitHub ecbe3158c0 Merge pull request #179 from Routstr/503-mint-service-bug
Fixed the 503 mint service unavailable bug
2025-09-22 16:27:36 +00:00
redshiftandGitHub bac50f2150 Fixed the 503 mint service unavailable bug
This flag fetch all keysets from the db into the wallet, which is wrong given that we're initiating it for a mint. And load_mint sets the first keyset from all the keysets from the db, irrespective of the unit and the mint. 

Fixes #175
2025-09-22 16:25:02 +00:00
shroominicandGitHub 4bf02226dc Merge pull request #172 from Routstr/fix-max-tokens-string-int
fix max-tokens coming in as string
2025-09-16 13:05:39 +01:00
Shroominic c8f4e7d3e5 fix max-tokens coming in as string 2025-09-16 13:00:38 +01:00
shroominicandGitHub 3d4a8e0e14 Merge pull request #169 from Routstr/dev
## Routstr v0.1.3 — 2025-09-15

### Highlights

- **DB-backed settings with live updates**: Env vars seed on first run; DB is the source of truth (except `DATABASE_URL`). Manage via Admin UI or `PATCH /admin/api/settings`.
- **Models moved to database** with background sats-pricing computation and optional periodic refresh.
- **Smarter max-cost reservation**: Discount based on prompt tokens and `max_tokens` using `TOLERANCE_PERCENTAGE`, with a `MIN_REQUEST_MSAT` floor.
- **Faster startup and provider discovery**; `.onion` endpoints use Tor proxy by default.

### Breaking changes

- **Configuration is DB-first** after bootstrap. Env changes won’t auto-apply (besides `DATABASE_URL`).
- **Models endpoint**: Use `/v1/models` for canonical model info; `/v1/info` keeps an empty `models` field for back-compat.
- **Legacy pricing envs** are auto-mapped but deprecated:
  - `MODEL_BASED_PRICING` → `!FIXED_PRICING`
  - `COST_PER_REQUEST` → `FIXED_COST_PER_REQUEST`
  - `COST_PER_1K_*` → `FIXED_PER_1K_*`

### Added

- **SettingsService** with env→computed→DB merge; runtime edits; app metadata (name/description) applied to OpenAPI.
- **Automatic migrations** on startup; new tables: `settings`, `models`.
- **Per-model sats pricing** and per-request maximums computed in the background.
- **Pricing controls**: `FIXED_PRICING`, `FIXED_COST_PER_REQUEST`, optional per-1k token overrides, `MIN_REQUEST_MSAT`.
- **Azure chat support**: Optional `CHAT_COMPLETIONS_API_VERSION` adds `api-version` to `/chat/completions`.
- **Derive `NPUB` from `NSEC`** during bootstrap if not provided.

### Changed / Improvements

- **Proxy header hardening**: Strip sensitive headers; replace Authorization with upstream key when configured.
- **Discovery**: Read `RELAYS` from settings; Tor proxy defaults for `.onion` via `TOR_PROXY_URL`; faster announcement fetching.
- **Admin**: Improved auth/log UX; fixed log search; show mint balances in dashboard.
- **Performance**: Optimized startup; lazy DB info loading.

### Fixed

- **Refunds**: Correct msat→sat conversion; configurable refund cache TTL via `REFUND_CACHE_TTL_SECONDS`; better error mapping for mint outages.
- **Balances**: Reserved balance reporting and negative-reserved edge cases.
- **Tests**: Updated to patch settings and use fixed-pricing flags.

### Upgrade notes (from v0.1.2)

1. Ensure `DATABASE_URL`, `UPSTREAM_BASE_URL`, and `ADMIN_PASSWORD` are set. Optionally set `UPSTREAM_API_KEY`, `RELAYS`, `CASHU_MINTS`, `TOR_PROXY_URL`.
2. Start the service; automatic Alembic migrations will run (`settings`, `models` tables).
3. Configure settings via Admin UI (`/admin`) or `PATCH /admin/api/settings`. Env changes after first run won’t apply automatically.
4. Prefer new pricing envs or a `models.json` at `MODELS_PATH`. Legacy envs are mapped but will be removed in a future release.
5. Optional tuning: `TOLERANCE_PERCENTAGE`, `MIN_REQUEST_MSAT`, `PRICING_REFRESH_INTERVAL_SECONDS`, `MODELS_REFRESH_INTERVAL_SECONDS`, `ENABLE_*_REFRESH`.
6. For Azure, set `CHAT_COMPLETIONS_API_VERSION`.

### References

- Configuration: `docs/getting-started/configuration.md`
- API: `docs/api/endpoints.md`
- Custom pricing: `docs/advanced/custom-pricing.md`

### Stats and credits

- 42 files changed, +1819/−832 lines.
- Merges: #170 token-discount-max-cost, #171 move-models-to-db.
- Contributors: Shroominic.
2025-09-16 12:43:54 +01:00
4 changed files with 28 additions and 14 deletions
+5 -2
View File
@@ -35,12 +35,15 @@ jobs:
run: |
uv run mypy .
- name: Run tests with pytest
- name: Run tests with pytest (with coverage & junit)
env:
UPSTREAM_BASE_URL: "http://test"
UPSTREAM_API_KEY: "test"
run: |
uv run pytest --verbose --tb=short
uv run pytest \
--verbose --tb=short \
--junitxml=pytest.xml \
--cov=routstr --cov-report=term
- name: Upload test results
if: always()
+5 -3
View File
@@ -320,18 +320,20 @@ async def fetch_provider_health(endpoint_url: str) -> dict[str, Any]:
is_onion = ".onion" in endpoint_url
# Set up client arguments conditionally
proxies = None
proxies: dict[str, str] | None = None
if is_onion:
try:
tor_proxy = settings.tor_proxy_url
except Exception:
tor_proxy = "socks5://127.0.0.1:9050"
proxies = {"http://": tor_proxy, "https://": tor_proxy} # type: ignore[assignment]
proxies = {"http://": tor_proxy, "https://": tor_proxy}
async with httpx.AsyncClient(
timeout=httpx.Timeout(30.0),
follow_redirects=True,
proxies=proxies, # type: ignore[arg-type]
# NOTE: httpx < 0.28 supports the 'proxies' kwarg, 0.28+ removed it.
# We ignore the call-arg type here to keep compatibility across versions.
proxies=proxies, # type: ignore[call-arg]
) as client:
# Prefer provider's /v1/info for full details
info_url = f"{endpoint_url.rstrip('/')}/v1/info"
+16 -7
View File
@@ -183,14 +183,23 @@ async def calculate_discounted_max_cost(
else:
adjusted = adjusted + math.ceil(-estimated_prompt_delta_sats * 1000)
if max_tokens := body.get("max_tokens"):
estimated_completion_delta_sats = (
max_completion_allowed_sats - max_tokens * model_pricing.completion
)
if estimated_completion_delta_sats >= 0:
adjusted = adjusted - math.floor(estimated_completion_delta_sats * 1000)
max_tokens_raw = body.get("max_tokens", None)
if max_tokens_raw is not None:
try:
max_tokens_int = int(max_tokens_raw)
except (TypeError, ValueError):
logger.warning(
"Invalid max_tokens; ignoring in cost adjustment",
extra={"max_tokens": str(max_tokens_raw)[:64], "model": model},
)
else:
adjusted = adjusted + math.ceil(-estimated_completion_delta_sats * 1000)
estimated_completion_delta_sats = (
max_completion_allowed_sats - max_tokens_int * model_pricing.completion
)
if estimated_completion_delta_sats >= 0:
adjusted = adjusted - math.floor(estimated_completion_delta_sats * 1000)
else:
adjusted = adjusted + math.ceil(-estimated_completion_delta_sats * 1000)
logger.debug(
"Discounted max cost computed",
+2 -2
View File
@@ -28,7 +28,7 @@ async def recieve_token(
wallet = await get_wallet(token_obj.mint, token_obj.unit, load=False)
wallet.keyset_id = token_obj.keysets[0]
if token_obj.mint not in settings.cashu_mints:
if token_obj.mint.rstrip("/") not in [mint.rstrip("/") for mint in settings.cashu_mints]:
return await swap_to_primary_mint(token_obj, wallet)
wallet.verify_proofs_dleq(token_obj.proofs)
@@ -153,7 +153,7 @@ async def get_wallet(mint_url: str, unit: str = "sat", load: bool = True) -> Wal
id = f"{mint_url}_{unit}"
if id not in _wallets:
_wallets[id] = await Wallet.with_db(
mint_url, db=".wallet", load_all_keysets=True, unit=unit
mint_url, db=".wallet", unit=unit
)
if load: