Skip to content

Conversation

@akshaydeo
Copy link
Contributor

@akshaydeo akshaydeo commented Oct 8, 2025

Author

@hazyone

Summary

Add support for Anthropic OAuth passthrough mode to enable Claude Code integration. This allows Bifrost to handle OAuth tokens (sk-ant-oat-*) by preserving the original request structure and headers.

Changes

  • Added AnthropicPassthroughProvider to handle OAuth authentication mode
  • Implemented automatic detection between API key and OAuth token authentication
  • Added passthrough streaming support that preserves raw SSE events
  • Added context keys to store original request body, headers, and path
  • Modified response handling to support raw response forwarding
  • Updated HTTP handlers to detect and route Anthropic requests appropriately

Type of change

  • Feature
  • Bug fix
  • Refactor
  • Documentation
  • Chore/CI

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Providers/Integrations
  • Plugins
  • UI (Next.js)
  • Docs

How to test

Test with Claude Code by configuring it to use Bifrost as a proxy:

# Run Bifrost with debug logging
go run cmd/bifrost/main.go -debug

# Configure Claude Code to use Bifrost as a proxy
# Use an OAuth token (sk-ant-oat-*) with Claude Code
# Verify requests are properly passed through

Breaking changes

  • Yes
  • No

Related issues

Enables Claude Code integration with Bifrost

Security considerations

  • OAuth tokens are passed through without modification
  • Original request headers are preserved for authentication
  • No additional PII is stored beyond what's in the original request

Checklist

  • I read docs/contributing/README.md and followed the guidelines
  • I added/updated tests where appropriate
  • I updated documentation where needed
  • I verified builds succeed (Go and UI)
  • I verified the CI pipeline passes locally if applicable

hazyone and others added 12 commits October 1, 2025 01:26
# Conflicts:
#	core/schemas/bifrost.go
#	transports/bifrost-http/integrations/anthropic.go
#	transports/bifrost-http/integrations/anthropic/types.go
#	transports/bifrost-http/integrations/utils.go
Feature: Add antrhopic passthrough mode
Copy link
Contributor Author

akshaydeo commented Oct 8, 2025

This stack of pull requests is managed by Graphite. Learn more about stacking.

@akshaydeo akshaydeo marked this pull request as ready for review October 8, 2025 07:41
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 8, 2025

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added Anthropic OAuth passthrough provider with direct request/response forwarding.
    • Streaming passthrough preserves raw SSE events for full fidelity.
    • Expanded Anthropic endpoints support (messages and nested paths).
    • Enhanced proxy support: HTTP, SOCKS5, and environment-based proxies.
    • Automatic detection of API key vs OAuth and seamless provider switching.
  • Improvements

    • Forward response headers in passthrough (non-streaming and streaming).
    • More robust streaming: proper DONE marker handling and improved error propagation.
    • No API key required when using Anthropic OAuth passthrough.

Walkthrough

Adds Anthropic Passthrough provider and wiring: new provider implementation, factory registration, schema/context keys for original request data and raw SSE, proxy/transport utilities, HTTP transport updates to preserve headers/path/body for OAuth passthrough, and streaming handler changes to forward raw SSE events and headers.

Changes

