mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: support adding GitHub Copilot AI provider via UI (#25888)
Copilot is the only AI provider type that could not be added through the `/ai/settings` UI. The aibridge runtime and the env-var seeding path already supported it, but the runtime CRUD API rejected `type=copilot` and the UI omitted it entirely. The root cause is that Copilot's auth model (a per-request GitHub OAuth token, with no pre-shared key) does not fit the credential-centric add-provider flow that every other provider uses. ## Backend Allow `type=copilot` in `CreateAIProviderRequest.Validate()`, and reject `api_keys` for Copilot on both create (validation) and update (handler sentinel), mirroring the existing Bedrock guards. Copilot carries no stored credential. ## Frontend Add Copilot to the provider type picker (with the `github-copilot.svg` icon) and give the form a credential-free branch: name, display name, and a free-text endpoint defaulting to `https://api.business.githubcopilot.com`, with copy explaining that authentication happens via the user's GitHub token at request time. Copilot maps to the distinct `copilot` wire type rather than collapsing to `openai`, and the edit flow recovers it correctly. The endpoint stays required with a business-tier default; users on the individual or enterprise endpoints edit the field. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
@@ -889,6 +889,75 @@ func TestAIProvidersKeyManagement(t *testing.T) {
|
||||
require.Contains(t, sdkErr.Message, "Bedrock providers do not accept api_keys")
|
||||
})
|
||||
|
||||
t.Run("CopilotCreateWithoutKeys", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
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.
|
||||
provider, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{
|
||||
Type: codersdk.AIProviderTypeCopilot,
|
||||
Name: "keys-copilot",
|
||||
Enabled: true,
|
||||
BaseURL: "https://api.business.githubcopilot.com",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, codersdk.AIProviderTypeCopilot, provider.Type)
|
||||
require.Empty(t, provider.APIKeys)
|
||||
})
|
||||
|
||||
t.Run("CopilotRejectsCreateWithKeys", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
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.
|
||||
_, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{
|
||||
Type: codersdk.AIProviderTypeCopilot,
|
||||
Name: "keys-copilot-create",
|
||||
Enabled: true,
|
||||
BaseURL: "https://api.business.githubcopilot.com",
|
||||
APIKeys: []string{"sk-should-be-rejected"}, //nolint:gosec // test fixture, not a real credential
|
||||
})
|
||||
require.Error(t, err)
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
||||
require.Len(t, sdkErr.Validations, 1)
|
||||
require.Equal(t, "api_keys", sdkErr.Validations[0].Field)
|
||||
require.Contains(t, sdkErr.Validations[0].Detail, "type=copilot does not accept api_keys")
|
||||
})
|
||||
|
||||
t.Run("CopilotRejectsUpdateWithKeys", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
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.
|
||||
provider, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{
|
||||
Type: codersdk.AIProviderTypeCopilot,
|
||||
Name: "keys-copilot-update",
|
||||
Enabled: true,
|
||||
BaseURL: "https://api.business.githubcopilot.com",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
rejected := []codersdk.AIProviderKeyMutation{
|
||||
{APIKey: ptr.Ref("sk-copilot-no")}, //nolint:gosec // test fixture, not a real credential
|
||||
}
|
||||
_, err = client.UpdateAIProvider(ctx, provider.Name, codersdk.UpdateAIProviderRequest{
|
||||
APIKeys: &rejected,
|
||||
})
|
||||
require.Error(t, err)
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
||||
require.Contains(t, sdkErr.Message, "Copilot providers do not accept api_keys")
|
||||
})
|
||||
|
||||
t.Run("EmptyKeyRejected", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
|
||||
Reference in New Issue
Block a user