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,
}),
});