Skip to content

Conversation

@RenKoya1
Copy link

@RenKoya1 RenKoya1 commented Nov 24, 2025

Background

The candidates field caused the following validation error when it was missing or undefined:

Invalid input: expected array, received undefined

Summary

  • Updated the schema for candidates to z.array(...).optional()
  • Prevents parsing errors when the field is missing in model responses
  • Improves compatibility with providers that do not always return candidates

Manual Verification

  • Verified that responses with candidates still parse correctly
  • Verified that responses without candidates no longer throw validation errors
  • Successfully ran the package locally via pnpm build and manual invocation to confirm end-to-end behavior

Checklist

  • Tests have been added / updated
  • Documentation has been updated if needed
  • A patch changeset has been added (pnpm changeset)
  • Self-review completed

Future Work

  • Review other response schemas to ensure optional fields are correctly modeled
  • Add tests to cover both presence and absence of candidates

Related Issues

N/A

@RenKoya1 RenKoya1 changed the title add optional to google candidates Make candidates optional in Google provider schema Nov 24, 2025
Copy link
Contributor

@vercel vercel bot left a comment

Choose a reason for hiding this comment

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

Additional Suggestion:

The doGenerate method assumes candidates is always present and tries to access it without checking if it's undefined. Since the schema now makes candidates optional, this will cause a runtime TypeError when the API returns a response without that field.

View Details
📝 Patch Details
diff --git a/packages/google/src/google-generative-ai-language-model.ts b/packages/google/src/google-generative-ai-language-model.ts
index 7c6557f5b..ca03856f7 100644
--- a/packages/google/src/google-generative-ai-language-model.ts
+++ b/packages/google/src/google-generative-ai-language-model.ts
@@ -210,11 +210,11 @@ export class GoogleGenerativeAILanguageModel implements LanguageModelV3 {
       fetch: this.config.fetch,
     });
 
-    const candidate = response.candidates[0];
+    const candidate = response.candidates?.[0];
     const content: Array<LanguageModelV3Content> = [];
 
     // map ordered parts to content:
-    const parts = candidate.content?.parts ?? [];
+    const parts = candidate?.content?.parts ?? [];
 
     const usageMetadata = response.usageMetadata;
 
