fix: crypto.randomUUID for IDs, Azure Foundry detection, safety filter visibility

Three targeted fixes:

1. Replace Math.random() with crypto.randomUUID() for message and tool
   call IDs in both openaiShim.ts and codexShim.ts. Math.random() is
   not cryptographically secure and predictable in seeded environments.

2. Anchor Azure endpoint detection to parsed hostname instead of raw
   URL regex. Adds support for Azure AI Foundry (services.ai.azure.com)
   alongside existing cognitiveservices and openai Azure endpoints.
   Prevents SSRF-style bypass via path segments.

3. Surface content safety filter blocks to the user. When Gemini or
   Azure returns finish_reason 'content_filter' or 'safety', emit a
   visible text block '[Content blocked by provider safety filter]'
   instead of silently returning empty/truncated content with
   stop_reason 'end_turn'. Applied to both streaming and non-streaming.
This commit is contained in:
Juan Camilo
2026-04-02 16:14:35 +02:00
parent 3353101e83
commit 5d6443799a
2 changed files with 35 additions and 4 deletions

View File

@@ -231,7 +231,7 @@ function convertMessages(
input?: unknown
extra_content?: Record<string, unknown>
}) => ({
id: tu.id ?? `call_${Math.random().toString(36).slice(2)}`,
id: tu.id ?? `call_${crypto.randomUUID().replace(/-/g, '')}`,
type: 'function' as const,
function: {
name: tu.name ?? 'unknown',
@@ -389,7 +389,7 @@ interface OpenAIStreamChunk {
}
function makeMessageId(): string {
return `msg_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`
return `msg_${crypto.randomUUID().replace(/-/g, '')}`
}
function convertChunkUsage(
@@ -610,6 +610,23 @@ async function* openaiStreamToAnthropic(
: choice.finish_reason === 'length'
? 'max_tokens'
: 'end_turn'
if (choice.finish_reason === 'content_filter' || choice.finish_reason === 'safety') {
// Gemini/Azure content safety filter blocked the response.
// Emit a visible text block so the user knows why output was truncated.
if (!hasEmittedContentStart) {
yield {
type: 'content_block_start',
index: contentBlockIndex,
content_block: { type: 'text', text: '' },
}
hasEmittedContentStart = true
}
yield {
type: 'content_block_delta',
index: contentBlockIndex,
delta: { type: 'text_delta', text: '\n\n[Content blocked by provider safety filter]' },
}
}
lastStopReason = stopReason
yield {
@@ -841,7 +858,14 @@ class OpenAIShimMessages {
}
const apiKey = process.env.OPENAI_API_KEY ?? ''
const isAzure = /cognitiveservices\.azure\.com|openai\.azure\.com/.test(request.baseUrl)
// Detect Azure endpoints by hostname (not raw URL) to prevent bypass via
// path segments like https://evil.com/cognitiveservices.azure.com/
let isAzure = false
try {
const { hostname } = new URL(request.baseUrl)
isAzure = hostname.endsWith('.azure.com') &&
(hostname.includes('cognitiveservices') || hostname.includes('openai') || hostname.includes('services.ai'))
} catch { /* malformed URL — not Azure */ }
if (apiKey) {
if (isAzure) {
@@ -1003,6 +1027,13 @@ class OpenAIShimMessages {
? 'max_tokens'
: 'end_turn'
if (choice?.finish_reason === 'content_filter' || choice?.finish_reason === 'safety') {
content.push({
type: 'text',
text: '\n\n[Content blocked by provider safety filter]',
})
}
return {
id: data.id ?? makeMessageId(),
type: 'message',