Skip to content

feat(ai): convert copilot API to structured outputs with Zod schema#12

Merged
arthrod merged 11 commits intomainfrom
claude/structured-ai-outputs-01QKTPgPJyoPEJsBf69nPFXR
Dec 5, 2025
Merged

feat(ai): convert copilot API to structured outputs with Zod schema#12
arthrod merged 11 commits intomainfrom
claude/structured-ai-outputs-01QKTPgPJyoPEJsBf69nPFXR

Conversation

@arthrod
Copy link
Copy Markdown
Owner

@arthrod arthrod commented Dec 4, 2025

User description

Replace generateText with generateObject for type-safe AI responses. The copilot completion endpoint now uses a Zod schema to define the response structure, ensuring consistent and validated outputs.

Checklist

  • yarn typecheck
  • yarn lint:fix
  • yarn test
  • yarn brl
  • yarn changeset
  • ui changelog

PR Type

Enhancement, Tests


Description

  • Replace generateText with generateObject for type-safe AI responses

  • Add Zod schema validation for structured completion outputs

  • Remove maxOutputTokens parameter from API calls

  • Add comprehensive unit test suites for copilot API routes


Diagram Walkthrough

flowchart LR
  A["generateText API"] -->|"Replace with"| B["generateObject API"]
  B -->|"Uses"| C["Zod Schema"]
  C -->|"Validates"| D["Completion Object"]
  D -->|"Returns"| E["Type-safe Response"]
Loading

File Walkthrough

Relevant files
Enhancement
route.ts
Convert to generateObject with Zod schema validation         

apps/www/src/registry/app/api/ai/copilot/route.ts

  • Replace generateText with generateObject for structured outputs
  • Add completionSchema Zod object defining text field validation
  • Add schema configuration parameters: schema, schemaName,
    schemaDescription
  • Remove maxOutputTokens parameter
  • Return full generateObject result preserving original response
    contract
+11/-3   
route.ts
Convert to generateObject with Zod schema validation         

templates/plate-playground-template/src/app/api/ai/copilot/route.ts

  • Replace generateText with generateObject for structured outputs
  • Add completionSchema Zod object defining text field validation
  • Add schema configuration parameters: schema, schemaName,
    schemaDescription
  • Remove maxOutputTokens parameter
  • Return full generateObject result to preserve response contract
+10/-2   
Tests
route.spec.ts
Add comprehensive unit test suite for copilot API               

apps/www/src/registry/app/api/ai/copilot/route.spec.ts

  • Add 641 lines of comprehensive unit tests using Bun test framework
  • Test authentication with API key in body, environment, and missing
    scenarios
  • Test happy path with valid requests and parameter passing
  • Test edge cases: empty prompts, special characters, Unicode, long
    prompts
  • Test error handling: AbortError, rate limits, network errors, timeouts
  • Test schema validation and response format preservation
+641/-0 
route.test.ts
Add comprehensive unit test suite for copilot API               

apps/www/src/registry/app/api/ai/copilot/route.test.ts

  • Add 756 lines of comprehensive unit tests using Bun test framework
  • Test authentication scenarios and API key precedence
  • Test happy path with model variations and parameter validation
  • Test edge cases including malformed JSON and null values
  • Test error handling with various error types and graceful degradation
  • Test response format validation and metadata preservation
  • Test concurrent request handling
+756/-0 
route.spec.ts
Add comprehensive unit test suite for template copilot API

templates/plate-playground-template/src/app/api/ai/copilot/route.spec.ts

  • Add 759 lines of comprehensive unit tests using Bun test framework
  • Test authentication with various API key scenarios
  • Test happy path with valid requests and schema validation
  • Test edge cases: empty prompts, special characters, Unicode handling
  • Test error handling: AbortError, rate limits, network errors
  • Test response format differences from registry version
  • Test backward compatibility and concurrent requests
+759/-0 
route.test.ts
Add comprehensive unit test suite for template copilot API

templates/plate-playground-template/src/app/api/ai/copilot/route.test.ts

  • Add 875 lines of comprehensive unit tests using Bun test framework
  • Test authentication and API key handling with environment variables
  • Test happy path with model variations and schema configuration
  • Test edge cases including malformed JSON and null value handling
  • Test error handling with various error types and graceful degradation
  • Test response format validation and metadata extraction
  • Test response differences from registry version (returns
    result.object)
  • Test concurrent request handling and schema validation
+875/-0 

Replace generateText with generateObject for type-safe AI responses.
The copilot completion endpoint now uses a Zod schema to define the
response structure, ensuring consistent and validated outputs.
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Dec 4, 2025

Reviewer's Guide

Refactors the copilot API routes to use generateObject with a Zod-defined completion schema instead of generateText, returning the validated structured object from the AI response.

Sequence diagram for copilot POST route using generateObject with Zod schema

sequenceDiagram
  actor User
  participant Client as FrontendClient
  participant Route as CopilotApiRoute
  participant AISDK as AISDK_generateObject
  participant Zod as Zod_completionSchema

  User->>Client: Trigger editor copilot completion
  Client->>Route: POST /api/ai/copilot { model, prompt, system }
  activate Route
  Route->>Route: Parse JSON body
  Route->>AISDK: generateObject(model, prompt, schema, schemaDescription, schemaName, abortSignal, system, temperature)
  activate AISDK
  AISDK->>Zod: Validate response with completionSchema
  Zod-->>AISDK: Validated object { text }
  AISDK-->>Route: result.object
  deactivate AISDK
  Route-->>Client: 200 OK { text }
  deactivate Route

  Client-->>User: Display completion suggestion in editor

  alt Request aborted
    Route->>AISDK: generateObject(..., abortSignal)
    AISDK-->>Route: Throw AbortError
    Route-->>Client: 408 Request Timeout
  end
Loading

File-Level Changes

Change Details Files
Switch AI completion from free-form text generation to schema-validated structured output.
  • Replace generateText with generateObject for copilot completion requests.
  • Introduce a shared Zod completionSchema describing a text field for the completion suggestion.
  • Pass schema, schemaName, and schemaDescription into the AI call for structured responses.
  • Adjust the POST handlers to return only the result.object instead of the full result payload.
  • Remove unused maxOutputTokens parameter from the AI call.
templates/plate-playground-template/src/app/api/ai/copilot/route.ts
apps/www/src/registry/app/api/ai/copilot/route.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello @arthrod, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the copilot AI API by transitioning from free-form text generation to structured, type-safe outputs. By integrating a Zod schema, the system now rigorously validates AI responses, ensuring they conform to a predefined format. This change boosts the reliability and predictability of the AI's suggestions, making integration with downstream systems more robust.

Highlights

  • Structured AI Outputs: The copilot API now utilizes generateObject instead of generateText from the ai library to produce structured, type-safe responses.
  • Zod Schema Integration: A completionSchema (defined with Zod) has been introduced to explicitly define and validate the expected structure of the AI's output, ensuring consistency and reliability.
  • Improved Type Safety: By enforcing a schema, the AI responses are now type-safe, which reduces potential runtime errors and enhances data reliability in the copilot completion endpoint.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Dec 4, 2025

PR Compliance Guide 🔍

(Compliance updated until commit a33d3ae)

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Consistent Naming Conventions

Objective: All new variables, functions, and classes must follow the project's established naming
standards

Status: Passed

No Dead or Commented-Out Code

Objective: Keep the codebase clean by ensuring all submitted code is active and necessary

Status: Passed

When relevant, utilize early return

Objective: In a code snippet containing multiple logic conditions (such as 'if-else'), prefer an
early return on edge cases than deep nesting

Status: Passed

Robust Error Handling

Objective: Ensure potential errors and edge cases are anticipated and handled gracefully throughout
the code

Status:
Limited Error Mapping: The handler only special-cases AbortError and otherwise returns a generic 500 without
distinguishing common AI errors (e.g., rate limits or timeouts) seen in tests, which may
warrant more explicit handling.

