diff --git a/.changeset/eleven-wombats-sin.md b/.changeset/eleven-wombats-sin.md new file mode 100644 index 000000000000..3f351e7fa05b --- /dev/null +++ b/.changeset/eleven-wombats-sin.md @@ -0,0 +1,5 @@ +--- +'@ai-sdk/codemod': patch +--- + +feat(codemods): add codemods for providerMetadata key change diff --git a/content/docs/08-migration-guides/24-migration-guide-6-0.mdx b/content/docs/08-migration-guides/24-migration-guide-6-0.mdx index 235cd4761ae6..b183182eed17 100644 --- a/content/docs/08-migration-guides/24-migration-guide-6-0.mdx +++ b/content/docs/08-migration-guides/24-migration-guide-6-0.mdx @@ -60,6 +60,7 @@ every file. | `rename-tool-call-options-to-tool-execution-options` | Renames the `ToolCallOptions` type to `ToolExecutionOptions` | | `rename-core-message-to-model-message` | Renames the `CoreMessage` type to `ModelMessage` | | `rename-converttocoremessages-to-converttomodelmessages` | Renames `convertToCoreMessages` function to `convertToModelMessages` | +| `rename-vertex-provider-metadata-key` | Renames `google` to `vertex` in `providerMetadata` and `providerOptions` for Google Vertex files | ## AI SDK Core diff --git a/packages/codemod/README.md b/packages/codemod/README.md index a5e8a11be258..a34e34c60c26 100644 --- a/packages/codemod/README.md +++ b/packages/codemod/README.md @@ -146,6 +146,7 @@ npx @ai-sdk/codemod v5/rename-format-stream-part . | `v6/rename-mock-v2-to-v3` | Transforms v6/rename mock v2 to v3 | | `v6/rename-text-embedding-to-embedding` | Transforms v6/rename text embedding to embedding | | `v6/rename-tool-call-options-to-tool-execution-options` | Transforms v6/rename tool call options to tool execution options | +| `v6/rename-vertex-provider-metadata-key` | Transforms v6/rename vertex provider metadata key | ## CLI Options diff --git a/packages/codemod/src/codemods/v6/rename-vertex-provider-metadata-key.ts b/packages/codemod/src/codemods/v6/rename-vertex-provider-metadata-key.ts new file mode 100644 index 000000000000..a5f6e2459fd5 --- /dev/null +++ b/packages/codemod/src/codemods/v6/rename-vertex-provider-metadata-key.ts @@ -0,0 +1,166 @@ +import { createTransformer } from '../lib/create-transformer'; + +export default createTransformer((fileInfo, api, options, context) => { + const { j, root } = context; + + // Only apply to files that import from @ai-sdk/google-vertex + const hasVertexImport = + root + .find(j.ImportDeclaration) + .filter(path => { + return ( + path.node.source.type === 'StringLiteral' && + (path.node.source.value === '@ai-sdk/google-vertex' || + path.node.source.value === '@ai-sdk/google-vertex/edge') + ); + }) + .size() > 0; + + if (!hasVertexImport) { + return; + } + + // Helper to check if a node represents providerMetadata access + const isProviderMetadataAccess = (object: any): boolean => { + if (object.type === 'Identifier') { + return object.name === 'providerMetadata'; + } + // Handle chained access like result.providerMetadata or event?.providerMetadata + if ( + object.type === 'MemberExpression' || + object.type === 'OptionalMemberExpression' + ) { + const prop = object.property; + return prop.type === 'Identifier' && prop.name === 'providerMetadata'; + } + return false; + }; + + // Helper to check if a node is inside a providerOptions object + const isInsideProviderOptions = (path: any): boolean => { + let current = path.parent; + while (current) { + if (current.node.type === 'ObjectExpression') { + const grandparent = current.parent; + if ( + grandparent && + (grandparent.node.type === 'Property' || + grandparent.node.type === 'ObjectProperty') + ) { + const key = grandparent.node.key; + if (key.type === 'Identifier' && key.name === 'providerOptions') { + return true; + } + } + } + current = current.parent; + } + return false; + }; + + // Transform providerMetadata?.google and providerMetadata.google + // Using MemberExpression (covers both optional and non-optional in jscodeshift) + root.find(j.MemberExpression).forEach(path => { + const property = path.node.property; + const object = path.node.object; + + // Property must be 'google' + if (property.type !== 'Identifier' || property.name !== 'google') { + return; + } + + // Check if accessing providerMetadata + if (isProviderMetadataAccess(object)) { + property.name = 'vertex'; + context.hasChanges = true; + } + }); + + // Transform destructuring: const { google } = providerMetadata + // Also handles: const { google: metadata } = providerMetadata + root.find(j.VariableDeclarator).forEach(path => { + const id = path.node.id; + const init = path.node.init; + + if (id.type !== 'ObjectPattern') { + return; + } + + // Check if init is providerMetadata or something?.providerMetadata + let isFromProviderMetadata = false; + if (init) { + if (init.type === 'Identifier' && init.name === 'providerMetadata') { + isFromProviderMetadata = true; + } else if ( + (init.type === 'MemberExpression' || + init.type === 'OptionalMemberExpression') && + init.property.type === 'Identifier' && + init.property.name === 'providerMetadata' + ) { + isFromProviderMetadata = true; + } else if ( + init.type === 'LogicalExpression' && + init.operator === '??' && + init.left.type === 'MemberExpression' && + init.left.property.type === 'Identifier' && + init.left.property.name === 'providerMetadata' + ) { + // Handle: const { google } = result.providerMetadata ?? {} + isFromProviderMetadata = true; + } + } + + if (!isFromProviderMetadata) { + return; + } + + // Find and rename 'google' property in destructuring + id.properties.forEach(prop => { + if (prop.type === 'ObjectProperty' || prop.type === 'Property') { + const key = prop.key; + if (key.type === 'Identifier' && key.name === 'google') { + key.name = 'vertex'; + // If shorthand, also rename the value + if ( + prop.shorthand && + prop.value.type === 'Identifier' && + prop.value.name === 'google' + ) { + prop.value.name = 'vertex'; + } + context.hasChanges = true; + } + } + }); + }); + + // Transform providerOptions: { google: {...} } → providerOptions: { vertex: {...} } + root.find(j.Property).forEach(path => { + const key = path.node.key; + + // Key must be 'google' + if (key.type !== 'Identifier' || key.name !== 'google') { + return; + } + + // Check if this property is inside a providerOptions object + if (isInsideProviderOptions(path)) { + key.name = 'vertex'; + context.hasChanges = true; + } + }); + + // Also handle ObjectProperty (for some AST variations) + root.find(j.ObjectProperty).forEach(path => { + const key = path.node.key; + + if (key.type !== 'Identifier' || key.name !== 'google') { + return; + } + + if (isInsideProviderOptions(path)) { + key.name = 'vertex'; + context.hasChanges = true; + } + }); +}); diff --git a/packages/codemod/src/lib/upgrade.ts b/packages/codemod/src/lib/upgrade.ts index 53c3f7619bf0..8922c757efdb 100644 --- a/packages/codemod/src/lib/upgrade.ts +++ b/packages/codemod/src/lib/upgrade.ts @@ -85,6 +85,7 @@ const bundle = [ 'v6/rename-tool-call-options-to-tool-execution-options', 'v6/rename-core-message-to-model-message', 'v6/rename-converttocoremessages-to-converttomodelmessages', + 'v6/rename-vertex-provider-metadata-key', ]; const log = debug('codemod:upgrade'); diff --git a/packages/codemod/src/test/__testfixtures__/rename-vertex-provider-metadata-key-google-only.input.ts b/packages/codemod/src/test/__testfixtures__/rename-vertex-provider-metadata-key-google-only.input.ts new file mode 100644 index 000000000000..b9310f88455a --- /dev/null +++ b/packages/codemod/src/test/__testfixtures__/rename-vertex-provider-metadata-key-google-only.input.ts @@ -0,0 +1,19 @@ +// @ts-nocheck +// This file uses @ai-sdk/google (NOT vertex) - should NOT be transformed +import { google } from '@ai-sdk/google'; +import { generateText } from 'ai'; + +const result = await generateText({ + model: google('gemini-2.5-flash'), + providerOptions: { + google: { + safetySettings: [], + }, + }, + prompt: 'Hello', +}); + +// These should stay as 'google' since we're using @ai-sdk/google +console.log(result.providerMetadata?.google?.safetyRatings); +const { google: metadata } = result.providerMetadata ?? {}; + diff --git a/packages/codemod/src/test/__testfixtures__/rename-vertex-provider-metadata-key-google-only.output.ts b/packages/codemod/src/test/__testfixtures__/rename-vertex-provider-metadata-key-google-only.output.ts new file mode 100644 index 000000000000..b9310f88455a --- /dev/null +++ b/packages/codemod/src/test/__testfixtures__/rename-vertex-provider-metadata-key-google-only.output.ts @@ -0,0 +1,19 @@ +// @ts-nocheck +// This file uses @ai-sdk/google (NOT vertex) - should NOT be transformed +import { google } from '@ai-sdk/google'; +import { generateText } from 'ai'; + +const result = await generateText({ + model: google('gemini-2.5-flash'), + providerOptions: { + google: { + safetySettings: [], + }, + }, + prompt: 'Hello', +}); + +// These should stay as 'google' since we're using @ai-sdk/google +console.log(result.providerMetadata?.google?.safetyRatings); +const { google: metadata } = result.providerMetadata ?? {}; + diff --git a/packages/codemod/src/test/__testfixtures__/rename-vertex-provider-metadata-key.input.ts b/packages/codemod/src/test/__testfixtures__/rename-vertex-provider-metadata-key.input.ts new file mode 100644 index 000000000000..bd29a8f57028 --- /dev/null +++ b/packages/codemod/src/test/__testfixtures__/rename-vertex-provider-metadata-key.input.ts @@ -0,0 +1,83 @@ +// @ts-nocheck +import { vertex } from '@ai-sdk/google-vertex'; +import { generateText, streamText } from 'ai'; + +// Case 1: Direct providerMetadata access with optional chaining +const result1 = await generateText({ + model: vertex('gemini-2.5-flash'), + prompt: 'Hello', +}); +console.log(result1.providerMetadata?.google?.safetyRatings); +console.log(result1.providerMetadata?.google?.groundingMetadata); +console.log(result1.providerMetadata?.google?.urlContextMetadata); +console.log(result1.providerMetadata?.google?.promptFeedback); +console.log(result1.providerMetadata?.google?.usageMetadata); + +// Case 2: Non-optional access +const metadata = result1.providerMetadata.google; +const ratings = result1.providerMetadata.google.safetyRatings; + +// Case 3: Destructuring from providerMetadata +const { google } = result1.providerMetadata ?? {}; +const { google: vertexMeta } = result1.providerMetadata ?? {}; + +// Case 4: providerOptions input +const result2 = await generateText({ + model: vertex('gemini-2.5-flash'), + providerOptions: { + google: { + safetySettings: [ + { + category: 'HARM_CATEGORY_DANGEROUS_CONTENT', + threshold: 'BLOCK_LOW_AND_ABOVE', + }, + ], + }, + }, + prompt: 'Hello', +}); + +// Case 5: Streaming with finish event +const { stream } = await streamText({ + model: vertex('gemini-2.5-flash'), + prompt: 'Hello', +}); + +for await (const event of stream) { + if (event.type === 'finish') { + console.log(event.providerMetadata?.google?.safetyRatings); + } +} + +// Case 6: Content parts with thoughtSignature +const result3 = await generateText({ + model: vertex('gemini-2.5-flash'), + prompt: 'Think step by step', +}); + +for (const part of result3.content) { + if (part.providerMetadata?.google?.thoughtSignature) { + console.log(part.providerMetadata.google.thoughtSignature); + } +} + +// Case 7: Variable assignment +const providerMetadata = result1.providerMetadata; +const googleMeta = providerMetadata?.google; + +// Case 8: Function that accesses providerMetadata +function logSafetyRatings(result: any) { + return result.providerMetadata?.google?.safetyRatings; +} + +// Case 9: Nested providerOptions in larger config +const config = { + model: vertex('gemini-2.5-flash'), + providerOptions: { + google: { + thinkingConfig: { + thinkingBudget: 1024, + }, + }, + }, +}; diff --git a/packages/codemod/src/test/__testfixtures__/rename-vertex-provider-metadata-key.output.ts b/packages/codemod/src/test/__testfixtures__/rename-vertex-provider-metadata-key.output.ts new file mode 100644 index 000000000000..d56bfc5b3d0c --- /dev/null +++ b/packages/codemod/src/test/__testfixtures__/rename-vertex-provider-metadata-key.output.ts @@ -0,0 +1,83 @@ +// @ts-nocheck +import { vertex } from '@ai-sdk/google-vertex'; +import { generateText, streamText } from 'ai'; + +// Case 1: Direct providerMetadata access with optional chaining +const result1 = await generateText({ + model: vertex('gemini-2.5-flash'), + prompt: 'Hello', +}); +console.log(result1.providerMetadata?.vertex?.safetyRatings); +console.log(result1.providerMetadata?.vertex?.groundingMetadata); +console.log(result1.providerMetadata?.vertex?.urlContextMetadata); +console.log(result1.providerMetadata?.vertex?.promptFeedback); +console.log(result1.providerMetadata?.vertex?.usageMetadata); + +// Case 2: Non-optional access +const metadata = result1.providerMetadata.vertex; +const ratings = result1.providerMetadata.vertex.safetyRatings; + +// Case 3: Destructuring from providerMetadata +const { vertex } = result1.providerMetadata ?? {}; +const { vertex: vertexMeta } = result1.providerMetadata ?? {}; + +// Case 4: providerOptions input +const result2 = await generateText({ + model: vertex('gemini-2.5-flash'), + providerOptions: { + vertex: { + safetySettings: [ + { + category: 'HARM_CATEGORY_DANGEROUS_CONTENT', + threshold: 'BLOCK_LOW_AND_ABOVE', + }, + ], + }, + }, + prompt: 'Hello', +}); + +// Case 5: Streaming with finish event +const { stream } = await streamText({ + model: vertex('gemini-2.5-flash'), + prompt: 'Hello', +}); + +for await (const event of stream) { + if (event.type === 'finish') { + console.log(event.providerMetadata?.vertex?.safetyRatings); + } +} + +// Case 6: Content parts with thoughtSignature +const result3 = await generateText({ + model: vertex('gemini-2.5-flash'), + prompt: 'Think step by step', +}); + +for (const part of result3.content) { + if (part.providerMetadata?.vertex?.thoughtSignature) { + console.log(part.providerMetadata.vertex.thoughtSignature); + } +} + +// Case 7: Variable assignment +const providerMetadata = result1.providerMetadata; +const googleMeta = providerMetadata?.vertex; + +// Case 8: Function that accesses providerMetadata +function logSafetyRatings(result: any) { + return result.providerMetadata?.vertex?.safetyRatings; +} + +// Case 9: Nested providerOptions in larger config +const config = { + model: vertex('gemini-2.5-flash'), + providerOptions: { + vertex: { + thinkingConfig: { + thinkingBudget: 1024, + }, + }, + }, +}; diff --git a/packages/codemod/src/test/rename-vertex-provider-metadata-key.test.ts b/packages/codemod/src/test/rename-vertex-provider-metadata-key.test.ts new file mode 100644 index 000000000000..ec3ba0ff5db7 --- /dev/null +++ b/packages/codemod/src/test/rename-vertex-provider-metadata-key.test.ts @@ -0,0 +1,16 @@ +import { describe, it } from 'vitest'; +import transformer from '../codemods/v6/rename-vertex-provider-metadata-key'; +import { testTransform } from './test-utils'; + +describe('rename-vertex-provider-metadata-key', () => { + it('transforms google-vertex imports correctly', () => { + testTransform(transformer, 'rename-vertex-provider-metadata-key'); + }); + + it('does not transform files using @ai-sdk/google', () => { + testTransform( + transformer, + 'rename-vertex-provider-metadata-key-google-only', + ); + }); +});