diff --git a/README.md b/README.md index 1035740..64961af 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,7 @@ Free AI APIs have rate limits, downtime, and variable speed. SuperBrain solves t | **Groq** | `GROQ_API_KEY` | Fastest inference — free tier at [console.groq.com](https://console.groq.com) | | **Google Gemini** | `GEMINI_API_KEY` | Most generous free tier at [aistudio.google.com](https://aistudio.google.com) | | **OpenRouter** | `OPENROUTER_API_KEY` | Free model router at [openrouter.ai](https://openrouter.ai) | +| **Requesty** | `REQUESTY_API_KEY` | Optional paid gateway at [requesty.ai](https://app.requesty.ai/api-keys). Not part of the fallback order: only used when a key is set, with `REQUESTY_MODEL` (default `openai/gpt-4o-mini`) tried first and the chain above as backup. `REQUESTY_BASE_URL=https://router.eu.requesty.ai/v1` routes through the EU region | | **Ollama** | *(no key needed)* | Local inference — `start.py` guides setup · recommended model: `qwen3-vl:4b` | > **Tip:** You don't need all providers — the router falls back automatically. Start with at least **Gemini** (most generous free tier). Ollama serves as the fully offline last resort. @@ -338,6 +339,7 @@ Edit `.env` and set at least: - `GEMINI_API_KEY` (recommended) - `GROQ_API_KEY` (optional) - `OPENROUTER_API_KEY` (optional) +- `REQUESTY_API_KEY` and `REQUESTY_MODEL` (optional) Then run: diff --git a/backend/.env.example b/backend/.env.example index bed2b39..423ee4e 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -25,6 +25,10 @@ DATABASE_TIMEOUT=30 GROQ_API_KEY= GEMINI_API_KEY= OPENROUTER_API_KEY= +# Optional: Requesty (https://app.requesty.ai/api-keys), used only when set +REQUESTY_API_KEY= +# REQUESTY_MODEL=openai/gpt-4o-mini +# REQUESTY_BASE_URL=https://router.eu.requesty.ai/v1 # ──── Whisper Configuration ──── # Options: tiny, base, small, medium, large diff --git a/backend/api.py b/backend/api.py index b3f2c2d..84d4af2 100644 --- a/backend/api.py +++ b/backend/api.py @@ -1754,6 +1754,11 @@ async def get_ai_providers(token: str = Depends(verify_token)): "has_key": providers.get("openrouter", False), "key_hint": "sk-or-..." if providers.get("openrouter") else None }, + "requesty": { + "name": "Requesty", + "has_key": providers.get("requesty", False), + "key_hint": "rqsty-..." if providers.get("requesty") else None + }, "ollama": { "name": "Ollama (Local)", "has_key": providers.get("ollama", False), @@ -1770,12 +1775,12 @@ async def set_ai_provider_key( """ Set an API key for an AI provider. - Requires API authentication - - provider: groq, gemini, or openrouter + - provider: groq, gemini, openrouter, or requesty """ from core.model_router import get_router import httpx - valid_providers = ["groq", "gemini", "openrouter"] + valid_providers = ["groq", "gemini", "openrouter", "requesty"] provider_slug = data.provider.lower() if provider_slug not in valid_providers: raise HTTPException( @@ -1801,6 +1806,10 @@ async def set_ai_provider_key( resp = await client.get("https://openrouter.ai/api/v1/auth/key", headers={"Authorization": f"Bearer {data.api_key.strip()}"}) if resp.status_code != 200: raise HTTPException(status_code=401, detail="Invalid OpenRouter API Key") + elif provider_slug == "requesty": + resp = await client.get("https://router.requesty.ai/v1/models", headers={"Authorization": f"Bearer {data.api_key.strip()}"}) + if resp.status_code != 200: + raise HTTPException(status_code=401, detail="Invalid Requesty API Key") except httpx.RequestError as e: raise HTTPException(status_code=502, detail=f"Network error validating API key: {e}") @@ -1824,7 +1833,7 @@ async def delete_ai_provider_key( """ from core.model_router import get_router - valid_providers = ["groq", "gemini", "openrouter"] + valid_providers = ["groq", "gemini", "openrouter", "requesty"] if provider.lower() not in valid_providers: raise HTTPException( status_code=400, diff --git a/backend/config/.api_keys.example b/backend/config/.api_keys.example index 86d9ec1..862dabc 100644 --- a/backend/config/.api_keys.example +++ b/backend/config/.api_keys.example @@ -6,6 +6,7 @@ # Groq → https://console.groq.com/keys # Gemini → https://aistudio.google.com/apikey # OpenRouter → https://openrouter.ai/keys +# Requesty → https://app.requesty.ai/api-keys (optional, used only when set) # # You don't need all three — the router falls back automatically. # Recommended: add at least Gemini (most generous free tier). @@ -14,6 +15,8 @@ GROQ_API_KEY=gsk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx GEMINI_API_KEY=AIzaxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx OPENROUTER_API_KEY=sk-or-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +# REQUESTY_API_KEY=rqsty-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +# REQUESTY_MODEL=openai/gpt-4o-mini # Instagram account used for downloading posts (instagrapi / Private API). # Use a secondary/burner account — not your main personal account. diff --git a/backend/config_settings.py b/backend/config_settings.py index 97d5ccf..fbea1df 100644 --- a/backend/config_settings.py +++ b/backend/config_settings.py @@ -72,6 +72,10 @@ class Settings: # OpenRouter API (for model fallback) OPENROUTER_API_KEY: Optional[str] = os.getenv("OPENROUTER_API_KEY") + # Requesty API (only used when a key is set, model via REQUESTY_MODEL) + REQUESTY_API_KEY: Optional[str] = os.getenv("REQUESTY_API_KEY") + REQUESTY_MODEL: str = os.getenv("REQUESTY_MODEL", "openai/gpt-4o-mini") + # Whisper Model Configuration WHISPER_MODEL: str = os.getenv("WHISPER_MODEL", "base") WHISPER_USE_CLOUD: bool = os.getenv("WHISPER_USE_CLOUD", "true").lower() == "true" diff --git a/backend/core/model_router.py b/backend/core/model_router.py index d23b9cd..e20bac7 100644 --- a/backend/core/model_router.py +++ b/backend/core/model_router.py @@ -22,6 +22,12 @@ GROQ_API_KEY=gsk_... GEMINI_API_KEY=AIza... OPENROUTER_API_KEY=sk-or-... + REQUESTY_API_KEY=rqsty-... + REQUESTY_MODEL=openai/gpt-4o-mini (optional) + +Requesty is not part of the priority order. It is only used when you add a +REQUESTY_API_KEY: requests then go to REQUESTY_MODEL first, and the normal +chain above is used if that call fails. Performance state persisted to backend/model_rankings.json (rankings survive restarts). Dynamic model list cached in backend/openrouter_free_models.json (refreshed every 6 h). @@ -56,6 +62,11 @@ OPENROUTER_FREE_CACHE_HOURS = 6 # re-fetch every 6 h OPENROUTER_API_MODELS_URL = "https://openrouter.ai/api/v1/models" +# Requesty (https://requesty.ai): OpenAI compatible gateway, only used when the user adds a key. +# Set REQUESTY_BASE_URL=https://router.eu.requesty.ai/v1 to route through the EU region. +REQUESTY_BASE_URL = os.environ.get("REQUESTY_BASE_URL", "https://router.requesty.ai/v1").rstrip("/") +REQUESTY_DEFAULT_MODEL = "openai/gpt-4o-mini" + # Trusted providers — affects scoring weight for dynamic discovery (FreeRide) TRUSTED_PROVIDERS = [ "google", "meta-llama", "mistralai", "deepseek", @@ -522,7 +533,7 @@ def __init__(self): def _load_api_keys(self): """Load API keys from environment and .api_keys file.""" - for k in ("GROQ_API_KEY", "GEMINI_API_KEY", "OPENROUTER_API_KEY"): + for k in ("GROQ_API_KEY", "GEMINI_API_KEY", "OPENROUTER_API_KEY", "REQUESTY_API_KEY", "REQUESTY_MODEL"): v = os.environ.get(k) if v: self._api_keys[k] = v @@ -550,13 +561,14 @@ def get_available_providers(self) -> Dict[str, bool]: "groq": bool(self._key("GROQ_API_KEY")), "gemini": bool(self._key("GEMINI_API_KEY")), "openrouter": bool(self._key("OPENROUTER_API_KEY")), + "requesty": bool(self._key("REQUESTY_API_KEY")), "ollama": True, # Always available } def set_api_key(self, provider: str, api_key: str) -> bool: """Set an API key for a provider and persist to file.""" key_name = f"{provider.upper()}_API_KEY" - valid_providers = ["GROQ_API_KEY", "GEMINI_API_KEY", "OPENROUTER_API_KEY"] + valid_providers = ["GROQ_API_KEY", "GEMINI_API_KEY", "OPENROUTER_API_KEY", "REQUESTY_API_KEY"] if key_name not in valid_providers: return False @@ -571,7 +583,7 @@ def set_api_key(self, provider: str, api_key: str) -> bool: def delete_api_key(self, provider: str) -> bool: """Delete an API key for a provider.""" key_name = f"{provider.upper()}_API_KEY" - valid_providers = ["GROQ_API_KEY", "GEMINI_API_KEY", "OPENROUTER_API_KEY"] + valid_providers = ["GROQ_API_KEY", "GEMINI_API_KEY", "OPENROUTER_API_KEY", "REQUESTY_API_KEY"] if key_name not in valid_providers: return False @@ -1048,6 +1060,78 @@ def _openrouter_vision(self, model_id: str, prompt: str, images_b64: List[str]) resp.raise_for_status() return resp.json()["choices"][0]["message"]["content"].strip() + def _requesty_text(self, model_id: str, prompt: str) -> str: + import requests + resp = requests.post( + f"{REQUESTY_BASE_URL}/chat/completions", + headers={ + "Authorization": f"Bearer {self._key('REQUESTY_API_KEY')}", + "HTTP-Referer": "https://github.com/superbrain", + "X-Title": "SuperBrain", + "Content-Type": "application/json", + }, + json={ + "model": model_id, + "messages": [{"role": "user", "content": prompt}], + "max_tokens": 800, + "temperature": 0.7, + }, + timeout=60, + ) + if resp.status_code == 429: + raise Exception(f"429 rate limit: {resp.text[:200]}") + resp.raise_for_status() + return resp.json()["choices"][0]["message"]["content"].strip() + + def _requesty_vision(self, model_id: str, prompt: str, images_b64: List[str]) -> str: + import requests + content: List[Dict] = [] + for b64 in images_b64: + content.append({ + "type": "image_url", + "image_url": {"url": f"data:image/jpeg;base64,{b64}"}, + }) + content.append({"type": "text", "text": prompt}) + resp = requests.post( + f"{REQUESTY_BASE_URL}/chat/completions", + headers={ + "Authorization": f"Bearer {self._key('REQUESTY_API_KEY')}", + "HTTP-Referer": "https://github.com/superbrain", + "X-Title": "SuperBrain", + "Content-Type": "application/json", + }, + json={ + "model": model_id, + "messages": [{"role": "user", "content": content}], + "max_tokens": 800, + "temperature": 0.7, + }, + timeout=90, + ) + if resp.status_code == 429: + raise Exception(f"429 rate limit: {resp.text[:200]}") + resp.raise_for_status() + return resp.json()["choices"][0]["message"]["content"].strip() + + def _try_requesty(self, call, *args) -> Optional[str]: + """ + Requesty is opt-in: it is only called when the user has added a REQUESTY_API_KEY, + and it never enters the ranked fallback order. Returns None if not configured or + if the call fails, so the caller continues with the normal chain. + """ + if not self._key("REQUESTY_API_KEY"): + return None + model_id = self._key("REQUESTY_MODEL") or REQUESTY_DEFAULT_MODEL + print(f" 🤖 [REQUESTY] {model_id} ...", flush=True) + t0 = time.time() + try: + result = call(model_id, *args) + except Exception as e: + print(f" ✗ Requesty failed ({type(e).__name__}), using fallback chain …", flush=True) + return None + print(f" ✓ {time.time() - t0:.1f}s", flush=True) + return result + def _ollama_text(self, model_id: str, prompt: str) -> str: import ollama r = ollama.generate( @@ -1075,6 +1159,10 @@ def generate_text(self, prompt: str) -> str: Falls back through the ranked list until one succeeds. Raises RuntimeError if all fail. """ + result = self._try_requesty(self._requesty_text, prompt) + if result is not None: + return result + ranked = self._ranked_models("text") if not ranked: raise RuntimeError( @@ -1122,6 +1210,10 @@ def analyze_images(self, prompt: str, images_b64: List[str]) -> str: Falls back through the ranked list until one succeeds. Raises RuntimeError if all fail. """ + result = self._try_requesty(self._requesty_vision, prompt, images_b64) + if result is not None: + return result + ranked = self._ranked_models("vision") if not ranked: raise RuntimeError( @@ -1224,6 +1316,8 @@ def _print_startup_status(self): parts.append("Groq ✓" if self._key("GROQ_API_KEY") else "Groq ✗") parts.append("Gemini ✓" if self._key("GEMINI_API_KEY") else "Gemini ✗") parts.append("OpenRouter ✓" if self._key("OPENROUTER_API_KEY") else "OpenRouter ✗") + if self._key("REQUESTY_API_KEY"): + parts.append(f"Requesty ✓ ({self._key('REQUESTY_MODEL') or REQUESTY_DEFAULT_MODEL})") parts.append("Ollama (fallback)") print(f"🌐 Model Router initialised: {' | '.join(parts)}") diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index 93c0b60..93071c1 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -37,6 +37,9 @@ services: GEMINI_API_KEY: ${GEMINI_API_KEY} GOOGLE_API_KEY: ${GOOGLE_API_KEY} OPENROUTER_API_KEY: ${OPENROUTER_API_KEY} + REQUESTY_API_KEY: ${REQUESTY_API_KEY} + REQUESTY_MODEL: ${REQUESTY_MODEL:-openai/gpt-4o-mini} + REQUESTY_BASE_URL: ${REQUESTY_BASE_URL:-https://router.requesty.ai/v1} # Instagram credentials INSTAGRAM_USERNAME: ${INSTAGRAM_USERNAME} diff --git a/backend/reset.py b/backend/reset.py index 2da0dac..301acfd 100644 --- a/backend/reset.py +++ b/backend/reset.py @@ -102,7 +102,7 @@ def _remove_dir(path: Path, label: str): # ── Reset actions ───────────────────────────────────────────────────────────── def reset_api_keys(): h1("Reset — API Keys") - warn("This removes ALL keys: Gemini / Groq / OpenRouter and Instagram credentials.") + warn("This removes ALL keys: Gemini / Groq / OpenRouter / Requesty and Instagram credentials.") if not ask_yn("Continue?", default=False): info("Skipped.") return diff --git a/backend/start-docker-prod.sh b/backend/start-docker-prod.sh index 4a704be..449e74c 100644 --- a/backend/start-docker-prod.sh +++ b/backend/start-docker-prod.sh @@ -57,9 +57,9 @@ fi # ──── Check required API keys ──── source .env -if [ -z "$GROQ_API_KEY" ] && [ -z "$GEMINI_API_KEY" ] && [ -z "$GOOGLE_API_KEY" ] && [ -z "$OPENROUTER_API_KEY" ]; then +if [ -z "$GROQ_API_KEY" ] && [ -z "$GEMINI_API_KEY" ] && [ -z "$GOOGLE_API_KEY" ] && [ -z "$OPENROUTER_API_KEY" ] && [ -z "$REQUESTY_API_KEY" ]; then echo -e "${YELLOW}⚠ Warning: No AI provider API keys configured${NC}" - echo -e "${YELLOW} Set at least one of: GROQ_API_KEY, GEMINI_API_KEY, OPENROUTER_API_KEY${NC}" + echo -e "${YELLOW} Set at least one of: GROQ_API_KEY, GEMINI_API_KEY, OPENROUTER_API_KEY, REQUESTY_API_KEY${NC}" fi # ──── Create necessary directories ──── diff --git a/backend/start.py b/backend/start.py index 9243349..b73f5cb 100644 --- a/backend/start.py +++ b/backend/start.py @@ -308,6 +308,22 @@ def _validate_openrouter(key: str): except Exception as e: return None, f"could not verify ({str(e)[:70]})" +def _validate_requesty(key: str): + try: + import urllib.request as _r, urllib.error as _e + req = _r.Request("https://router.requesty.ai/v1/models", + headers={"Authorization": f"Bearer {key}", + "Accept": "application/json", + "User-Agent": "Mozilla/5.0"}) + with _r.urlopen(req, timeout=8): + return True, "key valid" + except _e.HTTPError as e: + if e.code in (401, 403): + return False, f"invalid key ({e.code})" + return None, f"could not verify ({e.code} {e.reason})" + except Exception as e: + return None, f"could not verify ({str(e)[:70]})" + def _check_and_report(name: str, key: str, validator) -> str: """Validate `key`, print result inline, return the key unchanged.""" if not key: @@ -342,6 +358,9 @@ def setup_api_keys(): Groq → {CYAN}https://console.groq.com/keys{RESET} OpenRouter → {CYAN}https://openrouter.ai/keys{RESET} + Optional paid gateway (used only if you add a key, not part of the fallback order): + Requesty → {CYAN}https://app.requesty.ai/api-keys{RESET} + Press {BOLD}Enter{RESET} to skip any key you don't have yet. {DIM}Keys and passwords are visible as you paste — don't run setup in a screen share.{RESET} """) @@ -361,8 +380,13 @@ def setup_api_keys(): groq_k = _check_and_report("Groq", groq_k, _validate_groq) openr = ask("OpenRouter API key", default=existing.get("OPENROUTER_API_KEY"), paste=True) or "" openr = _check_and_report("OpenRouter", openr, _validate_openrouter) + requesty = ask("Requesty API key", default=existing.get("REQUESTY_API_KEY"), paste=True) or "" + requesty = _check_and_report("Requesty", requesty, _validate_requesty) + requesty_model = "" + if requesty: + requesty_model = ask("Requesty model", default=existing.get("REQUESTY_MODEL") or "openai/gpt-4o-mini") or "" - if not any([gemini, groq_k, openr]): + if not any([gemini, groq_k, openr, requesty]): warn("No AI keys entered. SuperBrain will still work but can only use") warn("local Ollama models (configured in the next step).") @@ -395,6 +419,8 @@ def setup_api_keys(): f"GEMINI_API_KEY={gemini}\n", f"GROQ_API_KEY={groq_k}\n", f"OPENROUTER_API_KEY={openr}\n", + f"REQUESTY_API_KEY={requesty}\n", + f"REQUESTY_MODEL={requesty_model}\n", "\n", f"INSTAGRAM_USERNAME={ig_user}\n", f"INSTAGRAM_PASSWORD={ig_pass}\n", @@ -417,7 +443,7 @@ def setup_ollama(): return keys = _load_saved_api_keys() - has_cloud_key = any(keys.get(k) for k in ("GEMINI_API_KEY", "GROQ_API_KEY", "OPENROUTER_API_KEY")) + has_cloud_key = any(keys.get(k) for k in ("GEMINI_API_KEY", "GROQ_API_KEY", "OPENROUTER_API_KEY", "REQUESTY_API_KEY")) print(f""" Ollama runs AI models {BOLD}locally on your machine{RESET} — no internet or API diff --git a/backend/tests/test_requesty_router.py b/backend/tests/test_requesty_router.py new file mode 100644 index 0000000..fc3f531 --- /dev/null +++ b/backend/tests/test_requesty_router.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +"""Requesty is opt-in: only used when a key is set, never part of the ranked fallback order.""" + +import os +import sys +import tempfile +import unittest +from pathlib import Path +from unittest import mock + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import core.model_router as model_router_module # noqa: E402 + +PROVIDER_ENV_VARS = ("GROQ_API_KEY", "GEMINI_API_KEY", "OPENROUTER_API_KEY", "REQUESTY_API_KEY", "REQUESTY_MODEL") + + +class RequestyRouterTests(unittest.TestCase): + def setUp(self): + self.temp_dir = tempfile.TemporaryDirectory() + temp = Path(self.temp_dir.name) + self.patches = [ + mock.patch.object(model_router_module, "API_KEYS_FILE", temp / ".api_keys"), + mock.patch.object(model_router_module, "RANKINGS_FILE", temp / "model_rankings.json"), + mock.patch.dict(os.environ, {}, clear=False), + ] + for p in self.patches: + p.start() + for k in PROVIDER_ENV_VARS: + os.environ.pop(k, None) + + def tearDown(self): + for p in reversed(self.patches): + p.stop() + self.temp_dir.cleanup() + + def _router(self, **keys): + os.environ.update(keys) + return model_router_module.ModelRouter() + + def test_requesty_is_never_in_ranked_fallback_order(self): + router = self._router(REQUESTY_API_KEY="test-key") + for task in ("text", "vision"): + providers = {model_router_module.MODELS_BY_KEY[k]["provider"] for k in router._ranked_models(task)} + self.assertNotIn("requesty", providers) + self.assertTrue(router.get_available_providers()["requesty"]) + + def test_requesty_not_called_without_key(self): + router = self._router() + with mock.patch.object(router, "_requesty_text", side_effect=AssertionError("should not be called")), \ + mock.patch.object(router, "_ollama_text", return_value="local"): + self.assertEqual(router.generate_text("hi"), "local") + + def test_requesty_uses_selected_model_when_key_set(self): + router = self._router(REQUESTY_API_KEY="test-key", REQUESTY_MODEL="anthropic/claude-sonnet-4-5") + fake = mock.Mock(status_code=200) + fake.json.return_value = {"choices": [{"message": {"content": " hello "}}]} + with mock.patch("requests.post", return_value=fake) as post: + self.assertEqual(router.generate_text("hi"), "hello") + url = post.call_args.args[0] + body = post.call_args.kwargs["json"] + self.assertEqual(url, f"{model_router_module.REQUESTY_BASE_URL}/chat/completions") + self.assertEqual(body["model"], "anthropic/claude-sonnet-4-5") + self.assertEqual(post.call_args.kwargs["headers"]["Authorization"], "Bearer test-key") + + def test_requesty_failure_falls_back_to_normal_chain(self): + router = self._router(REQUESTY_API_KEY="test-key") + with mock.patch.object(router, "_requesty_vision", side_effect=RuntimeError("boom")), \ + mock.patch.object(router, "_ollama_vision", return_value="local vision"): + self.assertEqual(router.analyze_images("describe", ["aGk="]), "local vision") + + +if __name__ == "__main__": + unittest.main() diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 548eed4..6fea42e 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -74,6 +74,15 @@ npx expo start 3. Create API key 4. Copy to `.env`: `OPENROUTER_API_KEY=your_key` +### Requesty (Optional) + +1. Visit [app.requesty.ai/api-keys](https://app.requesty.ai/api-keys) +2. Create API key +3. Copy to `.env`: `REQUESTY_API_KEY=your_key` +4. Optionally pick a model: `REQUESTY_MODEL=openai/gpt-4o-mini` + +Requesty is only used when this key is set; if a Requesty call fails the normal fallback chain takes over. + ## 📱 Testing the App 1. **Share Content** diff --git a/docs/TECHNICAL_REFERENCE.md b/docs/TECHNICAL_REFERENCE.md index 70badff..d2e2d7f 100644 --- a/docs/TECHNICAL_REFERENCE.md +++ b/docs/TECHNICAL_REFERENCE.md @@ -546,6 +546,17 @@ docker push gcr.io/project/superbrain:latest 3. Backend auto-discovers free models every 6 hours 4. Verify: Check `config/openrouter_free_models.json` +### Setting Up Requesty (Optional) +1. Get API key: https://app.requesty.ai/api-keys +2. Store in `config/.api_keys`: + ``` + REQUESTY_API_KEY=rqsty-xxxxx + REQUESTY_MODEL=openai/gpt-4o-mini + ``` +3. Requesty is not added to the ranked fallback order. When the key is set, text and vision + requests go to `REQUESTY_MODEL` first and fall back to the normal chain on error +4. For the EU region set `REQUESTY_BASE_URL=https://router.eu.requesty.ai/v1` in the environment + ### Setting Up Ollama (Local, Optional) 1. Install: https://ollama.ai/ 2. Pull model: `ollama pull mistral` diff --git a/superbrain-app/src/screens/AIProviderScreen.tsx b/superbrain-app/src/screens/AIProviderScreen.tsx index c24bfb3..bfb4f1d 100644 --- a/superbrain-app/src/screens/AIProviderScreen.tsx +++ b/superbrain-app/src/screens/AIProviderScreen.tsx @@ -85,6 +85,7 @@ const AIProviderScreen = () => { groq: 'Groq', gemini: 'Google Gemini', openrouter: 'OpenRouter', + requesty: 'Requesty', ollama: 'Ollama (Local)', }; @@ -92,6 +93,7 @@ const AIProviderScreen = () => { groq: 'hardware-chip-outline', gemini: 'sparkles-outline', openrouter: 'globe-outline', + requesty: 'git-network-outline', ollama: 'desktop-outline', }; @@ -99,6 +101,7 @@ const AIProviderScreen = () => { groq: '#f97316', gemini: '#4285f4', openrouter: '#8b5cf6', + requesty: '#0ea5e9', ollama: '#10b981', }; @@ -168,7 +171,7 @@ const AIProviderScreen = () => { Provider - {['groq', 'gemini', 'openrouter'].map(p => ( + {['groq', 'gemini', 'openrouter', 'requesty'].map(p => ( { • Groq: console.groq.com{'\n'} • Gemini: aistudio.google.com/apikey{'\n'} - • OpenRouter: openrouter.ai/keys + • OpenRouter: openrouter.ai/keys{'\n'} + • Requesty (optional, paid): app.requesty.ai/api-keys diff --git a/superbrain-app/src/screens/SettingsScreen.tsx b/superbrain-app/src/screens/SettingsScreen.tsx index f886c1f..c239185 100644 --- a/superbrain-app/src/screens/SettingsScreen.tsx +++ b/superbrain-app/src/screens/SettingsScreen.tsx @@ -458,7 +458,7 @@ const SettingsScreen = () => { icon="cloud-outline" iconColor="#8b5cf6" title="AI Providers" - subtitle="Configure Groq, Gemini, OpenRouter" + subtitle="Configure Groq, Gemini, OpenRouter, Requesty" onPress={() => navigation.navigate('AIProvider')} />