diff --git a/src/proxy/middleware/common.ts b/src/proxy/middleware/common.ts index 3f5dab1..d8d0e2f 100644 --- a/src/proxy/middleware/common.ts +++ b/src/proxy/middleware/common.ts @@ -264,7 +264,15 @@ export function getCompletionFromBody(req: Request, body: Record) { if ("choices" in body) { return body.choices[0].message.content; } - return body.candidates[0].content.parts[0].text; + const text = body.candidates[0].content?.parts?.[0]?.text; + if (!text) { + req.log.warn( + { body: JSON.stringify(body) }, + "Received empty Google AI text completion" + ); + return ""; + } + return text; case "openai-image": return body.data?.map((item: any) => item.url).join("\n"); default: diff --git a/src/proxy/middleware/response/streaming/transformers/google-ai-to-openai.ts b/src/proxy/middleware/response/streaming/transformers/google-ai-to-openai.ts index 8d6b1e5..b60151a 100644 --- a/src/proxy/middleware/response/streaming/transformers/google-ai-to-openai.ts +++ b/src/proxy/middleware/response/streaming/transformers/google-ai-to-openai.ts @@ -9,7 +9,7 @@ const log = logger.child({ type GoogleAIStreamEvent = { candidates: { - content: { parts: { text: string }[]; role: string }; + content?: { parts?: { text: string }[]; role: string }; finishReason?: "STOP" | "MAX_TOKENS" | "SAFETY" | "RECITATION" | "OTHER"; index: number; tokenCount?: number; @@ -34,9 +34,15 @@ export const googleAIToOpenAI: StreamingCompletionTransformer = (params) => { return { position: -1 }; } - const parts = completionEvent.candidates[0].content.parts; + const parts = completionEvent.candidates[0].content?.parts || []; let content = parts[0]?.text ?? ""; + if (isSafetyStop(completionEvent)) { + content = `[Proxy Warning] Gemini safety filter triggered: ${JSON.stringify( + completionEvent.candidates[0].safetyRatings + )}`; + } + // If this is the first chunk, try stripping speaker names from the response // e.g. "John: Hello" -> "Hello" if (index === 0) { @@ -60,6 +66,14 @@ export const googleAIToOpenAI: StreamingCompletionTransformer = (params) => { return { position: -1, event: newEvent }; }; +function isSafetyStop(completion: GoogleAIStreamEvent) { + const isSafetyStop = ["SAFETY", "OTHER"].includes( + completion.candidates[0].finishReason ?? "" + ); + const hasNoContent = completion.candidates[0].content?.parts?.length === 0; + return isSafetyStop && hasNoContent; +} + function asCompletion(event: ServerSentEvent): GoogleAIStreamEvent | null { try { const parsed = JSON.parse(event.data) as GoogleAIStreamEvent;