diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index ed1680dec4..1cd1ce11ef 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -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, diff --git a/coderd/database/migrations/000541_aibridge_interception_error.down.sql b/coderd/database/migrations/000541_aibridge_interception_error.down.sql new file mode 100644 index 0000000000..3bf6377ded --- /dev/null +++ b/coderd/database/migrations/000541_aibridge_interception_error.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE aibridge_interceptions + DROP COLUMN error_type, + DROP COLUMN error_message; + +DROP TYPE aibridge_interception_error_type; diff --git a/coderd/database/migrations/000541_aibridge_interception_error.up.sql b/coderd/database/migrations/000541_aibridge_interception_error.up.sql new file mode 100644 index 0000000000..d2783dad41 --- /dev/null +++ b/coderd/database/migrations/000541_aibridge_interception_error.up.sql @@ -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.'; diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index e5618d5564..4e8af395bc 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -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 } diff --git a/coderd/database/models.go b/coderd/database/models.go index 5f91bd7c37..d5f9434f62 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -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 diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 451a233abc..16b5c89bea 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -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 } diff --git a/coderd/database/sqlc.yaml b/coderd/database/sqlc.yaml index 42ba273df3..3090fb31a7 100644 --- a/coderd/database/sqlc.yaml +++ b/coderd/database/sqlc.yaml @@ -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