@@ -2,11 +2,13 @@ import OpenAI from "openai";
22import type { ChatCompletionCreateParamsNonStreaming } from "openai/resources/chat/completions.js" ;
33import type { ResponseCreateParamsNonStreaming } from "openai/resources/responses/responses.js" ;
44import { describe , expect , it } from "vitest" ;
5+ import { createCodexNativeWebSearchWrapper } from "../llm/providers/stream-wrappers/openai.js" ;
56import type { Context , Model } from "../llm/types.js" ;
67import { isLiveTestEnabled } from "./live-test-helpers.js" ;
78import {
89 buildOpenAICompletionsParams ,
910 buildOpenAIResponsesParams ,
11+ createOpenAIResponsesTransportStreamFn ,
1012} from "./openai-transport-stream.js" ;
1113
1214const OPENAI_KEY = process . env . OPENAI_API_KEY ?? "" ;
@@ -131,4 +133,95 @@ describeLive("OpenAI tool projection live", () => {
131133 value : "OPENAI_PROJECTION_OK" ,
132134 } ) ;
133135 } , 45_000 ) ;
136+
137+ it ( "keeps code-mode tools after a payload hook adds an unreadable sibling" , async ( ) => {
138+ const model = {
139+ id : modelId ,
140+ name : modelId ,
141+ api : "openai-responses" ,
142+ provider : "openai" ,
143+ baseUrl : "https://api.openai.com/v1" ,
144+ reasoning : true ,
145+ input : [ "text" ] ,
146+ cost : { input : 0 , output : 0 , cacheRead : 0 , cacheWrite : 0 } ,
147+ contextWindow : 200000 ,
148+ maxTokens : 256 ,
149+ } satisfies Model < "openai-responses" > ;
150+ const codeModeContext = {
151+ systemPrompt : "Call the requested function exactly once." ,
152+ messages : [
153+ {
154+ role : "user" ,
155+ content : "Call exec with value exactly OPENAI_POST_HOOK_OK." ,
156+ timestamp : 1 ,
157+ } ,
158+ ] ,
159+ tools : [
160+ {
161+ name : "exec" ,
162+ description : "Return the requested probe value." ,
163+ parameters : {
164+ type : "object" ,
165+ properties : { value : { type : "string" } } ,
166+ required : [ "value" ] ,
167+ additionalProperties : false ,
168+ } ,
169+ } ,
170+ {
171+ name : "wait" ,
172+ description : "Wait without doing work." ,
173+ parameters : {
174+ type : "object" ,
175+ properties : { } ,
176+ additionalProperties : false ,
177+ } ,
178+ } ,
179+ ] ,
180+ } satisfies Context ;
181+ const streamFn = createCodexNativeWebSearchWrapper ( createOpenAIResponsesTransportStreamFn ( ) , {
182+ codeModeToolSurfaceEnabled : true ,
183+ } ) ;
184+ const streamOptions = {
185+ apiKey : OPENAI_KEY ,
186+ maxTokens : 128 ,
187+ reasoning : "low" ,
188+ toolChoice : { type : "function" , name : "exec" } ,
189+ openclawCodeModeToolSurface : true ,
190+ onPayload ( payload : unknown ) {
191+ const record = payload as Record < string , unknown > ;
192+ const tools = record . tools ;
193+ if ( ! Array . isArray ( tools ) || tools . length !== 2 ) {
194+ throw new Error ( "Expected projected exec and wait tools" ) ;
195+ }
196+ record . tools = [
197+ tools [ 0 ] ,
198+ {
199+ type : "function" ,
200+ get function ( ) : { name : string } {
201+ throw new Error ( "live unreadable post-hook function getter" ) ;
202+ } ,
203+ } ,
204+ tools [ 1 ] ,
205+ ] ;
206+ return record ;
207+ } ,
208+ } satisfies Parameters < typeof streamFn > [ 2 ] & {
209+ reasoning : "low" ;
210+ toolChoice : { type : "function" ; name : string } ;
211+ openclawCodeModeToolSurface : true ;
212+ } ;
213+ const stream = await Promise . resolve ( streamFn ( model , codeModeContext , streamOptions ) ) ;
214+
215+ const result = await stream . result ( ) ;
216+ const toolCall = result . content . find (
217+ ( block ) => block . type === "toolCall" && block . name === "exec" ,
218+ ) ;
219+
220+ expect ( result . stopReason ) . toBe ( "toolUse" ) ;
221+ expect ( toolCall ) . toMatchObject ( {
222+ type : "toolCall" ,
223+ name : "exec" ,
224+ arguments : { value : "OPENAI_POST_HOOK_OK" } ,
225+ } ) ;
226+ } , 45_000 ) ;
134227} ) ;
0 commit comments