Files
OmniRoute/open-sse/handlers/moderations.ts
Armin Anton” ∴ 10276821cd Integration: security tier + self-hosted operator blockers (rebased onto v3.8.51) (#10952)
Validated on the resolved merge against the current tip (527da656 + the post-#11281 rebaseline): the single conflict was a comment-only collision in providers/[id]/models/route.ts (kept the tip's #10828-ordering note). Focused suites 125/125 across all 13 touched test files (build-sqlite-stub, cc-compatible, copilot-claude-messages, copilot-gemini-route, executor-github, ghe-copilot, github-copilot-discovery-token, github-copilot-model-discovery, noauth-sibling-7620, provider-header-profiles, provider-models-config, request-log-payloads, upstream-error-passthrough), typecheck:core clean, file-size/changelog-integrity OK. Merged --admin over the inherited 2026-08-23 base-red cluster (#9985) — the reds are proven tip failures (CLI catalog cluster + @testing-library allowlist, being drained by #11280), not from this diff. Note: the rebase means several items the body listed (relay x-relay-path SSRF, /v1/search blocked-providers, #10736 rotation fence, #10903, #10865, #10899, #10916) already landed upstream and are NOT in this delta — the delta is: better-sqlite3 build guard + build heap/worker caps + telemetry-off (#10060 re-derived), credential-echo passthrough refusal + OCR/moderation redaction + call-log key redaction, Copilot CLI 1.0.81-6 wire identity + Claude→/v1/messages name-matched routing + discovery token fix, CC model_not_found 400, compat overrides for no-auth aliases (#7620-pinned). The Copilot wire-identity change is the one to watch in production. Thank you @arminanton — and the ported-author credits in the commit history (@rqzbeh, yidecode, the #10899/#10916 authors) are preserved. Your config-posture finding (REQUIRE_API_KEY default vs 0.0.0.0) is noted for a maintainer decision, as you scoped it.
2026-08-23 16:51:25 -03:00

85 lines
2.6 KiB
TypeScript

import { CORS_HEADERS } from "../utils/cors.ts";
/**
* Moderation Handler
*
* Handles POST /v1/moderations (OpenAI Moderations API format).
*/
import { getModerationProvider, parseModerationModel } from "../config/moderationRegistry.ts";
import { errorResponse, redactSensitiveErrorText } from "../utils/error.ts";
import { attachOmniRouteMetaHeaders } from "@/domain/omnirouteResponseMeta";
import { generateRequestId } from "@/shared/utils/requestId";
/**
* Handle moderation request
*
* @param {Object} options
* @param {Object} options.body - JSON body { model, input }
* @param {Object} options.credentials - Provider credentials { apiKey }
* @returns {Response}
*/
/** @returns {Promise<unknown>} */
export async function handleModeration({ body, credentials }) {
const startTime = Date.now();
if (!body.input) {
return errorResponse(400, "input is required");
}
// Default to latest moderation model
const model = body.model || "omni-moderation-latest";
const { provider: providerId, model: modelId } = parseModerationModel(model);
const providerConfig = providerId ? getModerationProvider(providerId) : null;
if (!providerConfig) {
return errorResponse(
400,
`No moderation provider found for model "${model}". Available: openai`
);
}
const token = credentials?.apiKey || credentials?.accessToken;
if (!token) {
return errorResponse(401, `No credentials for moderation provider: ${providerId}`);
}
try {
const res = await fetch(providerConfig.baseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
model: modelId,
input: body.input,
}),
});
if (!res.ok) {
const errText = await res.text();
// secret-leak hardening: redact any credential the upstream echoed back
// before relaying the error body to the client (structure-preserving).
return new Response(redactSensitiveErrorText(errText), {
status: res.status,
headers: {
"Content-Type": "application/json",
...CORS_HEADERS,
},
});
}
const data = await res.json();
const headers = new Headers({ ...CORS_HEADERS, "Content-Type": "application/json" });
attachOmniRouteMetaHeaders(headers, {
provider: providerId,
model: modelId,
costUsd: 0,
latencyMs: Date.now() - startTime,
requestId: generateRequestId(),
});
return new Response(JSON.stringify(data), { status: 200, headers });
} catch (err) {
return errorResponse(500, `Moderation request failed: ${err.message}`);
}
}