Skip to content

Commit eb29833

Browse files
committed
feat: update OpenRouter model and fallback options; enhance Streamlit UI with model selection and testing features
1 parent a0ba0b4 commit eb29833

5 files changed

Lines changed: 271 additions & 61 deletions

File tree

.env.example

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,6 @@ OPENAI_MODEL=gpt-4o-mini
1818

1919
# ── OpenRouter (free alternative — https://openrouter.ai/keys) ──
2020
OPENROUTER_API_KEY=
21-
OPENROUTER_MODEL=meta-llama/llama-3.1-8b-instruct:free
21+
OPENROUTER_MODEL=stepfun/step-3.5-flash:free
22+
# Optional comma-separated fallback models (used when selected model has no endpoint)
23+
OPENROUTER_FALLBACK_MODELS=arcee-ai/trinity-large-preview:free,qwen/qwen3-next-80b-a3b-instruct:free,openai/gpt-oss-120b:free

.github/copilot-instructions.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ pipeline:
368368
### LLM Provider Configuration
369369
370370
- `LLM_PROVIDER=openai` → uses OpenAI API (requires `OPENAI_API_KEY`)
371-
- `LLM_PROVIDER=openrouter` → uses OpenRouter API gateway (requires `OPENROUTER_API_KEY`). Supports 200+ models including free ones (e.g., `meta-llama/llama-3.1-8b-instruct:free`). OpenAI-compatible API via `openai` Python package with custom `base_url`. Get a free key at https://openrouter.ai/keys.
371+
- `LLM_PROVIDER=openrouter` → uses OpenRouter API gateway (requires `OPENROUTER_API_KEY`). Supports 200+ models including free ones (e.g., `stepfun/step-3.5-flash:free`). OpenAI-compatible API via `openai` Python package with custom `base_url`. Get a free key at https://openrouter.ai/keys.
372372
- `LLM_PROVIDER=local` → uses the local HuggingFace text-completion-llm-service
373373
- Factory: `create_llm_provider(provider=None)` reads env var if not specified
374374

@@ -464,7 +464,7 @@ Single bridge network `etl-network`. Services reference each other by container
464464
| `OPENAI_API_KEY` | — | OpenAI API key |
465465
| `OPENAI_MODEL` | `gpt-4o-mini` | OpenAI model name |
466466
| `OPENROUTER_API_KEY` | — | OpenRouter API key (free at https://openrouter.ai/keys) |
467-
| `OPENROUTER_MODEL` | `meta-llama/llama-3.1-8b-instruct:free` | OpenRouter model identifier |
467+
| `OPENROUTER_MODEL` | `stepfun/step-3.5-flash:free` | OpenRouter model identifier |
468468

469469
---
470470

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ After execution, switch to the **Datasets** tab to browse output files, preview
6868

6969
### New in Streamlit UX
7070

71+
- **Dual prompt modes** in Pipeline Editor: `Guided` textarea and `Chat-style` conversational input
72+
- **OpenRouter model semaphore** in sidebar: one-click model reachability check before generation
7173
- **Platform Readiness** panel in Execution tab: live checks for Airflow, Streamlit, Prometheus, Grafana, including Airflow scheduler heartbeat status
7274
- **Quick Airflow Triggers** in Execution tab: trigger `hr_analytics_pipeline`, `ecommerce_pipeline`, or `weather_api_pipeline` without leaving Streamlit
7375
- **Execution insights**: successful steps, processed data volume, slowest step, and orchestration overhead (%)
@@ -91,7 +93,7 @@ Pipeline Compiler → executes steps in parallel via Preparator SDK
9193
Output: cleaned dataset saved in the requested format
9294
```
9395

94-
The AI agent supports both **OpenAI** (GPT-4o-mini) and **local HuggingFace** models. The YAML editor and validator work without any API key.
96+
The AI agent supports **OpenAI**, **OpenRouter**, and **local HuggingFace** models. The YAML editor and validator work without any API key.
9597

9698
---
9799

@@ -297,9 +299,13 @@ Full walkthrough: [docs/extending.md](docs/extending.md)
297299

298300
| Variable | Default | Description |
299301
|---|---|---|
300-
| `LLM_PROVIDER` | `openai` | AI agent provider (`openai` or `local`) |
302+
| `LLM_PROVIDER` | `openai` | AI agent provider (`openai`, `openrouter`, or `local`) |
301303
| `OPENAI_API_KEY` | — | Required if `LLM_PROVIDER=openai` |
302304
| `OPENAI_MODEL` | `gpt-4o-mini` | OpenAI model |
305+
| `OPENROUTER_API_KEY` | — | Required if `LLM_PROVIDER=openrouter` |
306+
| `OPENROUTER_MODEL` | `stepfun/step-3.5-flash:free` | Default OpenRouter model |
307+
| `OPENROUTER_FALLBACK_MODELS` | `arcee-ai/trinity-large-preview:free,...` | Comma-separated fallback models if selected model is unavailable |
308+
| `LOCAL_LLM_URL` | `http://localhost:5012` | Local text-completion service URL when running Streamlit on host |
303309
| `ETL_DATA_ROOT` | `/app/data` | Base directory for datasets and metadata |
304310
| `ALLOW_PRIVATE_API_URLS` | `false` | Allow private/local API targets in extract-api |
305311

@@ -314,7 +320,7 @@ See [`.env.example`](.env.example) for all available variables including databas
314320
| Microservices | Python 3.9, Flask, Gunicorn |
315321
| Data Format | Apache Arrow IPC (streaming) |
316322
| Orchestration | Apache Airflow |
317-
| AI Agent | OpenAI / HuggingFace Transformers |
323+
| AI Agent | OpenAI / OpenRouter / HuggingFace Transformers |
318324
| UI | Streamlit |
319325
| Containers | Docker, Docker Compose (PostgreSQL 16, Airflow 2.10.4) |
320326
| Monitoring | Prometheus + Grafana |

ai_agent/llm_provider.py

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,25 @@
1818

1919
logger = logging.getLogger("ai_agent.llm_provider")
2020

21+
DEFAULT_OPENROUTER_MODEL = "stepfun/step-3.5-flash:free"
22+
DEFAULT_OPENROUTER_FALLBACK_MODELS = [
23+
"arcee-ai/trinity-large-preview:free",
24+
"qwen/qwen3-next-80b-a3b-instruct:free",
25+
"openai/gpt-oss-120b:free",
26+
"openai/gpt-4o-mini",
27+
]
28+
29+
30+
def _default_local_llm_url() -> str:
31+
"""Choose sensible local LLM URL based on runtime context.
32+
33+
In Docker, services reach each other via container DNS.
34+
On host runs (e.g., Streamlit launched from a venv), localhost is expected.
35+
"""
36+
if os.path.exists("/.dockerenv"):
37+
return "http://text-completion-llm-service:5012"
38+
return "http://localhost:5012"
39+
2140

2241
class LLMProvider(ABC):
2342
"""Abstract base class for LLM providers."""
@@ -92,7 +111,7 @@ def __init__(self, model: str = None, api_key: str = None):
92111
except ImportError:
93112
raise ImportError("openai package not installed. Run: pip install openai")
94113

95-
self.model = model or os.getenv("OPENROUTER_MODEL", "meta-llama/llama-3.1-8b-instruct:free")
114+
self.model = model or os.getenv("OPENROUTER_MODEL", DEFAULT_OPENROUTER_MODEL)
96115
api_key = api_key or os.getenv("OPENROUTER_API_KEY")
97116
if not api_key:
98117
raise ValueError(
@@ -108,21 +127,61 @@ def __init__(self, model: str = None, api_key: str = None):
108127
"X-Title": "ArrowFlow ETL Platform",
109128
},
110129
)
130+
131+
raw_fallback = os.getenv("OPENROUTER_FALLBACK_MODELS", "")
132+
configured_fallbacks = [m.strip() for m in raw_fallback.split(",") if m.strip()]
133+
self.fallback_models = configured_fallbacks or DEFAULT_OPENROUTER_FALLBACK_MODELS
111134
logger.info(f"OpenRouter provider initialized with model: {self.model}")
112135

