mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
e00e85765b
This PR merges code from `coder/aibridge` repository into `coder/coder`. It was split into 4 PRs for easier review but stacked PRs will need to be merged into this PR so all checks pass. * https://github.com/coder/coder/pull/24190 -> raw code copy (this PR, before merging PRs on top of it, it was just 1 commit: https://github.com/coder/coder/commit/70d33f33200c7e77df910957595715f81f9bec24) * https://github.com/coder/coder/pull/24570 -> update imports in `coder/coder` to use copied code * https://github.com/coder/coder/pull/24586 -> linter fixes and CI integration (also added README.md) * https://github.com/coder/coder/pull/24571 -> added exclude to scripts/check_emdash.sh check Original PR message (before PR squash): Moves coder/aibridge code into coder/coder repository. Omitted files: - `go.mod`, `go.sum`, `.gitignore`, `.github/workflows/ci.yml,` `Makefile`, `LICENSE`, `README.md` (modified README.md is added later) - `.github`, `example`, `buildinfo,` `scripts` directories Simple verification script (will list omitted files) ``` tmp=$(mktemp -d) echo "$tmp" git clone --depth=1 https://github.com/coder/aibridge "$tmp/aibridge" git clone --depth=1 --branch pb/aibridge-code-move https://github.com/coder/coder "$tmp/coder" diff -rq --exclude=.git "$tmp/aibridge" "$tmp/coder/aibridge" # rm -rf "$tmp" ```
130 lines
3.7 KiB
Go
130 lines
3.7 KiB
Go
package apidump //nolint:testpackage // shares test helpers with apidump_test.go
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"cdr.dev/slog/v3"
|
|
"cdr.dev/slog/v3/sloggers/slogtest"
|
|
"github.com/coder/quartz"
|
|
)
|
|
|
|
func TestMiddleware_StreamingResponse(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmpDir := t.TempDir()
|
|
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: false}).Leveled(slog.LevelDebug)
|
|
clk := quartz.NewMock(t)
|
|
interceptionID := uuid.New()
|
|
|
|
middleware := NewBridgeMiddleware(tmpDir, "openai", "gpt-4", interceptionID, logger, clk)
|
|
require.NotNil(t, middleware)
|
|
|
|
req, err := http.NewRequestWithContext(t.Context(), http.MethodPost, "https://api.openai.com/v1/chat/completions", bytes.NewReader([]byte(`{}`)))
|
|
require.NoError(t, err)
|
|
|
|
// Simulate a streaming response with multiple chunks
|
|
chunks := []string{
|
|
"data: {\"chunk\": 1}\n\n",
|
|
"data: {\"chunk\": 2}\n\n",
|
|
"data: {\"chunk\": 3}\n\n",
|
|
"data: [DONE]\n\n",
|
|
}
|
|
|
|
// Create a pipe to simulate streaming
|
|
pr, pw := io.Pipe()
|
|
go func() {
|
|
defer pw.Close() //nolint:revive // error handled via pipe read side
|
|
for _, chunk := range chunks {
|
|
if _, err := pw.Write([]byte(chunk)); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
resp, err := middleware(req, func(r *http.Request) (*http.Response, error) {
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Status: "200 OK",
|
|
Proto: "HTTP/1.1",
|
|
Header: http.Header{"Content-Type": []string{"text/event-stream"}},
|
|
Body: pr,
|
|
}, nil
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Read response in small chunks to simulate streaming consumption
|
|
var receivedData bytes.Buffer
|
|
buf := make([]byte, 16)
|
|
for {
|
|
n, err := resp.Body.Read(buf)
|
|
if n > 0 {
|
|
_, _ = receivedData.Write(buf[:n]) // bytes.Buffer.Write never fails
|
|
}
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
require.NoError(t, err)
|
|
}
|
|
require.NoError(t, resp.Body.Close())
|
|
|
|
// Verify we received all the data
|
|
expectedData := strings.Join(chunks, "")
|
|
require.Equal(t, expectedData, receivedData.String())
|
|
|
|
// Verify the dump file was created and contains all the streamed data
|
|
modelDir := filepath.Join(tmpDir, "openai", "gpt-4")
|
|
respDumpPath := findDumpFile(t, modelDir, SuffixResponse)
|
|
respContent, err := os.ReadFile(respDumpPath)
|
|
require.NoError(t, err)
|
|
|
|
content := string(respContent)
|
|
require.Contains(t, content, "HTTP/1.1 200 OK")
|
|
require.Contains(t, content, "Content-Type: text/event-stream")
|
|
// All chunks should be in the dump
|
|
for _, chunk := range chunks {
|
|
require.Contains(t, content, chunk)
|
|
}
|
|
}
|
|
|
|
func TestMiddleware_PreservesResponseBody(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmpDir := t.TempDir()
|
|
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: false}).Leveled(slog.LevelDebug)
|
|
clk := quartz.NewMock(t)
|
|
interceptionID := uuid.New()
|
|
|
|
middleware := NewBridgeMiddleware(tmpDir, "openai", "gpt-4", interceptionID, logger, clk)
|
|
require.NotNil(t, middleware)
|
|
|
|
req, err := http.NewRequestWithContext(t.Context(), http.MethodPost, "https://api.openai.com/v1/chat/completions", bytes.NewReader([]byte(`{}`)))
|
|
require.NoError(t, err)
|
|
|
|
originalRespBody := `{"choices": [{"message": {"content": "hi"}}]}`
|
|
resp, err := middleware(req, func(r *http.Request) (*http.Response, error) {
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Status: "200 OK",
|
|
Proto: "HTTP/1.1",
|
|
Header: http.Header{},
|
|
Body: io.NopCloser(bytes.NewReader([]byte(originalRespBody))),
|
|
}, nil
|
|
})
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
// Verify the response body is still readable after middleware
|
|
capturedBody, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, originalRespBody, string(capturedBody))
|
|
}
|