Skip to content

Conversation

@timtmok
Copy link
Contributor

@timtmok timtmok commented Dec 3, 2025

Address #10818

This used Vercel's MCP server to assist in the migration to v5.

  • Removed the workaround for the temperature
  • Updated the field names that changed
  • Fixed the responses to match the v5 format

Release Notes

New Features

  • Update ai-sdk to v5

Bug Fixes

  • N/A

QA Notes

I did some light testing with tool calling with various providers. The chat response handling is where things changed the most.

@github-actions
Copy link

github-actions bot commented Dec 3, 2025

E2E Tests 🚀
This PR will run tests tagged with: @:critical

readme  valid tags

@timtmok timtmok marked this pull request as ready for review December 3, 2025 17:03
@timtmok timtmok requested a review from sharon-wang December 3, 2025 17:03
Copy link
Member

@sharon-wang sharon-wang left a 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) {
Copy link
Member

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?

Copy link
Contributor Author

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({
Copy link
Member

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
}

@sharon-wang sharon-wang requested a review from wch December 3, 2025 22:42
@timtmok timtmok requested a review from sharon-wang December 4, 2025 19:54
* Convert a tool result into a Vercel AI message with experimental content.
* This is useful for tool results that contain images.
*/
function convertToolResultToAiMessageExperimentalContent(
Copy link
Contributor

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 {
Copy link
Contributor

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.

Comment on lines 82 to 85
if (cacheBreakpoint && bedrockCacheBreakpoint) {
cacheBreakpoint = false;
markBedrockCacheBreakpoint(toolMessage);
}
Copy link
Contributor

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 };
Copy link
Contributor

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:

Suggested change
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);
Copy link
Contributor

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.

Suggested change
aiMessages.push(toolResultMessage as ai.ToolModelMessage);
aiMessages.push(toolResultMessage);

*--------------------------------------------------------------------------------------------*/

import { log } from './extension.js';

Copy link
Contributor

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

Suggested change
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)) {
Copy link
Contributor

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:

Suggested change
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') {
Copy link
Contributor

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.

Suggested change
if (choice.delta && typeof choice.delta === 'object') {

Comment on lines +131 to +134
// Fix empty role field
if (choice.delta.role === '') {
choice.delta.role = 'assistant';
}
Copy link
Contributor

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?

Copy link
Member

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)}`);
Copy link
Contributor

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)}`);

Copy link
Member

@sharon-wang sharon-wang left a comment

Choose a reason for hiding this comment

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

I can log in with Snowflake now ❄️ ✅

I'm not sure if this is my local state, but I lost the Snowflake icon in the models popup:

Image

The icon is there in a release build of 2026.01.0+24, so not sure if it's my dev build state or possibly something has changed?

* 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;
Copy link
Member

Choose a reason for hiding this comment

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

testing code left in!

Suggested change
export const IS_RUNNING_ON_PWB = true;
export const IS_RUNNING_ON_PWB = !!process.env.RS_SERVER_URL && vscode.env.uiKind === vscode.UIKind.Web;

Comment on lines +131 to +134
// Fix empty role field
if (choice.delta.role === '') {
choice.delta.role = 'assistant';
}
Copy link
Member

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

Comment on lines +58 to +68
// 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)}`);
Copy link
Member

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants