Skip to content
Closed
3 changes: 3 additions & 0 deletions fixtures/fixtures.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ var (

//go:embed openai/responses/blocking/wrong_response_format.txtar
OaiResponsesBlockingWrongResponseFormat []byte

//go:embed openai/responses/blocking/web_search.txtar
OaiResponsesBlockingWebSearch []byte
)

var (
Expand Down
103 changes: 103 additions & 0 deletions fixtures/openai/responses/blocking/web_search.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
-- request --
{
"input": [
{
"role": "user",
"content": "What is the current weather in Cape Town?"
}
],
"model": "gpt-5",
"stream": false,
"tools": [
{
"type": "web_search"
}
]
}

-- non-streaming --
{
"id": "resp_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1",
"object": "response",
"created_at": 1767875200,
"status": "completed",
"background": false,
"billing": {
"payer": "developer"
},
"completed_at": 1767875205,
"error": null,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": null,
"max_tool_calls": null,
"model": "gpt-5-0806",
"output": [
{
"id": "ws_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1",
"type": "web_search_call",
"status": "completed"
},
{
"id": "msg_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1",
"type": "message",
"status": "completed",
"role": "assistant",
"content": [
{
"type": "output_text",
"text": "Based on my search, the current weather in Cape Town is partly cloudy with a temperature of around 22°C (72°F).",
"annotations": [
{
"type": "url_citation",
"start_index": 28,
"end_index": 47,
"url": "https://weather.example.com/cape-town",
"title": "Cape Town Weather"
}
]
}
]
}
],
"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": "web_search"
}
],
"top_logprobs": 0,
"top_p": 1.0,
"truncation": "disabled",
"usage": {
"input_tokens": 42,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 150,
"output_tokens_details": {
"reasoning_tokens": 0
},
"total_tokens": 192
},
"user": null,
"metadata": {}
}
51 changes: 46 additions & 5 deletions intercept/responses/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,27 +204,68 @@ func (i *responsesInterceptionBase) recordNonInjectedToolUsage(ctx context.Conte
}

