feat: record provider_item_id for tool usage (#26856)

## Summary

Plumbs the Responses output item id (added as `ToolUsageRecord.ItemID` in #26855) through to the database, captured independently of the `provider_tool_call_id` correlation key. Hosted tools (`web_search_call`, etc.) only have an item id; agentic tools have both.

`provider_item_id` is specific to the OpenAI Responses API; it stays empty for chat completions and Anthropic messages, which have no separate item id.

## Changes

- Migration `000534`: nullable `provider_item_id` column on `aibridge_tool_usages`.
- Proto: `item_id` field 11 on `RecordToolUsageRequest`.
- Server handler: persists `provider_item_id` and adds it to structured logging.
- Translator: maps `ToolUsageRecord.ItemID` to the proto field.

## Tests

- `TestRecordToolUsageProviderItemID`: real-database round-trip asserting `provider_item_id` persists for both hosted and agentic tools, independently of `provider_tool_call_id`.

Stacked on #26855. Linear: AIGOV-96

---

_This PR was produced by opencode (agent) using the_ _`anthropic/claude-opus-4-8`_ _model, under human direction and review._
This commit is contained in:
Danny Kopping
2026-07-06 09:29:07 +02:00
committed by GitHub
parent 08a6359cac
commit dd216b96fe
12 changed files with 331 additions and 220 deletions
+1
View File
@@ -2078,6 +2078,7 @@ func AIBridgeToolUsage(t testing.TB, db database.Store, seed database.InsertAIBr
InterceptionID: takeFirst(seed.InterceptionID, uuid.New()),
ProviderResponseID: takeFirst(seed.ProviderResponseID, "provider_response_id"),
ProviderToolCallID: takeFirst(seed.ProviderToolCallID),
ProviderItemID: takeFirst(seed.ProviderItemID),
Tool: takeFirst(seed.Tool, "tool"),
ServerUrl: serverURL,
Input: takeFirst(seed.Input, "input"),
+4 -1
View File
@@ -1625,7 +1625,8 @@ CREATE TABLE aibridge_tool_usages (
invocation_error text,
metadata jsonb,
created_at timestamp with time zone NOT NULL,
provider_tool_call_id text
provider_tool_call_id text,
provider_item_id text
);
COMMENT ON TABLE aibridge_tool_usages IS 'Audit log of tool calls in intercepted requests in AI Bridge';
@@ -1638,6 +1639,8 @@ COMMENT ON COLUMN aibridge_tool_usages.injected IS 'Whether this tool was inject
COMMENT ON COLUMN aibridge_tool_usages.invocation_error IS 'Only injected tools are invoked.';
COMMENT ON COLUMN aibridge_tool_usages.provider_item_id IS 'Specific to the OpenAI Responses API: the unique id of the output item that carried the tool call. Distinct from provider_tool_call_id (the call_id correlation key), which is empty for hosted tools. Empty for the chat completions and Anthropic messages APIs, which have no separate item id.';
CREATE TABLE aibridge_user_prompts (
id uuid NOT NULL,
interception_id uuid NOT NULL,
@@ -0,0 +1,2 @@
ALTER TABLE aibridge_tool_usages
DROP COLUMN provider_item_id;
@@ -0,0 +1,4 @@
ALTER TABLE aibridge_tool_usages
ADD COLUMN provider_item_id text NULL; -- nullable to allow existing data to remain valid
COMMENT ON COLUMN aibridge_tool_usages.provider_item_id IS 'Specific to the OpenAI Responses API: the unique id of the output item that carried the tool call. Distinct from provider_tool_call_id (the call_id correlation key), which is empty for hosted tools. Empty for the chat completions and Anthropic messages APIs, which have no separate item id.';
+2
View File
@@ -4602,6 +4602,8 @@ type AIBridgeToolUsage struct {
Metadata pqtype.NullRawMessage `db:"metadata" json:"metadata"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
ProviderToolCallID sql.NullString `db:"provider_tool_call_id" json:"provider_tool_call_id"`
// Specific to the OpenAI Responses API: the unique id of the output item that carried the tool call. Distinct from provider_tool_call_id (the call_id correlation key), which is empty for hosted tools. Empty for the chat completions and Anthropic messages APIs, which have no separate item id.
ProviderItemID sql.NullString `db:"provider_item_id" json:"provider_item_id"`
}
// Audit log of prompts used by intercepted requests in AI Bridge
+10 -5
View File
@@ -1305,7 +1305,7 @@ func (q *sqlQuerier) GetAIBridgeTokenUsagesByInterceptionID(ctx context.Context,
const getAIBridgeToolUsagesByInterceptionID = `-- name: GetAIBridgeToolUsagesByInterceptionID :many
SELECT
id, interception_id, provider_response_id, server_url, tool, input, injected, invocation_error, metadata, created_at, provider_tool_call_id
id, interception_id, provider_response_id, server_url, tool, input, injected, invocation_error, metadata, created_at, provider_tool_call_id, provider_item_id
FROM
aibridge_tool_usages
WHERE
@@ -1336,6 +1336,7 @@ func (q *sqlQuerier) GetAIBridgeToolUsagesByInterceptionID(ctx context.Context,
&i.Metadata,
&i.CreatedAt,
&i.ProviderToolCallID,
&i.ProviderItemID,
); err != nil {
return nil, err
}
@@ -1566,11 +1567,11 @@ func (q *sqlQuerier) InsertAIBridgeTokenUsage(ctx context.Context, arg InsertAIB
const insertAIBridgeToolUsage = `-- name: InsertAIBridgeToolUsage :one
INSERT INTO aibridge_tool_usages (
id, interception_id, provider_response_id, provider_tool_call_id, tool, server_url, input, injected, invocation_error, metadata, created_at
id, interception_id, provider_response_id, provider_tool_call_id, provider_item_id, tool, server_url, input, injected, invocation_error, metadata, created_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, COALESCE($10::jsonb, '{}'::jsonb), $11
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, COALESCE($11::jsonb, '{}'::jsonb), $12
)
RETURNING id, interception_id, provider_response_id, server_url, tool, input, injected, invocation_error, metadata, created_at, provider_tool_call_id
RETURNING id, interception_id, provider_response_id, server_url, tool, input, injected, invocation_error, metadata, created_at, provider_tool_call_id, provider_item_id
`
type InsertAIBridgeToolUsageParams struct {
@@ -1578,6 +1579,7 @@ type InsertAIBridgeToolUsageParams struct {
InterceptionID uuid.UUID `db:"interception_id" json:"interception_id"`
ProviderResponseID string `db:"provider_response_id" json:"provider_response_id"`
ProviderToolCallID sql.NullString `db:"provider_tool_call_id" json:"provider_tool_call_id"`
ProviderItemID sql.NullString `db:"provider_item_id" json:"provider_item_id"`
Tool string `db:"tool" json:"tool"`
ServerUrl sql.NullString `db:"server_url" json:"server_url"`
Input string `db:"input" json:"input"`
@@ -1593,6 +1595,7 @@ func (q *sqlQuerier) InsertAIBridgeToolUsage(ctx context.Context, arg InsertAIBr
arg.InterceptionID,
arg.ProviderResponseID,
arg.ProviderToolCallID,
arg.ProviderItemID,
arg.Tool,
arg.ServerUrl,
arg.Input,
@@ -1614,6 +1617,7 @@ func (q *sqlQuerier) InsertAIBridgeToolUsage(ctx context.Context, arg InsertAIBr
&i.Metadata,
&i.CreatedAt,
&i.ProviderToolCallID,
&i.ProviderItemID,
)
return i, err
}
@@ -2279,7 +2283,7 @@ func (q *sqlQuerier) ListAIBridgeTokenUsagesByInterceptionIDs(ctx context.Contex
const listAIBridgeToolUsagesByInterceptionIDs = `-- name: ListAIBridgeToolUsagesByInterceptionIDs :many
SELECT
id, interception_id, provider_response_id, server_url, tool, input, injected, invocation_error, metadata, created_at, provider_tool_call_id
id, interception_id, provider_response_id, server_url, tool, input, injected, invocation_error, metadata, created_at, provider_tool_call_id, provider_item_id
FROM
aibridge_tool_usages
WHERE
@@ -2310,6 +2314,7 @@ func (q *sqlQuerier) ListAIBridgeToolUsagesByInterceptionIDs(ctx context.Context
&i.Metadata,
&i.CreatedAt,
&i.ProviderToolCallID,
&i.ProviderItemID,
); err != nil {
return nil, err
}
+2 -2
View File
@@ -56,9 +56,9 @@ RETURNING *;
-- name: InsertAIBridgeToolUsage :one
INSERT INTO aibridge_tool_usages (
id, interception_id, provider_response_id, provider_tool_call_id, tool, server_url, input, injected, invocation_error, metadata, created_at
id, interception_id, provider_response_id, provider_tool_call_id, provider_item_id, tool, server_url, input, injected, invocation_error, metadata, created_at
) VALUES (
@id, @interception_id, @provider_response_id, @provider_tool_call_id, @tool, @server_url, @input, @injected, @invocation_error, COALESCE(@metadata::jsonb, '{}'::jsonb), @created_at
@id, @interception_id, @provider_response_id, @provider_tool_call_id, @provider_item_id, @tool, @server_url, @input, @injected, @invocation_error, COALESCE(@metadata::jsonb, '{}'::jsonb), @created_at
)
RETURNING *;