diff --git a/bridge.go b/bridge.go index 4d79fba..21f4b6c 100644 --- a/bridge.go +++ b/bridge.go @@ -217,6 +217,7 @@ func newInterceptionProcessor(p provider.Provider, cbs *circuitbreaker.ProviderC asyncRecorder.WithClient(string(client)) interceptor.Setup(logger, asyncRecorder, mcpProxy) + cred := interceptor.Credential() if err := rec.RecordInterception(ctx, &recorder.InterceptionRecord{ ID: interceptor.ID().String(), InitiatorID: actor.ID, @@ -228,6 +229,8 @@ func newInterceptionProcessor(p provider.Provider, cbs *circuitbreaker.ProviderC Client: string(client), ClientSessionID: sessionID, CorrelatingToolCallID: interceptor.CorrelatingToolCallID(), + CredentialKind: string(cred.Kind), + CredentialHint: cred.Hint, }); err != nil { span.SetStatus(codes.Error, fmt.Sprintf("failed to record interception: %v", err)) logger.Warn(ctx, "failed to record interception", slog.Error(err)) @@ -242,6 +245,8 @@ func newInterceptionProcessor(p provider.Provider, cbs *circuitbreaker.ProviderC slog.F("interception_id", interceptor.ID()), slog.F("user_agent", r.UserAgent()), slog.F("streaming", interceptor.Streaming()), + slog.F("credential_kind", string(cred.Kind)), + slog.F("credential_hint", cred.Hint), ) log.Debug(ctx, "interception started") diff --git a/intercept/chatcompletions/base.go b/intercept/chatcompletions/base.go index 9c784e9..7569113 100644 --- a/intercept/chatcompletions/base.go +++ b/intercept/chatcompletions/base.go @@ -38,8 +38,9 @@ type interceptionBase struct { logger slog.Logger tracer trace.Tracer - recorder recorder.Recorder - mcpProxy mcp.ServerProxier + recorder recorder.Recorder + mcpProxy mcp.ServerProxier + credential intercept.CredentialInfo } func (i *interceptionBase) newCompletionsService() openai.ChatCompletionService { @@ -74,6 +75,10 @@ func (i *interceptionBase) ID() uuid.UUID { return i.id } +func (i *interceptionBase) Credential() intercept.CredentialInfo { + return i.credential +} + func (i *interceptionBase) Setup(logger slog.Logger, recorder recorder.Recorder, mcpProxy mcp.ServerProxier) { i.logger = logger i.recorder = recorder diff --git a/intercept/chatcompletions/blocking.go b/intercept/chatcompletions/blocking.go index f289ac9..532addd 100644 --- a/intercept/chatcompletions/blocking.go +++ b/intercept/chatcompletions/blocking.go @@ -36,6 +36,7 @@ func NewBlockingInterceptor( clientHeaders http.Header, authHeaderName string, tracer trace.Tracer, + cred intercept.CredentialInfo, ) *BlockingInterception { return &BlockingInterception{interceptionBase: interceptionBase{ id: id, @@ -45,6 +46,7 @@ func NewBlockingInterceptor( clientHeaders: clientHeaders, authHeaderName: authHeaderName, tracer: tracer, + credential: cred, }} } diff --git a/intercept/chatcompletions/streaming.go b/intercept/chatcompletions/streaming.go index c2d2a39..d7a5485 100644 --- a/intercept/chatcompletions/streaming.go +++ b/intercept/chatcompletions/streaming.go @@ -41,6 +41,7 @@ func NewStreamingInterceptor( clientHeaders http.Header, authHeaderName string, tracer trace.Tracer, + cred intercept.CredentialInfo, ) *StreamingInterception { return &StreamingInterception{interceptionBase: interceptionBase{ id: id, @@ -50,6 +51,7 @@ func NewStreamingInterceptor( clientHeaders: clientHeaders, authHeaderName: authHeaderName, tracer: tracer, + credential: cred, }} } diff --git a/intercept/chatcompletions/streaming_test.go b/intercept/chatcompletions/streaming_test.go index 54c4733..ee27f43 100644 --- a/intercept/chatcompletions/streaming_test.go +++ b/intercept/chatcompletions/streaming_test.go @@ -9,6 +9,7 @@ import ( "cdr.dev/slog/v3" "cdr.dev/slog/v3/sloggers/slogtest" "github.com/coder/aibridge/config" + "github.com/coder/aibridge/intercept" "github.com/coder/aibridge/internal/testutil" "github.com/google/uuid" "github.com/openai/openai-go/v3" @@ -86,7 +87,7 @@ func TestStreamingInterception_RelaysUpstreamErrorToClient(t *testing.T) { httpReq := httptest.NewRequest(http.MethodPost, "/chat/completions", nil) tracer := otel.Tracer("test") - interceptor := NewStreamingInterceptor(uuid.New(), req, config.ProviderOpenAI, cfg, httpReq.Header, "Authorization", tracer) + interceptor := NewStreamingInterceptor(uuid.New(), req, config.ProviderOpenAI, cfg, httpReq.Header, "Authorization", tracer, intercept.CredentialInfo{}) logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: false}).Leveled(slog.LevelDebug) interceptor.Setup(logger, &testutil.MockRecorder{}, nil) diff --git a/intercept/credential.go b/intercept/credential.go new file mode 100644 index 0000000..e652c21 --- /dev/null +++ b/intercept/credential.go @@ -0,0 +1,30 @@ +package intercept + +import "github.com/coder/aibridge/utils" + +// CredentialKind identifies how a request was authenticated. +// Keep in sync with the credential_kind enum in coderd's database. +type CredentialKind string + +// Credential kind constants for interception recording. +const ( + CredentialKindCentralized CredentialKind = "centralized" + CredentialKindPersonalAPIKey CredentialKind = "byok_api_key" + CredentialKindSubscription CredentialKind = "byok_subscription" +) + +// CredentialInfo holds credential metadata for an interception. +type CredentialInfo struct { + Kind CredentialKind + Hint string +} + +// NewCredentialInfo creates a CredentialInfo from a raw credential. +// The credential is automatically masked before storage so that the +// original secret is never retained. +func NewCredentialInfo(kind CredentialKind, credential string) CredentialInfo { + return CredentialInfo{ + Kind: kind, + Hint: utils.MaskSecret(credential), + } +} diff --git a/intercept/interceptor.go b/intercept/interceptor.go index cbd29d6..8b95428 100644 --- a/intercept/interceptor.go +++ b/intercept/interceptor.go @@ -25,6 +25,8 @@ type Interceptor interface { Streaming() bool // TraceAttributes returns tracing attributes for this [Interceptor] TraceAttributes(*http.Request) []attribute.KeyValue + // Credential returns the credential metadata for this interception. + Credential() CredentialInfo // CorrelatingToolCallID returns the ID of a tool call result submitted // in the request, if present. This is used to correlate the current // interception back to the previous interception that issued those tool diff --git a/intercept/messages/base.go b/intercept/messages/base.go index ccbd91b..a1458b0 100644 --- a/intercept/messages/base.go +++ b/intercept/messages/base.go @@ -77,14 +77,19 @@ type interceptionBase struct { tracer trace.Tracer logger slog.Logger - recorder recorder.Recorder - mcpProxy mcp.ServerProxier + recorder recorder.Recorder + mcpProxy mcp.ServerProxier + credential intercept.CredentialInfo } func (i *interceptionBase) ID() uuid.UUID { return i.id } +func (i *interceptionBase) Credential() intercept.CredentialInfo { + return i.credential +} + func (i *interceptionBase) Setup(logger slog.Logger, recorder recorder.Recorder, mcpProxy mcp.ServerProxier) { i.logger = logger i.recorder = recorder diff --git a/intercept/messages/blocking.go b/intercept/messages/blocking.go index da5526a..f83f218 100644 --- a/intercept/messages/blocking.go +++ b/intercept/messages/blocking.go @@ -37,6 +37,7 @@ func NewBlockingInterceptor( clientHeaders http.Header, authHeaderName string, tracer trace.Tracer, + cred intercept.CredentialInfo, ) *BlockingInterception { return &BlockingInterception{interceptionBase: interceptionBase{ id: id, @@ -47,6 +48,7 @@ func NewBlockingInterceptor( clientHeaders: clientHeaders, authHeaderName: authHeaderName, tracer: tracer, + credential: cred, }} } diff --git a/intercept/messages/streaming.go b/intercept/messages/streaming.go index 7fed361..760313e 100644 --- a/intercept/messages/streaming.go +++ b/intercept/messages/streaming.go @@ -43,6 +43,7 @@ func NewStreamingInterceptor( clientHeaders http.Header, authHeaderName string, tracer trace.Tracer, + cred intercept.CredentialInfo, ) *StreamingInterception { return &StreamingInterception{interceptionBase: interceptionBase{ id: id, @@ -53,6 +54,7 @@ func NewStreamingInterceptor( clientHeaders: clientHeaders, authHeaderName: authHeaderName, tracer: tracer, + credential: cred, }} } diff --git a/intercept/responses/base.go b/intercept/responses/base.go index 1dc0e8d..9949009 100644 --- a/intercept/responses/base.go +++ b/intercept/responses/base.go @@ -47,8 +47,9 @@ type responsesInterceptionBase struct { recorder recorder.Recorder mcpProxy mcp.ServerProxier - logger slog.Logger - tracer trace.Tracer + logger slog.Logger + tracer trace.Tracer + credential intercept.CredentialInfo } func (i *responsesInterceptionBase) newResponsesService() responses.ResponseService { @@ -83,6 +84,10 @@ func (i *responsesInterceptionBase) ID() uuid.UUID { return i.id } +func (i *responsesInterceptionBase) Credential() intercept.CredentialInfo { + return i.credential +} + func (i *responsesInterceptionBase) Setup(logger slog.Logger, recorder recorder.Recorder, mcpProxy mcp.ServerProxier) { i.logger = logger.With(slog.F("model", i.Model())) i.recorder = recorder diff --git a/intercept/responses/blocking.go b/intercept/responses/blocking.go index d64adf9..9d263de 100644 --- a/intercept/responses/blocking.go +++ b/intercept/responses/blocking.go @@ -33,6 +33,7 @@ func NewBlockingInterceptor( clientHeaders http.Header, authHeaderName string, tracer trace.Tracer, + cred intercept.CredentialInfo, ) *BlockingResponsesInterceptor { return &BlockingResponsesInterceptor{ responsesInterceptionBase: responsesInterceptionBase{ @@ -43,6 +44,7 @@ func NewBlockingInterceptor( clientHeaders: clientHeaders, authHeaderName: authHeaderName, tracer: tracer, + credential: cred, }, } } diff --git a/intercept/responses/streaming.go b/intercept/responses/streaming.go index 359f82e..0c692f8 100644 --- a/intercept/responses/streaming.go +++ b/intercept/responses/streaming.go @@ -40,6 +40,7 @@ func NewStreamingInterceptor( clientHeaders http.Header, authHeaderName string, tracer trace.Tracer, + cred intercept.CredentialInfo, ) *StreamingResponsesInterceptor { return &StreamingResponsesInterceptor{ responsesInterceptionBase: responsesInterceptionBase{ @@ -50,6 +51,7 @@ func NewStreamingInterceptor( clientHeaders: clientHeaders, authHeaderName: authHeaderName, tracer: tracer, + credential: cred, }, } } diff --git a/provider/anthropic.go b/provider/anthropic.go index 2800220..a4153d6 100644 --- a/provider/anthropic.go +++ b/provider/anthropic.go @@ -130,21 +130,29 @@ func (p *Anthropic) CreateInterceptor(w http.ResponseWriter, r *http.Request, tr // set BYOKBearerToken and clear the centralized key. // When both are present, X-Api-Key takes priority to match // claude-code behavior. + credKind := intercept.CredentialKindCentralized + credSecret := cfg.Key authHeaderName := p.AuthHeader() if apiKey := r.Header.Get("X-Api-Key"); apiKey != "" { cfg.Key = apiKey authHeaderName = "X-Api-Key" + credKind = intercept.CredentialKindPersonalAPIKey + credSecret = apiKey } else if token := utils.ExtractBearerToken(r.Header.Get("Authorization")); token != "" { cfg.BYOKBearerToken = token cfg.Key = "" authHeaderName = "Authorization" + credKind = intercept.CredentialKindSubscription + credSecret = token } + cred := intercept.NewCredentialInfo(credKind, credSecret) + var interceptor intercept.Interceptor if reqPayload.Stream() { - interceptor = messages.NewStreamingInterceptor(id, reqPayload, p.Name(), cfg, p.bedrockCfg, r.Header, authHeaderName, tracer) + interceptor = messages.NewStreamingInterceptor(id, reqPayload, p.Name(), cfg, p.bedrockCfg, r.Header, authHeaderName, tracer, cred) } else { - interceptor = messages.NewBlockingInterceptor(id, reqPayload, p.Name(), cfg, p.bedrockCfg, r.Header, authHeaderName, tracer) + interceptor = messages.NewBlockingInterceptor(id, reqPayload, p.Name(), cfg, p.bedrockCfg, r.Header, authHeaderName, tracer, cred) } span.SetAttributes(interceptor.TraceAttributes(r)...) return interceptor, nil diff --git a/provider/anthropic_test.go b/provider/anthropic_test.go index 3269c33..1ff9b16 100644 --- a/provider/anthropic_test.go +++ b/provider/anthropic_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/coder/aibridge/config" + "github.com/coder/aibridge/intercept" "github.com/coder/aibridge/internal/testutil" ) @@ -163,25 +164,33 @@ func TestAnthropic_CreateInterceptor_BYOK(t *testing.T) { t.Parallel() tests := []struct { - name string - setHeaders map[string]string - wantXApiKey string - wantAuthorization string + name string + setHeaders map[string]string + wantXApiKey string + wantAuthorization string + wantCredentialKind intercept.CredentialKind + wantCredentialHint string }{ { - name: "Messages_BYOK_BearerToken", - setHeaders: map[string]string{"Authorization": "Bearer user-access-token"}, - wantAuthorization: "Bearer user-access-token", + name: "Messages_BYOK_BearerToken", + setHeaders: map[string]string{"Authorization": "Bearer user-access-token"}, + wantAuthorization: "Bearer user-access-token", + wantCredentialKind: intercept.CredentialKindSubscription, + wantCredentialHint: "us*************en", }, { - name: "Messages_BYOK_APIKey", - setHeaders: map[string]string{"X-Api-Key": "user-api-key"}, - wantXApiKey: "user-api-key", + name: "Messages_BYOK_APIKey", + setHeaders: map[string]string{"X-Api-Key": "user-api-key"}, + wantXApiKey: "user-api-key", + wantCredentialKind: intercept.CredentialKindPersonalAPIKey, + wantCredentialHint: "us********ey", }, { - name: "Messages_Centralized_UsesCentralizedKey", - setHeaders: map[string]string{}, - wantXApiKey: "test-key", + name: "Messages_Centralized_UsesCentralizedKey", + setHeaders: map[string]string{}, + wantXApiKey: "test-key", + wantCredentialKind: intercept.CredentialKindCentralized, + wantCredentialHint: "********", }, { name: "Messages_BYOK_BearerToken_And_APIKey", @@ -189,7 +198,9 @@ func TestAnthropic_CreateInterceptor_BYOK(t *testing.T) { "Authorization": "Bearer user-access-token", "X-Api-Key": "user-api-key", }, - wantXApiKey: "user-api-key", + wantXApiKey: "user-api-key", + wantCredentialKind: intercept.CredentialKindPersonalAPIKey, + wantCredentialHint: "us********ey", }, } @@ -223,6 +234,10 @@ func TestAnthropic_CreateInterceptor_BYOK(t *testing.T) { require.NoError(t, err) require.NotNil(t, interceptor) + cred := interceptor.Credential() + assert.Equal(t, tc.wantCredentialKind, cred.Kind, "credential kind mismatch") + assert.Equal(t, tc.wantCredentialHint, cred.Hint, "credential hint mismatch") + logger := slog.Make() interceptor.Setup(logger, &testutil.MockRecorder{}, nil) diff --git a/provider/copilot.go b/provider/copilot.go index eeeb74d..a9675f1 100644 --- a/provider/copilot.go +++ b/provider/copilot.go @@ -145,6 +145,8 @@ func (p *Copilot) CreateInterceptor(_ http.ResponseWriter, r *http.Request, trac ExtraHeaders: extractCopilotHeaders(r), } + cred := intercept.NewCredentialInfo(intercept.CredentialKindSubscription, key) + var interceptor intercept.Interceptor path := strings.TrimPrefix(r.URL.Path, p.RoutePrefix()) @@ -156,9 +158,9 @@ func (p *Copilot) CreateInterceptor(_ http.ResponseWriter, r *http.Request, trac } if req.Stream { - interceptor = chatcompletions.NewStreamingInterceptor(id, &req, p.Name(), cfg, r.Header, p.AuthHeader(), tracer) + interceptor = chatcompletions.NewStreamingInterceptor(id, &req, p.Name(), cfg, r.Header, p.AuthHeader(), tracer, cred) } else { - interceptor = chatcompletions.NewBlockingInterceptor(id, &req, p.Name(), cfg, r.Header, p.AuthHeader(), tracer) + interceptor = chatcompletions.NewBlockingInterceptor(id, &req, p.Name(), cfg, r.Header, p.AuthHeader(), tracer, cred) } case routeCopilotResponses: @@ -172,9 +174,9 @@ func (p *Copilot) CreateInterceptor(_ http.ResponseWriter, r *http.Request, trac } if reqPayload.Stream() { - interceptor = responses.NewStreamingInterceptor(id, reqPayload, p.Name(), cfg, r.Header, p.AuthHeader(), tracer) + interceptor = responses.NewStreamingInterceptor(id, reqPayload, p.Name(), cfg, r.Header, p.AuthHeader(), tracer, cred) } else { - interceptor = responses.NewBlockingInterceptor(id, reqPayload, p.Name(), cfg, r.Header, p.AuthHeader(), tracer) + interceptor = responses.NewBlockingInterceptor(id, reqPayload, p.Name(), cfg, r.Header, p.AuthHeader(), tracer, cred) } default: diff --git a/provider/openai.go b/provider/openai.go index 1cd8591..0352891 100644 --- a/provider/openai.go +++ b/provider/openai.go @@ -113,9 +113,12 @@ func (p *OpenAI) CreateInterceptor(w http.ResponseWriter, r *http.Request, trace // // In BYOK mode the user's credential is in Authorization. Replace // the centralized key with it so it is forwarded upstream. + credKind := intercept.CredentialKindCentralized if token := utils.ExtractBearerToken(r.Header.Get("Authorization")); token != "" { cfg.Key = token + credKind = intercept.CredentialKindPersonalAPIKey } + cred := intercept.NewCredentialInfo(credKind, cfg.Key) path := strings.TrimPrefix(r.URL.Path, p.RoutePrefix()) switch path { @@ -126,9 +129,9 @@ func (p *OpenAI) CreateInterceptor(w http.ResponseWriter, r *http.Request, trace } if req.Stream { - interceptor = chatcompletions.NewStreamingInterceptor(id, &req, p.Name(), cfg, r.Header, p.AuthHeader(), tracer) + interceptor = chatcompletions.NewStreamingInterceptor(id, &req, p.Name(), cfg, r.Header, p.AuthHeader(), tracer, cred) } else { - interceptor = chatcompletions.NewBlockingInterceptor(id, &req, p.Name(), cfg, r.Header, p.AuthHeader(), tracer) + interceptor = chatcompletions.NewBlockingInterceptor(id, &req, p.Name(), cfg, r.Header, p.AuthHeader(), tracer, cred) } case routeResponses: @@ -141,9 +144,9 @@ func (p *OpenAI) CreateInterceptor(w http.ResponseWriter, r *http.Request, trace return nil, fmt.Errorf("unmarshal request body: %w", err) } if reqPayload.Stream() { - interceptor = responses.NewStreamingInterceptor(id, reqPayload, p.Name(), cfg, r.Header, p.AuthHeader(), tracer) + interceptor = responses.NewStreamingInterceptor(id, reqPayload, p.Name(), cfg, r.Header, p.AuthHeader(), tracer, cred) } else { - interceptor = responses.NewBlockingInterceptor(id, reqPayload, p.Name(), cfg, r.Header, p.AuthHeader(), tracer) + interceptor = responses.NewBlockingInterceptor(id, reqPayload, p.Name(), cfg, r.Header, p.AuthHeader(), tracer, cred) } default: diff --git a/provider/openai_test.go b/provider/openai_test.go index dcdd283..d06079d 100644 --- a/provider/openai_test.go +++ b/provider/openai_test.go @@ -11,6 +11,7 @@ import ( "cdr.dev/slog/v3" "github.com/coder/aibridge/config" + "github.com/coder/aibridge/intercept" "github.com/coder/aibridge/internal/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -197,44 +198,54 @@ func TestOpenAI_CreateInterceptor(t *testing.T) { t.Parallel() tests := []struct { - name string - route string - requestBody string - responseBody string - setHeaders map[string]string - wantAuthorization string + name string + route string + requestBody string + responseBody string + setHeaders map[string]string + wantAuthorization string + wantCredentialKind intercept.CredentialKind + wantCredentialHint string }{ { - name: "ChatCompletions_BYOK", - route: routeChatCompletions, - requestBody: `{"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}], "stream": false}`, - responseBody: chatCompletionResponse, - setHeaders: map[string]string{"Authorization": "Bearer user-token"}, - wantAuthorization: "Bearer user-token", + name: "ChatCompletions_BYOK", + route: routeChatCompletions, + requestBody: `{"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}], "stream": false}`, + responseBody: chatCompletionResponse, + setHeaders: map[string]string{"Authorization": "Bearer user-token"}, + wantAuthorization: "Bearer user-token", + wantCredentialKind: intercept.CredentialKindPersonalAPIKey, + wantCredentialHint: "us******en", }, { - name: "ChatCompletions_Centralized", - route: routeChatCompletions, - requestBody: `{"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}], "stream": false}`, - responseBody: chatCompletionResponse, - setHeaders: map[string]string{}, - wantAuthorization: "Bearer centralized-key", + name: "ChatCompletions_Centralized", + route: routeChatCompletions, + requestBody: `{"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}], "stream": false}`, + responseBody: chatCompletionResponse, + setHeaders: map[string]string{}, + wantAuthorization: "Bearer centralized-key", + wantCredentialKind: intercept.CredentialKindCentralized, + wantCredentialHint: "ce***********ey", }, { - name: "Responses_BYOK", - route: routeResponses, - requestBody: `{"model": "gpt-5", "input": "hello", "stream": false}`, - responseBody: responsesAPIResponse, - setHeaders: map[string]string{"Authorization": "Bearer user-token"}, - wantAuthorization: "Bearer user-token", + name: "Responses_BYOK", + route: routeResponses, + requestBody: `{"model": "gpt-5", "input": "hello", "stream": false}`, + responseBody: responsesAPIResponse, + setHeaders: map[string]string{"Authorization": "Bearer user-token"}, + wantAuthorization: "Bearer user-token", + wantCredentialKind: intercept.CredentialKindPersonalAPIKey, + wantCredentialHint: "us******en", }, { - name: "Responses_Centralized", - route: routeResponses, - requestBody: `{"model": "gpt-5", "input": "hello", "stream": false}`, - responseBody: responsesAPIResponse, - setHeaders: map[string]string{}, - wantAuthorization: "Bearer centralized-key", + name: "Responses_Centralized", + route: routeResponses, + requestBody: `{"model": "gpt-5", "input": "hello", "stream": false}`, + responseBody: responsesAPIResponse, + setHeaders: map[string]string{}, + wantAuthorization: "Bearer centralized-key", + wantCredentialKind: intercept.CredentialKindCentralized, + wantCredentialHint: "ce***********ey", }, // X-Api-Key should not appear in production since clients use Authorization, // but ensure it is stripped if it does arrive. @@ -247,7 +258,9 @@ func TestOpenAI_CreateInterceptor(t *testing.T) { "Authorization": "Bearer user-token", "X-Api-Key": "some-key", }, - wantAuthorization: "Bearer user-token", + wantAuthorization: "Bearer user-token", + wantCredentialKind: intercept.CredentialKindPersonalAPIKey, + wantCredentialHint: "us******en", }, { name: "Responses_BYOK_XApiKeyStripped", @@ -258,7 +271,9 @@ func TestOpenAI_CreateInterceptor(t *testing.T) { "Authorization": "Bearer user-token", "X-Api-Key": "some-key", }, - wantAuthorization: "Bearer user-token", + wantAuthorization: "Bearer user-token", + wantCredentialKind: intercept.CredentialKindPersonalAPIKey, + wantCredentialHint: "us******en", }, } @@ -292,6 +307,10 @@ func TestOpenAI_CreateInterceptor(t *testing.T) { require.NoError(t, err) require.NotNil(t, interceptor) + cred := interceptor.Credential() + assert.Equal(t, tc.wantCredentialKind, cred.Kind, "credential kind mismatch") + assert.Equal(t, tc.wantCredentialHint, cred.Hint, "credential hint mismatch") + logger := slog.Make() interceptor.Setup(logger, &testutil.MockRecorder{}, nil) diff --git a/recorder/types.go b/recorder/types.go index 9983dbd..cd541ee 100644 --- a/recorder/types.go +++ b/recorder/types.go @@ -39,6 +39,8 @@ type InterceptionRecord struct { Client string UserAgent string CorrelatingToolCallID *string + CredentialKind string + CredentialHint string } type InterceptionRecordEnded struct {