mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
fix(voice): keep a stopped round's actions and follow-up memory
A reply stopped partway through a round now keeps the actions that ran, answering the calls it never reached as skipped, so the history stays paired for both providers. A failed typed turn no longer resets the pointer, referents and follow-up memory, since its conversation continues. The server now tells a rejected key (401, or Gemini's 400 with reason API_KEY_INVALID, matched exactly), denied access (403) and an account out of credit (OpenAI's 429 insufficient_quota) apart; each cools down for a minute and gets its own message in the chat.
This commit is contained in:
1 parent
9d4d7b3ee0
commit
0a8d824124
19 files changed
+238
-51
No files matched your search
@@ -14,17 +14,31 @@ const FAILURE = 'Could not connect. Try again.';
|
||||
// A rejected key is not retried upstream for this long, unless the key changes.
|
||||
const REJECTED_KEY_COOLDOWN_MS = 60_000;
|
||||
|
||||
// Gemini answers an invalid key with 400 and this reason, not 401 or 403.
|
||||
const KEY_REJECTED_REASON = 'API_KEY_INVALID';
|
||||
async function rejectsKey(response, signal) {
|
||||
if ([401, 403].includes(response.status)) return true;
|
||||
if (response.status !== 400) return false;
|
||||
// What an upstream failure says about the key or the account. Gemini answers
|
||||
// an invalid key with 400 and an ErrorInfo reason, not 401; OpenAI answers an
|
||||
// account without credit with 429 insufficient_quota, which waiting won't fix.
|
||||
// A 403 is the key's project, region or permissions, so it cools down too but
|
||||
// does not blame the key. Only small error bodies are read.
|
||||
async function upstreamRefusal(response, signal) {
|
||||
const { status } = response;
|
||||
if (status === 401) return 'API_KEY_REJECTED';
|
||||
if (status === 403) return 'ACCESS_DENIED';
|
||||
if (![400, 429].includes(status)) return null;
|
||||
let error;
|
||||
try {
|
||||
const body = await readResponseJsonCapped(response, 16 * 1024, signal);
|
||||
return JSON.stringify(body).includes(KEY_REJECTED_REASON);
|
||||
error = (await readResponseJsonCapped(response, 16 * 1024, signal))?.error;
|
||||
} catch {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
status === 400 &&
|
||||
Array.isArray(error?.details) &&
|
||||
error.details.some((detail) => detail?.reason === 'API_KEY_INVALID')
|
||||
)
|
||||
return 'API_KEY_REJECTED';
|
||||
if (status === 429 && error?.code === 'insufficient_quota')
|
||||
return 'QUOTA_EXCEEDED';
|
||||
return null;
|
||||
}
|
||||
|
||||
// Pass through only a delay in whole seconds, capped at an hour.
|
||||
@@ -106,7 +120,7 @@ export function createTextChatHandler({
|
||||
'Retry-After',
|
||||
String(Math.max(1, Math.ceil((rejected.until - now()) / 1000))),
|
||||
);
|
||||
return reply(502, FAILURE, 'API_KEY_REJECTED');
|
||||
return reply(502, FAILURE, rejected.code);
|
||||
}
|
||||
if (limiter === undefined)
|
||||
limiter = makeCostRateLimiter(
|
||||
@@ -156,10 +170,15 @@ export function createTextChatHandler({
|
||||
if (response.redirected || (response.url && response.url !== endpoint))
|
||||
throw new Error('Unexpected destination');
|
||||
if (!response.ok) {
|
||||
if (await rejectsKey(response, lifetime.signal)) {
|
||||
const refusal = await upstreamRefusal(response, lifetime.signal);
|
||||
if (refusal) {
|
||||
void response.body?.cancel().catch(() => {});
|
||||
rejected = { key: apiKey, until: now() + REJECTED_KEY_COOLDOWN_MS };
|
||||
return reply(502, FAILURE, 'API_KEY_REJECTED');
|
||||
rejected = {
|
||||
key: apiKey,
|
||||
code: refusal,
|
||||
until: now() + REJECTED_KEY_COOLDOWN_MS,
|
||||
};
|
||||
return reply(502, FAILURE, refusal);
|
||||
}
|
||||
void response.body?.cancel().catch(() => {});
|
||||
if (![429, 503].includes(response.status)) return reply(502, FAILURE);
|
||||
|
||||
Reference in new issue
Block a user