Skip to content

fix(models): tag privacy-filter as a non-completion (classification) model instead of hiding it - #872

Open
Evrard-Nil wants to merge 1 commit into
mainfrom
fix/models-exclude-non-generative
Open

fix(models): tag privacy-filter as a non-completion (classification) model instead of hiding it#872
Evrard-Nil wants to merge 1 commit into
mainfrom
fix/models-exclude-non-generative

Conversation

@Evrard-Nil

@Evrard-Nil Evrard-Nil commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Problem

GET /v1/models (and GET /v1/model/list) advertised openai/privacy-filter as if it were an ordinary chat model. It is a token-classification (PII detection) model: its only real endpoints are /v1/privacy/classify and /v1/privacy/redact, and a /v1/chat/completions request against it hits a non-existent upstream path and 404s. Because the catalog exposed no notion of its kind, a client had no way to tell it apart from a completion model.

Fixes #615.

Approach (revised per maintainer feedback)

Maintainer feedback on the earlier version of this PR: "Model should be there but tagged as not a completion model. Same as image models. Hiding it is not good."

This PR supersedes its earlier hide-based version. Instead of filtering the model out of the catalog, it keeps the model listed and tags it with its true modality — exactly the way image and embedding models are represented ("list + tag, same as image models").

Why tagging alone is sufficient

The catalog already encodes model kind in output_modalities, and both listing endpoints already surface that field to clients:

  • GET /v1/models → each ModelInfo carries both the OpenRouter-flat output_modalities and the nested architecture.outputModalities.
  • GET /v1/model/list → each model's architecture.outputModalities (built by ModelArchitecture::from_options).

An image model is listed with outputModalities = ["image"]; an embedding model with ["embedding"]. The privacy filter was simply mislabeled ["text"], so it masqueraded as a chat model. Relabeling its output modality to ["classification"] makes it self-describe as a classifier — a client distinguishes it by reading output_modalities, exactly as it would for an image model. No new field or scheme is introduced; this reuses the existing modality mechanism.

Changes

  • Migration V0069 relabels the openai/privacy-filter row's output_modalities from the mislabeled ["text"] to ["classification"] (and mirrors the fix into the currently-open model_history snapshot). This is the whole fix. It is idempotent, a no-op on databases without the row (fresh/test DBs), and mirrors the in-place catalog-metadata repair pattern used by V0061. Input modality stays ["text"] — it still consumes text. The migration comment block states its purpose is to correctly tag the model's modality, not to feed an api-layer hide filter.
    • Renumbered V0068V0069: since this branch first opened, main landed its own V0068 (V0068__cache_read_cost_nullable.sql). Two migrations sharing version 0068 would make refinery reject startup, so this one moves to the next free version. The branch has also been rebased onto current main so it carries main's V0068 and this V0069 side by side.
  • Removed the catalog-hiding code from the earlier revision: the .filter(...) in routes::completions::models (/v1/models) and routes::models::list_models (/v1/model/list), plus the now-unused is_listed_in_model_catalog helper, the NON_GENERATIVE_OUTPUT_MODALITIES const, and its unit test in routes::common. (Net effect: common.rs and models.rs are unchanged from main; completions.rs gains only an explanatory comment.)

The privacy endpoints and direct GET /v1/model/{name} lookups were never affected either way.

Note: the branch name (fix/models-exclude-non-generative) is now a slight misnomer — the fix tags rather than excludes/hides. Kept to preserve PR #872's URL and history.

Note: the issue also raised (2) error categorization (upstream 404 → 400). That is a separate systemic wrapping concern shared with #605#609 and is intentionally out of scope here.

Tests

  • Replaced test_non_generative_models_excluded_from_models_list (asserted exclusion) with test_classification_model_listed_and_tagged: asserts openai/privacy-filter is listed in /v1/models and reports output_modalities / architecture.outputModalities = ["classification"], alongside a chat model still reporting ["text"].
  • Adjusted test_models_api to locate the configured chat model by id rather than data.first(). With the classifier no longer hidden, the shared e2e catalog can now contain a zero-priced classification model that may sort first under some collations; keying off the first entry was order-dependent. Every other listing test already scopes by id.

