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:
@@ -85,7 +85,7 @@ function makeUsage(usage?: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function makeMessageId(): string {
|
function makeMessageId(): string {
|
||||||
return `msg_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`
|
return `msg_${crypto.randomUUID().replace(/-/g, '')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeToolUseId(toolUseId: string | undefined): {
|
function normalizeToolUseId(toolUseId: string | undefined): {
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ function convertMessages(
|
|||||||
input?: unknown
|
input?: unknown
|
||||||
extra_content?: Record<string, 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,
|
type: 'function' as const,
|
||||||
function: {
|
function: {
|
||||||
name: tu.name ?? 'unknown',
|
name: tu.name ?? 'unknown',
|
||||||
@@ -389,7 +389,7 @@ interface OpenAIStreamChunk {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function makeMessageId(): string {
|
function makeMessageId(): string {
|
||||||
return `msg_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`
|
return `msg_${crypto.randomUUID().replace(/-/g, '')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertChunkUsage(
|
function convertChunkUsage(
|
||||||
@@ -610,6 +610,23 @@ async function* openaiStreamToAnthropic(
|
|||||||
: choice.finish_reason === 'length'
|
: choice.finish_reason === 'length'
|
||||||
? 'max_tokens'
|
? 'max_tokens'
|
||||||
: 'end_turn'
|
: '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
|
lastStopReason = stopReason
|
||||||
|
|
||||||
yield {
|
yield {
|
||||||
@@ -841,7 +858,14 @@ class OpenAIShimMessages {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const apiKey = process.env.OPENAI_API_KEY ?? ''
|
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 (apiKey) {
|
||||||
if (isAzure) {
|
if (isAzure) {
|
||||||
@@ -1003,6 +1027,13 @@ class OpenAIShimMessages {
|
|||||||
? 'max_tokens'
|
? 'max_tokens'
|
||||||
: 'end_turn'
|
: '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 {
|
return {
|
||||||
id: data.id ?? makeMessageId(),
|
id: data.id ?? makeMessageId(),
|
||||||
type: 'message',
|
type: 'message',
|
||||||
|
|||||||
Reference in New Issue
Block a user