Files
coder/coderd/chats_test.go
T

1660 lines
53 KiB
Go

package coderd_test
import (
"database/sql"
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/google/uuid"
"github.com/sqlc-dev/pqtype"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/chatd"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisioner/echo"
"github.com/coder/coder/v2/testutil"
"github.com/coder/serpent"
)
func int64Ptr(v int64) *int64 {
return &v
}
func TestChats(t *testing.T) {
t.Parallel()
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
Message: "Test Chat",
})
require.NoError(t, err)
require.Equal(t, "Test Chat", chat.Title)
require.Equal(t, user.UserID, chat.OwnerID)
require.Equal(t, codersdk.ChatStatusPending, chat.Status)
})
t.Run("CreateTitleFromMessage", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
Message: "Build a new feature",
})
require.NoError(t, err)
require.Equal(t, "Build a new feature", chat.Title)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client, _, api := coderdtest.NewWithAPI(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
// Create two chats.
chatWithStatus, err := client.CreateChat(ctx, codersdk.CreateChatRequest{Message: "Chat 1"})
require.NoError(t, err)
chatWithoutStatus, err := client.CreateChat(ctx, codersdk.CreateChatRequest{Message: "Chat 2"})
require.NoError(t, err)
_, err = api.Database.UpsertChatDiffStatus(
dbauthz.AsSystemRestricted(ctx),
database.UpsertChatDiffStatusParams{
ChatID: chatWithStatus.ID,
Url: sql.NullString{String: "https://github.com/octocat/hello-world/pull/99", Valid: true},
PullRequestState: sql.NullString{String: "open", Valid: true},
ChangesRequested: true,
Additions: 17,
Deletions: 4,
ChangedFiles: 3,
RefreshedAt: time.Now().UTC(),
StaleAt: time.Now().UTC().Add(time.Minute),
},
)
require.NoError(t, err)
chats, err := client.ListChats(ctx)
require.NoError(t, err)
require.Len(t, chats, 2)
chatsByID := make(map[uuid.UUID]codersdk.Chat, len(chats))
for _, chat := range chats {
require.NotNil(t, chat.DiffStatus)
require.Equal(t, chat.ID, chat.DiffStatus.ChatID)
chatsByID[chat.ID] = chat
}
withStatus, ok := chatsByID[chatWithStatus.ID]
require.True(t, ok)
require.NotNil(t, withStatus.DiffStatus.URL)
require.Equal(t, "https://github.com/octocat/hello-world/pull/99", *withStatus.DiffStatus.URL)
require.NotNil(t, withStatus.DiffStatus.PullRequestState)
require.Equal(t, "open", *withStatus.DiffStatus.PullRequestState)
require.True(t, withStatus.DiffStatus.ChangesRequested)
require.EqualValues(t, 17, withStatus.DiffStatus.Additions)
require.EqualValues(t, 4, withStatus.DiffStatus.Deletions)
require.EqualValues(t, 3, withStatus.DiffStatus.ChangedFiles)
require.NotNil(t, withStatus.DiffStatus.RefreshedAt)
require.NotNil(t, withStatus.DiffStatus.StaleAt)
withoutStatus, ok := chatsByID[chatWithoutStatus.ID]
require.True(t, ok)
require.Nil(t, withoutStatus.DiffStatus.URL)
require.Nil(t, withoutStatus.DiffStatus.PullRequestState)
require.False(t, withoutStatus.DiffStatus.ChangesRequested)
require.EqualValues(t, 0, withoutStatus.DiffStatus.Additions)
require.EqualValues(t, 0, withoutStatus.DiffStatus.Deletions)
require.EqualValues(t, 0, withoutStatus.DiffStatus.ChangedFiles)
require.Nil(t, withoutStatus.DiffStatus.RefreshedAt)
require.Nil(t, withoutStatus.DiffStatus.StaleAt)
})
t.Run("Get", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{Message: "Test Chat"})
require.NoError(t, err)
result, err := client.GetChat(ctx, chat.ID)
require.NoError(t, err)
require.Equal(t, chat.ID, result.Chat.ID)
userMessage, ok := firstChatMessageByRole(result.Messages, "user")
require.True(t, ok)
require.Equal(t, "user", userMessage.Role)
})
t.Run("Delete", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{Message: "Test Chat"})
require.NoError(t, err)
err = client.DeleteChat(ctx, chat.ID)
require.NoError(t, err)
// Verify it's deleted.
_, err = client.GetChat(ctx, chat.ID)
require.Error(t, err)
})
t.Run("CreateMessage", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{Message: "Test Chat"})
require.NoError(t, err)
before, err := client.GetChat(ctx, chat.ID)
require.NoError(t, err)
messages, err := client.CreateChatMessage(ctx, chat.ID, codersdk.CreateChatMessageRequest{
Role: "user",
Content: json.RawMessage(`"Hello, AI!"`),
})
require.NoError(t, err)
require.False(t, messages.Queued)
require.Len(t, messages.Messages, len(before.Messages)+1)
require.Equal(t, "user", messages.Messages[len(messages.Messages)-1].Role)
// Verify messages were saved.
result, err := client.GetChat(ctx, chat.ID)
require.NoError(t, err)
require.Len(t, result.Messages, len(before.Messages)+1)
})
t.Run("GetIncludesMessageUsage", func(t *testing.T) {
t.Parallel()
client, _, api := coderdtest.NewWithAPI(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
Message: "Test usage fields",
})
require.NoError(t, err)
_, err = api.Database.InsertChatMessage(
dbauthz.AsSystemRestricted(ctx),
database.InsertChatMessageParams{
ChatID: chat.ID,
Role: "assistant",
Content: pqtype.NullRawMessage{RawMessage: json.RawMessage(`"usage response"`), Valid: true},
Hidden: false,
InputTokens: sql.NullInt64{
Int64: 120,
Valid: true,
},
OutputTokens: sql.NullInt64{
Int64: 45,
Valid: true,
},
ReasoningTokens: sql.NullInt64{
Int64: 12,
Valid: true,
},
CacheCreationTokens: sql.NullInt64{
Int64: 3,
Valid: true,
},
CacheReadTokens: sql.NullInt64{
Int64: 9,
Valid: true,
},
TotalTokens: sql.NullInt64{
Int64: 165,
Valid: true,
},
ContextLimit: sql.NullInt64{
Int64: 200000,
Valid: true,
},
},
)
require.NoError(t, err)
result, err := client.GetChat(ctx, chat.ID)
require.NoError(t, err)
assistantMessage, ok := firstChatMessageByRole(result.Messages, "assistant")
require.True(t, ok)
require.NotNil(t, assistantMessage.InputTokens)
require.Equal(t, int64(120), *assistantMessage.InputTokens)
require.NotNil(t, assistantMessage.OutputTokens)
require.Equal(t, int64(45), *assistantMessage.OutputTokens)
require.NotNil(t, assistantMessage.ReasoningTokens)
require.Equal(t, int64(12), *assistantMessage.ReasoningTokens)
require.NotNil(t, assistantMessage.CacheCreationTokens)
require.Equal(t, int64(3), *assistantMessage.CacheCreationTokens)
require.NotNil(t, assistantMessage.CacheReadTokens)
require.Equal(t, int64(9), *assistantMessage.CacheReadTokens)
require.NotNil(t, assistantMessage.TotalTokens)
require.Equal(t, int64(165), *assistantMessage.TotalTokens)
require.NotNil(t, assistantMessage.ContextLimit)
require.Equal(t, int64(200000), *assistantMessage.ContextLimit)
userMessage, ok := firstChatMessageByRole(result.Messages, "user")
require.True(t, ok)
require.Nil(t, userMessage.InputTokens)
require.Nil(t, userMessage.OutputTokens)
require.Nil(t, userMessage.ReasoningTokens)
require.Nil(t, userMessage.CacheCreationTokens)
require.Nil(t, userMessage.CacheReadTokens)
require.Nil(t, userMessage.TotalTokens)
require.Nil(t, userMessage.ContextLimit)
})
t.Run("NotFound", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
// Use a random UUID that doesn't exist.
randomID := uuid.New()
_, err := client.GetChat(ctx, randomID)
require.Error(t, err)
})
}
func TestChatDiffStatus(t *testing.T) {
t.Parallel()
t.Run("NoPullRequestDetected", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
Message: "Summarize the latest work.",
})
require.NoError(t, err)
status, err := client.GetChatDiffStatus(ctx, chat.ID)
require.NoError(t, err)
require.Equal(t, chat.ID, status.ChatID)
require.Nil(t, status.URL)
require.Nil(t, status.PullRequestState)
require.False(t, status.ChangesRequested)
require.EqualValues(t, 0, status.Additions)
require.EqualValues(t, 0, status.Deletions)
require.EqualValues(t, 0, status.ChangedFiles)
require.Nil(t, status.RefreshedAt)
require.Nil(t, status.StaleAt)
})
t.Run("BranchRefWithoutPullRequest", func(t *testing.T) {
t.Parallel()
client, _, api := coderdtest.NewWithAPI(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
githubServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/repos/octocat/hello-world/pulls" {
http.NotFound(rw, r)
return
}
rw.Header().Set("Content-Type", "application/json")
_, _ = rw.Write([]byte(`[]`))
}))
t.Cleanup(githubServer.Close)
githubURL, err := url.Parse(githubServer.URL)
require.NoError(t, err)
rewriteTransport := roundTripperFunc(func(req *http.Request) (*http.Response, error) {
if req.URL.Host == "api.github.com" {
cloned := req.Clone(req.Context())
cloned.URL = &url.URL{
Scheme: githubURL.Scheme,
Host: githubURL.Host,
Path: req.URL.Path,
RawPath: req.URL.RawPath,
RawQuery: req.URL.RawQuery,
}
return http.DefaultTransport.RoundTrip(cloned)
}
return http.DefaultTransport.RoundTrip(req)
})
api.HTTPClient = &http.Client{Transport: rewriteTransport}
chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
Message: "Show me branch-only diff status.",
})
require.NoError(t, err)
_, err = api.Database.UpsertChatDiffStatusReference(
dbauthz.AsSystemRestricted(ctx),
database.UpsertChatDiffStatusReferenceParams{
ChatID: chat.ID,
GitBranch: "feature/branch-only",
GitRemoteOrigin: "https://github.com/octocat/hello-world.git",
StaleAt: time.Now().UTC().Add(-time.Minute),
},
)
require.NoError(t, err)
status, err := client.GetChatDiffStatus(ctx, chat.ID)
require.NoError(t, err)
require.Equal(t, chat.ID, status.ChatID)
require.NotNil(t, status.URL)
require.Equal(t, "https://github.com/octocat/hello-world/tree/feature%2Fbranch-only", *status.URL)
require.Nil(t, status.PullRequestState)
require.False(t, status.ChangesRequested)
require.EqualValues(t, 0, status.Additions)
require.EqualValues(t, 0, status.Deletions)
require.EqualValues(t, 0, status.ChangedFiles)
require.Nil(t, status.RefreshedAt)
require.NotNil(t, status.StaleAt)
})
t.Run("RefreshAndCache", func(t *testing.T) {
t.Parallel()
client, _, api := coderdtest.NewWithAPI(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
var apiCallCount atomic.Int32
githubServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
apiCallCount.Add(1)
switch r.URL.Path {
case "/repos/octocat/hello-world/pulls/42":
rw.Header().Set("Content-Type", "application/json")
_, _ = rw.Write([]byte(`{
"state": "open",
"additions": 12,
"deletions": 3,
"changed_files": 4
}`))
case "/repos/octocat/hello-world/pulls/42/reviews":
rw.Header().Set("Content-Type", "application/json")
_, _ = rw.Write([]byte(`[
{
"id": 10,
"state": "CHANGES_REQUESTED",
"user": { "login": "reviewer-one" }
},
{
"id": 20,
"state": "APPROVED",
"user": { "login": "reviewer-two" }
}
]`))
default:
http.NotFound(rw, r)
}
}))
t.Cleanup(githubServer.Close)
githubURL, err := url.Parse(githubServer.URL)
require.NoError(t, err)
rewriteTransport := roundTripperFunc(func(req *http.Request) (*http.Response, error) {
if req.URL.Host == "api.github.com" {
cloned := req.Clone(req.Context())
cloned.URL = &url.URL{
Scheme: githubURL.Scheme,
Host: githubURL.Host,
Path: req.URL.Path,
RawPath: req.URL.RawPath,
RawQuery: req.URL.RawQuery,
}
return http.DefaultTransport.RoundTrip(cloned)
}
return http.DefaultTransport.RoundTrip(req)
})
api.HTTPClient = &http.Client{
Transport: rewriteTransport,
}
chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
Message: "Please review the latest changes.",
})
require.NoError(t, err)
_, err = api.Database.UpsertChatDiffStatusReference(
dbauthz.AsSystemRestricted(ctx),
database.UpsertChatDiffStatusReferenceParams{
ChatID: chat.ID,
Url: sql.NullString{String: "https://github.com/octocat/hello-world/pull/42", Valid: true},
StaleAt: time.Now().UTC().Add(-time.Minute),
},
)
require.NoError(t, err)
status, err := client.GetChatDiffStatus(ctx, chat.ID)
require.NoError(t, err)
require.Equal(t, chat.ID, status.ChatID)
require.NotNil(t, status.URL)
require.Equal(t, "https://github.com/octocat/hello-world/pull/42", *status.URL)
require.NotNil(t, status.PullRequestState)
require.Equal(t, "open", *status.PullRequestState)
require.True(t, status.ChangesRequested)
require.EqualValues(t, 12, status.Additions)
require.EqualValues(t, 3, status.Deletions)
require.EqualValues(t, 4, status.ChangedFiles)
require.NotNil(t, status.RefreshedAt)
require.NotNil(t, status.StaleAt)
require.EqualValues(t, 2, apiCallCount.Load())
_, err = client.GetChatDiffStatus(ctx, chat.ID)
require.NoError(t, err)
require.EqualValues(t, 2, apiCallCount.Load())
})
}
func TestChatDiffContents(t *testing.T) {
t.Parallel()
t.Run("FromCachedPullRequestURL", func(t *testing.T) {
t.Parallel()
client, _, api := coderdtest.NewWithAPI(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
const expectedDiff = "diff --git a/main.go b/main.go\n+fmt.Println(\"hello\")\n"
githubServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/repos/octocat/hello-world/pulls/42" {
http.NotFound(rw, r)
return
}
if strings.Contains(r.Header.Get("Accept"), "application/vnd.github.diff") {
rw.Header().Set("Content-Type", "text/plain; charset=utf-8")
_, _ = rw.Write([]byte(expectedDiff))
return
}
rw.Header().Set("Content-Type", "application/json")
_, _ = rw.Write([]byte(`{
"state": "open",
"additions": 1,
"deletions": 0,
"changed_files": 1
}`))
}))
t.Cleanup(githubServer.Close)
githubURL, err := url.Parse(githubServer.URL)
require.NoError(t, err)
rewriteTransport := roundTripperFunc(func(req *http.Request) (*http.Response, error) {
if req.URL.Host == "api.github.com" {
cloned := req.Clone(req.Context())
cloned.URL = &url.URL{
Scheme: githubURL.Scheme,
Host: githubURL.Host,
Path: req.URL.Path,
RawPath: req.URL.RawPath,
RawQuery: req.URL.RawQuery,
}
return http.DefaultTransport.RoundTrip(cloned)
}
return http.DefaultTransport.RoundTrip(req)
})
api.HTTPClient = &http.Client{
Transport: rewriteTransport,
}
chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
Message: "Please inspect the latest pull request.",
})
require.NoError(t, err)
_, err = api.Database.UpsertChatDiffStatusReference(
dbauthz.AsSystemRestricted(ctx),
database.UpsertChatDiffStatusReferenceParams{
ChatID: chat.ID,
Url: sql.NullString{String: "https://github.com/octocat/hello-world/pull/42", Valid: true},
StaleAt: time.Now().UTC().Add(-time.Minute),
},
)
require.NoError(t, err)
diff, err := client.GetChatDiffContents(ctx, chat.ID)
require.NoError(t, err)
require.Equal(t, chat.ID, diff.ChatID)
require.NotNil(t, diff.PullRequestURL)
require.Equal(t, "https://github.com/octocat/hello-world/pull/42", *diff.PullRequestURL)
require.Equal(t, expectedDiff, diff.Diff)
})
t.Run("FromBranchReferenceWithoutPullRequest", func(t *testing.T) {
t.Parallel()
client, _, api := coderdtest.NewWithAPI(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
const expectedDiff = "diff --git a/main.go b/main.go\n+fmt.Println(\"branch\")\n"
githubServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
switch {
case r.URL.Path == "/repos/octocat/hello-world/pulls":
rw.Header().Set("Content-Type", "application/json")
_, _ = rw.Write([]byte(`[]`))
return
case r.URL.Path == "/repos/octocat/hello-world":
rw.Header().Set("Content-Type", "application/json")
_, _ = rw.Write([]byte(`{"default_branch":"main"}`))
return
case strings.HasPrefix(r.URL.Path, "/repos/octocat/hello-world/compare/"):
if strings.Contains(r.Header.Get("Accept"), "application/vnd.github.diff") {
rw.Header().Set("Content-Type", "text/plain; charset=utf-8")
_, _ = rw.Write([]byte(expectedDiff))
return
}
}
http.NotFound(rw, r)
}))
t.Cleanup(githubServer.Close)
githubURL, err := url.Parse(githubServer.URL)
require.NoError(t, err)
rewriteTransport := roundTripperFunc(func(req *http.Request) (*http.Response, error) {
if req.URL.Host == "api.github.com" {
cloned := req.Clone(req.Context())
cloned.URL = &url.URL{
Scheme: githubURL.Scheme,
Host: githubURL.Host,
Path: req.URL.Path,
RawPath: req.URL.RawPath,
RawQuery: req.URL.RawQuery,
}
return http.DefaultTransport.RoundTrip(cloned)
}
return http.DefaultTransport.RoundTrip(req)
})
api.HTTPClient = &http.Client{
Transport: rewriteTransport,
}
chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
Message: "Please inspect branch changes.",
})
require.NoError(t, err)
_, err = api.Database.UpsertChatDiffStatusReference(
dbauthz.AsSystemRestricted(ctx),
database.UpsertChatDiffStatusReferenceParams{
ChatID: chat.ID,
GitBranch: "feature/branch-only",
GitRemoteOrigin: "https://github.com/octocat/hello-world.git",
StaleAt: time.Now().UTC().Add(-time.Minute),
},
)
require.NoError(t, err)
status, err := client.GetChatDiffStatus(ctx, chat.ID)
require.NoError(t, err)
require.NotNil(t, status.URL)
require.Equal(t, "https://github.com/octocat/hello-world/tree/feature%2Fbranch-only", *status.URL)
require.Nil(t, status.PullRequestState)
diff, err := client.GetChatDiffContents(ctx, chat.ID)
require.NoError(t, err)
require.Equal(t, chat.ID, diff.ChatID)
require.NotNil(t, diff.Provider)
require.Equal(t, "github", *diff.Provider)
require.NotNil(t, diff.RemoteOrigin)
require.Equal(t, "https://github.com/octocat/hello-world", *diff.RemoteOrigin)
require.NotNil(t, diff.Branch)
require.Equal(t, "feature/branch-only", *diff.Branch)
require.Nil(t, diff.PullRequestURL)
require.Equal(t, expectedDiff, diff.Diff)
})
}
func TestCreateChat_SystemPromptUnauthorized(t *testing.T) {
t.Parallel()
ownerClient := coderdtest.New(t, nil)
owner := coderdtest.CreateFirstUser(t, ownerClient)
memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
ctx := testutil.Context(t, testutil.WaitShort)
_, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{
Message: "Test chat",
SystemPrompt: "Only admins should be able to set this.",
})
require.Error(t, err)
require.Equal(t, http.StatusForbidden, coderdtest.SDKError(t, err).StatusCode())
}
func TestCreateChat_DefaultSystemPrompt(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
Input: &codersdk.ChatInput{
Parts: []codersdk.ChatInputPart{
{Type: codersdk.ChatInputPartTypeText, Text: "Build a test feature."},
},
},
})
require.NoError(t, err)
result, err := client.GetChat(ctx, chat.ID)
require.NoError(t, err)
require.GreaterOrEqual(t, len(result.Messages), 2)
require.Equal(t, "system", result.Messages[0].Role)
var gotSystemPrompt string
require.NoError(t, json.Unmarshal(result.Messages[0].Content, &gotSystemPrompt))
require.Equal(t, chatd.DefaultSystemPrompt, gotSystemPrompt)
}
func TestCreateChat_DefaultSystemPromptFallback(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
Input: &codersdk.ChatInput{
Parts: []codersdk.ChatInputPart{
{Type: codersdk.ChatInputPartTypeText, Text: "Build a test feature."},
},
},
SystemPrompt: "\n\t ",
})
require.NoError(t, err)
result, err := client.GetChat(ctx, chat.ID)
require.NoError(t, err)
require.GreaterOrEqual(t, len(result.Messages), 2)
require.Equal(t, "system", result.Messages[0].Role)
var gotSystemPrompt string
require.NoError(t, json.Unmarshal(result.Messages[0].Content, &gotSystemPrompt))
require.Equal(t, chatd.DefaultSystemPrompt, gotSystemPrompt)
}
func TestCreateChat_StructuredInput(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
Input: &codersdk.ChatInput{
Parts: []codersdk.ChatInputPart{
{Type: codersdk.ChatInputPartTypeText, Text: "Plan rollout"},
{Type: codersdk.ChatInputPartTypeText, Text: "for chat defaults"},
},
},
})
require.NoError(t, err)
require.Equal(t, "Plan rollout for chat defaults", chat.Title)
result, err := client.GetChat(ctx, chat.ID)
require.NoError(t, err)
userMessage, ok := firstChatMessageByRole(result.Messages, "user")
require.True(t, ok)
require.Len(t, userMessage.Parts, 2)
require.Equal(t, codersdk.ChatMessagePartTypeText, userMessage.Parts[0].Type)
require.Equal(t, "Plan rollout", userMessage.Parts[0].Text)
require.Equal(t, codersdk.ChatMessagePartTypeText, userMessage.Parts[1].Type)
require.Equal(t, "for chat defaults", userMessage.Parts[1].Text)
}
func TestCreateChat_StructuredInputUnsupportedType(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
_, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
Input: &codersdk.ChatInput{
Parts: []codersdk.ChatInputPart{
{Type: codersdk.ChatInputPartType("file"), Text: "README.md"},
},
},
})
require.Error(t, err)
require.Equal(t, http.StatusBadRequest, coderdtest.SDKError(t, err).StatusCode())
}
func TestCreateChat_SystemPromptAuthorized(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
systemPrompt := "You are a strict coding assistant."
chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
Input: &codersdk.ChatInput{
Parts: []codersdk.ChatInputPart{
{Type: codersdk.ChatInputPartTypeText, Text: "Build a test feature."},
},
},
SystemPrompt: systemPrompt,
})
require.NoError(t, err)
result, err := client.GetChat(ctx, chat.ID)
require.NoError(t, err)
require.Len(t, result.Messages, 2)
require.Equal(t, "system", result.Messages[0].Role)
require.Equal(t, "user", result.Messages[1].Role)
var gotSystemPrompt string
require.NoError(t, json.Unmarshal(result.Messages[0].Content, &gotSystemPrompt))
require.Equal(t, systemPrompt, gotSystemPrompt)
}
func TestCreateChat_WorkspaceSelectionAuthorization(t *testing.T) {
t.Parallel()
ownerClient := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
})
owner := coderdtest.CreateFirstUser(t, ownerClient)
memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
workspace, workspaceAgentID := createWorkspaceWithAgent(t, ownerClient, owner.OrganizationID)
t.Run("WorkspaceIDAuthorized", func(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitShort)
workspaceID := workspace.ID
chat, err := ownerClient.CreateChat(ctx, codersdk.CreateChatRequest{
Message: "Use my workspace.",
WorkspaceID: &workspaceID,
})
require.NoError(t, err)
require.NotNil(t, chat.WorkspaceID)
require.Equal(t, workspaceID, *chat.WorkspaceID)
})
t.Run("WorkspaceIDUnauthorized", func(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitShort)
workspaceID := workspace.ID
_, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{
Message: "Try to use someone else's workspace.",
WorkspaceID: &workspaceID,
})
require.Error(t, err)
sdkErr := coderdtest.SDKError(t, err)
require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
require.Equal(t, "Workspace not found or you do not have access to this resource", sdkErr.Message)
})
t.Run("WorkspaceAgentIDAuthorized", func(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitShort)
workspaceID := workspace.ID
agentID := workspaceAgentID
chat, err := ownerClient.CreateChat(ctx, codersdk.CreateChatRequest{
Message: "Use my workspace agent.",
WorkspaceID: &workspaceID,
WorkspaceAgentID: &agentID,
})
require.NoError(t, err)
require.NotNil(t, chat.WorkspaceAgentID)
require.Equal(t, agentID, *chat.WorkspaceAgentID)
})
t.Run("WorkspaceAgentIDUnauthorized", func(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitShort)
agentID := workspaceAgentID
_, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{
Message: "Try to use someone else's workspace agent.",
WorkspaceAgentID: &agentID,
})
require.Error(t, err)
sdkErr := coderdtest.SDKError(t, err)
require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
require.Equal(t, "Workspace agent not found or you do not have access to this resource", sdkErr.Message)
})
}
func TestCreateChat_LocalWorkspaceDisabled(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
_, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
Message: "Bootstrap local workspace chat.",
WorkspaceMode: codersdk.ChatWorkspaceModeLocal,
})
require.Error(t, err)
require.Equal(t, http.StatusForbidden, coderdtest.SDKError(t, err).StatusCode())
}
func TestCreateChat_HierarchyMetadata(t *testing.T) {
t.Parallel()
t.Run("CreateRootChat", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
rootChat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
Message: "Create a root chat.",
})
require.NoError(t, err)
require.Nil(t, rootChat.ParentChatID)
require.NotNil(t, rootChat.RootChatID)
require.Equal(t, rootChat.ID, *rootChat.RootChatID)
got, err := client.GetChat(ctx, rootChat.ID)
require.NoError(t, err)
require.Nil(t, got.Chat.ParentChatID)
require.NotNil(t, got.Chat.RootChatID)
require.Equal(t, rootChat.ID, *got.Chat.RootChatID)
chats, err := client.ListChats(ctx)
require.NoError(t, err)
require.Len(t, chats, 1)
require.Nil(t, chats[0].ParentChatID)
require.NotNil(t, chats[0].RootChatID)
require.Equal(t, rootChat.ID, *chats[0].RootChatID)
})
t.Run("CreateChildChatWithValidParent", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
parent, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
Message: "Create a parent chat.",
})
require.NoError(t, err)
parentID := parent.ID
child, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
Message: "Create a child chat.",
ParentChatID: &parentID,
})
require.NoError(t, err)
require.NotNil(t, child.ParentChatID)
require.Equal(t, parent.ID, *child.ParentChatID)
require.NotNil(t, child.RootChatID)
require.Equal(t, parent.ID, *child.RootChatID)
gotChild, err := client.GetChat(ctx, child.ID)
require.NoError(t, err)
require.NotNil(t, gotChild.Chat.ParentChatID)
require.Equal(t, parent.ID, *gotChild.Chat.ParentChatID)
require.NotNil(t, gotChild.Chat.RootChatID)
require.Equal(t, parent.ID, *gotChild.Chat.RootChatID)
chats, err := client.ListChats(ctx)
require.NoError(t, err)
require.Len(t, chats, 2)
byID := make(map[uuid.UUID]codersdk.Chat, len(chats))
for _, chat := range chats {
byID[chat.ID] = chat
}
require.Contains(t, byID, parent.ID)
require.Contains(t, byID, child.ID)
require.Nil(t, byID[parent.ID].ParentChatID)
require.NotNil(t, byID[parent.ID].RootChatID)
require.Equal(t, parent.ID, *byID[parent.ID].RootChatID)
require.NotNil(t, byID[child.ID].ParentChatID)
require.Equal(t, parent.ID, *byID[child.ID].ParentChatID)
require.NotNil(t, byID[child.ID].RootChatID)
require.Equal(t, parent.ID, *byID[child.ID].RootChatID)
})
t.Run("RejectParentFromAnotherOwner", func(t *testing.T) {
t.Parallel()
ownerClient := coderdtest.New(t, nil)
owner := coderdtest.CreateFirstUser(t, ownerClient)
memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
ctx := testutil.Context(t, testutil.WaitShort)
parent, err := ownerClient.CreateChat(ctx, codersdk.CreateChatRequest{
Message: "Owner parent chat.",
})
require.NoError(t, err)
parentID := parent.ID
_, err = memberClient.CreateChat(ctx, codersdk.CreateChatRequest{
Message: "Attempt child chat.",
ParentChatID: &parentID,
})
require.Error(t, err)
sdkErr := coderdtest.SDKError(t, err)
require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
require.Equal(t, "Parent chat not found or you do not have access to this resource", sdkErr.Message)
})
t.Run("RejectWorkspaceMismatch", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
})
user := coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitLong)
parentWorkspace, parentWorkspaceAgentID := createWorkspaceWithAgent(t, client, user.OrganizationID)
otherWorkspace, _ := createWorkspaceWithAgent(t, client, user.OrganizationID)
parentWorkspaceID := parentWorkspace.ID
parentAgentID := parentWorkspaceAgentID
parent, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
Message: "Parent chat with workspace.",
WorkspaceID: &parentWorkspaceID,
WorkspaceAgentID: &parentAgentID,
})
require.NoError(t, err)
parentID := parent.ID
otherWorkspaceID := otherWorkspace.ID
_, err = client.CreateChat(ctx, codersdk.CreateChatRequest{
Message: "Child chat with mismatched workspace.",
ParentChatID: &parentID,
WorkspaceID: &otherWorkspaceID,
})
require.Error(t, err)
sdkErr := coderdtest.SDKError(t, err)
require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
require.Equal(t, "Child chat workspace must match parent chat workspace.", sdkErr.Message)
})
}
func TestChatModels_NoEnabledModels(t *testing.T) {
client := coderdtest.New(t, &coderdtest.Options{
DeploymentValues: coderdtest.DeploymentValues(t, func(cfg *codersdk.DeploymentValues) {
cfg.AI.BridgeConfig.OpenAI.Key = serpent.String("")
cfg.AI.BridgeConfig.Anthropic.Key = serpent.String("")
}),
})
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
catalog, err := client.ListChatModels(ctx)
require.NoError(t, err)
require.Len(t, catalog.Providers, 8)
for _, provider := range catalog.Providers {
require.False(t, provider.Available)
require.Equal(t, codersdk.ChatModelProviderUnavailableMissingAPIKey, provider.UnavailableReason)
require.Empty(t, provider.Models)
}
}
func TestChatModels_NoEnabledModelsUsesMergedProviderKeys(t *testing.T) {
client := coderdtest.New(t, &coderdtest.Options{
DeploymentValues: coderdtest.DeploymentValues(t, func(cfg *codersdk.DeploymentValues) {
cfg.AI.BridgeConfig.OpenAI.Key = serpent.String("env-openai")
cfg.AI.BridgeConfig.Anthropic.Key = serpent.String("")
}),
})
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
_, err := client.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
Provider: "anthropic",
APIKey: "provider-anthropic",
})
require.NoError(t, err)
catalog, err := client.ListChatModels(ctx)
require.NoError(t, err)
require.Len(t, catalog.Providers, 8)
byProvider := make(map[string]codersdk.ChatModelProvider, len(catalog.Providers))
for _, provider := range catalog.Providers {
byProvider[provider.Provider] = provider
}
require.True(t, byProvider["openai"].Available)
require.Empty(t, byProvider["openai"].UnavailableReason)
require.Empty(t, byProvider["openai"].Models)
require.True(t, byProvider["anthropic"].Available)
require.Empty(t, byProvider["anthropic"].UnavailableReason)
require.Empty(t, byProvider["anthropic"].Models)
for _, provider := range []string{
"azure",
"bedrock",
"google",
"openai-compat",
"openrouter",
"vercel",
} {
entry := byProvider[provider]
require.False(t, entry.Available)
require.Equal(t, codersdk.ChatModelProviderUnavailableMissingAPIKey, entry.UnavailableReason)
require.Empty(t, entry.Models)
}
}
func TestChatProviders(t *testing.T) {
t.Parallel()
t.Run("CreateAdminAllowed", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
provider, err := client.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
Provider: "openai",
DisplayName: "OpenAI",
APIKey: "openai-key",
BaseURL: "https://proxy.example.com/v1",
})
require.NoError(t, err)
require.Equal(t, "openai", provider.Provider)
require.Equal(t, "OpenAI", provider.DisplayName)
require.True(t, provider.Enabled)
require.True(t, provider.HasAPIKey)
require.Equal(t, "https://proxy.example.com/v1", provider.BaseURL)
})
t.Run("CreateNonAdminForbidden", func(t *testing.T) {
t.Parallel()
ownerClient := coderdtest.New(t, nil)
owner := coderdtest.CreateFirstUser(t, ownerClient)
memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
ctx := testutil.Context(t, testutil.WaitShort)
_, err := memberClient.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
Provider: "openai",
APIKey: "openai-key",
})
require.Error(t, err)
require.Equal(t, http.StatusForbidden, coderdtest.SDKError(t, err).StatusCode())
})
t.Run("CreateConflict", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
_, err := client.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
Provider: "openai",
APIKey: "openai-key",
})
require.NoError(t, err)
_, err = client.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
Provider: "openai",
APIKey: "openai-key-2",
})
require.Error(t, err)
require.Equal(t, http.StatusConflict, coderdtest.SDKError(t, err).StatusCode())
})
t.Run("UpdateAdminAllowed", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
provider, err := client.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
Provider: "openai",
DisplayName: "OpenAI",
APIKey: "openai-key",
})
require.NoError(t, err)
enabled := false
baseURL := "https://proxy-updated.example.com/v1"
updated, err := client.UpdateChatProvider(ctx, provider.ID, codersdk.UpdateChatProviderConfigRequest{
DisplayName: "OpenAI Updated",
Enabled: &enabled,
BaseURL: &baseURL,
})
require.NoError(t, err)
require.Equal(t, provider.ID, updated.ID)
require.Equal(t, "OpenAI Updated", updated.DisplayName)
require.False(t, updated.Enabled)
require.True(t, updated.HasAPIKey)
require.Equal(t, "https://proxy-updated.example.com/v1", updated.BaseURL)
emptyBaseURL := ""
updated, err = client.UpdateChatProvider(ctx, provider.ID, codersdk.UpdateChatProviderConfigRequest{
BaseURL: &emptyBaseURL,
})
require.NoError(t, err)
require.Empty(t, updated.BaseURL)
})
t.Run("InvalidBaseURLRejected", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
_, err := client.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
Provider: "openai",
APIKey: "openai-key",
BaseURL: "not-a-url",
})
require.Error(t, err)
require.Equal(t, http.StatusBadRequest, coderdtest.SDKError(t, err).StatusCode())
provider, err := client.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
Provider: "openai",
APIKey: "openai-key",
})
require.NoError(t, err)
invalidBaseURL := "ftp://proxy.example.com"
_, err = client.UpdateChatProvider(ctx, provider.ID, codersdk.UpdateChatProviderConfigRequest{
BaseURL: &invalidBaseURL,
})
require.Error(t, err)
require.Equal(t, http.StatusBadRequest, coderdtest.SDKError(t, err).StatusCode())
})
t.Run("UpdateNonAdminForbidden", func(t *testing.T) {
t.Parallel()
ownerClient := coderdtest.New(t, nil)
owner := coderdtest.CreateFirstUser(t, ownerClient)
memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
ctx := testutil.Context(t, testutil.WaitShort)
provider, err := ownerClient.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
Provider: "openai",
APIKey: "openai-key",
})
require.NoError(t, err)
enabled := false
_, err = memberClient.UpdateChatProvider(ctx, provider.ID, codersdk.UpdateChatProviderConfigRequest{
DisplayName: "Should Fail",
Enabled: &enabled,
})
require.Error(t, err)
require.Equal(t, http.StatusForbidden, coderdtest.SDKError(t, err).StatusCode())
})
t.Run("DeleteAdminAllowed", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
provider, err := client.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
Provider: "openai",
APIKey: "openai-key",
})
require.NoError(t, err)
err = client.DeleteChatProvider(ctx, provider.ID)
require.NoError(t, err)
providers, err := client.ListChatProviders(ctx)
require.NoError(t, err)
require.Len(t, providers, 8)
for _, provider := range providers {
require.NotEqual(t, codersdk.ChatProviderConfigSourceDatabase, provider.Source)
}
})
t.Run("DeleteNonAdminForbidden", func(t *testing.T) {
t.Parallel()
ownerClient := coderdtest.New(t, nil)
owner := coderdtest.CreateFirstUser(t, ownerClient)
memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
ctx := testutil.Context(t, testutil.WaitShort)
provider, err := ownerClient.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
Provider: "openai",
APIKey: "openai-key",
})
require.NoError(t, err)
err = memberClient.DeleteChatProvider(ctx, provider.ID)
require.Error(t, err)
require.Equal(t, http.StatusForbidden, coderdtest.SDKError(t, err).StatusCode())
})
}
func TestChatProviders_ListIncludesSupportedProvidersAndEnvPresets(t *testing.T) {
client := coderdtest.New(t, &coderdtest.Options{
DeploymentValues: coderdtest.DeploymentValues(t, func(cfg *codersdk.DeploymentValues) {
cfg.AI.BridgeConfig.OpenAI.Key = serpent.String("env-openai-key")
cfg.AI.BridgeConfig.Anthropic.Key = serpent.String("")
}),
})
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
providers, err := client.ListChatProviders(ctx)
require.NoError(t, err)
require.Len(t, providers, 8)
byProvider := make(map[string]codersdk.ChatProviderConfig, len(providers))
for _, provider := range providers {
byProvider[provider.Provider] = provider
}
openai := byProvider["openai"]
require.Equal(t, codersdk.ChatProviderConfigSourceEnvPreset, openai.Source)
require.Equal(t, uuid.Nil, openai.ID)
require.True(t, openai.Enabled)
require.True(t, openai.HasAPIKey)
anthropic := byProvider["anthropic"]
require.Equal(t, codersdk.ChatProviderConfigSourceSupported, anthropic.Source)
require.Equal(t, uuid.Nil, anthropic.ID)
require.False(t, anthropic.Enabled)
require.False(t, anthropic.HasAPIKey)
_, err = client.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
Provider: "openrouter",
APIKey: "openrouter-key",
})
require.NoError(t, err)
providers, err = client.ListChatProviders(ctx)
require.NoError(t, err)
require.Len(t, providers, 8)
byProvider = make(map[string]codersdk.ChatProviderConfig, len(providers))
for _, provider := range providers {
byProvider[provider.Provider] = provider
}
openrouter := byProvider["openrouter"]
require.Equal(t, codersdk.ChatProviderConfigSourceDatabase, openrouter.Source)
require.NotEqual(t, uuid.Nil, openrouter.ID)
require.True(t, openrouter.Enabled)
require.True(t, openrouter.HasAPIKey)
}
func TestChatModelConfigs(t *testing.T) {
t.Parallel()
t.Run("CreateAdminAllowed", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
_, err := client.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
Provider: "openai",
APIKey: "openai-key",
})
require.NoError(t, err)
config, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
Provider: "openai",
Model: "gpt-4.1",
DisplayName: "GPT 4.1",
ContextLimit: int64Ptr(200000),
})
require.NoError(t, err)
require.Equal(t, "openai", config.Provider)
require.Equal(t, "gpt-4.1", config.Model)
require.Equal(t, "GPT 4.1", config.DisplayName)
require.True(t, config.Enabled)
})
t.Run("CreateDuplicateProviderModelAllowed", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
_, err := client.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
Provider: "openai",
APIKey: "openai-key",
})
require.NoError(t, err)
first, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
Provider: "openai",
Model: "gpt-4.1",
DisplayName: "GPT 4.1 A",
ContextLimit: int64Ptr(200000),
})
require.NoError(t, err)
second, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
Provider: "openai",
Model: "gpt-4.1",
DisplayName: "GPT 4.1 B",
ContextLimit: int64Ptr(200000),
})
require.NoError(t, err)
require.NotEqual(t, first.ID, second.ID)
latestDisplayName := "GPT 4.1 Latest"
second, err = client.UpdateChatModelConfig(ctx, second.ID, codersdk.UpdateChatModelConfigRequest{
DisplayName: latestDisplayName,
})
require.NoError(t, err)
configs, err := client.ListChatModelConfigs(ctx)
require.NoError(t, err)
duplicateCount := 0
for _, config := range configs {
if config.Provider == "openai" && config.Model == "gpt-4.1" {
duplicateCount++
}
}
require.Equal(t, 2, duplicateCount)
catalog, err := client.ListChatModels(ctx)
require.NoError(t, err)
var openaiProvider *codersdk.ChatModelProvider
for index := range catalog.Providers {
if catalog.Providers[index].Provider == "openai" {
openaiProvider = &catalog.Providers[index]
break
}
}
require.NotNil(t, openaiProvider)
modelCount := 0
for _, model := range openaiProvider.Models {
if model.Model == "gpt-4.1" {
modelCount++
require.Equal(t, latestDisplayName, model.DisplayName)
}
}
require.Equal(t, 1, modelCount)
})
t.Run("CreateNonAdminForbidden", func(t *testing.T) {
t.Parallel()
ownerClient := coderdtest.New(t, nil)
owner := coderdtest.CreateFirstUser(t, ownerClient)
memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
ctx := testutil.Context(t, testutil.WaitShort)
_, err := memberClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
Provider: "openai",
Model: "gpt-4.1",
ContextLimit: int64Ptr(200000),
})
require.Error(t, err)
require.Equal(t, http.StatusForbidden, coderdtest.SDKError(t, err).StatusCode())
})
t.Run("UpdateAdminAllowed", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
_, err := client.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
Provider: "openai",
APIKey: "openai-key",
})
require.NoError(t, err)
config, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
Provider: "openai",
Model: "gpt-4.1",
DisplayName: "GPT 4.1",
ContextLimit: int64Ptr(200000),
})
require.NoError(t, err)
enabled := false
updated, err := client.UpdateChatModelConfig(ctx, config.ID, codersdk.UpdateChatModelConfigRequest{
Model: "gpt-4.1-mini",
DisplayName: "GPT 4.1 Mini",
Enabled: &enabled,
})
require.NoError(t, err)
require.Equal(t, config.ID, updated.ID)
require.Equal(t, "openai", updated.Provider)
require.Equal(t, "gpt-4.1-mini", updated.Model)
require.Equal(t, "GPT 4.1 Mini", updated.DisplayName)
require.False(t, updated.Enabled)
})
t.Run("CreateAndUpdateModelConfigTyped", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
_, err := client.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
Provider: "openai",
APIKey: "openai-key",
})
require.NoError(t, err)
maxOutputTokens := int64(4096)
temperature := 0.25
parallelToolCalls := true
reasoningEffort := "medium"
config, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
Provider: "openai",
Model: "gpt-4.1",
DisplayName: "GPT 4.1",
ContextLimit: int64Ptr(200000),
ModelConfig: &codersdk.ChatModelCallConfig{
MaxOutputTokens: &maxOutputTokens,
Temperature: &temperature,
ProviderOptions: &codersdk.ChatModelProviderOptions{
OpenAI: &codersdk.ChatModelOpenAIProviderOptions{
ParallelToolCalls: &parallelToolCalls,
ReasoningEffort: &reasoningEffort,
},
},
},
})
require.NoError(t, err)
require.NotNil(t, config.ModelConfig)
require.Equal(t, maxOutputTokens, *config.ModelConfig.MaxOutputTokens)
require.Equal(t, temperature, *config.ModelConfig.Temperature)
require.NotNil(t, config.ModelConfig.ProviderOptions)
require.NotNil(t, config.ModelConfig.ProviderOptions.OpenAI)
require.Equal(t, parallelToolCalls, *config.ModelConfig.ProviderOptions.OpenAI.ParallelToolCalls)
require.Equal(t, reasoningEffort, *config.ModelConfig.ProviderOptions.OpenAI.ReasoningEffort)
topP := 0.8
textVerbosity := "high"
updated, err := client.UpdateChatModelConfig(ctx, config.ID, codersdk.UpdateChatModelConfigRequest{
ModelConfig: &codersdk.ChatModelCallConfig{
TopP: &topP,
ProviderOptions: &codersdk.ChatModelProviderOptions{
OpenAI: &codersdk.ChatModelOpenAIProviderOptions{
TextVerbosity: &textVerbosity,
},
},
},
})
require.NoError(t, err)
require.NotNil(t, updated.ModelConfig)
require.Nil(t, updated.ModelConfig.MaxOutputTokens)
require.Nil(t, updated.ModelConfig.Temperature)
require.Equal(t, topP, *updated.ModelConfig.TopP)
require.NotNil(t, updated.ModelConfig.ProviderOptions)
require.NotNil(t, updated.ModelConfig.ProviderOptions.OpenAI)
require.Equal(t, textVerbosity, *updated.ModelConfig.ProviderOptions.OpenAI.TextVerbosity)
cleared, err := client.UpdateChatModelConfig(ctx, config.ID, codersdk.UpdateChatModelConfigRequest{
ModelConfig: &codersdk.ChatModelCallConfig{},
})
require.NoError(t, err)
require.Nil(t, cleared.ModelConfig)
})
t.Run("UpdateNonAdminForbidden", func(t *testing.T) {
t.Parallel()
ownerClient := coderdtest.New(t, nil)
owner := coderdtest.CreateFirstUser(t, ownerClient)
memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
ctx := testutil.Context(t, testutil.WaitShort)
_, err := ownerClient.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
Provider: "openai",
APIKey: "openai-key",
})
require.NoError(t, err)
config, err := ownerClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
Provider: "openai",
Model: "gpt-4.1",
ContextLimit: int64Ptr(200000),
})
require.NoError(t, err)
_, err = memberClient.UpdateChatModelConfig(ctx, config.ID, codersdk.UpdateChatModelConfigRequest{
DisplayName: "Should Fail",
})
require.Error(t, err)
require.Equal(t, http.StatusForbidden, coderdtest.SDKError(t, err).StatusCode())
})
t.Run("DeleteAdminAllowed", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
_, err := client.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
Provider: "openai",
APIKey: "openai-key",
})
require.NoError(t, err)
config, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
Provider: "openai",
Model: "gpt-4.1",
ContextLimit: int64Ptr(200000),
})
require.NoError(t, err)
err = client.DeleteChatModelConfig(ctx, config.ID)
require.NoError(t, err)
configs, err := client.ListChatModelConfigs(ctx)
require.NoError(t, err)
require.Len(t, configs, 0)
})
t.Run("DeleteNonAdminForbidden", func(t *testing.T) {
t.Parallel()
ownerClient := coderdtest.New(t, nil)
owner := coderdtest.CreateFirstUser(t, ownerClient)
memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
ctx := testutil.Context(t, testutil.WaitShort)
_, err := ownerClient.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
Provider: "openai",
APIKey: "openai-key",
})
require.NoError(t, err)
config, err := ownerClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
Provider: "openai",
Model: "gpt-4.1",
ContextLimit: int64Ptr(200000),
})
require.NoError(t, err)
err = memberClient.DeleteChatModelConfig(ctx, config.ID)
require.Error(t, err)
require.Equal(t, http.StatusForbidden, coderdtest.SDKError(t, err).StatusCode())
})
}
func createWorkspaceWithAgent(
t *testing.T,
client *codersdk.Client,
organizationID uuid.UUID,
) (codersdk.Workspace, uuid.UUID) {
t.Helper()
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, organizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, organizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
workspace.LatestBuild = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
require.NotEmpty(t, workspace.LatestBuild.Resources)
require.NotEmpty(t, workspace.LatestBuild.Resources[0].Agents)
return workspace, workspace.LatestBuild.Resources[0].Agents[0].ID
}
func firstChatMessageByRole(messages []codersdk.ChatMessage, role string) (codersdk.ChatMessage, bool) {
for _, message := range messages {
if message.Role == role {
return message, true
}
}
return codersdk.ChatMessage{}, false
}
type roundTripperFunc func(*http.Request) (*http.Response, error)
func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req)
}