diff --git a/coderd/ai_providers_test.go b/coderd/ai_providers_test.go index 5c3063327f..c020f90c26 100644 --- a/coderd/ai_providers_test.go +++ b/coderd/ai_providers_test.go @@ -327,6 +327,50 @@ func TestAIProvidersCRUD(t *testing.T) { require.Contains(t, sdkErr.Message, "At least one field must be provided") }) + t.Run("UpdateCannotMutateName", func(t *testing.T) { + t.Parallel() + // ai_providers.name is the stable key that aibridge_interceptions + // snapshots into provider_name. Renames would silently desync + // historical interceptions from their live row and break the + // future FK backfill, so the PATCH endpoint must ignore any "name" + // field in the payload. The SDK type intentionally has no Name + // field; this test sends raw JSON to defend against a future + // regression where someone adds one without thinking. + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + ctx := testutil.Context(t, testutil.WaitLong) + + //nolint:gocritic // Owner role is the audience for this endpoint. + created, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{ + Type: codersdk.AIProviderTypeOpenAI, + Name: "stable-name", + Enabled: true, + BaseURL: "https://api.openai.com/v1", + }) + require.NoError(t, err) + + res, err := client.Request(ctx, http.MethodPatch, + "/api/v2/ai/providers/"+created.Name, + json.RawMessage(`{"name":"renamed","display_name":"New Display"}`), + ) + require.NoError(t, err) + defer res.Body.Close() + require.Equal(t, http.StatusOK, res.StatusCode) + + got, err := client.AIProvider(ctx, created.Name) + require.NoError(t, err) + require.Equal(t, "stable-name", got.Name, "name must not be mutable via PATCH") + require.Equal(t, "New Display", got.DisplayName, "display_name should still update") + + // Confirm the original name still resolves and the attempted new + // name does not exist as a separate row. + _, err = client.AIProvider(ctx, "renamed") + require.Error(t, err) + var sdkErr *codersdk.Error + require.ErrorAs(t, err, &sdkErr) + require.Equal(t, http.StatusNotFound, sdkErr.StatusCode()) + }) + t.Run("UpdateSettingsEmptyObjectRejected", func(t *testing.T) { t.Parallel() // "settings": {} cannot decode because the _type discriminator diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 7b85bd3323..dce2daec4c 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -1436,7 +1436,7 @@ const docTemplate = `{ "parameters": [ { "type": "string", - "description": "Search query in the format ` + "`" + `key:value` + "`" + `. Available keys are: initiator, provider, model, started_after, started_before.", + "description": "Search query in the format ` + "`" + `key:value` + "`" + `. Available keys are: initiator, provider, provider_name, model, started_after, started_before.", "name": "q", "in": "query" }, @@ -1515,7 +1515,7 @@ const docTemplate = `{ "parameters": [ { "type": "string", - "description": "Search query in the format ` + "`" + `key:value` + "`" + `. Available keys are: initiator, provider, model, client, session_id, started_after, started_before.", + "description": "Search query in the format ` + "`" + `key:value` + "`" + `. Available keys are: initiator, provider, provider_name, model, client, session_id, started_after, started_before.", "name": "q", "in": "query" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 0bff17f6dc..88839dc5ac 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -1265,7 +1265,7 @@ "parameters": [ { "type": "string", - "description": "Search query in the format `key:value`. Available keys are: initiator, provider, model, started_after, started_before.", + "description": "Search query in the format `key:value`. Available keys are: initiator, provider, provider_name, model, started_after, started_before.", "name": "q", "in": "query" }, @@ -1336,7 +1336,7 @@ "parameters": [ { "type": "string", - "description": "Search query in the format `key:value`. Available keys are: initiator, provider, model, client, session_id, started_after, started_before.", + "description": "Search query in the format `key:value`. Available keys are: initiator, provider, provider_name, model, client, session_id, started_after, started_before.", "name": "q", "in": "query" }, diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index f33c047c94..73f973e15c 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -932,6 +932,7 @@ func (q *sqlQuerier) ListAuthorizedAIBridgeInterceptions(ctx context.Context, ar arg.StartedBefore, arg.InitiatorID, arg.Provider, + arg.ProviderName, arg.Model, arg.Client, arg.AfterID, @@ -998,6 +999,7 @@ func (q *sqlQuerier) CountAuthorizedAIBridgeInterceptions(ctx context.Context, a arg.StartedBefore, arg.InitiatorID, arg.Provider, + arg.ProviderName, arg.Model, arg.Client, ) @@ -1097,6 +1099,7 @@ func (q *sqlQuerier) ListAuthorizedAIBridgeSessions(ctx context.Context, arg Lis arg.StartedBefore, arg.InitiatorID, arg.Provider, + arg.ProviderName, arg.Model, arg.Client, arg.SessionID, @@ -1161,6 +1164,7 @@ func (q *sqlQuerier) CountAuthorizedAIBridgeSessions(ctx context.Context, arg Co arg.StartedBefore, arg.InitiatorID, arg.Provider, + arg.ProviderName, arg.Model, arg.Client, arg.SessionID, diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 270f017e57..be452f5d3a 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -892,14 +892,19 @@ WHERE WHEN $4::text != '' THEN aibridge_interceptions.provider = $4::text ELSE true END + -- Filter provider_name + AND CASE + WHEN $5::text != '' THEN aibridge_interceptions.provider_name = $5::text + ELSE true + END -- Filter model AND CASE - WHEN $5::text != '' THEN aibridge_interceptions.model = $5::text + WHEN $6::text != '' THEN aibridge_interceptions.model = $6::text ELSE true END -- Filter client AND CASE - WHEN $6::text != '' THEN COALESCE(aibridge_interceptions.client, 'Unknown') = $6::text + WHEN $7::text != '' THEN COALESCE(aibridge_interceptions.client, 'Unknown') = $7::text ELSE true END -- Authorize Filter clause will be injected below in ListAuthorizedAIBridgeInterceptions @@ -911,6 +916,7 @@ type CountAIBridgeInterceptionsParams struct { StartedBefore time.Time `db:"started_before" json:"started_before"` InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"` Provider string `db:"provider" json:"provider"` + ProviderName string `db:"provider_name" json:"provider_name"` Model string `db:"model" json:"model"` Client string `db:"client" json:"client"` } @@ -921,6 +927,7 @@ func (q *sqlQuerier) CountAIBridgeInterceptions(ctx context.Context, arg CountAI arg.StartedBefore, arg.InitiatorID, arg.Provider, + arg.ProviderName, arg.Model, arg.Client, ) @@ -956,19 +963,24 @@ WHERE WHEN $4::text != '' THEN aibridge_interceptions.provider = $4::text ELSE true END + -- Filter provider_name + AND CASE + WHEN $5::text != '' THEN aibridge_interceptions.provider_name = $5::text + ELSE true + END -- Filter model AND CASE - WHEN $5::text != '' THEN aibridge_interceptions.model = $5::text + WHEN $6::text != '' THEN aibridge_interceptions.model = $6::text ELSE true END -- Filter client AND CASE - WHEN $6::text != '' THEN COALESCE(aibridge_interceptions.client, 'Unknown') = $6::text + WHEN $7::text != '' THEN COALESCE(aibridge_interceptions.client, 'Unknown') = $7::text ELSE true END -- Filter session_id AND CASE - WHEN $7::text != '' THEN aibridge_interceptions.session_id = $7::text + WHEN $8::text != '' THEN aibridge_interceptions.session_id = $8::text ELSE true END -- Authorize Filter clause will be injected below in CountAuthorizedAIBridgeSessions @@ -980,6 +992,7 @@ type CountAIBridgeSessionsParams struct { StartedBefore time.Time `db:"started_before" json:"started_before"` InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"` Provider string `db:"provider" json:"provider"` + ProviderName string `db:"provider_name" json:"provider_name"` Model string `db:"model" json:"model"` Client string `db:"client" json:"client"` SessionID string `db:"session_id" json:"session_id"` @@ -991,6 +1004,7 @@ func (q *sqlQuerier) CountAIBridgeSessions(ctx context.Context, arg CountAIBridg arg.StartedBefore, arg.InitiatorID, arg.Provider, + arg.ProviderName, arg.Model, arg.Client, arg.SessionID, @@ -1611,19 +1625,24 @@ WHERE WHEN $4::text != '' THEN aibridge_interceptions.provider = $4::text ELSE true END + -- Filter provider_name + AND CASE + WHEN $5::text != '' THEN aibridge_interceptions.provider_name = $5::text + ELSE true + END -- Filter model AND CASE - WHEN $5::text != '' THEN aibridge_interceptions.model = $5::text + WHEN $6::text != '' THEN aibridge_interceptions.model = $6::text ELSE true END -- Filter client AND CASE - WHEN $6::text != '' THEN COALESCE(aibridge_interceptions.client, 'Unknown') = $6::text + WHEN $7::text != '' THEN COALESCE(aibridge_interceptions.client, 'Unknown') = $7::text ELSE true END -- Cursor pagination AND CASE - WHEN $7::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN ( + WHEN $8::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN ( -- The pagination cursor is the last ID of the previous page. -- The query is ordered by the started_at field, so select all -- rows before the cursor and before the after_id UUID. @@ -1631,8 +1650,8 @@ WHERE -- "after_id" terminology comes from our pagination parser in -- coderd. (aibridge_interceptions.started_at, aibridge_interceptions.id) < ( - (SELECT started_at FROM aibridge_interceptions WHERE id = $7), - $7::uuid + (SELECT started_at FROM aibridge_interceptions WHERE id = $8), + $8::uuid ) ) ELSE true @@ -1642,8 +1661,8 @@ WHERE ORDER BY aibridge_interceptions.started_at DESC, aibridge_interceptions.id DESC -LIMIT COALESCE(NULLIF($9::integer, 0), 100) -OFFSET $8 +LIMIT COALESCE(NULLIF($10::integer, 0), 100) +OFFSET $9 ` type ListAIBridgeInterceptionsParams struct { @@ -1651,6 +1670,7 @@ type ListAIBridgeInterceptionsParams struct { StartedBefore time.Time `db:"started_before" json:"started_before"` InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"` Provider string `db:"provider" json:"provider"` + ProviderName string `db:"provider_name" json:"provider_name"` Model string `db:"model" json:"model"` Client string `db:"client" json:"client"` AfterID uuid.UUID `db:"after_id" json:"after_id"` @@ -1669,6 +1689,7 @@ func (q *sqlQuerier) ListAIBridgeInterceptions(ctx context.Context, arg ListAIBr arg.StartedBefore, arg.InitiatorID, arg.Provider, + arg.ProviderName, arg.Model, arg.Client, arg.AfterID, @@ -2033,19 +2054,24 @@ session_page AS ( WHEN $5::text != '' THEN ai.provider = $5::text ELSE true END + -- Filter provider_name + AND CASE + WHEN $6::text != '' THEN ai.provider_name = $6::text + ELSE true + END -- Filter model AND CASE - WHEN $6::text != '' THEN ai.model = $6::text + WHEN $7::text != '' THEN ai.model = $7::text ELSE true END -- Filter client AND CASE - WHEN $7::text != '' THEN COALESCE(ai.client, 'Unknown') = $7::text + WHEN $8::text != '' THEN COALESCE(ai.client, 'Unknown') = $8::text ELSE true END -- Filter session_id AND CASE - WHEN $8::text != '' THEN ai.session_id = $8::text + WHEN $9::text != '' THEN ai.session_id = $9::text ELSE true END -- Authorize Filter clause will be injected below in ListAuthorizedAIBridgeSessions @@ -2069,8 +2095,8 @@ session_page AS ( ORDER BY last_active_at DESC, ai.session_id DESC - LIMIT COALESCE(NULLIF($10::integer, 0), 100) - OFFSET $9 + LIMIT COALESCE(NULLIF($11::integer, 0), 100) + OFFSET $10 ) SELECT sp.session_id, @@ -2137,6 +2163,7 @@ type ListAIBridgeSessionsParams struct { StartedBefore time.Time `db:"started_before" json:"started_before"` InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"` Provider string `db:"provider" json:"provider"` + ProviderName string `db:"provider_name" json:"provider_name"` Model string `db:"model" json:"model"` Client string `db:"client" json:"client"` SessionID string `db:"session_id" json:"session_id"` @@ -2179,6 +2206,7 @@ func (q *sqlQuerier) ListAIBridgeSessions(ctx context.Context, arg ListAIBridgeS arg.StartedBefore, arg.InitiatorID, arg.Provider, + arg.ProviderName, arg.Model, arg.Client, arg.SessionID, diff --git a/coderd/database/queries/aibridge.sql b/coderd/database/queries/aibridge.sql index bacec83dd6..7756c7086b 100644 --- a/coderd/database/queries/aibridge.sql +++ b/coderd/database/queries/aibridge.sql @@ -133,6 +133,11 @@ WHERE WHEN @provider::text != '' THEN aibridge_interceptions.provider = @provider::text ELSE true END + -- Filter provider_name + AND CASE + WHEN @provider_name::text != '' THEN aibridge_interceptions.provider_name = @provider_name::text + ELSE true + END -- Filter model AND CASE WHEN @model::text != '' THEN aibridge_interceptions.model = @model::text @@ -177,6 +182,11 @@ WHERE WHEN @provider::text != '' THEN aibridge_interceptions.provider = @provider::text ELSE true END + -- Filter provider_name + AND CASE + WHEN @provider_name::text != '' THEN aibridge_interceptions.provider_name = @provider_name::text + ELSE true + END -- Filter model AND CASE WHEN @model::text != '' THEN aibridge_interceptions.model = @model::text @@ -418,6 +428,11 @@ WHERE WHEN @provider::text != '' THEN aibridge_interceptions.provider = @provider::text ELSE true END + -- Filter provider_name + AND CASE + WHEN @provider_name::text != '' THEN aibridge_interceptions.provider_name = @provider_name::text + ELSE true + END -- Filter model AND CASE WHEN @model::text != '' THEN aibridge_interceptions.model = @model::text @@ -505,6 +520,11 @@ session_page AS ( WHEN @provider::text != '' THEN ai.provider = @provider::text ELSE true END + -- Filter provider_name + AND CASE + WHEN @provider_name::text != '' THEN ai.provider_name = @provider_name::text + ELSE true + END -- Filter model AND CASE WHEN @model::text != '' THEN ai.model = @model::text diff --git a/coderd/searchquery/search.go b/coderd/searchquery/search.go index 1e8c69d15a..4b808f7df9 100644 --- a/coderd/searchquery/search.go +++ b/coderd/searchquery/search.go @@ -387,6 +387,7 @@ func AIBridgeInterceptions(ctx context.Context, db database.Store, query string, parser := httpapi.NewQueryParamParser() filter.InitiatorID = parseUser(ctx, db, parser, values, "initiator", actorID) filter.Provider = parser.String(values, "", "provider") + filter.ProviderName = parseAIProviderName(ctx, db, parser, values) filter.Model = parser.String(values, "", "model") filter.Client = parser.String(values, "", "client") @@ -429,6 +430,7 @@ func AIBridgeSessions(ctx context.Context, db database.Store, query string, page parser := httpapi.NewQueryParamParser() filter.InitiatorID = parseUser(ctx, db, parser, values, "initiator", actorID) filter.Provider = parser.String(values, "", "provider") + filter.ProviderName = parseAIProviderName(ctx, db, parser, values) filter.Model = parser.String(values, "", "model") filter.Client = parser.String(values, "", "client") filter.SessionID = parser.String(values, "", "session_id") @@ -700,6 +702,24 @@ func parseOrganization(ctx context.Context, db database.Store, parser *httpapi.Q }) } +// parseAIProviderName resolves a "provider_name" filter param against +// ai_providers.name. Unknown names produce a validation error so typos +// surface immediately rather than returning a silently-empty result set. +func parseAIProviderName(ctx context.Context, db database.Store, parser *httpapi.QueryParamParser, vals url.Values) string { + name := parser.String(vals, "", "provider_name") + if name == "" { + return "" + } + if _, err := db.GetAIProviderByName(ctx, name); err != nil { + parser.Errors = append(parser.Errors, codersdk.ValidationError{ + Field: "provider_name", + Detail: `Query param "provider_name" has invalid value: provider not found or unauthorized`, + }) + return "" + } + return name +} + func parseUser(ctx context.Context, db database.Store, parser *httpapi.QueryParamParser, vals url.Values, queryParam string, actorID uuid.UUID) uuid.UUID { return httpapi.ParseCustom(parser, vals, uuid.Nil, queryParam, func(v string) (uuid.UUID, error) { if v == "" { diff --git a/codersdk/aibridge.go b/codersdk/aibridge.go index 4f7f314330..4e49176171 100644 --- a/codersdk/aibridge.go +++ b/codersdk/aibridge.go @@ -175,10 +175,14 @@ type AIBridgeListSessionsFilter struct { Initiator string `json:"initiator,omitempty"` StartedBefore time.Time `json:"started_before,omitempty" format:"date-time"` StartedAfter time.Time `json:"started_after,omitempty" format:"date-time"` - Provider string `json:"provider,omitempty"` - Model string `json:"model,omitempty"` - Client string `json:"client,omitempty"` - SessionID string `json:"session_id,omitempty"` + // Provider matches the provider type column (openai, anthropic, + // copilot). Retained for backward compatibility; new clients should + // prefer ProviderName, which scopes to a specific configured row. + Provider string `json:"provider,omitempty"` + ProviderName string `json:"provider_name,omitempty"` + Model string `json:"model,omitempty"` + Client string `json:"client,omitempty"` + SessionID string `json:"session_id,omitempty"` // AfterSessionID is a cursor for pagination. It is the session ID of the // last session in the previous page. @@ -198,9 +202,13 @@ type AIBridgeListInterceptionsFilter struct { Initiator string `json:"initiator,omitempty"` StartedBefore time.Time `json:"started_before,omitempty" format:"date-time"` StartedAfter time.Time `json:"started_after,omitempty" format:"date-time"` - Provider string `json:"provider,omitempty"` - Model string `json:"model,omitempty"` - Client string `json:"client,omitempty"` + // Provider matches the provider type column (openai, anthropic, + // copilot). Retained for backward compatibility; new clients should + // prefer ProviderName, which scopes to a specific configured row. + Provider string `json:"provider,omitempty"` + ProviderName string `json:"provider_name,omitempty"` + Model string `json:"model,omitempty"` + Client string `json:"client,omitempty"` FilterQuery string `json:"q,omitempty"` } @@ -224,6 +232,9 @@ func (f AIBridgeListInterceptionsFilter) asRequestOption() RequestOption { if f.Provider != "" { params = append(params, fmt.Sprintf("provider:%q", f.Provider)) } + if f.ProviderName != "" { + params = append(params, fmt.Sprintf("provider_name:%q", f.ProviderName)) + } if f.Model != "" { params = append(params, fmt.Sprintf("model:%q", f.Model)) } @@ -257,6 +268,9 @@ func (f AIBridgeListSessionsFilter) asRequestOption() RequestOption { if f.Provider != "" { params = append(params, fmt.Sprintf("provider:%q", f.Provider)) } + if f.ProviderName != "" { + params = append(params, fmt.Sprintf("provider_name:%q", f.ProviderName)) + } if f.Model != "" { params = append(params, fmt.Sprintf("model:%q", f.Model)) } diff --git a/docs/reference/api/aibridge.md b/docs/reference/api/aibridge.md index 65580263f4..ce6ee6cb86 100644 --- a/docs/reference/api/aibridge.md +++ b/docs/reference/api/aibridge.md @@ -48,12 +48,12 @@ curl -X GET http://coder-server:8080/api/v2/aibridge/interceptions \ ### Parameters -| Name | In | Type | Required | Description | -|------------|-------|---------|----------|------------------------------------------------------------------------------------------------------------------------| -| `q` | query | string | false | Search query in the format `key:value`. Available keys are: initiator, provider, model, started_after, started_before. | -| `limit` | query | integer | false | Page limit | -| `after_id` | query | string | false | Cursor pagination after ID (cannot be used with offset) | -| `offset` | query | integer | false | Offset pagination (cannot be used with after_id) | +| Name | In | Type | Required | Description | +|------------|-------|---------|----------|---------------------------------------------------------------------------------------------------------------------------------------| +| `q` | query | string | false | Search query in the format `key:value`. Available keys are: initiator, provider, provider_name, model, started_after, started_before. | +| `limit` | query | integer | false | Page limit | +| `after_id` | query | string | false | Cursor pagination after ID (cannot be used with offset) | +| `offset` | query | integer | false | Offset pagination (cannot be used with after_id) | ### Example responses @@ -189,12 +189,12 @@ curl -X GET http://coder-server:8080/api/v2/aibridge/sessions \ ### Parameters -| Name | In | Type | Required | Description | -|--------------------|-------|---------|----------|--------------------------------------------------------------------------------------------------------------------------------------------| -| `q` | query | string | false | Search query in the format `key:value`. Available keys are: initiator, provider, model, client, session_id, started_after, started_before. | -| `limit` | query | integer | false | Page limit | -| `after_session_id` | query | string | false | Cursor pagination after session ID (cannot be used with offset) | -| `offset` | query | integer | false | Offset pagination (cannot be used with after_session_id) | +| Name | In | Type | Required | Description | +|--------------------|-------|---------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------| +| `q` | query | string | false | Search query in the format `key:value`. Available keys are: initiator, provider, provider_name, model, client, session_id, started_after, started_before. | +| `limit` | query | integer | false | Page limit | +| `after_session_id` | query | string | false | Cursor pagination after session ID (cannot be used with offset) | +| `offset` | query | integer | false | Offset pagination (cannot be used with after_session_id) | ### Example responses diff --git a/docs/reference/cli/aibridge_interceptions_list.md b/docs/reference/cli/aibridge_interceptions_list.md index cba722a43e..796032edbe 100644 --- a/docs/reference/cli/aibridge_interceptions_list.md +++ b/docs/reference/cli/aibridge_interceptions_list.md @@ -43,6 +43,14 @@ Only return interceptions started after this time. Must be before 'started-befor Only return interceptions from this provider. +### --provider-name + +| | | +|------|---------------------| +| Type | string | + +Only return interceptions from the named provider. + ### --model | | | diff --git a/enterprise/cli/aibridge.go b/enterprise/cli/aibridge.go index 0d0c4b8e08..d809580bd3 100644 --- a/enterprise/cli/aibridge.go +++ b/enterprise/cli/aibridge.go @@ -48,6 +48,7 @@ func (r *RootCmd) aibridgeInterceptionsList() *serpent.Command { startedBeforeRaw string startedAfterRaw string provider string + providerName string model string client string afterIDRaw string @@ -82,6 +83,12 @@ func (r *RootCmd) aibridgeInterceptionsList() *serpent.Command { Default: "", Value: serpent.StringOf(&provider), }, + { + Flag: "provider-name", + Description: `Only return interceptions from the named provider.`, + Default: "", + Value: serpent.StringOf(&providerName), + }, { Flag: "model", Description: `Only return interceptions from this model.`, @@ -152,6 +159,7 @@ func (r *RootCmd) aibridgeInterceptionsList() *serpent.Command { StartedBefore: startedBefore, StartedAfter: startedAfter, Provider: provider, + ProviderName: providerName, Model: model, }) if err != nil { diff --git a/enterprise/cli/testdata/coder_aibridge_interceptions_list_--help.golden b/enterprise/cli/testdata/coder_aibridge_interceptions_list_--help.golden index 5f0d43b5dc..eaf45dc169 100644 --- a/enterprise/cli/testdata/coder_aibridge_interceptions_list_--help.golden +++ b/enterprise/cli/testdata/coder_aibridge_interceptions_list_--help.golden @@ -26,6 +26,9 @@ OPTIONS: --provider string Only return interceptions from this provider. + --provider-name string + Only return interceptions from the named provider. + --started-after string Only return interceptions started after this time. Must be before 'started-before' if set. Accepts a time in the RFC 3339 format, e.g. diff --git a/enterprise/coderd/aibridge.go b/enterprise/coderd/aibridge.go index 10e52ceec1..f08fd5b536 100644 --- a/enterprise/coderd/aibridge.go +++ b/enterprise/coderd/aibridge.go @@ -103,7 +103,7 @@ func aibridgeHandler(api *API, middlewares ...func(http.Handler) http.Handler) f // @Security CoderSessionToken // @Produce json // @Tags AI Bridge -// @Param q query string false "Search query in the format `key:value`. Available keys are: initiator, provider, model, started_after, started_before." +// @Param q query string false "Search query in the format `key:value`. Available keys are: initiator, provider, provider_name, model, started_after, started_before." // @Param limit query int false "Page limit" // @Param after_id query string false "Cursor pagination after ID (cannot be used with offset)" // @Param offset query int false "Offset pagination (cannot be used with after_id)" @@ -164,6 +164,7 @@ func (api *API) aiBridgeListInterceptions(rw http.ResponseWriter, r *http.Reques StartedBefore: filter.StartedBefore, InitiatorID: filter.InitiatorID, Provider: filter.Provider, + ProviderName: filter.ProviderName, Model: filter.Model, Client: filter.Client, }) @@ -217,7 +218,7 @@ func (api *API) aiBridgeListInterceptions(rw http.ResponseWriter, r *http.Reques // @Security CoderSessionToken // @Produce json // @Tags AI Bridge -// @Param q query string false "Search query in the format `key:value`. Available keys are: initiator, provider, model, client, session_id, started_after, started_before." +// @Param q query string false "Search query in the format `key:value`. Available keys are: initiator, provider, provider_name, model, client, session_id, started_after, started_before." // @Param limit query int false "Page limit" // @Param after_session_id query string false "Cursor pagination after session ID (cannot be used with offset)" // @Param offset query int false "Offset pagination (cannot be used with after_session_id)" @@ -296,6 +297,7 @@ func (api *API) aiBridgeListSessions(rw http.ResponseWriter, r *http.Request) { StartedBefore: filter.StartedBefore, InitiatorID: filter.InitiatorID, Provider: filter.Provider, + ProviderName: filter.ProviderName, Model: filter.Model, Client: filter.Client, SessionID: filter.SessionID, diff --git a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPage.tsx b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPage.tsx index c4c4127705..c6f3ed344a 100644 --- a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPage.tsx +++ b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPage.tsx @@ -48,11 +48,11 @@ const AISessionListPage: FC = () => { }); const providerMenu = useProviderFilterMenu({ - value: filter.values.provider, + value: filter.values.provider_name, onChange: (option) => filter.update({ ...filter.values, - provider: option?.value, + provider_name: option?.value, }), }); diff --git a/site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsFilter/ProviderFilter.tsx b/site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsFilter/ProviderFilter.tsx index 2085dee3db..92082b2f46 100644 --- a/site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsFilter/ProviderFilter.tsx +++ b/site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsFilter/ProviderFilter.tsx @@ -1,4 +1,6 @@ import type { FC } from "react"; +import { API } from "#/api/api"; +import type { AIProvider } from "#/api/typesGenerated"; import { type UseFilterMenuOptions, useFilterMenu, @@ -9,29 +11,13 @@ import { } from "#/components/Filter/SelectFilter"; import { AIBridgeProviderIcon } from "../icons/AIBridgeProviderIcon"; -const AIBRIDGE_PROVIDERS: SelectFilterOption[] = [ - { - label: "OpenAI", - value: "openai", - startIcon: ( - - ), - }, - { - label: "Anthropic", - value: "anthropic", - startIcon: ( - - ), - }, - { - label: "Copilot", - value: "copilot", - startIcon: ( - - ), - }, -]; +const toFilterOption = (provider: AIProvider): SelectFilterOption => ({ + value: provider.name, + label: provider.display_name || provider.name, + startIcon: ( + + ), +}); export const useProviderFilterMenu = ({ value, @@ -39,11 +25,18 @@ export const useProviderFilterMenu = ({ enabled, }: Pick) => { return useFilterMenu({ - id: "provider", - getSelectedOption: async () => - AIBRIDGE_PROVIDERS.find((option) => option.value === value) ?? null, + id: "provider_name", + getSelectedOption: async () => { + if (!value) { + return null; + } + const providers = await API.experimental.listAIProviders(); + const match = providers.find((p) => p.name === value); + return match ? toFilterOption(match) : null; + }, getOptions: async () => { - return AIBRIDGE_PROVIDERS; + const providers = await API.experimental.listAIProviders(); + return providers.map(toFilterOption); }, value, onChange, diff --git a/site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsPage.tsx b/site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsPage.tsx index c9cbc5a57f..d8cbe15ed6 100644 --- a/site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsPage.tsx +++ b/site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsPage.tsx @@ -46,11 +46,11 @@ const RequestLogsPage: FC = () => { }); const providerMenu = useProviderFilterMenu({ - value: filter.values.provider, + value: filter.values.provider_name, onChange: (option) => filter.update({ ...filter.values, - provider: option?.value, + provider_name: option?.value, }), });