Skip to content

feat(research): anthropic fallback when gemini quota is exhausted#22

Closed
drPod wants to merge 3 commits into
synthetic-sciences:masterfrom
drPod:feat/anthropic-research-fallback
Closed

feat(research): anthropic fallback when gemini quota is exhausted#22
drPod wants to merge 3 commits into
synthetic-sciences:masterfrom
drPod:feat/anthropic-research-fallback

Conversation

@drPod
Copy link
Copy Markdown
Contributor

@drPod drPod commented Jun 1, 2026

Summary

  • Adds a pluggable Anthropic provider (AnthropicResearchProvider) and a transparent _FallbackProvider wrapper around ResearchService.provider.
  • When the primary provider raises a quota / 429 / rate-limit error, the wrapper retries the same call against the configured fallback (mapping per-mode model IDs in the process).
  • Default fallback_provider=none, so existing Gemini-only deployments are untouched. Setting SYNSC_RESEARCH_FALLBACK_PROVIDER=anthropic + ANTHROPIC_API_KEY is all that's needed to opt in.
  • Anthropic provider uses streaming under the hood (messages.stream() + get_final_message()) so long contexts don't trip non-streaming HTTP timeouts.
  • env.example documents the new knobs alongside the existing research settings.

Motivation

The Gemini free tier daily quota on gemini-2.5-pro is easy to hit when iterating on research deep mode, which leaves the tool unusable until the quota resets. Anthropic's free tier and pricing make Claude a natural secondary, and the existing config types already pretended anthropic was a valid provider — but no implementation shipped.

Test plan

  • Forced 429 from gemini-2.5-pro inside the api container → wrapper falls back to claude-opus-4-8, returns grounded answer with usage metadata + warning log (research.primary_quota_exhausted_falling_back).
  • fallback_provider=none path returns the bare primary provider (no wrapping).
  • Non-quota errors from the primary propagate unchanged.
  • CI build with regenerated uv.lock.

🤖 Generated with Claude Code

Add `ResearchService._FallbackProvider` that wraps the configured primary
provider and routes to a secondary one on 429 / quota / rate-limit errors,
detected via SDK status codes and a small string-match list. Default fallback
provider is `none` so existing Gemini-only setups are unchanged.

- new `AnthropicResearchProvider` mirroring `GeminiResearchProvider` shape,
  uses `messages.stream()` + `get_final_message()` to avoid HTTP timeouts
  on long contexts
- config gains `fallback_provider`, `fallback_api_key`, and per-mode
  `fallback_model_quick` / `fallback_model_deep` (defaults
  `claude-opus-4-8`), with env loaders for `SYNSC_RESEARCH_FALLBACK_*`
  and `ANTHROPIC_API_KEY`
- `pyproject.toml`: `anthropic>=0.40.0` (lock regenerated)
- `env.example`: document the optional research-synthesis vars and the
  fallback knobs

Verified end-to-end inside the api container: forced 429 from
`gemini-2.5-pro` triggers the wrapper, which transparently calls
`claude-opus-4-8` and returns a grounded answer with usage metadata.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 1, 2026 14:35
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds an optional fallback research synthesis provider (Anthropic) that activates on quota/rate-limit errors from the primary provider.

Changes:

  • Extend research configuration/env vars to support a fallback provider + models.
  • Add Anthropic-based research provider implementation.
  • Add quota/rate-limit detection + fallback wrapper in the research service.

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
env.example Documents new research fallback-related environment variables.
backend/synsc/services/research_service.py Builds primary/fallback providers and routes on quota/rate-limit errors.
backend/synsc/services/research_providers/anthropic.py New Anthropic Claude provider for research synthesis.
backend/synsc/config.py Adds fallback provider settings and loads them from environment.
backend/pyproject.toml Adds the anthropic dependency.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread env.example
# Research synthesis (optional)
# =============================================================================
# Primary provider for `research` mode. Default: gemini.
# SYNSC_RESEARCH_PROVIDER=gemini
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The env var is correct. backend/synsc/config.py:485 reads SYNSC_RESEARCH_PROVIDER — same SYNSC_ prefix as the rest of the config. The docs match the loader.

Comment thread backend/synsc/config.py Outdated
Comment on lines +493 to +494
if fallback_provider := os.getenv("SYNSC_RESEARCH_FALLBACK_PROVIDER"):
config.research.fallback_provider = fallback_provider # type: ignore
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7a8f4dd. SynscConfig.from_env now validates SYNSC_RESEARCH_FALLBACK_PROVIDER against {gemini, anthropic, none} and lowercase-normalizes it before assignment, raising ValueError at config-load time for unknown values instead of letting them slip past # type: ignore into the model.

