Skip to content

Add orcarouter_chat example#323

Open
zhenjunchen-png wants to merge 2 commits into
reflex-dev:mainfrom
zhenjunchen-png:feat/orcarouter-chat-example
Open

Add orcarouter_chat example#323
zhenjunchen-png wants to merge 2 commits into
reflex-dev:mainfrom
zhenjunchen-png:feat/orcarouter-chat-example

Conversation

@zhenjunchen-png
Copy link
Copy Markdown

Adds an orcarouter_chat example: a minimal Reflex chat app that calls
OrcaRouter, an OpenAI-compatible LLM gateway, and lets you switch the model
from a dropdown populated at startup from OrcaRouter's public pricing
catalog, with a curated fallback list when that endpoint is unreachable.

Disclosure: I'm an engineer on the OrcaRouter team.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 30, 2026

Greptile Summary

This PR adds a new orcarouter_chat example demonstrating how to integrate OrcaRouter (an OpenAI-compatible LLM gateway) into a Reflex app, with a live-loaded model dropdown and streaming chat.

  • The submit event correctly uses @rx.event(background=True) with explicit async with self: locking, but load_models — which makes an HTTP request with a 10-second timeout — uses a plain @rx.event and therefore holds the Reflex state lock for the duration of the network call; it needs the same background-task treatment.
  • Minor UX gaps: the prompt input has no on_submit handler (Enter key does nothing), and custom-pinned models are silently dropped from the selection when Refresh fetches a new catalog that doesn't include them.

Confidence Score: 3/5

The app will work for basic use, but the catalog-fetch can freeze a user's UI for up to 10 seconds on every page load and every Refresh click.

The load_models handler awaits an external HTTP call while holding the Reflex state lock, meaning the UI is unresponsive for up to 10 seconds whenever a user loads the page or clicks Refresh. This affects every session and gets worse under slow networks. The rest of the code — streaming, error handling, retry logic — is well structured, and the two other findings are minor UX improvements.

orcarouter_chat/orcarouter_chat/orcarouter_chat.py — specifically the load_models event handler.

Important Files Changed

Filename Overview
orcarouter_chat/orcarouter_chat/orcarouter_chat.py Main app file: load_models holds the state lock during a 10-second HTTP call (should be a background task); Enter key submission missing; custom model silently reset on Refresh.
orcarouter_chat/requirements.txt Adds reflex, openai, httpx, and python-dotenv with appropriate minimum version pins. No issues.
orcarouter_chat/rxconfig.py Minimal Reflex config with env_file set to .env. No issues.
orcarouter_chat/.env.example Provides a safe placeholder API key. No issues.
orcarouter_chat/README.md Clear setup instructions and feature overview. No issues.

Reviews (1): Last reviewed commit: "Add orcarouter_chat example" | Re-trigger Greptile

Comment on lines +132 to +162
@rx.event
async def load_models(self):
"""Pull the live model catalog from OrcaRouter; fall back to the curated
flagship list if the network is unavailable so the demo always boots."""
try:
async with httpx.AsyncClient(timeout=10.0) as client:
resp = await client.get(ORCAROUTER_PRICING_URL)
resp.raise_for_status()
payload = resp.json()
entries = payload.get("data") or []
live = [
e["model_name"]
for e in entries
if "model_name" in e and is_chat_model(e["model_name"], e)
]
if live:
merged = ["orcarouter/auto", *sorted(m for m in live if m != "orcarouter/auto")]
self.models = merged
self.models_source = (
f"live ({len(merged)} models from https://www.orcarouter.ai/models)"
)
if self.model not in merged:
self.model = merged[0]
return
except Exception as exc: # noqa: BLE001 - demo path, surface the reason in UI
self.error = f"Could not load live model catalog: {exc}. Using fallback list."
self.models = list(FALLBACK_MODELS)
self.models_source = (
f"fallback ({len(FALLBACK_MODELS)} flagship models; "
"see https://www.orcarouter.ai/models for the full catalog)"
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 load_models holds the state lock during a 10-second network call

load_models is decorated with plain @rx.event, so Reflex holds the state lock for the entire handler — including the await client.get(ORCAROUTER_PRICING_URL) with a 10-second timeout. While that HTTP call is in flight, no other event (e.g. typing, sending a message) can mutate state for that user. The submit handler correctly uses @rx.event(background=True) and async with self: for exactly this reason; load_models needs the same treatment.

Rule Used: API calls should be made as background tasks to av... (source)

Learned From
reflex-dev/flexgen#2091

Comment on lines +375 to +380
rx.input(
placeholder="Ask anything...",
value=State.prompt,
on_change=State.set_prompt,
width="100%",
),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Enter key does not submit the message

The prompt rx.input has no on_submit (or on_key_down) handler, so pressing Enter in the input box does nothing — the user must click "Send" each time. Adding on_submit wires Enter to the same handler as the button.

Suggested change
rx.input(
placeholder="Ask anything...",
value=State.prompt,
on_change=State.set_prompt,
width="100%",
),
rx.input(
placeholder="Ask anything...",
value=State.prompt,
on_change=State.set_prompt,
on_submit=State.submit,
width="100%",
),

Comment on lines +153 to +155
if self.model not in merged:
self.model = merged[0]
return
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Custom model is silently dropped when load_models runs

When a user pins a custom model via apply_custom_model and then clicks Refresh (or the page reloads), load_models overwrites self.model with merged[0] if the custom model is not in the live catalog. The user loses their selection with no feedback. Preserving the selection when it isn't in the new list (or at least skipping the reset) avoids the silent drop.

Suggested change
if self.model not in merged:
self.model = merged[0]
return
if self.model not in merged:
merged = [self.model, *merged]
return

- load_models: run as background task (release state lock during HTTP fetch)
- Enter key submits via rx.form (was: button-only)
- pinned custom models persist across catalog refreshes
- assets/favicon.ico: include reflex default
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