Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
302 changes: 290 additions & 12 deletions contracts/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,39 @@ paths:
status:
type: string
enum: [ok]
/health/live:
get:
summary: Liveness probe
operationId: getHealthLive
responses:
'200':
description: Process is alive
content:
application/json:
schema:
type: object
required: [status]
properties:
status:
type: string
enum: [ok]
/health/ready:
get:
summary: Readiness probe
operationId: getHealthReady
responses:
'200':
description: Service is ready
content:
application/json:
schema:
$ref: '#/components/schemas/HealthReadyResponse'
'503':
description: Service is degraded
content:
application/json:
schema:
$ref: '#/components/schemas/HealthReadyResponse'
/metrics:
get:
summary: Runtime metrics snapshot
Expand Down Expand Up @@ -107,13 +140,136 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/v1/moderate/batch:
post:
summary: Moderate a batch of texts
operationId: moderateBatch
security:
- ApiKeyAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ModerationBatchRequest'
responses:
'200':
description: Batch moderation result
headers:
X-RateLimit-Limit:
description: Maximum requests allowed in the current window.
schema:
type: string
X-RateLimit-Remaining:
description: Requests remaining in the current window.
schema:
type: string
X-RateLimit-Reset:
description: Seconds until the current rate-limit window resets.
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/ModerationBatchResponse'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'429':
description: Rate limit exceeded
headers:
X-RateLimit-Limit:
description: Maximum requests allowed in the current window.
schema:
type: string
X-RateLimit-Remaining:
description: Always `0` when the request is throttled.
schema:
type: string
X-RateLimit-Reset:
description: Seconds until the current rate-limit window resets.
schema:
type: string
Retry-After:
description: Seconds the client should wait before retrying.
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'500':
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/v1/appeals:
post:
summary: Submit an appeal
operationId: createPublicAppeal
security:
- ApiKeyAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PublicAppealCreateRequest'
responses:
'201':
description: Appeal submitted
content:
application/json:
schema:
$ref: '#/components/schemas/PublicAppealCreateResponse'
'400':
description: Invalid request
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'429':
description: Rate limit exceeded
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'500':
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
components:
securitySchemes:
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
schemas:
HealthReadyResponse:
type: object
additionalProperties: false
required: [status, checks]
properties:
status:
type: string
enum: [ready, degraded]
checks:
type: object
additionalProperties:
type: string
enum: [ok, empty, error]
ModerationRequest:
type: object
additionalProperties: false
Expand All @@ -125,22 +281,144 @@ components:
minLength: 1
maxLength: 5000
context:
type: object
additionalProperties: false
properties:
source:
type: string
maxLength: 100
locale:
type: string
maxLength: 20
channel:
type: string
maxLength: 50
$ref: '#/components/schemas/ModerationContext'
request_id:
type: string
maxLength: 128
pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
ModerationBatchItem:
type: object
additionalProperties: false
required: [text]
properties:
text:
type: string
minLength: 1
maxLength: 5000
context:
$ref: '#/components/schemas/ModerationContext'
request_id:
type: string
maxLength: 128
pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
ModerationBatchRequest:
type: object
additionalProperties: false
required: [items]
properties:
items:
type: array
minItems: 1
maxItems: 50
items:
$ref: '#/components/schemas/ModerationBatchItem'
ModerationBatchItemResult:
type: object
additionalProperties: false
required: [request_id]
properties:
request_id:
type: string
maxLength: 128
result:
anyOf:
- $ref: '#/components/schemas/ModerationResponse'
- type: 'null'
error:
anyOf:
- $ref: '#/components/schemas/ErrorResponse'
- type: 'null'
ModerationBatchResponse:
type: object
additionalProperties: false
required: [items, total, succeeded, failed]
properties:
items:
type: array
items:
$ref: '#/components/schemas/ModerationBatchItemResult'
total:
type: integer
minimum: 0
succeeded:
type: integer
minimum: 0
failed:
type: integer
minimum: 0
ModerationContext:
type: object
additionalProperties: false
properties:
source:
type: string
maxLength: 100
locale:
type: string
maxLength: 20
channel:
type: string
maxLength: 50
PublicAppealCreateRequest:
type: object
additionalProperties: false
required:
- decision_request_id
- original_action
- original_reason_codes
- original_model_version
- original_lexicon_version
- original_policy_version
- original_pack_versions
properties:
decision_request_id:
type: string
minLength: 1
maxLength: 128
original_action:
type: string
enum: [ALLOW, REVIEW, BLOCK]
original_reason_codes:
type: array
minItems: 1
items:
type: string
pattern: '^R_[A-Z0-9_]+$'
original_model_version:
type: string
minLength: 1
maxLength: 128
original_lexicon_version:
type: string
minLength: 1
maxLength: 128
original_policy_version:
type: string
minLength: 1
maxLength: 128
original_pack_versions:
type: object
minProperties: 1
additionalProperties:
type: string
reason:
type: string
maxLength: 500
PublicAppealCreateResponse:
type: object
additionalProperties: false
required: [appeal_id, status, request_id]
properties:
appeal_id:
type: integer
minimum: 1
status:
type: string
enum: [submitted]
request_id:
type: string
minLength: 1
maxLength: 128
ModerationResponse:
type: object
additionalProperties: false
Expand Down
51 changes: 51 additions & 0 deletions migrations/0013_multi_model_embeddings.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
-- 0013_multi_model_embeddings.sql
--
-- Adds a multi-model embedding table to support coexisting embedding backends
-- (e.g. 64-dim hash-bow-v1 and 384-dim e5-multilingual-small-v1) while keeping
-- per-model ANN indexes dimension-consistent via partial indexes.
--
-- This migration is additive: it does not modify or drop lexicon_entry_embeddings (v1).

CREATE TABLE IF NOT EXISTS lexicon_entry_embeddings_v2 (
lexicon_entry_id BIGINT NOT NULL
REFERENCES lexicon_entries (id)
ON DELETE CASCADE,
embedding_model TEXT NOT NULL,
embedding_dim INT NOT NULL,
embedding VECTOR NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (lexicon_entry_id, embedding_model),
CONSTRAINT lexicon_entry_embeddings_v2_embedding_dim_check
CHECK (vector_dims(embedding) = embedding_dim)
);

CREATE INDEX IF NOT EXISTS ix_lex_emb_v2_model
ON lexicon_entry_embeddings_v2 (embedding_model, updated_at DESC);

-- Partial ANN indexes (IVFFlat requires a fixed vector dimension per index).
CREATE INDEX IF NOT EXISTS ix_lex_emb_v2_hash_bow_v1
ON lexicon_entry_embeddings_v2
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 32)
WHERE embedding_model = 'hash-bow-v1';

CREATE INDEX IF NOT EXISTS ix_lex_emb_v2_e5_small_v1
ON lexicon_entry_embeddings_v2
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 32)
WHERE embedding_model = 'e5-multilingual-small-v1';

-- Backfill existing hash-bow-v1 vectors from v1 to v2 (idempotent).
INSERT INTO lexicon_entry_embeddings_v2
(lexicon_entry_id, embedding_model, embedding_dim, embedding, created_at, updated_at)
SELECT
lexicon_entry_id,
'hash-bow-v1',
64,
embedding,
created_at,
updated_at
FROM lexicon_entry_embeddings
WHERE embedding_model = 'hash-bow-v1'
ON CONFLICT (lexicon_entry_id, embedding_model) DO NOTHING;
Loading
Loading