Phase 3 — Add routes and fallbacks
Time: ~20 minutes
Prerequisites: Phase 2 complete (resolve client works, feature flag wired)
You'll need: Access to the Dashboard, your project and environment from Phase 1
This phase moves your routing decisions from "Restormel picks the default provider" to "Restormel evaluates a named route with multiple steps and fails over automatically." By the end, you have a fallback chain configured in the dashboard, and your resolve call returns results shaped by that chain.
Step 3.1 — Understand routes and steps
A route is a named routing configuration inside your project. It contains one or more steps, evaluated in order. Each step specifies a provider preference and an optional model. The route mode controls how steps are evaluated.
Route: "ingestion" Mode: fallback_chain Step 1 → OpenAI (gpt-4o) → try first Step 2 → Anthropic (claude-sonnet) → if step 1 fails Step 3 → Google (gemini-2.5-pro) → if step 2 fails
When your backend calls resolve with routeId: "ingestion", Restormel walks the chain. If the first step's provider has a valid key and is not blocked by a policy, it's returned. If not (no key, rate-limited, deprecated), Restormel tries the next step.
| Mode | Behaviour |
|---|---|
fallback_chain | Try steps in order; return the first that resolves successfully |
user_preferred | Use the user's preferred provider if a BYOK key exists, then fall back to the chain |
You can create multiple routes per project — for example, ingestion for background jobs and interactive for user-facing requests with different fallback priorities.
Step 3.2 — Create your first route in the Dashboard
- Name: Give the route a descriptive name (e.g.
ingestion,chat,interactive). This becomes therouteIdyou pass to resolve. - Route mode: Select
fallback_chain. - Save the route.
At the moment, the Dashboard UI shows the route and its steps list, but step editing is API-first. You create, reorder, and disable steps via the Steps API in Step 3.4a.
You'll see
The route detail page in the dashboard showing your named route, mode, and the steps in order. Each step shows the provider, model, and fallback condition.
How to test
No code change yet. Confirm the route appears in the dashboard and the steps are in the correct order.
Step 3.3 — Resolve with the route ID
Update your resolve call to specify the route. This tells Restormel to evaluate that route's fallback chain instead of the project default.
curl test:
curl -X POST \
"https://restormel.dev/keys/dashboard/api/projects/${RESTORMEL_PROJECT_ID}/resolve" \
-H "Authorization: Bearer ${RESTORMEL_GATEWAY_KEY}" \
-H "Content-Type: application/json" \
-d '{ "environmentId": "production", "routeId": "ingestion" }'In your resolve client:
const result = await restormelResolve({
environmentId: process.env.RESTORMEL_ENVIRONMENT_ID ?? 'production',
routeId: 'ingestion',
});routeId configurable per call site. For example, your ingestion pipeline passes routeId: 'ingestion' while your chat handler passes routeId: 'interactive'. This lets different parts of your app have different fallback strategies.You'll see
The resolve response now reflects the route's first available step: data.routeId, data.providerType, data.modelId, data.explanation.
How to test
curl -s -X POST "..." -d '{ "environmentId": "production", "routeId": "ingestion" }' | jq '.data.providerType, .data.modelId'Expected output: "openai" and "gpt-4o" (or whatever your first step is).
Step 3.4 — Test fallback behaviour
To confirm the fallback chain works, make the first step unusable and confirm resolve returns the next enabled step. You create and manage steps via the Steps API (or the dashboard when a full step editor is available).
Step 3.4a — Create steps via the Steps API
The Steps API is how you configure the fallback chain programmatically. You’ll use two identifiers:
- Route name (e.g.
ingestion) — used in your Resolve call (routeIdfield). - Route internal ID (UUID) — used in the Steps API URL. Copy it from the Dashboard URL when viewing the route:
/keys/dashboard/projects/{projectId}/routes/{routeInternalId}.
Step schema (create):
{
"orderIndex": 0,
"providerPreference": "openai",
"modelId": "gpt-4o",
"fallbackOn": "error",
"enabled": true
}Valid values:
providerPreference:openai | anthropic | cohere | google | deepseek | groq | mistral | openrouter | portkey | together | vercel | voyagefallbackOn:error | rate_limit | no_key | policy_block | any(defaults toerror)
Ordering: orderIndex is 0-based; lower indices are tried first.
Create a step (orderIndex 0):
curl -s -X POST \
"https://restormel.dev/keys/dashboard/api/projects/${RESTORMEL_PROJECT_ID}/routes/${RESTORMEL_ROUTE_INTERNAL_ID}/steps" \
-H "Authorization: Bearer ${RESTORMEL_GATEWAY_KEY}" \
-H "Content-Type: application/json" \
-d '{
"orderIndex": 0,
"providerPreference": "openai",
"modelId": "gpt-4o",
"fallbackOn": "error",
"enabled": true
}' | jq '.data'Create a second step with orderIndex: 1 and another provider/model.
List steps (ordered by orderIndex):
curl -s \
"https://restormel.dev/keys/dashboard/api/projects/${RESTORMEL_PROJECT_ID}/routes/${RESTORMEL_ROUTE_INTERNAL_ID}/steps" \
-H "Authorization: Bearer ${RESTORMEL_GATEWAY_KEY}" | jq '.data'Update a step (disable):
curl -s -X PATCH \
"https://restormel.dev/keys/dashboard/api/projects/${RESTORMEL_PROJECT_ID}/routes/${RESTORMEL_ROUTE_INTERNAL_ID}/steps/${RESTORMEL_STEP_ID}" \
-H "Authorization: Bearer ${RESTORMEL_GATEWAY_KEY}" \
-H "Content-Type: application/json" \
-d '{ "enabled": false }' | jq '.data'Delete a step:
curl -s -X DELETE \
"https://restormel.dev/keys/dashboard/api/projects/${RESTORMEL_PROJECT_ID}/routes/${RESTORMEL_ROUTE_INTERNAL_ID}/steps/${RESTORMEL_STEP_ID}" \
-H "Authorization: Bearer ${RESTORMEL_GATEWAY_KEY}"Step 3.4b — Disable the first step and re-resolve
Temporarily disable (or delete) the first step so resolve returns the second step. Then call resolve again with the same route.
You'll see
The resolve response returns the second step's provider (data.providerType and data.modelId). Restormel skipped the first step and fell through to the next.
How to test
After removing or disabling the first step's credential, call resolve again. Expected: "anthropic" (or your second step's provider). Re-enable (or re-create) the first step after testing.
getPlatformKey. The dashboard does not accept raw provider API keys today; it uses Provider Integrations (credential references).Step 3.5 — Wire route IDs into your application
Make the route ID configurable in your resolve wrapper so different parts of your app can use different routes.
export async function resolveProvider(options?: { routeId?: string; model?: string }) {
if (USE_RESTORMEL_KEYS) {
try {
const result = await restormelResolve({
environmentId: process.env.RESTORMEL_ENVIRONMENT_ID ?? 'production',
routeId: options?.routeId,
});
return {
provider: result.data.providerType ?? process.env.DEFAULT_AI_PROVIDER ?? 'openai',
model: result.data.modelId ?? options?.model ?? null,
source: 'restormel',
};
} catch (err) {
console.error('[restormel] Resolve failed, falling back to legacy:', err);
}
}
return legacyResolve(options?.model);
}Call sites: resolveProvider({ routeId: 'ingestion' }) for ingestion; resolveProvider({ routeId: 'interactive', model: userSelectedModel }) for chat.
You'll see
No visible change yet (the flag is still off by default). When you test with the flag on, different call sites resolve through different routes.
How to test
USE_RESTORMEL_KEYS=true pnpm dev — trigger an ingestion job (should resolve via ingestion route) and a chat request (should resolve via interactive if you created one).
Implementors: See “Agent prompts for this phase” below for a prompt you can paste into a coding agent.
Step 3.6 — (Optional) Create a second route
If your app has distinct AI call patterns — for example, fast-and-cheap for autocomplete vs. powerful-and-expensive for analysis — create separate routes with different step orders and models (e.g. autocomplete with gpt-4o-mini first, analysis with claude-sonnet first). Create both in the Dashboard, then use the appropriate routeId in each call site.
How to test
Resolve with routeId: "autocomplete" and routeId: "analysis"; jq '.data.providerType, .data.modelId' should show different providers/models.
Prompts for this phase
These are optional and collapsed by default. Use them if you're implementing Phase 3 with a coding agent.
Checkpoint checklist: mark each step complete as you finish it.
Checklist
Checkpoint
You now have: at least one named route in the Dashboard with multiple steps (fallback chain); your resolve client passes a routeId so different parts of your app use different routing strategies; fallback verified. The feature flag is still off by default. When on, your app resolves through Restormel routes.