@@ -38,10 +38,8 @@ const (
3838
3939type responsesInterceptionBase struct {
4040 id uuid.UUID
41- req * ResponsesNewParamsWrapper
42- reqPayload []byte
41+ reqPayload ResponsesRequestPayload
4342 cfg config.OpenAI
44- model string
4543 recorder recorder.Recorder
4644 mcpProxy mcp.ServerProxier
4745 logger slog.Logger
@@ -71,26 +69,37 @@ func (i *responsesInterceptionBase) ID() uuid.UUID {
7169}
7270
7371func (i * responsesInterceptionBase ) Setup (logger slog.Logger , recorder recorder.Recorder , mcpProxy mcp.ServerProxier ) {
74- i .logger = logger .With (slog .F ("model" , i .model ))
72+ i .logger = logger .With (slog .F ("model" , i .Model () ))
7573 i .recorder = recorder
7674 i .mcpProxy = mcpProxy
7775}
7876
7977func (i * responsesInterceptionBase ) Model () string {
80- return i .model
78+ return i .reqPayload . model ()
8179}
8280
8381func (i * responsesInterceptionBase ) CorrelatingToolCallID () * string {
84- if len (i .req .Input .OfInputItemList ) == 0 {
82+ items := gjson .GetBytes (i .reqPayload , "input" )
83+ if ! items .IsArray () {
8584 return nil
8685 }
8786
88- // The tool result should be the last input message.
89- item := i .req .Input .OfInputItemList [len (i .req .Input .OfInputItemList )- 1 ]
90- if item .OfFunctionCallOutput == nil {
87+ arr := items .Array ()
88+ if len (arr ) == 0 {
9189 return nil
9290 }
93- return & item .OfFunctionCallOutput .CallID
91+
92+ last := arr [len (arr )- 1 ]
93+ if last .Get (string (constant .ValueOf [constant.Type ]())).String () != string (constant .ValueOf [constant.FunctionCallOutput ]()) {
94+ return nil
95+ }
96+
97+ callID := last .Get ("call_id" ).String ()
98+ if callID == "" {
99+ return nil
100+ }
101+
102+ return & callID
94103}
95104
96105func (i * responsesInterceptionBase ) baseTraceAttributes (r * http.Request , streaming bool ) []attribute.KeyValue {
@@ -105,13 +114,7 @@ func (i *responsesInterceptionBase) baseTraceAttributes(r *http.Request, streami
105114}
106115
107116func (i * responsesInterceptionBase ) validateRequest (ctx context.Context , w http.ResponseWriter ) error {
108- if i .req == nil {
109- err := errors .New ("developer error: req is nil" )
110- i .sendCustomErr (ctx , w , http .StatusInternalServerError , err )
111- return err
112- }
113-
114- if i .req .Background .Value {
117+ if i .reqPayload .background () {
115118 err := fmt .Errorf ("background requests are currently not supported by AI Bridge" )
116119 i .sendCustomErr (ctx , w , http .StatusNotImplemented , err )
117120 return err
@@ -144,15 +147,15 @@ func (i *responsesInterceptionBase) requestOptions(respCopy *responseCopier) []o
144147 // eg. Codex CLI produces requests without ID set in reasoning items: https://platform.openai.com/docs/api-reference/responses/create#responses_create-input-input_item_list-item-reasoning-id
145148 // when re-encoded, ID field is set to empty string which results
146149 // in bad request while not sending ID field at all somehow works.
147- option .WithRequestBody ("application/json" , i .reqPayload ),
150+ option .WithRequestBody ("application/json" , [] byte ( i .reqPayload ) ),
148151
149152 // copyMiddleware copies body of original response body to the buffer in responseCopier,
150153 // also reference to headers and status code is kept responseCopier.
151154 // responseCopier is used by interceptors to forward response as it was received,
152155 // eliminating any possibility of JSON re-encoding issues.
153156 option .WithMiddleware (respCopy .copyMiddleware ),
154157 }
155- if ! i .req .Stream {
158+ if ! i .reqPayload .Stream () {
156159 opts = append (opts , option .WithRequestTimeout (requestTimeout ))
157160 }
158161 return opts
@@ -161,81 +164,83 @@ func (i *responsesInterceptionBase) requestOptions(respCopy *responseCopier) []o
161164// lastUserPrompt returns input text with "user" role from last input item
162165// or string input value if it is present + bool indicating if input was found or not.
163166// If no such input was found empty string + false is returned.
164- func (i * responsesInterceptionBase ) lastUserPrompt (ctx context. Context ) (string , bool , error ) {
167+ func (i * responsesInterceptionBase ) lastUserPrompt () (string , bool , error ) {
165168 if i == nil {
166169 return "" , false , errors .New ("cannot get last user prompt: nil struct" )
167170 }
168- if i .req == nil {
171+ if i .reqPayload == nil {
169172 return "" , false , errors .New ("cannot get last user prompt: nil request struct" )
170173 }
171174
172- // 'input' field can be a string or array of objects :
175+ // 'input' can be either a string or an array of input items :
173176 // https://platform.openai.com/docs/api-reference/responses/create#responses_create-input
174-
175- // Check string variant
176- if i .req .Input .OfString .Valid () {
177- return i .req .Input .OfString .Value , true , nil
177+ inputItems := gjson .GetBytes (i .reqPayload , "input" )
178+ if ! inputItems .Exists () || inputItems .Type == gjson .Null {
179+ return "" , false , nil
178180 }
179181
180- // Fallback to parsing original bytes since golang SDK doesn't properly decode 'Input' field.
181- // If 'type' field of input item is not set it will be omitted from 'Input.OfInputItemList'
182- // It is an optional field according to API: https://platform.openai.com/docs/api-reference/responses/create#responses_create-input-input_item_list-input_message
183- // example: fixtures/openai/responses/blocking/builtin_tool.txtar
184- inputItems := gjson .GetBytes (i .reqPayload , "input" )
182+ // String variant: treat the whole input as the user prompt.
183+ if inputItems .Type == gjson .String {
184+ return inputItems .String (), true , nil
185+ }
185186
187+ // Array variant: checking only the last input item
186188 if ! inputItems .IsArray () {
187- if inputItems .Type == gjson .Null {
188- return "" , false , nil
189- }
190- return "" , false , fmt .Errorf ("unexpected input type: %v" , inputItems .Type .String ())
189+ return "" , false , fmt .Errorf ("unexpected input type: %s" , inputItems .Type )
191190 }
192191
193192 inputItemsArr := inputItems .Array ()
194193 if len (inputItemsArr ) == 0 {
195194 return "" , false , nil
196195 }
197- lastItem := inputItemsArr [len (inputItemsArr )- 1 ]
198196
199- // Request was likely not human-initiated.
197+ lastItem := inputItemsArr [ len ( inputItemsArr ) - 1 ]
200198 if lastItem .Get ("role" ).Str != string (constant .ValueOf [constant.User ]()) {
199+ // Request was likely not initiated by a prompt but is an iteration of agentic loop.
201200 return "" , false , nil
202201 }
203202
204- // content can be a string or array of objects :
203+ // Message content can be either a string or an array of typed content items :
205204 // https://platform.openai.com/docs/api-reference/responses/create#responses_create-input-input_item_list-input_message-content
206205 content := lastItem .Get (string (constant .ValueOf [constant.Content ]()))
206+ if ! content .Exists () || content .Type == gjson .Null {
207+ return "" , false , nil
208+ }
209+
210+ // String variant: use it directly as the prompt.
211+ if content .Type == gjson .String {
212+ return content .Str , true , nil
213+ }
207214
208- // non array case, should be string
209215 if ! content .IsArray () {
210- if content .Type == gjson .String {
211- return content .Str , true , nil
212- }
213- return "" , false , fmt .Errorf ("unexpected input content type: %v" , content .Type .String ())
216+ return "" , false , fmt .Errorf ("unexpected input content type: %s" , content .Type )
214217 }
215218
216219 var sb strings.Builder
217220 promptExists := false
218221 for _ , c := range content .Array () {
219- // ignore inputs of not `input_text` type
222+ // Ignore non-text content blocks such as images or files.
220223 if c .Get (string (constant .ValueOf [constant.Type ]())).Str != string (constant .ValueOf [constant.InputText ]()) {
221224 continue
222225 }
223226
224227 text := c .Get (string (constant .ValueOf [constant.Text ]()))
225- if text .Type == gjson .String {
226- promptExists = true
227- sb .WriteString (text .Str + "\n " )
228- } else {
229- i .logger .Warn (ctx , fmt .Sprintf ("unexpected input content array element text type: %v" , text .Type ))
228+ if text .Type != gjson .String {
229+ continue
230+ }
231+
232+ if promptExists {
233+ sb .WriteByte ('\n' )
230234 }
235+ promptExists = true
236+ sb .WriteString (text .Str )
231237 }
232238
233239 if ! promptExists {
234240 return "" , false , nil
235241 }
236242
237- prompt := strings .TrimSuffix (sb .String (), "\n " )
238- return prompt , true , nil
243+ return sb .String (), true , nil
239244}
240245
241246func (i * responsesInterceptionBase ) recordUserPrompt (ctx context.Context , responseID string , prompt string ) {
0 commit comments