@@ -279,7 +279,7 @@ export class GoogleGenerativeAILanguageModel implements LanguageModelV3 {
 
     const sources =
       extractSources({
-        groundingMetadata: candidate.groundingMetadata,
+        groundingMetadata: candidate?.groundingMetadata,
         generateId: this.config.generateId,
       }) ?? [];
     for (const source of sources) {
@@ -289,7 +289,7 @@ export class GoogleGenerativeAILanguageModel implements LanguageModelV3 {
     return {
       content,
       finishReason: mapGoogleGenerativeAIFinishReason({
-        finishReason: candidate.finishReason,
+        finishReason: candidate?.finishReason,
         hasToolCalls: content.some(part => part.type === 'tool-call'),
       }),
       usage: {
@@ -303,9 +303,9 @@ export class GoogleGenerativeAILanguageModel implements LanguageModelV3 {
       providerMetadata: {
         google: {
           promptFeedback: response.promptFeedback ?? null,
-          groundingMetadata: candidate.groundingMetadata ?? null,
-          urlContextMetadata: candidate.urlContextMetadata ?? null,
-          safetyRatings: candidate.safetyRatings ?? null,
+          groundingMetadata: candidate?.groundingMetadata ?? null,
+          urlContextMetadata: candidate?.urlContextMetadata ?? null,
+          safetyRatings: candidate?.safetyRatings ?? null,
           usageMetadata: usageMetadata ?? null,
         },
       },

Analysis

Missing null check on optional candidates field in doGenerate() method

What fails: GoogleGenerativeAILanguageModel.doGenerate() directly accesses response.candidates[0] without checking if candidates is undefined, causing a TypeError when the Google Generative AI API returns a response without the candidates field.

How to reproduce:

  1. Call the doGenerate() method on any Google Generative AI language model
  2. The API returns a response where candidates is undefined (allowed by the schema at line 880: .optional())
  3. TypeScript compilation fails with error: error TS18048: 'response.candidates' is possibly 'undefined'

Result: When response.candidates is undefined, accessing response.candidates[0] throws: TypeError: Cannot read property '0' of undefined

Expected: The code should handle responses without candidates gracefully, similar to the doStream() method which uses optional chaining value.candidates?.[0] and checks if (candidate == null) before using the candidate.

Root cause: The schema definition at line 880 makes candidates optional with .optional(), but the doGenerate method at line 213 assumes it always exists. The doStream method (line 400) correctly handles this with optional chaining.

Fix: Apply optional chaining throughout doGenerate() when accessing candidate properties:

  • Line 213: Changed response.candidates[0] to response.candidates?.[0]
  • Line 218: Changed candidate.content?.parts to candidate?.content?.parts
  • Lines 282, 292, 306-308: Added optional chaining for all candidate property accesses

This allows the method to gracefully handle responses without candidates by returning empty content while still providing usage metadata and other response data.

Fix on Vercel

@gr2m
Copy link
Collaborator

gr2m commented Nov 24, 2025

The candidates field caused the following validation error when it was missing or undefined:

can you share a code snippet that triggers the error? The response schema was unchanged since its introduction last year, and we didn't have problems with it thus far. Might be related to the new gemini models?

@gr2m
Copy link
Collaborator

gr2m commented Nov 24, 2025

  • A patch changeset has been added (pnpm changeset)

changeset is missing

@RenKoya1
Copy link
Author

I often get this error with gemini 2.5 pro and gemini 2.5 flash lite and gemini 3 pro
Error message: [ { "expected": "array", "code": "invalid_type", "path": [ "candidates" ], "message": "Invalid input: expected array, received undefined" }

urlContextMetadata: getUrlContextMetadataSchema().nullish(),
}),
),
).optional(),
Copy link
Contributor

@vercel vercel bot Nov 25, 2025

Choose a reason for hiding this comment

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

Making the candidates field optional breaks the type definitions that depend on it. The types at lines 893, 896, 904, and 908 attempt to access array elements via ['candidates'][number], which is invalid when candidates is optional (could be undefined).

View Details
📝 Patch Details
diff --git a/packages/google/src/google-generative-ai-language-model.ts b/packages/google/src/google-generative-ai-language-model.ts
index 7c6557f5b..c8b6f11d9 100644
--- a/packages/google/src/google-generative-ai-language-model.ts
+++ b/packages/google/src/google-generative-ai-language-model.ts
@@ -890,10 +890,10 @@ const responseSchema = lazySchema(() =>
 );
 
 type ContentSchema = NonNullable<
-  InferSchema<typeof responseSchema>['candidates'][number]['content']
+  NonNullable<InferSchema<typeof responseSchema>['candidates']>[number]['content']
 >;
 export type GroundingMetadataSchema = NonNullable<
-  InferSchema<typeof responseSchema>['candidates'][number]['groundingMetadata']
+  NonNullable<InferSchema<typeof responseSchema>['candidates']>[number]['groundingMetadata']
 >;
 
 type GroundingChunkSchema = NonNullable<
@@ -901,11 +901,11 @@ type GroundingChunkSchema = NonNullable<
 >[number];
 
 export type UrlContextMetadataSchema = NonNullable<
-  InferSchema<typeof responseSchema>['candidates'][number]['urlContextMetadata']
+  NonNullable<InferSchema<typeof responseSchema>['candidates']>[number]['urlContextMetadata']
 >;
 
 export type SafetyRatingSchema = NonNullable<
-  InferSchema<typeof responseSchema>['candidates'][number]['safetyRatings']
+  NonNullable<InferSchema<typeof responseSchema>['candidates']>[number]['safetyRatings']
 >[number];
 
 // limited version of the schema, focussed on what is needed for the implementation

Analysis

Optional candidates field breaks type definitions in responseSchema

What fails: TypeScript compilation fails with TS2537 errors when candidates field is marked as optional with .optional() at line 880, because type definitions at lines 893, 896, 904, and 908 attempt to index into the optional field using ['candidates'][number], which is invalid when the field can be undefined.

How to reproduce:

cd packages/google
pnpm type-check

Result: TypeScript compiler throws four TS2537 errors:

  • src/google-generative-ai-language-model.ts(893,52): error TS2537: Type '...' has no matching index signature for type 'number'.
  • src/google-generative-ai-language-model.ts(896,52): error TS2537: Type '...' has no matching index signature for type 'number'.
  • src/google-generative-ai-language-model.ts(904,52): error TS2537: Type '...' has no matching index signature for type 'number'.
  • src/google-generative-ai-language-model.ts(908,52): error TS2537: Type '...' has no matching index signature for type 'number'.

Expected: TypeScript compilation should succeed. The type definitions need to wrap the optional candidates field with NonNullable before attempting to index it, narrowing the type from Array<Candidate> | undefined to Array<Candidate>.

Fix: Wrap InferSchema<typeof responseSchema>['candidates'] with NonNullable in all four type definitions (ContentSchema, GroundingMetadataSchema, UrlContextMetadataSchema, and SafetyRatingSchema).

@RenKoya1
Copy link
Author

RenKoya1 commented Nov 25, 2025

Copy link
Contributor

@vercel vercel bot left a comment

Choose a reason for hiding this comment

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

Additional Suggestion:

The doGenerate method accesses response.candidates[0] without checking if candidates is null or undefined, which will crash when the API returns a response without candidates after the schema change.

View Details
📝 Patch Details
diff --git a/packages/google/src/google-generative-ai-language-model.test.ts b/packages/google/src/google-generative-ai-language-model.test.ts
index 6a33cc1fc..574c86119 100644
--- a/packages/google/src/google-generative-ai-language-model.test.ts
+++ b/packages/google/src/google-generative-ai-language-model.test.ts
@@ -1880,6 +1880,47 @@ describe('doGenerate', () => {
       },
     });
   });
