@@ -3,11 +3,14 @@ import * as Errors from "@effect/ai-openai/internal/errors"
33import * as OpenAiClient from "@effect/ai-openai/OpenAiClient"
44import { assert , describe , it } from "@effect/vitest"
55import { Config , ConfigProvider , Effect , Layer , Redacted , Schema , Stream } from "effect"
6+ import * as Fiber from "effect/Fiber"
67import type * as AiError from "effect/unstable/ai/AiError"
78import * as HttpClient from "effect/unstable/http/HttpClient"
89import * as HttpClientError from "effect/unstable/http/HttpClientError"
910import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"
1011import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"
12+ import * as Socket from "effect/unstable/socket/Socket"
13+ import { WS } from "vitest-websocket-mock"
1114
1215// =============================================================================
1316// Mock Helpers
@@ -631,5 +634,56 @@ describe("OpenAiClient", () => {
631634 assert . strictEqual ( result . reason . _tag , "InternalProviderError" )
632635 } ) . pipe ( Effect . provide ( MainLayer ) )
633636 } )
637+
638+ it . effect ( "sends response.cancel on interrupt in websocket mode" , ( ) => {
639+ const port = 42345
640+ const apiUrl = `http://localhost:${ port } /v1`
641+ const serverUrl = `ws://localhost:${ port } /v1/responses`
642+
643+ const HttpClientLayer = Layer . succeed (
644+ HttpClient . HttpClient ,
645+ makeMockHttpClient ( ( request ) => Effect . succeed ( makeMockResponse ( { status : 200 , body : { } , request } ) ) )
646+ )
647+
648+ const MainLayer = OpenAiClient . layer ( {
649+ apiKey : Redacted . make ( "test-key" ) ,
650+ apiUrl
651+ } ) . pipe ( Layer . provide ( HttpClientLayer ) )
652+
653+ return Effect . gen ( function * ( ) {
654+ const server = yield * Effect . acquireRelease (
655+ Effect . sync ( ( ) => new WS ( serverUrl , { jsonProtocol : true } ) ) ,
656+ ( server ) =>
657+ Effect . sync ( ( ) => {
658+ server . close ( )
659+ WS . clean ( )
660+ } )
661+ )
662+
663+ const client = yield * OpenAiClient . OpenAiClient
664+ const fiber = yield * Effect . forkScoped (
665+ OpenAiClient . withWebSocketMode (
666+ client . createResponseStream ( {
667+ model : "gpt-4o" ,
668+ input : "test"
669+ } ) . pipe (
670+ Effect . andThen ( ( [ _ , stream ] ) => Stream . runDrain ( stream ) )
671+ )
672+ ) ,
673+ { startImmediately : true }
674+ )
675+
676+ const createEvent = yield * Effect . promise ( ( ) => server . nextMessage as Promise < any > )
677+ assert . strictEqual ( createEvent . type , "response.create" )
678+
679+ yield * Fiber . interrupt ( fiber )
680+
681+ const cancelEvent = yield * Effect . promise ( ( ) => server . nextMessage as Promise < any > )
682+ assert . deepStrictEqual ( cancelEvent , { type : "response.cancel" } )
683+ } ) . pipe (
684+ Effect . provide ( MainLayer ) ,
685+ Effect . provideService ( Socket . WebSocketConstructor , ( url ) => new globalThis . WebSocket ( url ) )
686+ )
687+ } )
634688 } )
635689} )
0 commit comments