Skip to content

Commit 260d58a

Browse files
authored
Merge pull request #893 from helixml/feature/third-party-bff
UI polish, and first pass on multi-oauth provider support
2 parents 3b9c66f + 9e67ffe commit 260d58a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+11164
-2293
lines changed

.drone.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ steps:
1717
depends_on: []
1818

1919
- name: build-frontend
20-
image: node:21-alpine
20+
image: node:23-alpine
2121
commands:
2222
- cd frontend
2323
- yarn install
24+
- yarn test
2425
- yarn build
2526
depends_on: []
2627

README.md

-1
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,3 @@ Contributions to the source code are welcome, and by contributing you confirm th
7272
* We don't want cloud providers to take our open source code and build a rebranded service on top of it.
7373

7474
If you would like to use some part of this code under a more permissive license, please [get in touch](mailto:[email protected]).
75-

api/cmd/helix/serve.go

+11
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/helixml/helix/api/pkg/janitor"
2121
"github.com/helixml/helix/api/pkg/license"
2222
"github.com/helixml/helix/api/pkg/notification"
23+
"github.com/helixml/helix/api/pkg/oauth"
2324
"github.com/helixml/helix/api/pkg/openai"
2425
"github.com/helixml/helix/api/pkg/openai/logger"
2526
"github.com/helixml/helix/api/pkg/openai/manager"
@@ -383,6 +384,15 @@ func serve(cmd *cobra.Command, cfg *config.ServerConfig) error {
383384
RunnerController: runnerController,
384385
}
385386

