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
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""backfill legacy config completion type

Revision ID: 047
Revises: 046
Create Date: 2026-02-17 00:00:00.000000

"""
from alembic import op

revision = "047"
down_revision = "046"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.execute(
"""
UPDATE config_version
SET config_blob = jsonb_set(config_blob, '{completion,type}', '"text"')
WHERE config_blob->'completion' IS NOT NULL
AND config_blob->'completion'->>'type' IS NULL
"""
)


def downgrade() -> None:
# No-op: removing type='text' would drop a required field and break validation; NULL safely defaults to 'text'.
pass
10 changes: 9 additions & 1 deletion backend/app/crud/config/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ def _validate_immutable_fields(
existing_type = existing_completion.get("type")
merged_type = merged_completion.get("type")

# Legacy configs predate the 'type' field; all were text-only
if existing_type is None:
existing_type = "text"

if existing_type != merged_type:
raise HTTPException(
status_code=400,
Expand Down Expand Up @@ -274,7 +278,11 @@ def _validate_config_type_unchanged(
version_create.config_blob.model_dump().get("completion", {}).get("type")
)

if old_type is None or new_type is None:
# Legacy configs predate the 'type' field; all were text-only
if old_type is None:
old_type = "text"

if new_type is None:
logger.error(
f"[ConfigVersionCrud._validate_config_type_unchanged] Missing type field | "
f"{{'config_id': '{self.config_id}', 'old_type': {old_type}, 'new_type': {new_type}}}"
Expand Down
72 changes: 72 additions & 0 deletions backend/app/tests/crud/config/test_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,78 @@ def test_create_version_different_configs(
assert version2_config2.config_id == config2.id


def test_validate_immutable_fields_legacy_config_allows_text_update(
db: Session, example_config_blob: ConfigBlob
) -> None:
"""Test that a legacy config (no type field) allows updates with type='text'."""
config = create_test_config(db)
version_crud = ConfigVersionCrud(
session=db, project_id=config.project_id, config_id=config.id
)

# Simulate a legacy config_blob without 'type' in completion
legacy_blob = {
"completion": {
"provider": "openai-native",
"params": {
"model": "gpt-4",
"temperature": 0.7,
"max_tokens": 1000,
},
}
}
merged_blob = {
"completion": {
"provider": "openai-native",
"type": "text",
"params": {
"model": "gpt-4",
"temperature": 0.8,
"max_tokens": 1500,
},
}
}

# Should NOT raise — legacy configs default to "text"
version_crud._validate_immutable_fields(legacy_blob, merged_blob)


def test_validate_immutable_fields_legacy_config_rejects_non_text_update(
db: Session, example_config_blob: ConfigBlob
) -> None:
"""Test that a legacy config (no type field) rejects updates with type != 'text'."""
config = create_test_config(db)
version_crud = ConfigVersionCrud(
session=db, project_id=config.project_id, config_id=config.id
)

legacy_blob = {
"completion": {
"provider": "openai-native",
"params": {
"model": "gpt-4",
"temperature": 0.7,
},
}
}
merged_blob = {
"completion": {
"provider": "openai-native",
"type": "stt",
"params": {
"model": "gpt-4",
"temperature": 0.8,
},
}
}

with pytest.raises(
HTTPException,
match="Cannot change config type from 'text' to 'stt'",
):
version_crud._validate_immutable_fields(legacy_blob, merged_blob)


def test_read_all_versions_config_not_found(db: Session) -> None:
"""Test reading versions for a non-existent config raises HTTPException."""
project = create_test_project(db)
Expand Down