@@ -22,6 +22,7 @@ import { z } from 'zod';
2222import { CheckFn , GuardrailResult } from '../types' ;
2323import { defaultSpecRegistry } from '../registry' ;
2424import OpenAI from 'openai' ;
25+ import { SAFETY_IDENTIFIER , supportsSafetyIdentifier } from '../utils/safety-identifier' ;
2526
2627/**
2728 * Enumeration of supported moderation categories.
@@ -78,6 +79,42 @@ export const ModerationContext = z.object({
7879
7980export type ModerationContext = z . infer < typeof ModerationContext > ;
8081
82+ /**
83+ * Check if an error is a 404 Not Found error from the OpenAI API.
84+ *
85+ * @param error The error to check
86+ * @returns True if the error is a 404 error
87+ */
88+ function isNotFoundError ( error : unknown ) : boolean {
89+ return ! ! ( error && typeof error === 'object' && 'status' in error && error . status === 404 ) ;
90+ }
91+
92+ /**
93+ * Call the OpenAI moderation API.
94+ *
95+ * @param client The OpenAI client to use
96+ * @param data The text to analyze
97+ * @returns The moderation API response
98+ */
99+ function callModerationAPI (
100+ client : OpenAI ,
101+ data : string
102+ ) : ReturnType < OpenAI [ 'moderations' ] [ 'create' ] > {
103+ const params : Record < string , unknown > = {
104+ model : 'omni-moderation-latest' ,
105+ input : data ,
106+ } ;
107+
108+ // Only include safety_identifier for official OpenAI API (not Azure or local providers)
109+ if ( supportsSafetyIdentifier ( client ) ) {
110+ // @ts -ignore - safety_identifier is not defined in OpenAI types yet
111+ params . safety_identifier = SAFETY_IDENTIFIER ;
112+ }
113+
114+ // @ts -ignore - safety_identifier is not in the OpenAI types yet
115+ return client . moderations . create ( params ) ;
116+ }
117+
81118/**
82119 * Guardrail check_fn to flag disallowed content categories using OpenAI moderation API.
83120 *
@@ -102,39 +139,55 @@ export const moderationCheck: CheckFn<ModerationContext, string, ModerationConfi
102139 const configObj = actualConfig as Record < string , unknown > ;
103140 const categories = ( configObj . categories as string [ ] ) || Object . values ( Category ) ;
104141
105- // Reuse provided client only if it targets the official OpenAI API.
106- const reuseClientIfOpenAI = ( context : unknown ) : OpenAI | null => {
107- try {
108- const contextObj = context as Record < string , unknown > ;
109- const candidate = contextObj ?. guardrailLlm ;
110- if ( ! candidate || typeof candidate !== 'object' ) return null ;
111- if ( ! ( candidate instanceof OpenAI ) ) return null ;
112-
113- const candidateObj = candidate as unknown as Record < string , unknown > ;
114- const baseURL : string | undefined =
115- ( candidateObj . baseURL as string ) ??
116- ( ( candidateObj . _client as Record < string , unknown > ) ?. baseURL as string ) ??
117- ( candidateObj . _baseURL as string ) ;
118-
119- if (
120- baseURL === undefined ||
121- ( typeof baseURL === 'string' && baseURL . includes ( 'api.openai.com' ) )
122- ) {
123- return candidate as OpenAI ;
124- }
125- return null ;
126- } catch {
127- return null ;
142+ // Get client from context if available
143+ let client : OpenAI | null = null ;
144+ if ( ctx ) {
145+ const contextObj = ctx as Record < string , unknown > ;
146+ const candidate = contextObj . guardrailLlm ;
147+ if ( candidate && candidate instanceof OpenAI ) {
148+ client = candidate ;
128149 }
129- } ;
130-
131- const client = reuseClientIfOpenAI ( ctx ) ?? new OpenAI ( ) ;
150+ }
132151
133152 try {
134- const resp = await client . moderations . create ( {
135- model : 'omni-moderation-latest' ,
136- input : data ,
137- } ) ;
153+ // Try the context client first, fall back if moderation endpoint doesn't exist
154+ let resp : Awaited < ReturnType < typeof callModerationAPI > > ;
155+ if ( client !== null ) {
156+ try {
157+ resp = await callModerationAPI ( client , data ) ;
158+ } catch ( error ) {
159+
160+ // Moderation endpoint doesn't exist on this provider (e.g., third-party)
161+ // Fall back to the OpenAI client
162+ if ( isNotFoundError ( error ) ) {
163+ try {
164+ resp = await callModerationAPI ( new OpenAI ( ) , data ) ;
165+ } catch ( fallbackError ) {
166+ // If fallback fails, provide a helpful error message
167+ const errorMessage = fallbackError instanceof Error
168+ ? fallbackError . message
169+ : String ( fallbackError ) ;
170+
171+ // Check if it's an API key error
172+ if ( errorMessage . includes ( 'api_key' ) || errorMessage . includes ( 'OPENAI_API_KEY' ) ) {
173+ return {
174+ tripwireTriggered : false ,
175+ info : {
176+ checked_text : data ,
177+ error : 'Moderation API requires OpenAI API key. Set OPENAI_API_KEY environment variable or pass a client with valid credentials.' ,
178+ } ,
179+ } ;
180+ }
181+ throw fallbackError ;
182+ }
183+ } else {
184+ throw error ;
185+ }
186+ }
187+ } else {
188+ // No context client, use fallback
189+ resp = await callModerationAPI ( new OpenAI ( ) , data ) ;
190+ }
138191
139192 const results = resp . results || [ ] ;
140193 if ( ! results . length ) {
0 commit comments