AI
AI Errors and Retries
Every GET /v1/ai error status, what it costs in AI credits, how to retry it, the latency budget, and what to include in a support request.
/v1/ai uses the standard CurrencyCore error envelope, plus a set of 200
responses that are not direct answers. This page separates the two, gives the
credit cost of each case, and sets a retry policy that does not burn credits.
Error envelope
{
"error": {
"code": "unsupported_query",
"message": "This endpoint only answers currency conversion, exchange-rate, and PPP questions."
}
}
code is stable and machine-readable. message is for humans. details is
optional and is not used by the AI route today.
Error matrix
| Status | Code | When it happens | AI credits | Retry? |
|---|---|---|---|---|
400 | invalid_input | q is missing or blank. | 0 | No. Send a question. |
401 | unauthorized | Key missing, invalid, or revoked. Revocation clears within 5 minutes because of the auth cache. | 0 | No. Fix the key. |
402 | billing_past_due | The organization’s billing is past due. | 0 | No. Clear payment. |
403 | forbidden | AI is disabled for the organization, or disabled platform-wide. | 0 | No. Enable it in the dashboard, or contact support if the platform switch is off. |
413 | invalid_input | q or total model input exceeds the token budget. Rejected before the model. | 0 | No. Shorten the question. |
422 | unsupported_query | The question is off-topic or an attempt to change the assistant’s behavior. The model was consulted. | 1 (native) | No. Reformulate as a currency, FX, or PPP question. |
429 | rate_limited | Per-minute rate limit, monthly request quota (BYOK tool-call units), or AI credit pool is exhausted. | 0 | Yes for the per-minute limit. For credits, wait or buy more. |
500 | internal_error | An unexpected server error. | 0 | Yes, with backoff. |
503 | internal_error | No provider configured, every provider unreachable, or BYOK required with fallback off but unusable. | 0 | Yes for a transient outage. Fix BYOK if it persists. |
A 429 from the per-minute rate limit includes Retry-After in seconds. A 429
from an exhausted AI credit pool may not include it, because waiting is not what
fixes it. Read the message.
A native 422 is metered because the model did real work before declining. A
413 is not metered because it is rejected before the model runs. 503 is not
metered. See AI credits for the pool rules.
A 200 is not always an answer
These cases return 200. There is no outcome or kind field in the body today,
so inspect data and answer.
| Body | Meaning | What to do |
|---|---|---|
data non-empty | A normal answer with structured results. | Render answer; use data for numbers. |
data: [], answer asks for a missing detail | Clarification. For example, a salary question with no amount, or a multi-country currency with no default. | Ask the user for the detail, then send a new request. |
data: [], answer explains missing data | In-scope question, but no rate, PPP factor, currency, or date in coverage. | Show the explanation. Do not retry the same question. |
data: [], answer asks you to upgrade | A Free or Starter organization asked for Growth+ capability. | Upgrade, or ask a single-date question. |
data: [], toolCalls: 0, answer asks you to narrow the request | The analysis hit the orchestration deadline. Not a 504. | Narrow the window or the currency set. |
data: [], answer declines a PPP ranking | Cross-country purchasing-power ranking is not answerable from PPP factors alone. | Ask for a pairwise PPP comparison instead. |
data: [], answer gives a one-line topic refusal | A related financial topic, such as gold or oil, that the fiat-only data does not cover. | Show the refusal. Use a source that covers that asset. |
data: [], answer is plain prose with no tool | The answer did not need a data lookup. | Treat it as informational, not as a source of numbers. |
A 200 with data: [] still consumes one AI credit on the native path, because
the model was consulted. A too-large analysis with toolCalls: 0 is the same:
one native credit.
Retry policy
GET /v1/ai is safe to retry at the HTTP level, but a successful retry is a new
request. On the native path it consumes another AI credit. With BYOK it bills
tool calls again. Do not retry a 200 just because you did not like the answer.
type RetryDecision = "retry" | "do-not-retry";
export function retryDecision(status: number): RetryDecision {
if (status === 429 || status === 500 || status === 503) return "retry";
return "do-not-retry";
}
export async function askWithRetry(
q: string,
key: string,
maxAttempts = 3,
): Promise<Response> {
let last: Response | undefined;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const url = new URL("https://api.currency-core.com/v1/ai");
url.searchParams.set("q", q);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${key}` },
signal: AbortSignal.timeout(30_000),
});
if (res.ok) return res;
last = res;
if (retryDecision(res.status) === "do-not-retry") return res;
const retryAfter = Number(res.headers.get("Retry-After"));
const waitMs = Number.isFinite(retryAfter) && retryAfter > 0
? retryAfter * 1_000
: Math.min(8_000, 500 * 2 ** (attempt - 1)) + Math.random() * 250;
if (attempt === maxAttempts) return res;
await new Promise((resolve) => setTimeout(resolve, waitMs));
}
return last!;
}
Rules:
- Do not retry
400,401,402,403,413, or422. The request will fail the same way, and a native422costs a credit each time. - Retry
429only afterRetry-After. If the message says the AI credit balance is exhausted, stop and buy credits, wait for the cycle, or use BYOK with fallback disabled. - Retry
500and503with jittered exponential backoff, a few attempts. - Do not retry a client-side timeout without a new decision. The server can take up to 15 seconds per attempt, and a lost response may still have been metered.
- Treat a
200as final. If you need a different window or shape, send a new prompt, not a retry of the same one.
Latency budget
| Budget | Default | Notes |
|---|---|---|
| Orchestration deadline | 15 seconds per attempt | AI_TIMEOUT_MS can change it for a deployment. |
| Per-provider timeout | 8 seconds | AI_PROVIDER_TIMEOUT_MS can change it. Must stay below the orchestration deadline. |
| Attempts | Up to 3 | Only for transient provider unavailability (network, 429, 5xx). Deterministic errors and deadline timeouts are not retried. |
| Client timeout | 30 seconds suggested | Covers the 15-second deadline plus retries and network overhead. |
If the orchestration deadline is reached and the model produced usable text, the
request can still return 200. If it produced no usable answer, the route
returns a structured 200 asking you to narrow the request, not a 504. A
genuine infrastructure failure returns 503.
There is no published p50 or p95 latency commitment. Simple conversions are normally faster than multi-currency analytics. A request that fans out to many currencies or a long window is the most likely to hit the deadline.
Request IDs
There is no request ID header on /v1/ai today. Do not look for
x-request-id or a requestId body field.
The route does return diagnostic headers you can capture:
x-currencycore-ai-byokx-currencycore-ai-providerx-currencycore-ai-attemptsx-currencycore-ai-toolsx-currencycore-ai-fastx-currencycore-ai-msServer-Timing
These identify routing and timing, not a specific log record. For support, pair them with the UTC time.
Support checklist
Include all of the following in a support request. Do not include the full API key.
- The UTC timestamp of the request, with the timezone.
- The HTTP status, error
code, and full errormessage. - The exact
qvalue, or a minimal version that still reproduces the issue. - The organization id, and the key prefix only (for example
cc_live_ab12). - Whether BYOK was active, and which provider id.
- The response headers listed above, especially
x-currencycore-ai-byok,x-currencycore-ai-provider, andx-currencycore-ai-attempts. - The full response body if it was a
200non-answer, with any sensitive text removed. - Whether the issue reproduces with the same
q, and roughly how often. - For BYOK: the result of Test connection, the model id, and whether fallback is on. Do not send the provider key.
A support request that says only “the AI gave a wrong answer” is hard to act on.
A request that includes the data object, the asOf date, and the REST response
you compared against is actionable.