diff --git a/src/main/models/protocols/openaiCompletions.ts b/src/main/models/protocols/openaiCompletions.ts index 9d81582..14636c8 100644 --- a/src/main/models/protocols/openaiCompletions.ts +++ b/src/main/models/protocols/openaiCompletions.ts @@ -11,7 +11,15 @@ import type { ModelConnection } from '../../../shared/types/connection' import type { ProtocolAdapter } from './types' import { listOpenAICompatibleModels } from './modelListing' -export const PROVIDER_OPTIONS_KEY = 'openai-compatible' +/** + * The provider's name, and the key its `providerOptions` are namespaced under. + * + * AI SDK 7 wants the camelCase form. Passing `'openai-compatible'` still works, but + * it emits a deprecation warning on every call and lands `providerMetadata` under + * the kebab-case key. This value is what both the provider name and the options + * key resolve from, so nothing has to be kept in sync by hand. + */ +export const PROVIDER_OPTIONS_KEY = 'openaiCompatible' function createProvider(connection: ModelConnection) { return createOpenAICompatible({ diff --git a/test/connectionCapabilities.test.ts b/test/connectionCapabilities.test.ts index 269d3a2..00b207d 100644 --- a/test/connectionCapabilities.test.ts +++ b/test/connectionCapabilities.test.ts @@ -1,8 +1,13 @@ import { test } from 'node:test' import assert from 'node:assert/strict' import { MockLanguageModelV3, simulateReadableStream } from 'ai/test' +import { streamText } from 'ai' +import { createOpenAICompatible } from '@ai-sdk/openai-compatible' import { ModelClient } from '../src/main/models/ModelClient.ts' -import { openaiCompletionsAdapter } from '../src/main/models/protocols/openaiCompletions.ts' +import { + openaiCompletionsAdapter, + PROVIDER_OPTIONS_KEY +} from '../src/main/models/protocols/openaiCompletions.ts' import { openaiResponsesAdapter } from '../src/main/models/protocols/openaiResponses.ts' import { anthropicMessagesAdapter } from '../src/main/models/protocols/anthropicMessages.ts' import { googleGenerativeAiAdapter } from '../src/main/models/protocols/googleGenerativeAi.ts' @@ -27,7 +32,7 @@ test('openai-compatible connections translate effort into reasoningEffort', () = assert.deepEqual( openaiCompletionsAdapter.chatProviderOptions?.({ ...base, reasoningEffort: 'low' }), { - 'openai-compatible': { reasoningEffort: 'low' } + openaiCompatible: { reasoningEffort: 'low' } } ) assert.equal(openaiCompletionsAdapter.chatProviderOptions?.({ ...base }), undefined) @@ -142,7 +147,7 @@ test('the reasoning configuration reaches the provider request', async () => { await drain(client) assert.deepEqual(model.doStreamCalls[0]?.providerOptions, { - 'openai-compatible': { reasoningEffort: 'low' } + openaiCompatible: { reasoningEffort: 'low' } }) }) @@ -156,3 +161,35 @@ test('a connection with no reasoning metadata sends no provider options', async 'a connection that declared no reasoning settings was given some anyway' ) }) + +test('provider metadata is namespaced under the camelCase key', async () => { + // The provider picks the `providerMetadata` key from the key the caller sends in + // `providerOptions`. Sending the deprecated kebab-case key would put + // `providerMetadata['openai-compatible']` in front of every consumer and log a + // deprecation warning on each call, so this pins the camelCase form. + const sse = [ + 'data: {"id":"1","object":"chat.completion.chunk","created":0,"model":"m","choices":[{"index":0,"delta":{"role":"assistant","content":"ok"},"finish_reason":null}]}\n\n', + 'data: {"id":"1","object":"chat.completion.chunk","created":0,"model":"m","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}\n\n', + 'data: [DONE]\n\n' + ].join('') + + const provider = createOpenAICompatible({ + name: PROVIDER_OPTIONS_KEY, + baseURL: 'https://example.invalid/v1', + apiKey: 'test', + fetch: async () => + new Response(sse, { status: 200, headers: { 'content-type': 'text/event-stream' } }) + }) + + const result = streamText({ + model: provider('test-model'), + prompt: 'hi', + providerOptions: { [PROVIDER_OPTIONS_KEY]: { reasoningEffort: 'low' } } + }) + for await (const chunk of result.textStream) { + void chunk + } + + assert.deepEqual(Object.keys((await result.providerMetadata) ?? {}), [PROVIDER_OPTIONS_KEY]) + assert.equal(PROVIDER_OPTIONS_KEY, 'openaiCompatible') +})