for _, item := range response.Output {
var args recorder.ToolArgs
var (
args recorder.ToolArgs
toolName string
itemID string
callID string
)

// recording other function types to be considered: https://github.com/coder/aibridge/issues/121
switch item.Type {
case string(constant.ValueOf[constant.FunctionCall]()):
args = i.parseFunctionCallJSONArgs(ctx, item.Arguments)
toolName = item.Name
itemID = item.ID
callID = item.CallID

case string(constant.ValueOf[constant.CustomToolCall]()):
args = item.Input
toolName = item.Name
itemID = item.ID
callID = item.CallID

// Agentic tools: the client sends a corresponding *_output
// item correlated by call_id.
case "computer_call",
string(constant.ValueOf[constant.LocalShellCall]()),
string(constant.ValueOf[constant.ShellCall]()),
string(constant.ValueOf[constant.ApplyPatchCall]()):
toolName = item.Name
if toolName == "" {
toolName = item.Type
}
itemID = item.ID
callID = item.CallID

// Hosted tools: executed server-side, these output items
// carry only an id field — not call_id. The client never
// submits output for them.
// https://platform.openai.com/docs/api-reference/responses/create
case string(constant.ValueOf[constant.WebSearchCall]()),
string(constant.ValueOf[constant.FileSearchCall]()),
string(constant.ValueOf[constant.CodeInterpreterCall]()),
string(constant.ValueOf[constant.ImageGenerationCall]()),
string(constant.ValueOf[constant.McpCall]()):
toolName = item.Name
if toolName == "" {
toolName = item.Type
}
itemID = item.ID

default:
continue
}

if err := i.recorder.RecordToolUsage(ctx, &recorder.ToolUsageRecord{
InterceptionID: i.ID().String(),
MsgID: response.ID,
ToolCallID: item.CallID,
Tool: item.Name,
ItemID: itemID,
ToolCallID: callID,
Tool: toolName,
Args: args,
Injected: false,
}); err != nil {
i.logger.Warn(ctx, "failed to record tool usage", slog.Error(err), slog.F("tool", item.Name))
i.logger.Warn(ctx, "failed to record tool usage", slog.Error(err), slog.F("tool", toolName))
}
}
}
Expand Down
145 changes: 145 additions & 0 deletions intercept/responses/base_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ func TestRecordToolUsage(t *testing.T) {
{
InterceptionID: id.String(),
MsgID: "resp_456",
ItemID: "",
ToolCallID: "call_abc",
Tool: "get_weather",
Args: "",
Expand Down Expand Up @@ -160,6 +161,7 @@ func TestRecordToolUsage(t *testing.T) {
{
InterceptionID: id.String(),
MsgID: "resp_789",
ItemID: "",
ToolCallID: "call_1",
Tool: "get_weather",
Args: map[string]any{"location": "NYC"},
Expand All @@ -168,6 +170,7 @@ func TestRecordToolUsage(t *testing.T) {
{
InterceptionID: id.String(),
MsgID: "resp_789",
ItemID: "",
ToolCallID: "call_2",
Tool: "bad_json_args",
Args: `{"bad": args`,
Expand All @@ -176,6 +179,7 @@ func TestRecordToolUsage(t *testing.T) {
{
InterceptionID: id.String(),
MsgID: "resp_789",
ItemID: "",
ToolCallID: "call_3",
Tool: "search",
Args: `{\"query\": \"test\"}`,
Expand All @@ -184,13 +188,154 @@ func TestRecordToolUsage(t *testing.T) {
{
InterceptionID: id.String(),
MsgID: "resp_789",
ItemID: "",
ToolCallID: "call_4",
Tool: "calculate",
Args: map[string]any{"a": float64(1), "b": float64(2)},
Injected: false,
},
},
},
{
name: "web_search_call_with_no_name",
response: &oairesponses.Response{
ID: "resp_ws",
Output: []oairesponses.ResponseOutputItemUnion{
{
Type: "web_search_call",
ID: "ws_abc",
},
},
},
expected: []*recorder.ToolUsageRecord{
{
InterceptionID: id.String(),
MsgID: "resp_ws",
ItemID: "ws_abc",
ToolCallID: "",
Tool: "web_search_call",
Injected: false,
},
},
},
{
name: "all_additional_tool_types",
response: &oairesponses.Response{
ID: "resp_all",
Output: []oairesponses.ResponseOutputItemUnion{
{
Type: "web_search_call",
ID: "ws_1",
},
{
Type: "computer_call",
CallID: "call_comp",
},
{
Type: "local_shell_call",
CallID: "call_lsh",
},
{
Type: "shell_call",
CallID: "call_sh",
},
{
Type: "apply_patch_call",
CallID: "call_ap",
},
{
Type: "code_interpreter_call",
ID: "ci_1",
},
{
Type: "mcp_call",
ID: "mcp_1",
Name: "my_mcp_tool",
},
{
Type: "file_search_call",
ID: "fs_1",
},
{
Type: "image_generation_call",
ID: "ig_1",
},
{
Type: "message",
ID: "msg_skip",
},
{
Type: "reasoning",
ID: "rs_skip",
},
},
},
expected: []*recorder.ToolUsageRecord{
{
InterceptionID: id.String(),
MsgID: "resp_all",
ItemID: "ws_1",
Tool: "web_search_call",
Injected: false,
},
{
InterceptionID: id.String(),
MsgID: "resp_all",
ToolCallID: "call_comp",
Tool: "computer_call",
Injected: false,
},
{
InterceptionID: id.String(),
MsgID: "resp_all",
ToolCallID: "call_lsh",
Tool: "local_shell_call",
Injected: false,
},
{
InterceptionID: id.String(),
MsgID: "resp_all",
ToolCallID: "call_sh",
Tool: "shell_call",
Injected: false,
},
{
InterceptionID: id.String(),
MsgID: "resp_all",
ToolCallID: "call_ap",
Tool: "apply_patch_call",
Injected: false,
},
{
InterceptionID: id.String(),
MsgID: "resp_all",
ItemID: "ci_1",
Tool: "code_interpreter_call",
Injected: false,
},
{
InterceptionID: id.String(),
MsgID: "resp_all",
ItemID: "mcp_1",
Tool: "my_mcp_tool",
Injected: false,
},
{
InterceptionID: id.String(),
MsgID: "resp_all",
ItemID: "fs_1",
Tool: "file_search_call",
Injected: false,
},
{
InterceptionID: id.String(),
MsgID: "resp_all",
ItemID: "ig_1",
Tool: "image_generation_call",
Injected: false,
},
},
},
}

for _, tc := range tests {
Expand Down
1 change: 1 addition & 0 deletions intercept/responses/injected_tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ func (i *responsesInterceptionBase) invokeInjectedTool(ctx context.Context, resp
_ = i.recorder.RecordToolUsage(ctx, &recorder.ToolUsageRecord{
InterceptionID: i.ID().String(),
MsgID: responseID,
ItemID: fc.ID,
ToolCallID: fc.CallID,
ServerURL: &tool.ServerURL,
Tool: tool.Name,
Expand Down
Loading
Loading