Referred Code
  return NextResponse.json(result);
} catch (error) {
  if (error instanceof Error && error.name === 'AbortError') {
    return NextResponse.json(null, { status: 408 });
  }

  return NextResponse.json(
    { error: 'Failed to process AI request' },
  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

Previous compliance checks

Compliance check up to commit 9ec2fa9
Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Consistent Naming Conventions

Objective: All new variables, functions, and classes must follow the project's established naming
standards

Status: Passed

No Dead or Commented-Out Code

Objective: Keep the codebase clean by ensuring all submitted code is active and necessary

Status: Passed

When relevant, utilize early return

Objective: In a code snippet containing multiple logic conditions (such as 'if-else'), prefer an
early return on edge cases than deep nesting

Status: Passed

Robust Error Handling

Objective: Ensure potential errors and edge cases are anticipated and handled gracefully throughout
the code

Status:
Generic Errors: The catch-all error handling maps all non-AbortError failures (including rate limits and
network issues) to a 500 with a generic message, which may be acceptable but could lack
differentiated responses and logging.

Referred Code
  return NextResponse.json(result);
} catch (error) {
  if (error instanceof Error && error.name === 'AbortError') {
    return NextResponse.json(null, { status: 408 });
  }

  return NextResponse.json(
    { error: 'Failed to process AI request' },
  • Update
Compliance check up to commit 34f4404
Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Consistent Naming Conventions

Objective: All new variables, functions, and classes must follow the project's established naming
standards

Status: Passed

No Dead or Commented-Out Code

Objective: Keep the codebase clean by ensuring all submitted code is active and necessary

Status: Passed

When relevant, utilize early return

Objective: In a code snippet containing multiple logic conditions (such as 'if-else'), prefer an
early return on edge cases than deep nesting

Status: Passed

Robust Error Handling

Objective: Ensure potential errors and edge cases are anticipated and handled gracefully throughout
the code

Status:
Generic 500 error: The catch-all branch maps diverse errors (RateLimitError, TimeoutError, Network issues,
malformed JSON parsing upstream) to a generic 500 with 'Failed to process AI
request' without differentiated handling or user-friendly messaging.

Referred Code
} catch (error) {
  if (error instanceof Error && error.name === 'AbortError') {
    return NextResponse.json(null, { status: 408 });
  }

  return NextResponse.json(
    { error: 'Failed to process AI request' },
    { status: 500 }
Compliance check up to commit c58bb79
Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Consistent Naming Conventions

Objective: All new variables, functions, and classes must follow the project's established naming
standards

Status: Passed

No Dead or Commented-Out Code

Objective: Keep the codebase clean by ensuring all submitted code is active and necessary

Status: Passed

When relevant, utilize early return

Objective: In a code snippet containing multiple logic conditions (such as 'if-else'), prefer an
early return on edge cases than deep nesting

Status: Passed

Robust Error Handling

Objective: Ensure potential errors and edge cases are anticipated and handled gracefully throughout
the code

Status:
Missing validation: The new structured output introduces a schema but the API returns the entire result object
without explicit handling of schema validation failures or non-OK model responses, which
may require additional error checks.

Referred Code
const result = await generateObject({
  abortSignal: req.signal,
  model: `openai/${model}`,
  prompt,
  schema: completionSchema,
  schemaDescription: 'A text completion suggestion for the editor',
  schemaName: 'completion',
  system,
  temperature: 0.7,
});

// Return the full generateText result to preserve the original response contract
return NextResponse.json(result);
Compliance check up to commit 2dd3490
Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Consistent Naming Conventions

Objective: All new variables, functions, and classes must follow the project's established naming
standards

Status: Passed

No Dead or Commented-Out Code

Objective: Keep the codebase clean by ensuring all submitted code is active and necessary

Status: Passed

When relevant, utilize early return

Objective: In a code snippet containing multiple logic conditions (such as 'if-else'), prefer an
early return on edge cases than deep nesting

Status: Passed

Robust Error Handling

Objective: Ensure potential errors and edge cases are anticipated and handled gracefully throughout
the code

Status:
Error response shape: The handler now returns only result.object on success but keeps prior error responses
unchanged, which may alter response consistency and could require additional validation or
normalization of AI/object parsing errors beyond AbortError.

Referred Code
  const result = await generateObject({
    abortSignal: req.signal,
    model: `openai/${model}`,
    prompt,
    schema: completionSchema,
    schemaDescription: 'A text completion suggestion for the editor',
    schemaName: 'completion',
    system,
    temperature: 0.7,
  });

  return NextResponse.json(result.object);
} catch (error) {
  • Update

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Dec 4, 2025

Warning

Rate limit exceeded

@arthrod has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 4 minutes and 59 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 34f4404 and a33d3ae.

📒 Files selected for processing (1)
  • templates/plate-playground-template/src/app/api/ai/copilot/route.ts (2 hunks)

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Summary by CodeRabbit

  • Improvements

    • AI Copilot endpoint now delivers more consistent responses with enhanced error handling and validation.
  • Tests

    • Extensive test coverage added for the AI Copilot API endpoint, including authentication, error handling, edge cases, concurrent request scenarios, and model configurations.

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

The PR updates AI Copilot API routes in two locations (apps/www and templates/plate-playground-template) by replacing generateText with generateObject for structured schema validation. It adds comprehensive test suites covering authentication, happy paths, error scenarios, edge cases, and model/schema handling.

Changes

Cohort / File(s) Summary
AI Copilot Route Implementation
apps/www/src/registry/app/api/ai/copilot/route.ts, templates/plate-playground-template/src/app/api/ai/copilot/route.ts
Replaced generateText with generateObject; added Zod-based completionSchema with schema metadata (schemaName, schemaDescription); removed maxOutputTokens; modified return to extract only result.object instead of full result; maintained AbortError handling.
Test Suites (route.spec.ts)
apps/www/src/registry/app/api/ai/copilot/route.spec.ts, templates/plate-playground-template/src/app/api/ai/copilot/route.spec.ts
New comprehensive test suites validating authentication (body/env API key precedence, 401 on missing key), happy path (model prefixing, schema presence, parameter propagation), temperature enforcement (0.7 constant), edge cases (empty inputs, special characters, Unicode), error handling (AbortError → 408, general errors → 500), and response format (metadata preservation, text field validation).
Test Suites (route.test.ts)
apps/www/src/registry/app/api/ai/copilot/route.test.ts, templates/plate-playground-template/src/app/api/ai/copilot/route.test.ts
Extensive test coverage using bun:test; mocks AI SDK's generateObject; validates authentication flow, model handling with OpenAI prefixing, abort signal propagation, temperature defaults, schema structure and validation, concurrent request handling, response format (content-type JSON, field extraction), and robustness across various error shapes (AbortError, RateLimitError, network timeouts, malformed JSON, non-Error rejections).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Implementation changes are straightforward (generateText → generateObject swap with schema addition) but occur in two locations.
  • Test files are comprehensive (~300+ lines each) but follow consistent, homogeneous patterns across the four test files, reducing cognitive load per file.
  • Schema and model prefixing logic require careful verification for correctness and consistency between both implementations.
  • API contract change (returning only result.object vs full result) should be validated against downstream dependencies.
  • Error handling coverage is extensive and should be spot-checked for alignment between implementations.

Possibly related PRs

  • arthrod/potion#67: Modifies AI copilot route model selection and defaults (FASTMODEL constant), overlapping with this PR's model prefixing and generateObject integration.

Poem

🐰 With schema in paw, we hop and dance,
Structured responses now get a chance!
GenerateObject with Zod so keen,
The finest AI route you've seen!
Tests abound, edge cases caught,
Temperature steady—0.7, as sought!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(ai): convert copilot API to structured outputs with Zod schema' clearly and concisely summarizes the main change of replacing generateText with generateObject for type-safe AI responses using Zod validation.
Description check ✅ Passed The PR description clearly relates to the changeset, describing the replacement of generateText with generateObject, Zod schema validation, removal of maxOutputTokens, and addition of comprehensive test suites.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • The POST handlers now return result.object instead of the full generateObject result; consider typing the handler’s response explicitly based on z.infer<typeof completionSchema> to keep the API surface clearly aligned with the schema.
  • The completionSchema definition is duplicated between the template and registry routes; consider extracting a shared schema to avoid divergence if the completion shape changes in the future.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `POST` handlers now return `result.object` instead of the full `generateObject` result; consider typing the handler’s response explicitly based on `z.infer<typeof completionSchema>` to keep the API surface clearly aligned with the schema.
- The `completionSchema` definition is duplicated between the template and registry routes; consider extracting a shared schema to avoid divergence if the completion shape changes in the future.

## Individual Comments

### Comment 1
<location> `templates/plate-playground-template/src/app/api/ai/copilot/route.ts:26` </location>
<code_context>
-    const result = await generateText({
+    const result = await generateObject({
       abortSignal: req.signal,
-      maxOutputTokens: 50,
       model: `openai/${model}`,
       prompt,
</code_context>

<issue_to_address>
**suggestion (performance):** Dropping `maxOutputTokens` may lead to longer generations and higher latency/cost.

The original cap also prevented runaway generations. If `generateObject` supports it, consider specifying a similar token limit so behavior stays consistent with the prior endpoint, especially given how often this is called from the editor.

Suggested implementation:

```typescript
    const result = await generateObject({
      abortSignal: req.signal,
      maxOutputTokens: 50,
      model: `openai/${model}`,
      prompt,
      schema: completionSchema,
      schemaDescription: 'A text completion suggestion for the editor',
      schemaName: 'completion',
      system,
      temperature: 0.7,
    });

```

If `generateObject` in your SDK uses a different option name for limiting output (e.g. `maxTokens` instead of `maxOutputTokens`), adjust the property name accordingly while keeping the value at 50 to match prior behavior.
</issue_to_address>

### Comment 2
<location> `apps/www/src/registry/app/api/ai/copilot/route.ts:40` </location>
<code_context>
     });

-    return NextResponse.json(result);
+    return NextResponse.json(result.object);
   } catch (error) {
     if (error instanceof Error && error.name === 'AbortError') {
</code_context>

<issue_to_address>
**suggestion (bug_risk):** The registry example now returns a different JSON shape than before.

This change alters the response contract for the registry example: consumers previously received the full `generateText` result (including metadata), but now only `result.object`. If this isn’t intended, either restore a richer shape closer to the original or clearly document that the endpoint now returns only `{ text: string }`.

```suggestion
    // Return the full generateText result to preserve the original response contract
    return NextResponse.json(result);
```
</issue_to_address>

### Comment 3
<location> `apps/www/src/registry/app/api/ai/copilot/route.ts:29-36` </location>
<code_context>
-    const result = await generateText({
+    const result = await generateObject({
       abortSignal: req.signal,
-      maxOutputTokens: 50,
       model: `openai/${model}`,
       prompt,
</code_context>

<issue_to_address>
**suggestion:** Removing the output token limit from the registry example may make it less safe as sample code.

Since this is example/registry code, it’s helpful to demonstrate an explicit output limit as a best practice. If `generateObject` supports a token limit, consider adding it here (even if the production route doesn’t) to promote more controlled usage.

```suggestion
    const result = await generateObject({
      abortSignal: req.signal,
      maxOutputTokens: 50,
      model: `openai/${model}`,
      prompt,
      schema: completionSchema,
      schemaDescription: 'A text completion suggestion for the editor',
      schemaName: 'completion',
      system,
      temperature: 0.7,
    });
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Dec 4, 2025

PR Code Suggestions ✨

Latest suggestions up to a33d3ae

CategorySuggestion                                                                                                                                    Impact
Possible issue
Handle invalid JSON bodies

Add a try-catch block around req.json() to handle invalid JSON bodies and return
a 400 error instead of crashing.

apps/www/src/registry/app/api/ai/copilot/route.ts [12-17]

+let body: any;
+try {
+  body = await req.json();
+} catch {
+  return NextResponse.json({ error: 'Invalid JSON body.' }, { status: 400 });
+}
+
 const {
   apiKey: key,
   model = 'gpt-4o-mini',
   prompt,
   system,
-} = await req.json();
+} = body ?? {};
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that malformed JSON will cause an unhandled exception and provides a standard solution to gracefully return a 400 error, which improves API robustness and user experience.

Medium
General
Prevent empty completion text

Update the completionSchema to prevent empty strings in the text field by adding
.min(1).

apps/www/src/registry/app/api/ai/copilot/route.ts [7-9]

 const completionSchema = z.object({
-  text: z.string().describe('The completion text suggestion'),
+  text: z
+    .string()
+    .min(1, 'Completion text must not be empty')
+    .describe('The completion text suggestion'),
 });
  • Apply / Chat
Suggestion importance[1-10]: 5

__

Why: This is a reasonable improvement to the Zod schema that prevents the API from returning empty completions, making the endpoint more robust.

Low
Disallow empty completion outputs

Update the completionSchema to prevent empty strings in the text field by adding
.min(1).

templates/plate-playground-template/src/app/api/ai/copilot/route.ts [6-8]

 const completionSchema = z.object({
-  text: z.string().describe('The completion text suggestion'),
+  text: z
+    .string()
+    .min(1, 'Completion text must not be empty')
+    .describe('The completion text suggestion'),
 });
  • Apply / Chat
Suggestion importance[1-10]: 5

__

Why: This is a reasonable improvement to the Zod schema that prevents the API from returning empty completions, making the endpoint more robust.

Low
  • More

Previous suggestions

Suggestions up to commit 9ec2fa9
CategorySuggestion                                                                                                                                    Impact
Possible issue
Pass API key to SDK

Pass the apiKey to the generateObject call via an Authorization header to ensure
requests are properly authenticated.

apps/www/src/registry/app/api/ai/copilot/route.ts [11-52]

 import { generateObject } from 'ai';
 import { NextResponse } from 'next/server';
 import { z } from 'zod';
 
 const completionSchema = z.object({
   text: z.string().describe('The completion text suggestion'),
 });
 
 export async function POST(req: NextRequest) {
   const {
     apiKey: key,
     model = 'gpt-4o-mini',
     prompt,
     system,
   } = await req.json();
 
   const apiKey = key || process.env.AI_GATEWAY_API_KEY;
 
   if (!apiKey) {
     return NextResponse.json(
       { error: 'Missing ai gateway API key.' },
       { status: 401 }
     );
   }
 
   try {
     const result = await generateObject({
       abortSignal: req.signal,
       model: `openai/${model}`,
       prompt,
       schema: completionSchema,
       schemaDescription: 'A text completion suggestion for the editor',
       schemaName: 'completion',
       system,
       temperature: 0.7,
+      // Ensure the AI Gateway receives the API key
+      headers: {
+        Authorization: `Bearer ${apiKey}`,
+      },
     });
 
     // Return the full generateText result to preserve the original response contract
     return NextResponse.json(result);
   } catch (error) {
     if (error instanceof Error && error.name === 'AbortError') {
       return NextResponse.json(null, { status: 408 });
     }
 
     return NextResponse.json(
       { error: 'Failed to process AI request' },
       { status: 500 }
     );
   }
 }
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a critical bug where the apiKey is validated but not passed to the generateObject call, which would cause authentication to fail and break the endpoint's functionality.

High
Fix import and pass API key

Fix the import statement to use generateObject instead of generateText, and pass
the apiKey to the generateObject call via an Authorization header to ensure
proper authentication.

templates/plate-playground-template/src/app/api/ai/copilot/route.ts [10-50]

-import { generateText } from 'ai';
+import { generateObject } from 'ai';
 import type { NextRequest } from 'next/server';
 import { NextResponse } from 'next/server';
 import { z } from 'zod';
 
 const completionSchema = z.object({
   text: z.string().describe('The completion text suggestion'),
 });
 
 export async function POST(req: NextRequest) {
   const {
     apiKey: key,
     model = 'gpt-4o-mini',
     prompt,
     system,
   } = await req.json();
 
   const apiKey = key || process.env.AI_GATEWAY_API_KEY;
 
   if (!apiKey) {
     return NextResponse.json(
       { error: 'Missing ai gateway API key.' },
       { status: 401 }
     );
   }
 
   try {
     const result = await generateObject({
       abortSignal: req.signal,
       model: `openai/${model}`,
       prompt,
       schema: completionSchema,
       schemaDescription: 'A text completion suggestion for the editor',
       schemaName: 'completion',
       system,
       temperature: 0.7,
+      headers: {
+        Authorization: `Bearer ${apiKey}`,
+      },
     });
 
     return NextResponse.json(result.object);
   } catch (error) {
     if (error instanceof Error && error.name === 'AbortError') {
       return NextResponse.json(null, { status: 408 });
     }
 
     return NextResponse.json(
       { error: 'Failed to process AI request' },
       { status: 500 }
     );
   }
 }
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a critical bug where the apiKey is validated but not passed to the generateObject call, which would cause authentication to fail. It also correctly points out a wrong import that would cause a runtime error.

High
Qodo
best practice
Log exceptions with traceback

Log unexpected exceptions with get_logger().exception to capture stack traces
and context before returning the error response.

apps/www/src/registry/app/api/ai/copilot/route.ts [42-52]

 } catch (error) {
   if (error instanceof Error && error.name === 'AbortError') {
     return NextResponse.json(null, { status: 408 });
   }
-
+  get_logger().exception('Failed to process AI request', { artifact: { error } });
   return NextResponse.json(
     { error: 'Failed to process AI request' },
     { status: 500 }
   );
 }
Suggestion importance[1-10]: 6

__

Why:
Relevant best practice - Prefer get_logger().exception over generic error handling to include tracebacks and richer context.

Low
Add exception logging

Before returning a 500 response, log the caught exception using
get_logger().exception to include stack trace and error context.

templates/plate-playground-template/src/app/api/ai/copilot/route.ts [40-50]

 } catch (error) {
   if (error instanceof Error && error.name === 'AbortError') {
     return NextResponse.json(null, { status: 408 });
   }
-
+  get_logger().exception('Failed to process AI request', { artifact: { error } });
   return NextResponse.json(
     { error: 'Failed to process AI request' },
     { status: 500 }
   );
 }
Suggestion importance[1-10]: 5

__

Why:
Relevant best practice - Prefer get_logger().exception when catching errors to automatically include traceback information.

Low
✅ Suggestions up to commit 9ec2fa9
CategorySuggestion                                                                                                                                    Impact
Possible issue
Guard against undefined response body
Suggestion Impact:The commit changed the return to avoid directly returning result.object, instead returning the entire result object. This addresses the risk of result.object being undefined by not relying on it, aligning with the suggestion's intent to prevent invalid JSON responses, though the implementation differs.

code diff:

-    return NextResponse.json(result.object);
+    // Return the full generateObject result to preserve the original response contract
+    return NextResponse.json(result);

In templates/plate-playground-template/src/app/api/ai/copilot/route.ts, add a
nullish coalescing operator to handle cases where result.object is undefined,
ensuring the API always returns a valid JSON object.

templates/plate-playground-template/src/app/api/ai/copilot/route.ts [39]

 const result = await generateObject({
   abortSignal: req.signal,
   model: `openai/${model}`,
   prompt,
   schema: completionSchema,
   schemaDescription: 'A text completion suggestion for the editor',
   schemaName: 'completion',
   system,
   temperature: 0.7,
 });
 
-return NextResponse.json(result.object);
+const payload = result?.object ?? { text: '' };
+return NextResponse.json(payload);
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a potential runtime error where result.object could be undefined, leading to an invalid JSON response. This is a significant correctness issue, and the proposed fix ensures a valid JSON payload is always returned.

Medium
Qodo
best practice
Log exceptions with traceback

Log unexpected exceptions using get_logger().exception() before returning the
500 response to capture full traceback.

apps/www/src/registry/app/api/ai/copilot/route.ts [42-51]

 } catch (error) {
   if (error instanceof Error && error.name === 'AbortError') {
     return NextResponse.json(null, { status: 408 });
   }
 
+  get_logger().exception('Failed to process AI request', { error });
   return NextResponse.json(
     { error: 'Failed to process AI request' },
     { status: 500 }
   );
 }
Suggestion importance[1-10]: 6

__

Why:
Relevant best practice - Use try-catch with get_logger().exception() to log errors with full traceback when handling exceptions.

Low
Add exception logging with traceback

Before returning the 500 response, add get_logger().exception() to record the
error and traceback for better diagnostics.

templates/plate-playground-template/src/app/api/ai/copilot/route.ts [40-49]

 } catch (error) {
   if (error instanceof Error && error.name === 'AbortError') {
     return NextResponse.json(null, { status: 408 });
   }
 
+  get_logger().exception('Failed to process AI request', { error });
   return NextResponse.json(
     { error: 'Failed to process AI request' },
     { status: 500 }
   );
 }
Suggestion importance[1-10]: 5

__

Why:
Relevant best practice - Use try-catch with get_logger().exception() to log errors with full traceback when handling exceptions.

Low
Suggestions up to commit 34f4404
CategorySuggestion                                                                                                                                    Impact
Possible issue
Handle malformed JSON bodies

Add a try/catch block around req.json() to handle JSON parsing errors. Return a
400 Bad Request response if the request body contains malformed JSON.

apps/www/src/registry/app/api/ai/copilot/route.ts [12-17]

+let body: unknown;
+try {
+  body = await req.json();
+} catch {
+  return NextResponse.json({ error: 'Invalid JSON body.' }, { status: 400 });
+}
+
 const {
   apiKey: key,
   model = 'gpt-4o-mini',
   prompt,
   system,
-} = await req.json();
+} = (body as any) ?? {};
Suggestion importance[1-10]: 7

__

Why: This is a valid suggestion that improves error handling by returning a more specific 400 status for malformed JSON, instead of a generic 500.

Medium
Return 400 on invalid JSON

Add a try/catch block around req.json() to handle JSON parsing errors. Return a
400 Bad Request response if the request body contains malformed JSON.

templates/plate-playground-template/src/app/api/ai/copilot/route.ts [12-17]

+let body: unknown;
+try {
+  body = await req.json();
+} catch {
+  return NextResponse.json({ error: 'Invalid JSON body.' }, { status: 400 });
+}
+
 const {
   apiKey: key,
   model = 'gpt-4o-mini',
   prompt,
   system,
-} = await req.json();
+} = (body as any) ?? {};
Suggestion importance[1-10]: 7

__

Why: This is a valid suggestion that improves error handling by returning a more specific 400 status for malformed JSON, instead of a generic 500.

Medium
✅ Suggestions up to commit c58bb79
CategorySuggestion                                                                                                                                    Impact
Possible issue
Normalize response shape consistently

Modify the API response to maintain backward compatibility after switching to
generateObject. Return a custom object containing both the new result.object and
a derived top-level text field.

apps/www/src/registry/app/api/ai/copilot/route.ts [29-41]

 const result = await generateObject({
   abortSignal: req.signal,
   model: `openai/${model}`,
   prompt,
   schema: completionSchema,
   schemaDescription: 'A text completion suggestion for the editor',
   schemaName: 'completion',
   system,
   temperature: 0.7,
 });
 
-// Return the full generateText result to preserve the original response contract
-return NextResponse.json(result);
+// Preserve a stable response contract by returning the structured object
+// and a derived text field for compatibility.
+return NextResponse.json({
+  object: result.object,
+  text: result.object?.text ?? '',
+  usage: result.usage,
+  warnings: result.warnings,
+  finishReason: result.finishReason,
+  id: result.id,
+  model: result.model,
+  steps: result.steps,
+});
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a breaking change in the API response contract due to switching from generateText to generateObject, and provides a robust, backward-compatible solution.

High
General
Include metadata in responses
Suggestion Impact:The commit changed the response from returning only result.object to returning the full result object, thereby including metadata and preserving the original response contract.

code diff:

-    return NextResponse.json(result.object);
+    // Return the full generateObject result to preserve the original response contract
+    return NextResponse.json(result);

Instead of returning only result.object, return a comprehensive response that
includes both the structured object and important metadata like usage, finish
reason, and ID.

templates/plate-playground-template/src/app/api/ai/copilot/route.ts [29-40]

 const result = await generateObject({
   abortSignal: req.signal,
   model: `openai/${model}`,
   prompt,
   schema: completionSchema,
   schemaDescription: 'A text completion suggestion for the editor',
   schemaName: 'completion',
   system,
   temperature: 0.7,
 });
 
-return NextResponse.json(result.object);
+return NextResponse.json({
+  object: result.object,
+  text: result.object?.text ?? '',
+  usage: result.usage,
+  warnings: result.warnings,
+  finishReason: result.finishReason,
+  id: result.id,
+  model: result.model,
+  steps: result.steps,
+});
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly points out that returning only result.object discards useful metadata, which could be a breaking change for clients, and proposes returning a more complete response.

Medium
Prevent empty completion outputs

Update the completionSchema to prevent empty or whitespace-only strings for the
text field by adding .trim().min(1) to the Zod validation.

apps/www/src/registry/app/api/ai/copilot/route.ts [7-9]

 const completionSchema = z.object({
-  text: z.string().describe('The completion text suggestion'),
+  text: z.string().trim().min(1, 'Completion text must not be empty').describe('The completion text suggestion'),
 });
Suggestion importance[1-10]: 7

__

Why: This is a valuable suggestion for improving the robustness of the API by ensuring the generated text is not empty or just whitespace, preventing potential downstream errors.

Medium
Qodo
best practice
Fix misleading comment wording

Update the comment to reference generateObject to accurately describe the
current implementation.

apps/www/src/registry/app/api/ai/copilot/route.ts [40-41]

-// Return the full generateText result to preserve the original response contract
+// Return the full generateObject result to preserve the original response contract
 return NextResponse.json(result);
Suggestion importance[1-10]: 5

__

Why:
Relevant best practice - Avoid leaving misleading comments; ensure comments accurately reflect current code behavior to prevent confusion.

Low
✅ Suggestions up to commit c58bb79
CategorySuggestion                                                                                                                                    Impact
Possible issue
Preserve response shape compatibility

To preserve the API contract and prevent breaking changes, modify the response
to return the validated object from result.object instead of the entire result.
This ensures clients receive the expected { text: '...' } shape.

apps/www/src/registry/app/api/ai/copilot/route.ts [29-41]

 const result = await generateObject({
   abortSignal: req.signal,
   model: `openai/${model}`,
   prompt,
   schema: completionSchema,
   schemaDescription: 'A text completion suggestion for the editor',
   schemaName: 'completion',
   system,
   temperature: 0.7,
 });
 
-// Return the full generateText result to preserve the original response contract
-return NextResponse.json(result);
+// Preserve original contract by returning the validated object and a top-level text field
+return NextResponse.json({
+  ...result.object,
+  text: result.object.text,
+});
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies that returning the full generateObject result is a breaking change for clients expecting the generateText response shape, which has a top-level text property. This is a critical bug fix.

Medium
General
Enforce non-empty validated text

Strengthen the completionSchema by adding validation to trim and ensure the text
field is non-empty. This prevents unusable empty strings from being returned.

apps/www/src/registry/app/api/ai/copilot/route.ts [7-9]

 const completionSchema = z.object({
-  text: z.string().describe('The completion text suggestion'),
+  text: z
+    .string()
+    .transform((s) => s.trim())
+    .refine((s) => s.length > 0, { message: 'Completion text cannot be empty' })
+    .describe('The completion text suggestion'),
 });
Suggestion importance[1-10]: 6

__

Why: The suggestion improves data validation by ensuring the text field in the schema is not an empty or whitespace-only string, which enhances the robustness of the API response and prevents potential issues in the client.

Low
Qodo
best practice
Log exceptions with stack trace

Log the caught exception with a logger that records the stack trace before
returning the error response. Prefer an exception-level log (e.g., logger.error
with stack or logger.exception) to aid debugging.

apps/www/src/registry/app/api/ai/copilot/route.ts [28-51]

 try {
   const result = await generateObject({
     abortSignal: req.signal,
     model: `openai/${model}`,
     prompt,
     schema: completionSchema,
     schemaDescription: 'A text completion suggestion for the editor',
     schemaName: 'completion',
     system,
     temperature: 0.7,
   });
 
-  // Return the full generateText result to preserve the original response contract
   return NextResponse.json(result);
 } catch (error) {
   if (error instanceof Error && error.name === 'AbortError') {
     return NextResponse.json(null, { status: 408 });
   }
 
+  // Log the exception with stack for observability
+  console.error('Failed to process AI request', error);
   return NextResponse.json(
     { error: 'Failed to process AI request' },
     { status: 500 }
   );
 }
Suggestion importance[1-10]: 6

__

Why:
Relevant best practice - Add structured error handling and log exceptions with a logger to capture stack traces.

Low
Add exception logging

Before returning the error response, log the exception with a logger that
includes the stack trace to facilitate debugging and incident analysis.

templates/plate-playground-template/src/app/api/ai/copilot/route.ts [28-50]

 try {
   const result = await generateObject({
     abortSignal: req.signal,
     model: `openai/${model}`,
     prompt,
     schema: completionSchema,
     schemaDescription: 'A text completion suggestion for the editor',
     schemaName: 'completion',
     system,
     temperature: 0.7,
   });
 
   return NextResponse.json(result.object);
 } catch (error) {
   if (error instanceof Error && error.name === 'AbortError') {
     return NextResponse.json(null, { status: 408 });
   }
 
+  // Log the exception with stack for observability
+  console.error('Failed to process AI request', error);
   return NextResponse.json(
     { error: 'Failed to process AI request' },
     { status: 500 }
   );
 }
Suggestion importance[1-10]: 5

__

Why:
Relevant best practice - Add structured error handling and log exceptions with a logger to capture stack traces.

Low

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Dec 4, 2025

Code Review by Qodo (Alpha)

Analysis Unavailable

We weren’t able to complete the review on our side. Please try rerunning it

Qodo Logo

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Dec 4, 2025

Note

Unit test generation is an Early Access feature. Expect some limitations and changes as we gather feedback and continue to improve it.


Generating unit tests... This may take up to 20 minutes.

Copy link
Copy Markdown

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

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 successfully migrates the copilot API endpoint from generateText to generateObject, enabling type-safe structured outputs using a Zod schema. The changes correctly introduce the zod import, define a completionSchema, and update the API call parameters to leverage the new structured output capabilities. The maxOutputTokens parameter has been appropriately removed, and the response now correctly returns result.object for the structured data. The implementation is clean and directly addresses the stated objective of the pull request.

@qodo-code-review
Copy link
Copy Markdown

Persistent suggestions updated to latest commit c58bb79

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 408de05 and c58bb79.

📒 Files selected for processing (2)
  • apps/www/src/registry/app/api/ai/copilot/route.ts (2 hunks)
  • templates/plate-playground-template/src/app/api/ai/copilot/route.ts (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (4)
apps/www/src/registry/app/api/ai/copilot/route.ts (3)

3-5: LGTM! Clean imports for structured outputs.

The imports correctly bring in generateObject from the AI SDK and Zod for schema validation.


7-9: LGTM! Well-defined schema for completion responses.

The Zod schema is appropriately scoped with a descriptive text field, which will help the AI model understand the expected output format.


29-38: No changes needed — the generateObject call correctly uses supported parameters.

The ai library v5.0.28 fully supports the schema, schemaDescription, and schemaName parameters. The code is correctly implemented.

templates/plate-playground-template/src/app/api/ai/copilot/route.ts (1)

3-9: LGTM! Imports and schema match the registry example.

The imports and schema definition are consistent with the registry implementation.

Comment on lines +40 to 41
// Return the full generateText result to preserve the original response contract
return NextResponse.json(result);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix outdated comment reference.

The comment references "generateText result" but the code now uses generateObject.

Apply this diff:

-    // Return the full generateText result to preserve the original response contract
+    // Return the full generateObject result to preserve the original response contract
     return NextResponse.json(result);
📝 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
// Return the full generateText result to preserve the original response contract
return NextResponse.json(result);
// Return the full generateObject result to preserve the original response contract
return NextResponse.json(result);
🤖 Prompt for AI Agents
In apps/www/src/registry/app/api/ai/copilot/route.ts around lines 40 to 41, the
inline comment incorrectly references "generateText result" while the code
returns the result from generateObject; update the comment to accurately state
that it returns the full generateObject result (e.g., "Return the full
generateObject result to preserve the original response contract") so the
comment matches the current code.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Dec 4, 2025

Note

Unit test generation is an Early Access feature. Expect some limitations and changes as we gather feedback and continue to improve it.


Generating unit tests... This may take up to 20 minutes.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Dec 4, 2025

Note

Unit test generation is an Early Access feature. Expect some limitations and changes as we gather feedback and continue to improve it.


Generating unit tests... This may take up to 20 minutes.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Dec 4, 2025

🎯 Unit Test Generation Post-Process Complete

The unit test generation and CI/CD fixing process has completed after reaching the maximum number of iterations. Please review the generated tests and any remaining issues manually.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Dec 4, 2025

Creating a PR to put the unit tests in...

The changes have been created in this pull request: View PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Dec 4, 2025

🎯 Unit Test Generation Post-Process Complete

The unit test generation and CI/CD fixing process has completed after reaching the maximum number of iterations. Please review the generated tests and any remaining issues manually.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Dec 4, 2025

Creating a PR to put the unit tests in...

The changes have been created in this pull request: View PR

CodeRabbit Generated Unit Tests: Add comprehensive Bun test suites for AI Copilot API routes
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (2)
apps/www/src/registry/app/api/ai/copilot/route.ts (1)

40-41: Fix outdated comment reference.

The comment still references "generateText" but the code now uses generateObject.

templates/plate-playground-template/src/app/api/ai/copilot/route.ts (1)

40-40: Inconsistent response contract with registry example.

This file returns result.object while the registry version returns the full result. This inconsistency was previously flagged.

🧹 Nitpick comments (3)
apps/www/src/registry/app/api/ai/copilot/route.spec.ts (1)

462-473: Improve malformed JSON test assertion.

The test catches errors but doesn't assert anything meaningful about the error or response behavior. Consider using expect().rejects.toThrow() pattern for consistency with similar tests, or verify the route returns a proper error response.

-    it('should handle malformed JSON in request body', async () => {
-      const request = new NextRequest('http://localhost:3000/api/ai/copilot', {
-        method: 'POST',
-        body: 'not valid json',
-      });
-
-      try {
-        await POST(request);
-      } catch (error) {
-        expect(error).toBeDefined();
-      }
-    });
+    it('should handle malformed JSON in request body', async () => {
+      const request = new NextRequest('http://localhost:3000/api/ai/copilot', {
+        method: 'POST',
+        body: 'not valid json',
+      });
+
+      await expect(POST(request)).rejects.toThrow();
+    });
templates/plate-playground-template/src/app/api/ai/copilot/route.test.ts (1)

847-873: Test mocks schema-incompatible response.

The mock returns { text, metadata } but the completionSchema only defines { text: z.string() }. While this tests the passthrough behavior, it's inconsistent with the actual schema validation that generateObject would perform.

Consider either:

  1. Updating the test to only return schema-compliant data, or
  2. Adding a comment explaining this tests raw passthrough behavior (assuming generateObject has already validated)
templates/plate-playground-template/src/app/api/ai/copilot/route.spec.ts (1)

17-33: Make env restore robust to empty-string values

originalEnv is checked via truthiness, so an original value of '' would be treated as “unset” and removed in afterEach, changing process state. Checking explicitly for undefined is safer while still restoring previous config.

-  const originalEnv = process.env.AI_GATEWAY_API_KEY;
+  const originalEnv = process.env.AI_GATEWAY_API_KEY;

   afterEach(() => {
     mockGenerateObject.mockClear();
     // Restore original env
-    if (originalEnv) {
-      process.env.AI_GATEWAY_API_KEY = originalEnv;
-    } else {
-      delete process.env.AI_GATEWAY_API_KEY;
-    }
+    if (originalEnv !== undefined) {
+      process.env.AI_GATEWAY_API_KEY = originalEnv;
+    } else {
+      delete process.env.AI_GATEWAY_API_KEY;
+    }
   });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 408de05 and 34f4404.

📒 Files selected for processing (6)
  • apps/www/src/registry/app/api/ai/copilot/route.spec.ts (1 hunks)
  • apps/www/src/registry/app/api/ai/copilot/route.test.ts (1 hunks)
  • apps/www/src/registry/app/api/ai/copilot/route.ts (2 hunks)
  • templates/plate-playground-template/src/app/api/ai/copilot/route.spec.ts (1 hunks)
  • templates/plate-playground-template/src/app/api/ai/copilot/route.test.ts (1 hunks)
  • templates/plate-playground-template/src/app/api/ai/copilot/route.ts (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (10)
apps/www/src/registry/app/api/ai/copilot/route.ts (2)

3-9: LGTM! Clean migration to structured outputs.

The Zod schema definition is concise and correctly typed. The .describe() call provides useful context for the AI model.


29-38: LGTM! Correct usage of generateObject with schema validation.

The generateObject call properly passes the schema along with metadata (schemaName, schemaDescription). Temperature is hardcoded appropriately for consistent copilot behavior.

templates/plate-playground-template/src/app/api/ai/copilot/route.ts (1)

3-9: LGTM! Schema definition matches the registry version.

The Zod schema is correctly defined with the same structure as the registry route.

apps/www/src/registry/app/api/ai/copilot/route.spec.ts (2)

1-14: LGTM! Well-structured mock setup.

The mock correctly simulates the generateObject response structure with object, usage, and finishReason fields.


587-640: LGTM! Response format tests correctly verify full result is returned.

These tests properly verify that the registry route returns the complete generateObject result including object, usage, finishReason, and other metadata fields.

apps/www/src/registry/app/api/ai/copilot/route.test.ts (1)

569-591: LGTM! Response format validation is correct.

The test correctly verifies that all fields from the generateObject result are preserved in the response.

templates/plate-playground-template/src/app/api/ai/copilot/route.test.ts (3)

1-14: LGTM! Mock setup matches the other test files.

Consistent mock structure across test files aids maintainability.


150-153: Good explicit test for the different response contract.

This test clearly documents that the template version returns only result.object (the text field), which differs from the registry version that returns the full result.


820-845: Excellent documentation of intentional API differences.

This test suite explicitly documents the response contract difference between the template and registry versions, making it clear this is intentional behavior rather than a bug.

templates/plate-playground-template/src/app/api/ai/copilot/route.spec.ts (1)

735-757: Overall: strong coverage and clear backward‑compatibility check

The suite thoroughly exercises authentication, parameter plumbing, schema usage, error handling, and response shaping, and this final test nicely encodes the intended response contract for existing consumers. Aside from the minor issues called out above, the structure and intent look solid.

Comment on lines +1 to +14
import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
import { NextRequest } from 'next/server';
import { POST } from './route';

// Mock the AI SDK
const mockGenerateObject = mock(() => Promise.resolve({
object: { text: 'completion text' },
usage: { promptTokens: 10, completionTokens: 20 },
finishReason: 'stop',
}));

mock.module('ai', () => ({
generateObject: mockGenerateObject,
}));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if both test files exist and compare their similarity
echo "=== Looking for test files in the directory ==="
fd -e ts -p "route\.(spec|test)\.ts" apps/www/src/registry/app/api/ai/copilot/ 2>/dev/null || echo "No matching files found"

echo ""
echo "=== Checking specific file paths ==="
test -f "apps/www/src/registry/app/api/ai/copilot/route.spec.ts" && echo "route.spec.ts EXISTS" || echo "route.spec.ts NOT FOUND"
test -f "apps/www/src/registry/app/api/ai/copilot/route.test.ts" && echo "route.test.ts EXISTS" || echo "route.test.ts NOT FOUND"

echo ""
echo "=== Directory listing ==="
ls -la apps/www/src/registry/app/api/ai/copilot/ 2>/dev/null | grep -E "\.(spec|test)\.ts$" || echo "No spec/test files found in directory"

Repository: arthrod/plate

Length of output: 449


🏁 Script executed:

#!/bin/bash
# Compare the structure and content of both test files
echo "=== Comparing test file structure ==="

echo "File: route.spec.ts"
grep -E "^\s*(describe|it|test)\(" apps/www/src/registry/app/api/ai/copilot/route.spec.ts | head -20

echo ""
echo "File: route.test.ts"
grep -E "^\s*(describe|it|test)\(" apps/www/src/registry/app/api/ai/copilot/route.test.ts | head -20

echo ""
echo "=== Test counts ==="
echo "route.spec.ts test/describe count: $(grep -cE "^\s*(describe|it|test)\(" apps/www/src/registry/app/api/ai/copilot/route.spec.ts)"
echo "route.test.ts test/describe count: $(grep -cE "^\s*(describe|it|test)\(" apps/www/src/registry/app/api/ai/copilot/route.test.ts)"

echo ""
echo "=== First 50 lines of route.spec.ts ==="
head -50 apps/www/src/registry/app/api/ai/copilot/route.spec.ts

echo ""
echo "=== First 50 lines of route.test.ts ==="
head -50 apps/www/src/registry/app/api/ai/copilot/route.test.ts

Repository: arthrod/plate

Length of output: 5623


🏁 Script executed:

#!/bin/bash
# Get a more detailed comparison of the test cases
echo "=== Unique test descriptions in route.spec.ts ==="
grep -oE "it\('([^']+)" apps/www/src/registry/app/api/ai/copilot/route.spec.ts | sed "s/it('//g" | sort > /tmp/spec_tests.txt
cat /tmp/spec_tests.txt

echo ""
echo "=== Unique test descriptions in route.test.ts ==="
grep -oE "it\('([^']+)" apps/www/src/registry/app/api/ai/copilot/route.test.ts | sed "s/it('//g" | sort > /tmp/test_tests.txt
cat /tmp/test_tests.txt

echo ""
echo "=== Tests in route.spec.ts but NOT in route.test.ts ==="
comm -23 /tmp/spec_tests.txt /tmp/test_tests.txt

echo ""
echo "=== Tests in route.test.ts but NOT in route.spec.ts ==="
comm -13 /tmp/spec_tests.txt /tmp/test_tests.txt

Repository: arthrod/plate

Length of output: 5685


Consolidate duplicate test files to reduce maintenance burden.

This repository has two test suites for the same route: route.spec.ts and route.test.ts. While not identical, they contain significant overlap—both test the same core scenarios (authentication, happy path, edge cases, error handling) with similar test coverage but different naming conventions and slight variations. The test descriptions show near-equivalent coverage with some tests unique to each file (e.g., concurrent request handling in .test.ts, model name prepending in .spec.ts), but merging them into a single file would eliminate the need to maintain parallel test suites.

Consider consolidating into a single test file following your project's conventions (either .spec.ts or .test.ts) to avoid:

  • Duplicated test maintenance when fixing or updating behavior
  • Confusion about which file contains the authoritative test suite
  • Running overlapping tests during CI
🤖 Prompt for AI Agents
In apps/www/src/registry/app/api/ai/copilot/route.test.ts lines 1-14, there is a
duplicate test suite for the same route also covered by route.spec.ts;
consolidate into a single canonical test file (pick .spec.ts or .test.ts per
project convention), merge unique tests from both files into that one file,
unify and dedupe shared setup (imports, mocks, hooks like beforeEach/afterEach),
remove the redundant file, and update any test-runner/config references so only
the chosen filename remains in CI.

Comment on lines +94 to +110
it('should prefer apiKey from body over env variable', async () => {
process.env.AI_GATEWAY_API_KEY = 'env-key';

const request = new NextRequest('http://localhost:3000/api/ai/copilot', {
method: 'POST',
body: JSON.stringify({
apiKey: 'body-key',
model: 'gpt-4',
prompt: 'Test',
system: 'Test',
}),
});

const response = await POST(request);

expect(response.status).toBe(200);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Strengthen the “prefer apiKey from body over env” assertion

This test only checks for a 200 status, which would also be true if the implementation always preferred the env key. It doesn’t actually verify that the body apiKey wins over the env value.

You can assert on the arguments passed to generateObject (or however the key is forwarded) so the test fails if precedence is reversed.

     const response = await POST(request);

     expect(response.status).toBe(200);
+    const callArgs = mockGenerateObject.mock.calls[0][0];
+    // Adjust the property path to match how the api key is actually forwarded.
+    // For example, if you pass it directly:
+    // expect(callArgs.apiKey).toBe('body-key');
+    //
+    // Or, if you wrap it in a client:
+    // expect(callArgs.client.apiKey).toBe('body-key');

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In templates/plate-playground-template/src/app/api/ai/copilot/route.spec.ts
around lines 94-110, the test only asserts a 200 response but doesn't verify
that the body apiKey is used instead of the env var; update the test to spy/mock
the function that receives the API key (e.g., generateObject or whatever
internal call forwards the key), set process.env.AI_GATEWAY_API_KEY = 'env-key',
send the POST with body.apiKey = 'body-key', and assert the mocked function was
invoked with the apiKey 'body-key' (and not 'env-key'); ensure you restore/clear
the mock and env after the test to avoid leaking state.

Comment on lines +530 to +541
it('should handle malformed JSON in request body', async () => {
const request = new NextRequest('http://localhost:3000/api/ai/copilot', {
method: 'POST',
body: 'not valid json',
});

try {
await POST(request);
} catch (error) {
expect(error).toBeDefined();
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Malformed JSON test currently passes even if POST succeeds

In this test, if POST(request) does not throw, the catch block is never executed and the test still passes (there are no assertions after the await POST(request) call). So it doesn’t actually enforce any behavior for malformed JSON.

Consider asserting explicitly that the call either rejects or returns a specific error status.

For example, if the route is expected to throw on invalid JSON:

-    try {
-      await POST(request);
-    } catch (error) {
-      expect(error).toBeDefined();
-    }
+    await expect(POST(request)).rejects.toBeDefined();

Or, if you prefer a graceful HTTP response (recommended), assert on the status instead:

const response = await POST(request);
expect(response.status).toBe(400); // or whatever status you choose for bad JSON
🤖 Prompt for AI Agents
In templates/plate-playground-template/src/app/api/ai/copilot/route.spec.ts
around lines 530-541, the malformed JSON test currently just awaits
POST(request) and only asserts inside a catch block, so it will pass if POST
does not throw; update the test to explicitly assert the expected behavior:
either assert that the call rejects (e.g. use await
expect(POST(request)).rejects.toThrow() or similar) if the route should throw on
invalid JSON, or call await POST(request) and assert the returned
response.status equals the chosen error status (e.g. 400) if the route should
handle it gracefully. Ensure the test contains a single explicit assertion so it
fails when behavior differs.

Comment on lines +712 to +731
it('should handle result.object being undefined gracefully', async () => {
mockGenerateObject.mockImplementationOnce(async () => ({
object: undefined,
}));

const request = new NextRequest('http://localhost:3000/api/ai/copilot', {
method: 'POST',
body: JSON.stringify({
apiKey: 'test-key',
model: 'gpt-4',
prompt: 'Test',
system: 'Test',
}),
});

const response = await POST(request);
const data = await response.json();

expect(data).toBeUndefined();
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the route.ts file in the same directory as the test
find templates/plate-playground-template/src/app/api/ai/copilot -name "route.ts" -o -name "route.js"

Repository: arthrod/plate

Length of output: 124


🏁 Script executed:

# Also check what files are in that directory
ls -la templates/plate-playground-template/src/app/api/ai/copilot/

Repository: arthrod/plate

Length of output: 357


🏁 Script executed:

cat -n templates/plate-playground-template/src/app/api/ai/copilot/route.ts

Repository: arthrod/plate

Length of output: 1625


🌐 Web query:

Next.js NextResponse.json undefined behavior

💡 Result:

Short answer

  • NextResponse.json(undefined) is unreliable: NextResponse uses JSON serialization and passing undefined produces no valid JSON body (JSON.stringify(undefined) -> undefined), which can lead to an empty response body or framework errors/validation depending on runtime and status code. Use null or an explicit value instead. [1][2][3]

Why

  • NextResponse.json is a thin helper that serializes the value to JSON before returning a Response (see NextResponse docs). JSON.stringify(undefined) returns undefined (no JSON text), so sending undefined as the body can produce an empty body or trigger Next.js validation that complains about invalid/unsendable response bodies. [1][2]

Observed problems / real issues reported

  • Dev/prod inconsistencies and runtime errors have been reported when using NextResponse.json in edge/dev environments. There are GitHub issues describing Dev-mode errors and other unexpected behavior from malformed responses. [3][4]

Recommended fixes

  • Return a concrete JSON value: NextResponse.json(null) or NextResponse.json({}) or NextResponse.json({ message: '...' }) instead of undefined.
  • If you intend to return "no content", use an appropriate status with no body (e.g., return new Response(null, { status: 204 })) rather than NextResponse.json(undefined).
  • When returning from middleware/edge handlers make sure you return a valid NextResponse/Response object (see docs). [1][2]

References

  1. NextResponse docs — Next.js. [NextResponse functions/docs]. [1]
  2. Next.js docs: invalid API route status/body (204/304 + body) and response validation. [2]
  3. GitHub issues showing NextResponse.json/dev/runtime problems. [3][4]

(If you want, I can fetch a small repro or show the exact code to replace NextResponse.json(undefined) with a safe pattern.)


🏁 Script executed:

# Let me also check the test file to see what it actually asserts
sed -n '712,731p' templates/plate-playground-template/src/app/api/ai/copilot/route.spec.ts

Repository: arthrod/plate

Length of output: 634


The route handler should explicitly handle undefined from result.object instead of relying on NextResponse.json(undefined)

Line 40 of route.ts returns NextResponse.json(result.object) without checking if result.object is undefined. When result.object is undefined, JSON.stringify(undefined) produces an invalid JSON body, which can cause runtime inconsistencies and unreliable behavior depending on the environment.

Choose an explicit contract—either return null, {}, or a 204 status with no body—and align both the route and test accordingly. For example:

-    return NextResponse.json(result.object);
+    return NextResponse.json(result.object ?? null);

Then update the test assertion to match:

-      expect(data).toBeUndefined();
+      expect(data).toBeNull();

Committable suggestion skipped: line range outside the PR's diff.

@arthrod
Copy link
Copy Markdown
Owner Author

arthrod commented Dec 5, 2025

@coderabbitai review critic. Ur own review. Provide all the changes, if any, require. Dto. Make this work

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Dec 5, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch the actual implementation of both route files to review them critically
echo "=== apps/www route implementation ==="
cat apps/www/src/registry/app/api/ai/copilot/route.ts

echo -e "\n\n=== templates/plate-playground-template route implementation ==="
cat templates/plate-playground-template/src/app/api/ai/copilot/route.ts

Length of output: 2607


@arthrod: I found a critical inconsistency in the implementation that needs to be fixed:

🚨 Critical Issue

The two route files have different return behaviors:

  1. apps/www/src/registry/app/api/ai/copilot/route.ts - Returns the FULL result (incorrect)
  2. templates/plate-playground-template/src/app/api/ai/copilot/route.ts - Returns only result.object (correct)

According to your PR description, you intended to "return the validated object (result.object) rather than the full raw result", but the apps/www version is still returning the full result.

Required Fix

File: apps/www/src/registry/app/api/ai/copilot/route.ts

Change line 36-37 from:

// Return the full generateText result to preserve the original response contract
return NextResponse.json(result);

To:

return NextResponse.json(result.object);

Complete Corrected Implementation

Here's the corrected version of apps/www/src/registry/app/api/ai/copilot/route.ts:

import type { NextRequest } from 'next/server';

import { generateObject } from 'ai';
import { NextResponse } from 'next/server';
import { z } from 'zod';

const completionSchema = z.object({
  text: z.string().describe('The completion text suggestion'),
});

export async function POST(req: NextRequest) {
  const {
    apiKey: key,
    model = 'gpt-4o-mini',
    prompt,
    system,
  } = await req.json();

  const apiKey = key || process.env.AI_GATEWAY_API_KEY;

  if (!apiKey) {
    return NextResponse.json(
      { error: 'Missing ai gateway API key.' },
      { status: 401 }
    );
  }

  try {
    const result = await generateObject({
      abortSignal: req.signal,
      model: `openai/${model}`,
      prompt,
      schema: completionSchema,
      schemaDescription: 'A text completion suggestion for the editor',
      schemaName: 'completion',
      system,
      temperature: 0.7,
    });

    return NextResponse.json(result.object);
  } catch (error) {
    if (error instanceof Error && error.name === 'AbortError') {
      return NextResponse.json(null, { status: 408 });
    }

    return NextResponse.json(
      { error: 'Failed to process AI request' },
      { status: 500 }
    );
  }
}

Summary

  • Remove the outdated comment about "generateText"
  • Change return NextResponse.json(result) to return NextResponse.json(result.object)
  • This makes both route files consistent and aligns with your stated PR objectives

Make this change and your implementation will be correct and consistent across both files.

…te.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@openhands-ai
Copy link
Copy Markdown

openhands-ai bot commented Dec 5, 2025

Looks like there are a few issues preventing this PR from being merged!

  • GitHub Actions are failing:
    • CI Templates

If you'd like me to help, just leave a comment, like

@OpenHands please fix the failing actions on PR #12 at branch `claude/structured-ai-outputs-01QKTPgPJyoPEJsBf69nPFXR`

Feel free to include any additional details that might help me get this PR into a better state.

You can manage your notification settings

@arthrod arthrod merged commit e4c2027 into main Dec 5, 2025
2 of 4 checks passed
@qodo-code-review
Copy link
Copy Markdown

Persistent suggestions updated to latest commit 9ec2fa9

@arthrod arthrod deleted the claude/structured-ai-outputs-01QKTPgPJyoPEJsBf69nPFXR branch December 5, 2025 05:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants