Skip to content

feat: byok-observability for aibridge#239

Merged
evgeniy-scherbina merged 18 commits intomainfrom
yevhenii/byok-observability
Apr 8, 2026
Merged

feat: byok-observability for aibridge#239
evgeniy-scherbina merged 18 commits intomainfrom
yevhenii/byok-observability

Conversation

@evgeniy-scherbina
Copy link
Copy Markdown
Contributor

@evgeniy-scherbina evgeniy-scherbina commented Mar 30, 2026

Summary

Records credential metadata on each AI Bridge interception, enabling admins to see how requests were authenticated and identify which credential was used.

Each provider's CreateInterceptor now captures two new fields on the interceptor:

  • credential_kind: centralized, personal_api_key, or subscription
  • credential_hint: masked credential for audit (e.g. sk-a...(13)...efgh)

Changes

  • Extended Interceptor interface with SetCredential, CredentialKind, and CredentialHint, backed by an embeddable CredentialFields helper
  • Each provider sets credential metadata in CreateInterceptor:
    • OpenAI: centralized (admin key) or personal (Bearer token)
    • Anthropic: centralized, personal_api_key (X-Api-Key), or subscription (Bearer token)
    • Copilot: always subscription
  • Improved MaskSecret to embed hidden character count (e.g. sk-a...(13)...efgh instead of sk-a*************efgh)

Relates to coder/coder#23808

@evgeniy-scherbina evgeniy-scherbina force-pushed the yevhenii/byok-observability branch from 9bf93dd to c5dd566 Compare March 30, 2026 22:37
@evgeniy-scherbina evgeniy-scherbina changed the base branch from yevhenii/aibridge-chatgpt-provider to ssncf/feat-validate-provider-names March 30, 2026 22:38
@ssncferreira ssncferreira force-pushed the ssncf/feat-validate-provider-names branch 2 times, most recently from 71c69d3 to 30d306a Compare March 31, 2026 10:48
@evgeniy-scherbina evgeniy-scherbina force-pushed the yevhenii/byok-observability branch from c5dd566 to d6e95ea Compare March 31, 2026 14:00
@ssncferreira ssncferreira force-pushed the ssncf/feat-validate-provider-names branch from 7d0c198 to b1c6f3f Compare March 31, 2026 14:36
@evgeniy-scherbina evgeniy-scherbina force-pushed the yevhenii/byok-observability branch from d6e95ea to b6a01e9 Compare March 31, 2026 16:47
@evgeniy-scherbina evgeniy-scherbina changed the base branch from ssncf/feat-validate-provider-names to main March 31, 2026 16:47
@evgeniy-scherbina evgeniy-scherbina force-pushed the yevhenii/byok-observability branch from e1e15df to b6a01e9 Compare March 31, 2026 17:38
@evgeniy-scherbina evgeniy-scherbina force-pushed the yevhenii/byok-observability branch from a610090 to d3e5994 Compare March 31, 2026 19:20
//
// In BYOK mode the user's credential is in Authorization. Replace
// the centralized key with it so it is forwarded upstream.
credKind := intercept.CredentialKindCentralized
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't setting credHint missing here? Also, we should probably check if cfg.key is empty (chatgpt case), and error if the credential key is empty.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't setting credHint missing here?

Actually - no, it's later used like this:

interceptor.SetCredential(credKind, utils.MaskSecret(cfg.Key))

It's easier for openai case, because authz header is always used.


if cfg.key is empty (chatgpt case), and error if the credential key is empty

Yeah, I remember about this. I can add in this PR.

Copy link
Copy Markdown
Contributor Author

@evgeniy-scherbina evgeniy-scherbina Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ssncferreira

if cfg.key is empty (chatgpt case), and error if the credential key is empty

We already know that claude (maybe other clients) send HEAD requests without authentication, maybe we don't need it?

Also I think it's not specific to chatgpt case.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My suggestion was to use a credSecret := cfg.Key like we do in Anthropic, instead of setting cfg.Key directly.

Regarding the empty key, I think this case can still happen, where we proceed to the interceptors with an empty key. This would likely be a misconfiguration, e.g., someone using the ChatGPT provider (no key configured) without an actual subscription. I assume in this case, the upstream provider would return a 401, but I think aibridge should catch this early and fail fast rather than sending a request it knows will fail. Claude Code already does this: it errors when no key is configured.

This applies to all providers, not just OpenAI, so I'm fine with addressing it separately 🙂

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My suggestion was to use a credSecret := cfg.Key like we do in Anthropic, instead of setting cfg.Key directly.

In Anthropic it's a bit different, we have 2 fields in config:

  • Key
  • BYOKBearerToken
    So I had to use credSecret := cfg.Key. In OpenAI implementation is easier.

Instead of setting cfg.Key

