Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,8 @@ func newInterceptionProcessor(p provider.Provider, cbs *circuitbreaker.ProviderC
InitiatorID: actor.ID,
Metadata: actor.Metadata,
Model: interceptor.Model(),
Provider: p.Name(),
Provider: p.Type(),
ProviderName: p.Name(),
Comment on lines +201 to +202
Copy link
Copy Markdown
Contributor Author

@ssncferreira ssncferreira Mar 30, 2026

Choose a reason for hiding this comment

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

Keeping the provider type ("anthropic", "openai", "copilot") so interceptions preserve this value and we are still easily able to aggregate by provider. The ProviderName field provides the specific instance identity. Alternatively, we could store the upstream URL instead.

Examples:

  • Anthropic: Provider: anthropic, ProviderName: anthropic
  • Copilot: Provider: copilot, ProviderName: copilot
  • Copilot Business: Provider: copilot, ProviderName: copilot-business
  • Copilot Enterprise: Provider: copilot, ProviderName: copilot-enterprise

UserAgent: r.UserAgent(),
Client: string(client),
ClientSessionID: sessionID,
Expand Down
18 changes: 12 additions & 6 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const (
)

type Anthropic struct {
// Name is the provider instance name. If empty, defaults to "anthropic".
Name string
BaseURL string
Key string
APIDumpDir string
Expand All @@ -32,6 +34,8 @@ type AWSBedrock struct {
}

type OpenAI struct {
// Name is the provider instance name. If empty, defaults to "openai".
Name string
BaseURL string
Key string
APIDumpDir string
Expand All @@ -40,6 +44,14 @@ type OpenAI struct {
ExtraHeaders map[string]string
}

type Copilot struct {
// Name is the provider instance name. If empty, defaults to "copilot".
Name string
BaseURL string
APIDumpDir string
CircuitBreaker *CircuitBreaker
}

// CircuitBreaker holds configuration for circuit breakers.
type CircuitBreaker struct {
// MaxRequests is the maximum number of requests allowed in half-open state.
Expand Down Expand Up @@ -67,9 +79,3 @@ func DefaultCircuitBreaker() CircuitBreaker {
MaxRequests: 3,
}
}

type Copilot struct {
BaseURL string
APIDumpDir string
CircuitBreaker *CircuitBreaker
}
1 change: 1 addition & 0 deletions internal/testutil/mockprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type MockProvider struct {
InterceptorFunc func(w http.ResponseWriter, r *http.Request, tracer trace.Tracer) (intercept.Interceptor, error)
}

func (m *MockProvider) Type() string { return m.Name_ }
func (m *MockProvider) Name() string { return m.Name_ }
func (m *MockProvider) BaseURL() string { return m.URL }
func (m *MockProvider) RoutePrefix() string { return fmt.Sprintf("/%s", m.Name_) }
Expand Down
9 changes: 8 additions & 1 deletion provider/anthropic.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ var anthropicIsFailure = func(statusCode int) bool {
}

func NewAnthropic(cfg config.Anthropic, bedrockCfg *config.AWSBedrock) *Anthropic {
if cfg.Name == "" {
cfg.Name = config.ProviderAnthropic
}
if cfg.BaseURL == "" {
cfg.BaseURL = "https://api.anthropic.com/"
}
Expand All @@ -68,10 +71,14 @@ func NewAnthropic(cfg config.Anthropic, bedrockCfg *config.AWSBedrock) *Anthropi
}
}

func (p *Anthropic) Name() string {
func (p *Anthropic) Type() string {
return config.ProviderAnthropic
}

func (p *Anthropic) Name() string {
return p.cfg.Name
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.

Since Name defines route prefix does it need to be checked/sanitized?

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.

Yes, it should. I can add it in validateProviders on the upstack PR: #240

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.

Added here: 7d0c198

}

func (p *Anthropic) RoutePrefix() string {
return fmt.Sprintf("/%s", p.Name())
}
Expand Down
34 changes: 34 additions & 0 deletions provider/anthropic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,40 @@ import (
"github.com/coder/aibridge/internal/testutil"
)

func TestAnthropic_TypeAndName(t *testing.T) {
t.Parallel()

tests := []struct {
name string
cfg config.Anthropic
expectType string
expectName string
}{
{
name: "defaults",
cfg: config.Anthropic{},
expectType: config.ProviderAnthropic,
expectName: config.ProviderAnthropic,
},
{
name: "custom_name",
cfg: config.Anthropic{Name: "anthropic-custom"},
expectType: config.ProviderAnthropic,
expectName: "anthropic-custom",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

p := NewAnthropic(tc.cfg, nil)
assert.Equal(t, tc.expectType, p.Type())
assert.Equal(t, tc.expectName, p.Name())
})
}
}

func TestAnthropic_CreateInterceptor(t *testing.T) {
t.Parallel()

Expand Down
9 changes: 8 additions & 1 deletion provider/copilot.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ type Copilot struct {
var _ Provider = &Copilot{}

func NewCopilot(cfg config.Copilot) *Copilot {
if cfg.Name == "" {
cfg.Name = config.ProviderCopilot
}
if cfg.BaseURL == "" {
cfg.BaseURL = copilotBaseURL
}
Expand All @@ -67,10 +70,14 @@ func NewCopilot(cfg config.Copilot) *Copilot {
}
}

func (p *Copilot) Name() string {
func (p *Copilot) Type() string {
return config.ProviderCopilot
}

func (p *Copilot) Name() string {
return p.cfg.Name
}

func (p *Copilot) BaseURL() string {
return p.cfg.BaseURL
}
Expand Down
34 changes: 34 additions & 0 deletions provider/copilot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,40 @@ import (

var testTracer = otel.Tracer("copilot_test")

func TestCopilot_TypeAndName(t *testing.T) {
t.Parallel()

tests := []struct {
name string
cfg config.Copilot
expectType string
expectName string
}{
{
name: "defaults",
cfg: config.Copilot{},
expectType: config.ProviderCopilot,
expectName: config.ProviderCopilot,
},
{
name: "custom_name",
cfg: config.Copilot{Name: "copilot-business"},
expectType: config.ProviderCopilot,
expectName: "copilot-business",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

p := NewCopilot(tc.cfg)
assert.Equal(t, tc.expectType, p.Type())
assert.Equal(t, tc.expectName, p.Name())
})
}
}

func TestCopilot_InjectAuthHeader(t *testing.T) {
t.Parallel()

Expand Down
9 changes: 8 additions & 1 deletion provider/openai.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ type OpenAI struct {
var _ Provider = &OpenAI{}

func NewOpenAI(cfg config.OpenAI) *OpenAI {
if cfg.Name == "" {
cfg.Name = config.ProviderOpenAI
}
if cfg.BaseURL == "" {
cfg.BaseURL = "https://api.openai.com/v1/"
}
Expand All @@ -56,10 +59,14 @@ func NewOpenAI(cfg config.OpenAI) *OpenAI {
}
}

func (p *OpenAI) Name() string {
func (p *OpenAI) Type() string {
return config.ProviderOpenAI
}

func (p *OpenAI) Name() string {
return p.cfg.Name
}

func (p *OpenAI) RoutePrefix() string {
// Route prefix includes version to match default OpenAI base URL.
// More detailed explanation: https://github.com/coder/aibridge/pull/174#discussion_r2782320152
Expand Down
34 changes: 34 additions & 0 deletions provider/openai_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,40 @@ func generateResponsesPayload(payloadSize int, inputCount int, stream bool) []by
return bodyBytes
}

func TestOpenAI_TypeAndName(t *testing.T) {
t.Parallel()

tests := []struct {
name string
cfg config.OpenAI
expectType string
expectName string
}{
{
name: "defaults",
cfg: config.OpenAI{},
expectType: config.ProviderOpenAI,
expectName: config.ProviderOpenAI,
},
{
name: "custom_name",
cfg: config.OpenAI{Name: "openai-custom"},
expectType: config.ProviderOpenAI,
expectName: "openai-custom",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

p := NewOpenAI(tc.cfg)
assert.Equal(t, tc.expectType, p.Type())
assert.Equal(t, tc.expectName, p.Name())
})
}
}

func TestOpenAI_CreateInterceptor(t *testing.T) {
t.Parallel()

Expand Down
6 changes: 5 additions & 1 deletion provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ var UnknownRoute = errors.New("unknown route")
// OpenAI includes the version '/v1' in the base url while Anthropic does not.
// More details/examples: https://github.com/coder/aibridge/pull/174#discussion_r2782320152
type Provider interface {
// Name returns the provider's name.
// Type returns the provider type: "copilot", "openai", or "anthropic".
// Multiple provider instances can share the same type.
Type() string
// Name returns the provider instance name.
// Defaults to Type() when not explicitly configured.
Name() string
// BaseURL defines the base URL endpoint for this provider's API.
BaseURL() string
Expand Down
1 change: 1 addition & 0 deletions recorder/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type InterceptionRecord struct {
Metadata Metadata
Model string
Provider string
ProviderName string
StartedAt time.Time
ClientSessionID *string
Client string
Expand Down
Loading