99 normalizeReasoningEffort ,
1010 reasoningEffortForModel ,
1111 runCoach ,
12+ streamChatCoach ,
1213 type CoachRequest ,
1314} from "./ai-coach" ;
1415
@@ -21,6 +22,17 @@ function mockOpenAi(content: string, ok = true, status = 200) {
2122 } as unknown as Response ) ;
2223}
2324
25+ function mockOpenAiStream ( content : string ) {
26+ const body = new ReadableStream ( {
27+ start ( controller ) {
28+ const payload = JSON . stringify ( { choices : [ { delta : { content } } ] } ) ;
29+ controller . enqueue ( new TextEncoder ( ) . encode ( `data: ${ payload } \n\ndata: [DONE]\n\n` ) ) ;
30+ controller . close ( ) ;
31+ } ,
32+ } ) ;
33+ return vi . fn ( ) . mockResolvedValue ( { ok : true , status : 200 , body } as unknown as Response ) ;
34+ }
35+
2436const baseReq : CoachRequest = {
2537 mode : "explain" ,
2638 word : "見る" ,
@@ -111,6 +123,44 @@ describe("runCoach", () => {
111123 } ) ;
112124} ) ;
113125
126+ describe ( "streamChatCoach" , ( ) => {
127+ const chatReq : CoachRequest = {
128+ ...baseReq ,
129+ mode : "chat" ,
130+ message : "What does this mean?" ,
131+ } ;
132+
133+ it ( "omits reasoning_effort from the default Luna streaming request" , async ( ) => {
134+ const fetchMock = mockOpenAiStream ( "It means to look." ) ;
135+ vi . stubGlobal ( "fetch" , fetchMock ) ;
136+
137+ const chunks : string [ ] = [ ] ;
138+ for await ( const chunk of streamChatCoach ( "sk-test" , DEFAULT_COACH_MODEL , chatReq ) ) {
139+ chunks . push ( chunk ) ;
140+ }
141+
142+ expect ( chunks ) . toEqual ( [ "It means to look." ] ) ;
143+ const body = JSON . parse ( ( fetchMock . mock . calls [ 0 ] [ 1 ] as RequestInit ) . body as string ) ;
144+ expect ( body ) . toMatchObject ( { model : "gpt-5.6-luna" , stream : true } ) ;
145+ expect ( body . max_completion_tokens ) . toBeGreaterThan ( 400 ) ;
146+ expect ( body ) . not . toHaveProperty ( "reasoning_effort" ) ;
147+ expect ( body ) . not . toHaveProperty ( "temperature" ) ;
148+ expect ( body ) . not . toHaveProperty ( "max_tokens" ) ;
149+ } ) ;
150+
151+ it ( "preserves an explicit supported streaming effort" , async ( ) => {
152+ const fetchMock = mockOpenAiStream ( "It means to look." ) ;
153+ vi . stubGlobal ( "fetch" , fetchMock ) ;
154+
155+ for await ( const chunk of streamChatCoach ( "sk-test" , DEFAULT_COACH_MODEL , chatReq , "low" ) ) {
156+ expect ( chunk ) . toBe ( "It means to look." ) ;
157+ }
158+
159+ const body = JSON . parse ( ( fetchMock . mock . calls [ 0 ] [ 1 ] as RequestInit ) . body as string ) ;
160+ expect ( body . reasoning_effort ) . toBe ( "low" ) ;
161+ } ) ;
162+ } ) ;
163+
114164describe ( "reasoning model tuning" , ( ) => {
115165 it ( "defaults the coach to gpt-5.6-luna and OpenAI's medium effort" , ( ) => {
116166 expect ( DEFAULT_COACH_MODEL ) . toBe ( "gpt-5.6-luna" ) ;
0 commit comments