-
Notifications
You must be signed in to change notification settings - Fork 134
Ai sdk 5 migration #10903
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Ai sdk 5 migration #10903
Conversation
|
E2E Tests 🚀 |
sharon-wang
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good so far with Amazon Bedrock, OpenAI and OpenRouter via Custom Provider 👍 getPlot tool is working with Amazon Bedrock, so I wonder if our plot image handling is compatible with Anthropic models but not the OpenAI models?
Providers that don't support the responses endpoint aren't working though -- I think we'll need some way to override the chat endpoint for those providers to /chat/completions
| // This property is now called max_completion_tokens in the AI SDK v5. | ||
| // Example error message without this fix: | ||
| // [OpenAI] [gpt-5]' Error in chat response: {"error":{"message":"Unsupported parameter: 'max_tokens' is not supported with this model. Use 'max_completion_tokens' instead.","type":"invalid_request_error","param":"max_tokens","code":"unsupported_parameter"}} | ||
| if (requestBody.max_tokens !== undefined) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can remove this handling or maybe transformRequestBody altogether, now that we're passing maxOutputTokens instead of maxTokens to ai.streamText?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kept this in for Snowflake compatibility. I was getting errors without this handling.
| try { | ||
| log.debug(`[${this.providerName}] '${model}' Sending test message...`); | ||
|
|
||
| const result = await ai.generateText({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This no longer works for providers that don't support the /v1/responses endpoint, who are still on /v1/chat/completions, such as Snowflake Cortex and I think LM Studio.
Is there a way we can override the chat endpoint to support these providers?
Here's the log when trying to sign in with a provider (e.g. Snowflake) that doesn't support the responses endpoint:
2025-12-03 17:16:50.971 [debug] [Snowflake Cortex] 'claude-haiku-4-5' Sending test message...
2025-12-03 17:16:50.973 [debug] [Snowflake Cortex] [DEBUG] Original request body: {
"model": "claude-haiku-4-5",
"input": [
{
"role": "user",
"content": [
{
"type": "input_text",
"text": "I'm checking to see if you're there. Respond only with the word \"hello\"."
}
]
}
]
}
2025-12-03 17:16:50.973 [debug] [Snowflake Cortex] [DEBUG] Making request to: https://<ACCOUNT_ID>.snowflakecomputing.com/api/v2/cortex/v1/responses
2025-12-03 17:16:51.024 [debug] [Snowflake Cortex] [DEBUG] Response status: 404 Not Found
2025-12-03 17:16:51.025 [warning] [Snowflake Cortex] 'claude-haiku-4-5' Error sending test message: {
"name": "AI_APICallError",
"url": "https://<ACCOUNT_ID>.snowflakecomputing.com/api/v2/cortex/v1/responses",
"requestBodyValues": {
"model": "claude-haiku-4-5",
"input": [
{
"role": "user",
"content": [
{
"type": "input_text",
"text": "I'm checking to see if you're there. Respond only with the word \"hello\"."
}
]
}
]
},
"statusCode": 404,
"responseHeaders": {
"content-length": "0",
"date": "Wed, 03 Dec 2025 22:16:51 GMT",
"expect-ct": "enforce, max-age=3600",
"server": "SF-LB",
"strict-transport-security": "max-age=31536000",
"x-content-type-options": "nosniff",
"x-envoy-attempt-count": "1",
"x-envoy-upstream-service-time": "1",
"x-frame-options": "deny",
"x-snowflake-fe-config": "v20251201.0.0-08c9429c.1764622305.va3.1764800167826",
"x-snowflake-fe-instance": "-",
"x-xss-protection": "1; mode=block"
},
"responseBody": "",
"isRetryable": false
}| * Convert a tool result into a Vercel AI message with experimental content. | ||
| * This is useful for tool results that contain images. | ||
| */ | ||
| function convertToolResultToAiMessageExperimentalContent( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function name (and others with ExperimentalContent) is a bit of a misnomer, now that the output message no longer has an experimental_content field.
| * AI SDK 5 supports images in tool results via the 'content' output type with 'media' parts. | ||
| */ | ||
| function getPlotToolResultToAiMessage(part: vscode.LanguageModelToolResultPart2): ai.CoreUserMessage { | ||
| function getPlotToolResultToAiMessage(part: vscode.LanguageModelToolResultPart2): ai.ToolModelMessage { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just wondering: is this function still needed? I think that convertToolResultToAiMessageExperimentalContent should be able to handle tool results with images.
| if (cacheBreakpoint && bedrockCacheBreakpoint) { | ||
| cacheBreakpoint = false; | ||
| markBedrockCacheBreakpoint(toolMessage); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This probably doesn't have to be addressed just yet, but if/when Anthropic goes through this code path (instead of the separate Anthropic provider code), then we'll want to support their cache breakpoint markers as well.
| .join(''); | ||
|
|
||
| // Try to parse as JSON if it looks like JSON, otherwise use as text | ||
| let output: { type: 'json'; value: unknown } | { type: 'text'; value: string }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a cast down below which can be removed if this type is changed:
| let output: { type: 'json'; value: unknown } | { type: 'text'; value: string }; | |
| let output: { type: 'json'; value: ai.JSONValue } | { type: 'text'; value: string }; |
| ], | ||
| }); | ||
| }; | ||
| aiMessages.push(toolResultMessage as ai.ToolModelMessage); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This cast can be removed if the type of output is changed above.
| aiMessages.push(toolResultMessage as ai.ToolModelMessage); | |
| aiMessages.push(toolResultMessage); |
| *--------------------------------------------------------------------------------------------*/ | ||
|
|
||
| import { log } from './extension.js'; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you add the openai package with npm install --save-dev openai, then you can use the types in this file.
For example, here's a type guard function
| import type { OpenAI } from 'openai/client'; | |
| /** | |
| * Type guard to check if an object is a ChatCompletionChunk. | |
| * @param obj The object to check | |
| * @returns True if the object is a ChatCompletionChunk | |
| */ | |
| export function isChatCompletionChunk(obj: unknown): obj is OpenAI.ChatCompletionChunk { | |
| return ( | |
| typeof obj === 'object' && | |
| obj !== null && | |
| typeof (obj as OpenAI.ChatCompletionChunk).id === 'string' && | |
| Array.isArray((obj as OpenAI.ChatCompletionChunk).choices) && | |
| typeof (obj as OpenAI.ChatCompletionChunk).created === 'number' && | |
| typeof (obj as OpenAI.ChatCompletionChunk).model === 'string' && | |
| (obj as OpenAI.ChatCompletionChunk).object === 'chat.completion.chunk' | |
| ); | |
| } | |
| const data = JSON.parse(jsonStr); | ||
|
|
||
| // Fix empty role fields in delta objects within choices array | ||
| if (data.choices && Array.isArray(data.choices)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using the type guard function from above:
| if (data.choices && Array.isArray(data.choices)) { | |
| if (isChatCompletionChunk(data)) { |
(Although, do Snowflake and others provide chunks that don't quite fit this format?)
| for (const choice of data.choices) { | ||
| if (choice.delta && typeof choice.delta === 'object' && choice.delta.role === '') { | ||
| choice.delta.role = 'assistant'; | ||
| if (choice.delta && typeof choice.delta === 'object') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the type check, this if statement is no longer necessary.
| if (choice.delta && typeof choice.delta === 'object') { |
| // Fix empty role field | ||
| if (choice.delta.role === '') { | ||
| choice.delta.role = 'assistant'; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this check here because of Snowflake, or other providers that don't quite adhere to the OpenAI completions format?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah this was originally added for Snowflake, but it's possible other providers may omit this as well
| } | ||
| } | ||
|
|
||
| transformedLines.push(`data: ${JSON.stringify(data)}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using the types from OpenAI, I suggest actually extracting a lot of the code above into a function which takes as input something that's close to a ChatCompletionChunk, but where the type takes into account the possibly-missing fields. The function would return something that actually is a ChatCompletionChunk.
Then you can really lean into type checking to make sure all the cases are taken care of.
For example, if you define this type, then the role could be "":
type PossiblyBrokenChatCompletionChunk = Omit<OpenAI.ChatCompletionChunk, 'choices'> & {
choices: Array<Omit<OpenAI.ChatCompletionChunk['choices'][0], 'delta'> & {
delta: Omit<OpenAI.ChatCompletionChunk['choices'][0]['delta'], 'role'> & {
role?: OpenAI.ChatCompletionChunk['choices'][0]['delta']['role'] | ''
}
}>
};I know that's kind of awkward but it does work. It could probably be made a bit simpler and easier to read by referencing the types by name instead of by path. (I would just ask an LLM to add in the other possibly-empty fields as well.)
Next, you'd modify the type guard function above to have the signature:
export function isPossiblyBrokenChatCompletionChunk(obj: unknown): obj is PossiblyBrokenChatCompletionChunk {
// body of this function can remain the same
}Then you could add a function with this signature:
function fixPossiblyBrokenChatCompletionChunk(chunk: PossiblyBrokenChatCompletionChunk): OpenAI.ChatCompletionChunk {
// Implementation goes here
}Then up above you'd use:
if (isPossiblyBrokenChatCompletionChunk(data)) {
const fixedChunk = fixPossiblyBrokenChatCompletionChunk(data)
transformedLines.push(`data: ${JSON.stringify(fixedChunk)}`);
sharon-wang
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * Determines if the Posit Web environment is detected. | ||
| */ | ||
| export const IS_RUNNING_ON_PWB = !!process.env.RS_SERVER_URL && vscode.env.uiKind === vscode.UIKind.Web; | ||
| export const IS_RUNNING_ON_PWB = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
testing code left in!
| export const IS_RUNNING_ON_PWB = true; | |
| export const IS_RUNNING_ON_PWB = !!process.env.RS_SERVER_URL && vscode.env.uiKind === vscode.UIKind.Web; |
| // Fix empty role field | ||
| if (choice.delta.role === '') { | ||
| choice.delta.role = 'assistant'; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah this was originally added for Snowflake, but it's possible other providers may omit this as well
| // Transform tools to be compatible with OpenAI-compatible providers | ||
| // Some providers don't support the 'strict' field in tool function definitions | ||
| if (requestBody.tools && Array.isArray(requestBody.tools)) { | ||
| log.debug(`[${providerName}] Request contains ${requestBody.tools.length} tools: ${requestBody.tools.map((t: any) => t.function?.name || t.name).join(', ')}`); | ||
| for (const tool of requestBody.tools) { | ||
| if (tool.function && tool.function.strict !== undefined) { | ||
| delete tool.function.strict; | ||
| log.debug(`[${providerName}] Removed 'strict' field from tool: ${tool.function.name}`); | ||
| } | ||
| } | ||
| log.trace(`[${providerName}] Tools payload: ${JSON.stringify(requestBody.tools)}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thoughts on consolidating the empty input schema handling that is currently in models.ts in AILanguageModel.provideLanguageModelChatResponse() with the handling here, so that all the tool modifications are in one place?

Address #10818
This used Vercel's MCP server to assist in the migration to v5.
Release Notes
New Features
ai-sdkto v5Bug Fixes
QA Notes
I did some light testing with tool calling with various providers. The chat response handling is where things changed the most.