Skip to content

fix(models): filter and control model visibility in models - #654

Merged
steebchen merged 1 commit into
mainfrom
cursor/filter-and-control-model-visibility-in-v1-models-91aa
Aug 28, 2025
Merged

steebchen merged 1 commit into
mainfrom
cursor/filter-and-control-model-visibility-in-v1-models-91aa

Conversation

@steebchen

@steebchen steebchen commented Aug 28, 2025

Copy link
Copy Markdown
Member

Add optional filtering for deactivated and deprecated models to the /v1/models endpoint to provide more control over the returned model list.


Open in Cursor Open in Web

Summary by CodeRabbit

  • New Features

    • Models list API now supports optional query parameters:
      • include_deactivated=true to include deactivated models (default excludes them).
      • exclude_deprecated=true to omit deprecated models.
    • Both parameters can be used together. Response structure remains unchanged.
  • Tests

    • Added tests covering default behavior, inclusion of deactivated models, exclusion of deprecated models, and combined parameter handling.

Co-authored-by: contact <contact@polarlights.llc>
@cursor

cursor Bot commented Aug 28, 2025

Copy link
Copy Markdown

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@coderabbitai

coderabbitai Bot commented Aug 28, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Implements query-parameter-driven filtering on /v1/models: include_deactivated and exclude_deprecated. Updates the list handler to parse and apply filters against deactivatedAt and deprecatedAt timestamps, and updates tests to cover default behavior, each flag individually, and both together.

Changes

Cohort / File(s) Summary of changes
Models list endpoint implementation
apps/gateway/src/models/models.ts
Added optional query params include_deactivated and exclude_deprecated (parsed as booleans). Introduced filtering logic over modelsList using current time: exclude deactivated by default unless include_deactivated=true; exclude deprecated when exclude_deprecated=true. Updated OpenAPI request schema and used filteredModels for response mapping.
Models endpoint tests
apps/gateway/src/models/models.spec.ts
Added four tests covering: default exclusion of deactivated models; inclusion when include_deactivated=true; exclusion of deprecated models when exclude_deprecated=true; combined handling of both flags.

Sequence Diagram(s)

sequenceDiagram
  actor Client
  participant Gateway as Gateway /v1/models Handler
  participant Store as Models Source

  Client->>Gateway: GET /v1/models?include_deactivated&exclude_deprecated
  Note over Gateway: Parse query params as booleans<br/>includeDeactivated, excludeDeprecated<br/>currentDate = now
  Gateway->>Store: Fetch modelsList
  Store-->>Gateway: modelsList
  rect rgba(200,230,255,0.25)
    Note right of Gateway: Filtering
    Gateway->>Gateway: Remove deactivated if now >= deactivatedAt<br/>unless includeDeactivated==true
    Gateway->>Gateway: Remove deprecated if now >= deprecatedAt<br/>when excludeDeprecated==true
  end
  Gateway-->>Client: 200 OK { data: filtered models with ISO dates }
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • smakosh

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch cursor/filter-and-control-model-visibility-in-v1-models-91aa

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbit in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbit in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbit gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbit read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbit help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbit ignore or @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbit summary or @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbit or @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@steebchen
steebchen marked this pull request as ready for review August 28, 2025 16:23
@steebchen steebchen changed the title Filter and control model visibility in v1 models fix(models): filter and control model visibility in models Aug 28, 2025
@steebchen
steebchen added this pull request to the merge queue Aug 28, 2025
Merged via the queue into main with commit a9a7e02 Aug 28, 2025
9 of 12 checks passed
@steebchen
steebchen deleted the cursor/filter-and-control-model-visibility-in-v1-models-91aa branch August 28, 2025 16:26

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🧹 Nitpick comments (3)
apps/gateway/src/models/models.ts (1)

103-107: Use nullish coalescing for defaults

Keeps intent explicit and avoids truthiness pitfalls.

-const includeDeactivated = query.include_deactivated || false;
-const excludeDeprecated = query.exclude_deprecated || false;
+const includeDeactivated = query.include_deactivated ?? false;
+const excludeDeprecated = query.exclude_deprecated ?? false;
apps/gateway/src/models/models.spec.ts (2)

92-108: Validate combined flags via ordering relations between result sets.
Assert that:

  • both ≥ exclude_deprecated (adding include_deactivated cannot reduce), and
  • both ≤ include_deactivated (excluding deprecated cannot increase),
    plus keep the content check for deprecated models.

Proposed diff:

