Skip to content

fix(config): survive a rate-limited embedding probe when saving config#69

Merged
jaymeklein merged 1 commit into
mainfrom
fix/config-save-survives-provider-429
Jul 16, 2026
Merged

fix(config): survive a rate-limited embedding probe when saving config#69
jaymeklein merged 1 commit into
mainfrom
fix/config-save-survives-provider-429

Conversation

@jaymeklein

Copy link
Copy Markdown
Owner

What

Saving the embedding config rejected a valid change with 422 Invalid configuration
whenever the embedding provider was rate-limited (HTTP 429). The most perverse case:
editing max requests/min — the very throttle meant to relieve 429s — was impossible
while throttled.

Why

_build_adapters sizes the pgvector store from embed.dimensions, and for Gemini that
property probes the provider live (GeminiAdapter.dimensions embeds a short string to
learn the vector size). A 429 during that probe bubbled into the generic
except Exception -> HTTPException(422), so a transient rate limit was reported as an
invalid configuration.

Fix

A 429 means the provider is reachable and the key is valid — it's throttled, not
misconfigured. So:

  • On a RateLimitError during the dimension probe, when the embedding model shape is
    unchanged
    (provider, model, base_url, output_dimensionality), reuse the live
    adapter's already-known dimension — it cannot have changed — and let the save proceed.
  • Only a model change that also can't be probed re-raises, now surfaced as a
    transient 503 ("try again shortly") instead of a misleading 422.

Graceful degradation, matching the codebase's hard rule: a throttled provider falls back
to prior behaviour instead of turning a valid save into an error.

Tests

Two new unit tests in tests/unit/test_config_service.py:

  • an unchanged-model save survives a rate-limited probe (status applied, new max_rpm
    persisted);
  • a model-change save under a rate limit returns 503.

Gate green: ruff + mypy clean, 707 passed / 13 skipped.

@jaymeklein
jaymeklein force-pushed the fix/config-save-survives-provider-429 branch 2 times, most recently from 75959ae to c49a159 Compare July 16, 2026 18:41
…nfig

Saving the embedding config validates it by building the adapters, which sizes the pgvector store
from the embedding vector dimension. For Gemini that dimension is learned by probing the provider
(GeminiAdapter.dimensions embeds a short string). When the provider returns HTTP 429 the probe
raised and the save was rejected - first as a 422 in the API dry run, then as a 409 when each worker
rebuilt its own adapters on the reload and hit the same 429. So editing max requests/min, the very
knob meant to relieve 429s, was impossible while the provider was throttled.

A 429 means the provider is reachable and the key is valid; it is throttled, not misconfigured, and
must never block or roll back a config save. Add a shared resolve_dimensions() helper
(api/adapters/embeddings) used by BOTH the API apply and the worker reload: it probes the provider
first - so a bad key / unreachable host still fails fast, and a stale size self-heals once the
provider answers - but on a RateLimitError, when the embedding model shape is unchanged (provider,
model, base_url, output dimensionality), it reuses the existing store's known dimension instead of
failing. Expose PgvectorAdapter.dimensions for that reuse.

The API always has a boot-built store to reuse, so an unchanged-model save goes straight through;
only a model change it genuinely can't size while throttled still returns a transient 503. A worker
may have no store yet (a process throttled since its last restart never built one), so it can't
always reuse: there it *defers* - adopts the new embedder so the change takes effect, and nulls the
store for a lazy rebuild on next use - rather than error-acking and forcing a rollback. Any non-429
failure still propagates, so a genuinely bad config rolls back.

Tests: a focused suite for resolve_dimensions / same_embedding_shape; the API apply survives a
rate-limited probe on an unchanged model and 503s on a throttled model change; the worker reload
reuses the live dimension when it has one, defers the store when it doesn't (no store built, or a
model change), and still propagates non-rate-limit failures.
@jaymeklein
jaymeklein force-pushed the fix/config-save-survives-provider-429 branch from c49a159 to a4c0441 Compare July 16, 2026 19:00
@jaymeklein
jaymeklein merged commit 8ba3f1a into main Jul 16, 2026
8 checks passed
@jaymeklein
jaymeklein deleted the fix/config-save-survives-provider-429 branch July 16, 2026 20:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant