feat(coderd/database): add error columns to aibridge interception records (#26960)

Adds a nullable `aibridge_interception_error_type` enum and an
`error_message` column to `aibridge_interceptions`, so a failed
interception's terminal upstream error can be persisted.

Schema only: the write path and API exposure land in the stacked
backend PR.

*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-09 15:06:44 +02:00
committed by GitHub
parent cd1a676232
commit 63497ee9d8
7 changed files with 136 additions and 6 deletions
+17 -1
View File
@@ -27,6 +27,16 @@ CREATE TYPE ai_seat_usage_reason AS ENUM (
'task'
);
CREATE TYPE aibridge_interception_error_type AS ENUM (
'bad_request',
'unauthorized',
'rate_limited',
'overloaded',
'server_error',
'timeout',
'unknown'
);
CREATE TYPE api_key_scope AS ENUM (
'coder:all',
'coder:application_connect',
@@ -1559,7 +1569,9 @@ CREATE TABLE aibridge_interceptions (
credential_kind credential_kind DEFAULT 'centralized'::credential_kind NOT NULL,
credential_hint character varying(15) DEFAULT ''::character varying NOT NULL,
agent_firewall_session_id uuid,
agent_firewall_sequence_number integer
agent_firewall_sequence_number integer,
error_type aibridge_interception_error_type,
error_message character varying(1024)
);
COMMENT ON TABLE aibridge_interceptions IS 'Audit log of requests intercepted by AI Bridge';
@@ -1584,6 +1596,10 @@ COMMENT ON COLUMN aibridge_interceptions.agent_firewall_session_id IS 'The Agent
COMMENT ON COLUMN aibridge_interceptions.agent_firewall_sequence_number IS 'The Agent Firewall sequence number from the request header. Used to determine exact ordering of network requests relative to Agent Firewall audit events. NULL when the request did not pass through Agent Firewall.';
COMMENT ON COLUMN aibridge_interceptions.error_type IS 'Categorised terminal upstream error for a failed interception; NULL when the interception succeeded.';
COMMENT ON COLUMN aibridge_interceptions.error_message IS 'Raw terminal upstream error message for a failed interception; NULL when the interception succeeded.';
CREATE TABLE aibridge_model_thoughts (
interception_id uuid NOT NULL,
content text NOT NULL,
@@ -0,0 +1,5 @@
ALTER TABLE aibridge_interceptions
DROP COLUMN error_type,
DROP COLUMN error_message;
DROP TYPE aibridge_interception_error_type;
@@ -0,0 +1,19 @@
CREATE TYPE aibridge_interception_error_type AS ENUM (
'bad_request',
'unauthorized',
'rate_limited',
'overloaded',
'server_error',
'timeout',
'unknown'
);
-- Records the terminal upstream error observed when an interception failed.
-- Both columns are NULL for interceptions that completed successfully.
-- error_message is capped at 1024 characters as a hard schema-level bound.
ALTER TABLE aibridge_interceptions
ADD COLUMN error_type aibridge_interception_error_type,
ADD COLUMN error_message varchar(1024);
COMMENT ON COLUMN aibridge_interceptions.error_type IS 'Categorised terminal upstream error for a failed interception; NULL when the interception succeeded.';
COMMENT ON COLUMN aibridge_interceptions.error_message IS 'Raw terminal upstream error message for a failed interception; NULL when the interception succeeded.';
+2
View File
@@ -1151,6 +1151,8 @@ func (q *sqlQuerier) ListAuthorizedAIBridgeSessionThreads(ctx context.Context, a
&i.AIBridgeInterception.CredentialHint,
&i.AIBridgeInterception.AgentFirewallSessionID,
&i.AIBridgeInterception.AgentFirewallSequenceNumber,
&i.AIBridgeInterception.ErrorType,
&i.AIBridgeInterception.ErrorMessage,
); err != nil {
return nil, err
}
+77
View File
@@ -16,6 +16,79 @@ import (
"github.com/sqlc-dev/pqtype"
)
type AIBridgeInterceptionErrorType string
const (
AibridgeInterceptionErrorTypeBadRequest AIBridgeInterceptionErrorType = "bad_request"
AibridgeInterceptionErrorTypeUnauthorized AIBridgeInterceptionErrorType = "unauthorized"
AibridgeInterceptionErrorTypeRateLimited AIBridgeInterceptionErrorType = "rate_limited"
AibridgeInterceptionErrorTypeOverloaded AIBridgeInterceptionErrorType = "overloaded"
AibridgeInterceptionErrorTypeServerError AIBridgeInterceptionErrorType = "server_error"
AibridgeInterceptionErrorTypeTimeout AIBridgeInterceptionErrorType = "timeout"
AibridgeInterceptionErrorTypeUnknown AIBridgeInterceptionErrorType = "unknown"
)
func (e *AIBridgeInterceptionErrorType) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = AIBridgeInterceptionErrorType(s)
case string:
*e = AIBridgeInterceptionErrorType(s)
default:
return fmt.Errorf("unsupported scan type for AIBridgeInterceptionErrorType: %T", src)
}
return nil
}
type NullAIBridgeInterceptionErrorType struct {
AIBridgeInterceptionErrorType AIBridgeInterceptionErrorType `json:"aibridge_interception_error_type"`
Valid bool `json:"valid"` // Valid is true if AIBridgeInterceptionErrorType is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullAIBridgeInterceptionErrorType) Scan(value interface{}) error {
if value == nil {
ns.AIBridgeInterceptionErrorType, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.AIBridgeInterceptionErrorType.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullAIBridgeInterceptionErrorType) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.AIBridgeInterceptionErrorType), nil
}
func (e AIBridgeInterceptionErrorType) Valid() bool {
switch e {
case AibridgeInterceptionErrorTypeBadRequest,
AibridgeInterceptionErrorTypeUnauthorized,
AibridgeInterceptionErrorTypeRateLimited,
AibridgeInterceptionErrorTypeOverloaded,
AibridgeInterceptionErrorTypeServerError,
AibridgeInterceptionErrorTypeTimeout,
AibridgeInterceptionErrorTypeUnknown:
return true
}
return false
}
func AllAIBridgeInterceptionErrorTypeValues() []AIBridgeInterceptionErrorType {
return []AIBridgeInterceptionErrorType{
AibridgeInterceptionErrorTypeBadRequest,
AibridgeInterceptionErrorTypeUnauthorized,
AibridgeInterceptionErrorTypeRateLimited,
AibridgeInterceptionErrorTypeOverloaded,
AibridgeInterceptionErrorTypeServerError,
AibridgeInterceptionErrorTypeTimeout,
AibridgeInterceptionErrorTypeUnknown,
}
}
type AIProviderType string
const (
@@ -4570,6 +4643,10 @@ type AIBridgeInterception struct {
AgentFirewallSessionID uuid.NullUUID `db:"agent_firewall_session_id" json:"agent_firewall_session_id"`
// The Agent Firewall sequence number from the request header. Used to determine exact ordering of network requests relative to Agent Firewall audit events. NULL when the request did not pass through Agent Firewall.
AgentFirewallSequenceNumber sql.NullInt32 `db:"agent_firewall_sequence_number" json:"agent_firewall_sequence_number"`
// Categorised terminal upstream error for a failed interception; NULL when the interception succeeded.
ErrorType NullAIBridgeInterceptionErrorType `db:"error_type" json:"error_type"`
// Raw terminal upstream error message for a failed interception; NULL when the interception succeeded.
ErrorMessage sql.NullString `db:"error_message" json:"error_message"`
}
// Audit log of model thinking in intercepted requests in AI Bridge
+15 -5
View File
@@ -1158,7 +1158,7 @@ func (q *sqlQuerier) DeleteOldAIBridgeRecords(ctx context.Context, beforeTime ti
const getAIBridgeInterceptionByID = `-- name: GetAIBridgeInterceptionByID :one
SELECT
id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id, client, thread_parent_id, thread_root_id, client_session_id, session_id, provider_name, credential_kind, credential_hint, agent_firewall_session_id, agent_firewall_sequence_number
id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id, client, thread_parent_id, thread_root_id, client_session_id, session_id, provider_name, credential_kind, credential_hint, agent_firewall_session_id, agent_firewall_sequence_number, error_type, error_message
FROM
aibridge_interceptions
WHERE
@@ -1187,6 +1187,8 @@ func (q *sqlQuerier) GetAIBridgeInterceptionByID(ctx context.Context, id uuid.UU
&i.CredentialHint,
&i.AgentFirewallSessionID,
&i.AgentFirewallSequenceNumber,
&i.ErrorType,
&i.ErrorMessage,
)
return i, err
}
@@ -1221,7 +1223,7 @@ func (q *sqlQuerier) GetAIBridgeInterceptionLineageByToolCallID(ctx context.Cont
const getAIBridgeInterceptions = `-- name: GetAIBridgeInterceptions :many
SELECT
id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id, client, thread_parent_id, thread_root_id, client_session_id, session_id, provider_name, credential_kind, credential_hint, agent_firewall_session_id, agent_firewall_sequence_number
id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id, client, thread_parent_id, thread_root_id, client_session_id, session_id, provider_name, credential_kind, credential_hint, agent_firewall_session_id, agent_firewall_sequence_number, error_type, error_message
FROM
aibridge_interceptions
`
@@ -1254,6 +1256,8 @@ func (q *sqlQuerier) GetAIBridgeInterceptions(ctx context.Context) ([]AIBridgeIn
&i.CredentialHint,
&i.AgentFirewallSessionID,
&i.AgentFirewallSequenceNumber,
&i.ErrorType,
&i.ErrorMessage,
); err != nil {
return nil, err
}
@@ -1413,7 +1417,7 @@ INSERT INTO aibridge_interceptions (
) VALUES (
$1, $2, $3, $4, $5, $6, COALESCE($7::jsonb, '{}'::jsonb), $8, $9, $10, $11::uuid, $12::uuid, $13, $14, $15::uuid, $16
)
RETURNING id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id, client, thread_parent_id, thread_root_id, client_session_id, session_id, provider_name, credential_kind, credential_hint, agent_firewall_session_id, agent_firewall_sequence_number
RETURNING id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id, client, thread_parent_id, thread_root_id, client_session_id, session_id, provider_name, credential_kind, credential_hint, agent_firewall_session_id, agent_firewall_sequence_number, error_type, error_message
`
type InsertAIBridgeInterceptionParams struct {
@@ -1474,6 +1478,8 @@ func (q *sqlQuerier) InsertAIBridgeInterception(ctx context.Context, arg InsertA
&i.CredentialHint,
&i.AgentFirewallSessionID,
&i.AgentFirewallSequenceNumber,
&i.ErrorType,
&i.ErrorMessage,
)
return i, err
}
@@ -1904,7 +1910,7 @@ WITH paginated_threads AS (
)
SELECT
COALESCE(aibridge_interceptions.thread_root_id, aibridge_interceptions.id) AS thread_id,
aibridge_interceptions.id, aibridge_interceptions.initiator_id, aibridge_interceptions.provider, aibridge_interceptions.model, aibridge_interceptions.started_at, aibridge_interceptions.metadata, aibridge_interceptions.ended_at, aibridge_interceptions.api_key_id, aibridge_interceptions.client, aibridge_interceptions.thread_parent_id, aibridge_interceptions.thread_root_id, aibridge_interceptions.client_session_id, aibridge_interceptions.session_id, aibridge_interceptions.provider_name, aibridge_interceptions.credential_kind, aibridge_interceptions.credential_hint, aibridge_interceptions.agent_firewall_session_id, aibridge_interceptions.agent_firewall_sequence_number
aibridge_interceptions.id, aibridge_interceptions.initiator_id, aibridge_interceptions.provider, aibridge_interceptions.model, aibridge_interceptions.started_at, aibridge_interceptions.metadata, aibridge_interceptions.ended_at, aibridge_interceptions.api_key_id, aibridge_interceptions.client, aibridge_interceptions.thread_parent_id, aibridge_interceptions.thread_root_id, aibridge_interceptions.client_session_id, aibridge_interceptions.session_id, aibridge_interceptions.provider_name, aibridge_interceptions.credential_kind, aibridge_interceptions.credential_hint, aibridge_interceptions.agent_firewall_session_id, aibridge_interceptions.agent_firewall_sequence_number, aibridge_interceptions.error_type, aibridge_interceptions.error_message
FROM
aibridge_interceptions
JOIN
@@ -1970,6 +1976,8 @@ func (q *sqlQuerier) ListAIBridgeSessionThreads(ctx context.Context, arg ListAIB
&i.AIBridgeInterception.CredentialHint,
&i.AIBridgeInterception.AgentFirewallSessionID,
&i.AIBridgeInterception.AgentFirewallSequenceNumber,
&i.AIBridgeInterception.ErrorType,
&i.AIBridgeInterception.ErrorMessage,
); err != nil {
return nil, err
}
@@ -2398,7 +2406,7 @@ UPDATE aibridge_interceptions
WHERE
id = $3::uuid
AND ended_at IS NULL
RETURNING id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id, client, thread_parent_id, thread_root_id, client_session_id, session_id, provider_name, credential_kind, credential_hint, agent_firewall_session_id, agent_firewall_sequence_number
RETURNING id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id, client, thread_parent_id, thread_root_id, client_session_id, session_id, provider_name, credential_kind, credential_hint, agent_firewall_session_id, agent_firewall_sequence_number, error_type, error_message
`
type UpdateAIBridgeInterceptionEndedParams struct {
@@ -2429,6 +2437,8 @@ func (q *sqlQuerier) UpdateAIBridgeInterceptionEnded(ctx context.Context, arg Up
&i.CredentialHint,
&i.AgentFirewallSessionID,
&i.AgentFirewallSequenceNumber,
&i.ErrorType,
&i.ErrorMessage,
)
return i, err
}
+1
View File
@@ -261,6 +261,7 @@ sql:
latest_build_has_ai_task: LatestBuildHasAITask
cors_behavior: CorsBehavior
aibridge_interception: AIBridgeInterception
aibridge_interception_error_type: AIBridgeInterceptionErrorType
aibridge_tool_usage: AIBridgeToolUsage
aibridge_token_usage: AIBridgeTokenUsage
aibridge_user_prompt: AIBridgeUserPrompt