11import { describe , expect , it , vi } from "vitest" ;
22import type { AuthClient } from "@tailor-platform/auth-public-client" ;
3- import { createAIGatewayClient , type AIGatewayChatRequest , type AIGatewayClient } from "./client" ;
3+ import {
4+ createAIGatewayClient ,
5+ type AIGatewayChatRequest ,
6+ type AIGatewayClient ,
7+ type AIChatCompletionEvent ,
8+ } from "./client" ;
49
510function createMockAuthClient ( response : Response | Promise < Response > ) {
611 return {
@@ -29,24 +34,25 @@ function createSSEStream(chunks: string[]): ReadableStream<Uint8Array> {
2934 } ) ;
3035}
3136
32- async function collectStream ( client : AIGatewayClient , request : AIGatewayChatRequest ) {
33- const deltas : string [ ] = [ ] ;
37+ async function collectEvents ( client : AIGatewayClient , request : AIGatewayChatRequest ) {
38+ const events : AIChatCompletionEvent [ ] = [ ] ;
3439
35- for await ( const delta of client . chatCompletionStream ( request ) ) {
36- deltas . push ( delta ) ;
40+ for await ( const event of client . streamChatCompletion ( request ) ) {
41+ events . push ( event ) ;
3742 }
3843
39- return deltas ;
44+ return events ;
4045}
4146
4247describe ( "createAIGatewayClient" , ( ) => {
43- it ( "streams OpenAI-compatible text deltas through authClient.fetch" , async ( ) => {
48+ it ( "streams OpenAI-compatible events through authClient.fetch" , async ( ) => {
4449 const authClient = createMockAuthClient (
4550 new Response (
4651 createSSEStream ( [
4752 'data: {"choices":[{"delta":{"content":"Hel' ,
4853 'lo"}}]}\n\n' ,
4954 'data: {"choices":[{"delta":{"content":" world"}}]}\n\n' ,
55+ 'data: {"choices":[{"finish_reason":"stop"}]}\n\n' ,
5056 "data: [DONE]\n\n" ,
5157 ] ) ,
5258 {
@@ -61,9 +67,13 @@ describe("createAIGatewayClient", () => {
6167 authClient,
6268 } ) ;
6369
64- const deltas = await collectStream ( client , createRequest ( { signal } ) ) ;
70+ const events = await collectEvents ( client , createRequest ( { signal } ) ) ;
6571
66- expect ( deltas ) . toEqual ( [ "Hello" , " world" ] ) ;
72+ expect ( events ) . toEqual ( [
73+ { type : "text-delta" , text : "Hello" } ,
74+ { type : "text-delta" , text : " world" } ,
75+ { type : "done" , finishReason : "stop" } ,
76+ ] ) ;
6777 expect ( authClient . fetch ) . toHaveBeenCalledWith (
6878 "https://gateway.example.com/v1/chat/completions" ,
6979 expect . objectContaining ( {
@@ -82,7 +92,7 @@ describe("createAIGatewayClient", () => {
8292 } ) ;
8393 } ) ;
8494
85- it ( "yields a single text chunk for json responses" , async ( ) => {
95+ it ( "yields text and done events for json responses" , async ( ) => {
8696 const authClient = createMockAuthClient (
8797 new Response (
8898 JSON . stringify ( {
@@ -91,6 +101,7 @@ describe("createAIGatewayClient", () => {
91101 message : {
92102 content : [ { type : "text" , text : "Grounded answer" } ] ,
93103 } ,
104+ finish_reason : "stop" ,
94105 } ,
95106 ] ,
96107 } ) ,
@@ -105,12 +116,15 @@ describe("createAIGatewayClient", () => {
105116 authClient,
106117 } ) ;
107118
108- const deltas = await collectStream (
119+ const events = await collectEvents (
109120 client ,
110121 createRequest ( { model : "gemini-2.5-flash" , stream : false } ) ,
111122 ) ;
112123
113- expect ( deltas ) . toEqual ( [ "Grounded answer" ] ) ;
124+ expect ( events ) . toEqual ( [
125+ { type : "text-delta" , text : "Grounded answer" } ,
126+ { type : "done" , finishReason : "stop" } ,
127+ ] ) ;
114128 expect ( authClient . fetch ) . toHaveBeenCalledWith (
115129 "https://gateway.example.com/base/v1/chat/completions" ,
116130 expect . objectContaining ( {
@@ -127,43 +141,46 @@ describe("createAIGatewayClient", () => {
127141 } ) ;
128142 } ) ;
129143
130- it ( "throws on non-ok responses" , async ( ) => {
144+ it ( "emits done even when json responses do not include assistant text " , async ( ) => {
131145 const authClient = createMockAuthClient (
132- new Response ( "nope" , { status : 401 , statusText : "Nope" } ) ,
146+ new Response (
147+ JSON . stringify ( { choices : [ { message : { content : [ ] } , finish_reason : "tool_calls" } ] } ) ,
148+ { status : 200 } ,
149+ ) ,
133150 ) ;
134151 const client = createAIGatewayClient ( {
135152 gatewayUri : "https://gateway.example.com" ,
136153 authClient,
137154 } ) ;
138155
139- await expect ( collectStream ( client , createRequest ( ) ) ) . rejects . toThrow (
140- "AI Gateway streaming request failed (401 Nope): nope" ,
141- ) ;
156+ await expect (
157+ collectEvents ( client , createRequest ( { model : "gemini-2.5-flash" , stream : false } ) ) ,
158+ ) . resolves . toEqual ( [ { type : "done" , finishReason : "tool_calls" } ] ) ;
142159 } ) ;
143160
144- it ( "throws when a streaming response body is missing" , async ( ) => {
145- const authClient = createMockAuthClient ( new Response ( null , { status : 200 } ) ) ;
161+ it ( "throws on non-ok responses" , async ( ) => {
162+ const authClient = createMockAuthClient (
163+ new Response ( "nope" , { status : 401 , statusText : "Nope" } ) ,
164+ ) ;
146165 const client = createAIGatewayClient ( {
147166 gatewayUri : "https://gateway.example.com" ,
148167 authClient,
149168 } ) ;
150169
151- await expect ( collectStream ( client , createRequest ( ) ) ) . rejects . toThrow (
152- "AI Gateway streaming response did not include a body. " ,
170+ await expect ( collectEvents ( client , createRequest ( ) ) ) . rejects . toThrow (
171+ "AI Gateway streaming request failed (401 Nope): nope " ,
153172 ) ;
154173 } ) ;
155174
156- it ( "throws when json responses do not include assistant text" , async ( ) => {
157- const authClient = createMockAuthClient (
158- new Response ( JSON . stringify ( { choices : [ { message : { content : [ ] } } ] } ) , { status : 200 } ) ,
159- ) ;
175+ it ( "throws when a streaming response body is missing" , async ( ) => {
176+ const authClient = createMockAuthClient ( new Response ( null , { status : 200 } ) ) ;
160177 const client = createAIGatewayClient ( {
161178 gatewayUri : "https://gateway.example.com" ,
162179 authClient,
163180 } ) ;
164181
165- await expect (
166- collectStream ( client , createRequest ( { model : "gemini-2.5-flash" , stream : false } ) ) ,
167- ) . rejects . toThrow ( "AI Gateway JSON response did not include assistant text." ) ;
182+ await expect ( collectEvents ( client , createRequest ( ) ) ) . rejects . toThrow (
183+ "AI Gateway streaming response did not include a body." ,
184+ ) ;
168185 } ) ;
169186} ) ;
0 commit comments