136+
@staticmethod
137+
def _is_model_unavailable_error(exc: Exception) -> bool:
138+
msg = str(exc)
139+
return "No endpoints found for" in msg or "model not found" in msg.lower()
140+
113141
def generate(self, prompt: str, system_prompt: str = "", temperature: float = 0.3, max_tokens: int = 2048) -> str:
114142
messages = []
115143
if system_prompt:
116144
messages.append({"role": "system", "content": system_prompt})
117145
messages.append({"role": "user", "content": prompt})
118146

119-
response = self.client.chat.completions.create(
120-
model=self.model,
121-
messages=messages,
122-
temperature=temperature,
123-
max_tokens=max_tokens,
124-
)
125-
return response.choices[0].message.content
147+
def _call_chat(model_name: str) -> str:
148+
response = self.client.chat.completions.create(
149+
model=model_name,
150+
messages=messages,
151+
temperature=temperature,
152+
max_tokens=max_tokens,
153+
)
154+
return response.choices[0].message.content
155+
156+
try:
157+
return _call_chat(self.model)
158+
except Exception as exc:
159+
if not self._is_model_unavailable_error(exc):
160+
raise
161+
162+
tried = [self.model]
163+
for candidate in self.fallback_models:
164+
if candidate == self.model:
165+
continue
166+
tried.append(candidate)
167+
try:
168+
logger.warning(
169+
"OpenRouter model '%s' unavailable, retrying with fallback '%s'",
170+
self.model,
171+
candidate,
172+
)
173+
content = _call_chat(candidate)
174+
self.model = candidate
175+
return content
176+
except Exception as fallback_exc:
177+
if not self._is_model_unavailable_error(fallback_exc):
178+
raise
179+
180+
raise ValueError(
181+
"OpenRouter model unavailable. Tried: "
182+
+ ", ".join(tried)
183+
+ ". Select another model in Streamlit or set OPENROUTER_FALLBACK_MODELS."
184+
) from exc
126185

127186
def name(self) -> str:
128187
return f"OpenRouter ({self.model})"
@@ -134,9 +193,7 @@ class LocalProvider(LLMProvider):
134193
def __init__(self, service_url: str = None):
135194
import requests
136195
self.session = requests.Session()
137-
self.service_url = service_url or os.getenv(
138-
"LOCAL_LLM_URL", "http://text-completion-llm-service:5012"
139-
)
196+
self.service_url = service_url or os.getenv("LOCAL_LLM_URL") or _default_local_llm_url()
140197
logger.info(f"Local LLM provider initialized with URL: {self.service_url}")
141198

142199
def generate(self, prompt: str, system_prompt: str = "", temperature: float = 0.3, max_tokens: int = 2048) -> str:

0 commit comments

Comments
 (0)