A resilient runtime layer for free and cheap-tier LLM APIs.
Free-tier LLM providers are unreliable in ways that break production apps silently: models get deprecated overnight, quotas zero out without warning, and providers add card requirements with no notice. ftouter sits between your app and multiple providers, automatically falling back to the next available model when one fails — so a single dead endpoint doesn't take down your whole app.
.complete()chat_models, general_models, reasoning_models, tool_call_models, vision_modelspip install ftouter
.env file in your project root with the API keys for the providers you want to use:GROQ_API_KEY=your_key_here
OPENROUTER_API_KEY=your_key_here
MISTRAL_API_KEY=your_key_here
You don't need all three — ftouter skips any provider whose key isn't set and falls through to the next one.
from ftouter import chat_models
from dotenv import load_dotenv
load_dotenv()
result = chat_models.complete([
{"role": "user", "content": "Say hi in 5 words"}
])
print(result["choices"][0]["message"]["content"])
| Router | Use case |
|---|---|
chat_models.complete() | General-purpose conversational completions |
general_models.complete() | General-purpose tasks, pooled across all other routers |
reasoning_models.complete() | Tasks that benefit from reasoning-focused models |
tool_call_models.complete() | Completions that use function/tool calling |
vision_models.complete() | Completions that include image input |
Each router tries a prioritized list of models across providers and automatically moves to the next one on failure.
vision_models.complete() takes the same message format as the others — an image is just another block inside content, either an https:// URL or a base64 data: URI:
import base64
from ftouter import vision_models
from dotenv import load_dotenv
load_dotenv()
with open("example.jpg", "rb") as image_file:
image_data = base64.b64encode(image_file.read()).decode("utf-8")
result = vision_models.complete([
{
"role": "user",
"content": [
{"type": "text", "text": "what's in this image?"},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{image_data}"
}
}
]
}
])
print(result["choices"][0]["message"]["content"])
Vision draws from Groq, OpenRouter, and Mistral like the other routers, plus two vision-specific providers — add their keys to .env only if you want them in the fallback chain, ftouter skips them otherwise like any other missing key:
GEMINI_API_KEY=your_key_here
MOONDREAM_API_KEY=your_key_here
Note: base64 image support is confirmed for Groq and Gemini. OpenRouter should accept it (same spec, not independently verified here). Moondream's exact behavior with base64 input is unconfirmed — test it directly if you're relying on that provider.
When you call .complete(), ftouter:
RuntimeError only if every model in the list is exhausted or on cooldownThis means a single provider having a bad day doesn't crash your app — it just quietly routes around it.
If every provider fails, .complete() raises a RuntimeError:
try:
result = chat_models.complete([{"role": "user", "content": "Hello"}])
print(result["choices"][0]["message"]["content"])
except RuntimeError as e:
print(f"All providers failed: {e}")
Issues and pull requests are welcome. If you'd like to add support for another provider, open an issue first so the model list and provider config stay consistent.
MIT
32 commits
1 commits
Python
100.0%
A resilient runtime layer for free and cheap-tier LLM APIs.
Free-tier LLM providers are unreliable in ways that break production apps silently: models get deprecated overnight, quotas zero out without warning, and providers add card requirements with no notice. ftouter sits between your app and multiple providers, automatically falling back to the next available model when one fails — so a single dead endpoint doesn't take down your whole app.
.complete()chat_models, general_models, reasoning_models, tool_call_models, vision_modelspip install ftouter
.env file in your project root with the API keys for the providers you want to use:GROQ_API_KEY=your_key_here
OPENROUTER_API_KEY=your_key_here
MISTRAL_API_KEY=your_key_here
You don't need all three — ftouter skips any provider whose key isn't set and falls through to the next one.
from ftouter import chat_models
from dotenv import load_dotenv
load_dotenv()
result = chat_models.complete([
{"role": "user", "content": "Say hi in 5 words"}
])
print(result["choices"][0]["message"]["content"])
| Router | Use case |
|---|---|
chat_models.complete() | General-purpose conversational completions |
general_models.complete() | General-purpose tasks, pooled across all other routers |
reasoning_models.complete() | Tasks that benefit from reasoning-focused models |
tool_call_models.complete() | Completions that use function/tool calling |
vision_models.complete() | Completions that include image input |
Each router tries a prioritized list of models across providers and automatically moves to the next one on failure.
vision_models.complete() takes the same message format as the others — an image is just another block inside content, either an https:// URL or a base64 data: URI:
import base64
from ftouter import vision_models
from dotenv import load_dotenv
load_dotenv()
with open("example.jpg", "rb") as image_file:
image_data = base64.b64encode(image_file.read()).decode("utf-8")
result = vision_models.complete([
{
"role": "user",
"content": [
{"type": "text", "text": "what's in this image?"},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{image_data}"
}
}
]
}
])
print(result["choices"][0]["message"]["content"])
Vision draws from Groq, OpenRouter, and Mistral like the other routers, plus two vision-specific providers — add their keys to .env only if you want them in the fallback chain, ftouter skips them otherwise like any other missing key:
GEMINI_API_KEY=your_key_here
MOONDREAM_API_KEY=your_key_here
Note: base64 image support is confirmed for Groq and Gemini. OpenRouter should accept it (same spec, not independently verified here). Moondream's exact behavior with base64 input is unconfirmed — test it directly if you're relying on that provider.
When you call .complete(), ftouter:
RuntimeError only if every model in the list is exhausted or on cooldownThis means a single provider having a bad day doesn't crash your app — it just quietly routes around it.
If every provider fails, .complete() raises a RuntimeError:
try:
result = chat_models.complete([{"role": "user", "content": "Hello"}])
print(result["choices"][0]["message"]["content"])
except RuntimeError as e:
print(f"All providers failed: {e}")
Issues and pull requests are welcome. If you'd like to add support for another provider, open an issue first so the model list and provider config stay consistent.
MIT
32 commits
1 commits
Python
100.0%