387+
// Create the OAuth manager
388+
oauthManager := oauth.NewManager(postgresStore)
389+
if err := oauthManager.LoadProviders(ctx); err != nil {
390+
log.Error().Err(err).Msg("failed to load oauth providers")
391+
}
392+
393+
// Update controller options with the OAuth manager
394+
controllerOptions.OAuthManager = oauthManager
395+
386396
appController, err = controller.NewController(ctx, controllerOptions)
387397
if err != nil {
388398
return err
@@ -443,6 +453,7 @@ func serve(cmd *cobra.Command, cfg *config.ServerConfig) error {
443453
knowledgeReconciler,
444454
scheduler,
445455
pingService,
456+
oauthManager,
446457
)
447458
if err != nil {
448459
return err

api/pkg/controller/app_handlers.go

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package controller
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"strings"
8+
9+
"github.com/helixml/helix/api/pkg/oauth"
10+
"github.com/helixml/helix/api/pkg/types"
11+
"github.com/rs/zerolog/log"
12+
)
13+
14+
// getAppOAuthTokenEnv retrieves OAuth tokens for an app and returns them as environment variables
15+
// This function is currently unused but kept for future implementation
16+
// nolint:unused
17+
func (c *Controller) getAppOAuthTokenEnv(ctx context.Context, app *types.App, userID string) ([]string, error) {
18+
if c.Options.OAuthManager == nil {
19+
return nil, nil
20+
}
21+
22+
var envVars []string
23+
24+
// Check each assistant's tools for OAuth provider requirements
25+
for _, assistant := range app.Config.Helix.Assistants {
26+
for _, tool := range assistant.Tools {
27+
if tool.ToolType == types.ToolTypeAPI && tool.Config.API != nil && tool.Config.API.OAuthProvider != "" {
28+
providerType := tool.Config.API.OAuthProvider
29+
requiredScopes := tool.Config.API.OAuthScopes
30+
31+
token, err := c.Options.OAuthManager.GetTokenForTool(ctx, userID, providerType, requiredScopes)
32+
if err != nil {
33+
// Check if it's a scope error
34+
var scopeErr *oauth.ScopeError
35+
if errors.As(err, &scopeErr) {
36+
log.Warn().
37+
Str("app_id", app.ID).
38+
Str("user_id", userID).
39+
Str("provider", string(providerType)).
40+
Strs("missing_scopes", scopeErr.Missing).
41+
Msg("Missing required OAuth scopes for tool")
42+
43+
// Include this error in logs but continue with other providers
44+
continue
45+
}
46+
47+
// For other errors, log and continue
48+
log.Warn().
49+
Err(err).
50+
Str("app_id", app.ID).
51+
Str("user_id", userID).
52+
Str("provider", string(providerType)).
53+
Msg("Failed to get OAuth token for tool")
54+
55+
continue
56+
}
57+
58+
// Add the token as an environment variable
59+
envVar := fmt.Sprintf("OAUTH_TOKEN_%s=%s", strings.ToUpper(string(providerType)), token)
60+
envVars = append(envVars, envVar)
61+
}
62+
}
63+
}
64+
65+
return envVars, nil
66+
}

api/pkg/controller/controller.go

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/helixml/helix/api/pkg/janitor"
1212
"github.com/helixml/helix/api/pkg/model"
1313
"github.com/helixml/helix/api/pkg/notification"
14+
"github.com/helixml/helix/api/pkg/oauth"
1415
"github.com/helixml/helix/api/pkg/openai"
1516
"github.com/helixml/helix/api/pkg/openai/manager"
1617
"github.com/helixml/helix/api/pkg/pubsub"
@@ -35,6 +36,7 @@ type Options struct {
3536
DataprepOpenAIClient openai.Client
3637
Scheduler *scheduler.Scheduler
3738
RunnerController *scheduler.RunnerController
39+
OAuthManager *oauth.Manager
3840
}
3941

4042
type Controller struct {

api/pkg/controller/inference.go

+124-6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package controller
33
import (
44
"context"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"path/filepath"
89
"strings"
@@ -20,17 +21,18 @@ import (
2021
"github.com/helixml/helix/api/pkg/types"
2122
"gopkg.in/yaml.v2"
2223

24+
"github.com/helixml/helix/api/pkg/oauth"
2325
"github.com/rs/zerolog/log"
2426
openai "github.com/sashabaranov/go-openai"
2527
)
2628

2729
type ChatCompletionOptions struct {
28-
AppID string
29-
AssistantID string
30-
RAGSourceID string
31-
Provider string
32-
33-
QueryParams map[string]string
30+
AppID string
31+
AssistantID string
32+
RAGSourceID string
33+
Provider string
34+
QueryParams map[string]string
35+
OAuthEnvVars []string // OAuth environment variables
3436
}
3537

3638
// ChatCompletion is used by the OpenAI compatible API. Doesn't handle any historical sessions, etc.
@@ -87,6 +89,12 @@ func (c *Controller) ChatCompletion(ctx context.Context, user *types.User, req o
8789
return nil, nil, fmt.Errorf("failed to get client: %v", err)
8890
}
8991

92+
// Evaluate and add OAuth tokens
93+
err = c.evalAndAddOAuthTokens(ctx, client, opts, user)
94+
if err != nil {
95+
return nil, nil, fmt.Errorf("failed to add OAuth tokens: %w", err)
96+
}
97+
9098
resp, err := client.CreateChatCompletion(ctx, req)
9199
if err != nil {
92100
log.Err(err).Msg("error creating chat completion")
@@ -156,6 +164,12 @@ func (c *Controller) ChatCompletionStream(ctx context.Context, user *types.User,
156164
return nil, nil, fmt.Errorf("failed to get client: %v", err)
157165
}
158166

167+
// Evaluate and add OAuth tokens
168+
err = c.evalAndAddOAuthTokens(ctx, client, opts, user)
169+
if err != nil {
170+
return nil, nil, fmt.Errorf("failed to add OAuth tokens: %w", err)
171+
}
172+
159173
stream, err := client.CreateChatCompletionStream(ctx, req)
160174
if err != nil {
161175
log.Err(err).Msg("error creating chat completion stream")
@@ -919,3 +933,107 @@ func (c *Controller) UpdateSessionWithKnowledgeResults(ctx context.Context, sess
919933

920934
return nil
921935
}
936+
937+
func (c *Controller) getAppOAuthTokens(ctx context.Context, userID string, app *types.App) ([]string, error) {
938+
// Initialize empty slice for environment variables
939+
var envVars []string
940+
941+
// Only proceed if we have an OAuth manager
942+
if c.Options.OAuthManager == nil {
943+
log.Debug().Msg("No OAuth manager available")
944+
return nil, nil
945+
}
946+
947+
// If app is nil, return empty slice
948+
if app == nil {
949+
return nil, nil
950+
}
951+
952+
// Keep track of providers we've seen to avoid duplicates
953+
seenProviders := make(map[types.OAuthProviderType]bool)
954+
955+
// First, check the tools defined in assistants
956+
for _, assistant := range app.Config.Helix.Assistants {
957+
for _, tool := range assistant.Tools {
958+
if tool.ToolType == types.ToolTypeAPI && tool.Config.API != nil && tool.Config.API.OAuthProvider != "" {
959+
providerType := tool.Config.API.OAuthProvider
960+
requiredScopes := tool.Config.API.OAuthScopes
961+
962+
// Skip if we've already processed this provider
963+
if seenProviders[providerType] {
964+
continue
965+
}
966+
seenProviders[providerType] = true
967+
968+
token, err := c.Options.OAuthManager.GetTokenForTool(ctx, userID, providerType, requiredScopes)
969+
if err == nil && token != "" {
970+
envName := fmt.Sprintf("OAUTH_TOKEN_%s", strings.ToUpper(string(providerType)))
971+
envVars = append(envVars, fmt.Sprintf("%s=%s", envName, token))
972+
log.Debug().Str("provider", string(providerType)).Msg("Added OAuth token to app environment")
973+
} else {
974+
var scopeErr *oauth.ScopeError
975+
if errors.As(err, &scopeErr) {
976+
log.Warn().
977+
Str("app_id", app.ID).
978+
Str("user_id", userID).
979+
Str("provider", string(providerType)).
980+
Strs("missing_scopes", scopeErr.Missing).
981+
Msg("Missing required OAuth scopes for tool")
982+
} else {
983+
log.Debug().Err(err).Str("provider", string(providerType)).Msg("Failed to get OAuth token for tool")
984+
}
985+
}
986+
}
987+
}
988+
}
989+
990+
return envVars, nil
991+
}
992+
993+
func (c *Controller) evalAndAddOAuthTokens(ctx context.Context, client oai.Client, opts *ChatCompletionOptions, user *types.User) error {
994+
// If we already have OAuth tokens, use them
995+
if len(opts.OAuthEnvVars) > 0 {
996+
return nil
997+
}
998+
999+
// If we have an app ID, try to get OAuth tokens
1000+
if opts.AppID != "" && c.Options.OAuthManager != nil {
1001+
app, err := c.Options.Store.GetApp(ctx, opts.AppID)
1002+
if err != nil {
1003+
log.Debug().Err(err).Str("app_id", opts.AppID).Msg("Failed to get app for OAuth tokens")
1004+
return nil // Continue without OAuth tokens
1005+
}
1006+
1007+
// Get OAuth tokens as environment variables
1008+
oauthEnvVars, err := c.getAppOAuthTokens(ctx, user.ID, app)
1009+
if err != nil {
1010+
log.Debug().Err(err).Str("app_id", opts.AppID).Msg("Failed to get OAuth tokens for app")
1011+
return nil // Continue without OAuth tokens
1012+
}
1013+
1014+
// Add OAuth tokens to the options
1015+
opts.OAuthEnvVars = oauthEnvVars
1016+
1017+
// If we have tokens, add them to the client as well
1018+
if len(oauthEnvVars) > 0 {
1019+
for _, envVar := range oauthEnvVars {
1020+
parts := strings.SplitN(envVar, "=", 2)
1021+
if len(parts) == 2 {
1022+
envKey, envValue := parts[0], parts[1]
1023+
// Only process OAUTH_TOKEN_ variables
1024+
if strings.HasPrefix(envKey, "OAUTH_TOKEN_") {
1025+
// Extract provider type from env var name (e.g., OAUTH_TOKEN_GITHUB -> github)
1026+
providerType := strings.ToLower(strings.TrimPrefix(envKey, "OAUTH_TOKEN_"))
1027+
log.Debug().Str("provider", providerType).Msg("Added OAuth token to API client HTTP headers")
1028+
// Add OAuth token to client (if supported)
1029+
if retryableClient, ok := client.(*oai.RetryableClient); ok {
1030+
retryableClient.AddOAuthToken(providerType, envValue)
1031+
}
1032+
}
1033+
}
1034+
}
1035+
}
1036+
}
1037+
1038+
return nil
1039+
}

0 commit comments

Comments
 (0)