Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions cli/openai-bridge-retry.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
const DEFAULT_BRIDGE_MAX_RETRIES = 2;
const MIN_BRIDGE_MAX_RETRIES = 2;
const MAX_BRIDGE_MAX_RETRIES = 10;
const DEFAULT_BRIDGE_MAX_RETRIES = Infinity;
const BASE_TRANSIENT_RETRY_DELAY_MS = 200;
const MAX_TRANSIENT_RETRY_DELAY_MS = 5000;

function normalizeBridgeMaxRetries(value, fallback = DEFAULT_BRIDGE_MAX_RETRIES) {
const raw = Number(value);
const fallbackRaw = Number(fallback);
const base = Number.isFinite(raw) ? raw : (Number.isFinite(fallbackRaw) ? fallbackRaw : DEFAULT_BRIDGE_MAX_RETRIES);
return Math.min(MAX_BRIDGE_MAX_RETRIES, Math.max(MIN_BRIDGE_MAX_RETRIES, Math.floor(base)));
if (Number.isFinite(raw) && raw >= 0) return Math.floor(raw);
if (Number.isFinite(fallbackRaw) && fallbackRaw >= 0) return Math.floor(fallbackRaw);
return DEFAULT_BRIDGE_MAX_RETRIES;
}


function isTransientHttpStatus(status) {
const code = Number(status);
return code === 408 || code === 409 || code === 425 || code === 429 || code === 500 || code === 502 || code === 503 || code === 504 || code === 520 || code === 521 || code === 522 || code === 523 || code === 524;
}

function isTransientNetworkError(error) {
Expand All @@ -30,7 +35,7 @@ function getTransientRetryDelayMs(attempt) {
async function retryTransientRequest(executor, options = {}) {
const maxRetries = normalizeBridgeMaxRetries(options && options.maxRetries);
let lastResult = null;
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
for (let attempt = 0; attempt === 0 || attempt <= maxRetries; attempt += 1) {
if (attempt > 0) {
const delay = getTransientRetryDelayMs(attempt);
// eslint-disable-next-line no-await-in-loop
Expand All @@ -43,20 +48,22 @@ async function retryTransientRequest(executor, options = {}) {
const result = await executor(attempt);
lastResult = result;
if (!result) return result;
if (result.status && result.status > 0) {
if (!isTransientHttpStatus(result.status)) return result;
continue;
}
if (result.ok) return result;
if (result.retry) return result;
if (result.status && result.status > 0) return result;
if (!isTransientNetworkError(result.error)) return result;
}
return lastResult;
}

module.exports = {
DEFAULT_BRIDGE_MAX_RETRIES,
MIN_BRIDGE_MAX_RETRIES,
MAX_BRIDGE_MAX_RETRIES,
normalizeBridgeMaxRetries,
isTransientNetworkError,
isTransientHttpStatus,
getTransientRetryDelayMs,
retryTransientRequest
};
30 changes: 20 additions & 10 deletions cli/openai-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ function normalizeOpenaiUpstreamBaseUrl(rawValue) {
}

const {
normalizeBridgeMaxRetries,
parseJsonOrError,
extractChatCompletionResult,
convertResponsesRequestToChatCompletions,
Expand All @@ -114,11 +113,10 @@ function normalizeUpstreamEntry(entry) {
const apiKey = normalizeText(entry.apiKey || entry.api_key || entry.key || '');
const headersRaw = entry.headers || entry.extraHeaders || entry.extra_headers || null;
const headers = normalizeHeadersMap(headersRaw);
const maxRetries = normalizeBridgeMaxRetries(entry.maxRetries ?? entry.max_retries);
if (!baseUrl || !isValidHttpUrl(baseUrl)) {
return null;
}
return { baseUrl, apiKey, headers, maxRetries };
return { baseUrl, apiKey, headers };
}

function normalizeHeadersMap(value) {
Expand Down Expand Up @@ -169,7 +167,6 @@ function upsertOpenaiBridgeProvider(filePath, providerName, upstreamBaseUrl, api
const baseUrl = normalizeOpenaiUpstreamBaseUrl(upstreamBaseUrl);
const key = normalizeText(apiKey);
const nextHeaders = normalizeHeadersMap(headers);
const maxRetries = normalizeBridgeMaxRetries(options && options.maxRetries);

if (!name) {
return { error: 'Provider name is required' };
Expand All @@ -191,7 +188,6 @@ function upsertOpenaiBridgeProvider(filePath, providerName, upstreamBaseUrl, api
baseUrl,
apiKey: key,
headers: Object.keys(nextHeaders).length ? nextHeaders : existingHeaders,
maxRetries
}
}
};
Expand Down Expand Up @@ -384,7 +380,7 @@ function createOpenaiBridgeHttpHandler(options = {}) {
maxBytes: maxUpstreamBytes,
httpAgent,
httpsAgent
}), { maxRetries: upstream.maxRetries });
}));
if (!result.ok) {
res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({ error: `Upstream request failed: ${result.error}` }));
Expand Down Expand Up @@ -446,7 +442,7 @@ function createOpenaiBridgeHttpHandler(options = {}) {
res,
model: typeof chatBody.model === 'string' ? chatBody.model : '',
toolTypesByName: converted.toolTypesByName || {}
}), { maxRetries: upstream.maxRetries });
}));
if (!streamed.ok) {
if (res.writableEnded || res.destroyed) {
return;
Expand All @@ -471,7 +467,7 @@ function createOpenaiBridgeHttpHandler(options = {}) {
maxBytes: maxUpstreamBytes,
httpAgent,
httpsAgent
}), { maxRetries: upstream.maxRetries });
}));
if (!upstreamResult.ok) {
res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({ error: `Upstream request failed: ${upstreamResult.error}` }));
Expand Down Expand Up @@ -514,8 +510,23 @@ function createOpenaiBridgeHttpHandler(options = {}) {
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify(ensureResponseMetadata(responsesPayload)));
} catch (e) {
if (res.writableEnded || res.destroyed) {
return;
}
const message = e && e.message ? e.message : 'Internal Error';
if (res.headersSent) {
try {
res.end();
} catch (_) {
// Headers are already committed. Close the socket instead of leaving the client waiting forever.
if (!res.destroyed && typeof res.destroy === 'function') {
res.destroy(e);
}
}
return;
}
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({ error: e && e.message ? e.message : 'Internal Error' }));
res.end(JSON.stringify({ error: message }));
}
})();

Expand Down Expand Up @@ -543,7 +554,6 @@ module.exports = {
extractChatCompletionResult,
buildResponsesPayloadFromChatResult,
retryTransientRequest,
normalizeBridgeMaxRetries,
normalizeOpenaiUpstreamBaseUrl,
extractResponsesOutputText,
shouldFallbackFromUpstreamResponses,
Expand Down
Loading
Loading