Cohort / File(s) Summary
Anthropic Passthrough Provider
core/providers/anthropic_passthrough.go
New OAuth passthrough provider for Anthropic: forwards original headers/body/path, supports streaming with raw SSE forwarding and telemetry extraction; stubs unsupported ops; configures HTTP clients, timeouts, proxies, and error mapping.
Provider Utilities & Proxy
core/providers/utils.go
Adds proxy/transport configurators (HTTP, SOCKS5, env, none) and a helper to finalize passthrough stream end with post-hooks; includes validation and logging.
Schemas Updates
core/schemas/bifrost.go
Adds ModelProvider AnthropicPassthrough; new context keys for original request/body/headers/path; extends response extra fields with RawHeaders; extends stream with RawSSEEvent.
Core Factory & Key Rules
core/bifrost.go, core/utils.go
Registers AnthropicPassthrough in factory; streams now propagate RawSSEEvent on short-circuit; exempts AnthropicPassthrough from requiring API key.
HTTP Transport – Integrations
transports/bifrost-http/integrations/anthropic.go, transports/bifrost-http/integrations/utils.go
Introduces API key/OAuth detection and Anthropic route detection; captures original body/headers/path for passthrough; switches to AnthropicPassthrough for OAuth; updates success/streaming paths to handle raw bytes, headers, and RawSSEEvent; expands routes.
HTTP Transport – Streaming Handler
transports/bifrost-http/handlers/inference.go
Adds direct raw SSE write path with suppression of [DONE] marker when raw events are sent; preserves existing JSON SSE flow otherwise.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant HT as HTTP Transport
  participant BF as Bifrost Core
  participant PR as AnthropicPassthrough Provider
  participant A as Anthropic API

  rect rgba(230,240,255,0.6)
  note over HT: OAuth + Anthropic path detected
  C->>HT: POST /anthropic/v1/messages (OAuth)
  HT->>HT: Store original body/headers/path in context
  HT->>BF: Bifrost ChatRequest (Provider=AnthropicPassthrough)
  BF->>PR: ChatCompletion(ctx with originals)
  PR->>A: POST /v1/messages (forward headers/body)
  A-->>PR: Response (status, headers, body)
  PR-->>BF: BifrostResponse + RawHeaders
  BF-->>HT: Response passthrough bytes + headers
  HT-->>C: HTTP 200 with Anthropic headers/body
  end
Loading
sequenceDiagram
  autonumber
  participant C as Client
  participant HT as HTTP Transport (SSE)
  participant BF as Bifrost Core
  participant PR as AnthropicPassthrough Provider
  participant A as Anthropic SSE

  rect rgba(255,245,230,0.6)
  note over HT: Streaming passthrough with raw SSE
  C->>HT: POST (stream=true)
  HT->>BF: Stream request (ctx carries originals)
  BF->>PR: ChatCompletionStream
  PR-->>A: Open SSE stream (forward headers/body)
  A-->>PR: SSE events (raw)
  PR-->>BF: BifrostStream events (RawSSEEvent set)
  BF-->>HT: RawSSEEvent frames
  HT-->>C: Write raw SSE lines (no extra [DONE])
  PR->>PR: Collect usage/finish/message_id
  PR-->>BF: Final telemetry (no extra event emission)
  BF->>HT: Stream end (post-hooks)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • danpiths
  • Pratham-Mishra04

Poem

I hop through streams where Claudes now flow,
With headers intact and SSE’s glow.
I ferry each byte, no frills, no fuss—
A proxy warren built for us.
OAuth winds guide paths anew,
Passthrough done—hippity hooray, it’s true! 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (3 warnings)
Check name Status Explanation Resolution
Linked Issues Check ⚠️ Warning Linked issue #123 specifies adding Files API support (e.g., POST /v1/files) for file uploads and management, but this PR exclusively implements Anthropic OAuth passthrough behavior and streaming enhancements without introducing any file endpoint or file handling logic. Therefore, the changes do not satisfy the coding requirements of the linked issue. Please implement the file upload and management endpoints as outlined in issue #123, or remove the linkage if passthrough mode is outside the scope of that issue.
Out of Scope Changes Check ⚠️ Warning All modifications in this PR address OAuth passthrough for Anthropic rather than file API support, meaning none of the introduced functionality aligns with the objectives of issue #123. As a result, the entire PR falls outside the defined scope of the linked issue. Restrict this PR to file API support or split the Anthropic passthrough implementation into a separate PR so that each change set corresponds only to its linked issue’s objectives.
Description Check ⚠️ Warning The pull request description is just the unmodified template with no actual summary, change details, test instructions, or checklist items filled in, so it fails to convey the purpose, scope, and testing steps for this change. Please replace each template section with concrete information: add a concise summary of the PR’s goal and problem, list the specific changes and design decisions, select the change type and affected areas, provide detailed test steps (including any new configuration or environment variables), note any breaking changes or related issues, address security considerations, and complete the checklist to confirm tests, documentation, and CI status.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly indicates the introduction of an Anthropic passthrough mode, which directly reflects the primary functionality added in this changeset. It is concise, specific, and free of extraneous details, enabling a quick understanding of the main update. The phrasing aligns with the code changes around provider and streaming passthrough support.
Docstring Coverage ✅ Passed Docstring coverage is 84.62% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch anthropic-passthrough

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3873dfb and 19e5694.