+
+  it('should handle null candidates array', async () => {
+    server.urls[TEST_URL_GEMINI_PRO].response = {
+      type: 'json-value',
+      body: {
+        candidates: null,
+        usageMetadata: {
+          promptTokenCount: 10,
+          totalTokenCount: 10,
+        },
+      },
+    };
+
+    const { content, finishReason, usage } = await model.doGenerate({
+      prompt: TEST_PROMPT,
+    });
+
+    expect(content).toEqual([]);
+    expect(finishReason).toEqual('other');
+    expect(usage.inputTokens).toEqual(10);
+  });
+
+  it('should handle undefined candidates array', async () => {
+    server.urls[TEST_URL_GEMINI_PRO].response = {
+      type: 'json-value',
+      body: {
+        usageMetadata: {
+          promptTokenCount: 10,
+          totalTokenCount: 10,
+        },
+      },
+    };
+
+    const { content, finishReason, usage } = await model.doGenerate({
+      prompt: TEST_PROMPT,
+    });
+
+    expect(content).toEqual([]);
+    expect(finishReason).toEqual('other');
+    expect(usage.inputTokens).toEqual(10);
+  });
 });
 
 describe('doStream', () => {
diff --git a/packages/google/src/google-generative-ai-language-model.ts b/packages/google/src/google-generative-ai-language-model.ts
index 1759f6a95..10c170a77 100644
--- a/packages/google/src/google-generative-ai-language-model.ts
+++ b/packages/google/src/google-generative-ai-language-model.ts
@@ -210,9 +210,42 @@ export class GoogleGenerativeAILanguageModel implements LanguageModelV3 {
       fetch: this.config.fetch,
     });
 
-    const candidate = response.candidates[0];
+    const candidate = response.candidates?.[0];
     const content: Array<LanguageModelV3Content> = [];
 
+    // sometimes the API returns an empty candidates array
+    if (candidate == null) {
+      const usageMetadata = response.usageMetadata;
+
+      return {
+        content: [],
+        finishReason: 'other' as const,
+        usage: {
+          inputTokens: usageMetadata?.promptTokenCount ?? undefined,
+          outputTokens: usageMetadata?.candidatesTokenCount ?? undefined,
+          totalTokens: usageMetadata?.totalTokenCount ?? undefined,
+          reasoningTokens: usageMetadata?.thoughtsTokenCount ?? undefined,
+          cachedInputTokens: usageMetadata?.cachedContentTokenCount ?? undefined,
+        },
+        warnings,
+        providerMetadata: {
+          google: {
+            promptFeedback: response.promptFeedback ?? null,
+            groundingMetadata: null,
+            urlContextMetadata: null,
+            safetyRatings: null,
+            usageMetadata: usageMetadata ?? null,
+          },
+        },
+        request: { body },
+        response: {
+          // TODO timestamp, model id, id
+          headers: responseHeaders,
+          body: rawResponse,
+        },
+      };
+    }
+
     // map ordered parts to content:
     const parts = candidate.content?.parts ?? [];
 

Analysis

Missing null check when accessing response.candidates in doGenerate()

What fails: GoogleGenerativeAILanguageModel.doGenerate() crashes when the Google Generative AI API returns a response without a candidates field (or with null/undefined), because the code accesses response.candidates[0] directly without checking if candidates exists first.

TypeScript error: error TS18049: 'response.candidates' is possibly 'null' or 'undefined' at line 213

How to reproduce: The bug occurs automatically when the API returns either:

  1. { "candidates": null, "usageMetadata": {...} } - explicitly null
  2. { "usageMetadata": {...} } - field missing entirely

Both are valid API responses since the schema at line 872-882 declares candidates: z.array(...).nullish(), making it optional.

Result: Runtime error "Cannot read property '0' of undefined" or "Cannot read property '0' of null"

Expected: Should handle missing candidates gracefully by returning an empty response with appropriate metadata, matching the pattern already implemented in the doStream() method (lines 400-405).

Fix:

  • Changed line 213 from const candidate = response.candidates[0]; to const candidate = response.candidates?.[0]; using optional chaining
  • Added null check: if (candidate == null) { return {...}; } to handle the case where candidates is missing
  • Returns empty content array with finishReason: 'other' and preserves usage metadata, consistent with doStream() pattern

Tests added:

  • Test for null candidates array
  • Test for undefined/missing candidates array

Both tests pass, confirming the fix resolves the issue while maintaining backward compatibility with all 168 existing tests.

Fix on Vercel

@gr2m gr2m changed the title Make candidates optional in Google provider schema fix(google): make candidates in response schema optional Nov 25, 2025
gr2m
gr2m previously approved these changes Nov 25, 2025
Copy link
Collaborator

@gr2m gr2m left a comment

Choose a reason for hiding this comment

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

@gr2m gr2m enabled auto-merge (squash) November 25, 2025 22:47
@gr2m gr2m added the backport label Nov 25, 2025
Copy link
Contributor

@vercel vercel bot left a comment

Choose a reason for hiding this comment

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

Additional Suggestion:

The code will crash at runtime if the candidates field is undefined or null, since it directly accesses response.candidates[0] without checking if candidates exists first.

View Details
📝 Patch Details
diff --git a/packages/google/src/google-generative-ai-language-model.ts b/packages/google/src/google-generative-ai-language-model.ts
index e02f8a4d8..8c8e004b6 100644
--- a/packages/google/src/google-generative-ai-language-model.ts
+++ b/packages/google/src/google-generative-ai-language-model.ts
@@ -210,7 +210,16 @@ export class GoogleGenerativeAILanguageModel implements LanguageModelV3 {
       fetch: this.config.fetch,
     });
 
