Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/quiet-tomatoes-double.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@ai-sdk/openai-compatible": patch
"@ai-sdk/provider": patch
"ai": patch
---

feat(openai-compatible): emit warning when using kebab-case instead of camelCase
11 changes: 10 additions & 1 deletion packages/ai/src/logger/log-warnings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,11 @@ describe('logWarnings', () => {
feature: 'voice',
details: 'detail2',
},
{
type: 'deprecated',
setting: "providerOptions key 'old-key'",
message: "Use 'oldKey' instead.",
},
{
type: 'other',
message: 'other msg',
Expand All @@ -229,7 +234,7 @@ describe('logWarnings', () => {
logWarnings({ warnings, provider: 'zzz', model: 'MMM' });

expect(mockConsoleInfo).toHaveBeenCalledTimes(1);
expect(mockConsoleWarn).toHaveBeenCalledTimes(3);
expect(mockConsoleWarn).toHaveBeenCalledTimes(4);
expect(mockConsoleWarn).toHaveBeenNthCalledWith(
1,
'AI SDK Warning (zzz / MMM): ' +
Expand All @@ -242,6 +247,10 @@ describe('logWarnings', () => {
);
expect(mockConsoleWarn).toHaveBeenNthCalledWith(
3,
`AI SDK Warning (zzz / MMM): Deprecated: "providerOptions key 'old-key'". Use 'oldKey' instead.`,
);
expect(mockConsoleWarn).toHaveBeenNthCalledWith(
4,
'AI SDK Warning (zzz / MMM): other msg',
);
});
Expand Down
4 changes: 4 additions & 0 deletions packages/ai/src/logger/log-warnings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ function formatWarning({
return message;
}

case 'deprecated': {
return `${prefix} Deprecated: "${warning.setting}". ${warning.message}`;
}

case 'other': {
return `${prefix} ${warning.message}`;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -463,10 +463,15 @@ describe('doGenerate', () => {
}
`);

expect(result.warnings).toContainEqual({
type: 'other',
message: `The 'openai-compatible' key in providerOptions is deprecated. Use 'openaiCompatible' instead.`,
});
expect(result.warnings).toMatchInlineSnapshot(`
[
{
"message": "Use 'openaiCompatible' instead.",
"setting": "providerOptions key 'openai-compatible'",
"type": "deprecated",
},
]
`);
});

it('should include provider-specific options', async () => {
Expand Down Expand Up @@ -611,6 +616,40 @@ describe('doGenerate', () => {
});
});

it('should emit deprecated warning when raw provider options key is used', async () => {
prepareJsonResponse({ content: 'Hello!' });

const result = await provider('grok-3').doGenerate({
providerOptions: {
'test-provider': { reasoningEffort: 'high' },
},
prompt: TEST_PROMPT,
});

expect(result.warnings).toMatchInlineSnapshot(`
[
{
"message": "Use 'testProvider' instead.",
"setting": "providerOptions key 'test-provider'",
"type": "deprecated",
},
]
`);
});

it('should not emit deprecated warning when camelCase provider options key is used', async () => {
prepareJsonResponse({ content: 'Hello!' });

const result = await provider('grok-3').doGenerate({
providerOptions: {
testProvider: { reasoningEffort: 'high' },
},
prompt: TEST_PROMPT,
});

expect(result.warnings).toMatchInlineSnapshot(`[]`);
});

it('should use raw metadata key when no provider options are passed', async () => {
prepareJsonResponse({ content: 'Hello!' });

Expand Down Expand Up @@ -3124,6 +3163,48 @@ describe('doStream', () => {
});
});

it('should emit deprecated warning when raw provider options key is used', async () => {
prepareStreamResponse({ content: ['Hello'] });

const { stream } = await provider('grok-3').doStream({
providerOptions: {
'test-provider': { reasoningEffort: 'high' },
},
prompt: TEST_PROMPT,
includeRawChunks: false,
});

const parts = await convertReadableStreamToArray(stream);
const streamStart = parts.find(part => part.type === 'stream-start');

expect(streamStart?.warnings).toMatchInlineSnapshot(`
[
{
"message": "Use 'testProvider' instead.",
"setting": "providerOptions key 'test-provider'",
"type": "deprecated",
},
]
`);
});

it('should not emit deprecated warning when camelCase provider options key is used', async () => {
prepareStreamResponse({ content: ['Hello'] });

const { stream } = await provider('grok-3').doStream({
providerOptions: {
testProvider: { reasoningEffort: 'high' },
},
prompt: TEST_PROMPT,
includeRawChunks: false,
});

const parts = await convertReadableStreamToArray(stream);
const streamStart = parts.find(part => part.type === 'stream-start');

expect(streamStart?.warnings).toMatchInlineSnapshot(`[]`);
});

it('should use camelCase metadata key in finish event when camelCase options are used', async () => {
prepareStreamResponse({ content: ['Hello'] });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ import {
ResponseHandler,
} from '@ai-sdk/provider-utils';
import { z } from 'zod/v4';
import { resolveProviderOptionsKey, toCamelCase } from '../utils/to-camel-case';
import {
resolveProviderOptionsKey,
toCamelCase,
warnIfDeprecatedProviderOptionsKey,
} from '../utils/to-camel-case';
import {
defaultOpenAICompatibleErrorStructure,
ProviderErrorStructure,
Expand Down Expand Up @@ -140,11 +144,19 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV4 {

if (deprecatedOptions != null) {
warnings.push({
type: 'other',
message: `The 'openai-compatible' key in providerOptions is deprecated. Use 'openaiCompatible' instead.`,
type: 'deprecated',
setting: "providerOptions key 'openai-compatible'",
message: "Use 'openaiCompatible' instead.",
});
}

// Warn when the raw (non-camelCase) provider name is used
warnIfDeprecatedProviderOptionsKey({
rawName: this.providerOptionsName,
providerOptions,
warnings,
});

const compatibleOptions = Object.assign(
deprecatedOptions ?? {},
(await parseProviderOptions({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,44 @@ describe('doGenerate', () => {
someCustomOption: 'camel-value',
});
});

it('should emit deprecated warning when raw provider options key is used', async () => {
prepareJsonResponse({ content: '' });

const result = await provider
.completionModel('gpt-3.5-turbo-instruct')
.doGenerate({
prompt: TEST_PROMPT,
providerOptions: {
'test-provider': { someCustomOption: 'test-value' },
},
});

expect(result.warnings).toMatchInlineSnapshot(`
[
{
"message": "Use 'testProvider' instead.",
"setting": "providerOptions key 'test-provider'",
"type": "deprecated",
},
]
`);
});

it('should not emit deprecated warning when camelCase provider options key is used', async () => {
prepareJsonResponse({ content: '' });

const result = await provider
.completionModel('gpt-3.5-turbo-instruct')
.doGenerate({
prompt: TEST_PROMPT,
providerOptions: {
testProvider: { someCustomOption: 'test-value' },
},
});

expect(result.warnings).toMatchInlineSnapshot(`[]`);
});
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ import {
ResponseHandler,
} from '@ai-sdk/provider-utils';
import { z } from 'zod/v4';
import { toCamelCase } from '../utils/to-camel-case';
import {
toCamelCase,
warnIfDeprecatedProviderOptionsKey,
} from '../utils/to-camel-case';
import {
defaultOpenAICompatibleErrorStructure,
ProviderErrorStructure,
Expand Down Expand Up @@ -102,6 +105,13 @@ export class OpenAICompatibleCompletionLanguageModel implements LanguageModelV4
}: LanguageModelV4CallOptions) {
const warnings: SharedV4Warning[] = [];

// Warn when the raw (non-camelCase) provider name is used
warnIfDeprecatedProviderOptionsKey({
rawName: this.providerOptionsName,
providerOptions,
warnings,
});

// Parse provider options (support both raw and camelCase keys)
const completionOptions = Object.assign(
(await parseProviderOptions({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,57 @@ describe('doEmbed', () => {
dimensions: 64,
});

expect(result.warnings).toContainEqual({
type: 'other',
message: `The 'openai-compatible' key in providerOptions is deprecated. Use 'openaiCompatible' instead.`,
});
expect(result.warnings).toMatchInlineSnapshot(`
[
{
"message": "Use 'openaiCompatible' instead.",
"setting": "providerOptions key 'openai-compatible'",
"type": "deprecated",
},
]
`);
});

it('should emit deprecated warning when raw provider name key is used', async () => {
prepareJsonResponse();

const result = await provider
.embeddingModel('text-embedding-3-large')
.doEmbed({
values: testValues,
providerOptions: {
'test-provider': {
dimensions: 64,
},
},
});

expect(result.warnings).toMatchInlineSnapshot(`
[
{
"message": "Use 'testProvider' instead.",
"setting": "providerOptions key 'test-provider'",
"type": "deprecated",
},
]
`);
});

it('should not emit deprecated warning when camelCase provider name key is used', async () => {
prepareJsonResponse();

const result = await provider
.embeddingModel('text-embedding-3-large')
.doEmbed({
values: testValues,
providerOptions: {
testProvider: {
dimensions: 64,
},
},
});

expect(result.warnings).toMatchInlineSnapshot(`[]`);
});

it('should pass headers', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
defaultOpenAICompatibleErrorStructure,
ProviderErrorStructure,
} from '../openai-compatible-error';
import { warnIfDeprecatedProviderOptionsKey } from '../utils/to-camel-case';

type OpenAICompatibleEmbeddingConfig = {
/**
Expand Down Expand Up @@ -88,11 +89,19 @@ export class OpenAICompatibleEmbeddingModel implements EmbeddingModelV4 {

if (deprecatedOptions != null) {
warnings.push({
type: 'other',
message: `The 'openai-compatible' key in providerOptions is deprecated. Use 'openaiCompatible' instead.`,
type: 'deprecated',
setting: "providerOptions key 'openai-compatible'",
message: "Use 'openaiCompatible' instead.",
});
}

// Warn when the raw (non-camelCase) provider name is used
warnIfDeprecatedProviderOptionsKey({
rawName: this.providerOptionsName,
providerOptions,
warnings,
});

const compatibleOptions = Object.assign(
deprecatedOptions ?? {},
(await parseProviderOptions({
Expand Down
Loading
Loading