Generative Model Database Constraints#
Overview#
The generative_models table stores built-in and custom LLM pricing models used for cost attribution. It has two partial unique indexes (active only when deleted_at IS NULL) that enforce data integrity across the active (non-soft-deleted) row set.
Table Schema#
The GenerativeModel ORM model defines these columns relevant to constraints:
| Column | Type | Nullable | Notes |
|---|---|---|---|
name | String | NOT NULL | Human-readable display name |
provider | String | NOT NULL | LLM provider key (e.g., "AZURE_OPENAI") |
name_pattern | String (Regex) | NOT NULL | Regex used for span-to-model matching |
is_built_in | Boolean | NOT NULL | Distinguishes built-in vs. custom models |
deleted_at | Timestamp | nullable | Soft-delete marker; NULL = active |
The migration that created this table is a20694b15f82_cost.py (revision a20694b15f82).
Indexes and Constraints#
Both indexes are partial unique indexes conditioned on deleted_at IS NULL, meaning they only apply to active rows. Soft-deleted rows are excluded and can share values with active rows.
ix_generative_models_match_criteria#
UNIQUE ON (name_pattern, provider, is_built_in)
WHERE deleted_at IS NULL
Defined in models.py and created by migration in a20694b15f82_cost.py.
Purpose: Prevents two active models from having the same matching signature β i.e., the same regex pattern, same provider, and same built-in flag. This is the index used at runtime to look up a cost model for an incoming span.
ix_generative_models_name_is_built_in#
UNIQUE ON (name, is_built_in)
WHERE deleted_at IS NULL
Defined in models.py and created in the same migration .
Purpose: Prevents two active models from sharing the same display name within the same built-in/custom category.
token_prices Unique Constraint#
The child table token_prices has a separate UniqueConstraint("model_id", "token_type", "is_prompt") ensuring each model has at most one price entry per token type + prompt/completion direction.
Known Integrity Bug: Null provider Causes Misleading Conflict Error#
Issue: #14807 (reported 2026-07-28)
When provider is null or omitted in a createModel GraphQL mutation, the request always fails with "Model with name 'X' already exists" β even for names that have never been used.
Root Cause#
Two compounding problems:
-
Schema mismatch: The GraphQL input type declares
provider: Optional[str] = None, making it nullable at the API layer. But the database column isNOT NULL. Passingnulltherefore triggers a database-levelIntegrityErroron the NOT NULL constraint β not a name-uniqueness conflict at all. -
Opaque error handler: The mutation resolver catches all
IntegrityErrorexceptions with a single blanket handler and unconditionally maps them to the name-conflict message :except (PostgreSQLIntegrityError, SQLiteIntegrityError): raise Conflict(f"Model with name '{input.name}' already exists")This masks the actual cause. The same blanket handler is present in
update_model.
Impact#
- Any automated (CI/GraphQL-driven) model seeding that omits
providerwill fail with a phantom duplicate error on every attempt. - Affects PostgreSQL backend (confirmed in 19.5.0); also affects SQLite because both
IntegrityErrortypes are caught.
Workaround#
Pass any valid provider value (e.g., "AZURE_OPENAI") in the mutation input.
Fix Requirements#
- Align the schema: Either make
providerrequired in the GraphQL input, or make the DB column nullable with appropriate handling. - Discriminate errors: Inspect which constraint was violated before mapping to an error message β at minimum, distinguish NOT NULL violations from uniqueness violations.
Soft-Delete Pattern#
Deletion is implemented as a soft-delete: delete_model sets deleted_at = now() rather than removing the row. This means:
- The partial unique indexes allow previously-used
(name_pattern, provider, is_built_in)combinations to be reused after deletion. - Built-in models cannot be deleted (the mutation returns
BadRequestifis_built_inis true). SpanCost.model_idusesondelete="RESTRICT", preventing hard deletion of any model referenced by cost records β but soft deletion is allowed since no FK check fires.
Related Files#
| File | Purpose |
|---|---|
src/phoenix/db/models.py | ORM definition for GenerativeModel, indexes, and constraints |
src/phoenix/db/migrations/versions/a20694b15f82_cost.py | Migration that created generative_models, token_prices, span_costs, span_cost_details |
src/phoenix/server/api/mutations/model_mutations.py | GraphQL mutations for create/update/delete; error handling |