mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
a85462bd49
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)
69 lines
2.0 KiB
Go
69 lines
2.0 KiB
Go
package aibridgeproxyd
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"cdr.dev/slog/v3/sloggers/slogtest"
|
|
)
|
|
|
|
// TestReadErrorBodyForLog verifies that reading an aibridged error
|
|
// response body for logging leaves the body intact for downstream
|
|
// consumers (the proxy forwards it, and the response dumper reads it
|
|
// again), and that the logged rendering is capped.
|
|
func TestReadErrorBodyForLog(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
newResponse := func(body string) *http.Response {
|
|
return &http.Response{
|
|
StatusCode: http.StatusBadRequest,
|
|
Body: io.NopCloser(strings.NewReader(body)),
|
|
}
|
|
}
|
|
|
|
t.Run("ReturnsBodyAndRestores", func(t *testing.T) {
|
|
t.Parallel()
|
|
s := &Server{ctx: t.Context(), logger: slogtest.Make(t, nil)}
|
|
resp := newResponse(`{"error":"bad request"}`)
|
|
|
|
got := s.readErrorBodyForLog(resp, s.logger)
|
|
require.Equal(t, `{"error":"bad request"}`, got)
|
|
|
|
// The body must still be readable in full for the proxy and the
|
|
// response dumper.
|
|
restored, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, `{"error":"bad request"}`, string(restored))
|
|
})
|
|
|
|
t.Run("TruncatesLargeBodyButRestoresFull", func(t *testing.T) {
|
|
t.Parallel()
|
|
s := &Server{ctx: t.Context(), logger: slogtest.Make(t, nil)}
|
|
full := bytes.Repeat([]byte("a"), maxLoggedErrorBodyBytes+512)
|
|
resp := newResponse(string(full))
|
|
|
|
got := s.readErrorBodyForLog(resp, s.logger)
|
|
require.Len(t, got, maxLoggedErrorBodyBytes+len("...(truncated)"))
|
|
require.True(t, strings.HasSuffix(got, "...(truncated)"))
|
|
|
|
// Truncation only affects the log string; the restored body is
|
|
// the complete payload.
|
|
restored, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, full, restored)
|
|
})
|
|
|
|
t.Run("NilBody", func(t *testing.T) {
|
|
t.Parallel()
|
|
s := &Server{ctx: t.Context(), logger: slogtest.Make(t, nil)}
|
|
resp := &http.Response{StatusCode: http.StatusInternalServerError, Body: nil}
|
|
|
|
require.Equal(t, "", s.readErrorBodyForLog(resp, s.logger))
|
|
})
|
|
}
|