Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions fixtures/fixtures.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ var (
//go:embed openai/responses/streaming/builtin_tool.txtar
OaiResponsesStreamingBuiltinTool []byte

//go:embed openai/responses/streaming/custom_tool.txtar
OaiResponsesStreamingCustomTool []byte

//go:embed openai/responses/streaming/conversation.txtar
OaiResponsesStreamingConversation []byte

Expand Down
1 change: 1 addition & 0 deletions fixtures/openai/responses/streaming/builtin_tool.txtar
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,4 @@ data: {"type":"response.output_item.done","item":{"id":"fc_0c3fb28cfcf463a500695

event: response.completed
data: {"type":"response.completed","response":{"id":"resp_0c3fb28cfcf463a500695fa2f0239481a095ec6ce3dfe4d458","object":"response","created_at":1767875312,"status":"completed","background":false,"completed_at":1767875312,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4.1-2025-04-14","output":[{"id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","type":"function_call","status":"completed","arguments":"{\"a\":3,\"b\":5}","call_id":"call_7VaiUXZYuuuwWwviCrckxq6t","name":"add"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"function","description":"Add two numbers together.","name":"add","parameters":{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"],"additionalProperties":false},"strict":true}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":58,"input_tokens_details":{"cached_tokens":0},"output_tokens":18,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":76},"user":null,"metadata":{}},"sequence_number":14}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: expected or missing linter?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this new line completed event is not read by openAI library (stream.Next()/stream.Current()) .

mdn docs mention "Messages in the event stream are separated by a pair of newline characters." so I think without trailing new lines last event is skipped. I can see 2 new lines in what OpenAI API returns.

1 change: 1 addition & 0 deletions fixtures/openai/responses/streaming/codex_example.txtar

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions fixtures/openai/responses/streaming/conversation.txtar
Original file line number Diff line number Diff line change
Expand Up @@ -537,3 +537,4 @@ data: {"type":"response.output_item.done","item":{"id":"msg_0108ce40c6fb22bd0069

event: error
data: {"type":"error","error":{"type":"invalid_request_error","code":null,"message":"Conversation with id 'conv_695fa1132770819795d013275c77e8380108ce40c6fb22bd' not found.","param":null},"sequence_number":177}

54 changes: 54 additions & 0 deletions fixtures/openai/responses/streaming/custom_tool.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
-- request --
{
"input": "Use the code_exec tool to print hello world to the console.",
"model": "gpt-5",
"stream": true,
"tools": [
{
"type": "custom",
"name": "code_exec",
"description": "Executes arbitrary Python code."
}
]
}

-- streaming --
event: response.created
data: {"type":"response.created","response":{"id":"resp_0c26996bc41c2a0500696942e83634819fb71b2b8ff8a4a76c","object":"response","created_at":1768506088,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-5-2025-08-07","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":"medium","summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"custom","description":"Executes arbitrary Python code.","format":{"type":"text"},"name":"code_exec"}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0}

event: response.in_progress
data: {"type":"response.in_progress","response":{"id":"resp_0c26996bc41c2a0500696942e83634819fb71b2b8ff8a4a76c","object":"response","created_at":1768506088,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-5-2025-08-07","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":"medium","summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"custom","description":"Executes arbitrary Python code.","format":{"type":"text"},"name":"code_exec"}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1}

event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"rs_0c26996bc41c2a0500696942e8ae90819fb421c1b6a945aa99","type":"reasoning","summary":[]},"output_index":0,"sequence_number":2}

event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"rs_0c26996bc41c2a0500696942e8ae90819fb421c1b6a945aa99","type":"reasoning","summary":[]},"output_index":0,"sequence_number":3}

event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"ctc_0c26996bc41c2a0500696942ee6db8819fa6e841317eecbfb2","type":"custom_tool_call","status":"in_progress","call_id":"call_2gSnF58IEhXLwlbnqbm5XKMd","input":"","name":"code_exec"},"output_index":1,"sequence_number":4}

event: response.custom_tool_call_input.delta
data: {"type":"response.custom_tool_call_input.delta","delta":"print","item_id":"ctc_0c26996bc41c2a0500696942ee6db8819fa6e841317eecbfb2","obfuscation":"sTDUEAHu5aJ","output_index":1,"sequence_number":5}

event: response.custom_tool_call_input.delta
data: {"type":"response.custom_tool_call_input.delta","delta":"(\"","item_id":"ctc_0c26996bc41c2a0500696942ee6db8819fa6e841317eecbfb2","obfuscation":"qvFA5MbN9ZUnBH","output_index":1,"sequence_number":6}

event: response.custom_tool_call_input.delta
data: {"type":"response.custom_tool_call_input.delta","delta":"hello","item_id":"ctc_0c26996bc41c2a0500696942ee6db8819fa6e841317eecbfb2","obfuscation":"rRrXgQDOuwG","output_index":1,"sequence_number":7}

event: response.custom_tool_call_input.delta
data: {"type":"response.custom_tool_call_input.delta","delta":" world","item_id":"ctc_0c26996bc41c2a0500696942ee6db8819fa6e841317eecbfb2","obfuscation":"DwnJdEFXvZ","output_index":1,"sequence_number":8}

event: response.custom_tool_call_input.delta
data: {"type":"response.custom_tool_call_input.delta","delta":"\")","item_id":"ctc_0c26996bc41c2a0500696942ee6db8819fa6e841317eecbfb2","obfuscation":"pEr2t8Vpv3Ij96","output_index":1,"sequence_number":9}

event: response.custom_tool_call_input.done
data: {"type":"response.custom_tool_call_input.done","input":"print(\"hello world\")","item_id":"ctc_0c26996bc41c2a0500696942ee6db8819fa6e841317eecbfb2","output_index":1,"sequence_number":10}

event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"ctc_0c26996bc41c2a0500696942ee6db8819fa6e841317eecbfb2","type":"custom_tool_call","status":"completed","call_id":"call_2gSnF58IEhXLwlbnqbm5XKMd","input":"print(\"hello world\")","name":"code_exec"},"output_index":1,"sequence_number":11}

event: response.completed
data: {"type":"response.completed","response":{"id":"resp_0c26996bc41c2a0500696942e83634819fb71b2b8ff8a4a76c","object":"response","created_at":1768506088,"status":"completed","background":false,"completed_at":1768506095,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-5-2025-08-07","output":[{"id":"rs_0c26996bc41c2a0500696942e8ae90819fb421c1b6a945aa99","type":"reasoning","summary":[]},{"id":"ctc_0c26996bc41c2a0500696942ee6db8819fa6e841317eecbfb2","type":"custom_tool_call","status":"completed","call_id":"call_2gSnF58IEhXLwlbnqbm5XKMd","input":"print(\"hello world\")","name":"code_exec"}],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":"medium","summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"custom","description":"Executes arbitrary Python code.","format":{"type":"text"},"name":"code_exec"}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":64,"input_tokens_details":{"cached_tokens":0},"output_tokens":340,"output_tokens_details":{"reasoning_tokens":320},"total_tokens":404},"user":null,"metadata":{}},"sequence_number":12}

1 change: 1 addition & 0 deletions fixtures/openai/responses/streaming/prev_response_id.txtar
Original file line number Diff line number Diff line change
Expand Up @@ -573,3 +573,4 @@ data: {"type":"response.output_item.done","item":{"id":"msg_0f9c4b2f224d85800069

event: response.completed
data: {"type":"response.completed","response":{"id":"resp_0f9c4b2f224d858000695fa0649b8c8197b38914b15a7add0e","object":"response","created_at":1767874660,"status":"completed","background":false,"completed_at":1767874663,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[{"id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"The joke is funny because it uses a play on words, which is a common form of humor. \n\n1. **Double Meaning**: The phrase \"outstanding in his field\" can be interpreted literally, meaning the scarecrow is literally standing out in a field (as that's where scarecrows are found). However, it also has a figurative meaning: it suggests that someone is exceptionally skilled or accomplished in their area of expertise.\n\n2. **Surprise Element**: The punchline delivers an unexpected twist. You expect the award to be for some human trait, but it's actually a humorous observation about the scarecrow’s existence.\n\n3. **Absurdity**: The idea of a scarecrow, an inanimate object, receiving an award adds an element of absurdity, making it more amusing.\n\nOverall, it's the clever wordplay combined with an unexpected twist that makes the joke effective!"}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":"resp_0f9c4b2f224d858000695fa062bf048197a680f357bbb09000","prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":43,"input_tokens_details":{"cached_tokens":0},"output_tokens":182,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":225},"user":null,"metadata":{}},"sequence_number":188}

1 change: 1 addition & 0 deletions fixtures/openai/responses/streaming/simple.txtar
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,4 @@ data: {"type":"response.output_item.done","item":{"id":"msg_0f9c4b2f224d85800069

event: response.completed
data: {"type":"response.completed","response":{"id":"resp_0f9c4b2f224d858000695fa062bf048197a680f357bbb09000","object":"response","created_at":1767874658,"status":"completed","background":false,"completed_at":1767874660,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[{"id":"msg_0f9c4b2f224d858000695fa063d4708197af73c2f37cb0b9d3","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"Why did the scarecrow win an award?\n\nBecause he was outstanding in his field!"}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":11,"input_tokens_details":{"cached_tokens":0},"output_tokens":18,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":29},"user":null,"metadata":{}},"sequence_number":24}

1 change: 1 addition & 0 deletions fixtures/openai/responses/streaming/stream_error.txtar
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ data: {"type":"response.output_text.delta","item_id":"msg_123","output_index":0,

event: error
data: {"type":"error","code":"ERR_SOMETHING","message":"Something went wrong","param":null,"sequence_number":4}

1 change: 1 addition & 0 deletions fixtures/openai/responses/streaming/stream_failure.txtar
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ data: {"type":"response.output_text.delta","item_id":"msg_123","output_index":0,

event: response.failed
data: {"type":"response.failed","response":{"id":"resp_123","object":"response","status":"failed","error":{"code":"server_error","message":"The model failed to generate a response."},"output":[]},"sequence_number":4}

Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ ta: { "wrong format": should be forwarded as received

event: response.completed
data: {"type":"response.completed","response":{"id":"resp_123","object":"response","created_at":1767874658,"status":"completed","background":false,"completed_at":1767874660,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[{"id":"msg_0f9c4b2f224d858000695fa063d4708197af73c2f37cb0b9d3","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"Why did the scarecrow win an award?\n\nBecause he was outstanding in his field!"}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":11,"input_tokens_details":{"cached_tokens":0},"output_tokens":18,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":29},"user":null,"metadata":{}},"sequence_number":24}

19 changes: 14 additions & 5 deletions intercept/responses/streaming.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/coder/aibridge/recorder"
"github.com/google/uuid"
"github.com/openai/openai-go/v3/responses"
oaiconst "github.com/openai/openai-go/v3/shared/constant"
"go.opentelemetry.io/otel/attribute"
)

Expand Down Expand Up @@ -68,6 +69,7 @@ func (i *StreamingResponsesInterceptor) ProcessRequest(w http.ResponseWriter, r

var respCopy responseCopier
var responseID string
var completedResponse *responses.Response

srv := i.newResponsesService()
opts := i.requestOptions(&respCopy)
Expand All @@ -93,21 +95,28 @@ func (i *StreamingResponsesInterceptor) ProcessRequest(w http.ResponseWriter, r
}

for stream.Next() {
var ev responses.ResponseStreamEventUnion
ev = stream.Current()
ev := stream.Current()

// not every event has response.id set (eg: fixtures/openai/responses/streaming/simple.txtar).
// first event should be of 'response.created' type and have response.id set.
// set responseID to response.id of first event that has this field set.
// Not every event has response.id set (eg: fixtures/openai/responses/streaming/simple.txtar).
// First event should be of 'response.created' type and have response.id set.
// Set responseID to the first response.id that is set.
if responseID == "" && ev.Response.ID != "" {
responseID = ev.Response.ID
}

// Capture the response from the response.completed event.
// Only response.completed event type have 'usage' field set.
if ev.Type == string(oaiconst.ValueOf[oaiconst.ResponseCompleted]()) {
completedEvent := ev.AsResponseCompleted()
completedResponse = &completedEvent.Response
}
if err := events.Send(ctx, respCopy.buff.readDelta()); err != nil {
err = fmt.Errorf("failed to relay chunk: %w", err)
return err
}
}
i.recordUserPrompt(ctx, responseID)
i.recordToolUsage(ctx, completedResponse)

b, err := respCopy.readAll()
if err != nil {
Expand Down
19 changes: 19 additions & 0 deletions responses_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,25 @@ func TestResponsesOutputMatchesUpstream(t *testing.T) {
streaming: true,
expectModel: "gpt-4.1",
expectPromptRecorded: "Is 3 + 5 a prime number? Use the add function to calculate the sum.",
expectToolRecorded: &recorder.ToolUsageRecord{
MsgID: "resp_0c3fb28cfcf463a500695fa2f0239481a095ec6ce3dfe4d458",
Tool: "add",
Args: map[string]any{"a": float64(3), "b": float64(5)},
Injected: false,
},
},
{
name: "streaming_custom_tool",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these tests should be run for both blocking and streaming.

Adding transport-specific tests is going to lead to holes in coverage.
All that should be different is the fixture and the streaming flag.
In the other implementations the fixtures return the same results to make this easy.

fixture: fixtures.OaiResponsesStreamingCustomTool,
streaming: true,
expectModel: "gpt-5",
expectPromptRecorded: "Use the code_exec tool to print hello world to the console.",
expectToolRecorded: &recorder.ToolUsageRecord{
MsgID: "resp_0c26996bc41c2a0500696942e83634819fb71b2b8ff8a4a76c",
Tool: "code_exec",
Args: "print(\"hello world\")",
Injected: false,
},
},
{
name: "streaming_conversation",
Expand Down