Handle long runs, rate limits and timeouts

What a 202 means, which errors to retry, how long to wait, and what is never charged.

Long Default mode runs: 202

A Default mode decision can take longer than one HTTP response: a large one takes about a minute, and a model waking from idle adds a few minutes. When it is not done after about 50 seconds, POST /v1/decisions answers 202 with {id, status: "running", progress, poll_url, retry_after}. The decision goes on running; poll GET /v1/decisions/{id} every retry_after seconds until its status is ok (the decide answer is in response) or error (errorMessage says why; nothing is billed). It is billed when it completes, also if you stop polling. Send "async": true to get the 202 at once. Up to 3 Default mode decisions of an organization run at once; another one gets 429 until one is done.

Under a key with retention "Not stored", the answer of a decision that went on after a 202 is held for you to fetch once, and deleted on that fetch or after one hour at the latest.

Retry

Retry these failures; none of them is charged.

Do not retry 400, 401, 402 or 409: the request itself needs fixing (Errors).

const headers = { Authorization: `Bearer ${process.env.EEVO_API_KEY}`, 'Content-Type': 'application/json' };
const sleep = seconds => new Promise(resolve => setTimeout(resolve, seconds * 1000));

// A decision still running (202): poll it until it is done.
async function waitFor(running) {
  for (;;) {
    await sleep(running.retry_after);
    const decision = await (await fetch(running.poll_url, { headers })).json();
    if (decision.status === 'ok') return decision.response;
    if (decision.status === 'error') throw new Error(decision.errorMessage);
  }
}

async function decide(body) {
  for (let attempt = 0; attempt < 5; attempt++) {
    const response = await fetch('https://eevo.io/api/v1/decisions', { method: 'POST', headers, body: JSON.stringify(body) });
    if (response.status === 202) return await waitFor(await response.json());
    if (response.ok) return await response.json();
    const { error } = await response.json();
    const retryable =
      ['free_capacity_busy', 'rate_limited'].includes(error.type) || response.status >= 502;
    if (!retryable) throw new Error(`${error.type}: ${error.message}`);
    await sleep(Number(response.headers.get('Retry-After') ?? 30));
  }
  throw new Error('Eevo did not answer after 5 attempts');
}