Freeway: self-hosted OpenAI-compatible gateway for free-tier providers
One endpoint in front of every free LLM tier, with automatic failover and no provider key ever leaving the box. Providers are data, not code: adding one is a JSON file plus an env var, never a TypeScript change. Ships 12 providers pre-written; `verify-provider --apply` derives the live model list from your own key. - core: registry, bucketed quota meter (rolling + UTC-calendar windows), key-pool store, 6-step model router with diagnostic rejection reasons - gateway: node:http, failover that commits to a stream only after 200 headers, SSE passthrough with usage sniffing, virtual keys with admin/inference roles - context engine: sqlite sessions and a 5-rung refit ladder, so a 128k thread keeps working when it fails over to an 8k model - cache: exact (sha256, safe-by-default) + opt-in semantic (flat Float32Array) - guard: inbound limits, quota forecasting, limit calibration from upstream headers, key health, secret/PII/injection scanning - dashboard: one self-contained HTML file, live SSE request log, token usage - sdk: zero-dep typed client, drop-in for the OpenAI SDK Zero runtime dependencies. 326 tests, 27-check smoke suite against a scripted mock upstream so routing is testable without spending quota. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
commit
cb985dcf9c
86 files changed
+16350
No files matched your search
@@ -0,0 +1,142 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://freeway.local/schemas/provider.json",
|
||||
"title": "Freeway provider spec",
|
||||
"description": "One free-tier API provider, as data. Adding a provider means dropping one of these into providers/ and setting an env var — never editing TypeScript. Files whose name starts with _ are skipped by the loader, which is why this schema can live here.",
|
||||
"type": "object",
|
||||
"required": ["id", "baseUrl", "auth"],
|
||||
"additionalProperties": false,
|
||||
|
||||
"properties": {
|
||||
"$schema": { "type": "string" },
|
||||
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9][a-z0-9-]*$",
|
||||
"description": "Short slug. Becomes the first half of every model ref, e.g. `groq/llama-3.3-70b`."
|
||||
},
|
||||
"label": { "type": "string", "description": "Human name for the dashboard. Defaults to id." },
|
||||
"docs": { "type": ["string", "null"], "description": "API docs URL, linked from the provider card." },
|
||||
"console": { "type": ["string", "null"], "description": "Where to get a key. Linked as 'get a key' on unconfigured cards." },
|
||||
"enabled": { "type": "boolean", "default": true },
|
||||
"priority": { "type": "number", "default": 50, "description": "Lower is tried first under the `priority` strategy." },
|
||||
"adapter": { "enum": ["openai"], "default": "openai", "description": "Wire format. Only `openai` is implemented." },
|
||||
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"description": "OpenAI-compatible base, e.g. https://api.groq.com/openai/v1. May contain ${ENV_VAR}, which is how providers that embed an account id in the URL stay pure data.",
|
||||
"examples": [
|
||||
"https://api.groq.com/openai/v1",
|
||||
"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1"
|
||||
]
|
||||
},
|
||||
|
||||
"auth": {
|
||||
"type": "object",
|
||||
"required": ["type"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": ["bearer", "header", "query", "none"],
|
||||
"description": "bearer → Authorization: Bearer <key>. header → a named header. query → a query param. none → no credential (a local Ollama)."
|
||||
},
|
||||
"header": { "type": ["string", "null"], "description": "Header name when type is `header`, e.g. x-api-key." },
|
||||
"query": { "type": ["string", "null"], "description": "Query param name when type is `query`, e.g. key." },
|
||||
"prefix": { "type": ["string", "null"], "description": "Overrides the `Bearer ` prefix. Set \"\" for a bare token." },
|
||||
"envKeys": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Env vars to read keys from, in order. ONE variable may hold several comma-separated keys — that is the key pool, and quota is tracked per key.",
|
||||
"examples": [["GROQ_API_KEY", "GROQ_API_KEY_2"]]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"limits": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Free-tier ceilings. null means unknown or unlimited and is ALWAYS better than a guess — the guard layer overwrites nulls with values learned from upstream rate-limit headers.",
|
||||
"properties": {
|
||||
"rps": { "type": ["number", "null"], "exclusiveMinimum": 0, "description": "Requests per second (rolling)." },
|
||||
"rpm": { "type": ["number", "null"], "exclusiveMinimum": 0, "description": "Requests per minute (rolling 60s)." },
|
||||
"rpd": { "type": ["number", "null"], "exclusiveMinimum": 0, "description": "Requests per day (calendar, resets 00:00 UTC)." },
|
||||
"tpm": { "type": ["number", "null"], "exclusiveMinimum": 0, "description": "Tokens per minute (rolling 60s)." },
|
||||
"tpd": { "type": ["number", "null"], "exclusiveMinimum": 0, "description": "Tokens per day (calendar UTC)." },
|
||||
"tpmo": { "type": ["number", "null"], "exclusiveMinimum": 0, "description": "Tokens per calendar month." },
|
||||
"creditsPerDay": { "type": ["number", "null"], "exclusiveMinimum": 0, "description": "Abstract metered unit per day — Cloudflare bills free Workers AI in neurons." },
|
||||
"creditsPerRequest": { "type": ["number", "null"], "exclusiveMinimum": 0, "description": "Fallback flat credit cost when a model has no per-token rates." }
|
||||
}
|
||||
},
|
||||
|
||||
"limitsSource": {
|
||||
"enum": ["docs", "observed", "unverified"],
|
||||
"default": "unverified",
|
||||
"description": "Where the numbers above came from. Use `unverified` honestly — the dashboard badges it and nothing breaks."
|
||||
},
|
||||
"verifiedOn": { "type": ["string", "null"], "description": "ISO date the limits were last checked, e.g. 2026-08-16." },
|
||||
|
||||
"modelsEndpoint": {
|
||||
"type": ["string", "null"],
|
||||
"default": "/models",
|
||||
"description": "Path for live model discovery, relative to baseUrl. null disables it (Cloudflare's OpenAI surface has no /models)."
|
||||
},
|
||||
"modelsPath": {
|
||||
"type": "string",
|
||||
"default": "data[].id",
|
||||
"description": "Where ids live in the discovery response. Tiny grammar: `data[].id`, `models[].name`, `[].id`, `result[].name`. This is what keeps a non-OpenAI response shape a JSON edit.",
|
||||
"examples": ["data[].id", "models[].name", "result[].name", "[].id"]
|
||||
},
|
||||
|
||||
"dropParams": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Body params this provider rejects; stripped before forwarding. Find them by sending a request and reading the 400.",
|
||||
"examples": [["frequency_penalty", "presence_penalty"]]
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "string" },
|
||||
"description": "Extra request headers. Values may contain ${ENV_VAR}."
|
||||
},
|
||||
"notes": { "type": ["string", "null"], "description": "Anything the next reader needs — signup friction, quirks, why a number is what it is." },
|
||||
|
||||
"models": {
|
||||
"type": "array",
|
||||
"description": "Seed models. Prefer a handful you have actually verified over a long list you have not; live discovery fills in the rest.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": { "type": "string", "description": "Upstream model id, exactly as the provider spells it. May contain slashes." },
|
||||
"label": { "type": "string" },
|
||||
"alias": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Short names clients can route by. Several providers sharing an alias is the point — that is what makes failover work.",
|
||||
"examples": [["fast", "cheap"]]
|
||||
},
|
||||
"context": { "type": ["number", "null"], "exclusiveMinimum": 0, "description": "Context window in tokens. null when unpublished; the context engine then assumes a conservative default." },
|
||||
"caps": {
|
||||
"type": "array",
|
||||
"items": { "enum": ["chat", "tools", "json", "vision", "reasoning", "embed", "code", "long-context"] },
|
||||
"default": ["chat"],
|
||||
"description": "Drives routing. A request with `tools` only goes to models declaring `tools`. `embed` models never serve chat and vice versa."
|
||||
},
|
||||
"priority": { "type": "number", "default": 50 },
|
||||
"enabled": { "type": "boolean", "default": true },
|
||||
"maxOutput": { "type": ["number", "null"], "exclusiveMinimum": 0 },
|
||||
"credits": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Per-model credit rates. Output tokens typically cost 7-10x input, and a big model ~10x a small one, so one flat number per provider is wrong by an order of magnitude.",
|
||||
"properties": {
|
||||
"perMTokIn": { "type": ["number", "null"], "exclusiveMinimum": 0 },
|
||||
"perMTokOut": { "type": ["number", "null"], "exclusiveMinimum": 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "./_schema.json",
|
||||
"id": "cerebras",
|
||||
"label": "Cerebras",
|
||||
"docs": "https://inference-docs.cerebras.ai/",
|
||||
"console": "https://cloud.cerebras.ai/platform",
|
||||
"priority": 15,
|
||||
"baseUrl": "https://api.cerebras.ai/v1",
|
||||
"auth": { "type": "bearer", "envKeys": ["CEREBRAS_API_KEY"] },
|
||||
"limits": { "rpm": 30, "rpd": 14400, "tpm": 60000, "tpd": 1000000 },
|
||||
"limitsSource": "docs",
|
||||
"verifiedOn": "2026-08-16",
|
||||
"modelsEndpoint": "/models",
|
||||
"modelsPath": "data[].id",
|
||||
"notes": "Largest daily token budget of the free tiers (~1M tokens/day), no card required. Cerebras reports windowed rate-limit headers (x-ratelimit-limit-tokens-day and friends), so calibration gets exact per-key numbers after the first request.",
|
||||
"models": [
|
||||
{ "id": "llama-3.3-70b", "label": "Llama 3.3 70B", "alias": ["fast", "big"], "context": 65536, "caps": ["chat", "tools", "json"], "priority": 10 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"$schema": "./_schema.json",
|
||||
"id": "cloudflare",
|
||||
"label": "Cloudflare Workers AI",
|
||||
"docs": "https://developers.cloudflare.com/workers-ai/models/",
|
||||
"console": "https://dash.cloudflare.com/profile/api-tokens",
|
||||
"enabled": true,
|
||||
"priority": 20,
|
||||
"adapter": "openai",
|
||||
"baseUrl": "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1",
|
||||
"auth": {
|
||||
"type": "bearer",
|
||||
"envKeys": ["CLOUDFLARE_API_TOKEN"]
|
||||
},
|
||||
"limits": {
|
||||
"creditsPerDay": 10000,
|
||||
"creditsPerRequest": 15
|
||||
},
|
||||
"limitsSource": "docs",
|
||||
"verifiedOn": "2026-08-16",
|
||||
"modelsEndpoint": null,
|
||||
"modelsPath": "data[].id",
|
||||
"dropParams": [],
|
||||
"headers": {},
|
||||
"notes": "Free tier is 10,000 neurons/day (confirmed on the pricing page), metered per token rather than per request. Per-model neuron rates come straight from Cloudflare's published table; note output tokens cost roughly 7-10x input, which is why rates are per-model rather than one flat number. limits.creditsPerRequest is only a fallback for models without published rates. Model ids contain '/', which is why provider/model pins split on the FIRST slash only. The OpenAI-compatible surface exposes /chat/completions and /embeddings but not /models, hence modelsEndpoint: null.",
|
||||
"models": [
|
||||
{
|
||||
"id": "@cf/meta/llama-3.1-8b-instruct",
|
||||
"label": "Llama 3.1 8B Instruct",
|
||||
"alias": ["cheap"],
|
||||
"context": 8192,
|
||||
"caps": ["chat", "code"],
|
||||
"priority": 30,
|
||||
"credits": { "perMTokIn": 2457, "perMTokOut": 18252 }
|
||||
},
|
||||
{
|
||||
"id": "@cf/meta/llama-3.3-70b-instruct-fp8-fast",
|
||||
"label": "Llama 3.3 70B Instruct (fp8 fast)",
|
||||
"alias": ["big"],
|
||||
"context": 24000,
|
||||
"caps": ["chat", "tools", "code"],
|
||||
"priority": 20,
|
||||
"credits": { "perMTokIn": 26668, "perMTokOut": 204805 }
|
||||
},
|
||||
{
|
||||
"id": "@cf/meta/llama-3.2-3b-instruct",
|
||||
"label": "Llama 3.2 3B Instruct",
|
||||
"alias": [],
|
||||
"context": 8192,
|
||||
"caps": ["chat"],
|
||||
"priority": 40,
|
||||
"credits": { "perMTokIn": 2457, "perMTokOut": 18252 }
|
||||
},
|
||||
{
|
||||
"id": "@cf/mistralai/mistral-small-3.1-24b-instruct",
|
||||
"label": "Mistral Small 3.1 24B",
|
||||
"alias": [],
|
||||
"context": 128000,
|
||||
"caps": ["chat", "tools", "long-context"],
|
||||
"priority": 25
|
||||
},
|
||||
{
|
||||
"id": "@cf/zhipu/glm-4.7-flash",
|
||||
"label": "GLM 4.7 Flash",
|
||||
"alias": [],
|
||||
"context": 131072,
|
||||
"caps": ["chat", "tools", "reasoning", "long-context"],
|
||||
"priority": 25
|
||||
},
|
||||
{
|
||||
"id": "@cf/baai/bge-large-en-v1.5",
|
||||
"label": "BGE Large EN v1.5",
|
||||
"alias": ["embed-large"],
|
||||
"context": 512,
|
||||
"caps": ["embed"],
|
||||
"priority": 20,
|
||||
"credits": { "perMTokIn": 18582 }
|
||||
},
|
||||
{
|
||||
"id": "@cf/baai/bge-small-en-v1.5",
|
||||
"label": "BGE Small EN v1.5",
|
||||
"alias": [],
|
||||
"context": 512,
|
||||
"caps": ["embed"],
|
||||
"priority": 30,
|
||||
"credits": { "perMTokIn": 1841 }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "./_schema.json",
|
||||
"id": "github-models",
|
||||
"label": "GitHub Models",
|
||||
"docs": "https://docs.github.com/en/github-models",
|
||||
"console": "https://github.com/settings/tokens",
|
||||
"priority": 45,
|
||||
"baseUrl": "https://models.github.ai/inference",
|
||||
"auth": { "type": "bearer", "envKeys": ["GITHUB_MODELS_TOKEN", "GITHUB_TOKEN"] },
|
||||
"limits": { "rpm": null, "rpd": null },
|
||||
"limitsSource": "unverified",
|
||||
"verifiedOn": "2026-08-16",
|
||||
"modelsEndpoint": "/models",
|
||||
"modelsPath": "[].id",
|
||||
"notes": "Free for anyone with a GitHub account; needs a PAT with the models:read scope. Limits are tiered by account and not published as flat numbers, hence nulls — calibration learns them. Note modelsPath is [].id: the catalog is a bare array, not an OpenAI {data:[...]} envelope.",
|
||||
"models": []
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "./_schema.json",
|
||||
"id": "google-ai",
|
||||
"label": "Google AI Studio",
|
||||
"docs": "https://ai.google.dev/gemini-api/docs/openai",
|
||||
"console": "https://aistudio.google.com/apikey",
|
||||
"priority": 25,
|
||||
"baseUrl": "https://generativelanguage.googleapis.com/v1beta/openai",
|
||||
"auth": { "type": "bearer", "envKeys": ["GOOGLE_AI_API_KEY", "GEMINI_API_KEY"] },
|
||||
"limits": { "rpm": 15, "rpd": 1500 },
|
||||
"limitsSource": "docs",
|
||||
"verifiedOn": "2026-08-16",
|
||||
"modelsEndpoint": "/models",
|
||||
"modelsPath": "data[].id",
|
||||
"notes": "The only free tier with native multimodal input across text, image, audio and video in one call. IMPORTANT: the free tier is not available in the EU, UK or Switzerland. Limits shown are for Flash; Pro models are far tighter (~2 rpm).",
|
||||
"models": [
|
||||
{ "id": "gemini-2.5-flash", "label": "Gemini 2.5 Flash", "alias": ["fast", "vision"], "context": 1048576, "caps": ["chat", "tools", "json", "vision", "long-context"], "priority": 10 },
|
||||
{ "id": "text-embedding-004", "label": "Text Embedding 004", "alias": ["embed"], "context": 2048, "caps": ["embed"], "priority": 20 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "./_schema.json",
|
||||
"id": "groq",
|
||||
"label": "Groq",
|
||||
"docs": "https://console.groq.com/docs/models",
|
||||
"console": "https://console.groq.com/keys",
|
||||
"priority": 15,
|
||||
"baseUrl": "https://api.groq.com/openai/v1",
|
||||
"auth": { "type": "bearer", "envKeys": ["GROQ_API_KEY", "GROQ_API_KEY_2"] },
|
||||
"limits": { "rpm": 30, "rpd": 14400, "tpm": 6000 },
|
||||
"limitsSource": "docs",
|
||||
"verifiedOn": "2026-08-16",
|
||||
"modelsEndpoint": "/models",
|
||||
"modelsPath": "data[].id",
|
||||
"notes": "LPU inference, very fast. Free limits vary per model — the numbers here are the commonly published floor, and the guard layer replaces them with per-key values read from x-ratelimit-* response headers. Run scripts/verify-provider.mjs groq to populate the real model list.",
|
||||
"models": [
|
||||
{ "id": "llama-3.3-70b-versatile", "label": "Llama 3.3 70B", "alias": ["fast", "big"], "context": 131072, "caps": ["chat", "tools", "json", "long-context"], "priority": 10 },
|
||||
{ "id": "llama-3.1-8b-instant", "label": "Llama 3.1 8B Instant", "alias": ["cheap"], "context": 131072, "caps": ["chat", "tools", "json"], "priority": 20 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "./_schema.json",
|
||||
"id": "huggingface",
|
||||
"label": "Hugging Face Router",
|
||||
"docs": "https://huggingface.co/docs/inference-providers",
|
||||
"console": "https://huggingface.co/settings/tokens",
|
||||
"priority": 50,
|
||||
"baseUrl": "https://router.huggingface.co/v1",
|
||||
"auth": { "type": "bearer", "envKeys": ["HF_TOKEN", "HUGGINGFACE_API_KEY"] },
|
||||
"limits": { "rpm": null },
|
||||
"limitsSource": "unverified",
|
||||
"verifiedOn": "2026-08-16",
|
||||
"modelsEndpoint": "/models",
|
||||
"modelsPath": "data[].id",
|
||||
"notes": "Routes to whichever inference provider currently serves a model, so availability moves around. Free monthly credits are small; treat this as overflow rather than a primary. Model ids are vendor/model, handled by the first-slash pin rule.",
|
||||
"models": []
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
{
|
||||
"$schema": "./_schema.json",
|
||||
"id": "mistral",
|
||||
"label": "Mistral AI",
|
||||
"docs": "https://docs.mistral.ai/getting-started/models/models_overview/",
|
||||
"console": "https://console.mistral.ai/api-keys",
|
||||
"enabled": true,
|
||||
"priority": 10,
|
||||
"adapter": "openai",
|
||||
"baseUrl": "https://api.mistral.ai/v1",
|
||||
"auth": {
|
||||
"type": "bearer",
|
||||
"envKeys": [
|
||||
"MISTRAL_API_KEY",
|
||||
"MISTRAL_API_KEY_2"
|
||||
]
|
||||
},
|
||||
"limits": {
|
||||
"tpmo": 1000000000,
|
||||
"tpm": 25000
|
||||
},
|
||||
"limitsSource": "observed",
|
||||
"verifiedOn": "2026-08-17",
|
||||
"modelsEndpoint": "/models",
|
||||
"modelsPath": "data[].id",
|
||||
"dropParams": [],
|
||||
"headers": {},
|
||||
"notes": "Free 'Experiment' plan: phone verification required, no credit card. Model ids are -latest aliases confirmed against a live GET /models on 2026-08-17 — deliberately NOT the dated ids from the docs overview page, several of which return 'Invalid model' on the API. Prefer -latest: it survives Mistral retiring a dated version. tpm was observed from x-ratelimit headers on /chat/completions (Mistral sends none on /models). Regenerate with: npm run verify-provider mistral -- --validate --apply",
|
||||
"models": [
|
||||
{
|
||||
"id": "mistral-small-latest",
|
||||
"label": "Mistral Small",
|
||||
"alias": [
|
||||
"fast",
|
||||
"chat"
|
||||
],
|
||||
"context": 128000,
|
||||
"caps": [
|
||||
"chat",
|
||||
"tools",
|
||||
"json",
|
||||
"code"
|
||||
],
|
||||
"priority": 10
|
||||
},
|
||||
{
|
||||
"id": "mistral-medium-latest",
|
||||
"label": "Mistral Medium",
|
||||
"alias": [
|
||||
"smart"
|
||||
],
|
||||
"context": 128000,
|
||||
"caps": [
|
||||
"chat",
|
||||
"tools",
|
||||
"json",
|
||||
"code",
|
||||
"vision"
|
||||
],
|
||||
"priority": 15
|
||||
},
|
||||
{
|
||||
"id": "mistral-large-latest",
|
||||
"label": "Mistral Large",
|
||||
"alias": [
|
||||
"big"
|
||||
],
|
||||
"context": 128000,
|
||||
"caps": [
|
||||
"chat",
|
||||
"tools",
|
||||
"json",
|
||||
"code",
|
||||
"vision"
|
||||
],
|
||||
"priority": 20
|
||||
},
|
||||
{
|
||||
"id": "ministral-8b-latest",
|
||||
"label": "Ministral 8B",
|
||||
"alias": [
|
||||
"cheap"
|
||||
],
|
||||
"context": 128000,
|
||||
"caps": [
|
||||
"chat",
|
||||
"tools"
|
||||
],
|
||||
"priority": 30
|
||||
},
|
||||
{
|
||||
"id": "ministral-3b-latest",
|
||||
"label": "Ministral 3B",
|
||||
"alias": [],
|
||||
"context": 128000,
|
||||
"caps": [
|
||||
"chat",
|
||||
"tools"
|
||||
],
|
||||
"priority": 40
|
||||
},
|
||||
{
|
||||
"id": "magistral-small-latest",
|
||||
"label": "Magistral Small (reasoning)",
|
||||
"alias": [
|
||||
"reason"
|
||||
],
|
||||
"context": 128000,
|
||||
"caps": [
|
||||
"chat",
|
||||
"reasoning"
|
||||
],
|
||||
"priority": 25
|
||||
},
|
||||
{
|
||||
"id": "codestral-latest",
|
||||
"label": "Codestral",
|
||||
"alias": [
|
||||
"code"
|
||||
],
|
||||
"context": 256000,
|
||||
"caps": [
|
||||
"chat",
|
||||
"code"
|
||||
],
|
||||
"priority": 20
|
||||
},
|
||||
{
|
||||
"id": "devstral-medium-latest",
|
||||
"label": "Devstral Medium",
|
||||
"alias": [],
|
||||
"context": 128000,
|
||||
"caps": [
|
||||
"chat",
|
||||
"tools",
|
||||
"code"
|
||||
],
|
||||
"priority": 35
|
||||
},
|
||||
{
|
||||
"id": "mistral-embed",
|
||||
"label": "Mistral Embed",
|
||||
"alias": [
|
||||
"embed"
|
||||
],
|
||||
"context": 8000,
|
||||
"caps": [
|
||||
"embed"
|
||||
],
|
||||
"priority": 10
|
||||
},
|
||||
{
|
||||
"id": "mistral-medium-2505",
|
||||
"label": "mistral-medium-2505",
|
||||
"alias": [],
|
||||
"caps": [
|
||||
"chat"
|
||||
],
|
||||
"context": null,
|
||||
"priority": 50
|
||||
},
|
||||
{
|
||||
"id": "mistral-medium-2508",
|
||||
"label": "mistral-medium-2508",
|
||||
"alias": [],
|
||||
"caps": [
|
||||
"chat"
|
||||
],
|
||||
"context": null,
|
||||
"priority": 50
|
||||
},
|
||||
{
|
||||
"id": "codestral-2508",
|
||||
"label": "codestral-2508",
|
||||
"alias": [],
|
||||
"caps": [
|
||||
"chat",
|
||||
"code"
|
||||
],
|
||||
"context": null,
|
||||
"priority": 50
|
||||
},
|
||||
{
|
||||
"id": "mistral-code-latest",
|
||||
"label": "mistral-code-latest",
|
||||
"alias": [],
|
||||
"caps": [
|
||||
"chat",
|
||||
"code"
|
||||
],
|
||||
"context": null,
|
||||
"priority": 50
|
||||
},
|
||||
{
|
||||
"id": "mistral-code-fim-latest",
|
||||
"label": "mistral-code-fim-latest",
|
||||
"alias": [],
|
||||
"caps": [
|
||||
"chat",
|
||||
"code"
|
||||
],
|
||||
"context": null,
|
||||
"priority": 50
|
||||
},
|
||||
{
|
||||
"id": "devstral-2512",
|
||||
"label": "devstral-2512",
|
||||
"alias": [],
|
||||
"caps": [
|
||||
"chat",
|
||||
"code"
|
||||
],
|
||||
"context": null,
|
||||
"priority": 50
|
||||
},
|
||||
{
|
||||
"id": "devstral-latest",
|
||||
"label": "devstral-latest",
|
||||
"alias": [],
|
||||
"caps": [
|
||||
"chat",
|
||||
"code"
|
||||
],
|
||||
"context": null,
|
||||
"priority": 50
|
||||
},
|
||||
{
|
||||
"id": "mistral-small-2603",
|
||||
"label": "mistral-small-2603",
|
||||
"alias": [],
|
||||
"caps": [
|
||||
"chat"
|
||||
],
|
||||
"context": null,
|
||||
"priority": 50
|
||||
},
|
||||
{
|
||||
"id": "mistral-vibe-cli-fast",
|
||||
"label": "mistral-vibe-cli-fast",
|
||||
"alias": [],
|
||||
"caps": [
|
||||
"chat"
|
||||
],
|
||||
"context": null,
|
||||
"priority": 50
|
||||
},
|
||||
{
|
||||
"id": "mistral-large-2512",
|
||||
"label": "mistral-large-2512",
|
||||
"alias": [],
|
||||
"caps": [
|
||||
"chat"
|
||||
],
|
||||
"context": null,
|
||||
"priority": 50
|
||||
},
|
||||
{
|
||||
"id": "ministral-3b-2512",
|
||||
"label": "ministral-3b-2512",
|
||||
"alias": [],
|
||||
"caps": [
|
||||
"chat"
|
||||
],
|
||||
"context": null,
|
||||
"priority": 50
|
||||
},
|
||||
{
|
||||
"id": "ministral-8b-2512",
|
||||
"label": "ministral-8b-2512",
|
||||
"alias": [],
|
||||
"caps": [
|
||||
"chat"
|
||||
],
|
||||
"context": null,
|
||||
"priority": 50
|
||||
},
|
||||
{
|
||||
"id": "ministral-14b-2512",
|
||||
"label": "ministral-14b-2512",
|
||||
"alias": [],
|
||||
"caps": [
|
||||
"chat"
|
||||
],
|
||||
"context": null,
|
||||
"priority": 50
|
||||
},
|
||||
{
|
||||
"id": "ministral-14b-latest",
|
||||
"label": "ministral-14b-latest",
|
||||
"alias": [],
|
||||
"caps": [
|
||||
"chat"
|
||||
],
|
||||
"context": null,
|
||||
"priority": 50
|
||||
},
|
||||
{
|
||||
"id": "mistral-medium",
|
||||
"label": "mistral-medium",
|
||||
"alias": [],
|
||||
"caps": [
|
||||
"chat"
|
||||
],
|
||||
"context": null,
|
||||
"priority": 50
|
||||
},
|
||||
{
|
||||
"id": "mistral-medium-3-5",
|
||||
"label": "mistral-medium-3-5",
|
||||
"alias": [],
|
||||
"caps": [
|
||||
"chat"
|
||||
],
|
||||
"context": null,
|
||||
"priority": 50
|
||||
},
|
||||
{
|
||||
"id": "mistral-medium-3.5",
|
||||
"label": "mistral-medium-3.5",
|
||||
"alias": [],
|
||||
"caps": [
|
||||
"chat"
|
||||
],
|
||||
"context": null,
|
||||
"priority": 50
|
||||
},
|
||||
{
|
||||
"id": "mistral-medium-3",
|
||||
"label": "mistral-medium-3",
|
||||
"alias": [],
|
||||
"caps": [
|
||||
"chat"
|
||||
],
|
||||
"context": null,
|
||||
"priority": 50
|
||||
},
|
||||
{
|
||||
"id": "mistral-medium-2604",
|
||||
"label": "mistral-medium-2604",
|
||||
"alias": [],
|
||||
"caps": [
|
||||
"chat"
|
||||
],
|
||||
"context": null,
|
||||
"priority": 50
|
||||
},
|
||||
{
|
||||
"id": "mistral-vibe-cli-latest",
|
||||
"label": "mistral-vibe-cli-latest",
|
||||
"alias": [],
|
||||
"caps": [
|
||||
"chat"
|
||||
],
|
||||
"context": null,
|
||||
"priority": 50
|
||||
},
|
||||
{
|
||||
"id": "mistral-vibe-cli-with-tools",
|
||||
"label": "mistral-vibe-cli-with-tools",
|
||||
"alias": [],
|
||||
"caps": [
|
||||
"chat"
|
||||
],
|
||||
"context": null,
|
||||
"priority": 50
|
||||
},
|
||||
{
|
||||
"id": "mistral-embed-2312",
|
||||
"label": "mistral-embed-2312",
|
||||
"alias": [],
|
||||
"caps": [
|
||||
"embed"
|
||||
],
|
||||
"context": null,
|
||||
"priority": 50
|
||||
},
|
||||
{
|
||||
"id": "codestral-embed",
|
||||
"label": "codestral-embed",
|
||||
"alias": [],
|
||||
"caps": [
|
||||
"embed"
|
||||
],
|
||||
"context": null,
|
||||
"priority": 50
|
||||
},
|
||||
{
|
||||
"id": "codestral-embed-2505",
|
||||
"label": "codestral-embed-2505",
|
||||
"alias": [],
|
||||
"caps": [
|
||||
"embed"
|
||||
],
|
||||
"context": null,
|
||||
"priority": 50
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "./_schema.json",
|
||||
"id": "nvidia-nim",
|
||||
"label": "NVIDIA NIM",
|
||||
"docs": "https://docs.api.nvidia.com/nim/",
|
||||
"console": "https://build.nvidia.com/",
|
||||
"priority": 50,
|
||||
"baseUrl": "https://integrate.api.nvidia.com/v1",
|
||||
"auth": { "type": "bearer", "envKeys": ["NVIDIA_API_KEY"] },
|
||||
"limits": { "rpm": null },
|
||||
"limitsSource": "unverified",
|
||||
"verifiedOn": "2026-08-16",
|
||||
"modelsEndpoint": "/models",
|
||||
"modelsPath": "data[].id",
|
||||
"notes": "Free developer credits rather than a perpetual free tier — it runs out. Model ids are vendor/model.",
|
||||
"models": []
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "./_schema.json",
|
||||
"id": "ollama",
|
||||
"label": "Ollama (local)",
|
||||
"docs": "https://github.com/ollama/ollama/blob/main/docs/openai.md",
|
||||
"console": "https://ollama.com/library",
|
||||
"priority": 90,
|
||||
"baseUrl": "${OLLAMA_HOST}/v1",
|
||||
"auth": { "type": "none", "envKeys": [] },
|
||||
"limits": {},
|
||||
"limitsSource": "docs",
|
||||
"verifiedOn": "2026-08-16",
|
||||
"modelsEndpoint": "/models",
|
||||
"modelsPath": "data[].id",
|
||||
"notes": "Your own machine, so there is no quota to meter and no key to hold — the point of auth.type: none. Set OLLAMA_HOST=http://127.0.0.1:11434 to enable. Priority 90 keeps it as the last resort, i.e. the thing that still answers when every hosted free tier is exhausted. Leave models empty; discovery lists whatever you have pulled.",
|
||||
"models": []
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "./_schema.json",
|
||||
"id": "openrouter",
|
||||
"label": "OpenRouter (free models)",
|
||||
"docs": "https://openrouter.ai/docs",
|
||||
"console": "https://openrouter.ai/keys",
|
||||
"priority": 40,
|
||||
"baseUrl": "https://openrouter.ai/api/v1",
|
||||
"auth": { "type": "bearer", "envKeys": ["OPENROUTER_API_KEY"] },
|
||||
"limits": { "rpm": 20, "rpd": 50 },
|
||||
"limitsSource": "unverified",
|
||||
"verifiedOn": "2026-08-16",
|
||||
"modelsEndpoint": "/models",
|
||||
"modelsPath": "data[].id",
|
||||
"headers": { "HTTP-Referer": "https://github.com/freeway", "X-Title": "Freeway" },
|
||||
"notes": "Only :free-suffixed models cost nothing. Free daily request caps depend on account credit history, so limits here are unverified on purpose. Model ids contain a slash (vendor/model), which the first-slash pin rule handles.",
|
||||
"models": [
|
||||
{ "id": "meta-llama/llama-3.3-70b-instruct:free", "label": "Llama 3.3 70B (free)", "alias": ["big"], "context": 65536, "caps": ["chat", "tools"], "priority": 10 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "./_schema.json",
|
||||
"id": "scaleway",
|
||||
"label": "Scaleway Generative APIs",
|
||||
"docs": "https://www.scaleway.com/en/docs/generative-apis/",
|
||||
"console": "https://console.scaleway.com/iam/api-keys",
|
||||
"priority": 55,
|
||||
"baseUrl": "https://api.scaleway.ai/v1",
|
||||
"auth": { "type": "bearer", "envKeys": ["SCALEWAY_API_KEY"] },
|
||||
"limits": { "rpm": null, "tpm": null },
|
||||
"limitsSource": "unverified",
|
||||
"verifiedOn": "2026-08-16",
|
||||
"modelsEndpoint": "/models",
|
||||
"modelsPath": "data[].id",
|
||||
"notes": "EU-hosted (Paris), useful when data residency matters. Free tier exists but the published numbers move; nulls plus calibration are more honest than a snapshot.",
|
||||
"models": []
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "./_schema.json",
|
||||
"id": "together",
|
||||
"label": "Together AI",
|
||||
"docs": "https://docs.together.ai/docs/inference-models",
|
||||
"console": "https://api.together.xyz/settings/api-keys",
|
||||
"priority": 45,
|
||||
"baseUrl": "https://api.together.xyz/v1",
|
||||
"auth": { "type": "bearer", "envKeys": ["TOGETHER_API_KEY"] },
|
||||
"limits": { "rpm": 60 },
|
||||
"limitsSource": "unverified",
|
||||
"verifiedOn": "2026-08-16",
|
||||
"modelsEndpoint": "/models",
|
||||
"modelsPath": "[].id",
|
||||
"notes": "Free tier is limited to models with a -Free suffix; everything else bills. Leaving models empty on purpose so discovery reports what your key can actually reach rather than guessing at a moving list.",
|
||||
"models": []
|
||||
}
|
||||
Reference in new issue
Block a user