fix(gateway): return honest subscription bridge API status

This commit is contained in:
Rene Fichtmueller 2026-07-17 17:38:26 +02:00
parent 496975ed94
commit be8eda6430
2 changed files with 232 additions and 49 deletions

View File

@ -735,17 +735,114 @@ function chatMessagesToPrompt(messages: OpenAIChatCompletionRequest['messages'])
.trim();
}
async function callCodexBridgeChatCompletion(
body: OpenAIChatCompletionRequest,
startMs: number,
callId: string
): Promise<{ ok: true; body: Record<string, unknown> } | { ok: false; statusCode: number; error: string } | null> {
if (!isCodexBridgeModel(body.model)) return null;
interface SubscriptionBridgeTarget {
subscriptionId: string;
provider: string;
envKey: string;
defaultUrl: string;
}
const bridgeUrl = (process.env['CODEX_BRIDGE_URL'] ?? 'http://127.0.0.1:3253').replace(/\/$/, '');
const inputText = chatMessagesToPrompt(body.messages);
const SUBSCRIPTION_BRIDGE_TARGETS_BY_MODEL: Record<string, SubscriptionBridgeTarget> = {
'claude-opus-4-1': {
subscriptionId: 'claude-code',
provider: 'claude-bridge',
envKey: 'CLAUDE_BRIDGE_URL',
defaultUrl: 'http://127.0.0.1:3250',
},
'claude-sonnet-4-6': {
subscriptionId: 'claude-code',
provider: 'claude-bridge',
envKey: 'CLAUDE_BRIDGE_URL',
defaultUrl: 'http://127.0.0.1:3250',
},
'claude-haiku-3': {
subscriptionId: 'claude-code',
provider: 'claude-bridge',
envKey: 'CLAUDE_BRIDGE_URL',
defaultUrl: 'http://127.0.0.1:3250',
},
'microsoft-365-copilot': {
subscriptionId: 'microsoft-365-copilot',
provider: 'm365-copilot-bridge',
envKey: 'M365_COPILOT_BRIDGE_URL',
defaultUrl: 'http://127.0.0.1:3257',
},
'm365-copilot-chat': {
subscriptionId: 'microsoft-365-copilot',
provider: 'm365-copilot-bridge',
envKey: 'M365_COPILOT_BRIDGE_URL',
defaultUrl: 'http://127.0.0.1:3257',
},
'gemini-1.5-pro': {
subscriptionId: 'gemini',
provider: 'gemini-bridge',
envKey: 'GEMINI_BRIDGE_URL',
defaultUrl: 'http://127.0.0.1:3254',
},
'gemini-1.5-flash': {
subscriptionId: 'gemini',
provider: 'gemini-bridge',
envKey: 'GEMINI_BRIDGE_URL',
defaultUrl: 'http://127.0.0.1:3254',
},
'aider-default': {
subscriptionId: 'aider',
provider: 'aider-bridge',
envKey: 'AIDER_BRIDGE_URL',
defaultUrl: 'http://127.0.0.1:3256',
},
};
function subscriptionBridgeTargetForModel(model: string | undefined): SubscriptionBridgeTarget | null {
if (!model) return null;
if (isCodexBridgeModel(model)) {
return {
subscriptionId: 'codex',
provider: 'codex-bridge',
envKey: 'CODEX_BRIDGE_URL',
defaultUrl: 'http://127.0.0.1:3253',
};
}
return SUBSCRIPTION_BRIDGE_TARGETS_BY_MODEL[model.toLowerCase()] ?? null;
}
function subscriptionBridgeUrl(target: SubscriptionBridgeTarget): string {
return (process.env[target.envKey] ?? target.defaultUrl).replace(/\/$/, '');
}
function contentFromSubscriptionBridge(data: any): string {
return data?.choices?.[0]?.message?.content
?? data?.content
?? data?.response
?? data?.message?.content
?? '';
}
function bridgeErrorMessage(data: any, statusCode: number): string {
const value = data?.error ?? data?.message ?? data?.raw ?? `subscription bridge HTTP ${statusCode}`;
if (typeof value === 'string') return value;
try {
const upstream = await fetch(`${bridgeUrl}/v1/chat/completions`, {
return JSON.stringify(value);
} catch {
return String(value);
}
}
async function postSubscriptionBridgeCompletion(
target: SubscriptionBridgeTarget,
body: OpenAIChatCompletionRequest,
inputText: string
): Promise<{ statusCode: number; ok: boolean; data: any }> {
const parseBridgeResponse = async (response: Response): Promise<any> => {
const text = await response.text();
try {
return JSON.parse(text);
} catch {
return { raw: text };
}
};
const bridgeUrl = subscriptionBridgeUrl(target);
const chatResponse = await fetch(`${bridgeUrl}/v1/chat/completions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@ -755,23 +852,53 @@ async function callCodexBridgeChatCompletion(
max_tokens: body.max_tokens,
}),
});
const upstreamJson: any = await upstream.json().catch(() => ({}));
if (!upstream.ok || upstreamJson?.success === false) {
const error = upstreamJson?.error ?? upstream.statusText ?? 'codex bridge unavailable';
logger.warn({ callId, model: body.model, statusCode: upstream.status, error }, 'codex-bridge chat passthrough non-OK');
return { ok: false, statusCode: 502, error: String(error) };
const chatData: any = await parseBridgeResponse(chatResponse);
if (chatResponse.status !== 404) {
return { statusCode: chatResponse.status, ok: chatResponse.ok, data: chatData };
}
const text = upstreamJson?.choices?.[0]?.message?.content
?? upstreamJson?.content
?? upstreamJson?.response
?? '';
const promptTokens = upstreamJson?.usage?.prompt_tokens ?? Math.ceil(inputText.length / 4);
const completionTokens = upstreamJson?.usage?.completion_tokens ?? Math.ceil(String(text).length / 4);
const generateResponse = await fetch(`${bridgeUrl}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: body.model,
prompt: inputText,
stream: false,
temperature: body.temperature ?? 0,
max_tokens: body.max_tokens,
}),
});
const generateData: any = await parseBridgeResponse(generateResponse);
return { statusCode: generateResponse.status, ok: generateResponse.ok, data: generateData };
}
async function callSubscriptionBridgeChatCompletion(
body: OpenAIChatCompletionRequest,
startMs: number,
callId: string
): Promise<{ ok: true; body: Record<string, unknown> } | { ok: false; statusCode: number; error: string } | null> {
const target = subscriptionBridgeTargetForModel(body.model);
if (!target) return null;
const inputText = chatMessagesToPrompt(body.messages);
try {
const upstream = await postSubscriptionBridgeCompletion(target, body, inputText);
if (!upstream.ok || upstream.data?.success === false) {
const error = bridgeErrorMessage(upstream.data, upstream.statusCode);
logger.warn(
{ callId, model: body.model, provider: target.provider, statusCode: upstream.statusCode, error },
'subscription-bridge chat passthrough non-OK'
);
return { ok: false, statusCode: 424, error };
}
const text = contentFromSubscriptionBridge(upstream.data);
const promptTokens = upstream.data?.usage?.prompt_tokens ?? Math.ceil(inputText.length / 4);
const completionTokens = upstream.data?.usage?.completion_tokens ?? Math.ceil(String(text).length / 4);
return {
ok: true,
body: {
id: upstreamJson?.id ?? `chatcmpl-${Date.now()}`,
id: upstream.data?.id ?? `chatcmpl-${Date.now()}`,
object: 'chat.completion',
created: Math.floor(Date.now() / 1000),
model: body.model,
@ -789,18 +916,19 @@ async function callCodexBridgeChatCompletion(
},
gateway: {
status: 'approved',
provider: 'codex-bridge',
provider: target.provider,
subscription_id: target.subscriptionId,
latency_ms: Date.now() - startMs,
passthrough: true,
},
},
};
} catch (err) {
logger.error({ err, callId, model: body.model }, 'codex-bridge chat passthrough threw');
logger.error({ err, callId, model: body.model, provider: target.provider }, 'subscription-bridge chat passthrough threw');
return {
ok: false,
statusCode: 502,
error: err instanceof Error ? err.message : 'codex bridge unavailable',
statusCode: 424,
error: err instanceof Error ? err.message : 'subscription bridge unavailable',
};
}
}
@ -1175,21 +1303,21 @@ export async function completionRoute(fastify: FastifyInstance): Promise<void> {
}
const callId = `chatcmpl-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
const codexBridgeResult = await callCodexBridgeChatCompletion(parsed.data, startMs, callId);
if (codexBridgeResult) {
if (!codexBridgeResult.ok) {
return reply.status(codexBridgeResult.statusCode).send({
const subscriptionBridgeResult = await callSubscriptionBridgeChatCompletion(parsed.data, startMs, callId);
if (subscriptionBridgeResult) {
if (!subscriptionBridgeResult.ok) {
return reply.status(subscriptionBridgeResult.statusCode).send({
error: {
message: codexBridgeResult.error,
message: subscriptionBridgeResult.error,
type: 'upstream_error',
code: codexBridgeResult.statusCode,
code: subscriptionBridgeResult.statusCode,
},
});
}
if (parsed.data.stream) {
return await streamOpenAIChatResponse(reply, codexBridgeResult.body);
return await streamOpenAIChatResponse(reply, subscriptionBridgeResult.body);
}
return reply.status(200).send(codexBridgeResult.body);
return reply.status(200).send(subscriptionBridgeResult.body);
}
const gatewayRequest = openAIRequestToGatewayRequest(parsed.data, request);
@ -1284,7 +1412,13 @@ export async function completionRoute(fastify: FastifyInstance): Promise<void> {
messages: [{ role: 'user', content: inputText }],
}),
});
const upstreamJson: any = await upstream.json();
const upstreamText = await upstream.text();
let upstreamJson: any;
try {
upstreamJson = JSON.parse(upstreamText);
} catch {
upstreamJson = { raw: upstreamText };
}
if (upstream.ok && upstreamJson?.success !== false) {
const text = upstreamJson?.content ?? upstreamJson?.response ?? upstreamJson?.choices?.[0]?.message?.content ?? '';
const respBody = toOpenAIResponsesResponse({ output: text, model: parsed.data.model, status: 'approved' }, parsed.data.model);
@ -1349,9 +1483,24 @@ data: [DONE]
}
return reply.send(respBody);
}
logger.warn({ callId, model: parsed.data.model, upstreamJson }, 'codex-bridge upstream non-OK; falling back to standard pipeline');
const error = bridgeErrorMessage(upstreamJson, upstream.status);
logger.warn({ callId, model: parsed.data.model, upstreamJson }, 'codex-bridge upstream non-OK');
return reply.status(424).send({
error: {
message: error,
type: 'upstream_error',
code: 424,
},
});
} catch (err) {
logger.error({ err, callId, model: parsed.data.model }, 'codex-bridge passthrough threw; falling back');
logger.error({ err, callId, model: parsed.data.model }, 'codex-bridge passthrough threw');
return reply.status(424).send({
error: {
message: err instanceof Error ? err.message : 'codex bridge unavailable',
type: 'upstream_error',
code: 424,
},
});
}
}

View File

@ -223,6 +223,15 @@ async function postBridgeChat(
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
const startedAt = Date.now();
const parseResponse = async (response: Response): Promise<any> => {
const text = await response.text();
try {
return JSON.parse(text);
} catch {
return { raw: text };
}
};
try {
const response = await fetch(bridgeEndpoint(url, '/v1/chat/completions'), {
method: 'POST',
@ -235,14 +244,39 @@ async function postBridgeChat(
}),
signal: controller.signal,
});
const text = await response.text();
let data: any;
try {
data = JSON.parse(text);
} catch {
data = { raw: text };
}
const data = await parseResponse(response);
if (response.status !== 404) {
return { statusCode: response.status, ok: response.ok, data, latencyMs: Date.now() - startedAt };
}
const generateResponse = await fetch(bridgeEndpoint(url, '/api/generate'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model,
prompt: message,
stream: false,
temperature: 0,
max_tokens: 128,
}),
signal: controller.signal,
});
const generateData = await parseResponse(generateResponse);
return {
statusCode: generateResponse.status,
ok: generateResponse.ok,
data: generateData,
latencyMs: Date.now() - startedAt,
};
} catch (error) {
return {
statusCode: 0,
ok: false,
data: {
error: error instanceof Error ? error.message : 'bridge request failed',
},
latencyMs: Date.now() - startedAt,
};
} finally {
clearTimeout(timer);
}
@ -411,7 +445,7 @@ export async function subscriptionsRoute(fastify: FastifyInstance): Promise<void
const upstream = await postBridgeChat(url, model, message, timeoutMs);
const content = contentFromBridgeResponse(upstream.data);
return reply.status(upstream.ok ? 200 : 502).send({
return reply.status(upstream.ok ? 200 : 424).send({
success: upstream.ok,
data: {
subscription: summarizeSubscription(status, runningById, getRequestBaseUrl(request)),
@ -435,7 +469,7 @@ export async function subscriptionsRoute(fastify: FastifyInstance): Promise<void
});
} catch (error) {
logger.error({ error }, 'Subscription bridge test failed');
return reply.status(500).send({
return reply.status(424).send({
success: false,
error: error instanceof Error ? error.message : 'subscription bridge test failed',
});