📒 Files selected for processing (8)
  • core/bifrost.go (2 hunks)
  • core/providers/anthropic_passthrough.go (1 hunks)
  • core/providers/utils.go (4 hunks)
  • core/schemas/bifrost.go (4 hunks)
  • core/utils.go (1 hunks)
  • transports/bifrost-http/handlers/inference.go (2 hunks)
  • transports/bifrost-http/integrations/anthropic.go (2 hunks)
  • transports/bifrost-http/integrations/utils.go (9 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
core/utils.go (1)
core/schemas/bifrost.go (3)
  • Ollama (53-53)
  • SGL (55-55)
  • AnthropicPassthrough (48-48)
transports/bifrost-http/integrations/anthropic.go (3)
transports/bifrost-http/integrations/utils.go (3)
  • RouteConfig (142-153)
  • RequestConverter (81-81)
  • ResponseConverter (85-85)
core/schemas/providers/anthropic/types.go (1)
  • AnthropicMessageRequest (35-47)
core/schemas/bifrost.go (2)
  • BifrostRequest (15-27)
  • BifrostResponse (231-246)
transports/bifrost-http/integrations/utils.go (2)
core/schemas/bifrost.go (5)
  • Anthropic (47-47)
  • AnthropicPassthrough (48-48)
  • BifrostContextKeyOriginalRequest (116-116)
  • BifrostContextKeyOriginalHeaders (117-117)
  • BifrostContextKeyOriginalPath (118-118)
transports/bifrost-http/handlers/inference.go (1)
  • ChatRequest (165-169)
core/bifrost.go (2)
core/schemas/bifrost.go (1)
  • AnthropicPassthrough (48-48)
core/providers/anthropic_passthrough.go (1)
  • NewAnthropicPassthroughProvider (62-102)
core/schemas/bifrost.go (1)
ui/lib/types/config.ts (1)
  • ModelProvider (165-167)
core/providers/utils.go (4)
ui/lib/types/config.ts (1)
  • ProxyConfig (113-118)
core/schemas/provider.go (6)
  • ProxyConfig (79-84)
  • NoProxy (69-69)
  • HTTPProxy (71-71)
  • Socks5Proxy (73-73)
  • EnvProxy (75-75)
  • PostHookRunner (191-191)
core/schemas/logger.go (1)
  • Logger (28-55)
core/schemas/bifrost.go (2)
  • BifrostResponse (231-246)
  • BifrostContextKeyStreamEndIndicator (115-115)
core/providers/anthropic_passthrough.go (5)
core/schemas/logger.go (1)
  • Logger (28-55)
core/schemas/provider.go (5)
  • DefaultRequestTimeoutInSeconds (14-14)
  • ErrProviderJSONMarshaling (26-26)
  • PostHookRunner (191-191)
  • ErrProviderRequest (24-24)
  • DefaultStreamBufferSize (17-17)
core/schemas/bifrost.go (14)
  • ModelProvider (42-42)
  • AnthropicPassthrough (48-48)
  • BifrostError (537-546)
  • BifrostContextKeyOriginalRequest (116-116)
  • BifrostContextKeyOriginalPath (118-118)
  • BifrostContextKeyOriginalHeaders (117-117)
  • BifrostResponse (231-246)
  • BifrostChatRequest (182-188)
  • ChatCompletionRequest (95-95)
  • BifrostResponseExtraFields (490-500)
  • RequestType (90-90)
  • BifrostStream (525-529)
  • ChatCompletionStreamRequest (96-96)
  • LLMUsage (288-294)
core/schemas/providers/anthropic/types.go (2)
  • AnthropicMessageError (279-282)
  • AnthropicStreamEvent (233-241)
core/schemas/account.go (1)
  • Key (8-17)

Comment on lines +179 to +208
rawBytes := resp.Body()

if resp.StatusCode() != fasthttp.StatusOK {

var errorResp anthropic.AnthropicMessageError

bifrostErr := handleProviderAPIError(resp, &errorResp)
bifrostErr.Error.Type = &errorResp.Error.Type
bifrostErr.Error.Message = errorResp.Error.Message

return nil, nil, nil, bifrostErr
}

decodedBody := rawBytes
contentEncoding := string(resp.Header.Peek("Content-Encoding"))

if contentEncoding == "gzip" {
var err error
decodedBody, err = resp.BodyGunzip()
if err != nil {
return nil, nil, nil, newBifrostOperationError(schemas.ErrProviderJSONMarshaling, fmt.Errorf("failed to decompress gzip response: %w", err), provider.GetProviderKey())
}
}

httpHeaders := make(http.Header)
for k, v := range resp.Header.All() {
httpHeaders.Add(string(k), string(v))
}

return rawBytes, decodedBody, httpHeaders, nil
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Copy the fasthttp body before releasing the response.

fasthttp.AcquireResponse reuses its internal buffer once you Release it (which happens via the defer at Line 152). Returning rawBytes := resp.Body() hands callers a slice backed by that recycled buffer, so after the function returns the contents become undefined and can flip to unrelated data. Take a defensive copy of the body (and reuse that copy for decodedBody) before releasing the response object.

Apply this diff:

-	rawBytes := resp.Body()
-
-	decodedBody := rawBytes
+	rawBytes := append([]byte(nil), resp.Body()...)
+
+	decodedBody := rawBytes
 	contentEncoding := string(resp.Header.Peek("Content-Encoding"))

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In core/providers/anthropic_passthrough.go around lines 179 to 208, the code
returns resp.Body() which is a slice backed by fasthttp's internal buffer that
will be recycled when the response is released; make a defensive copy of the raw
body bytes immediately after reading resp.Body() (e.g., allocate a new slice and
copy into it) and use that copy for both rawBytes and as the starting point for
decodedBody (replace decodedBody with the copy, then overwrite it when
gunzipping into a new slice or reuse the copy as appropriate); ensure
httpHeaders construction remains the same and return the copied rawBytes and the
decodedBody copy so callers don't observe corrupted data after the response is
released.

Comment on lines +203 to +206
httpHeaders := make(http.Header)
for k, v := range resp.Header.All() {
httpHeaders.Add(string(k), string(v))
}
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 8, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Use VisitAll to materialize headers.

fasthttp.ResponseHeader doesn’t expose an All() helper, so this won’t compile. Iterate with VisitAll to populate the http.Header.

Apply this diff:

-	httpHeaders := make(http.Header)
-	for k, v := range resp.Header.All() {
-		httpHeaders.Add(string(k), string(v))
-	}
+	httpHeaders := make(http.Header)
+	resp.Header.VisitAll(func(k, v []byte) {
+		httpHeaders.Add(string(k), string(v))
+	})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
httpHeaders := make(http.Header)
for k, v := range resp.Header.All() {
httpHeaders.Add(string(k), string(v))
}
httpHeaders := make(http.Header)
resp.Header.VisitAll(func(k, v []byte) {
httpHeaders.Add(string(k), string(v))
})
🤖 Prompt for AI Agents
In core/providers/anthropic_passthrough.go around lines 203 to 206, the code
attempts to call resp.Header.All() which doesn't exist on
fasthttp.ResponseHeader; replace that iteration with
resp.Header.VisitAll(func(k, v []byte) { httpHeaders.Add(string(k), string(v))
}) so headers are materialized correctly into the http.Header (convert byte
slices to strings when adding).

Copy link

Choose a reason for hiding this comment

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

VisitAll deprecated

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🌐 Web query:

fasthttp ResponseHeader VisitAll deprecated alternative

💡 Result:

Use the new iterator/collection APIs instead — replace ResponseHeader.VisitAll with ResponseHeader.All. (Similarly: VisitAllInOrder → AllInOrder, VisitAllCookie → Cookies, VisitAllTrailer → Trailers.) [1]

Sources: fasthttp docs. [1] https://pkg.go.dev/github.com/valyala/fasthttp (see ResponseHeader methods).


@hazyone, you're absolutely right! I apologize for the confusion. VisitAll is indeed deprecated in favor of the newer iterator-based API. The original code is actually correct and should work as-is with modern versions of fasthttp:

httpHeaders := make(http.Header)
for k, v := range resp.Header.All() {
    httpHeaders.Add(string(k), string(v))
}

The All() method returns an iterator that can be used with Go's range loops (available in Go 1.23+), yielding byte slices for both keys and values. Please disregard my previous suggestion to use VisitAll.


(\\_/)
(•_•)  *scratches ear sheepishly*
/ > 📚

Comment on lines +250 to +254
for key, value := range ctx.Request.Header.All() {
keyStr := string(key)
headers[keyStr] = append(headers[keyStr], string(value))
}
return headers
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 8, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix: fasthttp header iteration uses VisitAll, not All().

fasthttp.RequestHeader has no All() method, so this addition breaks compilation. Use VisitAll (or similar) to collect headers instead of calling All().

-func extractHeaders(ctx *fasthttp.RequestCtx) map[string][]string {
-	headers := make(map[string][]string)
-	for key, value := range ctx.Request.Header.All() {
-		keyStr := string(key)
-		headers[keyStr] = append(headers[keyStr], string(value))
-	}
-	return headers
-}
+func extractHeaders(ctx *fasthttp.RequestCtx) map[string][]string {
+	headers := make(map[string][]string)
+	ctx.Request.Header.VisitAll(func(key, value []byte) {
+		k := string(key)
+		headers[k] = append(headers[k], string(value))
+	})
+	return headers
+}
🤖 Prompt for AI Agents
In transports/bifrost-http/integrations/utils.go around lines 250 to 254, the
code calls ctx.Request.Header.All() which does not exist on
fasthttp.RequestHeader and breaks compilation; replace the All() iteration with
Header.VisitAll and inside the VisitAll callback convert the key and value byte
slices to strings and append them into the headers map (headers[string(k)] =
append(headers[string(k)], string(v))) so all header occurrences are collected
correctly.

Copy link

Choose a reason for hiding this comment

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

VisitAll deprecated

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🌐 Web query:

fasthttp RequestHeader VisitAll deprecated alternative method

💡 Result:

Use the new iterator APIs (All / AllInOrder /Cookies /Trailers) instead of the deprecated VisitAll family. Example (RequestHeader):

  • RequestHeader.All() — iterate all headers (unordered)
  • RequestHeader.AllInOrder() — iterate headers in receive order
  • RequestHeader.Cookies() — replace VisitAllCookie
  • RequestHeader.Trailers() — replace VisitAllTrailer

Example usage:
h.All()(func(key, value []byte) bool {
// use key, value (copy if you need them after iteration)
return true // continue
})

(See fasthttp docs: RequestHeader.All / AllInOrder and deprecation note for VisitAll.) [1]

Sources
[1] fasthttp RequestHeader docs (All, AllInOrder, VisitAll deprecated).


@hazyone, you're absolutely right! VisitAll is deprecated. The original code in the PR was actually attempting to use the newer All() method, but with incorrect syntax.

The All() method returns an iterator function, so it needs to be called with a callback. Here's the corrected implementation:

func extractHeaders(ctx *fasthttp.RequestCtx) map[string][]string {
	headers := make(map[string][]string)
	ctx.Request.Header.All()(func(key, value []byte) bool {
		k := string(key)
		headers[k] = append(headers[k], string(value))
		return true // continue iteration
	})
	return headers
}

The key difference is that All() returns an iterator function, which you then immediately call with a callback function that receives key and value as byte slices. The callback should return true to continue iteration.

@akshaydeo
Copy link
Contributor Author

akshaydeo commented Oct 8, 2025

@hazyone apologies - when I was testing out we realized that we dont need this change - as we already have integrations in place (unlike passthrough requirement of litellm).

@Pratham-Mishra04 will share the video but to use Bifrost with Claude code for example

just change set env variable ANTHROPIC_BASE_URL=http(s)://<bifrost_base_url>/anthropic and it works.

He is currently fixing some logging issues

@Pratham-Mishra04
Copy link
Collaborator

Hey @hazyone, here's the video for Claude code setup - https://www.loom.com/share/5ab9a2354eec46998c0510dc0100fbf3?sid=1b65d658-b361-4c6a-998f-4d11f1e2e344

We have a few patches going in with #579 on a new version v1.3.0-prerelease5. Please give it a try and lmk if you're facing any issues

@hazyone
Copy link

hazyone commented Oct 8, 2025

@Pratham-Mishra04 I guess this would work only in case of using Anthropic API key. My PR is about using their MAX subscription, because they have Authorization with oauth2 and slightly different payloads.

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.

4 participants