149 lines
5.8 KiB
TypeScript
149 lines
5.8 KiB
TypeScript
import { Type, type Static } from "@sinclair/typebox";
|
|
import { Value } from "@sinclair/typebox/value";
|
|
|
|
const uuidPattern = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$";
|
|
|
|
export const stableEngineeringErrors = {
|
|
BROWSER_UNSUPPORTED: { httpStatus: 426, messageKey: "browser.unsupported" },
|
|
MODEL_CONFIG_VERSION_CONFLICT: { httpStatus: 412, messageKey: "MODEL_CONFIG_VERSION_CONFLICT" },
|
|
MODEL_DEFAULT_REPLACEMENT_REQUIRED: { httpStatus: 409, messageKey: "MODEL_DEFAULT_REPLACEMENT_REQUIRED" },
|
|
MODEL_DEFAULT_REPLACEMENT_INVALID: { httpStatus: 409, messageKey: "MODEL_DEFAULT_REPLACEMENT_INVALID" },
|
|
MODEL_RECOMMENDATION_PRIORITY_INVALID: { httpStatus: 400, messageKey: "MODEL_RECOMMENDATION_PRIORITY_INVALID" },
|
|
MODEL_RECOMMENDATION_PRIORITY_CONFLICT: { httpStatus: 409, messageKey: "MODEL_RECOMMENDATION_PRIORITY_CONFLICT" },
|
|
PRIVATE_CONTENT_NOTICE_ACK_REQUIRED: { httpStatus: 428, messageKey: "PRIVATE_CONTENT_NOTICE_ACK_REQUIRED" },
|
|
ASSET_HISTORY_REFERENCE_CONFLICT: { httpStatus: 409, messageKey: "ASSET_HISTORY_REFERENCE_CONFLICT" },
|
|
ASSET_CLEANUP_CANDIDATE_STALE: { httpStatus: 409, messageKey: "ASSET_CLEANUP_CANDIDATE_STALE" },
|
|
STORAGE_CAPACITY_EXCEEDED: { httpStatus: 507, messageKey: "STORAGE_CAPACITY_EXCEEDED" },
|
|
} as const;
|
|
|
|
export type StableEngineeringErrorCode = keyof typeof stableEngineeringErrors;
|
|
|
|
export const CorrelationIdSchema = Type.String({ pattern: uuidPattern, $id: "CorrelationId" });
|
|
export const GenerationErrorCategorySchema = Type.Union(
|
|
[
|
|
Type.Literal("upstream_timeout"),
|
|
Type.Literal("upstream_failed"),
|
|
Type.Literal("safety_rejected"),
|
|
Type.Literal("model_disabled"),
|
|
Type.Literal("gateway_balance_insufficient"),
|
|
Type.Literal("gateway_contract_invalid"),
|
|
Type.Literal("reference_invalid"),
|
|
Type.Literal("unknown_retryable"),
|
|
Type.Literal("unknown_non_retryable"),
|
|
],
|
|
{ $id: "GenerationErrorCategory" },
|
|
);
|
|
export const StableEngineeringErrorCodeSchema = Type.Union(
|
|
[
|
|
Type.Literal("BROWSER_UNSUPPORTED"),
|
|
Type.Literal("MODEL_CONFIG_VERSION_CONFLICT"),
|
|
Type.Literal("MODEL_DEFAULT_REPLACEMENT_REQUIRED"),
|
|
Type.Literal("MODEL_DEFAULT_REPLACEMENT_INVALID"),
|
|
Type.Literal("MODEL_RECOMMENDATION_PRIORITY_INVALID"),
|
|
Type.Literal("MODEL_RECOMMENDATION_PRIORITY_CONFLICT"),
|
|
Type.Literal("PRIVATE_CONTENT_NOTICE_ACK_REQUIRED"),
|
|
Type.Literal("ASSET_HISTORY_REFERENCE_CONFLICT"),
|
|
Type.Literal("ASSET_CLEANUP_CANDIDATE_STALE"),
|
|
Type.Literal("STORAGE_CAPACITY_EXCEEDED"),
|
|
],
|
|
{ $id: "StableEngineeringErrorCode" },
|
|
);
|
|
export const ErrorDetailsSchema = Type.Object(
|
|
{
|
|
reason: Type.Optional(
|
|
Type.Union([
|
|
Type.Literal("platform_unsupported"),
|
|
Type.Literal("brand_unsupported"),
|
|
Type.Literal("version_unsupported"),
|
|
Type.Literal("identity_unavailable"),
|
|
]),
|
|
),
|
|
supported_browsers: Type.Optional(
|
|
Type.Array(
|
|
Type.Object(
|
|
{
|
|
brand: Type.Union([Type.Literal("Google Chrome"), Type.Literal("Microsoft Edge")]),
|
|
major: Type.Integer({ minimum: 1 }),
|
|
},
|
|
{ additionalProperties: false },
|
|
),
|
|
{ maxItems: 2 },
|
|
),
|
|
),
|
|
capacity_status: Type.Optional(
|
|
Type.Union([
|
|
Type.Literal("normal"),
|
|
Type.Literal("warning"),
|
|
Type.Literal("critical"),
|
|
Type.Literal("full"),
|
|
Type.Literal("unavailable"),
|
|
]),
|
|
),
|
|
current_task_ref: Type.Optional(Type.String({ maxLength: 160, pattern: "^[a-z][a-z0-9_]*:[A-Za-z0-9_-]+$" })),
|
|
field_errors: Type.Optional(
|
|
Type.Array(
|
|
Type.Object(
|
|
{
|
|
field: Type.String({ maxLength: 120, pattern: "^[A-Za-z0-9_.\\[\\]-]+$" }),
|
|
message_key: Type.String({ maxLength: 120, pattern: "^[A-Za-z0-9_.-]+$" }),
|
|
},
|
|
{ additionalProperties: false },
|
|
),
|
|
{ maxItems: 32 },
|
|
),
|
|
),
|
|
latest_version: Type.Optional(Type.Union([Type.Integer({ minimum: 0 }), Type.String({ maxLength: 120 })])),
|
|
remaining_bytes: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
},
|
|
{ additionalProperties: false, $id: "ErrorDetails" },
|
|
);
|
|
export const ErrorEnvelopeSchema = Type.Object(
|
|
{
|
|
error: Type.Object(
|
|
{
|
|
code: StableEngineeringErrorCodeSchema,
|
|
message_key: Type.String({ maxLength: 120, pattern: "^[A-Za-z0-9_.-]+$" }),
|
|
correlation_id: CorrelationIdSchema,
|
|
error_category: Type.Optional(GenerationErrorCategorySchema),
|
|
details: ErrorDetailsSchema,
|
|
},
|
|
{ additionalProperties: false },
|
|
),
|
|
},
|
|
{ additionalProperties: false, $id: "ErrorEnvelope" },
|
|
);
|
|
|
|
export type ErrorDetails = Static<typeof ErrorDetailsSchema>;
|
|
export type ErrorEnvelope = Static<typeof ErrorEnvelopeSchema>;
|
|
export type GenerationErrorCategory = Static<typeof GenerationErrorCategorySchema>;
|
|
|
|
export function isCorrelationId(value: unknown): value is string {
|
|
return Value.Check(CorrelationIdSchema, value);
|
|
}
|
|
|
|
export function isErrorEnvelope(value: unknown): value is ErrorEnvelope {
|
|
if (!Value.Check(ErrorEnvelopeSchema, value)) return false;
|
|
const definition = stableEngineeringErrors[value.error.code];
|
|
return definition.messageKey === value.error.message_key;
|
|
}
|
|
|
|
export function createErrorEnvelope(input: {
|
|
code: StableEngineeringErrorCode;
|
|
correlationId: string;
|
|
details?: ErrorDetails;
|
|
errorCategory?: GenerationErrorCategory;
|
|
}): ErrorEnvelope {
|
|
const definition = stableEngineeringErrors[input.code];
|
|
const envelope: ErrorEnvelope = {
|
|
error: {
|
|
code: input.code,
|
|
correlation_id: input.correlationId,
|
|
details: input.details ?? {},
|
|
message_key: definition.messageKey,
|
|
...(input.errorCategory ? { error_category: input.errorCategory } : {}),
|
|
},
|
|
};
|
|
if (!isErrorEnvelope(envelope)) throw new Error("Error envelope does not match the frozen schema.");
|
|
return envelope;
|
|
}
|