-
Notifications
You must be signed in to change notification settings - Fork 3
feat: conditionally add actor headers #142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| package intercept | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "strings" | ||
|
|
||
| ant_option "github.com/anthropics/anthropic-sdk-go/option" | ||
| "github.com/coder/aibridge/context" | ||
| oai_option "github.com/openai/openai-go/v3/option" | ||
| ) | ||
|
|
||
| const ( | ||
| prefix = "X-AI-Bridge-Actor" | ||
| ) | ||
|
|
||
| func ActorIDHeader() string { | ||
| return fmt.Sprintf("%s-ID", prefix) | ||
| } | ||
|
|
||
| func ActorMetadataHeader(name string) string { | ||
| return fmt.Sprintf("%s-Metadata-%s", prefix, name) | ||
| } | ||
|
|
||
| func IsActorHeader(name string) bool { | ||
| return strings.HasPrefix(strings.ToLower(name), strings.ToLower(prefix)) | ||
| } | ||
|
|
||
| // ActorHeadersAsOpenAIOpts produces a slice of headers using OpenAI's RequestOption type. | ||
| func ActorHeadersAsOpenAIOpts(actor *context.Actor) []oai_option.RequestOption { | ||
| var opts []oai_option.RequestOption | ||
|
|
||
| headers := headersFromActor(actor) | ||
| if len(headers) == 0 { | ||
| return nil | ||
| } | ||
|
|
||
| for k, v := range headers { | ||
| // [k] will be canonicalized, see [http.Header]'s [Add] method. | ||
| opts = append(opts, oai_option.WithHeaderAdd(k, v)) | ||
| } | ||
|
|
||
| return opts | ||
| } | ||
|
|
||
| // ActorHeadersAsAnthropicOpts produces a slice of headers using Anthropic's RequestOption type. | ||
| func ActorHeadersAsAnthropicOpts(actor *context.Actor) []ant_option.RequestOption { | ||
| var opts []ant_option.RequestOption | ||
|
|
||
| headers := headersFromActor(actor) | ||
| if len(headers) == 0 { | ||
| return nil | ||
| } | ||
|
|
||
| for k, v := range headers { | ||
| // [k] will be canonicalized, see [http.Header]'s [Add] method. | ||
| opts = append(opts, ant_option.WithHeaderAdd(k, v)) | ||
| } | ||
|
|
||
| return opts | ||
| } | ||
|
|
||
| // headersFromActor produces a map of headers from a given [context.Actor]. | ||
| func headersFromActor(actor *context.Actor) map[string]string { | ||
| if actor == nil { | ||
| return nil | ||
| } | ||
|
|
||
| headers := make(map[string]string, len(actor.Metadata)+1) | ||
|
|
||
| // Add actor ID. | ||
| headers[ActorIDHeader()] = actor.ID | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: can actor.ID be |
||
|
|
||
| // Add headers for provided metadata. | ||
| for k, v := range actor.Metadata { | ||
| headers[ActorMetadataHeader(k)] = fmt.Sprintf("%v", v) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: we should probably be sanitizing the headers/vallues to make sure they don't have whitespaces or invalid characters |
||
| } | ||
|
|
||
| return headers | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| package intercept | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/coder/aibridge/context" | ||
| "github.com/coder/aibridge/recorder" | ||
| "github.com/google/uuid" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestNilActor(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| require.Nil(t, ActorHeadersAsOpenAIOpts(nil)) | ||
| require.Nil(t, ActorHeadersAsAnthropicOpts(nil)) | ||
| } | ||
|
|
||
| func TestBasic(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| actorID := uuid.NewString() | ||
| actor := &context.Actor{ | ||
| ID: actorID, | ||
| } | ||
|
|
||
| // We can't peek inside since these opts require an internal type to apply onto. | ||
| // All we can do is check the length. | ||
| // See TestActorHeaders for an integration test. | ||
| oaiOpts := ActorHeadersAsOpenAIOpts(actor) | ||
| require.Len(t, oaiOpts, 1) | ||
| antOpts := ActorHeadersAsAnthropicOpts(actor) | ||
| require.Len(t, antOpts, 1) | ||
| } | ||
|
|
||
| func TestBasicAndMetadata(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| actorID := uuid.NewString() | ||
| actor := &context.Actor{ | ||
| ID: actorID, | ||
| Metadata: recorder.Metadata{ | ||
| "This": "That", | ||
| "And": "The other", | ||
| }, | ||
| } | ||
|
|
||
| // We can't peek inside since these opts require an internal type to apply onto. | ||
| // All we can do is check the length. | ||
| // See TestActorHeaders for an integration test. | ||
| oaiOpts := ActorHeadersAsOpenAIOpts(actor) | ||
| require.Len(t, oaiOpts, 1+len(actor.Metadata)) | ||
| antOpts := ActorHeadersAsAnthropicOpts(actor) | ||
| require.Len(t, antOpts, 1+len(actor.Metadata)) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,8 @@ import ( | |
| "time" | ||
|
|
||
| "github.com/coder/aibridge/config" | ||
| aibcontext "github.com/coder/aibridge/context" | ||
| "github.com/coder/aibridge/intercept" | ||
| "github.com/coder/aibridge/intercept/eventstream" | ||
| "github.com/coder/aibridge/mcp" | ||
| "github.com/coder/aibridge/recorder" | ||
|
|
@@ -73,8 +75,12 @@ func (i *BlockingInterception) ProcessRequest(w http.ResponseWriter, r *http.Req | |
|
|
||
| for { | ||
| // TODO add outer loop span (https://github.com/coder/aibridge/issues/67) | ||
|
|
||
| var opts []option.RequestOption | ||
| opts = append(opts, option.WithRequestTimeout(time.Second*600)) | ||
| if actor := aibcontext.ActorFromContext(r.Context()); actor != nil && i.cfg.SendActorHeaders { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. minor nit: should we maybe check the cfg first, as in most cases this will be false? |
||
| opts = append(opts, intercept.ActorHeadersAsOpenAIOpts(actor)...) | ||
| } | ||
|
|
||
| completion, err = i.newChatCompletion(ctx, svc, opts) | ||
| if err != nil { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: shouldn't we check if name != ""?
Otherwise, we could end with headers like
X-AI-Bridge-Actor-Metadata-