diff --git a/tests/unit/proxy/transformers/sse-stream-transformer.test.ts b/tests/unit/proxy/transformers/sse-stream-transformer.test.ts index 9faebabc..61d99223 100644 --- a/tests/unit/proxy/transformers/sse-stream-transformer.test.ts +++ b/tests/unit/proxy/transformers/sse-stream-transformer.test.ts @@ -67,4 +67,108 @@ describe('proxy SSE stream transformer', () => { expect(body).toContain('"type":"text_delta"'); expect(body).toContain('event: message_stop'); }); + + // Helper: extract all JSON `data:` payloads from an Anthropic SSE response body, + // skipping the `[DONE]` sentinel and any non-JSON lines. + function parseSseDataEvents(text: string): Record[] { + const events: Record[] = []; + for (const line of text.split('\n')) { + if (!line.startsWith('data: ')) continue; + const raw = line.slice('data: '.length).trim(); + if (raw.length === 0 || raw === '[DONE]') continue; + try { + events.push(JSON.parse(raw) as Record); + } catch { + // skip non-JSON data lines + } + } + return events; + } + + it('emits thinking.signature as a non-empty string — JSON (Anthropic contract)', async () => { + // Anthropic requires a thinking block's `signature` to be a non-empty opaque + // STRING. This test asserts that contract; it is intentionally RED on the + // current code, which fabricates the signature as an object via + // generateThinkingSignature(). After the fix it becomes a regression guard. + const response = new Response( + JSON.stringify({ + id: 'chatcmpl_sig_json', + model: 'gpt-5.x', + choices: [ + { + index: 0, + message: { + role: 'assistant', + content: 'Final answer.', + reasoning_content: 'Step-by-step reasoning before answering.', + }, + finish_reason: 'stop', + }, + ], + usage: { prompt_tokens: 3, completion_tokens: 2, total_tokens: 5 }, + }), + { + status: 200, + headers: { 'Content-Type': 'application/json' }, + } + ); + + const transformed = await createAnthropicProxyResponse(response); + const body = (await transformed.json()) as { + content: Array<{ type: string; signature?: unknown }>; + }; + + const thinkingBlock = body.content.find((block) => block.type === 'thinking'); + expect( + thinkingBlock, + `no thinking block found; content = ${JSON.stringify(body.content)}` + ).toBeDefined(); + + const signature = thinkingBlock?.signature; + expect( + typeof signature, + `expected non-empty string signature, received ${JSON.stringify(signature)}` + ).toBe('string'); + expect((signature as string).length).toBeGreaterThan(0); + }); + + it('emits thinking signature as a non-empty string — SSE (Anthropic contract)', async () => { + // Streaming counterpart of the contract test above: mirrors the failed + // prod path where reasoning_content streams in and ccs closes the thinking + // block with a fabricated signature via createSignatureDeltaEvent(). + const openAISse = [ + 'data: {"id":"chatcmpl_sig_stream","object":"chat.completion.chunk","created":1,"model":"gpt-5.x","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"Planning the verification step by step."},"finish_reason":null}]}\n\n', + 'data: {"id":"chatcmpl_sig_stream","object":"chat.completion.chunk","created":1,"model":"gpt-5.x","choices":[{"index":0,"delta":{"content":"Final answer."},"finish_reason":null}]}\n\n', + 'data: {"id":"chatcmpl_sig_stream","object":"chat.completion.chunk","created":1,"model":"gpt-5.x","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":2,"total_tokens":5}}\n\n', + 'data: [DONE]\n\n', + ].join(''); + + const transformed = await createAnthropicProxyResponse( + new Response(openAISse, { + status: 200, + headers: { 'Content-Type': 'text/event-stream' }, + }) + ); + + const body = await transformed.text(); + const payloads = parseSseDataEvents(body); + + // ccs emits the thinking signature through createSignatureDeltaEvent, + // which produces a content_block_delta with delta.type === 'thinking_signature_delta'. + const signatureDelta = payloads.find((payload) => { + const delta = (payload as { delta?: { type?: string } }).delta; + return delta?.type === 'thinking_signature_delta'; + }); + expect( + signatureDelta, + `no thinking_signature_delta event in stream; payloads = ${JSON.stringify(payloads)}` + ).toBeDefined(); + + const signature = (signatureDelta as { delta?: { signature?: unknown } }).delta?.signature; + // Anthropic contract: streaming thinking signature MUST be a non-empty opaque string. + expect( + typeof signature, + `expected non-empty string signature, received ${JSON.stringify(signature)}` + ).toBe('string'); + }); });