Validation (rebased state: main + this change)

  • cargo fmt --all -- --check — clean
  • cargo check --workspace --tests — clean
  • cargo clippy --workspace --tests — clean
  • e2e against real Postgres: full migration set (including main's V0068 and this V0069) applies cleanly on a fresh bootstrap. test_classification_model_listed_and_tagged and test_models_api both pass, including against a catalog already containing the zero-priced classifier.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a mechanism to filter out non-generative models (such as token-classification models) from the public model catalog endpoints. It adds a helper function is_listed_in_model_catalog to determine visibility based on output modalities, integrates this filter into the models list and completions endpoints, adds a database migration to update the openai/privacy-filter model's modalities, and includes corresponding tests. Feedback focuses on improving the Rust API design by passing Option<&[String]> instead of &Option<Vec<String>> to avoid unnecessary allocations and references, and refactoring the SQL migration to avoid an unused data-modifying CTE.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +21 to +43
WITH target_model AS (
SELECT id
FROM models
WHERE model_name = 'openai/privacy-filter'
),
updated_model AS (
UPDATE models
SET
output_modalities = ARRAY['classification']::TEXT[],
updated_at = NOW()
WHERE model_name = 'openai/privacy-filter'
AND output_modalities IS DISTINCT FROM ARRAY['classification']::TEXT[]
RETURNING id
)
-- Correct the open snapshot in place (a metadata fix, not a new state
-- transition), independent of the `models` UPDATE so a prior manual patch to
-- `models` still repairs the open history snapshot.
UPDATE model_history mh
SET output_modalities = ARRAY['classification']::TEXT[]
FROM target_model
WHERE mh.model_id = target_model.id
AND mh.effective_until IS NULL
AND mh.output_modalities IS DISTINCT FROM ARRAY['classification']::TEXT[];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Defining updated_model as a data-modifying CTE but never referencing it in the main query is a code smell. While PostgreSQL executes data-modifying CTEs to completion even if they are not referenced, this behavior is non-standard and highly confusing to other developers. It is much cleaner and more idiomatic to split this into two separate, sequential SQL statements.

UPDATE models
SET
    output_modalities = ARRAY['classification']::TEXT[],
    updated_at = NOW()
WHERE model_name = 'openai/privacy-filter'
  AND output_modalities IS DISTINCT FROM ARRAY['classification']::TEXT[];

UPDATE model_history mh
SET output_modalities = ARRAY['classification']::TEXT[]
FROM models m
WHERE mh.model_id = m.id
  AND m.model_name = 'openai/privacy-filter'
  AND mh.effective_until IS NULL
  AND mh.output_modalities IS DISTINCT FROM ARRAY['classification']::TEXT[];

Comment thread crates/api/src/routes/common.rs Outdated
Comment on lines +537 to +544
pub fn is_listed_in_model_catalog(output_modalities: &Option<Vec<String>>) -> bool {
match output_modalities {
Some(modalities) if !modalities.is_empty() => !modalities
.iter()
.all(|m| NON_GENERATIVE_OUTPUT_MODALITIES.contains(&m.as_str())),
_ => true,
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Passing &Option<Vec<T>> as a function argument is a Rust anti-pattern. It unnecessarily restricts the caller to have an owned Option<Vec<T>> and forces a reference to it. Instead, use Option<&[T]> (or Option<&[String]> here) and let the caller use .as_deref() to convert their Option<Vec<T>> to Option<&[T]>.

Suggested change
pub fn is_listed_in_model_catalog(output_modalities: &Option<Vec<String>>) -> bool {
match output_modalities {
Some(modalities) if !modalities.is_empty() => !modalities
.iter()
.all(|m| NON_GENERATIVE_OUTPUT_MODALITIES.contains(&m.as_str())),
_ => true,
}
}
pub fn is_listed_in_model_catalog(output_modalities: Option<&[String]>) -> bool {
match output_modalities {
Some(modalities) if !modalities.is_empty() => !modalities
.iter()
.all(|m| NON_GENERATIVE_OUTPUT_MODALITIES.contains(&m.as_str())),
_ => true,
}
}

Comment thread crates/api/src/routes/common.rs Outdated
Comment on lines +849 to +851
let listed = |m: &[&str]| {
is_listed_in_model_catalog(&Some(m.iter().map(|s| s.to_string()).collect()))
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Update the test helper to match the new signature of is_listed_in_model_catalog taking Option<&[String]>.

Suggested change
let listed = |m: &[&str]| {
is_listed_in_model_catalog(&Some(m.iter().map(|s| s.to_string()).collect()))
};
let listed = |m: &[&str]| {
let modalities: Vec<String> = m.iter().map(|s| s.to_string()).collect();
is_listed_in_model_catalog(Some(&modalities))
};

Comment thread crates/api/src/routes/common.rs Outdated
Comment on lines +873 to +874
assert!(is_listed_in_model_catalog(&None));
assert!(is_listed_in_model_catalog(&Some(vec![])));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Update the assertions to pass None and Some(&[]) directly instead of referencing them.

Suggested change
assert!(is_listed_in_model_catalog(&None));
assert!(is_listed_in_model_catalog(&Some(vec![])));
assert!(is_listed_in_model_catalog(None));
assert!(is_listed_in_model_catalog(Some(&[])));

Comment thread crates/api/src/routes/completions.rs Outdated
// (issue #615). Chat/image/embedding models are unaffected.
data: models
.into_iter()
.filter(|m| crate::routes::common::is_listed_in_model_catalog(&m.output_modalities))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Use .as_deref() to pass Option<&[String]> to is_listed_in_model_catalog.

Suggested change
.filter(|m| crate::routes::common::is_listed_in_model_catalog(&m.output_modalities))
.filter(|m| crate::routes::common::is_listed_in_model_catalog(m.output_modalities.as_deref()))

Comment thread crates/api/src/routes/models.rs Outdated
// count stays consistent across pages.
let all_models: Vec<_> = all_models
.into_iter()
.filter(|m| crate::routes::common::is_listed_in_model_catalog(&m.output_modalities))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Use .as_deref() to pass Option<&[String]> to is_listed_in_model_catalog.

Suggested change
.filter(|m| crate::routes::common::is_listed_in_model_catalog(&m.output_modalities))
.filter(|m| crate::routes::common::is_listed_in_model_catalog(m.output_modalities.as_deref()))

@claude

claude Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review — PR #872 (exclude non-generative models from /v1/models)

Reviewed the diff against the catalog, privacy/auto-redact, billing, and discovery paths. This is a clean, well-scoped fix. No critical issues found.

Verified correct:

  • is_listed_in_model_catalog logic is sound — hides only when every declared modality is non-generative; None/empty modalities default to listed (backfill-safe); a mixed ["classification","text"] model correctly stays listed since it can still serve the generative surface.
  • Filter applied to both listing endpoints, and in list_models it precedes pagination so total and per-page results stay consistent.
  • Privacy endpoints unaffected: privacy_classify (via get_model_by_name), auto_redact (via the provider pool), and the classify-pass billing lookup (get_models_with_pricing called directly, unfiltered) all bypass the two route handlers where the filter lives. Direct GET /v1/model/{name} still resolves.
  • Migration V0068: correctly numbered (follows V0067), idempotent via IS DISTINCT FROM guards, and a no-op on DBs lacking the row. The unreferenced updated_model CTE still executes — PostgreSQL runs data-modifying CTEs to completion regardless of whether the primary query reads their output — so the models row is relabeled, and the separate model_history UPDATE repairs the open (effective_until IS NULL) snapshot. Good mirror of the V0061 in-place-repair pattern.

Minor, non-blocking observations:

  • The migration's durability depends on operators not re-upserting openai/privacy-filter with an explicit outputModalities: ["text"] — the repo upsert uses COALESCE(EXCLUDED.output_modalities, models.output_modalities), so a re-upsert that omits the field is safe, but one that re-sends ["text"] would revert it. Worth a heads-up to whoever manages the catalog seed, but not a code defect.
  • The filter is duplicated at both route handlers rather than the service layer. That's fine here (both call sites are covered and the shared helper keeps it DRY), just noting for future catalog surfaces — any new endpoint returning the catalog will need the same filter.

Test coverage (unit test_is_listed_in_model_catalog + e2e test_non_generative_models_excluded_from_models_list) matches the change well.

✅ (approved)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR prevents non-generative models (specifically openai/privacy-filter, a token-classification/PII model) from being advertised as OpenAI-compatible generative models in the public model catalogs, by filtering listings based on output_modalities and repairing the catalog metadata via a migration.

Changes:

  • Added routes::common::is_listed_in_model_catalog + NON_GENERATIVE_OUTPUT_MODALITIES to hide models whose declared output modalities are entirely non-generative.
  • Applied the filter to both listing endpoints: GET /v1/models and GET /v1/model/list (the latter before pagination).
  • Added migration V0068 to relabel openai/privacy-filter output_modalities to ["classification"], plus an e2e test asserting exclusion from /v1/models.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
crates/database/src/migrations/sql/V0068__mark_privacy_filter_non_generative.sql Repairs the seeded openai/privacy-filter output_modalities (and open history snapshot) to enable filtering it out of public catalogs.
crates/api/tests/e2e_all/general.rs Adds e2e coverage ensuring classification-only models don’t appear in GET /v1/models.
crates/api/src/routes/models.rs Filters non-generative models out of GET /v1/model/list prior to pagination for consistent total and pages.
crates/api/src/routes/completions.rs Filters non-generative models out of GET /v1/models.
crates/api/src/routes/common.rs Introduces the shared catalog-filter predicate and its unit test.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +31 to +33
WHERE model_name = 'openai/privacy-filter'
AND output_modalities IS DISTINCT FROM ARRAY['classification']::TEXT[]
RETURNING id
Comment on lines +41 to +43
WHERE mh.model_id = target_model.id
AND mh.effective_until IS NULL
AND mh.output_modalities IS DISTINCT FROM ARRAY['classification']::TEXT[];
@Evrard-Nil
Evrard-Nil force-pushed the fix/models-exclude-non-generative branch from ec1e98f to cb464d7 Compare July 8, 2026 16:21
@Evrard-Nil
Evrard-Nil had a problem deploying to Cloud API test env July 8, 2026 16:22 — with GitHub Actions Failure

@PierreLeGuen PierreLeGuen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The change itself is well-scoped and correct — a denylist filter on output_modalities applied to both user-facing listing endpoints (GET /v1/models in completions.rs, list_models in models.rs), safe by construction for models with no declared modalities or mixed modalities, with admin listing unaffected. The blocker is the migration, which fails to apply against the actual column types.

  • Blockingcrates/database/src/migrations/sql/V0068__mark_privacy_filter_non_generative.sql:29: the migration writes/compares ARRAY['classification']::TEXT[] against output_modalities, but V0043 defines both models.output_modalities and model_history.output_modalities as JSONB. CI E2E fails on V68 with ERROR: operator does not exist: jsonb = text[] (run 28957601638), so merging breaks migrations at deploy startup. Use JSONB throughout on both the models and model_history updates, e.g. set output_modalities = '["classification"]'::jsonb and guard with IS DISTINCT FROM '["classification"]'::jsonb.
  • Non-blocking — correct the catalog seed source for openai/privacy-filter to outputModalities: ["classification"] as well; a future re-upsert that explicitly re-sends ["text"] would revert this migration's effect.

Checks run: cargo check -p api passed and LSP diagnostics were clean on all changed files; CI inspected via gh run view, where lint/unit/integration passed and E2E failed on the migration above. Full test suite / migration apply not run locally (requires PostgreSQL).

@Evrard-Nil
Evrard-Nil force-pushed the fix/models-exclude-non-generative branch from cb464d7 to 8911538 Compare July 8, 2026 16:25
@Evrard-Nil
Evrard-Nil temporarily deployed to Cloud API test env July 8, 2026 16:25 — with GitHub Actions Inactive
@Evrard-Nil

Copy link
Copy Markdown
Contributor Author

Thanks @PierreLeGuen — I believe the blocking item was fixed in the current commit (8911538d), which landed right after your review, so the review was against the earlier TEXT[] version:

  • V0068 migration (blocking) — now JSONB throughout. Both the models and model_history updates set output_modalities = '["classification"]'::jsonb and are guarded with IS DISTINCT FROM '["classification"]'::jsonb (the form you suggested). The operator does not exist: jsonb = text[] failure is gone and E2E is green on the current commit.
  • Option<&[String]> / .as_deref() (gemini nits) — done: the helper is now is_listed_in_model_catalog(output_modalities: Option<&[String]>), the completions.rs and models.rs call sites pass m.output_modalities.as_deref(), and the unit test helper + assertions were updated to match.
  • Non-blocking — catalog seed source — good catch. output_modalities is populated from model discovery, so a future re-upsert that re-sends ["text"] would revert this migration. I think the durable fix belongs at the discovery/registry source and would suggest a follow-up rather than expanding this PR — happy to file a tracking issue and link it here. Let me know if you'd prefer it handled in-scope.

Re-requesting review — PTAL when you get a chance.

@Evrard-Nil
Evrard-Nil requested a review from PierreLeGuen July 9, 2026 07:39

@PierreLeGuen PierreLeGuen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The change correctly excludes non-generative models (e.g. privacy-filter) from both public listing endpoints, and the earlier blocking migration issue is resolved. Approving.

  • The filter in is_listed_in_model_catalog hides a model only when every declared modality is non-generative; None/empty modalities default to listed (backfill-safe), and mixed lists like ["classification","text"] stay listed. Both /v1/models (completions.rs) and /v1/model/list (models.rs, filtered before pagination so total stays consistent) are covered, and the privacy/auto-redact resolution paths are unaffected.
  • Migration V0068__mark_privacy_filter_non_generative.sql: now uses JSONB throughout ('["classification"]'::jsonb) matching the JSONB output_modalities columns from V0043, is idempotent via IS DISTINCT FROM, and is consecutively numbered after V0067 — the prior TEXT[]-vs-JSONB blocker is fixed.

Optional follow-up (non-blocking): the relabel in V0068 is durable only while the model-discovery/seed source doesn't re-upsert openai/privacy-filter with an explicit outputModalities: ["text"]. An omit-the-field re-upsert is safe (COALESCE(EXCLUDED...)), but a re-send of ["text"] would silently revert it. Already tracked in-thread; worth fixing at the discovery source to emit ["classification"].

Checks: cargo check --workspace --tests and cargo test -p api --lib test_is_listed_in_model_catalog passed; LSP diagnostics on common.rs/completions.rs/models.rs clean; CI (lint, unit, integration, e2e, security audit) green on 8911538d.

…model

`openai/privacy-filter` is a token-classification (PII detection) model, not a
generative chat model, but GET /v1/models and GET /v1/model/list advertised it
as an ordinary chat model. Rather than hiding it, tag it with its true modality
so clients can tell it apart, the same way image and embedding models are
represented.

Migration V0069 relabels the model row's output_modalities from the mislabeled
["text"] to ["classification"] (and mirrors the fix into the open model_history
snapshot). Both listing endpoints already surface output_modalities /
architecture.outputModalities to clients, so this tag alone achieves parity with
how image models (["image"]) are listed. No new field is introduced and no
catalog filtering is added.

Replaces test_non_generative_models_excluded_from_models_list with
test_classification_model_listed_and_tagged (asserts the model is listed AND
carries the classification tag). test_models_api now locates the configured chat
model by id instead of the first catalog entry, so it is robust to the shared
test catalog containing a zero-priced classification model.

Fixes #615.
@Evrard-Nil
Evrard-Nil force-pushed the fix/models-exclude-non-generative branch from 8911538 to 64e5726 Compare July 9, 2026 08:20
@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

Bugbot is not enabled for this team, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@Evrard-Nil Evrard-Nil changed the title fix(models): exclude non-generative models (e.g. privacy-filter) from /v1/models fix(models): tag privacy-filter as a non-completion (classification) model instead of hiding it Jul 9, 2026
@Evrard-Nil
Evrard-Nil temporarily deployed to Cloud API test env July 9, 2026 08:20 — with GitHub Actions Inactive
@Evrard-Nil

Copy link
Copy Markdown
Contributor Author

Revised per maintainer feedback: "Model should be there but tagged as not a completion model. Same as image models. Hiding it is not good." This supersedes the earlier hide-based version.

What changed vs the hide approach

  • Dropped the catalog-hiding filter entirely: removed the .filter(...) from both /v1/models (routes::completions::models) and /v1/model/list (routes::models::list_models), plus the now-unused is_listed_in_model_catalog helper, the NON_GENERATIVE_OUTPUT_MODALITIES const, and its unit test. Net result: common.rs and models.rs are unchanged from main; completions.rs gains only a comment.
  • openai/privacy-filter now stays listed in both endpoints and is tagged with its true modality.

How it mirrors image models
Both listing endpoints already expose output_modalities to clients — /v1/models via ModelInfo.output_modalities and the nested architecture.outputModalities; /v1/model/list via ModelMetadata.architecture.outputModalities (ModelArchitecture::from_options). An image model reports ["image"]; migration V0069 relabels privacy-filter from the mislabeled ["text"] to ["classification"], so it self-describes as a classifier exactly the way image/embedding models do. No new field or scheme was introduced.

Migration renumbered + branch rebased
main landed its own V0068 (cache_read_cost_nullable) after this branch first opened, so this migration moved V0068 → V0069 to avoid a duplicate refinery version. The branch was rebased onto current main, so it now carries main's V0068 and this V0069 side by side.

Tests

  • test_non_generative_models_excluded_from_models_listtest_classification_model_listed_and_tagged (asserts the model is listed and tagged ["classification"], alongside a chat model reporting ["text"]).
  • test_models_api now locates the configured chat model by id instead of data.first() — robust to the shared e2e catalog now containing the zero-priced classifier (previously hidden, so no listing test ever saw it).

Validation (rebased state = main + this change): cargo fmt --all -- --check, cargo check --workspace --tests, and cargo clippy --workspace --tests all clean; e2e against real Postgres — the full migration set (main's V0068 + this V0069) applies cleanly on a fresh bootstrap, and both tests pass.

Note: the branch name (fix/models-exclude-non-generative) is now a slight misnomer (the fix tags rather than excludes); kept to preserve this PR's URL and history.

@Evrard-Nil
Evrard-Nil requested a review from PierreLeGuen July 9, 2026 08:20

@PierreLeGuen PierreLeGuen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This PR relabels openai/privacy-filter as a non-completion (classification) model so it is listed and tagged by modality rather than hidden. The revised tag-based approach resolves the earlier blocking jsonb = text[] migration issue; the change (migration V0069 + a clarifying comment + e2e tests) is sound and safe to merge.

  • Migration V0069__mark_privacy_filter_non_generative.sql correctly uses '["classification"]'::jsonb against the JSONB output_modalities columns (defined in V0043) on both models and the open model_history snapshot, with IS DISTINCT FROM idempotency guards; it is a valid no-op on DBs lacking the row.
  • Relabeling is safe: output_modalities is used only in DB read/write plumbing and catalog-response mapping — the models listing (WHERE is_active = true) and chat completions routing (resolve_and_get_model) do not gate on it, so /v1/privacy/*, completions, and model lookup are unaffected.

Non-blocking follow-up:

  • V0069: the relabel is durable only until model-discovery/seed re-upserts openai/privacy-filter. An omit-the-field re-upsert is safe via COALESCE, but re-sending ["text"] would silently revert it. Not introduced by this PR and already tracked as a follow-up at the discovery source — noted for visibility only.

Local checks: cargo fmt --check, cargo check/clippy -D warnings on the affected crates, and the general::test_models_api + general::test_classification_model_listed_and_tagged e2e tests all passed (isolated test DB, DEV=1).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

openai/privacy-filter listed in /v1/models but every chat-completion attempt errors with upstream 404

3 participants