-    const res = await app.request(
-      "/v1/models?include_deactivated=true&exclude_deprecated=true",
-    );
-    expect(res.status).toBe(200);
-
-    const json = await res.json();
-    const currentDate = new Date();
-
-    // Should include deactivated models but exclude deprecated ones
-    for (const model of json.data) {
-      if (model.deprecated_at) {
-        const deprecatedAt = new Date(model.deprecated_at);
-        expect(currentDate <= deprecatedAt).toBe(true);
-      }
-    }
+    const [resInclude, resExclude, resBoth] = await Promise.all([
+      app.request("/v1/models?include_deactivated=true"),
+      app.request("/v1/models?exclude_deprecated=true"),
+      app.request("/v1/models?include_deactivated=true&exclude_deprecated=true"),
+    ]);
+    expect(resBoth.status).toBe(200);
+
+    const include = await resInclude.json();
+    const exclude = await resExclude.json();
+    const both = await resBoth.json();
+    expect(Array.isArray(include.data)).toBe(true);
+    expect(Array.isArray(exclude.data)).toBe(true);
+    expect(Array.isArray(both.data)).toBe(true);
+
+    // Monotonicity across flags
+    expect(both.data.length).toBeLessThanOrEqual(include.data.length);
+    expect(both.data.length).toBeGreaterThanOrEqual(exclude.data.length);
+
+    // Deprecated models still excluded under combined flags
+    const now = Date.now();
+    for (const model of both.data) {
+      if (model.deprecated_at) {
+        const deprecatedAtMs = Date.parse(model.deprecated_at);
+        expect(Number.isFinite(deprecatedAtMs)).toBe(true);
+        expect(now <= deprecatedAtMs).toBe(true);
+      }
+    }

46-61: Tighten assertion and guard against shape/time parsing issues

-    const json = await res.json();
-    const currentDate = new Date();
+    const json = await res.json();
+    expect(Array.isArray(json.data)).toBe(true);
+    const now = Date.now();
 
     // Verify that no deactivated models are returned
     for (const model of json.data) {
       if (model.deactivated_at) {
-        const deactivatedAt = new Date(model.deactivated_at);
-        expect(currentDate <= deactivatedAt).toBe(true);
+        const deactivatedAtMs = Date.parse(model.deactivated_at);
+        expect(Number.isFinite(deactivatedAtMs)).toBe(true);
+        expect(now <= deactivatedAtMs).toBe(true);
       }
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5b0028b and ea8a7a6.

📒 Files selected for processing (2)
  • apps/gateway/src/models/models.spec.ts (1 hunks)
  • apps/gateway/src/models/models.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use localStorage instead of cookies for client-side data persistence

Files:

  • apps/gateway/src/models/models.ts
  • apps/gateway/src/models/models.spec.ts
**/*.{js,ts}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{js,ts}: Use drizzle with the latest object syntax for database operations
For read queries, always use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • apps/gateway/src/models/models.ts
  • apps/gateway/src/models/models.spec.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Never use as any or : any in TypeScript files.

Files:

  • apps/gateway/src/models/models.ts
  • apps/gateway/src/models/models.spec.ts
apps/{api,gateway}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/{api,gateway}/**/*.{ts,tsx}: Use Drizzle ORM with the latest object syntax in backend services
For reads, use db().query.

.findMany() or db().query.
.findFirst()

Files:

  • apps/gateway/src/models/models.ts
  • apps/gateway/src/models/models.spec.ts
**/*.spec.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Name unit tests with the .spec.ts suffix

Files:

  • apps/gateway/src/models/models.spec.ts
{**/*.spec.ts,**/*.e2e.ts}

📄 CodeRabbit inference engine (CLAUDE.md)

Write tests using Vitest

Files:

  • apps/gateway/src/models/models.spec.ts
🧬 Code graph analysis (2)
apps/gateway/src/models/models.ts (1)
packages/models/src/models.ts (1)
  • ModelDefinition (79-116)
apps/gateway/src/models/models.spec.ts (1)
apps/gateway/src/index.ts (1)
  • app (43-43)
🔇 Additional comments (2)
apps/gateway/src/models/models.ts (2)

131-131: LGTM: mapping over filteredModels is correct

Ensures filtering is applied before projection.


108-129: Retain existing > comparison. It aligns with the API e2e test’s new Date() <= model.deactivatedAt logic and Date objects already compare by their numeric timestamp; switching to >= would alter behavior.

Likely an incorrect or invalid review comment.

Comment on lines +62 to +75
test("GET /v1/models?include_deactivated=true should include deactivated models", async () => {
const res = await app.request("/v1/models?include_deactivated=true");
expect(res.status).toBe(200);

const json = await res.json();
expect(json).toHaveProperty("data");
expect(Array.isArray(json.data)).toBe(true);

// The response should include all models (including deactivated ones)
// We can't easily test this without knowing specific deactivated models,
// but we can at least verify the endpoint works with the parameter
expect(json.data.length).toBeGreaterThan(0);
});

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.

🛠️ Refactor suggestion

Make the test meaningful: assert monotonicity (include_deactivated cannot reduce results).
Currently it only checks non-emptiness; assert that enabling the flag yields a superset or equal set size vs default.

Proposed diff:

-    const res = await app.request("/v1/models?include_deactivated=true");
-    expect(res.status).toBe(200);
-
-    const json = await res.json();
-    expect(json).toHaveProperty("data");
-    expect(Array.isArray(json.data)).toBe(true);
-
-    // The response should include all models (including deactivated ones)
-    // We can't easily test this without knowing specific deactivated models,
-    // but we can at least verify the endpoint works with the parameter
-    expect(json.data.length).toBeGreaterThan(0);
+    const [resDefault, res] = await Promise.all([
+      app.request("/v1/models"),
+      app.request("/v1/models?include_deactivated=true"),
+    ]);
+    expect(res.status).toBe(200);
+
+    const base = await resDefault.json();
+    const json = await res.json();
+    expect(Array.isArray(base.data)).toBe(true);
+    expect(Array.isArray(json.data)).toBe(true);
+    // Enabling include_deactivated should never reduce the number of models
+    expect(json.data.length).toBeGreaterThanOrEqual(base.data.length);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test("GET /v1/models?include_deactivated=true should include deactivated models", async () => {
const res = await app.request("/v1/models?include_deactivated=true");
expect(res.status).toBe(200);
const json = await res.json();
expect(json).toHaveProperty("data");
expect(Array.isArray(json.data)).toBe(true);
// The response should include all models (including deactivated ones)
// We can't easily test this without knowing specific deactivated models,
// but we can at least verify the endpoint works with the parameter
expect(json.data.length).toBeGreaterThan(0);
});
test("GET /v1/models?include_deactivated=true should include deactivated models", async () => {
const [resDefault, res] = await Promise.all([
app.request("/v1/models"),
app.request("/v1/models?include_deactivated=true"),
]);
expect(res.status).toBe(200);
const base = await resDefault.json();
const json = await res.json();
expect(Array.isArray(base.data)).toBe(true);
expect(Array.isArray(json.data)).toBe(true);
// Enabling include_deactivated should never reduce the number of models
expect(json.data.length).toBeGreaterThanOrEqual(base.data.length);
});
🤖 Prompt for AI Agents
In apps/gateway/src/models/models.spec.ts around lines 62 to 75, the test only
checks non-emptiness; update it to request the endpoint twice (once without
params and once with ?include_deactivated=true) and assert that the response
with include_deactivated has a length greater than or equal to the default
response (and optionally that the set of IDs in the include_deactivated response
is a superset of the default IDs) to ensure enabling the flag cannot reduce
results.

Comment on lines +76 to +91
test("GET /v1/models?exclude_deprecated=true should exclude deprecated models", async () => {
const res = await app.request("/v1/models?exclude_deprecated=true");
expect(res.status).toBe(200);

const json = await res.json();
const currentDate = new Date();

// Verify that no deprecated models are returned
for (const model of json.data) {
if (model.deprecated_at) {
const deprecatedAt = new Date(model.deprecated_at);
expect(currentDate <= deprecatedAt).toBe(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.

🛠️ Refactor suggestion

Strengthen exclusion test: assert both content constraint and monotonicity (exclude_deprecated cannot increase results).
This makes the test robust regardless of fixture data.

Proposed diff:

-    const res = await app.request("/v1/models?exclude_deprecated=true");
-    expect(res.status).toBe(200);
-
-    const json = await res.json();
-    const currentDate = new Date();
-
-    // Verify that no deprecated models are returned
-    for (const model of json.data) {
-      if (model.deprecated_at) {
-        const deprecatedAt = new Date(model.deprecated_at);
-        expect(currentDate <= deprecatedAt).toBe(true);
-      }
-    }
+    const [resDefault, res] = await Promise.all([
+      app.request("/v1/models"),
+      app.request("/v1/models?exclude_deprecated=true"),
+    ]);
+    expect(res.status).toBe(200);
+
+    const base = await resDefault.json();
+    const json = await res.json();
+    expect(Array.isArray(base.data)).toBe(true);
+    expect(Array.isArray(json.data)).toBe(true);
+    // Disallow deprecated models and ensure count is not larger than baseline
+    const now = Date.now();
+    expect(json.data.length).toBeLessThanOrEqual(base.data.length);
+    for (const model of json.data) {
+      if (model.deprecated_at) {
+        const deprecatedAtMs = Date.parse(model.deprecated_at);
+        expect(Number.isFinite(deprecatedAtMs)).toBe(true);
+        expect(now <= deprecatedAtMs).toBe(true);
+      }
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test("GET /v1/models?exclude_deprecated=true should exclude deprecated models", async () => {
const res = await app.request("/v1/models?exclude_deprecated=true");
expect(res.status).toBe(200);
const json = await res.json();
const currentDate = new Date();
// Verify that no deprecated models are returned
for (const model of json.data) {
if (model.deprecated_at) {
const deprecatedAt = new Date(model.deprecated_at);
expect(currentDate <= deprecatedAt).toBe(true);
}
}
});
test("GET /v1/models?exclude_deprecated=true should exclude deprecated models", async () => {
// Fetch both the default list and the filtered list in parallel
const [resDefault, res] = await Promise.all([
app.request("/v1/models"),
app.request("/v1/models?exclude_deprecated=true"),
]);
expect(res.status).toBe(200);
const base = await resDefault.json();
const json = await res.json();
expect(Array.isArray(base.data)).toBe(true);
expect(Array.isArray(json.data)).toBe(true);
// Ensure filtering never increases the total number of models
expect(json.data.length).toBeLessThanOrEqual(base.data.length);
// Disallow deprecated models and verify any deprecated_at is in the future
const now = Date.now();
for (const model of json.data) {
if (model.deprecated_at) {
const deprecatedAtMs = Date.parse(model.deprecated_at);
expect(Number.isFinite(deprecatedAtMs)).toBe(true);
expect(now <= deprecatedAtMs).toBe(true);
}
}
});
🤖 Prompt for AI Agents
In apps/gateway/src/models/models.spec.ts around lines 76 to 91, the test only
verifies that returned models are not already deprecated but doesn't assert
monotonicity (that excluding deprecated models never increases the result set);
update the test to first fetch the baseline list with no query params, store its
total count, then fetch with ?exclude_deprecated=true and assert (1) every
returned model has no deprecated_at or deprecated_at is in the future, and (2)
the count with exclude_deprecated is less than or equal to the baseline count to
enforce monotonicity; keep error messages or expect messages clear so failures
indicate whether the content constraint or the monotonicity check failed.

Comment on lines +75 to +88
request: {
query: z.object({
include_deactivated: z
.string()
.optional()
.transform((val) => val === "true")
.describe("Include deactivated models in the response"),
exclude_deprecated: z
.string()
.optional()
.transform((val) => val === "true")
.describe("Exclude deprecated models from the response"),
}),
},

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.

🛠️ Refactor suggestion

Parse booleans strictly and expose them as booleans in the OpenAPI spec

Current transform treats any non-"true" value (including typos) as false and will likely render as "string" in the schema. Tighten validation to accepted literals only.

 request: {
   query: z.object({
-    include_deactivated: z
-      .string()
-      .optional()
-      .transform((val) => val === "true")
-      .describe("Include deactivated models in the response"),
-    exclude_deprecated: z
-      .string()
-      .optional()
-      .transform((val) => val === "true")
-      .describe("Exclude deprecated models from the response"),
+    include_deactivated: z
+      .enum(["true", "false"])
+      .optional()
+      .transform((val) => val === "true")
+      .describe("Include deactivated models in the response"),
+    exclude_deprecated: z
+      .enum(["true", "false"])
+      .optional()
+      .transform((val) => val === "true")
+      .describe("Exclude deprecated models from the response"),
   }),
 },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
request: {
query: z.object({
include_deactivated: z
.string()
.optional()
.transform((val) => val === "true")
.describe("Include deactivated models in the response"),
exclude_deprecated: z
.string()
.optional()
.transform((val) => val === "true")
.describe("Exclude deprecated models from the response"),
}),
},
request: {
query: z.object({
- include_deactivated: z
- .string()
- .optional()
- .transform((val) => val === "true")
- .describe("Include deactivated models in the response"),
- exclude_deprecated: z
- .string()
- .optional()
- .transform((val) => val === "true")
include_deactivated: z
.enum(["true", "false"])
.optional()
.transform((val) => val === "true")
.describe("Include deactivated models in the response"),
exclude_deprecated: z
.enum(["true", "false"])
.optional()
.transform((val) => val === "true")
.describe("Exclude deprecated models from the response"),
}),
},

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.

2 participants