Skip to content

feat: REST API PUT /prompt_versions/{id}/tags/{tag_name} — upsert tag#12428

Open
mikeldking wants to merge 2 commits intomainfrom
claude/issue-12424
Open

feat: REST API PUT /prompt_versions/{id}/tags/{tag_name} — upsert tag#12428
mikeldking wants to merge 2 commits intomainfrom
claude/issue-12424

Conversation

@mikeldking
Copy link
Copy Markdown
Collaborator

Summary

Implements PUT /prompt_versions/{prompt_version_id}/tags/{tag_name} — the upsert endpoint for prompt version tags, as specified in #12424.

  • Creates the tag if it doesn't exist on any version of the prompt
  • Moves the tag to this version if it already exists on another version of the same prompt (upsert semantics via ON CONFLICT DO UPDATE on the unique (name, prompt_id) constraint)
  • Returns 204 No Content on success
  • Requires MEMBER+ write access (via Depends(is_not_locked))
  • Optional JSON body: {"description": "..."} — all fields optional, body may be omitted entirely

What changed

  • src/phoenix/server/api/routers/v1/prompts.py: Added UpsertPromptVersionTagRequestBody model and PUT /prompt_versions/{prompt_version_id}/tags/{tag_name} endpoint
  • schemas/openapi.json: Added new path and UpsertPromptVersionTagRequestBody schema component
  • tests/unit/server/api/routers/v1/test_prompts.py: Six new unit tests covering create, move, no-body, invalid version ID, version not found, and invalid tag name cases
  • tests/integration/_helpers.py: Registered the new PUT endpoint in _VIEWER_BLOCKED_WRITE_OPERATIONS to ensure viewer-role access control coverage

How to verify

# Create a prompt version, then tag it:
PUT /v1/prompt_versions/<version_id>/tags/production
Content-Type: application/json

{"description": "Current production version"}
# 204 No Content

# Move the same tag to a different version:
PUT /v1/prompt_versions/<other_version_id>/tags/production
# 204 No Content (tag moved from first version to this one)

Checklist

  • Implementation follows existing codebase patterns (mirrors createPromptVersionTag POST endpoint)
  • except ValidationError used (consistent with other Identifier validation in file)
  • Depends(is_not_locked) applied
  • 6 unit tests covering happy path, upsert/move, no-body, 404, and 422 cases
  • Viewer-blocked write operations list updated in integration test helpers
  • OpenAPI schema updated
  • No JS/TS packages modified — no changeset needed

🤖 Generated with Claude Code

Implements the REST endpoint to create or move a named prompt version
tag. If the tag already exists on another version of the same prompt, it
is moved to the specified version (upsert semantics using ON CONFLICT DO
UPDATE on the unique (name, prompt_id) constraint).

Closes #12424

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mikeldking mikeldking requested a review from a team as a code owner March 28, 2026 05:09
@github-project-automation github-project-automation Bot moved this to 📘 Todo in phoenix Mar 28, 2026
@mintlify
Copy link
Copy Markdown
Contributor

mintlify Bot commented Mar 28, 2026

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
arize-phoenix 🟢 Ready View Preview Mar 28, 2026, 5:10 AM

@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Mar 28, 2026
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 28, 2026

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

The filter_by queries were passing plain strings where the column type
expects Identifier, causing SQLAlchemy bind-param assertion failures.
Copy link
Copy Markdown
Contributor

@anticorrelator anticorrelator left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good — clean implementation that follows existing patterns well. One thing to address:

Upsert silently wipes existing tag description when no body is sent

When request_body is None or {}, description resolves to None. Since insert_on_conflict with DO_UPDATE updates all non-PK columns (via _clean(stmt.excluded.items())), a caller who only wants to move a tag to a different version loses its description as a side effect.

This affects both the no-body and empty-body cases. A few options:

  1. Only include description in the conflict update set when it's explicitly provided in the body
  2. Use COALESCE(excluded.description, prompt_version_tag.description) to preserve existing values
  3. Document the behavior explicitly if it's intentional

Also worth adding test coverage for: (a) upsert that updates description, (b) move that verifies description is preserved/cleared as intended.

@github-project-automation github-project-automation Bot moved this from 📘 Todo to 👍 Approved in phoenix Mar 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files.

Projects

Status: 👍 Approved

Development

Successfully merging this pull request may close these issues.

2 participants