How to View the Full Attempt Trail for Failed Requests in FreeLLMAPI
Yes, FreeLLMAPI captures every routing attempt—including failures—via the AttemptTrace system and persists them to the database for later inspection.
FreeLLMAPI is an open-source LLM routing engine that automatically fails over between multiple providers when rate limits or errors occur. Understanding exactly which providers were tried and why each attempt failed is critical for debugging production issues. This article explains how the platform maintains a full attempt trail for failed requests using the AttemptTrace context system and persistent storage.
How FreeLLMAPI Records Failed Request Attempts
When a request enters the system, the framework creates an AttemptTrace instance stored in AsyncLocalStorage. This context survives across the entire failover ladder, accumulating a record for every provider attempt whether it succeeds or fails.
The AttemptTrace Context System
The AttemptTrace class defined in server/src/lib/attempt-trace.ts serves as the backbone for request tracing. As the fallback loop dispatches each provider call, the system appends an AttemptTraceRecord containing:
- Platform (e.g., "openrouter", "groq")
- Model ID and key ordinal/label
- Outcome status (ok, rate_limited, error, timeout)
- Timing metrics (start offset and duration in milliseconds)
- Redacted error summary for diagnostics
Because the trace lives in AsyncLocalStorage, it maintains state across asynchronous boundaries throughout the request lifecycle.
Logging and Linking Request IDs
In server/src/lib/request-log.ts, the logRequest() function writes the terminal request row to the requests table. If an active AttemptTrace exists, the code calls noteRequestRowId() to capture the database ID of the request. This link ensures that subsequent attempt records can be associated with the correct parent request, even when the final outcome is an error.
Persisting the Complete Failover Chain
Once the response finishes (success or failure), the persistRequestAttempts() function writes every AttemptTraceRecord to the request_attempts table. This operation happens off the client-latency path, guaranteeing that every attempt in the failover chain is stored, including those that encountered rate limits, timeouts, or provider errors.
Database Schema for Request Attempts
The attempt trail schema is defined in server/src/db/migrations/20260726_000002_request_attempts.ts. The table structure includes:
request_id: Foreign key linking torequests.idordinal: The zero-based position in the failover sequenceplatformandmodel_id: Target provider and modelkey_ordinalandkey_label: Which API key was usedoutcome: Final status of the specific attemptstart_offset_msandduration_ms: Performance timingerror_summary: Sanitized error message (if applicable)
This schema ensures comprehensive auditability for every hop a request makes through the provider ladder.
Retrieving the Full Attempt Trail
Via the Analytics API
The primary method for accessing attempt data is the /v1/analytics/requests/:id endpoint defined in server/src/routes/analytics.ts. This route performs a SQL join between the requests and request_attempts tables, returning a JSON payload that includes the full attempts array.
GET https://api.free-llmapi.com/v1/analytics/requests/42?includeAttempts=true
Accept: application/json
Authorization: Bearer <YOUR_API_KEY>
Sample response:
{
"id": 42,
"status": "failed",
"platform": "openrouter",
"model_id": "meta-llama/Meta-Llama-3.1-70B-Instruct",
"attempts": [
{
"ordinal": 0,
"platform": "openrouter",
"model_id": "meta-llama/Meta-Llama-3.1-70B-Instruct",
"key_ordinal": 1,
"key_label": "OpenRouter-Key-A",
"outcome": "rate_limited",
"start_offset_ms": 0,
"duration_ms": 312,
"error_summary": "Rate limit exceeded"
},
{
"ordinal": 1,
"platform": "groq",
"model_id": "mixtral-8x7b-32768",
"key_ordinal": 2,
"key_label": "Groq-Key-B",
"outcome": "ok",
"start_offset_ms": 312,
"duration_ms": 874,
"error_summary": null
}
],
"error": {
"message": "Provider error (openrouter): rate limit exceeded",
"type": "rate_limit_error"
}
}
The dashboard UI consumes this same endpoint to render the "Fallback detail" panel, visualizing the exact path a request took through the provider ladder.
Direct Database Queries
For internal debugging or custom analytics, query the request_attempts table directly using SQLite:
SELECT
ra.ordinal,
ra.platform,
ra.model_id,
ra.key_ordinal,
ra.key_label,
ra.outcome,
ra.start_offset_ms,
ra.duration_ms,
ra.error_summary
FROM request_attempts ra
WHERE ra.request_id = 42
ORDER BY ra.ordinal;
Building Custom Endpoints
If you require a different view of the attempt data, you can extend the Express router. Create a new route file and query the database using the prepared statement pattern:
// In server/src/routes/custom.ts
import { Router } from 'express';
import { getDb } from '../db/index.js';
const router = Router();
router.get('/request/:id/attempts', async (req, res) => {
const db = getDb();
const rows = db.prepare(`
SELECT * FROM request_attempts
WHERE request_id = ?
ORDER BY ordinal
`).all(req.params.id);
res.json(rows);
});
export default router;
Register this router in your main server configuration to expose the custom endpoint.
Summary
- Every attempt is traced: FreeLLMAPI creates an
AttemptTraceviaAsyncLocalStorageat the start of each request and records every provider hop inserver/src/lib/attempt-trace.ts. - Failed attempts persist: The
persistRequestAttempts()function inserver/src/lib/request-log.tswrites all attempts—including failures—to therequest_attemptstable without blocking the response. - Schema supports forensics: The migration in
server/src/db/migrations/20260726_000002_request_attempts.tsstores platform, model, key usage, timing, and error summaries. - Multiple access patterns: Retrieve trails via the
/v1/analytics/requests/:idendpoint inserver/src/routes/analytics.tsor query the SQLite database directly.
Frequently Asked Questions
Does FreeLLMAPI log attempts that fail due to rate limits?
Yes. The AttemptTraceRecord captures attempts with outcome values like rate_limited, timeout, or error. These records are persisted to request_attempts exactly like successful attempts, allowing you to see which keys hit limits and when the failover succeeded.
How does the system handle sensitive error details in the attempt trail?
The system stores a redacted error summary rather than full stack traces or raw provider responses. This balances diagnostic utility with security, ensuring that API keys or sensitive user data do not leak into the logs while still indicating the failure mode (e.g., "Rate limit exceeded" or "Invalid model").
Can I query the attempt trail for requests that timed out completely?
Yes. Even if the final request results in a timeout error returned to the client, all prior attempts (and any partial progress timing data) are preserved in the database. Query the request_attempts table filtered by request_id to reconstruct the full timeline.
Where is the attempt data stored before the request finishes?
During request processing, attempt data lives in an AsyncLocalStorage context managed by the AttemptTrace class. This in-memory storage survives across asynchronous provider calls orchestrated by server/src/lib/fallback-loop.ts. Only after the response completes does persistRequestAttempts() flush the data to the SQLite database.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →