mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 03:56:55 +03:00
fix(ai): classify OpenAI image moderation blocks as content policy (#58544)
This commit is contained in:
committed by
GitHub
Unverified
parent
ddaf06adaf
commit
43f7997bfa
@@ -8,10 +8,11 @@ import {
|
||||
type ImageRequestFor,
|
||||
} from "../image.js"
|
||||
import { Media } from "../media.js"
|
||||
import { classifyProviderFailure } from "../provider-error.js"
|
||||
import { Framing } from "../route/framing.js"
|
||||
import { MediaProtocol } from "../route/media-protocol.js"
|
||||
import { MediaRoute } from "../route/media.js"
|
||||
import { mergeJsonRecords, ProviderID, type MediaUsage, type OpenString } from "../schema/index.js"
|
||||
import { AIError, mergeJsonRecords, ProviderID, type MediaUsage, type OpenString } from "../schema/index.js"
|
||||
import { ProviderShared } from "./shared.js"
|
||||
import { ImageSizing } from "./utils/image-sizing.js"
|
||||
import { MediaInput } from "./utils/media-input.js"
|
||||
@@ -87,6 +88,10 @@ const StreamEvent = Schema.Union([
|
||||
output_format: Schema.String,
|
||||
usage: Schema.optional(Usage),
|
||||
}),
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("error"),
|
||||
error: Schema.optional(Schema.Struct({ message: Schema.optional(Schema.String) })),
|
||||
}),
|
||||
])
|
||||
|
||||
const decodeEvent = route.decodeFrame(StreamEvent)
|
||||
@@ -249,6 +254,13 @@ const eventImage = (frame: string, label: string, data: string, format: string,
|
||||
|
||||
const onEvent = Effect.fnUntraced(function* (state: State, frame: string) {
|
||||
const event = yield* decodeEvent(frame)
|
||||
if (event.type === "error")
|
||||
return yield* new AIError({
|
||||
reason: classifyProviderFailure({
|
||||
message: `${route.name} stream failed: ${event.error?.message ?? "unknown error"}`,
|
||||
rawBody: frame,
|
||||
}),
|
||||
})
|
||||
const format = event.output_format
|
||||
if ("partial_image_index" in event) {
|
||||
const image = yield* eventImage(frame, `${route.name} partial image`, event.b64_json, format, event.size)
|
||||
|
||||
@@ -154,7 +154,7 @@ const INVALID_REQUEST_CODES = new Set([
|
||||
// Azure OpenAI reports `content_filter` with `innererror.code` ResponsibleAIPolicyViolation.
|
||||
// OpenRouter tags provider failures with a typed `error_type`; its Responses skin also
|
||||
// emits `image_content_policy_violation` as the native code. Alibaba Model Studio reports
|
||||
// moderation rejections as `DataInspectionFailed`.
|
||||
// moderation rejections as `DataInspectionFailed`. OpenAI Images reports `moderation_blocked`.
|
||||
const CONTENT_POLICY_CODES = new Set([
|
||||
"content_filter",
|
||||
"content_moderation",
|
||||
@@ -163,6 +163,7 @@ const CONTENT_POLICY_CODES = new Set([
|
||||
"image_content_policy_violation",
|
||||
"refusal",
|
||||
"datainspectionfailed",
|
||||
"moderation_blocked",
|
||||
])
|
||||
// OpenCode Zen replaces upstream codes outside its allow-list but keeps the original
|
||||
// as a `[code]` label at the start of the rewritten message.
|
||||
@@ -177,9 +178,9 @@ const QUOTA_TEXT =
|
||||
// Policy rejections without a dedicated code, matched against the provider's own
|
||||
// explanation only. OpenAI reuses `invalid_prompt` for usage-policy rejections while
|
||||
// Bedrock Mantle reuses it for schema validation; Anthropic reports blocked output
|
||||
// under `invalid_request_error`.
|
||||
// under `invalid_request_error`. The Vercel AI Gateway drops OpenAI's `moderation_blocked` code.
|
||||
const CONTENT_POLICY_TEXT =
|
||||
/violating our usage policy|blocked by content filtering policy|content[-_\s]?policy|rejected as a result of our safety system|detected potentially unsafe or sensitive content/i
|
||||
/violating our usage policy|blocked by content filtering policy|content[-_\s]?policy|rejected (?:as a result of our|by the) safety system|detected potentially unsafe or sensitive content/i
|
||||
const SERVER_ERROR_TEXT =
|
||||
/\b(?:try again|(?:please |you can )?retry (?:the |this |your )?request|try (?:the |this |your )?request again|(?:currently |temporarily )?at capacity|overloaded|temporarily unavailable|service[-_\s]?unavailable|(?:server|internal)[-_\s]?error|server (?:is )?busy|provider returned (?:an )?error|resource[-_\s]?exhausted|upstream (?:connect|connection|request)|request buffer limit while retrying upstream)\b/i
|
||||
|
||||
|
||||
@@ -169,6 +169,38 @@ describe("Image", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("classifies an OpenAI stream moderation error event as a content policy rejection", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* Image.stream({
|
||||
model: OpenAI.configure({ apiKey: "test", baseURL: "https://openai.test/v1" }).image("gpt-image-1-mini"),
|
||||
prompt: "A lighthouse",
|
||||
}).pipe(Stream.runDrain, Effect.flip)
|
||||
expect(error.reason._tag).toBe("ContentPolicy")
|
||||
expect(error.message).toContain("rejected by the safety system")
|
||||
}).pipe(
|
||||
Effect.provide(
|
||||
layer((input) =>
|
||||
Effect.succeed(
|
||||
input.respond(
|
||||
`event: error\ndata: ${JSON.stringify({
|
||||
type: "error",
|
||||
error: {
|
||||
type: "image_generation_user_error",
|
||||
code: "moderation_blocked",
|
||||
message:
|
||||
"Your request was rejected by the safety system. If you believe this is an error, contact us at help.openai.com and include the request ID req_123.",
|
||||
param: null,
|
||||
},
|
||||
sequence_number: 0,
|
||||
})}\n\n`,
|
||||
{ headers: { "content-type": "text/event-stream" } },
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("lowers aspect to the nearest OpenAI size and reports the substitution", () =>
|
||||
Effect.gen(function* () {
|
||||
const openai = OpenAI.configure({ apiKey: "test", baseURL: "https://openai.test/v1" })
|
||||
|
||||
@@ -218,6 +218,35 @@ describe("provider error classification", () => {
|
||||
).toBe("ContentPolicy")
|
||||
})
|
||||
|
||||
test("classifies OpenAI image moderation blocks directly and through the Vercel AI Gateway", () => {
|
||||
const message =
|
||||
"Your request was rejected by the safety system. If you believe this is an error, contact us at help.openai.com and include the request ID req_123."
|
||||
const openai = {
|
||||
error: {
|
||||
message,
|
||||
type: "image_generation_user_error",
|
||||
param: null,
|
||||
code: "moderation_blocked",
|
||||
moderation_details: { moderation_stage: "input", categories: ["other"] },
|
||||
},
|
||||
}
|
||||
// The gateway keeps OpenAI's message but replaces its type and drops the code.
|
||||
const vercel = {
|
||||
error: {
|
||||
message,
|
||||
type: "AI_APICallError",
|
||||
param: { error: message, statusCode: 400, name: "AI_APICallError", message, isRetryable: false },
|
||||
},
|
||||
}
|
||||
|
||||
expect(
|
||||
[openai, vercel].map(
|
||||
(body) =>
|
||||
classifyProviderFailure({ message: body.error.message, status: 400, rawBody: JSON.stringify(body) })._tag,
|
||||
),
|
||||
).toEqual(["ContentPolicy", "ContentPolicy"])
|
||||
})
|
||||
|
||||
test("recovers policy codes that OpenCode Zen preserves only in its message label", () => {
|
||||
// Zen drops upstream codes outside its allow-list and replaces the type, but
|
||||
// `Protocol.errorMessage` keeps the original code as a `[code]` prefix.
|
||||
|
||||
Reference in New Issue
Block a user