We set cfg.Key in Anthropic as well.

Regarding the empty key, I think this case can still happen, where we proceed to the interceptors with an empty key.

Theoretically it may be possible. My main concern that as we already know, request without authenticate maybe a valid request (e.g. ClaudeCode sending HEAD healthcheck request).
So I wasn't sure if we want to intervene and fail every request without authentication?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for now we need to assume that all requests that reach this point have been authenticated by coder (HEAD requests without auth do not get this far).
The case I was mentioning is the case where the user is correctly authenticated via coder, but not sending any authentication to the upstream provider. This is an edge case, and as we talked yesterday, we can address this after 🙂

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ssncferreira yeah, let's address later

The case I was mentioning is the case where the user is correctly authenticated via coder, but not sending any authentication to the upstream provider.

I meant the same. e.g. agent sent Coder-Token, but didn't send LLM token. I didn't want to fail such request, because it maybe valid request (e.g. healthcheck)?

@ssncferreira
Copy link
Copy Markdown
Contributor

I think we are missing adding the credential info to the logs, especially for error cases like 401/403/429

@evgeniy-scherbina
Copy link
Copy Markdown
Contributor Author

evgeniy-scherbina commented Apr 3, 2026

@ssncferreira

I think we are missing adding the credential info to the logs, especially for error cases like 401/403/429

I extended logger with credential info in newInterceptionProcessor where we call interceptor.ProcessRequest(rw, r).
It should capture all cases you mentioned.

//
// In BYOK mode the user's credential is in Authorization. Replace
// the centralized key with it so it is forwarded upstream.
credKind := intercept.CredentialKindCentralized
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My suggestion was to use a credSecret := cfg.Key like we do in Anthropic, instead of setting cfg.Key directly.

Regarding the empty key, I think this case can still happen, where we proceed to the interceptors with an empty key. This would likely be a misconfiguration, e.g., someone using the ChatGPT provider (no key configured) without an actual subscription. I assume in this case, the upstream provider would return a 401, but I think aibridge should catch this early and fail fast rather than sending a request it knows will fail. Claude Code already does this: it errors when no key is configured.

This applies to all providers, not just OpenAI, so I'm fine with addressing it separately 🙂

Copy link
Copy Markdown
Collaborator

@dannykopping dannykopping left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

{"long_api_key", "sk-ant-api03-abcdefgh", "sk-a*************efgh"},
{"unicode", "hélloworld🌍!", "hé********🌍!"},
{"github_token", "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh", "ghp_******************************efgh"},
{"short", "short", "***"},
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add a follow-up to make these more useful as previously discussed? Not sure what happened to my comment.

Copy link
Copy Markdown
Contributor Author

@evgeniy-scherbina evgeniy-scherbina Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added logging of secret_length as we discussed:

aibridge/bridge.go

Lines 249 to 250 in fefb191

slog.F("credential_hint", cred.Hint),
slog.F("credential_length", cred.Length),

On the call consensus was to use short format to avoid wasting DB space.

If you refer to the fact that whole secret is masked, it's by design? Because secret is too short to reveal smth. (shouldn't happen with real providers).

I think this was your original comment: #239 (comment)

But okay, I'll create follow-up PR on that.

Copy link
Copy Markdown
Contributor

@ssncferreira ssncferreira left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 🚀

//
// In BYOK mode the user's credential is in Authorization. Replace
// the centralized key with it so it is forwarded upstream.
credKind := intercept.CredentialKindCentralized
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for now we need to assume that all requests that reach this point have been authenticated by coder (HEAD requests without auth do not get this far).
The case I was mentioning is the case where the user is correctly authenticated via coder, but not sending any authentication to the upstream provider. This is an edge case, and as we talked yesterday, we can address this after 🙂

{"medium_15_chars", "thisisquitelong", "th***ng"},
{"long_api_key", "sk-ant-api03-abcdefgh", "sk-a***efgh"},
{"unicode", "hélloworld🌍!", "hé***🌍!"},
{"github_token", "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh", "ghp_***efgh"},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small nit: I prefer ... instead of ***. The latter is generally associated with censored words, which is not the case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay, I'll replace considering Anthropic and OpenAI use ..., and we use it here: https://github.com/coder/aibridge/blob/main/intercept/apidump/headers.go#L25

I remember @dannykopping proposed ***, so I kept ***.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

ExtraHeaders: extractCopilotHeaders(r),
}

cred := intercept.NewCredentialInfo(intercept.CredentialKindBYOK, key)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if it makes sense to add the github token here 🤔 I don't think the copilot case fits the reasoning for logging and debugging on the UI.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ssncferreira
I had very similar proposal, see: #216 (comment)

@evgeniy-scherbina evgeniy-scherbina merged commit f72a795 into main Apr 8, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants