Thanks to visit codestin.com
Credit goes to github.com

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: 3 additions & 0 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions coderd/database/db2sdk/db2sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,9 @@ func AIBridgeInterception(interception database.AIBridgeInterception, initiator
UserPrompts: sdkUserPrompts,
ToolUsages: sdkToolUsages,
}
if interception.APIKeyID.Valid {
intc.APIKeyID = &interception.APIKeyID.String
}
if interception.EndedAt.Valid {
intc.EndedAt = &interception.EndedAt.Time
}
Expand Down
1 change: 1 addition & 0 deletions coderd/database/dbgen/dbgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -1496,6 +1496,7 @@ func ClaimPrebuild(
func AIBridgeInterception(t testing.TB, db database.Store, seed database.InsertAIBridgeInterceptionParams, endedAt *time.Time) database.AIBridgeInterception {
interception, err := db.InsertAIBridgeInterception(genCtx, database.InsertAIBridgeInterceptionParams{
ID: takeFirst(seed.ID, uuid.New()),
APIKeyID: seed.APIKeyID,
InitiatorID: takeFirst(seed.InitiatorID, uuid.New()),
Provider: takeFirst(seed.Provider, "provider"),
Model: takeFirst(seed.Model, "model"),
Expand Down
3 changes: 2 additions & 1 deletion coderd/database/dump.sql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE aibridge_interceptions DROP COLUMN api_key_id;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- column is nullable to not break interceptions recorded before this column was added
ALTER TABLE aibridge_interceptions ADD COLUMN api_key_id text;
1 change: 1 addition & 0 deletions coderd/database/modelqueries.go
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,7 @@ func (q *sqlQuerier) ListAuthorizedAIBridgeInterceptions(ctx context.Context, ar
&i.AIBridgeInterception.StartedAt,
&i.AIBridgeInterception.Metadata,
&i.AIBridgeInterception.EndedAt,
&i.AIBridgeInterception.APIKeyID,
&i.VisibleUser.ID,
&i.VisibleUser.Username,
&i.VisibleUser.Name,
Expand Down
1 change: 1 addition & 0 deletions coderd/database/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 14 additions & 7 deletions coderd/database/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions coderd/database/queries/aibridge.sql
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
-- name: InsertAIBridgeInterception :one
INSERT INTO aibridge_interceptions (
id, initiator_id, provider, model, metadata, started_at
id, api_key_id, initiator_id, provider, model, metadata, started_at
) VALUES (
@id, @initiator_id, @provider, @model, COALESCE(@metadata::jsonb, '{}'::jsonb), @started_at
@id, @api_key_id, @initiator_id, @provider, @model, COALESCE(@metadata::jsonb, '{}'::jsonb), @started_at
)
RETURNING *;

Expand Down
1 change: 1 addition & 0 deletions codersdk/aibridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

type AIBridgeInterception struct {
ID uuid.UUID `json:"id" format:"uuid"`
APIKeyID *string `json:"api_key_id"`
Initiator MinimalUser `json:"initiator"`
Provider string `json:"provider"`
Model string `json:"model"`
Expand Down
1 change: 1 addition & 0 deletions docs/reference/api/aibridge.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions docs/reference/api/schemas.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions enterprise/aibridged/aibridged_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/externalauth"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/enterprise/aibridged"
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
Expand Down Expand Up @@ -221,6 +222,18 @@ func TestIntegration(t *testing.T) {
require.NoError(t, err)
require.Len(t, interceptions, 1)

intc0 := interceptions[0]
keyID, _, err := httpmw.SplitAPIToken(apiKey.Key)
require.NoError(t, err)
require.Equal(t, user.ID, intc0.InitiatorID)
require.True(t, intc0.APIKeyID.Valid)
require.Equal(t, keyID, intc0.APIKeyID.String)
require.Equal(t, "openai", intc0.Provider)
require.Equal(t, "gpt-4.1", intc0.Model)
require.True(t, intc0.EndedAt.Valid)
require.True(t, intc0.StartedAt.Before(intc0.EndedAt.Time))
require.Less(t, intc0.EndedAt.Time.Sub(intc0.StartedAt), 5*time.Second)

prompts, err := db.GetAIBridgeUserPromptsByInterceptionID(ctx, interceptions[0].ID)
require.NoError(t, err)
require.Len(t, prompts, 1)
Expand Down
1 change: 1 addition & 0 deletions enterprise/aibridged/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ func (s *Server) ServeHTTP(rw http.ResponseWriter, r *http.Request) {

handler, err := s.GetRequestHandler(ctx, Request{
SessionKey: key,
APIKeyID: resp.ApiKeyId,
InitiatorID: id,
})
if err != nil {
Expand Down
23 changes: 12 additions & 11 deletions enterprise/aibridged/pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,17 +111,9 @@ func (p *CachedBridgePool) Acquire(ctx context.Context, req Request, clientFn Cl
// may visit the slow path unnecessarily.
defer p.cache.Wait()

recorder := aibridge.NewRecorder(p.logger.Named("recorder"), func() (aibridge.Recorder, error) {
client, err := clientFn()
if err != nil {
return nil, xerrors.Errorf("acquire client: %w", err)
}

return &recorderTranslation{client: client}, nil
})

// Fast path.
bridge, ok := p.cache.Get(req.InitiatorID.String())
cacheKey := req.InitiatorID.String() + "|" + req.APIKeyID
bridge, ok := p.cache.Get(cacheKey)
if ok && bridge != nil {
// TODO: future improvement:
// Once we can detect token expiry against an MCP server, we no longer need to let these instances
Expand All @@ -131,6 +123,15 @@ func (p *CachedBridgePool) Acquire(ctx context.Context, req Request, clientFn Cl
return bridge, nil
}

recorder := aibridge.NewRecorder(p.logger.Named("recorder"), func() (aibridge.Recorder, error) {
client, err := clientFn()
if err != nil {
return nil, xerrors.Errorf("acquire client: %w", err)
}

return &recorderTranslation{apiKeyID: req.APIKeyID, client: client}, nil
})

// Slow path.
// Creating an *aibridge.RequestBridge may take some time, so gate all subsequent callers behind the initial request and return the resulting value.
// TODO: track startup time since it adds latency to first request (histogram count will also help us see how often this occurs).
Expand Down Expand Up @@ -158,7 +159,7 @@ func (p *CachedBridgePool) Acquire(ctx context.Context, req Request, clientFn Cl
return nil, xerrors.Errorf("create new request bridge: %w", err)
}

p.cache.SetWithTTL(req.InitiatorID.String(), bridge, cacheCost, p.options.TTL)
p.cache.SetWithTTL(cacheKey, bridge, cacheCost, p.options.TTL)

return bridge, nil
})
Expand Down
23 changes: 22 additions & 1 deletion enterprise/aibridged/pool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func TestPool(t *testing.T) {
require.NoError(t, err)
t.Cleanup(func() { pool.Shutdown(context.Background()) })

id, id2 := uuid.New(), uuid.New()
id, id2, apiKeyID1, apiKeyID2 := uuid.New(), uuid.New(), uuid.New(), uuid.New()
clientFn := func() (aibridged.DRPCClient, error) {
return client, nil
}
Expand All @@ -50,13 +50,15 @@ func TestPool(t *testing.T) {
inst, err := pool.Acquire(t.Context(), aibridged.Request{
SessionKey: "key",
InitiatorID: id,
APIKeyID: apiKeyID1.String(),
}, clientFn, newMockMCPFactory(mcpProxy))
require.NoError(t, err, "acquire pool instance")

// ...and it will return it when acquired again.
instB, err := pool.Acquire(t.Context(), aibridged.Request{
SessionKey: "key",
InitiatorID: id,
APIKeyID: apiKeyID1.String(),
}, clientFn, newMockMCPFactory(mcpProxy))
require.NoError(t, err, "acquire pool instance")
require.Same(t, inst, instB)
Expand All @@ -74,6 +76,7 @@ func TestPool(t *testing.T) {
inst2, err := pool.Acquire(t.Context(), aibridged.Request{
SessionKey: "key",
InitiatorID: id2,
APIKeyID: apiKeyID1.String(),
}, clientFn, newMockMCPFactory(mcpProxy))
require.NoError(t, err, "acquire pool instance")
require.NotSame(t, inst, inst2)
Expand All @@ -84,6 +87,24 @@ func TestPool(t *testing.T) {
require.EqualValues(t, 1, metrics.Hits())
require.EqualValues(t, 2, metrics.Misses())

// This will get called again because a new instance will be created.
mcpProxy.EXPECT().Init(gomock.Any()).Times(1).Return(nil)

// New instance is created for different api key id
inst2B, err := pool.Acquire(t.Context(), aibridged.Request{
SessionKey: "key",
InitiatorID: id2,
APIKeyID: apiKeyID2.String(),
}, clientFn, newMockMCPFactory(mcpProxy))
require.NoError(t, err, "acquire pool instance 2B")
require.NotSame(t, inst2, inst2B)

metrics = pool.Metrics()
require.EqualValues(t, 3, metrics.KeysAdded())
require.EqualValues(t, 2, metrics.KeysEvicted())
require.EqualValues(t, 1, metrics.Hits())
require.EqualValues(t, 3, metrics.Misses())

// TODO: add test for expiry.
// This requires Go 1.25's [synctest](https://pkg.go.dev/testing/synctest) since the
// internal cache lib cannot be tested using coder/quartz.
Expand Down
Loading
Loading