diff --git a/src/services/api/openaiShim.test.ts b/src/services/api/openaiShim.test.ts index e1413df687..0f4278b266 100644 --- a/src/services/api/openaiShim.test.ts +++ b/src/services/api/openaiShim.test.ts @@ -582,68 +582,12 @@ afterEach(() => { } }) -test('strips canonical Anthropic headers from direct shim defaultHeaders', async () => { - let capturedHeaders: Headers | undefined - - globalThis.fetch = (async (_input, init) => { - capturedHeaders = new Headers(init?.headers) - - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'gpt-4o', - choices: [ - { - message: { - role: 'assistant', - content: 'ok', - }, - finish_reason: 'stop', - }, - ], - usage: { - prompt_tokens: 8, - completion_tokens: 3, - total_tokens: 11, - }, - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({ - defaultHeaders: { - 'anthropic-version': '2023-06-01', - 'anthropic-beta': 'prompt-caching-2024-07-31', - 'x-anthropic-additional-protection': 'true', - 'x-claude-remote-session-id': 'remote-123', - 'x-app': 'cli', - 'x-client-app': 'sdk', - 'x-safe-header': 'keep-me', - }, - }) as OpenAIShimClient +// openaiShim test extraction seam 001 start: strips canonical Anthropic headers from direct shim defaultHeaders - await client.beta.messages.create({ - model: 'gpt-4o', - system: 'test system', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }) +// openaiShim test extraction seam 001 end - expect(capturedHeaders?.get('anthropic-version')).toBeNull() - expect(capturedHeaders?.get('anthropic-beta')).toBeNull() - expect(capturedHeaders?.get('x-anthropic-additional-protection')).toBeNull() - expect(capturedHeaders?.get('x-claude-remote-session-id')).toBeNull() - expect(capturedHeaders?.get('x-app')).toBeNull() - expect(capturedHeaders?.get('x-client-app')).toBeNull() - expect(capturedHeaders?.get('x-safe-header')).toBe('keep-me') -}) +// openaiShim test extraction seam 002 start: uses OpenAI-compatible responses endpoint when OPENAI_API_FORMAT=responses test('uses OpenAI-compatible responses endpoint when OPENAI_API_FORMAT=responses', async () => { process.env.OPENAI_API_FORMAT = 'responses' let capturedUrl = '' @@ -701,7 +645,10 @@ test('uses OpenAI-compatible responses endpoint when OPENAI_API_FORMAT=responses }, ]) }) +// openaiShim test extraction seam 002 end + +// openaiShim test extraction seam 003 start: nests reasoning effort for OpenAI-compatible responses endpoint test('nests reasoning effort for OpenAI-compatible responses endpoint', async () => { process.env.OPENAI_API_FORMAT = 'responses' let capturedBody: Record | undefined @@ -743,6 +690,7 @@ test('nests reasoning effort for OpenAI-compatible responses endpoint', async () expect(capturedBody).not.toHaveProperty('reasoning_effort') expect(capturedBody).not.toHaveProperty('reasoning_summary') }) +// openaiShim test extraction seam 003 end test('auto-routes gpt-5.6 to /responses on api.openai.com with tools and nested reasoning', async () => { // No OPENAI_API_FORMAT set: the model+base predicate must pick responses. @@ -1310,6 +1258,7 @@ test('auto-routed gpt-5.6 on an Azure base nests reasoning.effort and the encryp expect(capturedBody?.include).toEqual(['reasoning.encrypted_content']) }) +// openaiShim test extraction seam 004 start: uses OpenAI-compatible responses endpoint with text chunk types when OPENAI_API_FORMAT=responses_compat test('uses OpenAI-compatible responses endpoint with text chunk types when OPENAI_API_FORMAT=responses_compat', async () => { process.env.OPENAI_API_FORMAT = 'responses_compat' let capturedUrl = '' @@ -1367,7 +1316,10 @@ test('uses OpenAI-compatible responses endpoint with text chunk types when OPENA }, ]) }) +// openaiShim test extraction seam 004 end + +// openaiShim test extraction seam 005 start: uses correct empty input fallback schema for standard responses and responses_compat test('uses correct empty input fallback schema for standard responses and responses_compat', async () => { let capturedBody: Record | undefined @@ -1412,7 +1364,10 @@ test('uses correct empty input fallback schema for standard responses and respon }, ]) }) +// openaiShim test extraction seam 005 end + +// openaiShim test extraction seam 006 start: strips store from strict OpenAI-compatible responses providers test('strips store from strict OpenAI-compatible responses providers', async () => { process.env.OPENAI_BASE_URL = 'https://api.moonshot.ai/v1' process.env.OPENAI_API_FORMAT = 'responses' @@ -1455,7 +1410,10 @@ test('strips store from strict OpenAI-compatible responses providers', async () expect(capturedUrl).toBe('https://api.moonshot.ai/v1/responses') expect(capturedBody?.store).toBeUndefined() }) +// openaiShim test extraction seam 006 end + +// openaiShim test extraction seam 007 start: strips store when providerOverride routes chat_completions to the Gemini host test('strips store when providerOverride routes chat_completions to the Gemini host', async () => { let capturedBody: Record | undefined @@ -1488,7 +1446,10 @@ test('strips store when providerOverride routes chat_completions to the Gemini h expect(capturedBody?.store).toBeUndefined() }) +// openaiShim test extraction seam 007 end + +// openaiShim test extraction seam 008 start: strips store when providerOverride routes responses API to the Gemini host test('strips store when providerOverride routes responses API to the Gemini host', async () => { process.env.OPENAI_API_FORMAT = 'responses' let capturedBody: Record | undefined @@ -1528,7 +1489,10 @@ test('strips store when providerOverride routes responses API to the Gemini host expect(capturedBody?.store).toBeUndefined() }) +// openaiShim test extraction seam 008 end + +// openaiShim test extraction seam 009 start: uses custom OpenAI-compatible auth header value when configured test('uses custom OpenAI-compatible auth header value when configured', async () => { process.env.OPENAI_API_KEY = 'generic-key' process.env.OPENAI_AUTH_HEADER = 'api-key' @@ -1563,7 +1527,10 @@ test('uses custom OpenAI-compatible auth header value when configured', async () expect(capturedHeaders?.get('api-key')).toBe('hicap-header-value') expect(capturedHeaders?.get('authorization')).toBeNull() }) +// openaiShim test extraction seam 009 end + +// openaiShim test extraction seam 010 start: uses Hicap api-key auth header for the Hicap route test('uses Hicap api-key auth header for the Hicap route', async () => { process.env.OPENAI_API_KEY = 'hicap-live-key' process.env.OPENAI_BASE_URL = 'https://api.hicap.ai/v1' @@ -1597,7 +1564,10 @@ test('uses Hicap api-key auth header for the Hicap route', async () => { expect(capturedHeaders?.get('api-key')).toBe('hicap-live-key') expect(capturedHeaders?.get('authorization')).toBeNull() }) +// openaiShim test extraction seam 010 end + +// openaiShim test extraction seam 011 start: defaults Authorization custom auth header to bearer scheme test('defaults Authorization custom auth header to bearer scheme', async () => { process.env.OPENAI_API_KEY = 'authorization-key' process.env.OPENAI_AUTH_HEADER = 'Authorization' @@ -1630,7 +1600,10 @@ test('defaults Authorization custom auth header to bearer scheme', async () => { expect(capturedHeaders?.get('authorization')).toBe('Bearer authorization-key') }) +// openaiShim test extraction seam 011 end + +// openaiShim test extraction seam 012 start: honors bearer scheme for custom OpenAI-compatible auth headers test('honors bearer scheme for custom OpenAI-compatible auth headers', async () => { process.env.OPENAI_API_KEY = 'custom-key' process.env.OPENAI_AUTH_HEADER = 'X-Custom-Authorization' @@ -1665,7 +1638,10 @@ test('honors bearer scheme for custom OpenAI-compatible auth headers', async () expect(capturedHeaders?.get('x-custom-authorization')).toBe('Bearer custom-key') expect(capturedHeaders?.get('authorization')).toBeNull() }) +// openaiShim test extraction seam 012 end + +// openaiShim test extraction seam 013 start: ignores custom auth header value when no custom header is configured test('ignores custom auth header value when no custom header is configured', async () => { delete process.env.OPENAI_API_KEY process.env.OPENAI_AUTH_HEADER_VALUE = 'gateway-header-value' @@ -1698,64 +1674,15 @@ test('ignores custom auth header value when no custom header is configured', asy expect(capturedHeaders?.get('authorization')).toBeNull() }) +// openaiShim test extraction seam 013 end -test('strips canonical Anthropic headers from per-request shim headers too', async () => { - let capturedHeaders: Headers | undefined - - globalThis.fetch = (async (_input, init) => { - capturedHeaders = new Headers(init?.headers) - - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'gpt-4o', - choices: [ - { - message: { - role: 'assistant', - content: 'ok', - }, - finish_reason: 'stop', - }, - ], - usage: { - prompt_tokens: 8, - completion_tokens: 3, - total_tokens: 11, - }, - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType - const client = createOpenAIShimClient({}) as OpenAIShimClient +// openaiShim test extraction seam 014 start: strips canonical Anthropic headers from per-request shim headers too - await client.beta.messages.create( - { - model: 'gpt-4o', - system: 'test system', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }, - { - headers: { - 'anthropic-version': '2023-06-01', - 'anthropic-beta': 'prompt-caching-2024-07-31', - 'x-safe-header': 'keep-me', - }, - }, - ) +// openaiShim test extraction seam 014 end - expect(capturedHeaders?.get('anthropic-version')).toBeNull() - expect(capturedHeaders?.get('anthropic-beta')).toBeNull() - expect(capturedHeaders?.get('x-safe-header')).toBe('keep-me') -}) +// openaiShim test extraction seam 015 start: applies descriptor static headers before client and request headers test('applies descriptor static headers before client and request headers', async () => { let capturedHeaders: Headers | undefined @@ -1839,7 +1766,10 @@ test('applies descriptor static headers before client and request headers', asyn expect(capturedHeaders?.get('x-static-header')).toBe('from-descriptor') expect(capturedHeaders?.get('x-override-header')).toBe('from-request') }) +// openaiShim test extraction seam 015 end + +// openaiShim test extraction seam 016 start: opengateway sends Accept-Encoding: identity header on chat requests test('opengateway sends Accept-Encoding: identity header on chat requests', async () => { let capturedHeaders: Headers | undefined @@ -1922,7 +1852,10 @@ test('opengateway sends Accept-Encoding: identity header on chat requests', asyn expect(capturedHeaders?.get('Accept-Encoding')).toBe('identity') }) +// openaiShim test extraction seam 016 end + +// openaiShim test extraction seam 017 start: strips Anthropic-specific headers on GitHub Codex transport requests test('strips Anthropic-specific headers on GitHub Codex transport requests', async () => { let capturedHeaders: Headers | undefined @@ -1971,7 +1904,10 @@ test('strips Anthropic-specific headers on GitHub Codex transport requests', asy expect(capturedHeaders?.get('authorization')).toBe('Bearer github-test-key') expect(capturedHeaders?.get('editor-plugin-version')).toBe('copilot-chat/0.26.7') }) +// openaiShim test extraction seam 017 end + +// openaiShim test extraction seam 018 start: uses direct GitHub Copilot Enterprise key for shim authentication test('uses direct GitHub Copilot Enterprise key for shim authentication', async () => { process.env.CLAUDE_CODE_USE_GITHUB = '1' process.env.GITHUB_COPILOT_KEY = 'enterprise-direct-key' @@ -1986,7 +1922,10 @@ test('uses direct GitHub Copilot Enterprise key for shim authentication', async expect(authorization).toBe('Bearer enterprise-direct-key') expect(url).toBe('https://github.mycompany.com/api/copilot/chat/completions') }) +// openaiShim test extraction seam 018 end + +// openaiShim test extraction seam 019 start: direct GitHub Copilot key wins over stale OpenAI key test('direct GitHub Copilot key wins over stale OpenAI key', async () => { process.env.CLAUDE_CODE_USE_GITHUB = '1' process.env.GITHUB_COPILOT_KEY = 'enterprise-direct-key' @@ -2000,7 +1939,10 @@ test('direct GitHub Copilot key wins over stale OpenAI key', async () => { expect(authorization).toBe('Bearer enterprise-direct-key') }) +// openaiShim test extraction seam 019 end + +// openaiShim test extraction seam 020 start: strips Anthropic-specific headers on GitHub Codex transport with providerOverride API key test('strips Anthropic-specific headers on GitHub Codex transport with providerOverride API key', async () => { let capturedHeaders: Headers | undefined @@ -2051,7 +1993,10 @@ test('strips Anthropic-specific headers on GitHub Codex transport with providerO expect(capturedHeaders?.get('authorization')).toBe('Bearer provider-override-key') expect(capturedHeaders?.get('editor-plugin-version')).toBe('copilot-chat/0.26.7') }) +// openaiShim test extraction seam 020 end + +// openaiShim test extraction seam 021 start: preserves usage from final OpenAI stream chunk with empty choices test('preserves usage from final OpenAI stream chunk with empty choices', async () => { globalThis.fetch = (async (_input, init) => { const url = typeof _input === 'string' ? _input : _input.url @@ -2127,9 +2072,12 @@ test('preserves usage from final OpenAI stream chunk with empty choices', async expect(usageEvent?.usage?.input_tokens).toBe(123) expect(usageEvent?.usage?.output_tokens).toBe(45) }) +// openaiShim test extraction seam 021 end + // Extraction seam: stream conversion usage | shared stream control. +// openaiShim test extraction seam 022 start: readWithIdleTimeout rejects quickly and cancels a stalled reader test('readWithIdleTimeout rejects quickly and cancels a stalled reader', async () => { const testApi = await getStreamIdleTestApi('stream-idle-helper') const cancelReasons: unknown[] = [] @@ -2153,7 +2101,10 @@ test('readWithIdleTimeout rejects quickly and cancels a stalled reader', async ( expect(cancelReasons).toHaveLength(1) expect(cancelReasons[0]).toBeInstanceOf(testApi.StreamIdleTimeoutError) }) +// openaiShim test extraction seam 022 end + +// openaiShim test extraction seam 023 start: readWithIdleTimeout preserves parent abort instead of reporting idle timeout test('readWithIdleTimeout preserves parent abort instead of reporting idle timeout', async () => { const testApi = await getStreamIdleTestApi('stream-idle-user-abort') const parent = new AbortController() @@ -2182,7 +2133,10 @@ test('readWithIdleTimeout preserves parent abort instead of reporting idle timeo expect(cancelReasons[0]).toBeInstanceOf(DOMException) expect((cancelReasons[0] as DOMException).name).toBe('AbortError') }) +// openaiShim test extraction seam 023 end + +// openaiShim test extraction seam 024 start: stream idle timeout env parser parses and bounds overrides test('stream idle timeout env parser parses and bounds overrides', async () => { const testApi = await getStreamIdleTestApi('stream-idle-env-parser') @@ -2210,6 +2164,7 @@ test('stream idle timeout env parser parses and bounds overrides', async () => { process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS = '-5' expect(testApi.getStreamIdleTimeoutMs()).toBe(90_000) }) +// openaiShim test extraction seam 024 end test('API timeout env parser accepts safe positive integers and falls back otherwise', async () => { const testApi = await getStreamIdleTestApi('api-timeout-env-parser') @@ -2232,6 +2187,7 @@ test('API timeout env parser accepts safe positive integers and falls back other } }) +// openaiShim test extraction seam 025 start: Anthropic-compatible passthrough stream rejects with idle timeout when it stalls test('Anthropic-compatible passthrough stream rejects with idle timeout when it stalls', async () => { process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS = '25' const stalled = makeStallingResponse( @@ -2277,9 +2233,12 @@ test('Anthropic-compatible passthrough stream rejects with idle timeout when it expect((caught as Error).name).toBe('StreamIdleTimeoutError') expect((stalled.cancelReasons[0] as Error).name).toBe('StreamIdleTimeoutError') }) +// openaiShim test extraction seam 025 end + // Extraction seam: shared stream control | Gemini stream conversion. +// openaiShim test extraction seam 026 start: Gemini SSE stream rejects with idle timeout when it stalls test('Gemini SSE stream rejects with idle timeout when it stalls', async () => { process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS = '25' const stalled = makeStallingResponse( @@ -2322,7 +2281,10 @@ test('Gemini SSE stream rejects with idle timeout when it stalls', async () => { expect((caught as Error).name).toBe('StreamIdleTimeoutError') expect((stalled.cancelReasons[0] as Error).name).toBe('StreamIdleTimeoutError') }) +// openaiShim test extraction seam 026 end + +// openaiShim test extraction seam 027 start: OpenAI-compatible stream rejects with idle timeout when it stalls after a chunk test('OpenAI-compatible stream rejects with idle timeout when it stalls after a chunk', async () => { await getStreamIdleTestApi('stream-idle-openai-stall') process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS = '25' @@ -2367,7 +2329,10 @@ test('OpenAI-compatible stream rejects with idle timeout when it stalls after a }) expect(textDeltas).toEqual(['partial']) }) +// openaiShim test extraction seam 027 end + +// openaiShim test extraction seam 028 start: OpenAI-compatible stream keeps slow active chunks alive under the idle timeout test('OpenAI-compatible stream keeps slow active chunks alive under the idle timeout', async () => { await getStreamIdleTestApi('stream-idle-openai-active') process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS = '500' @@ -2469,7 +2434,10 @@ test('OpenAI-compatible stream keeps slow active chunks alive under the idle tim expect(Date.now() - startedAt).toBeGreaterThan(500) expect(textDeltas.join('')).toBe('hello') }) +// openaiShim test extraction seam 028 end + +// openaiShim test extraction seam 029 start: controller abort reaches generic OpenAI SSE converter test('controller abort reaches generic OpenAI SSE converter', async () => { const stalled = makeStallingResponse( makeOpenAIStreamFrame({ role: 'assistant', content: 'partial' }), @@ -2502,7 +2470,10 @@ test('controller abort reaches generic OpenAI SSE converter', async () => { stalled.close() } }) +// openaiShim test extraction seam 029 end + +// openaiShim test extraction seam 030 start: controller abort cancels generic OpenAI SSE before iteration starts test('controller abort cancels generic OpenAI SSE before iteration starts', async () => { const stalled = makeStallingResponse( makeOpenAIStreamFrame({ role: 'assistant', content: 'partial' }), @@ -2539,7 +2510,10 @@ test('controller abort cancels generic OpenAI SSE before iteration starts', asyn stalled.close() } }) +// openaiShim test extraction seam 030 end + +// openaiShim test extraction seam 031 start: controller abort cancels generic OpenAI SSE when paused after message_start test('controller abort cancels generic OpenAI SSE when paused after message_start', async () => { const stalled = makeStallingResponse( makeOpenAIStreamFrame({ role: 'assistant', content: 'partial' }), @@ -2568,7 +2542,10 @@ test('controller abort cancels generic OpenAI SSE when paused after message_star stalled.close() } }) +// openaiShim test extraction seam 031 end + +// openaiShim test extraction seam 032 start: controller abort stops buffered generic OpenAI SSE events test('controller abort stops buffered generic OpenAI SSE events', async () => { const stalled = makeStallingResponse( makeOpenAIStreamFrame({ role: 'assistant', content: 'first' }) + @@ -2599,7 +2576,10 @@ test('controller abort stops buffered generic OpenAI SSE events', async () => { stalled.close() } }) +// openaiShim test extraction seam 032 end + +// openaiShim test extraction seam 033 start: controller abort reaches Anthropic messages SSE passthrough test('controller abort reaches Anthropic messages SSE passthrough', async () => { const stalled = makeStallingResponse( `data: ${JSON.stringify({ @@ -2645,7 +2625,10 @@ test('controller abort reaches Anthropic messages SSE passthrough', async () => stalled.close() } }) +// openaiShim test extraction seam 033 end + +// openaiShim test extraction seam 034 start: controller abort cancels Anthropic messages SSE when paused after event test('controller abort cancels Anthropic messages SSE when paused after event', async () => { const stalled = makeStallingResponse( `data: ${JSON.stringify({ @@ -2687,7 +2670,10 @@ test('controller abort cancels Anthropic messages SSE when paused after event', stalled.close() } }) +// openaiShim test extraction seam 034 end + +// openaiShim test extraction seam 035 start: controller abort stops buffered Anthropic messages SSE events test('controller abort stops buffered Anthropic messages SSE events', async () => { const stalled = makeStallingResponse( [ @@ -2759,7 +2745,10 @@ test('controller abort stops buffered Anthropic messages SSE events', async () = stalled.close() } }) +// openaiShim test extraction seam 035 end + +// openaiShim test extraction seam 036 start: parent signal abort still reaches OpenAI SSE converter test('parent signal abort still reaches OpenAI SSE converter', async () => { const stalled = makeStallingResponse( makeOpenAIStreamFrame({ role: 'assistant', content: 'partial' }), @@ -2796,7 +2785,10 @@ test('parent signal abort still reaches OpenAI SSE converter', async () => { stalled.close() } }) +// openaiShim test extraction seam 036 end + +// openaiShim test extraction seam 037 start: parent signal abort cancels OpenAI SSE before iteration starts test('parent signal abort cancels OpenAI SSE before iteration starts', async () => { const stalled = makeStallingResponse( makeOpenAIStreamFrame({ role: 'assistant', content: 'partial' }), @@ -2837,7 +2829,10 @@ test('parent signal abort cancels OpenAI SSE before iteration starts', async () stalled.close() } }) +// openaiShim test extraction seam 037 end + +// openaiShim test extraction seam 038 start: controller abort reaches Codex responses stream converter test('controller abort reaches Codex responses stream converter', async () => { const stalled = makeStallingResponse( `event: response.output_text.delta\ndata: ${JSON.stringify({ delta: 'partial' })}\n\n`, @@ -2871,7 +2866,10 @@ test('controller abort reaches Codex responses stream converter', async () => { stalled.close() } }) +// openaiShim test extraction seam 038 end + +// openaiShim test extraction seam 039 start: controller abort cancels Codex responses stream when paused after message_start test('controller abort cancels Codex responses stream when paused after message_start', async () => { const stalled = makeStallingResponse( `event: response.output_text.delta\ndata: ${JSON.stringify({ delta: 'partial' })}\n\n`, @@ -2901,7 +2899,10 @@ test('controller abort cancels Codex responses stream when paused after message_ stalled.close() } }) +// openaiShim test extraction seam 039 end + +// openaiShim test extraction seam 040 start: controller abort reaches Gemini SSE converter test('controller abort reaches Gemini SSE converter', async () => { const stalled = makeStallingResponse( `data: ${JSON.stringify({ @@ -2944,7 +2945,10 @@ test('controller abort reaches Gemini SSE converter', async () => { stalled.close() } }) +// openaiShim test extraction seam 040 end + +// openaiShim test extraction seam 041 start: controller abort stops buffered Gemini SSE events test('controller abort stops buffered Gemini SSE events', async () => { const makeGeminiFrame = (text: string) => `data: ${JSON.stringify({ @@ -2986,9 +2990,12 @@ test('controller abort stops buffered Gemini SSE events', async () => { stalled.close() } }) +// openaiShim test extraction seam 041 end + // Extraction seam: Gemini stream conversion | native Ollama stream adaptation. +// openaiShim test extraction seam 042 start: controller abort reaches native Ollama converted stream test('controller abort reaches native Ollama converted stream', async () => { const previousBaseUrl = process.env.OPENAI_BASE_URL let stalled: StallingResponse | undefined @@ -3033,7 +3040,10 @@ test('controller abort reaches native Ollama converted stream', async () => { restoreEnv('OPENAI_BASE_URL', previousBaseUrl) } }) +// openaiShim test extraction seam 042 end + +// openaiShim test extraction seam 043 start: normal OpenAI SSE stream still completes after controller wiring test('normal OpenAI SSE stream still completes after controller wiring', async () => { globalThis.fetch = (async () => makeSseResponse(makeStreamChunks([ @@ -3084,7 +3094,10 @@ test('normal OpenAI SSE stream still completes after controller wiring', async ( expect(textDeltas.join('')).toBe('complete') expect((result.data as unknown as ShimStream).controller.signal.aborted).toBe(false) }) +// openaiShim test extraction seam 043 end + +// openaiShim test extraction seam 044 start: uses max_tokens instead of max_completion_tokens for local providers test('uses max_tokens instead of max_completion_tokens for local providers', async () => { process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' @@ -3123,6 +3136,7 @@ test('uses max_tokens instead of max_completion_tokens for local providers', asy stream: false, }) }) +// openaiShim test extraction seam 044 end test('does not send stream_options to local OpenAI-compatible servers', async () => { process.env.OPENAI_BASE_URL = 'http://127.0.0.1:8000/v1' @@ -3145,6 +3159,7 @@ test('does not send stream_options to local OpenAI-compatible servers', async () }) }) +// openaiShim test extraction seam 045 start: keeps max_completion_tokens for non-local non-github providers test('keeps max_completion_tokens for non-local non-github providers', async () => { process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' @@ -3189,7 +3204,10 @@ test('keeps max_completion_tokens for non-local non-github providers', async () stream: false, }) }) +// openaiShim test extraction seam 045 end + +// openaiShim test extraction seam 046 start: uses route-specific credential env vars for descriptor-backed openai-compatible routes test('uses route-specific credential env vars for descriptor-backed openai-compatible routes', async () => { let capturedHeaders: Headers | undefined @@ -3239,8 +3257,23 @@ test('uses route-specific credential env vars for descriptor-backed openai-compa expect(capturedHeaders?.get('authorization')).toBe('Bearer or-route-key') }) +// openaiShim test extraction seam 046 end + + +// openaiShim test extraction seam 047 start: preserves Gemini tool call extra_content in follow-up requests + +// openaiShim test extraction seam 047 end + + +// openaiShim test extraction seam 048 start: replays Gemini tool signatures for OpenGateway Gemini models -test('preserves Gemini tool call extra_content in follow-up requests', async () => { +// openaiShim test extraction seam 048 end + + +// openaiShim test extraction seam 049 start: OpenGateway MiMo replays real reasoning_content without adding empty fallback +test('OpenGateway MiMo replays real reasoning_content without adding empty fallback', async () => { + process.env.OPENAI_BASE_URL = 'https://opengateway.gitlawb.com/v1' + process.env.OPENAI_MODEL = 'mimo-v2.5-pro' let requestBody: Record | undefined globalThis.fetch = (async (_input, init) => { @@ -3248,8 +3281,8 @@ test('preserves Gemini tool call extra_content in follow-up requests', async () return new Response( JSON.stringify({ - id: 'chatcmpl-1', - model: 'google/gemini-3.1-pro-preview', + id: 'chatcmpl-opengateway-mimo', + model: 'mimo-v2.5-pro', choices: [ { message: { @@ -3259,11 +3292,6 @@ test('preserves Gemini tool call extra_content in follow-up requests', async () finish_reason: 'stop', }, ], - usage: { - prompt_tokens: 12, - completion_tokens: 4, - total_tokens: 16, - }, }), { headers: { @@ -3276,23 +3304,24 @@ test('preserves Gemini tool call extra_content in follow-up requests', async () const client = createOpenAIShimClient({}) as OpenAIShimClient await client.beta.messages.create({ - model: 'google/gemini-3.1-pro-preview', - system: 'test system', + model: 'mimo-v2.5-pro', messages: [ - { role: 'user', content: 'Use Bash' }, + { role: 'user', content: 'Use an agent' }, { role: 'assistant', content: [ - { type: 'thinking', thinking: 'I should inspect the working tree first.' }, + { + type: 'thinking', + thinking: 'Need to inspect code with an agent.', + }, { type: 'tool_use', - id: 'call_1', - name: 'Bash', - input: { command: 'pwd' }, - extra_content: { - google: { - thought_signature: 'sig-123', - }, + id: 'call_agent_1', + name: 'Agent', + input: { + description: 'Inspect code', + prompt: 'Look at the relevant code', + subagent_type: 'general-purpose', }, }, ], @@ -3302,8 +3331,8 @@ test('preserves Gemini tool call extra_content in follow-up requests', async () content: [ { type: 'tool_result', - tool_use_id: 'call_1', - content: 'D:\\repo', + tool_use_id: 'call_agent_1', + content: 'Agent finished', }, ], }, @@ -3314,25 +3343,23 @@ test('preserves Gemini tool call extra_content in follow-up requests', async () const assistantWithToolCall = (requestBody?.messages as Array>).find( message => Array.isArray(message.tool_calls), - ) as { tool_calls?: Array> } | undefined + ) - expect(assistantWithToolCall?.tool_calls?.[0]).toMatchObject({ - id: 'call_1', - type: 'function', - function: { - name: 'Bash', - arguments: JSON.stringify({ command: 'pwd' }), - }, - extra_content: { - google: { - thought_signature: 'sig-123', - }, - }, - }) + expect(assistantWithToolCall).toBeDefined() + expect(assistantWithToolCall?.reasoning_content).toBe( + 'Need to inspect code with an agent.', + ) + expect(requestBody).not.toHaveProperty('store') }) +// openaiShim test extraction seam 049 end -test('replays Gemini tool signatures for OpenGateway Gemini models', async () => { - process.env.OPENAI_BASE_URL = 'https://opengateway.gitlawb.com/v1' + +// openaiShim test extraction seam 050 start: Xiaomi MiMo replays real reasoning_content without adding empty fallback +test('Xiaomi MiMo replays real reasoning_content without adding empty fallback', async () => { + process.env.OPENAI_BASE_URL = 'https://api.xiaomimimo.com/v1' + process.env.OPENAI_MODEL = 'mimo-v2.5-pro' + process.env.MIMO_API_KEY = 'mimo-test-key' + delete process.env.OPENAI_API_KEY let requestBody: Record | undefined globalThis.fetch = (async (_input, init) => { @@ -3340,8 +3367,8 @@ test('replays Gemini tool signatures for OpenGateway Gemini models', async () => return new Response( JSON.stringify({ - id: 'chatcmpl-1', - model: 'google/gemini-3.1-flash-lite', + id: 'chatcmpl-mimo', + model: 'mimo-v2.5-pro', choices: [ { message: { @@ -3351,11 +3378,6 @@ test('replays Gemini tool signatures for OpenGateway Gemini models', async () => finish_reason: 'stop', }, ], - usage: { - prompt_tokens: 12, - completion_tokens: 4, - total_tokens: 16, - }, }), { headers: { @@ -3368,18 +3390,25 @@ test('replays Gemini tool signatures for OpenGateway Gemini models', async () => const client = createOpenAIShimClient({}) as OpenAIShimClient await client.beta.messages.create({ - model: 'google/gemini-3.1-flash-lite', + model: 'mimo-v2.5-pro', messages: [ - { role: 'user', content: 'Use Write' }, + { role: 'user', content: 'Use an agent' }, { role: 'assistant', content: [ + { + type: 'thinking', + thinking: 'Need to inspect code with an agent.', + }, { type: 'tool_use', - id: 'call_1', - name: 'Write', - input: { file_path: 'todo.md', content: 'todo' }, - signature: 'sig-opengateway', + id: 'call_agent_1', + name: 'Agent', + input: { + description: 'Inspect code', + prompt: 'Look at the relevant code', + subagent_type: 'general-purpose', + }, }, ], }, @@ -3388,8 +3417,8 @@ test('replays Gemini tool signatures for OpenGateway Gemini models', async () => content: [ { type: 'tool_result', - tool_use_id: 'call_1', - content: 'created', + tool_use_id: 'call_agent_1', + content: 'Agent finished', }, ], }, @@ -3400,19 +3429,19 @@ test('replays Gemini tool signatures for OpenGateway Gemini models', async () => const assistantWithToolCall = (requestBody?.messages as Array>).find( message => Array.isArray(message.tool_calls), - ) as { tool_calls?: Array> } | undefined + ) - expect(assistantWithToolCall?.tool_calls?.[0]).toMatchObject({ - id: 'call_1', - extra_content: { - google: { - thought_signature: 'sig-opengateway', - }, - }, - }) + expect(assistantWithToolCall).toBeDefined() + expect(assistantWithToolCall?.reasoning_content).toBe( + 'Need to inspect code with an agent.', + ) + expect(requestBody).not.toHaveProperty('store') }) +// openaiShim test extraction seam 050 end -test('OpenGateway MiMo replays real reasoning_content without adding empty fallback', async () => { + +// openaiShim test extraction seam 051 start: OpenGateway MiMo does not synthesize empty reasoning_content when missing +test('OpenGateway MiMo does not synthesize empty reasoning_content when missing', async () => { process.env.OPENAI_BASE_URL = 'https://opengateway.gitlawb.com/v1' process.env.OPENAI_MODEL = 'mimo-v2.5-pro' let requestBody: Record | undefined @@ -3451,10 +3480,6 @@ test('OpenGateway MiMo replays real reasoning_content without adding empty fallb { role: 'assistant', content: [ - { - type: 'thinking', - thinking: 'Need to inspect code with an agent.', - }, { type: 'tool_use', id: 'call_agent_1', @@ -3487,171 +3512,14 @@ test('OpenGateway MiMo replays real reasoning_content without adding empty fallb ) expect(assistantWithToolCall).toBeDefined() - expect(assistantWithToolCall?.reasoning_content).toBe( - 'Need to inspect code with an agent.', - ) + expect(assistantWithToolCall).not.toHaveProperty('reasoning_content') expect(requestBody).not.toHaveProperty('store') }) +// openaiShim test extraction seam 051 end -test('Xiaomi MiMo replays real reasoning_content without adding empty fallback', async () => { - process.env.OPENAI_BASE_URL = 'https://api.xiaomimimo.com/v1' - process.env.OPENAI_MODEL = 'mimo-v2.5-pro' - process.env.MIMO_API_KEY = 'mimo-test-key' - delete process.env.OPENAI_API_KEY - let requestBody: Record | undefined - - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - - return new Response( - JSON.stringify({ - id: 'chatcmpl-mimo', - model: 'mimo-v2.5-pro', - choices: [ - { - message: { - role: 'assistant', - content: 'done', - }, - finish_reason: 'stop', - }, - ], - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - await client.beta.messages.create({ - model: 'mimo-v2.5-pro', - messages: [ - { role: 'user', content: 'Use an agent' }, - { - role: 'assistant', - content: [ - { - type: 'thinking', - thinking: 'Need to inspect code with an agent.', - }, - { - type: 'tool_use', - id: 'call_agent_1', - name: 'Agent', - input: { - description: 'Inspect code', - prompt: 'Look at the relevant code', - subagent_type: 'general-purpose', - }, - }, - ], - }, - { - role: 'user', - content: [ - { - type: 'tool_result', - tool_use_id: 'call_agent_1', - content: 'Agent finished', - }, - ], - }, - ], - max_tokens: 64, - stream: false, - }) - - const assistantWithToolCall = (requestBody?.messages as Array>).find( - message => Array.isArray(message.tool_calls), - ) - - expect(assistantWithToolCall).toBeDefined() - expect(assistantWithToolCall?.reasoning_content).toBe( - 'Need to inspect code with an agent.', - ) - expect(requestBody).not.toHaveProperty('store') -}) - -test('OpenGateway MiMo does not synthesize empty reasoning_content when missing', async () => { - process.env.OPENAI_BASE_URL = 'https://opengateway.gitlawb.com/v1' - process.env.OPENAI_MODEL = 'mimo-v2.5-pro' - let requestBody: Record | undefined - - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - - return new Response( - JSON.stringify({ - id: 'chatcmpl-opengateway-mimo', - model: 'mimo-v2.5-pro', - choices: [ - { - message: { - role: 'assistant', - content: 'done', - }, - finish_reason: 'stop', - }, - ], - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - await client.beta.messages.create({ - model: 'mimo-v2.5-pro', - messages: [ - { role: 'user', content: 'Use an agent' }, - { - role: 'assistant', - content: [ - { - type: 'tool_use', - id: 'call_agent_1', - name: 'Agent', - input: { - description: 'Inspect code', - prompt: 'Look at the relevant code', - subagent_type: 'general-purpose', - }, - }, - ], - }, - { - role: 'user', - content: [ - { - type: 'tool_result', - tool_use_id: 'call_agent_1', - content: 'Agent finished', - }, - ], - }, - ], - max_tokens: 64, - stream: false, - }) - - const assistantWithToolCall = (requestBody?.messages as Array>).find( - message => Array.isArray(message.tool_calls), - ) - - expect(assistantWithToolCall).toBeDefined() - expect(assistantWithToolCall).not.toHaveProperty('reasoning_content') - expect(requestBody).not.toHaveProperty('store') -}) - -test('strips unsupported stream_options for Xiaomi MiMo streams', async () => { + +// openaiShim test extraction seam 052 start: strips unsupported stream_options for Xiaomi MiMo streams +test('strips unsupported stream_options for Xiaomi MiMo streams', async () => { process.env.OPENAI_BASE_URL = 'https://api.xiaomimimo.com/v1' process.env.OPENAI_MODEL = 'mimo-v2.5-pro' process.env.MIMO_API_KEY = 'mimo-test-key' @@ -3707,131 +3575,28 @@ test('strips unsupported stream_options for Xiaomi MiMo streams', async () => { expect(requestBody).not.toHaveProperty('stream_options') expect(requestBody).not.toHaveProperty('store') }) +// openaiShim test extraction seam 052 end -test('preserves Grep tool pattern field in OpenAI-compatible schemas', async () => { - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) +// openaiShim test extraction seam 053 start: preserves Grep tool pattern field in OpenAI-compatible schemas - return new Response( - JSON.stringify({ - id: 'chatcmpl-grep-schema', - model: 'qwen/qwen3.6-plus', - choices: [ - { - message: { - role: 'assistant', - content: 'done', - }, - finish_reason: 'stop', - }, - ], - usage: { - prompt_tokens: 12, - completion_tokens: 4, - total_tokens: 16, - }, - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType +// openaiShim test extraction seam 053 end - const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'qwen/qwen3.6-plus', - system: 'test system', - messages: [{ role: 'user', content: 'Use Grep' }], - tools: [ - { - name: 'Grep', - description: 'Search file contents', - input_schema: { - type: 'object', - properties: { - pattern: { type: 'string', description: 'Search pattern' }, - path: { type: 'string' }, - }, - required: ['pattern'], - additionalProperties: false, - }, - }, - ], - max_tokens: 64, - stream: false, - }) - - const tools = requestBody?.tools as Array> | undefined - const grepTool = tools?.find(tool => (tool.function as Record)?.name === 'Grep') as - | { function?: { parameters?: { properties?: Record; required?: string[] } } } - | undefined - - expect(Object.keys(grepTool?.function?.parameters?.properties ?? {})).toContain('pattern') - expect(grepTool?.function?.parameters?.required).toContain('pattern') -}) - -test('does not infer Gemini mode from OPENAI_BASE_URL path substrings', async () => { - let capturedAuthorization: string | null = null - - process.env.OPENAI_BASE_URL = - 'https://evil.example/generativelanguage.googleapis.com/v1beta/openai' - delete process.env.OPENAI_API_KEY - process.env.GEMINI_API_KEY = 'gemini-secret' +// openaiShim test extraction seam 054 start: does not infer Gemini mode from OPENAI_BASE_URL path substrings - globalThis.fetch = (async (_input, init) => { - const headers = init?.headers as Record | undefined - capturedAuthorization = - headers?.Authorization ?? headers?.authorization ?? null - - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'fake-model', - choices: [ - { - message: { - role: 'assistant', - content: 'ok', - }, - finish_reason: 'stop', - }, - ], - usage: { - prompt_tokens: 12, - completion_tokens: 4, - total_tokens: 16, - }, - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - await client.beta.messages.create({ - model: 'fake-model', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }) +// openaiShim test extraction seam 054 end - expect(capturedAuthorization).toBeNull() -}) +// openaiShim test extraction seam 055 start: the OpenAI shim façade exposes the beta.messages namespace test('the OpenAI shim façade exposes the beta.messages namespace', () => { const client = createOpenAIShimClient({}) as OpenAIShimClient expect(client.beta.messages).toBeDefined() }) +// openaiShim test extraction seam 055 end + +// openaiShim test extraction seam 056 start: preserves image tool results as placeholders in follow-up requests test('preserves image tool results as placeholders in follow-up requests', async () => { let requestBody: Record | undefined @@ -3934,7 +3699,10 @@ test('preserves image tool results as placeholders in follow-up requests', async }, ]) }) +// openaiShim test extraction seam 056 end + +// openaiShim test extraction seam 057 start: adds text part for image-only user messages test('adds text part for image-only user messages', async () => { let requestBody: Record | undefined @@ -4010,7 +3778,10 @@ test('adds text part for image-only user messages', async () => { }, ]) }) +// openaiShim test extraction seam 057 end + +// openaiShim test extraction seam 058 start: preserves mixed text and image tool results as multipart content test('preserves mixed text and image tool results as multipart content', async () => { let requestBody: Record | undefined @@ -4105,7 +3876,10 @@ test('preserves mixed text and image tool results as multipart content', async ( image_url: { url: 'data:image/png;base64,ZmFrZQ==' }, }) }) +// openaiShim test extraction seam 058 end + +// openaiShim test extraction seam 059 start: uses GEMINI_ACCESS_TOKEN for Gemini OpenAI-compatible requests test('uses GEMINI_ACCESS_TOKEN for Gemini OpenAI-compatible requests', async () => { let capturedAuthorization: string | null = null let capturedProject: string | null = null @@ -4177,7 +3951,10 @@ test('uses GEMINI_ACCESS_TOKEN for Gemini OpenAI-compatible requests', async () expect(capturedAuthorization).toBe('Bearer gemini-access-token') expect(capturedProject).toBe('gemini-project') }) +// openaiShim test extraction seam 059 end + +// openaiShim test extraction seam 060 start: uses NVIDIA_API_KEY for NVIDIA NIM requests without OPENAI_API_KEY test('uses NVIDIA_API_KEY for NVIDIA NIM requests without OPENAI_API_KEY', async () => { let capturedAuthorization: string | null = null @@ -4231,7 +4008,10 @@ test('uses NVIDIA_API_KEY for NVIDIA NIM requests without OPENAI_API_KEY', async expect(capturedAuthorization).toBe('Bearer nvidia-live-key') }) +// openaiShim test extraction seam 060 end + +// openaiShim test extraction seam 061 start: does not use stale NVIDIA_API_KEY for non-NVIDIA OpenAI-compatible routes test('does not use stale NVIDIA_API_KEY for non-NVIDIA OpenAI-compatible routes', async () => { let capturedAuthorization: string | null = null @@ -4281,7 +4061,10 @@ test('does not use stale NVIDIA_API_KEY for non-NVIDIA OpenAI-compatible routes' expect(capturedAuthorization).toBeNull() }) +// openaiShim test extraction seam 061 end + +// openaiShim test extraction seam 062 start: does not use MINIMAX_API_KEY for non-MiniMax OpenAI-compatible routes test('does not use MINIMAX_API_KEY for non-MiniMax OpenAI-compatible routes', async () => { let capturedAuthorization: string | null = null @@ -4330,7 +4113,10 @@ test('does not use MINIMAX_API_KEY for non-MiniMax OpenAI-compatible routes', as expect(capturedAuthorization).toBeNull() }) +// openaiShim test extraction seam 062 end + +// openaiShim test extraction seam 063 start: xiaomi mimo route uses api-key auth header and max_completion_tokens test('xiaomi mimo route uses api-key auth header and max_completion_tokens', async () => { let capturedHeaders: Record | undefined let capturedBody: Record | undefined @@ -4381,6 +4167,9 @@ test('xiaomi mimo route uses api-key auth header and max_completion_tokens', asy expect(capturedBody).toMatchObject({ max_completion_tokens: 32 }) expect(capturedBody).not.toHaveProperty('max_tokens') }) +// openaiShim test extraction seam 063 end + +// openaiShim test extraction seam 064 start: xiaomi mimo token plan uses raw api-key and OpenAI-compatible reasoning_effort test('xiaomi mimo token plan uses raw api-key and OpenAI-compatible reasoning_effort', async () => { let capturedHeaders: Record | undefined let capturedBody: Record | undefined @@ -4419,6 +4208,8 @@ test('xiaomi mimo token plan uses raw api-key and OpenAI-compatible reasoning_ef expect(capturedBody).not.toHaveProperty('store') expect(capturedBody).not.toHaveProperty('stream_options') }) +// openaiShim test extraction seam 064 end + test.each([ 'minimax-m3', @@ -4501,6 +4292,7 @@ test.each([ expect(capturedBody).not.toHaveProperty('store') }) +// openaiShim test extraction seam 065 start: opencode go messages endpoint rotates raw x-api-key credentials after rate-limit failure test('opencode go messages endpoint rotates raw x-api-key credentials after rate-limit failure', async () => { const capturedUrls: string[] = [] const capturedKeys: Array = [] @@ -4561,7 +4353,10 @@ test('opencode go messages endpoint rotates raw x-api-key credentials after rate ]) expect(capturedKeys).toEqual(['fake-opencode-a', 'fake-opencode-b']) }) +// openaiShim test extraction seam 065 end + +// openaiShim test extraction seam 066 start: gitlawb opengateway provider flag sends OPENGATEWAY_API_KEY as bearer auth despite stale generic base URL test('gitlawb opengateway provider flag sends OPENGATEWAY_API_KEY as bearer auth despite stale generic base URL', async () => { process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' process.env.OPENAI_MODEL = 'gpt-5.5' @@ -4576,7 +4371,10 @@ test('gitlawb opengateway provider flag sends OPENGATEWAY_API_KEY as bearer auth expect(captured.url).toBe('https://opengateway.gitlawb.com/v1/chat/completions') expect(captured.authorization).toBe('Bearer fake-ogw-key') }) +// openaiShim test extraction seam 066 end + +// openaiShim test extraction seam 067 start: gitlawb opengateway provider flag accepts OPENAI_API_KEY compatibility fallback test('gitlawb opengateway provider flag accepts OPENAI_API_KEY compatibility fallback', async () => { delete process.env.OPENAI_BASE_URL delete process.env.OPENGATEWAY_API_KEY @@ -4589,7 +4387,10 @@ test('gitlawb opengateway provider flag accepts OPENAI_API_KEY compatibility fal expect(captured.authorization).toBe('Bearer fake-openai-fallback') }) +// openaiShim test extraction seam 067 end + +// openaiShim test extraction seam 068 start: gitlawb opengateway provider flag sends OPENAI_API_KEY fallback despite stale generic base URL test('gitlawb opengateway provider flag sends OPENAI_API_KEY fallback despite stale generic base URL', async () => { process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' process.env.OPENAI_API_KEY = 'fake-openai-fallback' @@ -4603,7 +4404,10 @@ test('gitlawb opengateway provider flag sends OPENAI_API_KEY fallback despite st expect(captured.url).toBe('https://opengateway.gitlawb.com/v1/chat/completions') expect(captured.authorization).toBe('Bearer fake-openai-fallback') }) +// openaiShim test extraction seam 068 end + +// openaiShim test extraction seam 069 start: gitlawb opengateway provider flag trims OPENGATEWAY_API_KEY before bearer auth test('gitlawb opengateway provider flag trims OPENGATEWAY_API_KEY before bearer auth', async () => { process.env.OPENGATEWAY_API_KEY = ' fake-ogw-key ' delete process.env.OPENAI_API_KEY @@ -4615,7 +4419,10 @@ test('gitlawb opengateway provider flag trims OPENGATEWAY_API_KEY before bearer expect(captured.authorization).toBe('Bearer fake-ogw-key') }) +// openaiShim test extraction seam 069 end + +// openaiShim test extraction seam 070 start: gitlawb opengateway provider flag ignores blank OPENGATEWAY_API_KEY and uses OPENAI_API_KEY fallback test('gitlawb opengateway provider flag ignores blank OPENGATEWAY_API_KEY and uses OPENAI_API_KEY fallback', async () => { process.env.OPENGATEWAY_API_KEY = ' ' process.env.OPENAI_API_KEY = 'fake-openai-fallback' @@ -4627,7 +4434,10 @@ test('gitlawb opengateway provider flag ignores blank OPENGATEWAY_API_KEY and us expect(captured.authorization).toBe('Bearer fake-openai-fallback') }) +// openaiShim test extraction seam 070 end + +// openaiShim test extraction seam 071 start: gitlawb opengateway provider flag sends OPENGATEWAY_API_KEY to OPENGATEWAY_BASE_URL override test('gitlawb opengateway provider flag sends OPENGATEWAY_API_KEY to OPENGATEWAY_BASE_URL override', async () => { process.env.OPENGATEWAY_BASE_URL = 'http://localhost:8181/v1' process.env.OPENGATEWAY_API_KEY = 'fake-ogw-key' @@ -4641,7 +4451,10 @@ test('gitlawb opengateway provider flag sends OPENGATEWAY_API_KEY to OPENGATEWAY expect(captured.url).toBe('http://localhost:8181/v1/chat/completions') expect(captured.authorization).toBe('Bearer fake-ogw-key') }) +// openaiShim test extraction seam 071 end + +// openaiShim test extraction seam 072 start: gitlawb opengateway provider flag sends OPENGATEWAY_API_KEY to custom OPENAI_BASE_URL fallback test('gitlawb opengateway provider flag sends OPENGATEWAY_API_KEY to custom OPENAI_BASE_URL fallback', async () => { process.env.OPENAI_BASE_URL = 'http://localhost:8181/v1' process.env.OPENGATEWAY_API_KEY = 'fake-ogw-key' @@ -4656,7 +4469,10 @@ test('gitlawb opengateway provider flag sends OPENGATEWAY_API_KEY to custom OPEN expect(captured.url).toBe('http://localhost:8181/v1/chat/completions') expect(captured.authorization).toBe('Bearer fake-ogw-key') }) +// openaiShim test extraction seam 072 end + +// openaiShim test extraction seam 073 start: gitlawb opengateway provider flag prefers OPENGATEWAY_API_KEY over generic OPENAI_API_KEY for custom base URL test('gitlawb opengateway provider flag prefers OPENGATEWAY_API_KEY over generic OPENAI_API_KEY for custom base URL', async () => { process.env.OPENGATEWAY_BASE_URL = 'http://localhost:8181/v1' process.env.OPENGATEWAY_API_KEY = 'fake-ogw-key' @@ -4670,7 +4486,10 @@ test('gitlawb opengateway provider flag prefers OPENGATEWAY_API_KEY over generic expect(captured.url).toBe('http://localhost:8181/v1/chat/completions') expect(captured.authorization).toBe('Bearer fake-ogw-key') }) +// openaiShim test extraction seam 073 end + +// openaiShim test extraction seam 074 start: gitlawb opengateway provider flag prefers OPENGATEWAY_API_KEY over generic OPENAI_API_KEYS pool test('gitlawb opengateway provider flag prefers OPENGATEWAY_API_KEY over generic OPENAI_API_KEYS pool', async () => { process.env.OPENGATEWAY_BASE_URL = 'http://localhost:8181/v1' process.env.OPENGATEWAY_API_KEY = 'fake-ogw-key' @@ -4685,6 +4504,7 @@ test('gitlawb opengateway provider flag prefers OPENGATEWAY_API_KEY over generic expect(captured.url).toBe('http://localhost:8181/v1/chat/completions') expect(captured.authorization).toBe('Bearer fake-ogw-key') }) +// openaiShim test extraction seam 074 end test('longcat provider flag prefers LONGCAT_API_KEY over generic OPENAI_API_KEYS pool', async () => { process.env.LONGCAT_API_KEY = 'fake-longcat-key' @@ -4877,6 +4697,7 @@ test('dedicated-only ClinePass route never falls back to generic OpenAI credenti expect(captured.authorization).toBeNull() }) +// openaiShim test extraction seam 075 start: gitlawb opengateway provider flag uses generic OPENAI_API_KEYS pool before generic OPENAI_API_KEY fallback test('gitlawb opengateway provider flag uses generic OPENAI_API_KEYS pool before generic OPENAI_API_KEY fallback', async () => { process.env.OPENGATEWAY_BASE_URL = 'http://localhost:8181/v1' process.env.OPENAI_API_KEYS = 'fake-openai-pool-a,fake-openai-pool-b' @@ -4891,7 +4712,10 @@ test('gitlawb opengateway provider flag uses generic OPENAI_API_KEYS pool before expect(captured.url).toBe('http://localhost:8181/v1/chat/completions') expect(captured.authorization).toBe('Bearer fake-openai-pool-a') }) +// openaiShim test extraction seam 075 end + +// openaiShim test extraction seam 076 start: gitlawb opengateway stored provider profile key becomes bearer auth test('gitlawb opengateway stored provider profile key becomes bearer auth', async () => { delete process.env.OPENAI_API_KEY delete process.env.OPENGATEWAY_API_KEY @@ -4909,7 +4733,10 @@ test('gitlawb opengateway stored provider profile key becomes bearer auth', asyn expect(captured.authorization).toBe('Bearer fake-profile-key') }) +// openaiShim test extraction seam 076 end + +// openaiShim test extraction seam 077 start: openai route still sends OPENAI_API_KEY as bearer auth test('openai route still sends OPENAI_API_KEY as bearer auth', async () => { process.env.CLAUDE_CODE_USE_OPENAI = '1' process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' @@ -4921,7 +4748,10 @@ test('openai route still sends OPENAI_API_KEY as bearer auth', async () => { expect(captured.authorization).toBe('Bearer fake-openai-key') }) +// openaiShim test extraction seam 077 end + +// openaiShim test extraction seam 078 start: OPENAI_API_KEYS rejects placeholder values before sending requests test('OPENAI_API_KEYS rejects placeholder values before sending requests', async () => { const authorizations: Array = [] @@ -4949,6 +4779,9 @@ test('OPENAI_API_KEYS rejects placeholder values before sending requests', async expect(authorizations).toEqual([]) }) +// openaiShim test extraction seam 078 end + +// openaiShim test extraction seam 079 start: OPENAI_API_KEYS rotates to the next key on rate-limit failure test('OPENAI_API_KEYS rotates to the next key on rate-limit failure', async () => { const authorizations: Array = [] @@ -4982,7 +4815,10 @@ test('OPENAI_API_KEYS rotates to the next key on rate-limit failure', async () = expect(authorizations).toEqual(['Bearer key-a', 'Bearer key-b']) }) +// openaiShim test extraction seam 079 end + +// openaiShim test extraction seam 080 start: OPENAI_API_KEYS does not reuse a cooled-down key after every key is rate-limited test('OPENAI_API_KEYS does not reuse a cooled-down key after every key is rate-limited', async () => { const authorizations: Array = [] @@ -5013,7 +4849,10 @@ test('OPENAI_API_KEYS does not reuse a cooled-down key after every key is rate-l expect(authorizations).toEqual(['Bearer key-a', 'Bearer key-b']) }) +// openaiShim test extraction seam 080 end + +// openaiShim test extraction seam 081 start: comma-separated OPENAI_API_KEY rotates to the next key on rate-limit failure test('comma-separated OPENAI_API_KEY rotates to the next key on rate-limit failure', async () => { const authorizations: Array = [] @@ -5047,7 +4886,10 @@ test('comma-separated OPENAI_API_KEY rotates to the next key on rate-limit failu expect(authorizations).toEqual(['Bearer key-a', 'Bearer key-b']) }) +// openaiShim test extraction seam 081 end + +// openaiShim test extraction seam 082 start: OPENAI_API_KEYS does not rotate through pool on provider 5xx outage test('OPENAI_API_KEYS does not rotate through pool on provider 5xx outage', async () => { const authorizations: Array = [] @@ -5079,6 +4921,9 @@ test('OPENAI_API_KEYS does not rotate through pool on provider 5xx outage', asyn expect(authorizations).toEqual(['Bearer key-a']) }) +// openaiShim test extraction seam 082 end + +// openaiShim test extraction seam 083 start: OPENAI_API_KEYS preserves cooldown state across client requests test('OPENAI_API_KEYS preserves cooldown state across client requests', async () => { const authorizations: Array = [] @@ -5117,7 +4962,10 @@ test('OPENAI_API_KEYS preserves cooldown state across client requests', async () 'Bearer key-b', ]) }) +// openaiShim test extraction seam 083 end + +// openaiShim test extraction seam 084 start: OPENAI_API_KEYS rotates Azure api-key auth on auth failure test('OPENAI_API_KEYS rotates Azure api-key auth on auth failure', async () => { const apiKeys: Array = [] @@ -5150,7 +4998,10 @@ test('OPENAI_API_KEYS rotates Azure api-key auth on auth failure', async () => { expect(apiKeys).toEqual(['azure-key-a', 'azure-key-b']) }) +// openaiShim test extraction seam 084 end + +// openaiShim test extraction seam 085 start: OPENAI_API_KEYS does not reuse auth-disabled credentials across client requests test('OPENAI_API_KEYS does not reuse auth-disabled credentials across client requests', async () => { const authorizations: Array = [] @@ -5191,7 +5042,10 @@ test('OPENAI_API_KEYS does not reuse auth-disabled credentials across client req expect(authorizations).toEqual(['Bearer key-a', 'Bearer key-b']) }) +// openaiShim test extraction seam 085 end + +// openaiShim test extraction seam 086 start: OPENAI_API_KEYS permanently evicts 403 auth failures test('OPENAI_API_KEYS permanently evicts 403 auth failures', async () => { const authorizations: Array = [] @@ -5232,6 +5086,9 @@ test('OPENAI_API_KEYS permanently evicts 403 auth failures', async () => { expect(authorizations).toEqual(['Bearer key-a', 'Bearer key-b']) }) +// openaiShim test extraction seam 086 end + +// openaiShim test extraction seam 087 start: does not use BNKR_API_KEY for non-Bankr OpenAI-compatible routes test('does not use BNKR_API_KEY for non-Bankr OpenAI-compatible routes', async () => { let capturedAuthorization: string | null = null @@ -5280,250 +5137,27 @@ test('does not use BNKR_API_KEY for non-Bankr OpenAI-compatible routes', async ( expect(capturedAuthorization).toBeNull() }) +// openaiShim test extraction seam 087 end -test('preserves Gemini tool call extra_content from streaming chunks', async () => { - globalThis.fetch = (async (_input, _init) => { - const chunks = makeStreamChunks([ - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'google/gemini-3.1-pro-preview', - choices: [ - { - index: 0, - delta: { - role: 'assistant', - tool_calls: [ - { - index: 0, - id: 'function-call-1', - type: 'function', - extra_content: { - google: { - thought_signature: 'sig-stream', - }, - }, - function: { - name: 'Bash', - arguments: '{"command":"pwd"}', - }, - }, - ], - }, - finish_reason: null, - }, - ], - }, - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'google/gemini-3.1-pro-preview', - choices: [ - { - index: 0, - delta: {}, - finish_reason: 'tool_calls', - }, - ], - }, - ]) - - return makeSseResponse(chunks) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - const result = await client.beta.messages - .create({ - model: 'google/gemini-3.1-pro-preview', - system: 'test system', - messages: [{ role: 'user', content: 'Use Bash' }], - max_tokens: 64, - stream: true, - }) - .withResponse() - const events: Array> = [] - for await (const event of result.data) { - events.push(event) - } +// openaiShim test extraction seam 088 start: preserves Gemini tool call extra_content from streaming chunks - const toolStart = events.find( - event => - event.type === 'content_block_start' && - typeof event.content_block === 'object' && - event.content_block !== null && - (event.content_block as Record).type === 'tool_use', - ) as { content_block?: Record } | undefined +// openaiShim test extraction seam 088 end - expect(toolStart?.content_block).toMatchObject({ - type: 'tool_use', - id: 'function-call-1', - name: 'Bash', - extra_content: { - google: { - thought_signature: 'sig-stream', - }, - }, - }) -}) -test('preserves Gemini thought signature from streaming delta extra_content', async () => { - globalThis.fetch = (async (_input, _init) => { - const chunks = makeStreamChunks([ - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'google/gemini-3.1-flash-lite', - choices: [ - { - index: 0, - delta: { - role: 'assistant', - extra_content: { - google: { - thought_signature: 'sig-delta', - }, - }, - tool_calls: [ - { - index: 0, - id: 'function-call-1', - type: 'function', - function: { - name: 'Write', - arguments: '{"file_path":"todo.md","content":"todo"}', - }, - }, - ], - }, - finish_reason: null, - }, - ], - }, - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'google/gemini-3.1-flash-lite', - choices: [ - { - index: 0, - delta: {}, - finish_reason: 'tool_calls', - }, - ], - }, - ]) - - return makeSseResponse(chunks) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - const result = await client.beta.messages - .create({ - model: 'google/gemini-3.1-flash-lite', - messages: [{ role: 'user', content: 'Use Write' }], - max_tokens: 64, - stream: true, - }) - .withResponse() - - const events: Array> = [] - for await (const event of result.data) { - events.push(event) - } - - const toolStart = events.find( - event => - event.type === 'content_block_start' && - typeof event.content_block === 'object' && - event.content_block !== null && - (event.content_block as Record).type === 'tool_use', - ) as { content_block?: Record } | undefined - - expect(toolStart?.content_block).toMatchObject({ - type: 'tool_use', - id: 'function-call-1', - name: 'Write', - extra_content: { - google: { - thought_signature: 'sig-delta', - }, - }, - signature: 'sig-delta', - }) -}) - -test('preserves Gemini thought signature from non-streaming message extra_content', async () => { - globalThis.fetch = (async (_input, _init) => { - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'google/gemini-3.1-flash-lite', - choices: [ - { - message: { - role: 'assistant', - extra_content: { - google: { - thought_signature: 'sig-message', - }, - }, - tool_calls: [ - { - id: 'function-call-1', - type: 'function', - function: { - name: 'Write', - arguments: '{"file_path":"todo.md","content":"todo"}', - }, - }, - ], - }, - finish_reason: 'tool_calls', - }, - ], - usage: { - prompt_tokens: 12, - completion_tokens: 4, - total_tokens: 16, - }, - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - const message = await client.beta.messages.create({ - model: 'google/gemini-3.1-flash-lite', - messages: [{ role: 'user', content: 'Use Write' }], - max_tokens: 64, - stream: false, - }) as { - content?: Array> - } - - expect(message.content?.[0]).toMatchObject({ - type: 'tool_use', - id: 'function-call-1', - name: 'Write', - extra_content: { - google: { - thought_signature: 'sig-message', - }, - }, - signature: 'sig-message', - }) -}) +// openaiShim test extraction seam 089 start: preserves Gemini thought signature from streaming delta extra_content + +// openaiShim test extraction seam 089 end + + +// openaiShim test extraction seam 090 start: preserves Gemini thought signature from non-streaming message extra_content + +// openaiShim test extraction seam 090 end + // Extraction seam: provider signature metadata | raw streaming tool fallback. +// openaiShim test extraction seam 091 start: converts Gemini raw tool-call text into streaming tool_use blocks test('converts Gemini raw tool-call text into streaming tool_use blocks', async () => { globalThis.fetch = (async (_input, _init) => { const chunks = makeStreamChunks([ @@ -5630,9 +5264,12 @@ test('converts Gemini raw tool-call text into streaming tool_use blocks', async | undefined expect(stop?.delta?.stop_reason).toBe('tool_use') }) +// openaiShim test extraction seam 091 end + // Extraction seam: streaming conversion | non-streaming response conversion. +// openaiShim test extraction seam 092 start: converts Gemini raw tool-call text into non-streaming tool_use blocks test('converts Gemini raw tool-call text into non-streaming tool_use blocks', async () => { globalThis.fetch = (async (_input, _init) => { return new Response( @@ -5689,7 +5326,10 @@ test('converts Gemini raw tool-call text into non-streaming tool_use blocks', as }, ]) }) +// openaiShim test extraction seam 092 end + +// openaiShim test extraction seam 093 start: normalizes plain string Bash tool arguments from OpenAI-compatible responses test('normalizes plain string Bash tool arguments from OpenAI-compatible responses', async () => { globalThis.fetch = (async (_input, _init) => { return new Response( @@ -5751,7 +5391,10 @@ test('normalizes plain string Bash tool arguments from OpenAI-compatible respons }, ]) }) +// openaiShim test extraction seam 093 end + +// openaiShim test extraction seam 094 start: normalizes Bash tool arguments that are valid JSON strings test('normalizes Bash tool arguments that are valid JSON strings', async () => { globalThis.fetch = (async (_input, _init) => { return new Response( @@ -5811,6 +5454,8 @@ test('normalizes Bash tool arguments that are valid JSON strings', async () => { }, ]) }) +// openaiShim test extraction seam 094 end + test.each([ ['false', false], @@ -5879,6 +5524,7 @@ test.each([ }, ) +// openaiShim test extraction seam 095 start: keeps terminal empty Bash tool arguments invalid in non-streaming responses test('keeps terminal empty Bash tool arguments invalid in non-streaming responses', async () => { globalThis.fetch = (async (_input, _init) => { return new Response( @@ -5938,9 +5584,12 @@ test('keeps terminal empty Bash tool arguments invalid in non-streaming response }, ]) }) +// openaiShim test extraction seam 095 end + // Extraction seam: completed tool parsing | streamed tool normalization. +// openaiShim test extraction seam 096 start: normalizes plain string Bash tool arguments in streaming responses test('normalizes plain string Bash tool arguments in streaming responses', async () => { globalThis.fetch = (async (_input, _init) => { const chunks = makeStreamChunks([ @@ -6016,7 +5665,10 @@ test('normalizes plain string Bash tool arguments in streaming responses', async expect(normalizedInput).toBe('{"command":"pwd"}') }) +// openaiShim test extraction seam 096 end + +// openaiShim test extraction seam 097 start: normalizes plain string Bash tool arguments when streaming starts with an empty chunk test('normalizes plain string Bash tool arguments when streaming starts with an empty chunk', async () => { globalThis.fetch = (async (_input, _init) => { const chunks = makeStreamChunks([ @@ -6114,7 +5766,10 @@ test('normalizes plain string Bash tool arguments when streaming starts with an expect(normalizedInput).toBe('{"command":"pwd"}') }) +// openaiShim test extraction seam 097 end + +// openaiShim test extraction seam 098 start: normalizes plain string Bash tool arguments when streaming starts with whitespace test('normalizes plain string Bash tool arguments when streaming starts with whitespace', async () => { globalThis.fetch = (async (_input, _init) => { const chunks = makeStreamChunks([ @@ -6212,7 +5867,10 @@ test('normalizes plain string Bash tool arguments when streaming starts with whi expect(normalizedInput).toBe('{"command":" pwd"}') }) +// openaiShim test extraction seam 098 end + +// openaiShim test extraction seam 099 start: keeps terminal whitespace-only Bash arguments invalid in streaming responses test('keeps terminal whitespace-only Bash arguments invalid in streaming responses', async () => { globalThis.fetch = (async (_input, _init) => { const chunks = makeStreamChunks([ @@ -6288,7 +5946,10 @@ test('keeps terminal whitespace-only Bash arguments invalid in streaming respons expect(normalizedInput).toBe('{}') }) +// openaiShim test extraction seam 099 end + +// openaiShim test extraction seam 100 start: normalizes streaming Bash arguments that begin with bracket syntax test('normalizes streaming Bash arguments that begin with bracket syntax', async () => { globalThis.fetch = (async (_input, _init) => { const chunks = makeStreamChunks([ @@ -6364,7 +6025,10 @@ test('normalizes streaming Bash arguments that begin with bracket syntax', async expect(normalizedInput).toBe('{"command":"[ -f package.json ] && pwd"}') }) +// openaiShim test extraction seam 100 end + +// openaiShim test extraction seam 101 start: normalizes streaming Bash arguments when the first chunk is only an opening brace test('normalizes streaming Bash arguments when the first chunk is only an opening brace', async () => { globalThis.fetch = (async (_input, _init) => { const chunks = makeStreamChunks([ @@ -6462,7 +6126,10 @@ test('normalizes streaming Bash arguments when the first chunk is only an openin expect(normalizedInput).toBe('{"command":"{ pwd; }"}') }) +// openaiShim test extraction seam 101 end + +// openaiShim test extraction seam 102 start: repairs truncated structured Bash JSON in streaming responses test('repairs truncated structured Bash JSON in streaming responses', async () => { globalThis.fetch = (async (_input, _init) => { const chunks = makeStreamChunks([ @@ -6538,7 +6205,10 @@ test('repairs truncated structured Bash JSON in streaming responses', async () = expect(normalizedInput).toBe('{"command":"pwd"}') }) +// openaiShim test extraction seam 102 end + +// openaiShim test extraction seam 103 start: does not normalize incomplete streamed Bash commands when finish_reason is length test('does not normalize incomplete streamed Bash commands when finish_reason is length', async () => { globalThis.fetch = (async (_input, _init) => { const chunks = makeStreamChunks([ @@ -6614,7 +6284,10 @@ test('does not normalize incomplete streamed Bash commands when finish_reason is expect(streamedInput).toBe('rg --fi') }) +// openaiShim test extraction seam 103 end + +// openaiShim test extraction seam 104 start: repairs truncated JSON objects even without command field test('repairs truncated JSON objects even without command field', async () => { globalThis.fetch = (async (_input, _init) => { const chunks = makeStreamChunks([ @@ -6690,9 +6363,12 @@ test('repairs truncated JSON objects even without command field', async () => { expect(streamedInput).toBe('{"cwd":"/tmp"}') }) +// openaiShim test extraction seam 104 end + // Extraction seam: streamed tool normalization | schema and tool conversion. +// openaiShim test extraction seam 105 start: preserves raw input for unknown plain string tool arguments test('preserves raw input for unknown plain string tool arguments', async () => { globalThis.fetch = (async (_input, _init) => { return new Response( @@ -6752,7 +6428,10 @@ test('preserves raw input for unknown plain string tool arguments', async () => }, ]) }) +// openaiShim test extraction seam 105 end + +// openaiShim test extraction seam 106 start: preserves parsed string input for unknown JSON string tool arguments test('preserves parsed string input for unknown JSON string tool arguments', async () => { globalThis.fetch = (async (_input, _init) => { return new Response( @@ -6812,9 +6491,12 @@ test('preserves parsed string input for unknown JSON string tool arguments', asy }, ]) }) +// openaiShim test extraction seam 106 end + // Extraction seam: argument parsing | schema sanitation. +// openaiShim test extraction seam 107 start: sanitizes malformed MCP tool schemas before sending them to OpenAI test('sanitizes malformed MCP tool schemas before sending them to OpenAI', async () => { let requestBody: Record | undefined @@ -6889,7 +6571,10 @@ test('sanitizes malformed MCP tool schemas before sending them to OpenAI', async expect(properties?.priority?.enum).toEqual([0, 1, 2, 3]) expect(properties?.priority).not.toHaveProperty('default') }) +// openaiShim test extraction seam 107 end + +// openaiShim test extraction seam 108 start: optional tool properties are not added to required[] — fixes Groq/Azure 400 tool_use_failed test('optional tool properties are not added to required[] — fixes Groq/Azure 400 tool_use_failed', async () => { // Regression test for: all optional properties being sent as required in strict mode, // causing providers like Groq to reject valid tool calls where the model omits optional args. @@ -6946,6 +6631,8 @@ test('optional tool properties are not added to required[] — fixes Groq/Azure expect(required).not.toContain('pages') expect(parameters?.additionalProperties).toBe(false) }) +// openaiShim test extraction seam 108 end + // Extraction seam: schema sanitation | message conversion façade. @@ -6958,10 +6645,13 @@ test('optional tool properties are not added to required[] — fixes Groq/Azure // // --------------------------------------------------------------------------- +// openaiShim test extraction seam 109 start: the OpenAI shim façade exposes the messages.create contract test('the OpenAI shim façade exposes the messages.create contract', () => { const client = createOpenAIShimClient({}) as OpenAIShimClient expect(typeof client.beta.messages.create).toBe('function') }) +// openaiShim test extraction seam 109 end + function makeNonStreamResponse(content = 'ok'): Response { return new Response( @@ -6975,6 +6665,7 @@ function makeNonStreamResponse(content = 'ok'): Response { ) } +// openaiShim test extraction seam 110 start: coalesces consecutive user messages to avoid alternation errors (issue #202) test('coalesces consecutive user messages to avoid alternation errors (issue #202)', async () => { let sentMessages: Array<{ role: string; content: unknown }> | undefined @@ -7003,7 +6694,10 @@ test('coalesces consecutive user messages to avoid alternation errors (issue #20 expect(userContent).toContain('first message') expect(userContent).toContain('second message') }) +// openaiShim test extraction seam 110 end + +// openaiShim test extraction seam 111 start: coalesces consecutive assistant messages preserving tool_calls (issue #202) test('coalesces consecutive assistant messages preserving tool_calls (issue #202)', async () => { let sentMessages: Array<{ role: string; content: unknown; tool_calls?: unknown[] }> | undefined @@ -7034,6 +6728,8 @@ test('coalesces consecutive assistant messages preserving tool_calls (issue #202 expect(assistantMsgs?.length).toBe(1) expect(assistantMsgs?.[0]?.tool_calls?.length).toBeGreaterThan(0) }) +// openaiShim test extraction seam 111 end + // --------------------------------------------------------------------------- // Extraction boundary: message conversion | non-streaming response conversion @@ -7044,6 +6740,7 @@ test('coalesces consecutive assistant messages preserving tool_calls (issue #202 // // --------------------------------------------------------------------------- +// openaiShim test extraction seam 112 start: the OpenAI shim façade creates independent client instances test('the OpenAI shim façade creates independent client instances', () => { const first = createOpenAIShimClient({}) as OpenAIShimClient const second = createOpenAIShimClient({}) as OpenAIShimClient @@ -7051,6 +6748,7 @@ test('the OpenAI shim façade creates independent client instances', () => { expect(first.beta).not.toBe(second.beta) expect(first.beta.messages).not.toBe(second.beta.messages) }) +// openaiShim test extraction seam 112 end test('raw-text and XML fallback tool calls use one unique sequence', () => { const text = parseTextToolCalls('{"name":"from_text","arguments":{}}') @@ -7060,6 +6758,8 @@ test('raw-text and XML fallback tool calls use one unique sequence', () => { expect(text.calls[0]?.id?.replace(/^\D+/, '')).not.toBe(xml.calls[0]?.id?.replace(/^\D+/, '')) }) +// --------------------------------------------------------------------------- +// openaiShim test extraction seam 113 start: non-streaming: reasoning_content emitted as thinking block only when content is null test('non-streaming: reasoning_content emitted as thinking block only when content is null', async () => { globalThis.fetch = (async (_input, _init) => { return new Response( @@ -7104,7 +6804,10 @@ test('non-streaming: reasoning_content emitted as thinking block only when conte { type: 'thinking', thinking: 'Let me think about this step by step.' }, ]) }) +// openaiShim test extraction seam 113 end + +// openaiShim test extraction seam 114 start: non-streaming: empty string content does not fall through to reasoning_content as text test('non-streaming: empty string content does not fall through to reasoning_content as text', async () => { globalThis.fetch = (async (_input, _init) => { return new Response( @@ -7149,7 +6852,10 @@ test('non-streaming: empty string content does not fall through to reasoning_con { type: 'thinking', thinking: 'Chain of thought here.' }, ]) }) +// openaiShim test extraction seam 114 end + +// openaiShim test extraction seam 115 start: non-streaming: real content takes precedence over reasoning_content test('non-streaming: real content takes precedence over reasoning_content', async () => { globalThis.fetch = (async (_input, _init) => { return new Response( @@ -7195,7 +6901,10 @@ test('non-streaming: real content takes precedence over reasoning_content', asyn { type: 'text', text: 'The answer is 42.' }, ]) }) +// openaiShim test extraction seam 115 end + +// openaiShim test extraction seam 116 start: non-streaming: preserves response body when usage parsing fails test('non-streaming: preserves response body when usage parsing fails', async () => { const json = JSON as unknown as { parse: typeof JSON.parse } const originalJSONParse = json.parse @@ -7263,7 +6972,10 @@ test('non-streaming: preserves response body when usage parsing fails', async () json.parse = originalJSONParse } }) +// openaiShim test extraction seam 116 end + +// openaiShim test extraction seam 117 start: non-streaming: preserves response.url routing metadata after body read test('non-streaming: preserves response.url routing metadata after body read', async () => { // _doRequest reads the body for usage extraction and recreates the // Response with new Response(bodyText, ...). That drops response.url to @@ -7311,7 +7023,10 @@ test('non-streaming: preserves response.url routing metadata after body read', a // and content would not match. expect(result.content).toEqual([{ type: 'text', text: 'passthrough ok' }]) }) +// openaiShim test extraction seam 117 end + +// openaiShim test extraction seam 118 start: non-streaming: strips tag block from assistant content test('non-streaming: strips tag block from assistant content', async () => { globalThis.fetch = asMockFetch(mock(async () => { return new Response( @@ -7351,9 +7066,12 @@ test('non-streaming: strips tag block from assistant content', async () { type: 'text', text: 'Hey! How can I help you today?' }, ]) }) +// openaiShim test extraction seam 118 end + // Extraction seam: non-streaming response conversion | streaming event conversion. +// openaiShim test extraction seam 119 start: streaming: thinking block closed before tool call test('streaming: thinking block closed before tool call', async () => { globalThis.fetch = (async (_input, _init) => { const chunks = makeStreamChunks([ @@ -7445,7 +7163,10 @@ test('streaming: thinking block closed before tool call', async () => { } expect(thinkingStart?.content_block?.type).toBe('thinking') }) +// openaiShim test extraction seam 119 end + +// openaiShim test extraction seam 120 start: streaming: strips tag block from assistant content deltas test('streaming: strips tag block from assistant content deltas', async () => { globalThis.fetch = asMockFetch(mock(async () => { const chunks = makeStreamChunks([ @@ -7503,7 +7224,10 @@ test('streaming: strips tag block from assistant content deltas', async expect(textDeltas.join('')).toBe('Hey! How can I help you today?') }) +// openaiShim test extraction seam 120 end + +// openaiShim test extraction seam 121 start: streaming: strips tag split across multiple content chunks test('streaming: strips tag split across multiple content chunks', async () => { globalThis.fetch = asMockFetch(mock(async () => { const chunks = makeStreamChunks([ @@ -7589,7 +7313,10 @@ test('streaming: strips tag split across multiple content chunks', async expect(textDeltas.join('')).toBe('Hey! How can I help you today?') }) +// openaiShim test extraction seam 121 end + +// openaiShim test extraction seam 122 start: streaming: preserves prose without tags (no phrase-based false positive) test('streaming: preserves prose without tags (no phrase-based false positive)', async () => { // Regression: older phrase-based sanitizer would strip "I should..." prose. // The tag-based approach leaves legitimate assistant output alone. @@ -7651,10 +7378,13 @@ test('streaming: preserves prose without tags (no phrase-based false positive)', 'I should note that the user role requires a briefly concise friendly response format.', ) }) +// openaiShim test extraction seam 122 end + // Extraction boundary: response conversion | executor network behavior. // The executor suite owns the contiguous network-classification block below. // Keep this marker stable for independent adjacent test migrations. +// openaiShim test extraction seam 123 start: strips credentials and query params from URL in fetch network error message test('strips credentials and query params from URL in fetch network error message', async () => { process.env.OPENAI_BASE_URL = 'https://user:password@internal.example.test/v1?token=abc123' @@ -7687,6 +7417,7 @@ test('strips credentials and query params from URL in fetch network error messag expect(message).not.toContain('user:') expect(message).not.toContain('token=abc123') }) +// openaiShim test extraction seam 123 end test('redacts configured secret substrings from fetch network error messages', async () => { const secret = 'route/key+AbC123' @@ -7754,6 +7485,7 @@ test('redacts encoded configured secrets from non-URL transport error messages', expect(message).not.toContain(malformedAdjacentSecret) }) +// openaiShim test extraction seam 124 start: classifies localhost transport failures with actionable category marker test('classifies localhost transport failures with actionable category marker', async () => { process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' @@ -7785,7 +7517,10 @@ test('classifies localhost transport failures with actionable category marker', }), ).rejects.toThrow('local server is running') }) +// openaiShim test extraction seam 124 end + +// openaiShim test extraction seam 125 start: transport failures are not labeled with HTTP status 503 test('transport failures are not labeled with HTTP status 503', async () => { // Issue #971: ENETDOWN (and other transport errors) are emitted before any // HTTP response is received. Reporting them as "503" makes users believe the @@ -7823,8 +7558,10 @@ test('transport failures are not labeled with HTTP status 503', async () => { expect(err.message).toContain('code=ENETDOWN') expect(err.message).toContain('openai_category=network_error') }) +// openaiShim test extraction seam 125 end test('propagates caller AbortError without wrapping it as transport failure', async () => { +// openaiShim test extraction seam 126 start: propagates AbortError without wrapping it as transport failure process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' const abortError = new DOMException('The operation was aborted.', 'AbortError') @@ -8357,7 +8094,10 @@ test('disarms the API timeout after headers arrive while the body keeps streamin expect(fetchSignals).toHaveLength(1) expect(fetchSignals[0].aborted).toBe(false) }) +// openaiShim test extraction seam 126 end + +// openaiShim test extraction seam 127 start: classifies chat-completions endpoint 404 failures with endpoint_not_found marker test('classifies chat-completions endpoint 404 failures with endpoint_not_found marker', async () => { process.env.OPENAI_BASE_URL = 'http://localhost:11434' @@ -8380,6 +8120,9 @@ test('classifies chat-completions endpoint 404 failures with endpoint_not_found }), ).rejects.toThrow('openai_category=endpoint_not_found') }) +// openaiShim test extraction seam 127 end + +// openaiShim test extraction seam 128 start: self-heals localhost resolution failures by retrying local loopback base URL test('self-heals localhost resolution failures by retrying local loopback base URL', async () => { process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' @@ -8437,10 +8180,13 @@ test('self-heals localhost resolution failures by retrying local loopback base U expect(requestUrls[0]).toBe('http://localhost:11434/api/chat') expect(requestUrls).toContain('http://127.0.0.1:11434/api/chat') }) +// openaiShim test extraction seam 128 end + // Extraction boundary: executor network behavior | native Ollama routing. // Native Ollama endpoint selection remains an adapter/facade integration concern. // Keep this marker stable for independent adjacent test migrations. +// openaiShim test extraction seam 129 start: uses native Ollama chat endpoint when local base URL omits /v1 test('uses native Ollama chat endpoint when local base URL omits /v1', async () => { process.env.OPENAI_BASE_URL = 'http://localhost:11434' @@ -8483,7 +8229,10 @@ test('uses native Ollama chat endpoint when local base URL omits /v1', async () expect(requestUrls).toEqual(['http://localhost:11434/api/chat']) }) +// openaiShim test extraction seam 129 end + +// openaiShim test extraction seam 130 start: keeps remote Ollama-named gateways on chat completions test('keeps remote Ollama-named gateways on chat completions', async () => { process.env.OPENAI_BASE_URL = 'https://ollama-gateway.example.com/v1' @@ -8513,7 +8262,10 @@ test('keeps remote Ollama-named gateways on chat completions', async () => { 'https://ollama-gateway.example.com/v1/chat/completions', ]) }) +// openaiShim test extraction seam 130 end + +// openaiShim test extraction seam 131 start: keeps HTTPS localhost Ollama-port proxies on chat completions test('keeps HTTPS localhost Ollama-port proxies on chat completions', async () => { process.env.OPENAI_BASE_URL = 'https://localhost:11434/v1' @@ -8543,10 +8295,13 @@ test('keeps HTTPS localhost Ollama-port proxies on chat completions', async () = 'https://localhost:11434/v1/chat/completions', ]) }) +// openaiShim test extraction seam 131 end + // Extraction boundary: native Ollama routing | executor tool self-healing. // The single retry test below moves with request execution. // Keep this marker stable for independent adjacent test migrations. +// openaiShim test extraction seam 132 start: self-heals tool-call incompatibility by retrying local Ollama requests without tools test('self-heals tool-call incompatibility by retrying local Ollama requests without tools', async () => { process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' @@ -8626,10 +8381,13 @@ test('self-heals tool-call incompatibility by retrying local Ollama requests wit expect(requestBodies[1]?.tool_choice).toBeUndefined() expect(requestBodies[1]?.tool_stream).toBeUndefined() }) +// openaiShim test extraction seam 132 end + // Extraction boundary: executor tool self-healing | message conversion. // Message-history normalization below belongs to the message converter. // Keep this marker stable for independent adjacent test migrations. +// openaiShim test extraction seam 133 start: preserves valid tool_result and drops orphan tool_result test('preserves valid tool_result and drops orphan tool_result', async () => { let requestBody: Record | undefined @@ -8725,7 +8483,10 @@ test('preserves valid tool_result and drops orphan tool_result', async () => { const assistantMessages = messages.filter(m => m.role === 'assistant') expect(assistantMessages.some(m => m.content === '[Tool results received]')).toBe(true) }) +// openaiShim test extraction seam 133 end + +// openaiShim test extraction seam 134 start: drops empty assistant message when only thinking block was present and stripped test('drops empty assistant message when only thinking block was present and stripped', async () => { let requestBody: Record | undefined @@ -8762,7 +8523,10 @@ test('drops empty assistant message when only thinking block was present and str expect(String(messages[0].content)).toContain('Initial') expect(String(messages[0].content)).toContain('Interrupting query') }) +// openaiShim test extraction seam 134 end + +// openaiShim test extraction seam 135 start: drops empty assistant message when only redacted_thinking block was present and stripped test('drops empty assistant message when only redacted_thinking block was present and stripped', async () => { let requestBody: Record | undefined @@ -8799,7 +8563,10 @@ test('drops empty assistant message when only redacted_thinking block was presen expect(String(messages[0].content)).toContain('Initial') expect(String(messages[0].content)).toContain('Interrupting query') }) +// openaiShim test extraction seam 135 end + +// openaiShim test extraction seam 136 start: injects semantic assistant message when tool result is followed by user message test('injects semantic assistant message when tool result is followed by user message', async () => { let requestBody: Record | undefined @@ -8847,10 +8614,13 @@ test('injects semantic assistant message when tool result is followed by user me expect(semanticMsg.content).not.toContain('interrupted') expect(semanticMsg.content).not.toContain('user') }) +// openaiShim test extraction seam 136 end + // Extraction boundary: executor tool self-healing | message/provider shaping. // Provider request shaping below is not owned by the executor. // Keep this marker stable for independent adjacent test migrations. +// openaiShim test extraction seam 137 start: Moonshot: uses max_tokens (not max_completion_tokens) and strips store test('Moonshot: uses max_tokens (not max_completion_tokens) and strips store', async () => { process.env.OPENAI_BASE_URL = 'https://api.moonshot.ai/v1' process.env.OPENAI_API_KEY = 'sk-moonshot-test' @@ -8884,7 +8654,10 @@ test('Moonshot: uses max_tokens (not max_completion_tokens) and strips store', a expect(requestBody?.max_completion_tokens).toBeUndefined() expect(requestBody?.store).toBeUndefined() }) +// openaiShim test extraction seam 137 end + +// openaiShim test extraction seam 138 start: Cerebras: strips unsupported store on chat_completions (#1023) test('Cerebras: strips unsupported store on chat_completions (#1023)', async () => { process.env.OPENAI_BASE_URL = 'https://api.cerebras.ai/v1' process.env.OPENAI_API_KEY = 'csk-test' @@ -8916,7 +8689,10 @@ test('Cerebras: strips unsupported store on chat_completions (#1023)', async () expect(requestBody?.store).toBeUndefined() }) +// openaiShim test extraction seam 138 end + +// openaiShim test extraction seam 139 start: Local provider (vLLM/Ollama/etc.): strips unsupported store on chat_completions (#672) test('Local provider (vLLM/Ollama/etc.): strips unsupported store on chat_completions (#672)', async () => { process.env.OPENAI_BASE_URL = 'http://localhost:8000/v1' process.env.OPENAI_API_KEY = 'sk-local' @@ -8948,7 +8724,10 @@ test('Local provider (vLLM/Ollama/etc.): strips unsupported store on chat_comple expect(requestBody?.store).toBeUndefined() }) +// openaiShim test extraction seam 139 end + +// openaiShim test extraction seam 140 start: Mistral: strips unsupported store on chat_completions (#739) test('Mistral: strips unsupported store on chat_completions (#739)', async () => { process.env.OPENAI_BASE_URL = 'https://api.mistral.ai/v1' process.env.OPENAI_API_KEY = 'mistral-test' @@ -8980,7 +8759,10 @@ test('Mistral: strips unsupported store on chat_completions (#739)', async () => expect(requestBody?.store).toBeUndefined() }) +// openaiShim test extraction seam 140 end + +// openaiShim test extraction seam 141 start: Mistral host fallback: strips store on an unresolved Mistral-host route (#739) test('Mistral host fallback: strips store on an unresolved Mistral-host route (#739)', async () => { // `api.mistral.ai/v1` resolves to the Mistral descriptor route, whose // removeBodyFields already strips `store` — so the test above passes even @@ -9025,19 +8807,15 @@ test('Mistral host fallback: strips store on an unresolved Mistral-host route (# expect(requestBody?.max_completion_tokens).toBeUndefined() expect(requestBody?.max_tokens).toBe(64) }) +// openaiShim test extraction seam 141 end -test('hasMistralApiHost matches the Mistral host and its subdomains only', () => { - expect(hasMistralApiHost('https://api.mistral.ai/v1')).toBe(true) - expect(hasMistralApiHost('https://proxy.mistral.ai/v1')).toBe(true) - expect(hasMistralApiHost('https://eu.mistral.ai/v1')).toBe(true) - // Non-Mistral hosts (and look-alikes) must keep `store`. - expect(hasMistralApiHost('https://api.openai.com/v1')).toBe(false) - expect(hasMistralApiHost('https://notmistral.ai/v1')).toBe(false) - expect(hasMistralApiHost('https://api.mistral.ai.evil.com/v1')).toBe(false) - expect(hasMistralApiHost(undefined)).toBe(false) - expect(hasMistralApiHost('not a url')).toBe(false) -}) +// openaiShim test extraction seam 142 start: hasMistralApiHost matches the Mistral host and its subdomains only + +// openaiShim test extraction seam 142 end + + +// openaiShim test extraction seam 143 start: Groq: keeps max_completion_tokens and strips unsupported store test('Groq: keeps max_completion_tokens and strips unsupported store', async () => { process.env.OPENAI_BASE_URL = 'https://api.groq.com/openai/v1' process.env.OPENAI_API_KEY = 'gsk-test' @@ -9071,8 +8849,11 @@ test('Groq: keeps max_completion_tokens and strips unsupported store', async () expect(requestBody?.max_tokens).toBeUndefined() expect(requestBody?.store).toBeUndefined() }) +// openaiShim test extraction seam 143 end + +// openaiShim test extraction seam 144 start: Groq: strips reasoning_effort even when compat inference matches the model test('Groq: strips reasoning_effort even when compat inference matches the model', async () => { process.env.OPENAI_BASE_URL = 'https://api.groq.com/openai/v1' process.env.OPENAI_API_KEY = 'gsk-test' @@ -9107,6 +8888,9 @@ test('Groq: strips reasoning_effort even when compat inference matches the model expect(requestBody?.reasoning_effort).toBeUndefined() expect(requestBody?.store).toBeUndefined() }) +// openaiShim test extraction seam 144 end + +// openaiShim test extraction seam 145 start: Moonshot: echoes reasoning_content on assistant tool-call messages test('Moonshot: echoes reasoning_content on assistant tool-call messages', async () => { // Regression for: "API Error: 400 {"error":{"message":"thinking is enabled // but reasoning_content is missing in assistant tool call message at index @@ -9178,7 +8962,10 @@ test('Moonshot: echoes reasoning_content on assistant tool-call messages', async 'Need to inspect logs via Bash; running a cat.', ) }) +// openaiShim test extraction seam 145 end + +// openaiShim test extraction seam 146 start: DeepSeek echoes reasoning_content on assistant tool-call messages test('DeepSeek echoes reasoning_content on assistant tool-call messages', async () => { process.env.OPENAI_BASE_URL = 'https://api.deepseek.com/v1' process.env.OPENAI_API_KEY = 'sk-deepseek' @@ -9236,7 +9023,10 @@ test('DeepSeek echoes reasoning_content on assistant tool-call messages', async expect(assistantWithToolCall).toBeDefined() expect(assistantWithToolCall?.reasoning_content).toBe('thought') }) +// openaiShim test extraction seam 146 end + +// openaiShim test extraction seam 147 start: generic OpenAI-compatible providers do not echo reasoning_content on assistant tool-call messages test('generic OpenAI-compatible providers do not echo reasoning_content on assistant tool-call messages', async () => { process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' process.env.OPENAI_API_KEY = 'sk-openai-test' @@ -9294,7 +9084,10 @@ test('generic OpenAI-compatible providers do not echo reasoning_content on assis expect(assistantWithToolCall).toBeDefined() expect(assistantWithToolCall?.reasoning_content).toBeUndefined() }) +// openaiShim test extraction seam 147 end + +// openaiShim test extraction seam 148 start: gateway-routed DeepSeek models inherit descriptor-backed reasoning and token shaping test('gateway-routed DeepSeek models inherit descriptor-backed reasoning and token shaping', async () => { process.env.CLAUDE_CODE_USE_OPENAI = '1' process.env.OPENAI_BASE_URL = 'https://openrouter.ai/api/v1' @@ -9361,7 +9154,10 @@ test('gateway-routed DeepSeek models inherit descriptor-backed reasoning and tok expect(requestBody?.max_completion_tokens).toBeUndefined() expect(requestBody?.store).toBeUndefined() }) +// openaiShim test extraction seam 148 end + +// openaiShim test extraction seam 149 start: Moonshot: cn host is also detected test('Moonshot: cn host is also detected', async () => { process.env.OPENAI_BASE_URL = 'https://api.moonshot.cn/v1' process.env.OPENAI_API_KEY = 'sk-moonshot-test' @@ -9393,7 +9189,10 @@ test('Moonshot: cn host is also detected', async () => { expect(requestBody?.store).toBeUndefined() }) +// openaiShim test extraction seam 149 end + +// openaiShim test extraction seam 150 start: Kimi Code endpoint inherits Moonshot max_tokens/store compatibility test('Kimi Code endpoint inherits Moonshot max_tokens/store compatibility', async () => { process.env.OPENAI_BASE_URL = 'https://api.kimi.com/coding/v1' process.env.OPENAI_API_KEY = 'sk-kimi-test' @@ -9427,7 +9226,10 @@ test('Kimi Code endpoint inherits Moonshot max_tokens/store compatibility', asyn expect(requestBody?.max_completion_tokens).toBeUndefined() expect(requestBody?.store).toBeUndefined() }) +// openaiShim test extraction seam 150 end + +// openaiShim test extraction seam 151 start: Kimi Code endpoint echoes reasoning_content on assistant tool-call messages test('Kimi Code endpoint echoes reasoning_content on assistant tool-call messages', async () => { process.env.OPENAI_BASE_URL = 'https://api.kimi.com/coding/v1' process.env.OPENAI_API_KEY = 'sk-kimi-test' @@ -9494,7 +9296,10 @@ test('Kimi Code endpoint echoes reasoning_content on assistant tool-call message 'Need to inspect logs via Bash; running a cat.', ) }) +// openaiShim test extraction seam 151 end + +// openaiShim test extraction seam 152 start: DeepSeek sends thinking toggle and normalized reasoning effort test('DeepSeek sends thinking toggle and normalized reasoning effort', async () => { process.env.OPENAI_BASE_URL = 'https://api.deepseek.com/v1' process.env.OPENAI_API_KEY = 'sk-deepseek' @@ -9533,82 +9338,20 @@ test('DeepSeek sends thinking toggle and normalized reasoning effort', async () expect(requestBody?.max_completion_tokens).toBeUndefined() expect(requestBody?.store).toBeUndefined() }) +// openaiShim test extraction seam 152 end -test('NVIDIA NIM DeepSeek sends chat template thinking kwargs', async () => { - process.env.OPENAI_BASE_URL = 'https://integrate.api.nvidia.com/v1' - process.env.NVIDIA_API_KEY = 'nvapi-test' - - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'deepseek-ai/deepseek-v4-pro', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType - const client = createOpenAIShimClient({ - reasoningEffort: 'xhigh', - }) as OpenAIShimClient - await client.beta.messages.create({ - model: 'deepseek-ai/deepseek-v4-pro', - system: 'test', - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 64, - stream: false, - thinking: { type: 'enabled' }, - }) +// openaiShim test extraction seam 153 start: NVIDIA NIM DeepSeek sends chat template thinking kwargs - expect(requestBody?.thinking).toEqual({ type: 'enabled' }) - expect(requestBody?.reasoning_effort).toBe('max') - expect(requestBody?.chat_template_kwargs).toEqual({ - thinking: true, - enable_thinking: true, - }) -}) +// openaiShim test extraction seam 153 end -test('NVIDIA NIM DeepSeek omits chat template thinking kwargs when thinking is disabled', async () => { - process.env.OPENAI_BASE_URL = 'https://integrate.api.nvidia.com/v1' - process.env.NVIDIA_API_KEY = 'nvapi-test' - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'deepseek-ai/deepseek-v4-pro', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType +// openaiShim test extraction seam 154 start: NVIDIA NIM DeepSeek omits chat template thinking kwargs when thinking is disabled - const client = createOpenAIShimClient({ - reasoningEffort: 'xhigh', - }) as OpenAIShimClient - await client.beta.messages.create({ - model: 'deepseek-ai/deepseek-v4-pro?thinking=disabled', - system: 'test', - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 64, - stream: false, - }) +// openaiShim test extraction seam 154 end - expect(requestBody?.thinking).toBeUndefined() - expect(requestBody?.reasoning_effort).toBeUndefined() - expect(requestBody?.chat_template_kwargs).toBeUndefined() -}) +// openaiShim test extraction seam 155 start: DeepSeek omits thinking controls when the Anthropic-side request does not set them test('DeepSeek omits thinking controls when the Anthropic-side request does not set them', async () => { process.env.OPENAI_BASE_URL = 'https://api.deepseek.com/v1' process.env.OPENAI_API_KEY = 'sk-deepseek' @@ -9641,7 +9384,10 @@ test('DeepSeek omits thinking controls when the Anthropic-side request does not expect(requestBody?.thinking).toBeUndefined() expect(requestBody?.reasoning_effort).toBeUndefined() }) +// openaiShim test extraction seam 155 end + +// openaiShim test extraction seam 156 start: DeepSeek forwards an explicit thinking disable toggle for V4 models test('DeepSeek forwards an explicit thinking disable toggle for V4 models', async () => { process.env.OPENAI_BASE_URL = 'https://api.deepseek.com/v1' process.env.OPENAI_API_KEY = 'sk-deepseek' @@ -9675,8 +9421,11 @@ test('DeepSeek forwards an explicit thinking disable toggle for V4 models', asyn expect(requestBody?.thinking).toEqual({ type: 'disabled' }) expect(requestBody?.reasoning_effort).toBeUndefined() }) +// openaiShim test extraction seam 156 end + +// openaiShim test extraction seam 157 start: collapses multiple text blocks in tool_result to string for DeepSeek compatibility (issue #774) test('collapses multiple text blocks in tool_result to string for DeepSeek compatibility (issue #774)', async () => { let requestBody: Record | undefined @@ -9753,7 +9502,10 @@ test('collapses multiple text blocks in tool_result to string for DeepSeek compa expect(typeof toolMessages[0].content).toBe('string') expect(toolMessages[0].content).toBe('line one\n\nline two') }) +// openaiShim test extraction seam 157 end + +// openaiShim test extraction seam 158 start: collapses multiple text blocks into a single string for DeepSeek compatibility (issue #774) test('collapses multiple text blocks into a single string for DeepSeek compatibility (issue #774)', async () => { let requestBody: Record | undefined @@ -9811,7 +9563,10 @@ test('collapses multiple text blocks into a single string for DeepSeek compatibi expect(typeof messages[1].content).toBe('string') expect(messages[1].content).toBe('Hello!\n\nHow are you?') }) +// openaiShim test extraction seam 158 end + +// openaiShim test extraction seam 159 start: preserves mixed text and image tool results as multipart content test('preserves mixed text and image tool results as multipart content', async () => { let requestBody: Record | undefined @@ -9897,7 +9652,10 @@ test('preserves mixed text and image tool results as multipart content', async ( expect(content[0].type).toBe('text') expect(content[1].type).toBe('image_url') }) +// openaiShim test extraction seam 159 end + +// openaiShim test extraction seam 160 start: Z.AI: uses max_tokens (not max_completion_tokens) and strips store test('Z.AI: uses max_tokens (not max_completion_tokens) and strips store', async () => { process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' process.env.OPENAI_API_KEY = 'sk-zai-test' @@ -9931,7 +9689,10 @@ test('Z.AI: uses max_tokens (not max_completion_tokens) and strips store', async expect(requestBody?.max_completion_tokens).toBeUndefined() expect(requestBody?.store).toBeUndefined() }) +// openaiShim test extraction seam 160 end + +// openaiShim test extraction seam 161 start: Z.AI: thinking mode enabled when requested test('Z.AI: thinking mode enabled when requested', async () => { process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' process.env.OPENAI_API_KEY = 'sk-zai-test' @@ -9973,7 +9734,10 @@ test('Z.AI: thinking mode enabled when requested', async () => { expect(requestBody?.max_completion_tokens).toBeUndefined() expect(requestBody?.max_tokens).toBe(1024) }) +// openaiShim test extraction seam 161 end + +// openaiShim test extraction seam 162 start: Z.AI GLM-5.2: default request relies on provider thinking defaults test('Z.AI GLM-5.2: default request relies on provider thinking defaults', async () => { process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' process.env.OPENAI_API_KEY = 'sk-zai-test' @@ -10005,7 +9769,10 @@ test('Z.AI GLM-5.2: default request relies on provider thinking defaults', async expect(requestBody?.thinking).toBeUndefined() expect(requestBody?.reasoning_effort).toBeUndefined() }) +// openaiShim test extraction seam 162 end + +// openaiShim test extraction seam 163 start: Z.AI GLM-5.2: user-selected xhigh effort maps to provider max effort test('Z.AI GLM-5.2: user-selected xhigh effort maps to provider max effort', async () => { process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' process.env.OPENAI_API_KEY = 'sk-zai-test' @@ -10039,6 +9806,8 @@ test('Z.AI GLM-5.2: user-selected xhigh effort maps to provider max effort', asy expect(requestBody?.thinking).toEqual({ type: 'enabled' }) expect(requestBody?.reasoning_effort).toBe('max') }) +// openaiShim test extraction seam 163 end + test.each([ ['glm-5.2?reasoning=low', 'high'], @@ -10117,6 +9886,7 @@ test.each([ expect(requestBody?.reasoning_effort).toBeUndefined() }) +// openaiShim test extraction seam 164 start: Z.AI GLM-5.2: model-query thinking disable omits reasoning effort test('Z.AI GLM-5.2: model-query thinking disable omits reasoning effort', async () => { process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' process.env.OPENAI_API_KEY = 'sk-zai-test' @@ -10148,7 +9918,10 @@ test('Z.AI GLM-5.2: model-query thinking disable omits reasoning effort', async expect(requestBody?.thinking).toEqual({ type: 'disabled' }) expect(requestBody?.reasoning_effort).toBeUndefined() }) +// openaiShim test extraction seam 164 end + +// openaiShim test extraction seam 165 start: Z.AI GLM-5.2: per-turn thinking overrides model-query default test('Z.AI GLM-5.2: per-turn thinking overrides model-query default', async () => { process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' process.env.OPENAI_API_KEY = 'sk-zai-test' @@ -10180,109 +9953,23 @@ test('Z.AI GLM-5.2: per-turn thinking overrides model-query default', async () = expect(requestBody?.thinking).toEqual({ type: 'enabled' }) expect(requestBody?.reasoning_effort).toBe('high') }) +// openaiShim test extraction seam 165 end -test('NVIDIA NIM Z.AI GLM sends chat template thinking kwargs', async () => { - process.env.OPENAI_BASE_URL = 'https://integrate.api.nvidia.com/v1' - process.env.NVIDIA_API_KEY = 'nvapi-test' - - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'z-ai/glm-5.2', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({ - reasoningEffort: 'xhigh', - }) as OpenAIShimClient - await client.beta.messages.create({ - model: 'z-ai/glm-5.2', - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 64, - stream: false, - }) - expect(requestBody?.thinking).toEqual({ type: 'enabled' }) - expect(requestBody?.reasoning_effort).toBe('max') - expect(requestBody?.chat_template_kwargs).toEqual({ - thinking: true, - enable_thinking: true, - }) -}) +// openaiShim test extraction seam 166 start: NVIDIA NIM Z.AI GLM sends chat template thinking kwargs -test('NVIDIA NIM Z.AI GLM omits chat template thinking kwargs without a reasoning request', async () => { - process.env.OPENAI_BASE_URL = 'https://integrate.api.nvidia.com/v1' - process.env.NVIDIA_API_KEY = 'nvapi-test' +// openaiShim test extraction seam 166 end - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'z-ai/glm-5.2', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType - const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'z-ai/glm-5.2', - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 64, - stream: false, - }) +// openaiShim test extraction seam 167 start: NVIDIA NIM Z.AI GLM omits chat template thinking kwargs without a reasoning request - expect(requestBody?.thinking).toBeUndefined() - expect(requestBody?.reasoning_effort).toBeUndefined() - expect(requestBody?.chat_template_kwargs).toBeUndefined() -}) +// openaiShim test extraction seam 167 end -test('NVIDIA NIM Z.AI GLM omits chat template thinking kwargs when thinking is disabled', async () => { - process.env.OPENAI_BASE_URL = 'https://integrate.api.nvidia.com/v1' - process.env.NVIDIA_API_KEY = 'nvapi-test' - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'z-ai/glm-5.2', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType +// openaiShim test extraction seam 168 start: NVIDIA NIM Z.AI GLM omits chat template thinking kwargs when thinking is disabled - const client = createOpenAIShimClient({ - reasoningEffort: 'xhigh', - }) as OpenAIShimClient - await client.beta.messages.create({ - model: 'z-ai/glm-5.2?thinking=disabled', - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 64, - stream: false, - }) +// openaiShim test extraction seam 168 end - expect(requestBody?.thinking).toEqual({ type: 'disabled' }) - expect(requestBody?.reasoning_effort).toBeUndefined() - expect(requestBody?.chat_template_kwargs).toBeUndefined() -}) // Extraction boundary: provider reasoning compatibility | tool-stream routing. // The gateway emission regression below remains provider/request-shaping coverage. @@ -10292,6 +9979,7 @@ test('NVIDIA NIM Z.AI GLM omits chat template thinking kwargs when thinking is d // `tool_stream` parameter. Streaming tool calls are simply not streamed on // this gateway; sending the parameter aborts the request with // `400 Unsupported parameter(s): tool_stream`. +// openaiShim test extraction seam 169 start: NVIDIA NIM Z.AI GLM streaming request with tools does not send tool_stream (regression #1950) test('NVIDIA NIM Z.AI GLM streaming request with tools does not send tool_stream (regression #1950)', async () => { process.env.OPENAI_BASE_URL = 'https://integrate.api.nvidia.com/v1' process.env.NVIDIA_API_KEY = 'nvapi-test' @@ -10339,6 +10027,8 @@ test('NVIDIA NIM Z.AI GLM streaming request with tools does not send tool_stream // aren't streamed on this gateway. expect(requestBody?.tool_stream).toBeUndefined() }) +// openaiShim test extraction seam 169 end + // Extraction boundary: provider tool-stream shaping | executor tool-stream retry. // The three retry-state tests below move together with request execution. @@ -10349,6 +10039,7 @@ test('NVIDIA NIM Z.AI GLM streaming request with tools does not send tool_stream // Here we exercise the generic self-heal using a Z.AI-contract gateway that // actually sends `tool_stream`, then rejects it — proving the retry drops the // parameter rather than surfacing a hard error. +// openaiShim test extraction seam 170 start: Shim self-heals a JSON `tool_stream` rejection by retrying without it (#1950) test('Shim self-heals a JSON `tool_stream` rejection by retrying without it (#1950)', async () => { process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' process.env.OPENAI_API_KEY = 'sk-zai-test' @@ -10407,7 +10098,10 @@ test('Shim self-heals a JSON `tool_stream` rejection by retrying without it (#19 // Tools are preserved across the retry. expect(Array.isArray(requestBodies[1]?.tools)).toBe(true) }) +// openaiShim test extraction seam 170 end + +// openaiShim test extraction seam 171 start: Shim stops after one tool_stream self-heal retry when the retry also fails (#1950) test('Shim stops after one tool_stream self-heal retry when the retry also fails (#1950)', async () => { process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' process.env.OPENAI_API_KEY = 'sk-zai-test' @@ -10444,7 +10138,10 @@ test('Shim stops after one tool_stream self-heal retry when the retry also fails expect(requestBodies[0]?.tool_stream).toBe(true) expect(requestBodies[1]?.tool_stream).toBeUndefined() }) +// openaiShim test extraction seam 171 end + +// openaiShim test extraction seam 172 start: Shim retries a tool_stream rejection with the same pooled credential (#1950) test('Shim retries a tool_stream rejection with the same pooled credential (#1950)', async () => { process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' process.env.OPENAI_API_KEYS = 'key-a,key-b' @@ -10493,10 +10190,13 @@ test('Shim retries a tool_stream rejection with the same pooled credential (#195 expect(authorizations).toEqual(['Bearer key-a', 'Bearer key-a']) }) +// openaiShim test extraction seam 172 end + // Extraction boundary: executor tool-stream retry | provider tool-stream shaping. // Provider emission rules below remain with compatibility/request planning. // Keep this marker stable for independent adjacent test migrations. +// openaiShim test extraction seam 173 start: Z.AI GLM-5.2: streaming requests with tools send tool_stream test('Z.AI GLM-5.2: streaming requests with tools send tool_stream', async () => { process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' process.env.OPENAI_API_KEY = 'sk-zai-test' @@ -10541,7 +10241,10 @@ test('Z.AI GLM-5.2: streaming requests with tools send tool_stream', async () => expect(requestBody?.tool_stream).toBe(true) }) +// openaiShim test extraction seam 173 end + +// openaiShim test extraction seam 174 start: Hicap GLM-5.2: uses Z.AI-compatible request shaping test('Hicap GLM-5.2: uses Z.AI-compatible request shaping', async () => { process.env.OPENAI_BASE_URL = 'https://api.hicap.ai/v1' process.env.HICAP_API_KEY = 'sk-hicap-test' @@ -10592,6 +10295,9 @@ test('Hicap GLM-5.2: uses Z.AI-compatible request shaping', async () => { expect(requestBody?.reasoning_effort).toBe('max') expect(requestBody?.tool_stream).toBe(true) }) +// openaiShim test extraction seam 174 end + +// openaiShim test extraction seam 175 start: Z.AI GLM-5.2: remote tool incompatibility does not use local toolless retry test('Z.AI GLM-5.2: remote tool incompatibility does not use local toolless retry', async () => { process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' process.env.OPENAI_API_KEY = 'sk-zai-test' @@ -10629,6 +10335,8 @@ test('Z.AI GLM-5.2: remote tool incompatibility does not use local toolless retr expect(requestBodies).toHaveLength(1) expect(requestBodies[0]?.tool_stream).toBe(true) }) +// openaiShim test extraction seam 175 end + test.each([ ['non-streaming Z.AI request with tools', 'https://api.z.ai/api/coding/paas/v4', false, true, 'glm-5.2'], @@ -10693,6 +10401,7 @@ test.each([ expect(requestBody?.tool_stream).toBeUndefined() }) +// openaiShim test extraction seam 176 start: Z.AI GLM-5.2: preserved thinking round-trips with tool calls test('Z.AI GLM-5.2: preserved thinking round-trips with tool calls', async () => { process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' process.env.OPENAI_API_KEY = 'sk-zai-test' @@ -10759,7 +10468,10 @@ test('Z.AI GLM-5.2: preserved thinking round-trips with tool calls', async () => }, ]) }) +// openaiShim test extraction seam 176 end + +// openaiShim test extraction seam 177 start: strips Anthropic attribution header block from chat-completions system prompt (#607) test('strips Anthropic attribution header block from chat-completions system prompt (#607)', async () => { let capturedBody: Record | undefined @@ -10809,7 +10521,10 @@ test('strips Anthropic attribution header block from chat-completions system pro expect(sysMsg?.content).toContain('You are Claude Code, helpful assistant.') expect(sysMsg?.content).toContain('Project context: bun + react.') }) +// openaiShim test extraction seam 177 end + +// openaiShim test extraction seam 178 start: strips Anthropic attribution header block from responses-API instructions (#607) test('strips Anthropic attribution header block from responses-API instructions (#607)', async () => { process.env.OPENAI_API_FORMAT = 'responses' let capturedBody: Record | undefined @@ -10855,7 +10570,10 @@ test('strips Anthropic attribution header block from responses-API instructions expect(instructions).not.toContain('cc_version=') expect(instructions).toContain('You are Claude Code.') }) +// openaiShim test extraction seam 178 end + +// openaiShim test extraction seam 179 start: emits reasoning_effort on chat_completions when reasoningEffort is passed test('emits reasoning_effort on chat_completions when reasoningEffort is passed', async () => { process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' process.env.OPENAI_API_KEY = 'test-key' @@ -10896,7 +10614,10 @@ test('emits reasoning_effort on chat_completions when reasoningEffort is passed' expect(requestBody?.reasoning_effort).toBe('xhigh') }) +// openaiShim test extraction seam 179 end + +// openaiShim test extraction seam 180 start: omits reasoning_effort on chat_completions when no override and model has no alias default test('omits reasoning_effort on chat_completions when no override and model has no alias default', async () => { process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' process.env.OPENAI_API_KEY = 'test-key' @@ -10932,7 +10653,10 @@ test('omits reasoning_effort on chat_completions when no override and model has expect(requestBody && 'reasoning_effort' in requestBody).toBe(false) }) +// openaiShim test extraction seam 180 end + +// openaiShim test extraction seam 181 start: emits reasoning_effort from codex alias default when no override is passed test('emits reasoning_effort from codex alias default when no override is passed', async () => { process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' process.env.OPENAI_API_KEY = 'test-key' @@ -10971,7 +10695,10 @@ test('emits reasoning_effort from codex alias default when no override is passed expect(requestBody?.reasoning_effort).toBe('high') }) +// openaiShim test extraction seam 181 end + +// openaiShim test extraction seam 182 start: DeepSeek: redacted_thinking block preserves continuity with reasoning_content: "" test('DeepSeek: redacted_thinking block preserves continuity with reasoning_content: ""', async () => { process.env.OPENAI_BASE_URL = 'https://api.deepseek.com/v1' process.env.OPENAI_API_KEY = 'sk-deepseek' @@ -11032,7 +10759,10 @@ test('DeepSeek: redacted_thinking block preserves continuity with reasoning_cont // message carries a tool_call, so it falls back to reasoning_content: "" expect(assistantWithToolCall?.reasoning_content).toBe('') }) +// openaiShim test extraction seam 182 end + +// openaiShim test extraction seam 183 start: DeepSeek: redacted_thinking block with non-empty data propagates data into reasoning_content test('DeepSeek: redacted_thinking block with non-empty data propagates data into reasoning_content', async () => { process.env.OPENAI_BASE_URL = 'https://api.deepseek.com/v1' process.env.OPENAI_API_KEY = 'sk-deepseek' @@ -11099,7 +10829,10 @@ test('DeepSeek: redacted_thinking block with non-empty data propagates data into 'encrypted_chain_of_thought_payload_v1', ) }) +// openaiShim test extraction seam 183 end + +// openaiShim test extraction seam 184 start: renders tool_reference blocks as text on the chat/completions path test('renders tool_reference blocks as text on the chat/completions path', async () => { const { __test } = await import('./openaiShim.ts') @@ -11136,7 +10869,10 @@ test('renders tool_reference blocks as text on the chat/completions path', async expect(content).toContain('mcp__example__memory_search') expect(content).toContain('mcp__example__memory_store') }) +// openaiShim test extraction seam 184 end + +// openaiShim test extraction seam 185 start: preserves valid tool pairs after history pruning while dropping orphaned tool calls test('preserves valid tool pairs after history pruning while dropping orphaned tool calls', async () => { const { __test } = await import('./openaiShim.ts') @@ -11188,6 +10924,8 @@ test('preserves valid tool pairs after history pruning while dropping orphaned t expect(toolMessages).toHaveLength(1) expect(toolMessages[0]?.tool_call_id).toBe('call_retained') }) +// openaiShim test extraction seam 185 end + // Extraction boundary: history pruning | executor Copilot refresh behavior. // The contiguous Copilot authentication retry block below moves with execution. @@ -11386,9 +11124,7 @@ test('GitHub Copilot responses fallback does not retry non-retryable HTTP failur expect(fetchCalls).toBe(2) }) -// Extraction boundary: history pruning | executor Copilot refresh behavior. -// The contiguous Copilot authentication retry block below moves with execution. -// Keep this marker stable for independent adjacent test migrations. +// openaiShim test extraction seam 186 start: GitHub Copilot 401 chat_completions retries with refreshed token test('GitHub Copilot 401 chat_completions retries with refreshed token', async () => { const realModule = realGithubModelsCredentials try { @@ -11458,7 +11194,10 @@ test('GitHub Copilot 401 chat_completions retries with refreshed token', async ( mock.module('../../utils/githubModelsCredentials.js', () => realModule) } }) +// openaiShim test extraction seam 186 end + +// openaiShim test extraction seam 187 start: GitHub Copilot 401 codex_responses retries with refreshed token test('GitHub Copilot 401 codex_responses retries with refreshed token', async () => { const realGithubModule = realGithubModelsCredentials const realCodexModule = realCodexShim @@ -11535,7 +11274,10 @@ test('GitHub Copilot 401 codex_responses retries with refreshed token', async () mock.module('./codexShim.js', () => realCodexModule) } }) +// openaiShim test extraction seam 187 end + +// openaiShim test extraction seam 188 start: GitHub Copilot 401 with credential pool uses refreshed token not pool key test('GitHub Copilot 401 with credential pool uses refreshed token not pool key', async () => { const realGithubModule = realGithubModelsCredentials try { @@ -11597,7 +11339,10 @@ test('GitHub Copilot 401 with credential pool uses refreshed token not pool key' mock.module('../../utils/githubModelsCredentials.js', () => realGithubModule) } }) +// openaiShim test extraction seam 188 end + +// openaiShim test extraction seam 189 start: GitHub Copilot 401 with "token has expired" triggers refresh test('GitHub Copilot 401 with "token has expired" triggers refresh', async () => { const realGithubModule = realGithubModelsCredentials try { @@ -11653,7 +11398,10 @@ test('GitHub Copilot 401 with "token has expired" triggers refresh', async () => mock.module('../../utils/githubModelsCredentials.js', () => realGithubModule) } }) +// openaiShim test extraction seam 189 end + +// openaiShim test extraction seam 190 start: GitHub Copilot 401 without expired-token message does not trigger refresh test('GitHub Copilot 401 without expired-token message does not trigger refresh', async () => { const realGithubModule = realGithubModelsCredentials try { @@ -11701,7 +11449,10 @@ test('GitHub Copilot 401 without expired-token message does not trigger refresh' mock.module('../../utils/githubModelsCredentials.js', () => realGithubModule) } }) +// openaiShim test extraction seam 190 end + +// openaiShim test extraction seam 191 start: GitHub Copilot 401 refresh returning same token does not update auth test('GitHub Copilot 401 refresh returning same token does not update auth', async () => { const realGithubModule = realGithubModelsCredentials try { @@ -11758,7 +11509,10 @@ test('GitHub Copilot 401 refresh returning same token does not update auth', asy mock.module('../../utils/githubModelsCredentials.js', () => realGithubModule) } }) +// openaiShim test extraction seam 191 end + +// openaiShim test extraction seam 192 start: GitHub Copilot 401 codex_responses with providerOverride does not trigger refresh test('GitHub Copilot 401 codex_responses with providerOverride does not trigger refresh', async () => { const realGithubModule = realGithubModelsCredentials try { @@ -11809,7 +11563,10 @@ test('GitHub Copilot 401 codex_responses with providerOverride does not trigger mock.module('../../utils/githubModelsCredentials.js', () => realGithubModule) } }) +// openaiShim test extraction seam 192 end + +// openaiShim test extraction seam 193 start: GitHub Copilot 401 chat_completions with providerOverride does not trigger refresh test('GitHub Copilot 401 chat_completions with providerOverride does not trigger refresh', async () => { const realGithubModule = realGithubModelsCredentials try { @@ -11859,6 +11616,8 @@ test('GitHub Copilot 401 chat_completions with providerOverride does not trigger mock.module('../../utils/githubModelsCredentials.js', () => realGithubModule) } }) +// openaiShim test extraction seam 193 end + // Extraction boundary: executor Copilot refresh behavior | JSON fallback conversion. // JSON fallback response conversion below is not owned by request execution. @@ -11903,6 +11662,7 @@ async function collectFallbackEvents( } } +// openaiShim test extraction seam 194 start: JSON fallback: preserves tool_calls as a tool_use block test('JSON fallback: preserves tool_calls as a tool_use block', async () => { const events = await collectFallbackEvents({ id: 'chatcmpl-json-tool', @@ -11954,7 +11714,10 @@ test('JSON fallback: preserves tool_calls as a tool_use block', async () => { | undefined expect(stopEvent?.delta?.stop_reason).toBe('tool_use') }) +// openaiShim test extraction seam 194 end + +// openaiShim test extraction seam 195 start: JSON fallback: maps finish_reason=length to max_tokens test('JSON fallback: maps finish_reason=length to max_tokens', async () => { const events = await collectFallbackEvents({ id: 'chatcmpl-json-len', @@ -11968,7 +11731,10 @@ test('JSON fallback: maps finish_reason=length to max_tokens', async () => { | undefined expect(stopEvent?.delta?.stop_reason).toBe('max_tokens') }) +// openaiShim test extraction seam 195 end + +// openaiShim test extraction seam 196 start: JSON fallback: preserves OpenCode Go quota error guidance test('JSON fallback: preserves OpenCode Go quota error guidance', async () => { process.env.OPENAI_BASE_URL = 'https://opencode.ai/zen/go/v1' const previousFetch = globalThis.fetch @@ -12017,7 +11783,10 @@ test('JSON fallback: preserves OpenCode Go quota error guidance', async () => { globalThis.fetch = previousFetch } }) +// openaiShim test extraction seam 196 end + +// openaiShim test extraction seam 197 start: JSON fallback: strips tags from emitted text test('JSON fallback: strips tags from emitted text', async () => { const events = await collectFallbackEvents({ id: 'chatcmpl-json-think', @@ -12039,7 +11808,10 @@ test('JSON fallback: strips tags from emitted text', async () => { expect(textDelta?.delta?.text).toBe('visible answer') expect(textDelta?.delta?.text).not.toContain('private plan') }) +// openaiShim test extraction seam 197 end + +// openaiShim test extraction seam 198 start: JSON fallback: normalizes array content into a text string test('JSON fallback: normalizes array content into a text string', async () => { const events = await collectFallbackEvents({ id: 'chatcmpl-json-array', @@ -12067,7 +11839,10 @@ test('JSON fallback: normalizes array content into a text string', async () => { expect(typeof textDelta?.delta?.text).toBe('string') expect(textDelta?.delta?.text).toBe('line one\nline two') }) +// openaiShim test extraction seam 198 end + +// openaiShim test extraction seam 199 start: JSON fallback: recovers raw-text tool call into tool_use block test('JSON fallback: recovers raw-text tool call into tool_use block', async () => { const events = await collectFallbackEvents({ id: 'chatcmpl-json-raw', @@ -12103,7 +11878,10 @@ test('JSON fallback: recovers raw-text tool call into tool_use block', async () expect(stopEvent?.delta?.stop_reason).toBe('tool_use') }) +// openaiShim test extraction seam 199 end + +// openaiShim test extraction seam 200 start: JSON fallback façade terminates converted messages test('JSON fallback façade terminates converted messages', async () => { const events = await collectFallbackEvents({ id: 'chatcmpl-json-boundary', @@ -12116,7 +11894,10 @@ test('JSON fallback façade terminates converted messages', async () => { expect(events.at(-1)?.type).toBe('message_stop') }) +// openaiShim test extraction seam 200 end + +// openaiShim test extraction seam 201 start: JSON fallback: recovers Tencent HY3 text tool calls into tool_use blocks test('JSON fallback: recovers Tencent HY3 text tool calls into tool_use blocks', async () => { const events = await collectFallbackEvents({ id: 'chatcmpl-json-hy3', @@ -12159,7 +11940,10 @@ test('JSON fallback: recovers Tencent HY3 text tool calls into tool_use blocks', | undefined expect(stopEvent?.delta?.stop_reason).toBe('tool_use') }) +// openaiShim test extraction seam 201 end + +// openaiShim test extraction seam 202 start: JSON fallback: preserves HY3-looking text for non-Tencent model names test('JSON fallback: preserves HY3-looking text for non-Tencent model names', async () => { const text = 'TaskCreate\nsubject: merely a documentation example\n' @@ -12191,7 +11975,10 @@ test('JSON fallback: preserves HY3-looking text for non-Tencent model names', as expect(toolStart).toBeUndefined() expect(textDelta?.delta?.text).toBe(text) }) +// openaiShim test extraction seam 202 end + +// openaiShim test extraction seam 203 start: JSON fallback: empty tool_calls array does not block raw-text recovery test('JSON fallback: empty tool_calls array does not block raw-text recovery', async () => { // tool_calls: [] is truthy; it must be treated as "no structured tool calls" // so the raw "Tool calls requested" recovery still runs. @@ -12223,7 +12010,10 @@ test('JSON fallback: empty tool_calls array does not block raw-text recovery', a name: 'Bash', }) }) +// openaiShim test extraction seam 203 end + +// openaiShim test extraction seam 204 start: JSON fallback: empty tool_calls does not block raw-text recovery on array content test('JSON fallback: empty tool_calls does not block raw-text recovery on array content', async () => { // Companion to the string-content case above: the array-content branch must // also treat tool_calls: [] as "no structured tool calls" so raw recovery runs. @@ -12257,3 +12047,4 @@ test('JSON fallback: empty tool_calls does not block raw-text recovery on array name: 'Bash', }) }) +// openaiShim test extraction seam 204 end diff --git a/src/services/api/openaiShim.ts b/src/services/api/openaiShim.ts index e8c7ce0515..b1ae67a6f7 100644 --- a/src/services/api/openaiShim.ts +++ b/src/services/api/openaiShim.ts @@ -139,6 +139,20 @@ import { parseCredentialList, } from './credentialPool.js' import { MIN_RECOMMENDED_OLLAMA_CONTEXT_TOKENS } from '../../utils/ollamaContext.js' +import { + filterAnthropicHeaders, + geminiThoughtSignatureFromExtraContent, + hasCerebrasApiHost, + hasGeminiApiHost as matchesGeminiApiHost, + hasMistralApiHost, + isGithubModelsMode, + isGeminiModelName, + mergeGeminiThoughtSignature, + maybeSetNvidiaNimChatTemplateThinking, + shouldPreserveGeminiThoughtSignature as shouldPreserveGeminiThoughtSignatureForRoute, +} from './openaiShim/providerCompatibility.js' + +export { hasMistralApiHost } const GITHUB_429_MAX_RETRIES = 3 const GITHUB_429_BASE_DELAY_SEC = 1 @@ -385,150 +399,20 @@ async function fetchWithHeadersDeadline( ) } -function isGithubModelsMode(): boolean { - return isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB) -} - -function filterAnthropicHeaders( - headers: Record | undefined, -): Record { - if (!headers) return {} - - const filtered: Record = {} - for (const [key, value] of Object.entries(headers)) { - const lower = key.toLowerCase() - if ( - lower.startsWith('x-anthropic') || - lower.startsWith('anthropic-') || - lower.startsWith('x-claude') || - lower === 'x-app' || - lower === 'x-client-app' || - lower === 'authorization' || - lower === 'x-api-key' || - lower === 'api-key' - ) { - continue - } - filtered[key] = value - } - - return filtered -} - function hasGeminiApiHost(baseUrl: string | undefined): boolean { - if (!baseUrl) return false - - try { - return new URL(baseUrl).hostname.toLowerCase() === GEMINI_API_HOST - } catch { - return false - } -} - -function isGeminiModelName(model: string | undefined): boolean { - const normalized = model?.trim().toLowerCase() - return ( - normalized?.startsWith('google/gemini-') === true || - normalized?.startsWith('gemini-') === true - ) + return matchesGeminiApiHost(baseUrl, GEMINI_API_HOST) } function shouldPreserveGeminiThoughtSignature( model: string | undefined, baseUrl?: string, ): boolean { - return isGeminiMode() || hasGeminiApiHost(baseUrl) || isGeminiModelName(model) -} - -function geminiThoughtSignatureFromExtraContent( - extraContent: unknown, -): string | undefined { - if (!extraContent || typeof extraContent !== 'object') return undefined - const google = (extraContent as Record).google - if (!google || typeof google !== 'object') return undefined - const signature = (google as Record).thought_signature - return typeof signature === 'string' && signature.length > 0 ? signature : undefined -} - -function mergeGeminiThoughtSignature( - extraContent: Record | undefined, - signature: string | undefined, -): Record | undefined { - if (!signature) return extraContent - const existingGoogle = - extraContent?.google && typeof extraContent.google === 'object' - ? extraContent.google as Record - : {} - return { - ...extraContent, - google: { - ...existingGoogle, - thought_signature: signature, - }, - } -} - -function hasCerebrasApiHost(baseUrl: string | undefined): boolean { - if (!baseUrl) return false - - try { - const host = new URL(baseUrl).hostname.toLowerCase() - return host === 'api.cerebras.ai' || host.endsWith('.cerebras.ai') - } catch { - return false - } -} - -export function hasMistralApiHost(baseUrl: string | undefined): boolean { - if (!baseUrl) return false - - try { - const host = new URL(baseUrl).hostname.toLowerCase() - return host === 'api.mistral.ai' || host.endsWith('.mistral.ai') - } catch { - return false - } -} - -function hasNvidiaNimApiHost(baseUrl: string | undefined): boolean { - if (!baseUrl) return false - - try { - return new URL(baseUrl).hostname.toLowerCase() === 'integrate.api.nvidia.com' - } catch { - return false - } -} - -function setNvidiaNimChatTemplateThinking(body: Record): void { - const existing = body.chat_template_kwargs - const kwargs = - existing && typeof existing === 'object' && !Array.isArray(existing) - ? { ...(existing as Record) } - : {} - - kwargs.thinking = true - kwargs.enable_thinking = true - body.chat_template_kwargs = kwargs -} - -function maybeSetNvidiaNimChatTemplateThinking( - body: Record, - baseUrl: string | undefined, - reasoningRequestPlan: { - thinkingType?: string - reasoningEffort?: string - }, -): void { - if (!hasNvidiaNimApiHost(baseUrl)) return - if ( - reasoningRequestPlan.thinkingType !== 'enabled' && - !reasoningRequestPlan.reasoningEffort - ) { - return - } - - setNvidiaNimChatTemplateThinking(body) + return shouldPreserveGeminiThoughtSignatureForRoute( + model, + baseUrl, + isGeminiMode(), + GEMINI_API_HOST, + ) } function formatRetryAfterHint(response: Response): string { diff --git a/src/services/api/openaiShim/providerCompatibility.test.ts b/src/services/api/openaiShim/providerCompatibility.test.ts new file mode 100644 index 0000000000..97d10723e3 --- /dev/null +++ b/src/services/api/openaiShim/providerCompatibility.test.ts @@ -0,0 +1,610 @@ +import { afterEach, beforeEach, expect, test } from 'bun:test' +import { acquireSharedMutationLock, releaseSharedMutationLock } from '../../../test/sharedMutationLock.js' +import { + createOpenAIShimClient, + hasMistralApiHost as facadeHasMistralApiHost, +} from '../openaiShim.js' +import { + filterAnthropicHeaders, + geminiThoughtSignatureFromExtraContent, + hasCerebrasApiHost, + hasGeminiApiHost, + hasMistralApiHost, + isGithubModelsMode, + isGeminiModelName, + mergeGeminiThoughtSignature, + maybeSetNvidiaNimChatTemplateThinking, + shouldPreserveGeminiThoughtSignature, +} from './providerCompatibility.js' + +const GEMINI_API_HOST = 'generativelanguage.googleapis.com' + +const originalEnv = { + CLAUDE_CODE_USE_GITHUB: process.env.CLAUDE_CODE_USE_GITHUB, + OPENAI_API_KEY: process.env.OPENAI_API_KEY, + OPENAI_API_KEYS: process.env.OPENAI_API_KEYS, + OPENAI_AZURE_STYLE: process.env.OPENAI_AZURE_STYLE, + OPENAI_BASE_URL: process.env.OPENAI_BASE_URL, + OPENAI_API_FORMAT: process.env.OPENAI_API_FORMAT, + OPENAI_AUTH_HEADER: process.env.OPENAI_AUTH_HEADER, + OPENAI_AUTH_SCHEME: process.env.OPENAI_AUTH_SCHEME, + OPENAI_AUTH_HEADER_VALUE: process.env.OPENAI_AUTH_HEADER_VALUE, + CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI, + CLAUDE_CODE_USE_GEMINI: process.env.CLAUDE_CODE_USE_GEMINI, + CLAUDE_CODE_USE_MISTRAL: process.env.CLAUDE_CODE_USE_MISTRAL, + MISTRAL_API_KEY: process.env.MISTRAL_API_KEY, + NVIDIA_API_KEY: process.env.NVIDIA_API_KEY, + NVIDIA_NIM: process.env.NVIDIA_NIM, + GEMINI_API_KEY: process.env.GEMINI_API_KEY, + GEMINI_AUTH_MODE: process.env.GEMINI_AUTH_MODE, + GEMINI_BASE_URL: process.env.GEMINI_BASE_URL, + GEMINI_MODEL: process.env.GEMINI_MODEL, + GEMINI_ACCESS_TOKEN: process.env.GEMINI_ACCESS_TOKEN, + GOOGLE_API_KEY: process.env.GOOGLE_API_KEY, +} +const originalFetch = globalThis.fetch + +beforeEach(async () => { + await acquireSharedMutationLock('openaiShim-providerCompatibility.test.ts') + delete process.env.CLAUDE_CODE_USE_GITHUB + process.env.OPENAI_API_KEY = 'test-key' + delete process.env.OPENAI_API_KEYS + delete process.env.OPENAI_AZURE_STYLE + process.env.OPENAI_BASE_URL = 'https://api.example.test/v1' + delete process.env.OPENAI_API_FORMAT + delete process.env.OPENAI_AUTH_HEADER + delete process.env.OPENAI_AUTH_SCHEME + delete process.env.OPENAI_AUTH_HEADER_VALUE + delete process.env.CLAUDE_CODE_USE_OPENAI + delete process.env.CLAUDE_CODE_USE_GEMINI + delete process.env.CLAUDE_CODE_USE_MISTRAL + delete process.env.MISTRAL_API_KEY + delete process.env.NVIDIA_API_KEY + delete process.env.NVIDIA_NIM + delete process.env.GEMINI_API_KEY + delete process.env.GEMINI_AUTH_MODE + delete process.env.GEMINI_BASE_URL + delete process.env.GEMINI_MODEL + delete process.env.GEMINI_ACCESS_TOKEN + delete process.env.GOOGLE_API_KEY +}) + +afterEach(() => { + try { + for (const [key, value] of Object.entries(originalEnv)) { + if (value === undefined) delete process.env[key] + else process.env[key] = value + } + globalThis.fetch = originalFetch + } finally { + releaseSharedMutationLock() + } +}) + +type ShimClient = { + beta: { + messages: { + create: ( + params: Record, + options?: Record, + ) => Promise & { + withResponse: () => Promise<{ + data: AsyncIterable> + }> + } + } + } +} + +function completionResponse({ + id = 'chatcmpl-test', + model = 'test-model', + content = 'ok', + usage, +}: { + id?: string + model?: string + content?: string + usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number } +} = {}): Response { + return new Response(JSON.stringify({ + id, + model, + choices: [{ message: { role: 'assistant', content }, finish_reason: 'stop' }], + ...(usage ? { usage } : {}), + }), { headers: { 'Content-Type': 'application/json' } }) +} + +function sseResponse(chunks: Array>): Response { + const body = `${chunks.map(chunk => `data: ${JSON.stringify(chunk)}\n\n`).join('')}data: [DONE]\n\n` + return new Response(body, { headers: { 'Content-Type': 'text/event-stream' } }) +} + +test('preserves Grep tool pattern fields for OpenAI-compatible providers', async () => { + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return completionResponse({ + id: 'chatcmpl-grep-schema', + model: 'qwen/qwen3.6-plus', + content: 'done', + usage: { prompt_tokens: 12, completion_tokens: 4, total_tokens: 16 }, + }) + }) as unknown as typeof globalThis.fetch + + const client = createOpenAIShimClient({}) as unknown as ShimClient + await client.beta.messages.create({ + model: 'qwen/qwen3.6-plus', + system: 'test system', + messages: [{ role: 'user', content: 'Use Grep' }], + tools: [{ + name: 'Grep', + description: 'Search file contents', + input_schema: { + type: 'object', + properties: { + pattern: { type: 'string', description: 'Search pattern' }, + path: { type: 'string' }, + }, + required: ['pattern'], + additionalProperties: false, + }, + }], + max_tokens: 64, + stream: false, + }) + + const tools = requestBody?.tools as Array> | undefined + const grepTool = tools?.find(tool => (tool.function as Record)?.name === 'Grep') as + | { function?: { parameters?: { properties?: Record; required?: string[] } } } + | undefined + expect(Object.keys(grepTool?.function?.parameters?.properties ?? {})).toContain('pattern') + expect(grepTool?.function?.parameters?.required).toContain('pattern') +}) + +test('filters Anthropic and authentication headers while preserving compatible headers', () => { + expect(filterAnthropicHeaders({ + 'anthropic-version': '2023-06-01', + 'x-anthropic-version': '2023-06-01', + 'x-anthropic-additional-protection': 'true', + 'x-claude-remote-session-id': 'secret', + 'x-app': 'secret', + 'x-client-app': 'secret', + authorization: 'Bearer secret', + 'x-api-key': 'secret', + 'x-custom': 'keep', + })).toEqual({ 'x-custom': 'keep' }) +}) + +test('the façade applies provider header filtering to a real request', async () => { + const requests: Headers[] = [] + globalThis.fetch = (async (_input, init) => { + requests.push(new Headers(init?.headers)) + return completionResponse({ + id: 'chatcmpl-1', + model: 'gpt-4o', + usage: { prompt_tokens: 8, completion_tokens: 3, total_tokens: 11 }, + }) + }) as unknown as typeof globalThis.fetch + const client = createOpenAIShimClient({ + defaultHeaders: { + 'anthropic-version': '2023-06-01', + 'anthropic-beta': 'prompt-caching-2024-07-31', + 'x-anthropic-version': '2023-06-01', + 'x-anthropic-additional-protection': 'true', + 'x-claude-remote-session-id': 'remote-123', + 'x-app': 'cli', + 'x-client-app': 'sdk', + 'x-api-key': 'anthropic-secret', + 'x-safe-header': 'keep-me', + 'x-custom': 'keep', + }, + }) as unknown as ShimClient + + await client.beta.messages.create({ + model: 'gpt-4o', + system: 'test system', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }) + await client.beta.messages.create({ + model: 'gpt-4o', + system: 'test system', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }, { + headers: { + 'anthropic-version': '2023-06-01', + 'anthropic-beta': 'prompt-caching-2024-07-31', + authorization: 'request-secret', + 'x-safe-header': 'keep-me', + 'x-request-custom': 'keep', + }, + }) + + expect(requests[0]?.get('anthropic-version')).toBeNull() + expect(requests[0]?.get('anthropic-beta')).toBeNull() + expect(requests[0]?.get('x-anthropic-version')).toBeNull() + expect(requests[0]?.get('x-anthropic-additional-protection')).toBeNull() + expect(requests[0]?.get('x-claude-remote-session-id')).toBeNull() + expect(requests[0]?.get('x-app')).toBeNull() + expect(requests[0]?.get('x-client-app')).toBeNull() + expect(requests[0]?.get('x-api-key')).toBeNull() + expect(requests[0]?.get('x-safe-header')).toBe('keep-me') + expect(requests[0]?.get('x-custom')).toBe('keep') + expect(requests[1]?.get('anthropic-version')).toBeNull() + expect(requests[1]?.get('anthropic-beta')).toBeNull() + expect(requests[1]?.get('authorization')).toBe('Bearer test-key') + expect(requests[1]?.get('x-safe-header')).toBe('keep-me') + expect(requests[1]?.get('x-request-custom')).toBe('keep') +}) + +test('recognizes only supported provider hosts', () => { + expect(hasGeminiApiHost('https://generativelanguage.googleapis.com/v1beta/openai', GEMINI_API_HOST)).toBe(true) + expect(hasGeminiApiHost('https://example.com/generativelanguage.googleapis.com', GEMINI_API_HOST)).toBe(false) + expect(hasGeminiApiHost('not a URL', GEMINI_API_HOST)).toBe(false) + expect(hasCerebrasApiHost('https://api.cerebras.ai/v1')).toBe(true) + expect(hasCerebrasApiHost('https://notcerebras.ai/v1')).toBe(false) + expect(hasMistralApiHost('https://api.mistral.ai/v1')).toBe(true) + expect(hasMistralApiHost('https://proxy.mistral.ai/v1')).toBe(true) + expect(hasMistralApiHost('https://eu.mistral.ai/v1')).toBe(true) + expect(hasMistralApiHost('https://edge.api.mistral.ai/v1')).toBe(true) + expect(hasMistralApiHost('https://mistral.ai/v1')).toBe(false) + expect(hasMistralApiHost('https://api.openai.com/v1')).toBe(false) + expect(hasMistralApiHost('https://notmistral.ai/v1')).toBe(false) + expect(hasMistralApiHost('https://api.mistral.ai.evil.com/v1')).toBe(false) + expect(hasMistralApiHost('not a url')).toBe(false) + expect(hasMistralApiHost(undefined)).toBe(false) + expect(facadeHasMistralApiHost('https://api.mistral.ai/v1')).toBe(true) +}) + +test('the façade does not infer Gemini mode from URL path text', async () => { + process.env.OPENAI_BASE_URL = + 'https://evil.example/generativelanguage.googleapis.com/v1beta/openai' + delete process.env.OPENAI_API_KEY + process.env.GEMINI_API_KEY = 'gemini-secret' + let authorization: string | null = null + globalThis.fetch = (async (_input, init) => { + authorization = new Headers(init?.headers).get('authorization') + return completionResponse({ + id: 'chatcmpl-1', + model: 'fake-model', + usage: { prompt_tokens: 12, completion_tokens: 4, total_tokens: 16 }, + }) + }) as unknown as typeof globalThis.fetch + const client = createOpenAIShimClient({}) as unknown as ShimClient + + await client.beta.messages.create({ + model: 'fake-model', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }) + + expect(authorization).toBeNull() +}) + +test('applies NIM thinking kwargs only for an enabled reasoning request', () => { + const enabled: Record = {} + maybeSetNvidiaNimChatTemplateThinking( + enabled, + 'https://integrate.api.nvidia.com/v1', + { reasoningEffort: 'high' }, + ) + expect(enabled.chat_template_kwargs).toEqual({ + thinking: true, + enable_thinking: true, + }) + + for (const [baseUrl, plan] of [ + ['https://api.example.test/v1', { reasoningEffort: 'high' }], + ['https://integrate.api.nvidia.com/v1', {}], + ['https://integrate.api.nvidia.com/v1', { thinkingType: 'disabled' }], + ['https://integrate.api.nvidia.com/v1', { thinkingType: 'disabled', reasoningEffort: 'high' }], + ] as const) { + const body: Record = {} + maybeSetNvidiaNimChatTemplateThinking(body, baseUrl, plan) + expect(body.chat_template_kwargs).toBeUndefined() + } +}) + +test('the façade applies NIM thinking kwargs across DeepSeek and GLM reasoning states', async () => { + process.env.OPENAI_BASE_URL = 'https://integrate.api.nvidia.com/v1' + process.env.NVIDIA_API_KEY = 'nvapi-test' + const bodies: Array> = [] + globalThis.fetch = (async (_input, init) => { + bodies.push(JSON.parse(String(init?.body)) as Record) + const index = bodies.length - 1 + return completionResponse({ + id: 'chatcmpl-1', + model: index < 2 ? 'deepseek-ai/deepseek-v4-pro' : 'z-ai/glm-5.2', + ...(index === 0 + ? { usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 } } + : {}), + }) + }) as unknown as typeof globalThis.fetch + const client = createOpenAIShimClient({ reasoningEffort: 'xhigh' }) as unknown as ShimClient + + await client.beta.messages.create({ + model: 'deepseek-ai/deepseek-v4-pro', + system: 'test', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 64, + stream: false, + thinking: { type: 'enabled' }, + }) + await client.beta.messages.create({ + model: 'deepseek-ai/deepseek-v4-pro?thinking=disabled', + system: 'test', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 64, + stream: false, + }) + await client.beta.messages.create({ + model: 'z-ai/glm-5.2', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 64, + stream: false, + }) + + const defaultClient = createOpenAIShimClient({}) as unknown as ShimClient + await defaultClient.beta.messages.create({ + model: 'z-ai/glm-5.2', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 64, + stream: false, + }) + await client.beta.messages.create({ + model: 'z-ai/glm-5.2?thinking=disabled', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 64, + stream: false, + }) + + expect(bodies[0]?.thinking).toEqual({ type: 'enabled' }) + expect(bodies[0]?.reasoning_effort).toBe('max') + expect(bodies[0]?.chat_template_kwargs).toEqual({ + thinking: true, + enable_thinking: true, + }) + expect(bodies[1]?.thinking).toBeUndefined() + expect(bodies[1]?.reasoning_effort).toBeUndefined() + expect(bodies[1]?.chat_template_kwargs).toBeUndefined() + expect(bodies[2]?.thinking).toEqual({ type: 'enabled' }) + expect(bodies[2]?.reasoning_effort).toBe('max') + expect(bodies[2]?.chat_template_kwargs).toEqual({ + thinking: true, + enable_thinking: true, + }) + expect(bodies[3]?.thinking).toBeUndefined() + expect(bodies[3]?.reasoning_effort).toBeUndefined() + expect(bodies[3]?.chat_template_kwargs).toBeUndefined() + expect(bodies[4]?.thinking).toEqual({ type: 'disabled' }) + expect(bodies[4]?.reasoning_effort).toBeUndefined() + expect(bodies[4]?.chat_template_kwargs).toBeUndefined() +}) + +test('reads GitHub mode from the owning compatibility module', () => { + expect(isGithubModelsMode()).toBe(false) + process.env.CLAUDE_CODE_USE_GITHUB = '1' + expect(isGithubModelsMode()).toBe(true) +}) + +test('recognizes Gemini model routes and preserves their thought signatures', () => { + expect(isGeminiModelName('gemini-2.5-pro')).toBe(true) + expect(isGeminiModelName('google/gemini-2.5-pro')).toBe(true) + expect(isGeminiModelName('not-gemini')).toBe(false) + expect(shouldPreserveGeminiThoughtSignature(undefined, undefined, false, GEMINI_API_HOST)).toBe(false) + expect(shouldPreserveGeminiThoughtSignature('gemini-2.5-pro', undefined, false, GEMINI_API_HOST)).toBe(true) + expect(shouldPreserveGeminiThoughtSignature(undefined, undefined, true, GEMINI_API_HOST)).toBe(true) +}) + +test('reads and merges Gemini thought signatures without dropping metadata', () => { + const extra = { google: { thought_signature: 'signature', other: true }, keep: 1 } + expect(geminiThoughtSignatureFromExtraContent(extra)).toBe('signature') + expect(geminiThoughtSignatureFromExtraContent({ google: {} })).toBeUndefined() + expect(mergeGeminiThoughtSignature(extra, 'replacement')).toEqual({ + google: { thought_signature: 'replacement', other: true }, + keep: 1, + }) + expect(mergeGeminiThoughtSignature(extra, undefined)).toBe(extra) +}) + +test('the façade replays Gemini signatures in follow-up tool calls', async () => { + const bodies: Array> = [] + globalThis.fetch = (async (_input, init) => { + bodies.push(JSON.parse(String(init?.body)) as Record) + return completionResponse({ + id: 'chatcmpl-1', + model: bodies.length === 1 + ? 'google/gemini-3.1-pro-preview' + : 'google/gemini-3.1-flash-lite', + content: 'done', + usage: { prompt_tokens: 12, completion_tokens: 4, total_tokens: 16 }, + }) + }) as unknown as typeof globalThis.fetch + const client = createOpenAIShimClient({}) as unknown as ShimClient + + await client.beta.messages.create({ + model: 'google/gemini-3.1-pro-preview', + system: 'test system', + messages: [ + { role: 'user', content: 'Use Bash' }, + { + role: 'assistant', + content: [{ + type: 'thinking', + thinking: 'I should inspect the working tree first.', + }, { + type: 'tool_use', + id: 'call_1', + name: 'Bash', + input: { command: 'pwd' }, + extra_content: { google: { thought_signature: 'sig-123' } }, + }], + }, + { role: 'user', content: [{ type: 'tool_result', tool_use_id: 'call_1', content: 'D:\\repo' }] }, + ], + max_tokens: 64, + stream: false, + }) + + process.env.OPENAI_BASE_URL = 'https://opengateway.gitlawb.com/v1' + await client.beta.messages.create({ + model: 'google/gemini-3.1-flash-lite', + messages: [ + { role: 'user', content: 'Use Write' }, + { + role: 'assistant', + content: [{ + type: 'tool_use', + id: 'call_1', + name: 'Write', + input: { file_path: 'todo.md', content: 'todo' }, + signature: 'sig-opengateway', + }], + }, + { role: 'user', content: [{ type: 'tool_result', tool_use_id: 'call_1', content: 'created' }] }, + ], + max_tokens: 64, + stream: false, + }) + + const firstMessages = bodies[0]?.messages as Array> + const firstAssistant = firstMessages.find(message => Array.isArray(message.tool_calls)) as { + tool_calls?: Array> + } + expect(firstAssistant.tool_calls?.[0]).toMatchObject({ + id: 'call_1', + type: 'function', + function: { name: 'Bash', arguments: JSON.stringify({ command: 'pwd' }) }, + extra_content: { google: { thought_signature: 'sig-123' } }, + }) + + const secondMessages = bodies[1]?.messages as Array> + const secondAssistant = secondMessages.find(message => Array.isArray(message.tool_calls)) as { + tool_calls?: Array> + } + expect(secondAssistant.tool_calls?.[0]).toMatchObject({ + id: 'call_1', + extra_content: { google: { thought_signature: 'sig-opengateway' } }, + }) +}) + +test('the façade preserves Gemini signatures from streaming tool and delta metadata', async () => { + const signatures = [ + { + location: 'tool', + value: 'sig-stream', + model: 'google/gemini-3.1-pro-preview', + toolName: 'Bash', + arguments: '{"command":"pwd"}', + userText: 'Use Bash', + system: 'test system', + }, + { + location: 'delta', + value: 'sig-delta', + model: 'google/gemini-3.1-flash-lite', + toolName: 'Write', + arguments: '{"file_path":"todo.md","content":"todo"}', + userText: 'Use Write', + system: undefined, + }, + ] as const + + for (const { location, value, model, toolName, arguments: toolArguments, userText, system } of signatures) { + globalThis.fetch = (async () => sseResponse([ + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model, + choices: [{ + index: 0, + delta: { + role: 'assistant', + ...(location === 'delta' + ? { extra_content: { google: { thought_signature: value } } } + : {}), + tool_calls: [{ + index: 0, + id: 'function-call-1', + type: 'function', + ...(location === 'tool' + ? { extra_content: { google: { thought_signature: value } } } + : {}), + function: { name: toolName, arguments: toolArguments }, + }], + }, + finish_reason: null, + }], + }, + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model, + choices: [{ index: 0, delta: {}, finish_reason: 'tool_calls' }], + }, + ])) as unknown as typeof globalThis.fetch + + const client = createOpenAIShimClient({}) as unknown as ShimClient + const result = await client.beta.messages.create({ + model, + ...(system ? { system } : {}), + messages: [{ role: 'user', content: userText }], + max_tokens: 64, + stream: true, + }).withResponse() + const events: Array> = [] + for await (const event of result.data) events.push(event) + const toolStart = events.find(event => event.type === 'content_block_start') as { + content_block?: Record + } + + expect(toolStart.content_block).toMatchObject({ + type: 'tool_use', + id: 'function-call-1', + name: toolName, + extra_content: { google: { thought_signature: value } }, + ...(location === 'delta' ? { signature: value } : {}), + }) + } +}) + +test('the façade preserves Gemini signatures from non-streaming message metadata', async () => { + globalThis.fetch = (async () => new Response(JSON.stringify({ + id: 'chatcmpl-1', + model: 'google/gemini-3.1-flash-lite', + choices: [{ + message: { + role: 'assistant', + extra_content: { google: { thought_signature: 'sig-message' } }, + tool_calls: [{ + id: 'function-call-1', + type: 'function', + function: { name: 'Write', arguments: '{"file_path":"todo.md","content":"todo"}' }, + }], + }, + finish_reason: 'tool_calls', + }], + usage: { prompt_tokens: 12, completion_tokens: 4, total_tokens: 16 }, + }), { headers: { 'Content-Type': 'application/json' } })) as unknown as typeof globalThis.fetch + const client = createOpenAIShimClient({}) as unknown as ShimClient + + const message = await client.beta.messages.create({ + model: 'google/gemini-3.1-flash-lite', + messages: [{ role: 'user', content: 'Use Write' }], + max_tokens: 64, + stream: false, + }) as { content?: Array> } + + expect(message.content?.[0]).toMatchObject({ + type: 'tool_use', + id: 'function-call-1', + name: 'Write', + extra_content: { google: { thought_signature: 'sig-message' } }, + signature: 'sig-message', + }) +}) diff --git a/src/services/api/openaiShim/providerCompatibility.ts b/src/services/api/openaiShim/providerCompatibility.ts new file mode 100644 index 0000000000..924afaa9d4 --- /dev/null +++ b/src/services/api/openaiShim/providerCompatibility.ts @@ -0,0 +1,149 @@ +import { isEnvTruthy } from '../../../utils/envUtils.js' + +export function isGithubModelsMode(): boolean { + return isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB) +} + +export function filterAnthropicHeaders( + headers: Record | undefined, +): Record { + if (!headers) return {} + + const filtered: Record = {} + for (const [key, value] of Object.entries(headers)) { + const lower = key.toLowerCase() + if ( + lower.startsWith('x-anthropic') || + lower.startsWith('anthropic-') || + lower.startsWith('x-claude') || + lower === 'x-app' || + lower === 'x-client-app' || + lower === 'authorization' || + lower === 'x-api-key' || + lower === 'api-key' + ) { + continue + } + filtered[key] = value + } + + return filtered +} + +export function hasGeminiApiHost( + baseUrl: string | undefined, + expectedHost: string, +): boolean { + if (!baseUrl) return false + + try { + return new URL(baseUrl).hostname.toLowerCase() === expectedHost + } catch { + return false + } +} + +export function isGeminiModelName(model: string | undefined): boolean { + const normalized = model?.trim().toLowerCase() + return ( + normalized?.startsWith('google/gemini-') === true || + normalized?.startsWith('gemini-') === true + ) +} + +export function shouldPreserveGeminiThoughtSignature( + model: string | undefined, + baseUrl: string | undefined, + isGeminiMode: boolean, + geminiApiHost: string, +): boolean { + return ( + isGeminiMode || + hasGeminiApiHost(baseUrl, geminiApiHost) || + isGeminiModelName(model) + ) +} + +export function geminiThoughtSignatureFromExtraContent( + extraContent: unknown, +): string | undefined { + if (!extraContent || typeof extraContent !== 'object') return undefined + const google = (extraContent as Record).google + if (!google || typeof google !== 'object') return undefined + const signature = (google as Record).thought_signature + return typeof signature === 'string' && signature.length > 0 ? signature : undefined +} + +export function mergeGeminiThoughtSignature( + extraContent: Record | undefined, + signature: string | undefined, +): Record | undefined { + if (!signature) return extraContent + const existingGoogle = + extraContent?.google && typeof extraContent.google === 'object' + ? extraContent.google as Record + : {} + return { + ...extraContent, + google: { ...existingGoogle, thought_signature: signature }, + } +} + +export function hasCerebrasApiHost(baseUrl: string | undefined): boolean { + if (!baseUrl) return false + + try { + const host = new URL(baseUrl).hostname.toLowerCase() + return host === 'api.cerebras.ai' || host.endsWith('.cerebras.ai') + } catch { + return false + } +} + +export function hasMistralApiHost(baseUrl: string | undefined): boolean { + if (!baseUrl) return false + + try { + const host = new URL(baseUrl).hostname.toLowerCase() + return host === 'api.mistral.ai' || host.endsWith('.mistral.ai') + } catch { + return false + } +} + +function hasNvidiaNimApiHost(baseUrl: string | undefined): boolean { + if (!baseUrl) return false + + try { + return new URL(baseUrl).hostname.toLowerCase() === 'integrate.api.nvidia.com' + } catch { + return false + } +} + +export function maybeSetNvidiaNimChatTemplateThinking( + body: Record, + baseUrl: string | undefined, + reasoningRequestPlan: { + thinkingType?: string + reasoningEffort?: string + }, +): void { + if (!hasNvidiaNimApiHost(baseUrl)) return + if ( + reasoningRequestPlan.thinkingType === 'disabled' || + (reasoningRequestPlan.thinkingType !== 'enabled' && + !reasoningRequestPlan.reasoningEffort) + ) { + return + } + + const existing = body.chat_template_kwargs + const kwargs = + existing && typeof existing === 'object' && !Array.isArray(existing) + ? { ...(existing as Record) } + : {} + kwargs.thinking = true + kwargs.enable_thinking = true + body.chat_template_kwargs = kwargs +}