1+ import {
2+ classifySafetyTransportError ,
3+ isSafetyCheckV2Record ,
4+ } from "@mavis/shared/safety-check-v2" ;
5+ import { createSafetyFailureReporter } from "./diagnostics.js" ;
16import {
27 getRuntimeBuildEnv ,
38 getRuntimeRegion ,
@@ -8,7 +13,6 @@ import {
813import { callTurnSafetyApi } from "./turn-api.js" ;
914import { resolveSafetyApiBase } from "./api-base.js" ;
1015
11- import { logger } from "../common/logger.js" ;
1216import type { LocalRuntimeAuthContext } from "../runtime/model-resolver.js" ;
1317import {
1418 managedBackendRoutingHeaders ,
@@ -126,12 +130,7 @@ export async function callSafetyApi(input: {
126130 const accessToken =
127131 input . authContext ?. accessToken ?. trim ( ) ||
128132 process . env . MAVIS_ACCESS_TOKEN ?. trim ( ) ;
129- // TODO(shared-oauth): Remove this local-only diagnostic after the content-safety
130- // resource server accepts mcode-public Bearer tokens in the joint test environment.
131- // Never add the token, request body/content, or response body to these fields.
132- const logLocalUpstream =
133- resolvedBuildEnv === "dev" || resolvedBuildEnv === "test" ;
134- const startedAt = Date . now ( ) ;
133+ const reportFailure = createSafetyFailureReporter ( url , "v1" , input . scene ) ;
135134 let response : Response ;
136135 try {
137136 response = await input . fetchImpl ( url , {
@@ -162,20 +161,10 @@ export async function callSafetyApi(input: {
162161 signal : AbortSignal . timeout ( CONTENT_REVIEW_TIMEOUT_MS ) ,
163162 } ) ;
164163 } catch ( error ) {
165- if ( logLocalUpstream ) {
166- logger . info (
167- {
168- url,
169- method : "POST" ,
170- status : null ,
171- scene : input . scene ,
172- hasBearer : Boolean ( accessToken ) ,
173- durationMs : Date . now ( ) - startedAt ,
174- errorType : error instanceof Error ? error . name : typeof error ,
175- } ,
176- "[content-safety] upstream transport failure" ,
177- ) ;
178- }
164+ reportFailure ( {
165+ failureKind : "transport" ,
166+ transportKind : classifySafetyTransportError ( error ) ,
167+ } ) ;
179168 // fetch threw (offline / DNS / connection refused / TLS) or the 10s timeout
180169 // fired: we never reached a verdict. No gateway confirmation → fail closed.
181170 return {
@@ -185,26 +174,21 @@ export async function callSafetyApi(input: {
185174 } ;
186175 }
187176
188- if ( logLocalUpstream ) {
189- logger . info (
190- {
191- url,
192- method : "POST" ,
193- status : response . status ,
194- scene : input . scene ,
195- hasBearer : Boolean ( accessToken ) ,
196- durationMs : Date . now ( ) - startedAt ,
197- upstreamTraceId :
198- response . headers . get ( "trace-id" ) ??
199- response . headers . get ( "x-trace-id" ) ??
200- response . headers . get ( "x-request-id" ) ??
201- null ,
202- } ,
203- "[content-safety] upstream response" ,
204- ) ;
177+ let text : string ;
178+ try {
179+ text = await response . text ( ) ;
180+ } catch ( error ) {
181+ reportFailure ( {
182+ failureKind : "transport" ,
183+ transportKind : classifySafetyTransportError ( error ) ,
184+ statusCode : response . status ,
185+ } ) ;
186+ return {
187+ pass : false ,
188+ reason : "Service response could not be read" ,
189+ errorKind : "local_error" ,
190+ } ;
205191 }
206-
207- const text = await response . text ( ) ;
208192 if ( ! response . ok ) {
209193 // Non-2xx from the gateway, split by class so an auth failure can never be a
210194 // bypass:
@@ -215,6 +199,10 @@ export async function callSafetyApi(input: {
215199 // acceptable, so there is NO valid verdict on our side → `local_error`
216200 // (fail closed). Treating 401/403 as `api_error` would let an
217201 // unauthenticated user bypass every content gate.
202+ reportFailure ( {
203+ failureKind : response . status === 401 || response . status === 403 ? "auth" : "http" ,
204+ statusCode : response . status ,
205+ } ) ;
218206 const error = readErrorText ( text ) ;
219207 return {
220208 pass : false ,
@@ -240,6 +228,7 @@ export async function callSafetyApi(input: {
240228 try {
241229 data = JSON . parse ( text ) as typeof data ;
242230 } catch {
231+ reportFailure ( { failureKind : "response" , statusCode : response . status } ) ;
243232 // 2xx but the body is not JSON — we cannot read a verdict. Treat as no
244233 // gateway confirmation → fail closed on the input path.
245234 return {
@@ -249,8 +238,9 @@ export async function callSafetyApi(input: {
249238 } ;
250239 }
251240
252- const pass = data . pass ?? data . safe ;
241+ const pass = isSafetyCheckV2Record ( data ) ? data . pass ?? data . safe : undefined ;
253242 if ( typeof pass !== "boolean" ) {
243+ reportFailure ( { failureKind : "response" , statusCode : response . status } ) ;
254244 // 2xx JSON but no boolean pass/safe field — again no usable verdict → fail
255245 // closed on the input path.
256246 return {
@@ -262,6 +252,9 @@ export async function callSafetyApi(input: {
262252 const activeRegion = region ( ) ;
263253 const upstreamCode =
264254 typeof data . errorCode === "number" ? data . errorCode : undefined ;
255+ if ( ! pass && upstreamCode === UPSTREAM_CODE . SERVICE_UNAVAILABLE ) {
256+ reportFailure ( { failureKind : "upstream" , statusCode : upstreamCode } ) ;
257+ }
265258 return {
266259 pass,
267260 reason :
0 commit comments