From b44d4dcf0119f17cb5b864ebadc668990d8d9378 Mon Sep 17 00:00:00 2001 From: Polly Labs Date: Wed, 15 Jul 2026 21:54:14 -0500 Subject: [PATCH] fix: omit auth header for keyless providers --- src/providers/openai-compatible.ts | 38 +++++++--------- tests/providers/provider-adapters.test.mjs | 52 ++++++++++++++++++++++ 2 files changed, 68 insertions(+), 22 deletions(-) diff --git a/src/providers/openai-compatible.ts b/src/providers/openai-compatible.ts index d5cb616..efc8235 100644 --- a/src/providers/openai-compatible.ts +++ b/src/providers/openai-compatible.ts @@ -19,24 +19,30 @@ function buildOpenAiRequestBody(params: ChatParams, options: { stream?: boolean return body; } +function buildOpenAiHeaders(apiKey: string): Record { + const headers: Record = { + 'Content-Type': 'application/json', + }; + + const trimmedApiKey = apiKey.trim(); + if (trimmedApiKey) { + headers['Authorization'] = `Bearer ${trimmedApiKey}`; + } + + return headers; +} + export class OpenAICompatibleProvider implements Provider { async complete(params: ChatParams): Promise { const { model, apiKey, baseUrl } = params; const url = `${baseUrl.replace(/\/+$/, '')}/chat/completions`; - const headers: Record = { - 'Content-Type': 'application/json', - }; - if (apiKey) { - headers['Authorization'] = `Bearer ${apiKey}`; - } - const response = await fetchWithTimeout( url, { method: 'POST', - headers, + headers: buildOpenAiHeaders(apiKey), body: JSON.stringify(buildOpenAiRequestBody(params)), }, 'OpenAI-compatible API request', @@ -68,18 +74,11 @@ export class OpenAICompatibleProvider implements Provider { const url = `${baseUrl.replace(/\/+$/, '')}/chat/completions`; - const headers: Record = { - 'Content-Type': 'application/json', - }; - if (apiKey) { - headers['Authorization'] = `Bearer ${apiKey}`; - } - const response = await fetchWithTimeout( url, { method: 'POST', - headers, + headers: buildOpenAiHeaders(apiKey), body: JSON.stringify(buildOpenAiRequestBody(params, { stream: true })), }, 'OpenAI-compatible streaming request', @@ -111,12 +110,7 @@ export class OpenAICompatibleProvider implements Provider { async fetchModels(baseUrl: string, apiKey: string): Promise { const url = `${baseUrl.replace(/\/+$/, '')}/models`; - const headers: Record = { - 'Content-Type': 'application/json', - }; - if (apiKey) { - headers['Authorization'] = `Bearer ${apiKey}`; - } + const headers = buildOpenAiHeaders(apiKey); const response = await fetchWithTimeout(url, { headers }, 'OpenAI-compatible model request'); diff --git a/tests/providers/provider-adapters.test.mjs b/tests/providers/provider-adapters.test.mjs index 5c9843e..1c13f21 100644 --- a/tests/providers/provider-adapters.test.mjs +++ b/tests/providers/provider-adapters.test.mjs @@ -331,6 +331,32 @@ test('OpenAICompatibleProvider complete forwards messages and trims the first ch }); }); +test('OpenAICompatibleProvider complete omits authorization for keyless providers', async (t) => { + const provider = new OpenAICompatibleProvider(); + const requestLog = []; + + mockFetch(t, async (url, init) => { + requestLog.push({ url, init }); + return new Response( + JSON.stringify({ + model: 'llama3', + choices: [{ message: { content: 'local answer' } }], + }), + { status: 200, headers: { 'Content-Type': 'application/json' } }, + ); + }); + + await provider.complete({ + model: 'llama3', + baseUrl: 'http://localhost:11434/v1', + apiKey: '', + messages: [{ role: 'user', content: 'hello' }], + }); + + assert.equal(requestLog.length, 1); + assert.equal(requestLog[0].init?.headers.Authorization, undefined); +}); + test('OpenAICompatibleProvider complete surfaces API errors with the response body', async (t) => { const provider = new OpenAICompatibleProvider(); @@ -482,3 +508,29 @@ test('OpenAICompatibleProvider completeStream parses streaming chunks and stops { kind: 'text', text: ' world' }, ]); }); + +test('OpenAICompatibleProvider completeStream omits authorization for keyless providers', async (t) => { + const provider = new OpenAICompatibleProvider(); + const requestLog = []; + + mockFetch(t, async (url, init) => { + requestLog.push({ url, init }); + return new Response('data: [DONE]', { + status: 200, + headers: { 'Content-Type': 'text/event-stream' }, + }); + }); + + const chunks = await collectChunks( + provider.completeStream({ + model: 'llama3', + baseUrl: 'http://localhost:11434/v1', + apiKey: '', + messages: [{ role: 'user', content: 'hello' }], + }), + ); + + assert.deepEqual(chunks, []); + assert.equal(requestLog.length, 1); + assert.equal(requestLog[0].init?.headers.Authorization, undefined); +});