fix(gateway): return honest subscription bridge API status
This commit is contained in:
parent
496975ed94
commit
be8eda6430
@ -735,43 +735,170 @@ function chatMessagesToPrompt(messages: OpenAIChatCompletionRequest['messages'])
|
||||
.trim();
|
||||
}
|
||||
|
||||
async function callCodexBridgeChatCompletion(
|
||||
interface SubscriptionBridgeTarget {
|
||||
subscriptionId: string;
|
||||
provider: string;
|
||||
envKey: string;
|
||||
defaultUrl: string;
|
||||
}
|
||||
|
||||
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 {
|
||||
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({
|
||||
model: body.model,
|
||||
messages: body.messages,
|
||||
temperature: body.temperature,
|
||||
max_tokens: body.max_tokens,
|
||||
}),
|
||||
});
|
||||
const chatData: any = await parseBridgeResponse(chatResponse);
|
||||
if (chatResponse.status !== 404) {
|
||||
return { statusCode: chatResponse.status, ok: chatResponse.ok, data: chatData };
|
||||
}
|
||||
|
||||
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> {
|
||||
if (!isCodexBridgeModel(body.model)) return null;
|
||||
const target = subscriptionBridgeTargetForModel(body.model);
|
||||
if (!target) return null;
|
||||
|
||||
const bridgeUrl = (process.env['CODEX_BRIDGE_URL'] ?? 'http://127.0.0.1:3253').replace(/\/$/, '');
|
||||
const inputText = chatMessagesToPrompt(body.messages);
|
||||
try {
|
||||
const upstream = await fetch(`${bridgeUrl}/v1/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: body.model,
|
||||
messages: body.messages,
|
||||
temperature: body.temperature,
|
||||
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 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 = 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 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
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',
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user