Spaces:
Runtime error
Runtime error
| /** | |
| * Tests for Responses API Adapter - Logging and Cache Behavior | |
| * | |
| * These tests verify that: | |
| * 1. The response logged is in Responses API format (not Chat Completions) | |
| * 2. The endpoint (rubeusURL) is correctly set to 'createModelResponse' | |
| * 3. Caching works correctly for both streaming and non-streaming responses | |
| */ | |
| import { transformChatCompletionsToResponses } from '../responses/responseTransform'; | |
| import { transformStreamChunk, createStreamState } from '../responses/streamTransform'; | |
| import { describe, test, expect } from 'vitest'; | |
| describe('Responses API Adapter - Logging Behavior', () => { | |
| describe('Non-streaming response logging', () => { | |
| test('transformed response should be in Responses API format for logging', () => { | |
| // Simulate what the handler receives from Chat Completions endpoint | |
| const chatCompletionsResponse = { | |
| id: 'chatcmpl-123', | |
| object: 'chat.completion', | |
| created: 1700000000, | |
| model: 'claude-3-sonnet-20240229', | |
| choices: [ | |
| { | |
| index: 0, | |
| message: { | |
| role: 'assistant', | |
| content: 'Hello! How can I help you today?', | |
| }, | |
| finish_reason: 'stop', | |
| }, | |
| ], | |
| usage: { prompt_tokens: 10, completion_tokens: 15, total_tokens: 25 }, | |
| }; | |
| // Transform to Responses API format (this is what should be logged) | |
| const responsesApiResponse = transformChatCompletionsToResponses( | |
| chatCompletionsResponse as any, | |
| 200, | |
| 'anthropic', | |
| ); | |
| // Verify it's in Responses API format | |
| expect(responsesApiResponse).toHaveProperty('object', 'response'); | |
| expect(responsesApiResponse).toHaveProperty('status', 'completed'); | |
| expect(responsesApiResponse).toHaveProperty('output'); | |
| expect(Array.isArray((responsesApiResponse as any).output)).toBe(true); | |
| // Verify the output structure matches Responses API spec | |
| const output = (responsesApiResponse as any).output[0]; | |
| expect(output.type).toBe('message'); | |
| expect(output.role).toBe('assistant'); | |
| expect(output.content[0].type).toBe('output_text'); | |
| expect(output.content[0].text).toBe('Hello! How can I help you today?'); | |
| // Verify usage is in Responses API format | |
| expect((responsesApiResponse as any).usage).toEqual({ | |
| input_tokens: 10, | |
| input_tokens_details: { cached_tokens: 0 }, | |
| output_tokens: 15, | |
| output_tokens_details: { reasoning_tokens: 0 }, | |
| total_tokens: 25, | |
| }); | |
| }); | |
| test('requestOptions.response should contain Responses API format JSON', () => { | |
| const chatCompletionsResponse = { | |
| id: 'chatcmpl-456', | |
| object: 'chat.completion', | |
| created: 1700000000, | |
| model: 'gpt-4', | |
| choices: [ | |
| { | |
| index: 0, | |
| message: { role: 'assistant', content: 'Test response' }, | |
| finish_reason: 'stop', | |
| }, | |
| ], | |
| usage: { prompt_tokens: 5, completion_tokens: 3, total_tokens: 8 }, | |
| }; | |
| const responsesJson = transformChatCompletionsToResponses( | |
| chatCompletionsResponse as any, | |
| 200, | |
| 'openrouter', | |
| ); | |
| // Simulate what the handler does: create a new Response for requestOptions | |
| const responseForLogging = new Response(JSON.stringify(responsesJson), { | |
| status: 200, | |
| headers: new Headers({ 'content-type': 'application/json' }), | |
| }); | |
| // Verify the response can be parsed and is in correct format | |
| return responseForLogging.json().then((parsed) => { | |
| expect(parsed.object).toBe('response'); | |
| expect(parsed.status).toBe('completed'); | |
| expect(parsed.output).toBeDefined(); | |
| expect(parsed.output[0].type).toBe('message'); | |
| }); | |
| }); | |
| test('rubeusURL should be set to createModelResponse endpoint', () => { | |
| // Simulate the requestOptions update | |
| const mockRequestOptions = [ | |
| { | |
| providerOptions: { | |
| provider: 'anthropic', | |
| rubeusURL: 'chatComplete', // This is set by tryTargetsRecursively | |
| }, | |
| response: null, | |
| }, | |
| ]; | |
| // Simulate what the handler does | |
| const endpoint = 'createModelResponse'; | |
| mockRequestOptions[0].providerOptions.rubeusURL = endpoint; | |
| expect(mockRequestOptions[0].providerOptions.rubeusURL).toBe('createModelResponse'); | |
| }); | |
| }); | |
| describe('Streaming response logging', () => { | |
| test('accumulated stream state should produce valid Responses API format', () => { | |
| const state = createStreamState(); | |
| // Simulate streaming chunks | |
| const chunks = [ | |
| 'data: {"id":"chatcmpl-1","model":"claude-3","choices":[{"delta":{"content":"Hello"}}]}', | |
| 'data: {"id":"chatcmpl-1","model":"claude-3","choices":[{"delta":{"content":" world"}}]}', | |
| 'data: {"id":"chatcmpl-1","model":"claude-3","choices":[{"delta":{"content":"!"}}],"usage":{"prompt_tokens":10,"completion_tokens":3}}', | |
| 'data: [DONE]', | |
| ]; | |
| // Process chunks | |
| for (const chunk of chunks) { | |
| transformStreamChunk(chunk, state); | |
| } | |
| // Verify state accumulated correctly | |
| expect(state.accumulatedText).toBe('Hello world!'); | |
| expect(state.model).toBe('claude-3'); | |
| expect(state.inputTokens).toBe(10); | |
| expect(state.outputTokens).toBe(3); | |
| expect(state.hasStarted).toBe(true); | |
| expect(state.responseId).toMatch(/^resp_/); | |
| expect(state.outputItemId).toMatch(/^msg_/); | |
| }); | |
| test('final Responses API object should be built from stream state', () => { | |
| const state = createStreamState(); | |
| // Process some chunks to populate state | |
| transformStreamChunk( | |
| 'data: {"id":"chatcmpl-1","model":"claude-3-haiku","choices":[{"delta":{"content":"Test"}}]}', | |
| state, | |
| ); | |
| transformStreamChunk( | |
| 'data: {"id":"chatcmpl-1","model":"claude-3-haiku","choices":[{"delta":{"content":" response"}}],"usage":{"prompt_tokens":20,"completion_tokens":5}}', | |
| state, | |
| ); | |
| transformStreamChunk('data: [DONE]', state); | |
| // Build the final response object (same as handler does) | |
| const finalResponsesJson = { | |
| id: state.responseId, | |
| object: 'response', | |
| created_at: Math.floor(Date.now() / 1000), | |
| model: state.model, | |
| status: 'completed', | |
| output: [ | |
| { | |
| id: state.outputItemId, | |
| type: 'message', | |
| role: 'assistant', | |
| status: 'completed', | |
| content: [ | |
| { | |
| type: 'output_text', | |
| text: state.accumulatedText, | |
| annotations: [], | |
| }, | |
| ], | |
| }, | |
| ], | |
| usage: { | |
| input_tokens: state.inputTokens, | |
| input_tokens_details: { cached_tokens: 0 }, | |
| output_tokens: state.outputTokens, | |
| output_tokens_details: { reasoning_tokens: 0 }, | |
| total_tokens: state.inputTokens + state.outputTokens, | |
| }, | |
| }; | |
| // Verify the structure matches Responses API spec | |
| expect(finalResponsesJson.object).toBe('response'); | |
| expect(finalResponsesJson.status).toBe('completed'); | |
| expect(finalResponsesJson.model).toBe('claude-3-haiku'); | |
| expect(finalResponsesJson.output[0].type).toBe('message'); | |
| expect(finalResponsesJson.output[0].content[0].type).toBe('output_text'); | |
| expect(finalResponsesJson.output[0].content[0].text).toBe('Test response'); | |
| expect(finalResponsesJson.usage.input_tokens).toBe(20); | |
| expect(finalResponsesJson.usage.output_tokens).toBe(5); | |
| expect(finalResponsesJson.usage.total_tokens).toBe(25); | |
| }); | |
| test('streaming requestOptions.response should be JSON (not SSE)', () => { | |
| const state = createStreamState(); | |
| // Process chunks | |
| transformStreamChunk( | |
| 'data: {"id":"chatcmpl-1","model":"gpt-4","choices":[{"delta":{"content":"Hi"}}]}', | |
| state, | |
| ); | |
| transformStreamChunk( | |
| 'data: {"id":"chatcmpl-1","model":"gpt-4","choices":[],"usage":{"prompt_tokens":5,"completion_tokens":1}}', | |
| state, | |
| ); | |
| transformStreamChunk('data: [DONE]', state); | |
| // Build and serialize for logging | |
| const finalJson = { | |
| id: state.responseId, | |
| object: 'response', | |
| model: state.model, | |
| status: 'completed', | |
| output: [ | |
| { | |
| id: state.outputItemId, | |
| type: 'message', | |
| role: 'assistant', | |
| status: 'completed', | |
| content: [{ type: 'output_text', text: state.accumulatedText }], | |
| }, | |
| ], | |
| usage: { | |
| input_tokens: state.inputTokens, | |
| output_tokens: state.outputTokens, | |
| total_tokens: state.inputTokens + state.outputTokens, | |
| }, | |
| }; | |
| // The logged response should be JSON, not SSE | |
| const responseForLogging = new Response(JSON.stringify(finalJson), { | |
| status: 200, | |
| headers: new Headers({ 'content-type': 'application/json' }), | |
| }); | |
| expect(responseForLogging.headers.get('content-type')).toBe('application/json'); | |
| return responseForLogging.json().then((parsed) => { | |
| expect(parsed.object).toBe('response'); | |
| expect(parsed.output[0].content[0].text).toBe('Hi'); | |
| }); | |
| }); | |
| }); | |
| describe('Cache behavior', () => { | |
| test('cached Chat Completions response should be transformed to Responses API', () => { | |
| // Simulate a cached Chat Completions response | |
| const cachedChatCompletionsResponse = { | |
| id: 'chatcmpl-cached', | |
| object: 'chat.completion', | |
| created: 1699999999, | |
| model: 'claude-3-sonnet-20240229', | |
| choices: [ | |
| { | |
| index: 0, | |
| message: { | |
| role: 'assistant', | |
| content: 'This is a cached response', | |
| }, | |
| finish_reason: 'stop', | |
| }, | |
| ], | |
| usage: { prompt_tokens: 8, completion_tokens: 6, total_tokens: 14 }, | |
| }; | |
| // When cache hit occurs, the handler transforms it | |
| const responsesApiResponse = transformChatCompletionsToResponses( | |
| cachedChatCompletionsResponse as any, | |
| 200, | |
| 'anthropic', | |
| ); | |
| // Verify it's correctly transformed | |
| expect((responsesApiResponse as any).object).toBe('response'); | |
| expect((responsesApiResponse as any).output[0].content[0].text).toBe( | |
| 'This is a cached response', | |
| ); | |
| }); | |
| test('cache should store Chat Completions format, serve Responses API format', () => { | |
| // This tests the flow: | |
| // 1. User calls Responses API | |
| // 2. Handler transforms to Chat Completions | |
| // 3. Cache stores Chat Completions response | |
| // 4. On cache hit, handler transforms back to Responses API | |
| // What gets stored in cache (Chat Completions format) | |
| const cachedData = { | |
| id: 'chatcmpl-cache-test', | |
| object: 'chat.completion', | |
| created: 1700000001, | |
| model: 'claude-3-haiku-20240307', | |
| choices: [ | |
| { | |
| index: 0, | |
| message: { | |
| role: 'assistant', | |
| content: 'Cached answer to your question', | |
| }, | |
| finish_reason: 'stop', | |
| }, | |
| ], | |
| usage: { prompt_tokens: 12, completion_tokens: 8, total_tokens: 20 }, | |
| }; | |
| // What user receives (Responses API format) | |
| const userResponse = transformChatCompletionsToResponses(cachedData as any, 200, 'anthropic'); | |
| // Verify the formats are different | |
| expect(cachedData.object).toBe('chat.completion'); | |
| expect((userResponse as any).object).toBe('response'); | |
| // Verify user gets proper Responses API structure | |
| expect((userResponse as any).output).toBeDefined(); | |
| expect((userResponse as any).output[0].type).toBe('message'); | |
| expect((userResponse as any).usage.input_tokens).toBe(12); | |
| }); | |
| test('tool calls in cached response should be transformed correctly', () => { | |
| const cachedWithToolCalls = { | |
| id: 'chatcmpl-tools-cached', | |
| object: 'chat.completion', | |
| created: 1700000000, | |
| model: 'claude-3-sonnet-20240229', | |
| choices: [ | |
| { | |
| index: 0, | |
| message: { | |
| role: 'assistant', | |
| content: null, | |
| tool_calls: [ | |
| { | |
| id: 'call_cached_123', | |
| type: 'function', | |
| function: { | |
| name: 'get_weather', | |
| arguments: '{"location":"New York"}', | |
| }, | |
| }, | |
| ], | |
| }, | |
| finish_reason: 'tool_calls', | |
| }, | |
| ], | |
| usage: { prompt_tokens: 25, completion_tokens: 20, total_tokens: 45 }, | |
| }; | |
| const responsesApiResponse = transformChatCompletionsToResponses( | |
| cachedWithToolCalls as any, | |
| 200, | |
| 'anthropic', | |
| ); | |
| // Verify tool call is properly transformed | |
| const output = (responsesApiResponse as any).output; | |
| const functionCall = output.find((item: any) => item.type === 'function_call'); | |
| expect(functionCall).toBeDefined(); | |
| expect(functionCall.name).toBe('get_weather'); | |
| expect(functionCall.arguments).toBe('{"location":"New York"}'); | |
| expect(functionCall.call_id).toBe('call_cached_123'); | |
| }); | |
| }); | |
| describe('originalResponse vs responseBody distinction', () => { | |
| test('originalResponse should contain Chat Completions, responseBody should contain Responses API', () => { | |
| // Simulate the flow in the handler | |
| const chatCompletionsFromProvider = { | |
| id: 'chatcmpl-provider', | |
| object: 'chat.completion', | |
| created: 1700000000, | |
| model: 'claude-3-sonnet-20240229', | |
| choices: [ | |
| { | |
| index: 0, | |
| message: { role: 'assistant', content: 'Provider response' }, | |
| finish_reason: 'stop', | |
| }, | |
| ], | |
| usage: { prompt_tokens: 5, completion_tokens: 3, total_tokens: 8 }, | |
| }; | |
| // originalResponse.body - what comes from the provider (set by handlerUtils) | |
| const originalResponse = { | |
| body: chatCompletionsFromProvider, | |
| }; | |
| // responseBody - what gets logged (should be Responses API format) | |
| const responsesApiFormat = transformChatCompletionsToResponses( | |
| chatCompletionsFromProvider as any, | |
| 200, | |
| 'anthropic', | |
| ); | |
| // Verify they are different formats | |
| expect(originalResponse.body.object).toBe('chat.completion'); | |
| expect((responsesApiFormat as any).object).toBe('response'); | |
| // This is the key assertion: the logged responseBody should be Responses API format | |
| expect((responsesApiFormat as any).output).toBeDefined(); | |
| expect((responsesApiFormat as any).output[0].type).toBe('message'); | |
| }); | |
| }); | |
| }); | |