-    const candidate = response.candidates[0];
+    const candidate = response.candidates?.[0];
+
+    if (candidate == null) {
+      throw new Error(
+        'No candidates returned from Google Generative AI API. ' +
+          'This can happen when the prompt is blocked by safety filters. ' +
+          `Check promptFeedback for details: ${JSON.stringify(response.promptFeedback)}`
+      );
+    }
+
     const content: Array<LanguageModelV3Content> = [];
 
     // map ordered parts to content:

Analysis

Unsafe access to candidates array in GoogleGenerativeAILanguageModel.doGenerate()

What fails: doGenerate() method in packages/google/src/google-generative-ai-language-model.ts crashes at runtime when the Google Generative AI API returns a response with candidates field set to null or undefined.

How to reproduce: The schema at line 894 defines candidates: z.array(...).nullish(), which allows the field to be null, undefined, or missing. When the API returns such a response (which can happen when the prompt is blocked by safety filters), the code at line 213 attempts to access response.candidates[0] without checking if candidates exists first.

Example that triggers the error:

// API returns: { candidates: null, promptFeedback: {...} }
const candidate = response.candidates[0]; // TypeError: Cannot read properties of null (reading '0')

Result: Runtime error - TypeError: Cannot read properties of null (reading '0') or similar

Expected: Should handle missing/null candidates gracefully with a descriptive error, consistent with how doStream() handles this at line 400 using optional chaining value.candidates?.[0]

Related code pattern: The doStream() method correctly handles this case:

const candidate = value.candidates?.[0];
if (candidate == null) {
  return;
}

API Reference: Google Gemini API documentation indicates candidates field is optional and can be missing when the prompt is blocked or fails content filtering.

Fix applied: Added optional chaining and null check before accessing candidates array, with a descriptive error message for debugging.

Fix on Vercel

@gr2m
Copy link
Collaborator

gr2m commented Nov 25, 2025

hmm there is lots of type errors remaining, run

pnpm type-check

It's not a straight forward fix. Can you look into these?

@gr2m gr2m dismissed their stale review November 25, 2025 23:08

pr is not ready

@gr2m
Copy link
Collaborator

gr2m commented Nov 25, 2025

see the review by the vercel bot: #10522 (review). It suggests this might be related to safety filters.

auto-merge was automatically disabled November 26, 2025 08:00

Head branch was pushed to by a user without write access

@RenKoya1
Copy link
Author

@gr2m
Thank you for your supporting.
I've fixed type errors. Please review it!

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