1. 21 Jul, 2026 5 commits
    • Stefy Lanza (nextime / spora )'s avatar
      Stop upstream 429s from disabling a codex provider · f9d15eee
      Stefy Lanza (nextime / spora ) authored
      A 429 from the ChatGPT backend is per-quota-bucket, not a provider
      fault: x-codex-active-limit names the bucket that refused, and the
      account keeps serving from another one meanwhile. In one hour of
      production traffic codex_think returned 1888 x 200 interleaved with
      1282 x 429. Every 429 was recorded as a provider failure, so three in a
      row tripped the three-strikes cooldown and aisbf spent five minutes at a
      time rejecting requests itself -- including the majority the upstream
      would have answered. One client fired 32 requests during a cooldown and
      got 32 x 503 without a single one reaching OpenAI.
      
      - codex: raise RateLimitError on a 429 instead of raise_for_status(),
        without disabling the provider, on both the streaming and
        non-streaming OAuth paths.
      - handlers: _should_record_failure() now excludes upstream rate limits
        as well as CoderAI warm-up, and a quota refusal is forwarded to the
        client as 429 rather than a generic 500.
      
      Send ChatGPT-Account-ID again. The header was only set from
      tokens.account_id, which is null in every credentials file the login
      flow writes; the real value is in the id_token's chatgpt_account_id
      claim. Without it the backend picks a workspace itself, so an account
      belonging to several workspaces cannot be steered at the configured one.
      
      Make rate_limit actually do something. The spacing timestamp lived on
      the handler, but get_provider_handler() builds a fresh handler per
      request, so it was always 0 on arrival and no wait was ever applied --
      a configured rate_limit was silently inert. Move the timestamps to a
      process-wide registry guarded by a per-slot lock, without which N
      concurrent requests all read the same stale timestamp and burst
      together. Verified: 4 concurrent requests at 0.5s spacing now take
      1.50s, previously 0.00s.
      
      Bump version to 0.99.90.
      Co-Authored-By: 's avatarClaude Opus 4.8 (1M context) <noreply@anthropic.com>
      f9d15eee
    • Stefy Lanza (nextime / spora )'s avatar
      Advertise a provider's configured context window in /v1/models · b1320e45
      Stefy Lanza (nextime / spora ) authored
      Two bugs made codex_think advertise a context window it was not
      configured with, so clients that size their context from the model
      listing refused to run against it.
      
      1. The endpoint-level model cache was keyed on type+endpoint alone.
         codex_think, openai_think and bigscreen are all
         codex:https://api.openai.com/v1 but authenticate as different
         accounts, so whichever prefetched first populated the shared entry
         and the others served its model list -- an OAuth ChatGPT provider
         inherited an API-key provider's generic OpenAI models, contexts and
         all. Same collision across the three kilo-* providers. Key the entry
         on a digest of the provider's credentials too. Also fix
         invalidate_provider_cache(), which tried to find a provider's
         endpoint entry with a substring match on a key that never contains
         the provider id.
      
      2. Provider models were published exactly as the upstream API returned
         them, so default_context_size / default_max_request_tokens never
         reached the listing. Stamp them on, letting explicit configuration
         override the fetched value.
      
      _configured_context_size() deliberately does not call
      get_context_config_for_model(): that helper ends in
      _infer_context_size_from_model(), whose generic 8192 fallback is right
      for sizing a request but would overwrite a real fetched window (272000)
      with a guess when published.
      
      Bump version to 0.99.89.
      Co-Authored-By: 's avatarClaude Opus 4.8 (1M context) <noreply@anthropic.com>
      b1320e45
    • Stefy Lanza (nextime / spora )'s avatar
    • Stefy Lanza (nextime / spora )'s avatar
      Say why a provider request is rejected with 503 · a968e661
      Stefy Lanza (nextime / spora ) authored
      Every codex_think request was returning 503 within a millisecond with
      nothing in debug.log to explain it. The provider had simply been toggled
      off from the dashboard, but the six 503 sites in the request path all
      collapsed three distinct states -- the manual dashboard toggle, the
      failure cooldown and the usage-limit cooldown -- into a bare
      "Provider temporarily unavailable" with no log line, making a disabled
      provider indistinguishable from an upstream outage.
      
      Route all six through _raise_if_provider_unavailable(), which logs a
      WARNING and puts the actual reason (and remaining cooldown, where there
      is one) in the response detail.
      Co-Authored-By: 's avatarClaude Opus 4.8 (1M context) <noreply@anthropic.com>
      a968e661
    • Stefy Lanza (nextime / spora )'s avatar
      Update · e6065d17
      Stefy Lanza (nextime / spora ) authored
      e6065d17
  2. 15 Jul, 2026 5 commits
    • Stefy Lanza (nextime / spora )'s avatar
      Make the CLI wait for the MCP tool shim; bump to 0.99.87 · da972924
      Stefy Lanza (nextime / spora ) authored
      A claude_think request with tools registered none: the CLI reported tools:[] at
      init with the shim still "pending", so the model had nothing to call and instead
      described the calls in prose ("[Tool: list_dir]") with stop_reason=end_turn. The
      turn looked successful, so this surfaced as a wrong answer rather than an error.
      
      The CLI connects --mcp-config servers "fully async (nonblocking)" and starts the
      turn without waiting, so whether any tools exist is a race between the shim's
      handshake and the first API request. Marking the server alwaysLoad puts it on
      the CLI's blocking path instead (its own two code paths: alwaysLoad servers are
      awaited, the rest are fire-and-forget).
      
      The race is host-dependent, which is why local testing never caught it — my box
      won it every time and registered the tools. Confirmed on the box that loses it,
      with identical CLI versions:
      
        without alwaysLoad -> tools=[],                        mcp=pending
        with    alwaysLoad -> tools=['mcp__aisbf__get_weather'], mcp=connected
      
      Ruled out first: the shim is deployed there, both interpreters run it in ~20ms,
      and it answers initialize/tools-list correctly under production's python.
      Co-Authored-By: 's avatarClaude Opus 4.8 (1M context) <noreply@anthropic.com>
      da972924
    • Stefy Lanza (nextime / spora )'s avatar
      Stop the admin provider UI from silently dropping cli_credentials_file; bump to 0.99.86 · 294a7273
      Stefy Lanza (nextime / spora ) authored
      The CLI credentials upload stored claude_config.cli_credentials_file server-side,
      but the path was then lost and CLI mode fell back to the OAuth2 credentials file:
      
      - uploadClaudeCliFile() only showed a toast; unlike its sibling
        uploadFileChunked(), it never mirrored the stored path into providersData.
      - saveProvider() posts providersData wholesale, and api_provider_save replaced
        the provider config outright, so a page loaded before the upload wiped the key
        on the next save.
      - There was no field for cli_credentials_file — it appeared only in help text —
        so the value was invisible and unrecoverable once lost.
      
      Preserve keys inside *_config blocks the client did not send, which fixes the
      whole class rather than this one key: any server-set key would have been dropped
      the same way. Only merges into a block the client actually sent, so a provider
      type change still drops the old block and nothing is resurrected. A key the
      client does send always wins, including falsy values like use_cli_mode=false.
      
      Add the missing text field (admin only — DB users keep CLI credentials in
      user_oauth2_credentials, not in claude_config), and mirror the upload result
      into providersData. The chunk endpoint now also returns config_path, the tilde
      form actually written to providers.json; file_path is absolute, so mirroring it
      would post back a value that disagrees with what the server stored.
      Co-Authored-By: 's avatarClaude Opus 4.8 (1M context) <noreply@anthropic.com>
      294a7273
    • Stefy Lanza (nextime / spora )'s avatar
      Accept Claude CLI-format credentials files · 77ac9262
      Stefy Lanza (nextime / spora ) authored
      A CLI .credentials.json placed at a provider's credentials_file loaded fine and
      then reported "credentials are invalid or missing": _load_credentials() did a
      bare json.load, but every reader looks for `access_token`, while the CLI schema
      nests `claudeAiOauth.accessToken`. The tokens are equivalent — only the spelling
      differs — so normalize on load instead of rejecting them.
      
      ClaudeAuth.normalize_tokens() is the inverse of
      ClaudeProviderHandler._oauth_tokens_to_cli_credentials(); keep the two in step.
      It also carries subscriptionType/rateLimitTier across, so converting back to CLI
      shape reproduces the real values rather than falling back to that function's
      'pro'/'default_claude_ai' defaults. Anything already in AISBF shape is untouched.
      
      Applied to the DB path too, which assigns auth.tokens directly and hits the
      identical mismatch if a CLI blob was stored verbatim.
      Co-Authored-By: 's avatarClaude Opus 4.8 (1M context) <noreply@anthropic.com>
      77ac9262
    • Stefy Lanza (nextime / spora )'s avatar
      Fix Claude CLI mode: transport, real tool calling via MCP shim, dashboard toggle; bump to 0.99.85 · 2c3e2d8e
      Stefy Lanza (nextime / spora ) authored
      CLI mode never worked. Four independent defects, any one fatal:
      
      - The stdin frame used {"type":"user_message",...}; the CLI expects the
        Anthropic envelope {"type":"user","message":{...}} and silently discards
        anything else, so requests produced no output at all. With
        --input-format stream-json the CLI also ignores a prompt passed as an argv
        positional, so it must go over stdin.
      - _handle_cli_streaming_request() took no tools parameter but was called with
        tools=..., raising TypeError on every streaming request.
      - The event parser dispatched on a top-level content_block_delta/message_stop,
        but the CLI wraps Anthropic events as {"type":"stream_event","event":{...}},
        so those branches were dead code.
      - Tool definitions were passed to --tools, which only selects built-in tools by
        name; a JSON blob there registers nothing.
      
      Tools now reach the model through an MCP stdio shim (claude_mcp_shim.py), which
      advertises the caller's definitions via tools/list. It never executes: in the
      OpenAI protocol the client runs tools, so the first tool_use ends the turn and
      is returned as tool_calls. Past calls/results are replayed as text since each
      request is a fresh session, with a system-prompt directive so the model trusts
      a replayed result instead of re-calling.
      
      The shim forces one deviation from the intended flag set: --disallowedTools
      'mcp__*' is a blanket deny that also blocks the shim, and deny beats
      --allowedTools, so it cannot be kept alongside tool calling. It is retained
      when no tools are requested; with tools, --strict-mcp-config preserves the same
      isolation by loading only our config and ignoring the host's MCP servers.
      
      Consequences of the event model (one assistant event per content block, not per
      message): break on message_delta stop_reason == 'tool_use' rather than the first
      tool_use, or parallel calls are dropped; dedupe text/arguments across the delta
      and assistant paths; and emit sequential tool_call indices, since content-block
      indices count text/thinking blocks and leave holes that break client-side
      accumulation.
      
      System messages now go to --system-prompt instead of being inlined as user text
      the model could ignore. Non-streaming reports real token usage instead of zeros.
      
      Also fix the dashboard toggle that made this unreachable: providers.py imported
      _claude_cli_mode from startup, binding a copy of False at import time, while
      detection only ever wrote app_state['_claude_cli_mode']. The template therefore
      always received False and the use_cli_mode checkbox never rendered. Inject the
      value through init() like every other route global, and drop the orphaned
      startup global.
      Co-Authored-By: 's avatarClaude Opus 4.8 (1M context) <noreply@anthropic.com>
      2c3e2d8e
    • Stefy Lanza (nextime / spora )'s avatar
      Skip already-disabled providers before building handlers in rotation · 2f3cbf0e
      Stefy Lanza (nextime / spora ) authored
      Add is_provider_disabled_cheap() so the rotation scan can skip providers
      that are disabled — manually via the dashboard toggle or by an auto-disable
      cooldown — without constructing the handler. Building a handler validates
      credentials, which for some provider types performs a network round-trip, so
      a disabled provider was still being contacted. The cheap pre-check reads the
      same cache/DB keys as is_rate_limited() and fails safe, leaving the
      authoritative check in place.
      
      Also stop recording a failure for non-retryable errors (400/401/403/404/422).
      Those are client/configuration problems, not provider-health problems: counting
      them tripped the consecutive-failure auto-disable and pulled healthy providers
      out of rotation for the whole cooldown.
      Co-Authored-By: 's avatarClaude Opus 4.8 (1M context) <noreply@anthropic.com>
      2f3cbf0e
  3. 24 Jun, 2026 1 commit
    • Stefy Lanza (nextime / spora )'s avatar
      Make CoderAI broker queue hold time configurable; bump to 0.99.83 · 8efe5650
      Stefy Lanza (nextime / spora ) authored
      When a coderai broker provider's worker is offline, requests are held and
      retried while it warms up. The hold window was hardcoded (10s x 3 = 30s).
      Expose it per provider via coderai_config:
      - broker_queue_timeout_seconds: total hold time (0 disables holding)
      - broker_warmup_wait_seconds: poll interval between retries
      - broker_max_warmup_waits: explicit retry count (when no queue timeout)
      
      Defaults preserve prior behavior. Applied to both warm-up retry loops.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      8efe5650
  4. 23 Jun, 2026 2 commits
    • Stefy Lanza (nextime / spora )'s avatar
      Resolve max_tokens per-model + defaults across rotations and autoselect; bump to 0.99.82 · 5dff11f4
      Stefy Lanza (nextime / spora ) authored
      Extend max output token resolution so per-model and default values are
      honored at every layer when the client omits max_tokens.
      
      Effective priority (client value always wins if present):
        autoselect per-model -> rotation per-model -> provider per-model
        -> provider default -> rotation default -> autoselect default
      
      - rotation path: consult the selected provider's config (new
        RotationHandler._get_provider_config) so a provider default_max_tokens
        applies even though rotations can't configure max_tokens themselves
      - AutoselectModelInfo.max_tokens: new per-model override field
      - AutoselectHandler._apply_autoselect_max_tokens applied in both the
        streaming and non-streaming dispatch: per-model override set directly
        (highest), autoselect default threaded via _autoselect_default_max_tokens
        as a lowest-priority fallback
      - rotation and both RequestHandler injections consume the threaded
        autoselect default, covering autoselect->rotation and
        autoselect->provider/model dispatch routes
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      5dff11f4
    • Stefy Lanza (nextime / spora )'s avatar
      Honor provider/rotation/autoselect context & token defaults; bump to 0.99.81 · d626dda9
      Stefy Lanza (nextime / spora ) authored
      Provider-level defaults were ignored when a provider had no per-model
      configs, so coderai/broker providers reported a fixed 4096 context window
      and clients fell back to their own defaults.
      
      - model list (handlers.py): fall back to provider_config.default_context_size
        for context_window/context_length before inferring
      - rotation/autoselect model list (routes/api.py): attach context_window/
        context_length resolved from rotation/autoselect/provider defaults (was
        absent entirely, causing clients to default to 4096)
      - handle_rotation_model_list: honor provider default_context_size before
        heuristic auto-derivation
      - max output tokens: add ProviderModelConfig.max_tokens and
        default_max_tokens on Provider/Rotation/Autoselect configs, plus
        get_max_completion_tokens_for_model resolver, applied as a fallback in
        all request paths when the client omits max_tokens
      - get_max_request_tokens_for_model: fall back to
        provider_config.default_max_request_tokens; drop duplicated dead block
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      d626dda9
  5. 22 Jun, 2026 14 commits
    • Stefy Lanza (nextime / spora )'s avatar
    • Stefy Lanza (nextime / spora )'s avatar
      coderai broker: actually stream responses (don't wait for done before yielding) · 3c7fc7c2
      Stefy Lanza (nextime / spora ) authored
      Streaming was scaffolded (stream_queue, _publish_stream_response,
      wait_for_stream_event, _iter_broker_stream_chunks) but send_request always awaited
      the terminal future and then popped the pending entry in finally — so by the time
      _broker_stream began consuming, the stream was over and its queue was gone. Result:
      the whole reply arrived at once.
      
      Fixes:
      - send_request: for streaming requests, return a {event:"stream_start"} immediately
        and do NOT pop _pending in finally — the consumer drives it. Pop on error.
      - _publish_stream_response: queue EVERY event (chunks AND the terminal done) so the
        consumer's loop receives the end marker; still resolves the future for safety.
      - finish_stream(): new — removes the streaming request's _pending once drained.
      - wait_for_stream_event: simplified to a queue.get (dropped the event_log path).
      - providers/coderai._iter_broker_stream_chunks: wrap in try/finally that calls
        finish_stream(request_id) on completion/error; treat stream_start as a no-data
        continue.
      
      Pairs with the coderai-side change that emits chunk/done envelopes.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      3c7fc7c2
    • Stefy Lanza (nextime / spora )'s avatar
      eecf51f9
    • Stefy Lanza (nextime / spora )'s avatar
      Fix retry processor schema/code bugs and align retry tests · 38d63406
      Stefy Lanza (nextime / spora ) authored
      Real bugs surfaced by the retry tests (code referenced schema that did not
      exist / wrong table):
      - subscriptions was missing cancelled_at (written when a renewal exhausts its
        retries and cancels the subscription).
      - payment_retry_queue was missing last_attempt_at (written when scheduling the
        next retry).
      - retry.py queried a non-existent `tiers` table for the free tier; corrected to
        account_tiers (and is_default = 1 for cross-DB boolean compatibility).
      
      Migration changes add the two columns to the CREATE statements and provide
      idempotent, cross-DB (SQLite PRAGMA / MySQL INFORMATION_SCHEMA) ALTERs for
      existing databases.
      
      test_retry: crypto retries are intentionally skipped without incrementing while
      the wallet is unfunded, so the increment/downgrade tests now drive a failing
      gateway charge instead, and expect the same free tier the code selects.
      
      Verified all payment migration DDL (CREATE + new ALTERs + INFORMATION_SCHEMA
      existence checks) executes cleanly on MariaDB/MySQL as well as SQLite.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      38d63406
    • Stefy Lanza (nextime / spora )'s avatar
      test_integration: align with current payment schema and code · eb63b216
      Stefy Lanza (nextime / spora ) authored
      Rewrite the integration tests against the current schema/behaviour:
      - crypto flow: process_transaction records the tx itself (drop the manual
        crypto_transactions insert with non-existent from_address/to_address columns;
        ensure a crypto wallet row exists; pass a float amount for SQLite binding).
      - subscriptions: use account_tiers + subscriptions (not subscription_tiers/
        user_subscriptions), create the required card payment method, and make the
        renewal subscription actually due (current_period_end in the past); assert via
        process_renewals' processed count.
      - consolidation: enable crypto_consolidation_settings so candidates are queued.
      - notifications: enable email_notification_settings and add an email_templates
        row so the notification is actually sent.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      eb63b216
    • Stefy Lanza (nextime / spora )'s avatar
      Fix auto_topup and wallet_renewal payment tests for current code · eee8b347
      Stefy Lanza (nextime / spora ) authored
      - test_auto_topup: use MagicMock (not Mock) so the DB context manager works in
        auto_charge; mock _get_or_create_customer and return a Stripe-object-shaped
        PaymentIntent (attribute access, not dict); replace the retry-logic test's
        bare AsyncMock with a small stateful fake session so record_auto_topup_attempt
        actually transitions auto_topup_enabled to False after 3 failures.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      eee8b347
    • Stefy Lanza (nextime / spora )'s avatar
      test_topup: use real migrated DB and current handler API · b8a1d9a6
      Stefy Lanza (nextime / spora ) authored
      PaymentService construction now initializes crypto master keys, so the tests
      need a real migrated SQLite DB rather than a Mock. Also align with the current
      code: initiate_topup delegates to create_topup_intent/create_topup_order; the
      Stripe webhook path requires amount in metadata, passes a metadata dict, and is
      exercised by patching stripe.Webhook.construct_event; crypto deposits credit the
      fiat wallet directly via SQL (assert the resulting balance) using the monitor's
      own price service (returning a float for SQLite binding).
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      b8a1d9a6
    • Stefy Lanza (nextime / spora )'s avatar
      Fix payment schema migrations to match the code (SQLite + MySQL) · 9ab81894
      Stefy Lanza (nextime / spora ) authored
      Several payment code paths referenced database schema that no migration ever
      created, so they failed at runtime:
      
      - distributed_locks: created the table used by PaymentScheduler._acquire_lock /
        _release_lock (it was referenced but never defined anywhere).
      - email_notification_queue: the queue code uses context_json + retry_count, but
        the table was defined with recipient_email/subject/body. Redefined the table
        to match the code.
      - crypto_consolidation_queue: WalletConsolidator writes user_id/from_address/
        to_address/amount, but the table had total_balance/address_count. Redefined to
        match the code.
      - payment_methods.gateway: the base DatabaseManager schema creates
        payment_methods without a gateway column, so the migration's CREATE TABLE IF
        NOT EXISTS was a no-op and the column (read by the renewal processor) was
        missing. Added an idempotent ALTER.
      
      Added cross-DB helpers (_table_exists/_column_exists work on both SQLite via
      PRAGMA and MySQL via INFORMATION_SCHEMA). The two transient queue tables hold
      only regenerable rows, so a legacy-shape table is dropped and recreated rather
      than ALTERed column-by-column (avoids fragile cross-dialect constraint changes);
      the gateway add is a plain additive ALTER guarded by _column_exists.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      9ab81894
    • Stefy Lanza (nextime / spora )'s avatar
      Payments tests: fix test-drift in wallet, renewal, integration, topup · 794dbe03
      Stefy Lanza (nextime / spora ) authored
      Bring several payments tests back in line with the current implementation:
      
      - test_wallet.py: mock self.db.begin() as an async context manager (was an
        AsyncMock coroutine) for credit/debit; update get_or_create_user_address to
        the new contract (legacy helper now allocates a fresh address per call).
      - test_wallet_renewal.py: SubscriptionRenewalProcessor now takes its gateway/
        price collaborators explicitly (pass mocks); patch trigger_auto_topup at its
        real source aisbf.payments.scheduler; renewal DB cursor access is synchronous
        (MagicMock, not AsyncMock).
      - test_integration.py: use a valid Fernet encryption key; DatabaseManager takes
        a config dict (not db_type/db_path kwargs); decorate the async payment_service
        fixture with @pytest_asyncio.fixture (strict asyncio mode).
      - test_topup.py: use a valid Fernet encryption key.
      
      Payments suite: 31 not-passing (17 failed + 14 errors) -> 22 failed, 52 passed;
      remaining failures now surface real causes instead of setup errors.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      794dbe03
    • Stefy Lanza (nextime / spora )'s avatar
      Fix studio and market-reference route tests for current implementation · 01a2551d
      Stefy Lanza (nextime / spora ) authored
      These tests lagged behind two refactors:
      
      - build_studio_catalog is now an async coroutine. Update the studio tests to
        await it: direct-call tests wrap it in asyncio.run(), and the HTTP tests'
        monkeypatched stubs are now async (the route handler awaits the result, so a
        sync stub returning a dict raised "object dict can't be used in 'await'
        expression").
      - The providers page bootstraps provider data from a dedicated JSON <script>
        element (providers_bootstrap_json, escaped by the route's _json_parse_bootstrap
        helper) parsed via JSON.parse, instead of an inline `let providersData = {...}`
        literal. Update the admin bootstrap test to feed providers_bootstrap_json and
        assert on the <script id="providers-bootstrap"> element (escaping + double
        JSON decode) and the JSON.parse usage.
      - Add the missing record_dashboard_event method to MarketReferenceImportDbStub,
        which the market import route now calls.
      
      tests/routes now passes 86/86 (was 18 failing).
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      01a2551d
    • Stefy Lanza (nextime / spora )'s avatar
      Fix dashboard route tests' auth redirect loop via shared conftest · e69ce2d5
      Stefy Lanza (nextime / spora ) authored
      User-scoped dashboard route tests set a session cookie for a synthetic user but
      only stubbed the database in the route-handler modules — not the one the auth
      middleware consults. The middleware's "account deleted" guard
      (get_user_by_id(user_id) is falsy -> redirect to /dashboard/login) therefore
      bounced every request until httpx raised TooManyRedirects, failing ~32 tests.
      
      Add tests/routes/conftest.py with an autouse fixture that wraps
      DatabaseRegistry.get_config_database so get_user_by_id returns a present user
      ONLY when the lookup comes from the auth middleware (detected via call stack).
      Direct database assertions in other tests (e.g. signup cleanup, which expects
      deleted users to read back as None) keep observing the real database, and tests
      that install their own get_config_database stub still win.
      
      Route suite: TooManyRedirects failures 32 -> 0. Remaining route failures are
      unrelated pre-existing issues (studio tests await-ing coroutines, a handler
      awaiting a dict, a stub missing record_dashboard_event, template bootstrap
      content drift) and are out of scope for this fix.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      e69ce2d5
    • Stefy Lanza (nextime / spora )'s avatar
      Fix stale CoderAI broker tests to match current implementation · f3718598
      Stefy Lanza (nextime / spora ) authored
      The broker was refactored (file-based session persistence -> shared-cache
      persistence; WebSocket handshake -> client-speaks-first), leaving 5 tests
      failing against APIs that no longer exist. Update them to the current design:
      
      - WebSocket tests now send op=register first, then expect event=registered
        (the server no longer greets before the client registers).
      - Remote-node queue test simulates a session owned by another cluster node via
        a direct shared-cache entry (foreign broker_node_id, no local WebSocket), so
        send_request() correctly takes the queue slow path instead of a websocket
        send. The old approach (touch broker_node_id) no longer works because the
        cache always records the owning node.
      - Replace the obsolete file-based persistence tests (_persist_sessions_locked /
        _load_persisted_sessions / _state_path, all removed) with cache-based
        persistence tests: cross-node session visibility and the offline tombstone.
      
      Full file now passes 11/11 (was 6 passed / 5 failed).
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      f3718598
    • Stefy Lanza (nextime / spora )'s avatar
      CoderAI: warm-up wait-and-retry on all paths + multi-hour request timeout · 59156998
      Stefy Lanza (nextime / spora ) authored
      Move the CoderAI broker warm-up handling into the provider so it applies on
      every request path (rotation, autoselect and direct), and only for the
      broker-session "cooling down" error:
      
      - coderai.py: handle_request now wraps the broker decision in a warm-up retry
        loop — on "No active CoderAI broker session" it waits CODERAI_WARMUP_WAIT_
        SECONDS (10s) and retries the same provider up to CODERAI_MAX_WARMUP_WAITS
        (3) times, then surfaces the error WITHOUT recording a failure. Streaming
        and native-proxy paths get the same treatment via _broker_request_with_warmup
        (lazy generators retry on first broker contact). Other errors still record a
        failure as before.
      - handlers.py: every place that could disable a provider on a caught error now
        skips record_failure() for the CoderAI warm-up condition (direct chat, direct
        streaming, rotation streaming, audio/TTS/image/embeddings, and the rotation
        retry loop), via the shared _is_coderai_warmup_error() helper. The rotation
        loop no longer sleeps itself (the provider already waited) — it just fails
        over without recording a failure.
      - Default CoderAI request timeout raised from 5 min to 3 hours
        (CODERAI_DEFAULT_REQUEST_TIMEOUT, overridable via coderai_config.request_
        timeout); the timeout is also applied to the direct OpenAI client.
      
      Bump version to 0.99.77.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      59156998
    • Stefy Lanza (nextime / spora )'s avatar
      Treat CoderAI broker warm-up as wait-and-retry, not a provider failure · 578eeb9d
      Stefy Lanza (nextime / spora ) authored
      CoderAI workers often run on small/edge hardware that drops its broker
      session while cooling down to avoid overheating. A missing broker session
      ("No active CoderAI broker session...") was treated as a hard error:
      record_failure() was called in both the provider handler and the rotation
      loop, so three of them in one request disabled the provider for a 300s
      cooldown — exactly when it just needed a moment to warm back up.
      
      Now this transient condition is handled gracefully:
      - coderai.py no longer records a failure when the error is a missing broker
        session (in any request path), so it never counts toward the disable
        threshold.
      - The rotation handler detects the CoderAI warm-up condition and waits
        CODERAI_WARMUP_WAIT_SECONDS (10s) before retrying the same provider, up to
        CODERAI_MAX_WARMUP_WAITS (3) times, fully transparent to the client. Only
        after that does it fail over to the next provider — still without recording
        a failure.
      
      Bump version to 0.99.76.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      578eeb9d
  6. 21 Jun, 2026 11 commits
    • Stefy Lanza (nextime / spora )'s avatar
      Group provider/rotation/autoselect config forms into collapsible sub-panels · 03f3e110
      Stefy Lanza (nextime / spora ) authored
      The provider, rotation and autoselect detail forms were single huge forms.
      Split them into collapsible sub-panels so only basic settings stay visible:
      
      - Shared subPanel()/toggleSubPanel() helper + CSS in base.html, defined
        before the content block so page scripts can call it during first render.
      - Providers: basic fields visible; Authentication, Pricing & Tiers, Rate
        limits & defaults, Feature Overrides, Native Caching, Models as panels;
        each model has basic config visible + an Advanced sub-panel.
      - Rotations: basic visible; Advanced configuration panel; one panel per
        provider (model-count + usage badge); one panel per model with an
        Advanced sub-panel.
      - Autoselect: basic visible; Classification, Feature Overrides, Default
        settings panels; one panel per available model.
      - Panel open/close state and top-level item expansion persist via
        sessionStorage, so they survive navigation within a session.
      
      Bump version to 0.99.75.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      03f3e110
    • Stefy Lanza (nextime / spora )'s avatar
      Treat HTTP 402 Payment Required as non-retryable in rotation failover · bbe17ff0
      Stefy Lanza (nextime / spora ) authored
      A 402 (e.g. Kilo "usage_limit_exceeded"/out of credits) is deterministic:
      retrying the same provider only burns the failure budget and needlessly
      disables it within a single request. Add 402 to the non-retryable set so
      the rotation fails over to the next provider immediately.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      bbe17ff0
    • Stefy Lanza (nextime / spora )'s avatar
      Routes: support model IDs with slashes; add llama.cpp/Ollama compat probes · ab8f5b69
      Stefy Lanza (nextime / spora ) authored
      - /api/v1/models/{model_id} and /api/u/{username}/models/{model_id} used a
        single-segment param, so model IDs containing '/' (e.g. rotations/lisa)
        returned 404. Use {model_id:path}.
      - Add capability-probe endpoints so OpenAI/Ollama/llama.cpp-style clients stop
        getting 404/405 on connect: GET /api/version, /v1/version, /version;
        GET /api/tags (Ollama model list); GET /props, /api/props, /v1/props.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      ab8f5b69
    • Stefy Lanza (nextime / spora )'s avatar
      Streaming support: anthropic + ollama, and true incremental streaming for google · 42e06a0c
      Stefy Lanza (nextime / spora ) authored
      - anthropic: previously ignored the stream flag entirely (always non-stream).
        Add _handle_streaming_request converting Anthropic SSE events into
        OpenAI-compatible chunks (text deltas, tool_use/input_json deltas, usage).
      - ollama: previously hard-coded stream=False. Add incremental streaming over
        /api/generate yielding OpenAI chunks.
      - google: was buffering the entire response before yielding. Stream chunks
        incrementally as they arrive when no tools are requested (the common case);
        keep the buffer-and-parse path only when tools are present, since the
        text-encoded tool-call detection needs the complete response.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      42e06a0c
    • Stefy Lanza (nextime / spora )'s avatar
      Fix rotation providers never degrading: don't record success on lazy streams · afef7809
      Stefy Lanza (nextime / spora ) authored
      For streaming requests providers returned a lazy generator and immediately
      called record_success(), resetting the failure counter to 0 BEFORE the
      rotation handler primed the stream and the upstream returned 400/429. Every
      attempt did success(reset->0) then failure(->1), so the disable threshold (3)
      was never reached and a consistently-failing provider was never disabled.
      
      Guard the premature record_success() so it does not fire for streaming
      (codex, openai, runpod, coderai). The caller already records success after
      the stream is primed/consumed, so the failure counter now accumulates and a
      failing provider is correctly disabled/cooled-down. Bump to 0.99.74.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      afef7809
    • Stefy Lanza (nextime / spora )'s avatar
      Manual disable/enable provider button (persistent across reboot) + cooldown recovery · 1d726a93
      Stefy Lanza (nextime / spora ) authored
      Adds a per-provider Disable/Enable button to the rotations page for both admin
      and users, exposing a manual disable that — unlike the failure cooldown — never
      auto-expires and persists across reboots.
      
      - providers/base.py: manual_disable()/manual_enable()/is_manually_disabled();
        manual disable takes precedence in is_rate_limited(); manual_enable() also
        clears any cooldown and resets the failure budget. Source of truth is the
        database (mirrored to cache as a fast path) so state survives restarts and
        cache flushes. Also: when a failure cooldown elapses, the provider is
        reactivated with a fresh failure budget instead of a hair-trigger.
      - database.py: dedicated provider_manual_disabled table (+migration) and
        get/set/clear helpers, user-scoped so a user's toggle never affects others.
      - dashboard: manual-disable / manual-enable / bulk manual-status endpoints; the
        rotations pages render and toggle the button state.
      
      Bump to 0.99.73.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      1d726a93
    • Stefy Lanza (nextime / spora )'s avatar
      Optional rotation/autoselect override for the internal local models · df8d927d
      Stefy Lanza (nextime / spora ) authored
      Each internal-model functionality can now point at a rotation or autoselect
      instead of the local model, falling back to the local model when the override is
      empty or fails. Per-functionality fields in internal_model (default empty):
      condensation_override, autoselect_override, nsfw_classifier_override,
      privacy_classifier_override.
      
      - handlers.py: run_meta_target() runs a global rotation/autoselect and returns
        the assistant text (or None -> fallback); classification overrides prompt the
        chat model for a strict YES/NO and parse it, falling back to the local
        classifier; autoselect selection can run through an override.
      - context.py: condensation prefers the override, falling back to the local model.
      - settings page: per-field override inputs backed by a datalist of available
        rotations/autoselects; saved to internal_model.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      df8d927d
    • Stefy Lanza (nextime / spora )'s avatar
      Rotation entries can forward to a rotation/autoselect, with loop protection · c666aab2
      Stefy Lanza (nextime / spora ) authored
      A rotation provider entry whose provider_id is "rotations" or "autoselect" is a
      meta-provider: each model "name" is the id of a target rotation/autoselect, and
      the request is forwarded there (weighted like any other entry, with failover to
      the next entry on total failure).
      
      - handlers.py: detect meta-providers in model-building, delegate in the retry
        loop, and resolve rotation vs autoselect targets.
      - Loop protection: a delegation chain rides on request_data and is registered by
        both RotationHandler and AutoselectHandler; re-entering an id already in the
        chain (A->B->A) or exceeding depth 8 raises HTTP 508 (caught upstream -> fail
        over). Loops are also pre-filtered at model-build time.
      - Dashboard: the provider select in rotations.html / user_rotations.html now
        offers "rotations"/"autoselect"; the model row becomes a target picker.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      c666aab2
    • Stefy Lanza (nextime / spora )'s avatar
      Notify client on rotation failover (banner/metadata/headers); bump to 0.99.71 · 0a3a1df7
      Stefy Lanza (nextime / spora ) authored
      When a rotation fails over to a different provider/model than the preferred
      (highest-weight) pick, optionally notify the client so the provider/model
      change isn't silent. Opt-in via a new notify_on_failover flag, settable
      globally (rotations.json top-level) or per-rotation, mirroring notifyerrors.
      
      On a real switch (served provider/model differs from preferred), three
      mechanisms carry the notice:
      - Visible banner: prepended to message content (non-streaming) or a leading
        delta chunk (streaming).
      - JSON metadata: an aisbf_failover object on the body / leading stream chunk.
      - HTTP headers: X-AISBF-Failover/Provider/Model/Preferred-* on the
        StreamingResponse and (for non-streaming) at the rotation route.
      
      Applied to both the normal and chunked rotation paths. The banner is injected
      after caching so cache hits don't replay a stale notice; no switch means zero
      overhead and nothing emitted; default off preserves existing behavior.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      0a3a1df7
    • Stefy Lanza (nextime / spora )'s avatar
      Transparent rotation failover for streaming provider errors; bump to 0.99.70 · d9227cb1
      Stefy Lanza (nextime / spora ) authored
      Streaming rotation requests returned the provider's lazy async generator to
      the client before the upstream HTTP call was made, so an immediate provider
      error (e.g. HTTP 400 "model not supported") surfaced mid-stream after the
      200 OK headers were already sent — leaking the error to the client instead
      of failing over.
      
      - Prime streaming responses (_prime_stream) inside the rotation retry loop so
        immediate provider errors are raised within the try/except and trigger
        failover to the next provider by weight, transparently to the client.
      - Add _is_non_retryable_error to skip pointless same-provider retries for
        client errors (400/401/403/404/405/422) and move straight to the next
        provider; 5xx/timeouts/network remain retryable.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      d9227cb1
    • Stefy Lanza (nextime / spora )'s avatar
      Fix missing market/usage helpers on Rotation & Autoselect handlers; bump to 0.99.69 · bd7bfb97
      Stefy Lanza (nextime / spora ) authored
      Add _record_dashboard_proxy_event, _settle_market_result,
      _get_market_source_details, _market_request_id and
      _extract_usage_from_sse_chunk to RotationHandler, and the market
      settlement cluster to AutoselectHandler. These were defined only on
      RequestHandler, so rotation/autoselect requests raised AttributeError
      (crash on the dashboard event path, silently swallowed elsewhere),
      dropping market settlement and streaming usage capture.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      bd7bfb97
  7. 19 Jun, 2026 1 commit
  8. 19 May, 2026 1 commit