Comment thread backend/synsc/config.py Outdated
Comment on lines +495 to +498
if fallback_key := os.getenv("ANTHROPIC_API_KEY") or os.getenv(
"SYNSC_RESEARCH_FALLBACK_API_KEY"
):
config.research.fallback_api_key = fallback_key
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2bb9794. ANTHROPIC_API_KEY is now only consumed by the research fallback when SYNSC_RESEARCH_FALLBACK_PROVIDER=anthropic is also set. SYNSC_RESEARCH_FALLBACK_API_KEY remains the unconditional override. Test coverage: test_research_config_fallback_key_ignored_when_provider_disabled.

Comment on lines +94 to +107
if self._provider is not None:
return self._provider
cfg = self.config.research
if cfg.provider == "gemini":
primary = self._build_provider(cfg.provider, cfg.api_key)

if cfg.fallback_provider == "none" or not cfg.fallback_api_key:
return primary

fallback = self._build_provider(cfg.fallback_provider, cfg.fallback_api_key)
model_map = {
cfg.model_quick: cfg.fallback_model_quick,
cfg.model_deep: cfg.fallback_model_deep,
}
return _FallbackProvider(primary, fallback, model_map)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7a8f4dd. ResearchService now memoizes the built provider in self._cached_provider — first .provider access builds primary (and fallback wrapper if configured), every subsequent access returns the same instance. Injected providers (constructor arg) still bypass the cache. Coverage: test_research_service_caches_built_provider, test_research_service_caches_fallback_wrapper, test_research_service_injected_provider_bypasses_cache.

Comment on lines +103 to +106
model_map = {
cfg.model_quick: cfg.fallback_model_quick,
cfg.model_deep: cfg.fallback_model_deep,
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a bug in this design. If model_quick == model_deep, the user has explicitly chosen the same model for both modes — the collapsed map still routes correctly because both modes look up the same primary key, which maps to a single fallback. Routing is not 'incorrect' here; it's the only correct behavior when both modes are aliased to one model. Adding logic to disambiguate would require fabricating distinct fallbacks the user did not configure.

Comment on lines +54 to +55
"You are a research assistant answering questions grounded in the provided context.",
"Cite sources inline as [chunk:<chunk_id>]. Answer in Markdown.",
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of scope for this PR — AnthropicResearchProvider._render_prompt intentionally mirrors GeminiResearchProvider._render_prompt (backend/synsc/services/research_providers/gemini.py), which has the same numeric-index rendering. If the citation-format mismatch is a real concern, it should be fixed across both providers in a follow-up PR rather than diverging the two implementations here.

Comment on lines +61 to +64
for i, b in enumerate(blocks, 1):
parts.append(
f"[{i}] source={b.get('source_id')} chunk={b.get('chunk_id')}\n{b.get('text','')}"
)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the comment above on line 55: _render_prompt mirrors GeminiResearchProvider._render_prompt deliberately. Citation-format unification belongs in a follow-up across both providers.

drPod and others added 2 commits June 1, 2026 09:58
…rovider

Address review feedback on the Anthropic fallback PR:

- 27 new tests cover `_is_quota_error` (9-row parametrize over
  `status_code` / `code` / message-substring shapes), `_FallbackProvider`
  (quota → fallback with mapped model; non-quota propagates; unmapped
  model passes through), `ResearchService.provider` (bare-primary when
  fallback disabled or key missing; wrapper-with-model-map when both
  configured), `AnthropicResearchProvider` (empty-key reject, streaming
  call shape, prompt rendering, missing-usage defaults to 0/0), and the
  new role-annotated build_provider failure path.

- `ANTHROPIC_API_KEY` env var only feeds `research.fallback_api_key` when
  `SYNSC_RESEARCH_FALLBACK_PROVIDER=anthropic`. Stops a stray key (set
  for an unrelated tool in the same shell) from silently re-arming the
  research fallback after a user disables it.

- `_build_provider_with_role` wraps `_build_provider` so a bad provider
  name raises e.g. `ValueError: fallback provider misconfigured: Unknown
  research provider: openai` — caller can tell primary from fallback at
  a glance.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Address Copilot review feedback on PR synthetic-sciences#22:

- `SYNSC_RESEARCH_FALLBACK_PROVIDER` is now validated and lowercase-
  normalized in `SynscConfig.from_env`. Invalid values (e.g. `openai`)
  raise `ValueError` at config-load time with a message naming the env
  var and the allowed set, instead of slipping past `# type: ignore`
  into the model and failing at first call. Mixed-case (`Anthropic`) is
  coerced to the canonical lowercase form.

- `ResearchService.provider` now memoizes the built provider in
  `self._cached_provider`. Without this, every `.provider` access
  rebuilt the SDK clients (Gemini + the fallback wrapper), which
  needlessly recreated connection pools across `.run()` invocations.
  Injected providers (constructor arg) still bypass the cache so test
  injection stays ergonomic.

- 5 new tests: invalid-fallback-rejection, case-normalization, primary
  cache, fallback-wrapper cache, injected-bypass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@aayambansal
Copy link
Copy Markdown
Contributor

Closing to clean up the contributors widget on the repo home page. Thanks for the work — happy to revisit if you want to re-open against the current master.

@aayambansal aayambansal closed this Jun 4, 2026
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.

3 participants