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
10 changes: 9 additions & 1 deletion src/main/models/protocols/openaiCompletions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
43 changes: 40 additions & 3 deletions test/connectionCapabilities.test.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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)
Expand Down Expand Up @@ -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' }
})
})

Expand All @@ -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')
})
Loading