diff --git a/.claude/docs/FRONTEND_PATTERNS.md b/.claude/docs/FRONTEND_PATTERNS.md index 6b80efde782..1a52ae555db 100644 --- a/.claude/docs/FRONTEND_PATTERNS.md +++ b/.claude/docs/FRONTEND_PATTERNS.md @@ -70,15 +70,15 @@ export const SelectModel: Story = { **Incorrect:** ```tsx -const config = data as unknown as ChatModelConfig; +const config = data as unknown as ChatModel; ``` **Correct:** ```tsx -import type { ChatModelConfig } from "api/typesGenerated"; +import type { ChatModel } from "api/typesGenerated"; -const config: ChatModelConfig = parseConfig(data); +const config: ChatModel = parseConfig(data); ``` ## FE3: Reuse before building, and keep PRs single-purpose @@ -167,17 +167,17 @@ with `useState` + `useEffect`. ```tsx parameters: { - queries: [{ key: ["chat-model-configs"], data: [MockChatModelConfig] }], + queries: [{ key: ["chat-models"], data: [MockChatModel] }], }, ``` **Correct (imported constant):** ```tsx -import { chatModelConfigsKey } from "api/queries/chats"; +import { chatModelsKey } from "api/queries/chats"; parameters: { - queries: [{ key: chatModelConfigsKey, data: [MockChatModelConfig] }], + queries: [{ key: chatModelsKey, data: [MockChatModel] }], }, ``` @@ -203,7 +203,7 @@ Decide where logic goes before reaching for `useEffect`: ## FE9: Fixtures and mocks follow repo conventions - Represent entities with shared `Mock*` constants in `site/src/testHelpers/` - (for example `MockChatModelConfig` in `testHelpers/chatModels.ts`). When a + (for example `MockChatModel` in `testHelpers/chatModels.ts`). When a story needs a variant, spread the base fixture into a named local constant. - Compose story query wiring (`{ key, data }`) inline per story so each story is readable on its own. Share the entity fixture, not a pre-wired query diff --git a/cli/exp_scaletest_chat.go b/cli/exp_scaletest_chat.go index 992a1944d99..8de6809e722 100644 --- a/cli/exp_scaletest_chat.go +++ b/cli/exp_scaletest_chat.go @@ -77,7 +77,7 @@ func (r *RootCmd) scaletestChat() *serpent.Command { } logger := inv.Logger - modelConfigID, err := chat.EnsureScaletestModelConfig(ctx, client, logger, llmMockURL, providerPropagationWait) + modelID, err := chat.EnsureScaletestChatModel(ctx, client, logger, llmMockURL, providerPropagationWait) if err != nil { return err } @@ -128,7 +128,7 @@ func (r *RootCmd) scaletestChat() *serpent.Command { OrganizationID: targetWorkspace.OrganizationID, WorkspaceID: targetWorkspace.ID, Prompt: prompt, - ModelConfigID: modelConfigID, + ModelConfigID: modelID, Turns: int(turns), TurnStartDelay: turnStartDelay, TurnStartReadyWaitGroup: turnStartReadyWaitGroup, diff --git a/cli/exp_scaletest_chat_test.go b/cli/exp_scaletest_chat_test.go index 0529258123a..b83c7bfdbac 100644 --- a/cli/exp_scaletest_chat_test.go +++ b/cli/exp_scaletest_chat_test.go @@ -77,9 +77,9 @@ func TestScaleTestChat(t *testing.T) { require.Equal(t, mockURL, provider.BaseURL) expClient := codersdk.NewExperimentalClient(client) - configs, err := expClient.ListChatModelConfigs(ctx) + configs, err := expClient.ChatModels(ctx) require.NoError(t, err) - matchingConfigs := scaletestModelConfigsForProvider(configs, provider.ID) + matchingConfigs := scaletestModelsForProvider(configs, provider.ID) require.Len(t, matchingConfigs, 1) require.True(t, matchingConfigs[0].Enabled) @@ -125,8 +125,8 @@ func chatMessageText(messages []codersdk.ChatMessage, role codersdk.ChatMessageR return b.String(), found } -func scaletestModelConfigsForProvider(configs []codersdk.ChatModelConfig, providerID uuid.UUID) []codersdk.ChatModelConfig { - matches := make([]codersdk.ChatModelConfig, 0, 1) +func scaletestModelsForProvider(configs []codersdk.ChatModel, providerID uuid.UUID) []codersdk.ChatModel { + matches := make([]codersdk.ChatModel, 0, 1) for _, config := range configs { if config.AIProviderID != providerID { continue diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index baad1b12691..98905869138 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -497,7 +497,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.ChatModelsResponse" + "$ref": "#/definitions/codersdk.ChatModelAvailabilityResponse" } } }, @@ -18627,7 +18627,25 @@ const docTemplate = `{ } } }, - "codersdk.ChatModel": { + "codersdk.ChatModelAvailabilityResponse": { + "type": "object", + "properties": { + "providers": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.ChatModelProvider" + } + }, + "unsupported_providers": { + "description": "UnsupportedProviders lists configured providers the Agents harness\ncannot use, so the UI can explain the empty state.", + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.ChatUnsupportedProvider" + } + } + } + }, + "codersdk.ChatModelCatalogEntry": { "type": "object", "properties": { "display_name": { @@ -18653,7 +18671,7 @@ const docTemplate = `{ "models": { "type": "array", "items": { - "$ref": "#/definitions/codersdk.ChatModel" + "$ref": "#/definitions/codersdk.ChatModelCatalogEntry" } }, "provider": { @@ -18677,24 +18695,6 @@ const docTemplate = `{ "ChatModelProviderUnavailableReasonUserAPIKeyRequired" ] }, - "codersdk.ChatModelsResponse": { - "type": "object", - "properties": { - "providers": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.ChatModelProvider" - } - }, - "unsupported_providers": { - "description": "UnsupportedProviders lists configured providers the Agents harness\ncannot use, so the UI can explain the empty state.", - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.ChatUnsupportedProvider" - } - } - } - }, "codersdk.ChatPlanMode": { "type": "string", "enum": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index cea51e0dcca..039748a88c2 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -436,7 +436,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.ChatModelsResponse" + "$ref": "#/definitions/codersdk.ChatModelAvailabilityResponse" } } }, @@ -16809,7 +16809,25 @@ } } }, - "codersdk.ChatModel": { + "codersdk.ChatModelAvailabilityResponse": { + "type": "object", + "properties": { + "providers": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.ChatModelProvider" + } + }, + "unsupported_providers": { + "description": "UnsupportedProviders lists configured providers the Agents harness\ncannot use, so the UI can explain the empty state.", + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.ChatUnsupportedProvider" + } + } + } + }, + "codersdk.ChatModelCatalogEntry": { "type": "object", "properties": { "display_name": { @@ -16835,7 +16853,7 @@ "models": { "type": "array", "items": { - "$ref": "#/definitions/codersdk.ChatModel" + "$ref": "#/definitions/codersdk.ChatModelCatalogEntry" } }, "provider": { @@ -16855,24 +16873,6 @@ "ChatModelProviderUnavailableReasonUserAPIKeyRequired" ] }, - "codersdk.ChatModelsResponse": { - "type": "object", - "properties": { - "providers": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.ChatModelProvider" - } - }, - "unsupported_providers": { - "description": "UnsupportedProviders lists configured providers the Agents harness\ncannot use, so the UI can explain the empty state.", - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.ChatUnsupportedProvider" - } - } - } - }, "codersdk.ChatPlanMode": { "type": "string", "enum": ["plan"], diff --git a/coderd/coderdtest/chat.go b/coderd/coderdtest/chat.go index 3f67c7d0acb..ebb4501c25c 100644 --- a/coderd/coderdtest/chat.go +++ b/coderd/coderdtest/chat.go @@ -46,15 +46,15 @@ func FakeOpenAICompatProviderAPIKeys(t testing.TB) chatprovider.ProviderAPIKeys return OpenAICompatProviderAPIKeys(chattest.OpenAI(t)) } -// CreateOpenAICompatChatModelConfig creates the default provider and model -// config used by chat runtime tests. Tests can pass a baseURL to route chat work -// to a specific local provider. If baseURL is empty, this helper starts a fake +// CreateOpenAICompatChatModel creates the default provider and ChatModel used +// by chat runtime tests. Tests can pass a baseURL to route chat work to a +// specific local provider. If baseURL is empty, this helper starts a fake // OpenAI-compatible provider. -func CreateOpenAICompatChatModelConfig( +func CreateOpenAICompatChatModel( t testing.TB, client *codersdk.ExperimentalClient, baseURL string, -) codersdk.ChatModelConfig { +) codersdk.ChatModel { t.Helper() if baseURL == "" { @@ -72,14 +72,14 @@ func CreateOpenAICompatChatModelConfig( require.NoError(t, err) contextLimit := int64(4096) isDefault := true - modelConfig, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + model, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &provider.ID, Model: TestChatModelOpenAICompat, ContextLimit: &contextLimit, IsDefault: &isDefault, }) require.NoError(t, err) - return modelConfig + return model } // WaitForChatSettled waits for a chat to leave active processing and drains diff --git a/coderd/exp_chats.go b/coderd/exp_chats.go index f572ba7439c..e450ab3157e 100644 --- a/coderd/exp_chats.go +++ b/coderd/exp_chats.go @@ -1604,7 +1604,7 @@ func (api *API) postChats(rw http.ResponseWriter, r *http.Request) { // @Security CoderSessionToken // @Tags Chats // @Produce json -// @Success 200 {object} codersdk.ChatModelsResponse +// @Success 200 {object} codersdk.ChatModelAvailabilityResponse // @Router /api/experimental/chats/models [get] // @Description Experimental: this endpoint is subject to change. func (api *API) listChatModels(rw http.ResponseWriter, r *http.Request) { @@ -1627,7 +1627,7 @@ func (api *API) listChatModels(rw http.ResponseWriter, r *http.Request) { return } catalog := chatprovider.NewModelCatalog() - var response codersdk.ChatModelsResponse + var response codersdk.ChatModelAvailabilityResponse if configured, ok := catalog.ListConfiguredModels( availability.configuredProviders, availability.configuredModels, @@ -6953,7 +6953,7 @@ func (api *API) listChatModelConfigs(rw http.ResponseWriter, r *http.Request) { return } - resp := make([]codersdk.ChatModelConfig, 0, len(allConfigs)) + resp := make([]codersdk.ChatModel, 0, len(allConfigs)) for _, config := range allConfigs { if config.OrganizationID == defaultOrg.ID { resp = append(resp, convertChatModelConfig(config)) @@ -7024,7 +7024,7 @@ func (api *API) createChatModelConfig(rw http.ResponseWriter, r *http.Request) { return } - var req codersdk.CreateChatModelConfigRequest + var req codersdk.CreateChatModelRequest if !httpapi.Read(ctx, rw, r, &req) { return } @@ -7255,7 +7255,7 @@ func (api *API) updateChatModelConfig(rw http.ResponseWriter, r *http.Request) { return } - var req codersdk.UpdateChatModelConfigRequest + var req codersdk.UpdateChatModelRequest if !httpapi.Read(ctx, rw, r, &req) { return } @@ -7623,7 +7623,7 @@ func parseChatModelConfigID(rw http.ResponseWriter, r *http.Request) (uuid.UUID, return modelConfigID, true } -func convertChatModelConfig(config database.ChatModelConfig) codersdk.ChatModelConfig { +func convertChatModelConfig(config database.ChatModelConfig) codersdk.ChatModel { modelConfig := unmarshalChatModelCallConfig(config.Options) var reasoningEffortConfig *codersdk.ChatModelReasoningEffortConfig if modelConfig != nil { @@ -7632,7 +7632,7 @@ func convertChatModelConfig(config database.ChatModelConfig) codersdk.ChatModelC // Active configs always carry a non-null ai_provider_id (CHECK // chat_model_configs_ai_provider_required_when_active). - return codersdk.ChatModelConfig{ + return codersdk.ChatModel{ ID: config.ID, AIProviderID: config.AIProviderID.UUID, Model: config.Model, diff --git a/coderd/exp_chats_acl_test.go b/coderd/exp_chats_acl_test.go index e3bd2958e70..837dd590e04 100644 --- a/coderd/exp_chats_acl_test.go +++ b/coderd/exp_chats_acl_test.go @@ -36,7 +36,7 @@ func TestChatACLSharingLifecycle(t *testing.T) { opts.NotificationsEnqueuer = notifyEnq }) firstUser := coderdtest.CreateFirstUser(t, client.Client) - _ = createChatModelConfig(t, client) + _ = createChatModel(t, client) sharedClient, sharedUser := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) sharedClientExp := codersdk.NewExperimentalClient(sharedClient) @@ -223,7 +223,7 @@ func TestChatACLSharingNotifiesDirectReadersOnly(t *testing.T) { opts.NotificationsEnqueuer = notifyEnq }) firstUser := coderdtest.CreateFirstUser(t, client.Client) - _ = createChatModelConfig(t, client) + _ = createChatModel(t, client) // A non-owner org admin can share another user's chat via ActionShare. adminClient, admin := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleOrgAdmin(firstUser.OrganizationID)) @@ -271,7 +271,7 @@ func TestChatACLSubChatInheritance(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) client, db := newChatClientWithDatabase(t) firstUser := coderdtest.CreateFirstUser(t, client.Client) - modelConfig := createChatModelConfig(t, client) + modelConfig := createChatModel(t, client) sharedClient, sharedUser := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) sharedClientExp := codersdk.NewExperimentalClient(sharedClient) @@ -319,7 +319,7 @@ func TestChatACLValidation(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) client := newChatClient(t) firstUser := coderdtest.CreateFirstUser(t, client.Client) - _ = createChatModelConfig(t, client) + _ = createChatModel(t, client) chat := createChatForSharing(ctx, t, client, firstUser.OrganizationID, "validation chat") missingUserID := uuid.New() missingGroupID := uuid.New() @@ -410,7 +410,7 @@ func TestSharedReaderStreamChat(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) client, db := newChatClientWithDatabase(t) firstUser := coderdtest.CreateFirstUser(t, client.Client) - modelConfig := createChatModelConfig(t, client) + modelConfig := createChatModel(t, client) sharedClient, sharedUser := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) sharedClientExp := codersdk.NewExperimentalClient(sharedClient) chat := dbgen.Chat(t, db, database.Chat{ @@ -462,7 +462,7 @@ func TestListChatsSharedScope(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) client, db := newChatClientWithDatabase(t) firstUser := coderdtest.CreateFirstUser(t, client.Client) - modelConfig := createChatModelConfig(t, client) + modelConfig := createChatModel(t, client) viewerClient, viewer := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID)) viewerClientExp := codersdk.NewExperimentalClient(viewerClient) sharedChat := dbgen.Chat(t, db, database.Chat{ @@ -561,7 +561,7 @@ func TestChatSharingDisabled(t *testing.T) { opts.Pubsub = pubsub }) firstUser := coderdtest.CreateFirstUser(t, client.Client) - modelConfig := createChatModelConfig(t, client) + modelConfig := createChatModel(t, client) viewerClient, viewer := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID)) viewerClientExp := codersdk.NewExperimentalClient(viewerClient) diff --git a/coderd/exp_chats_chatstate_test.go b/coderd/exp_chats_chatstate_test.go index 424cc5c8e1e..7dd6208d352 100644 --- a/coderd/exp_chats_chatstate_test.go +++ b/coderd/exp_chats_chatstate_test.go @@ -99,7 +99,7 @@ func TestPostChatsStartsRunning(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) client, api := newChatClientWithAPI(t, withChatWorkerDisabled) firstUser := coderdtest.CreateFirstUser(t, client.Client) - _ = createChatModelConfig(t, client) + _ = createChatModel(t, client) chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{ OrganizationID: firstUser.OrganizationID, @@ -133,7 +133,7 @@ func TestArchiveChatStateTransitions(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) client, api := newChatClientWithAPI(t, withChatWorkerDisabled) firstUser := coderdtest.CreateFirstUser(t, client.Client) - _ = createChatModelConfig(t, client) + _ = createChatModel(t, client) chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{ OrganizationID: firstUser.OrganizationID, @@ -157,7 +157,7 @@ func TestArchiveChatStateTransitions(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) client := newChatClient(t, withChatWorkerDisabled) firstUser := coderdtest.CreateFirstUser(t, client.Client) - _ = createChatModelConfig(t, client) + _ = createChatModel(t, client) chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{ OrganizationID: firstUser.OrganizationID, @@ -183,7 +183,7 @@ func TestPostChatMessagesBusyInterrupt(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) client := newChatClient(t, withChatWorkerDisabled) firstUser := coderdtest.CreateFirstUser(t, client.Client) - _ = createChatModelConfig(t, client) + _ = createChatModel(t, client) chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{ OrganizationID: firstUser.OrganizationID, @@ -219,7 +219,7 @@ func TestDeleteChatQueuedMessageMissingReturns404(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) client := newChatClient(t, withChatWorkerDisabled) firstUser := coderdtest.CreateFirstUser(t, client.Client) - _ = createChatModelConfig(t, client) + _ = createChatModel(t, client) chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{ OrganizationID: firstUser.OrganizationID, @@ -254,7 +254,7 @@ func TestDeleteChatQueuedMessageEmptyQueueReturnsConflict(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) client := newChatClient(t, withChatWorkerDisabled) firstUser := coderdtest.CreateFirstUser(t, client.Client) - _ = createChatModelConfig(t, client) + _ = createChatModel(t, client) chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{ OrganizationID: firstUser.OrganizationID, @@ -282,7 +282,7 @@ func TestPromoteChatQueuedMessageMissingReturns404(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) client := newChatClient(t, withChatWorkerDisabled) firstUser := coderdtest.CreateFirstUser(t, client.Client) - _ = createChatModelConfig(t, client) + _ = createChatModel(t, client) chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{ OrganizationID: firstUser.OrganizationID, @@ -316,7 +316,7 @@ func TestPromoteChatQueuedMessageEmptyQueueReturnsConflict(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) client := newChatClient(t, withChatWorkerDisabled) firstUser := coderdtest.CreateFirstUser(t, client.Client) - _ = createChatModelConfig(t, client) + _ = createChatModel(t, client) chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{ OrganizationID: firstUser.OrganizationID, @@ -345,7 +345,7 @@ func TestInterruptChatIdleReturnsConflict(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) client, api := newChatClientWithAPI(t, withChatWorkerDisabled) firstUser := coderdtest.CreateFirstUser(t, client.Client) - _ = createChatModelConfig(t, client) + _ = createChatModel(t, client) chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{ OrganizationID: firstUser.OrganizationID, @@ -367,7 +367,7 @@ func TestSubmitToolResultsWrongStateReturnsConflict(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) client := newChatClient(t, withChatWorkerDisabled) firstUser := coderdtest.CreateFirstUser(t, client.Client) - _ = createChatModelConfig(t, client) + _ = createChatModel(t, client) chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{ OrganizationID: firstUser.OrganizationID, @@ -395,7 +395,7 @@ func TestSubmitToolResultsRequiresActionSucceeds(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) client, api := newChatClientWithAPI(t, withChatWorkerDisabled) firstUser := coderdtest.CreateFirstUser(t, client.Client) - _ = createChatModelConfig(t, client) + _ = createChatModel(t, client) dynamicTools := []codersdk.DynamicTool{{ Name: "echo", @@ -447,7 +447,7 @@ func TestPatchChatArchiveChildRejected(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) client, db, api := newChatClientWithAPIAndDatabase(t, withChatWorkerDisabled) firstUser := coderdtest.CreateFirstUser(t, client.Client) - modelConfig := createChatModelConfig(t, client) + modelConfig := createChatModel(t, client) root, err := client.CreateChat(ctx, codersdk.CreateChatRequest{ OrganizationID: firstUser.OrganizationID, @@ -497,7 +497,7 @@ func TestPatchChatUnarchiveChildRejected(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) client, db, api := newChatClientWithAPIAndDatabase(t, withChatWorkerDisabled) firstUser := coderdtest.CreateFirstUser(t, client.Client) - modelConfig := createChatModelConfig(t, client) + modelConfig := createChatModel(t, client) root, err := client.CreateChat(ctx, codersdk.CreateChatRequest{ OrganizationID: firstUser.OrganizationID, @@ -556,7 +556,7 @@ func TestPatchChatArchiveRootRollsBackWhenChildCannotArchive(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) client, db, api := newChatClientWithAPIAndDatabase(t, withChatWorkerDisabled) firstUser := coderdtest.CreateFirstUser(t, client.Client) - modelConfig := createChatModelConfig(t, client) + modelConfig := createChatModel(t, client) root, err := client.CreateChat(ctx, codersdk.CreateChatRequest{ OrganizationID: firstUser.OrganizationID, @@ -596,7 +596,7 @@ func TestPostChatMessagesInvalidStateReturnsSharedResponse(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) client, _, api := newChatClientWithAPIAndDatabase(t, withChatWorkerDisabled) firstUser := coderdtest.CreateFirstUser(t, client.Client) - _ = createChatModelConfig(t, client) + _ = createChatModel(t, client) chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{ OrganizationID: firstUser.OrganizationID, @@ -629,7 +629,7 @@ func TestPostChatToolResultsInvalidStateReturnsSharedResponse(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) client, _, api := newChatClientWithAPIAndDatabase(t, withChatWorkerDisabled) firstUser := coderdtest.CreateFirstUser(t, client.Client) - _ = createChatModelConfig(t, client) + _ = createChatModel(t, client) chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{ OrganizationID: firstUser.OrganizationID, @@ -663,7 +663,7 @@ func TestReconcileInvalidChatStateSucceeds(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) client, db, api := newChatClientWithAPIAndDatabase(t, withChatWorkerDisabled) firstUser := coderdtest.CreateFirstUser(t, client.Client) - _ = createChatModelConfig(t, client) + _ = createChatModel(t, client) chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{ OrganizationID: firstUser.OrganizationID, @@ -703,7 +703,7 @@ func TestReconcileInvalidChatStateNotInvalidReturnsConflict(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) client := newChatClient(t, withChatWorkerDisabled) firstUser := coderdtest.CreateFirstUser(t, client.Client) - _ = createChatModelConfig(t, client) + _ = createChatModel(t, client) // A freshly created chat starts in the valid running state (R0). chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{ diff --git a/coderd/exp_chats_hooks_test.go b/coderd/exp_chats_hooks_test.go index f5f9cca09a8..8af1ed85116 100644 --- a/coderd/exp_chats_hooks_test.go +++ b/coderd/exp_chats_hooks_test.go @@ -75,7 +75,7 @@ func TestPostChatsInitialPromptHookErrors(t *testing.T) { opts.DeploymentValues.AI.Chat.HookEnabled = serpent.Bool(true) }) user := coderdtest.CreateFirstUser(t, client.Client) - model := createAdditionalChatModelConfig(t, client, "openai", "gpt-4.1") + model := createAdditionalChatModel(t, client, "openai", "gpt-4.1") ctx := testutil.Context(t, testutil.WaitLong) res, err := client.Request(ctx, http.MethodPost, "/api/experimental/chats", codersdk.CreateChatRequest{ @@ -130,7 +130,7 @@ func TestChatLifecycleHooksExperimentDisabled(t *testing.T) { opts.DeploymentValues.AI.Chat.HookEnabled = serpent.Bool(true) }) user := coderdtest.CreateFirstUser(t, client.Client) - model := createAdditionalChatModelConfig(t, client, "openai", "gpt-4.1") + model := createAdditionalChatModel(t, client, "openai", "gpt-4.1") ctx := testutil.Context(t, testutil.WaitLong) _, err := client.CreateChat(ctx, codersdk.CreateChatRequest{ @@ -168,7 +168,7 @@ func TestChatPromptHookContextHiddenFromAPI(t *testing.T) { opts.DeploymentValues.AI.Chat.HookEnabled = serpent.Bool(true) }) user := coderdtest.CreateFirstUser(t, client.Client) - model := createAdditionalChatModelConfig(t, client, "openai", "gpt-4.1") + model := createAdditionalChatModel(t, client, "openai", "gpt-4.1") ctx := testutil.Context(t, testutil.WaitLong) chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{ @@ -275,7 +275,7 @@ func TestChatLifecycleHooksWorkedExample(t *testing.T) { opts.DeploymentValues.AI.Chat.HookEnabled = serpent.Bool(true) }) user := coderdtest.CreateFirstUser(t, client.Client) - model := createChatModelConfigWithBaseURL(t, client, modelURL) + model := createChatModelWithBaseURL(t, client, modelURL) chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{ OrganizationID: user.OrganizationID, @@ -410,7 +410,7 @@ func TestChatHooksFileLinksAfterPromptOverride(t *testing.T) { opts.DeploymentValues.AI.Chat.HookEnabled = serpent.Bool(true) }) user := coderdtest.CreateFirstUser(t, client.Client) - model := createChatModelConfigWithBaseURL(t, client, modelURL) + model := createChatModelWithBaseURL(t, client, modelURL) uploadFile := func(name string) uuid.UUID { pngData := append([]byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}, make([]byte, 16)...) @@ -513,7 +513,7 @@ func TestChatHookNoticeMessagesInResponses(t *testing.T) { opts.DeploymentValues.AI.Chat.HookEnabled = serpent.Bool(true) }) user := coderdtest.CreateFirstUser(t, client.Client) - model := createChatModelConfigWithBaseURL(t, client, modelURL) + model := createChatModelWithBaseURL(t, client, modelURL) chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{ OrganizationID: user.OrganizationID, diff --git a/coderd/exp_chats_model_config_list_test.go b/coderd/exp_chats_model_config_list_test.go index c1478f02243..2cf19fcc2f8 100644 --- a/coderd/exp_chats_model_config_list_test.go +++ b/coderd/exp_chats_model_config_list_test.go @@ -90,7 +90,7 @@ func TestChatModelConfigListReadContracts(t *testing.T) { ) t.Cleanup(scopedClient.HTTPClient.CloseIdleConnections) - configs, err := codersdk.NewExperimentalClient(scopedClient).ListChatModelConfigs(ctx) + configs, err := codersdk.NewExperimentalClient(scopedClient).ChatModels(ctx) if testCase.wantStatus != 0 { requireSDKError(t, err, testCase.wantStatus) return @@ -200,21 +200,21 @@ func TestChatModelConfigListReadContracts(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) - configs, err := testCase.client(t, ctx).ListChatModelConfigs(ctx) + models, err := testCase.client(t, ctx).ChatModels(ctx) require.NoError(t, err) for _, id := range testCase.visible { - require.True(t, containsChatModelConfig(configs, id), "must see config %s", id) + require.True(t, containsChatModel(models, id), "must see ChatModel %s", id) } for _, id := range testCase.hidden { - require.False(t, containsChatModelConfig(configs, id), "must not see config %s", id) + require.False(t, containsChatModel(models, id), "must not see ChatModel %s", id) } }) } } -func containsChatModelConfig(configs []codersdk.ChatModelConfig, id uuid.UUID) bool { - return slices.ContainsFunc(configs, func(config codersdk.ChatModelConfig) bool { - return config.ID == id +func containsChatModel(models []codersdk.ChatModel, id uuid.UUID) bool { + return slices.ContainsFunc(models, func(model codersdk.ChatModel) bool { + return model.ID == id }) } diff --git a/coderd/exp_chats_test.go b/coderd/exp_chats_test.go index abca4d0f6ef..f8c9528a9a2 100644 --- a/coderd/exp_chats_test.go +++ b/coderd/exp_chats_test.go @@ -2490,7 +2490,7 @@ func TestListChatModels(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client.Client) modelConfig := createChatModelConfig(t, client) - models, err := client.ListChatModels(ctx) + models, err := client.ChatModelAvailability(ctx) require.NoError(t, err) var openAIProvider *codersdk.ChatModelProvider @@ -2521,7 +2521,7 @@ func TestListChatModels(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client.Client) unauthenticatedClient := codersdk.NewExperimentalClient(codersdk.New(client.URL)) - _, err := unauthenticatedClient.ListChatModels(ctx) + _, err := unauthenticatedClient.ChatModelAvailability(ctx) requireSDKError(t, err, http.StatusUnauthorized) }) @@ -2537,7 +2537,7 @@ func TestListChatModels(t *testing.T) { // than vanish, so the empty state can explain why. _ = createAIProviderForTest(t, client, string(codersdk.AIProviderTypeCopilot), "") - models, err := client.ListChatModels(ctx) + models, err := client.ChatModelAvailability(ctx) require.NoError(t, err) require.False(t, slices.ContainsFunc(models.Providers, func(p codersdk.ChatModelProvider) bool { @@ -2560,7 +2560,7 @@ func TestListChatModels(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client.Client) _ = createChatModelConfig(t, client) - models, err := client.ListChatModels(ctx) + models, err := client.ChatModelAvailability(ctx) require.NoError(t, err) require.Empty(t, models.UnsupportedProviders) }) @@ -2573,7 +2573,7 @@ func TestListChatModels(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client.Client) _ = createChatModelConfig(t, client) - models, err := client.ListChatModels(ctx) + models, err := client.ChatModelAvailability(ctx) require.NoError(t, err) var openAIProvider *codersdk.ChatModelProvider @@ -2598,14 +2598,14 @@ func TestListChatModels(t *testing.T) { provider := createAIProviderForTest(t, client, string(providerType), "") contextLimit := int64(4096) - _, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + _, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &provider.ID, Model: "claude-sonnet", ContextLimit: &contextLimit, }) require.NoError(t, err) - models, err := client.ListChatModels(ctx) + models, err := client.ChatModelAvailability(ctx) require.NoError(t, err) var anthropicProvider *codersdk.ChatModelProvider @@ -2624,7 +2624,7 @@ func TestListChatModels(t *testing.T) { }) require.NoError(t, err) - models, err = client.ListChatModels(ctx) + models, err = client.ChatModelAvailability(ctx) require.NoError(t, err) anthropicProvider = nil @@ -2648,14 +2648,14 @@ func TestListChatModels(t *testing.T) { provider := createAIProviderForTest(t, client, "google", "provider-api-key") contextLimit := int64(4096) - _, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + _, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &provider.ID, Model: "gemini-1.5-pro", ContextLimit: &contextLimit, }) require.NoError(t, err) - models, err := client.ListChatModels(ctx) + models, err := client.ChatModelAvailability(ctx) require.NoError(t, err) var googleProvider *codersdk.ChatModelProvider @@ -2673,7 +2673,7 @@ func TestListChatModels(t *testing.T) { }) require.NoError(t, err) - models, err = client.ListChatModels(ctx) + models, err = client.ChatModelAvailability(ctx) require.NoError(t, err) googleProvider = nil @@ -2695,7 +2695,7 @@ func TestListChatModels(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client.Client) provider := createAIProviderForTest(t, client, "openai-compat", "test-api-key") contextLimit := int64(4096) - defaultConfig, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + defaultConfig, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &provider.ID, Model: "default-" + uuid.NewString(), ContextLimit: &contextLimit, @@ -2708,7 +2708,7 @@ func TestListChatModels(t *testing.T) { OrganizationID: otherOrganization.ID, }) - models, err := client.ListChatModels(ctx) + models, err := client.ChatModelAvailability(ctx) require.NoError(t, err) var catalogModels []string @@ -2733,14 +2733,14 @@ func TestListChatModels(t *testing.T) { provider := createAIProviderForTest(t, client, "openai", "test-key") contextLimit := int64(4096) - _, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + _, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &provider.ID, Model: "gpt-4o-mini", ContextLimit: &contextLimit, }) require.NoError(t, err) - models, err := client.ListChatModels(ctx) + models, err := client.ChatModelAvailability(ctx) require.NoError(t, err) require.Len(t, models.Providers, 1) require.Equal(t, "openai", models.Providers[0].Provider) @@ -2753,7 +2753,7 @@ func TestListChatModels(t *testing.T) { }) require.NoError(t, err) - models, err = client.ListChatModels(ctx) + models, err = client.ChatModelAvailability(ctx) require.NoError(t, err) require.Empty(t, models.Providers) }) @@ -2762,7 +2762,7 @@ func TestListChatModels(t *testing.T) { func TestListChats_Search(t *testing.T) { t.Parallel() - setup := func(t *testing.T) (context.Context, *codersdk.ExperimentalClient, database.Store, codersdk.CreateFirstUserResponse, codersdk.ChatModelConfig) { + setup := func(t *testing.T) (context.Context, *codersdk.ExperimentalClient, database.Store, codersdk.CreateFirstUserResponse, codersdk.ChatModel) { t.Helper() ctx := testutil.Context(t, testutil.WaitLong) client, db := newChatClientWithDatabase(t) @@ -4539,7 +4539,7 @@ func TestListChatModelConfigs(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client.Client) modelConfig := createChatModelConfig(t, client) - configs, err := client.ListChatModelConfigs(ctx) + configs, err := client.ChatModels(ctx) require.NoError(t, err) require.NotEmpty(t, configs) @@ -4566,7 +4566,7 @@ func TestListChatModelConfigs(t *testing.T) { contextLimit := int64(4096) enabled := false - disabledConfig, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + disabledConfig, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &aiProvider.ID, Model: "gpt-4o-disabled", DisplayName: "GPT-4o Disabled", @@ -4576,7 +4576,7 @@ func TestListChatModelConfigs(t *testing.T) { require.NoError(t, err) require.False(t, disabledConfig.Enabled) - configs, err := client.ListChatModelConfigs(ctx) + configs, err := client.ChatModels(ctx) require.NoError(t, err) found := false @@ -4602,7 +4602,7 @@ func TestListChatModelConfigs(t *testing.T) { contextLimit := int64(4096) enabled := false - _, err := adminClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + _, err := adminClient.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &enabledConfig.AIProviderID, Model: "gpt-4o-disabled", DisplayName: "GPT-4o Disabled", @@ -4611,13 +4611,13 @@ func TestListChatModelConfigs(t *testing.T) { }) require.NoError(t, err) - configs, err := memberClient.ListChatModelConfigs(ctx) + configs, err := memberClient.ChatModels(ctx) require.NoError(t, err) require.Len(t, configs, 2) - require.True(t, slices.ContainsFunc(configs, func(config codersdk.ChatModelConfig) bool { + require.True(t, slices.ContainsFunc(configs, func(config codersdk.ChatModel) bool { return config.ID == enabledConfig.ID && config.Enabled })) - require.True(t, slices.ContainsFunc(configs, func(config codersdk.ChatModelConfig) bool { + require.True(t, slices.ContainsFunc(configs, func(config codersdk.ChatModel) bool { return config.Model == "gpt-4o-disabled" && !config.Enabled })) }) @@ -4639,7 +4639,7 @@ func TestListChatModelConfigs(t *testing.T) { memberClientRaw, _ := coderdtest.CreateAnotherUser(t, adminClient.Client, firstUser.OrganizationID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) - adminConfigs, err := adminClient.ListChatModelConfigs(ctx) + adminConfigs, err := adminClient.ChatModels(ctx) require.NoError(t, err) adminIDs := make([]uuid.UUID, 0, len(adminConfigs)) for _, config := range adminConfigs { @@ -4647,7 +4647,7 @@ func TestListChatModelConfigs(t *testing.T) { } require.Contains(t, adminIDs, providerDisabledConfig.ID) - memberConfigs, err := memberClient.ListChatModelConfigs(ctx) + memberConfigs, err := memberClient.ChatModels(ctx) require.NoError(t, err) memberIDs := make([]uuid.UUID, 0, len(memberConfigs)) for _, config := range memberConfigs { @@ -4670,7 +4670,7 @@ func TestListChatModelConfigs(t *testing.T) { AIProviderID: uuid.NullUUID{UUID: defaultConfig.AIProviderID, Valid: true}, }) - configs, err := client.ListChatModelConfigs(ctx) + configs, err := client.ChatModels(ctx) require.NoError(t, err) require.Contains(t, configs, defaultConfig) for _, config := range configs { @@ -4689,7 +4689,7 @@ func TestListChatModelConfigs(t *testing.T) { memberClient := codersdk.NewExperimentalClient(memberClientRaw) // The management list returns all rows allowed by the row ACL. - configs, err := memberClient.ListChatModelConfigs(ctx) + configs, err := memberClient.ChatModels(ctx) require.NoError(t, err) require.NotEmpty(t, configs) @@ -4719,7 +4719,7 @@ func TestCreateChatModelConfig(t *testing.T) { contextLimit := int64(4096) isDefault := true - modelConfig, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + modelConfig, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &aiProvider.ID, Model: "gpt-4o-mini", ContextLimit: &contextLimit, @@ -4732,7 +4732,7 @@ func TestCreateChatModelConfig(t *testing.T) { require.EqualValues(t, 4096, modelConfig.ContextLimit) require.True(t, modelConfig.IsDefault) - configs, err := client.ListChatModelConfigs(ctx) + configs, err := client.ChatModels(ctx) require.NoError(t, err) require.Len(t, configs, 1) }) @@ -4748,7 +4748,7 @@ func TestCreateChatModelConfig(t *testing.T) { store.armFailProviderReferenceLock(aiProvider.ID) contextLimit := int64(4096) - _, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + _, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &aiProvider.ID, Model: "gpt-4o-mini", ContextLimit: &contextLimit, @@ -4773,11 +4773,11 @@ func TestCreateChatModelConfig(t *testing.T) { // the losers 409 on the single-default unique index. const creators = 10 contextLimit := int64(4096) - var claimed codersdk.ChatModelConfig + var claimed codersdk.ChatModel var eg errgroup.Group for i := range creators - 1 { eg.Go(func() error { - _, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + _, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &aiProvider.ID, Model: fmt.Sprintf("gpt-4o-mini-%d", i), ContextLimit: &contextLimit, @@ -4786,7 +4786,7 @@ func TestCreateChatModelConfig(t *testing.T) { }) } eg.Go(func() error { - created, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + created, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &aiProvider.ID, Model: "gpt-4o", ContextLimit: &contextLimit, @@ -4794,7 +4794,7 @@ func TestCreateChatModelConfig(t *testing.T) { if err != nil { return xerrors.Errorf("create claimed config: %w", err) } - claimed, err = client.UpdateChatModelConfig(ctx, created.ID, codersdk.UpdateChatModelConfigRequest{ + claimed, err = client.UpdateChatModel(ctx, created.ID, codersdk.UpdateChatModelRequest{ IsDefault: ptr.Ref(true), }) if err != nil { @@ -4804,7 +4804,7 @@ func TestCreateChatModelConfig(t *testing.T) { }) require.NoError(t, eg.Wait()) - configs, err := client.ListChatModelConfigs(ctx) + configs, err := client.ChatModels(ctx) require.NoError(t, err) require.Len(t, configs, creators) var defaults []uuid.UUID @@ -4849,7 +4849,7 @@ func TestCreateChatModelConfig(t *testing.T) { aiProvider := createAIProviderForTest(t, client, "openai", "test-api-key") contextLimit := int64(4096) - modelConfig, err := adminClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + modelConfig, err := adminClient.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &aiProvider.ID, Model: "gpt-4o-mini", ContextLimit: &contextLimit, @@ -4907,26 +4907,26 @@ func TestCreateChatModelConfig(t *testing.T) { defaultConfig := createChatModelConfig(t, client) contextLimit := int64(4096) - modelConfig, err := writerClient.CreateChatModelConfig(setupCtx, codersdk.CreateChatModelConfigRequest{ + modelConfig, err := writerClient.CreateChatModel(setupCtx, codersdk.CreateChatModelRequest{ AIProviderID: &aiProvider.ID, Model: "gpt-4o-mini", ContextLimit: &contextLimit, }) require.NoError(t, err) - updated, err := writerClient.UpdateChatModelConfig(setupCtx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + updated, err := writerClient.UpdateChatModel(setupCtx, modelConfig.ID, codersdk.UpdateChatModelRequest{ DisplayName: "Window Write", }) require.NoError(t, err) require.Equal(t, "Window Write", updated.DisplayName) - require.NoError(t, writerClient.DeleteChatModelConfig(setupCtx, modelConfig.ID)) + require.NoError(t, writerClient.DeleteChatModel(setupCtx, modelConfig.ID)) // TODO(mafredri): remove these default-transition checks after // CODAGT-709 M3 (org-scoping cutover). ctx := testutil.Context(t, testutil.WaitLong) notDefault := false - updated, err = writerClient.UpdateChatModelConfig(ctx, defaultConfig.ID, codersdk.UpdateChatModelConfigRequest{ + updated, err = writerClient.UpdateChatModel(ctx, defaultConfig.ID, codersdk.UpdateChatModelRequest{ IsDefault: ¬Default, }) require.NoError(t, err) @@ -4935,27 +4935,27 @@ func TestCreateChatModelConfig(t *testing.T) { require.NoError(t, err) require.True(t, row.IsDefault, "the sole config must remain default") - replacement, err := writerClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + replacement, err := writerClient.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &aiProvider.ID, Model: "gpt-4o-mini-replacement", ContextLimit: &contextLimit, }) require.NoError(t, err) - require.NoError(t, writerClient.DeleteChatModelConfig(ctx, defaultConfig.ID)) + require.NoError(t, writerClient.DeleteChatModel(ctx, defaultConfig.ID)) _, err = rawDB.GetChatModelConfigByID(ctx, defaultConfig.ID) require.ErrorIs(t, err, sql.ErrNoRows) row, err = rawDB.GetChatModelConfigByID(ctx, replacement.ID) require.NoError(t, err) require.True(t, row.IsDefault, "replacement must be promoted") - candidate, err := writerClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + candidate, err := writerClient.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &aiProvider.ID, Model: "gpt-4o-mini-promote", ContextLimit: &contextLimit, }) require.NoError(t, err) isDefault := true - updated, err = writerClient.UpdateChatModelConfig(ctx, candidate.ID, codersdk.UpdateChatModelConfigRequest{ + updated, err = writerClient.UpdateChatModel(ctx, candidate.ID, codersdk.UpdateChatModelRequest{ IsDefault: &isDefault, }) require.NoError(t, err) @@ -4978,7 +4978,7 @@ func TestCreateChatModelConfig(t *testing.T) { aiProvider := createAIProviderForTest(t, client, "openai", "test-api-key") contextLimit := int64(4096) - modelConfig, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + modelConfig, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &aiProvider.ID, Model: "gpt-4o-mini", ContextLimit: &contextLimit, @@ -5009,7 +5009,7 @@ func TestCreateChatModelConfig(t *testing.T) { aiProvider := createAIProviderForTest(t, client, "openai", "test-api-key") contextLimit := int64(4096) - _, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + _, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &aiProvider.ID, Model: "gpt-4o-mini", ContextLimit: &contextLimit, @@ -5034,7 +5034,7 @@ func TestCreateChatModelConfig(t *testing.T) { aiProvider := createAIProviderForTest(t, client, "openai", "test-api-key") contextLimit := int64(4096) - _, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + _, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &aiProvider.ID, Model: "gpt-4o-mini", ContextLimit: &contextLimit, @@ -5064,7 +5064,7 @@ func TestCreateChatModelConfig(t *testing.T) { aiProvider := createAIProviderForTest(t, client, "openai", "test-api-key") contextLimit := int64(4096) - _, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + _, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &aiProvider.ID, Model: "gpt-4o-mini", ContextLimit: &contextLimit, @@ -5088,7 +5088,7 @@ func TestCreateChatModelConfig(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client.Client) aiProvider := createAIProviderForTest(t, client, "openai", "test-api-key") - _, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + _, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &aiProvider.ID, Model: "gpt-4o-mini", }) @@ -5104,7 +5104,7 @@ func TestCreateChatModelConfig(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client.Client) contextLimit := int64(4096) - _, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + _, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ Model: "gpt-4o-mini", ContextLimit: &contextLimit, }) @@ -5121,7 +5121,7 @@ func TestCreateChatModelConfig(t *testing.T) { contextLimit := int64(4096) missingProviderID := uuid.New() - _, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + _, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &missingProviderID, Model: "gpt-4o-mini", ContextLimit: &contextLimit, @@ -5145,7 +5145,7 @@ func TestCreateChatModelConfig(t *testing.T) { require.NoError(t, err) contextLimit := int64(4096) - modelConfig, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + modelConfig, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &provider.ID, Model: "gpt-4o-mini", ContextLimit: &contextLimit, @@ -5164,7 +5164,7 @@ func TestCreateChatModelConfig(t *testing.T) { missingProviderID := uuid.New() contextLimit := int64(4096) - _, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + _, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &missingProviderID, Model: "gpt-4o-mini", ContextLimit: &contextLimit, @@ -5188,7 +5188,7 @@ func TestCreateChatModelConfig(t *testing.T) { require.NoError(t, err) contextLimit := int64(4096) - _, err = client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + _, err = client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &provider.ID, Model: "gpt-4o-mini", ContextLimit: &contextLimit, @@ -5214,7 +5214,7 @@ func TestCreateChatModelConfig(t *testing.T) { require.NoError(t, err) contextLimit := int64(4096) - _, err = client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + _, err = client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &aiProvider.ID, Model: "anthropic/claude-opus-4.6", ContextLimit: &contextLimit, @@ -5236,7 +5236,7 @@ func TestCreateChatModelConfig(t *testing.T) { aiProvider := createAIProviderForTest(t, adminClient, "openai", "test-api-key") contextLimit := int64(4096) - _, err := memberClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + _, err := memberClient.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &aiProvider.ID, Model: "gpt-4o-mini", ContextLimit: &contextLimit, @@ -5257,7 +5257,7 @@ func TestUpdateChatModelConfig(t *testing.T) { modelConfig := createChatModelConfig(t, client) contextLimit := int64(8192) - updated, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + updated, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{ DisplayName: "GPT-4o Mini Updated", ContextLimit: &contextLimit, }) @@ -5266,7 +5266,7 @@ func TestUpdateChatModelConfig(t *testing.T) { require.Equal(t, "GPT-4o Mini Updated", updated.DisplayName) require.EqualValues(t, 8192, updated.ContextLimit) - configs, err := client.ListChatModelConfigs(ctx) + configs, err := client.ChatModels(ctx) require.NoError(t, err) require.Len(t, configs, 1) }) @@ -5284,7 +5284,7 @@ func TestUpdateChatModelConfig(t *testing.T) { AIProviderID: uuid.NullUUID{UUID: defaultConfig.AIProviderID, Valid: true}, }) - _, err := client.UpdateChatModelConfig(ctx, otherConfig.ID, codersdk.UpdateChatModelConfigRequest{ + _, err := client.UpdateChatModel(ctx, otherConfig.ID, codersdk.UpdateChatModelRequest{ DisplayName: "not allowed", }) requireSDKError(t, err, http.StatusNotFound) @@ -5302,7 +5302,7 @@ func TestUpdateChatModelConfig(t *testing.T) { firstUser := coderdtest.CreateFirstUser(t, client.Client) modelConfig := createChatModelConfig(t, client) - updated, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + updated, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{ IsDefault: ptr.Ref(false), }) require.NoError(t, err) @@ -5334,7 +5334,7 @@ func TestUpdateChatModelConfig(t *testing.T) { IsDefault: true, }) - updated, err := client.UpdateChatModelConfig(ctx, candidateConfig.ID, codersdk.UpdateChatModelConfigRequest{ + updated, err := client.UpdateChatModel(ctx, candidateConfig.ID, codersdk.UpdateChatModelRequest{ IsDefault: ptr.Ref(true), }) require.NoError(t, err) @@ -5356,7 +5356,7 @@ func TestUpdateChatModelConfig(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client.Client) modelConfig := createChatModelConfig(t, client) - updated, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + updated, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{ Model: "gpt-4o-mini-updated", }) require.NoError(t, err) @@ -5383,14 +5383,14 @@ func TestUpdateChatModelConfig(t *testing.T) { require.NoError(t, err) contextLimit := int64(4096) - modelConfig, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + modelConfig, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &aiProvider.ID, Model: "gpt-4o-mini", ContextLimit: &contextLimit, }) require.NoError(t, err) - _, err = client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + _, err = client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{ Model: "anthropic/claude-opus-4.6", }) sdkErr := requireSDKError(t, err, http.StatusBadRequest) @@ -5419,7 +5419,7 @@ func TestUpdateChatModelConfig(t *testing.T) { AIProviderID: uuid.NullUUID{UUID: aiProvider.ID, Valid: true}, }) - updated, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + updated, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{ DisplayName: "Existing OpenRouter Config", }) require.NoError(t, err) @@ -5452,14 +5452,14 @@ func TestUpdateChatModelConfig(t *testing.T) { require.NoError(t, err) contextLimit := int64(4096) - modelConfig, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + modelConfig, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &validProvider.ID, Model: "anthropic/claude-opus-4.6", ContextLimit: &contextLimit, }) require.NoError(t, err) - _, err = client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + _, err = client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{ AIProviderID: &misconfiguredProvider.ID, }) sdkErr := requireSDKError(t, err, http.StatusBadRequest) @@ -5478,14 +5478,14 @@ func TestUpdateChatModelConfig(t *testing.T) { modelConfig := createChatModelConfig(t, adminClient) enabled := false - updated, err := adminClient.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + updated, err := adminClient.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{ Enabled: &enabled, }) require.NoError(t, err) require.Equal(t, modelConfig.ID, updated.ID) require.False(t, updated.Enabled) - adminConfigs, err := adminClient.ListChatModelConfigs(ctx) + adminConfigs, err := adminClient.ChatModels(ctx) require.NoError(t, err) foundForAdmin := false @@ -5497,7 +5497,7 @@ func TestUpdateChatModelConfig(t *testing.T) { } require.True(t, foundForAdmin) - memberConfigs, err := memberClient.ListChatModelConfigs(ctx) + memberConfigs, err := memberClient.ChatModels(ctx) require.NoError(t, err) foundForMember := false @@ -5523,7 +5523,7 @@ func TestUpdateChatModelConfig(t *testing.T) { contextLimit := int64(4096) enabled := false - modelConfig, err := adminClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + modelConfig, err := adminClient.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &aiProvider.ID, Model: "gpt-4o-reenable", DisplayName: "GPT-4o Re-enable", @@ -5533,7 +5533,7 @@ func TestUpdateChatModelConfig(t *testing.T) { require.NoError(t, err) require.False(t, modelConfig.Enabled) - memberConfigs, err := memberClient.ListChatModelConfigs(ctx) + memberConfigs, err := memberClient.ChatModels(ctx) require.NoError(t, err) foundForMember := false @@ -5546,14 +5546,14 @@ func TestUpdateChatModelConfig(t *testing.T) { require.True(t, foundForMember) enabled = true - updated, err := adminClient.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + updated, err := adminClient.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{ Enabled: &enabled, }) require.NoError(t, err) require.Equal(t, modelConfig.ID, updated.ID) require.True(t, updated.Enabled) - memberConfigs, err = memberClient.ListChatModelConfigs(ctx) + memberConfigs, err = memberClient.ChatModels(ctx) require.NoError(t, err) foundForMember = false @@ -5581,7 +5581,7 @@ func TestUpdateChatModelConfig(t *testing.T) { }) require.NoError(t, err) - updated, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + updated, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{ AIProviderID: &provider.ID, Model: "claude-3-5-sonnet-latest", }) @@ -5605,14 +5605,14 @@ func TestUpdateChatModelConfig(t *testing.T) { }) require.NoError(t, err) - updated, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + updated, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{ AIProviderID: &provider.ID, Model: "claude-3-5-sonnet-latest", }) require.NoError(t, err) require.NotEqual(t, uuid.Nil, updated.AIProviderID) - updated, err = client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + updated, err = client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{ Model: "claude-3-5-haiku-latest", }) require.NoError(t, err) @@ -5629,7 +5629,7 @@ func TestUpdateChatModelConfig(t *testing.T) { modelConfig := createChatModelConfig(t, client) missingProviderID := uuid.New() - _, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + _, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{ AIProviderID: &missingProviderID, }) sdkErr := requireSDKError(t, err, http.StatusPreconditionFailed) @@ -5651,7 +5651,7 @@ func TestUpdateChatModelConfig(t *testing.T) { }) require.NoError(t, err) - _, err = client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + _, err = client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{ AIProviderID: &provider.ID, }) sdkErr := requireSDKError(t, err, http.StatusPreconditionFailed) @@ -5667,7 +5667,7 @@ func TestUpdateChatModelConfig(t *testing.T) { modelConfig := createChatModelConfig(t, client) missingProviderID := uuid.New() - _, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + _, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{ AIProviderID: &missingProviderID, }) sdkErr := requireSDKError(t, err, http.StatusPreconditionFailed) @@ -5684,7 +5684,7 @@ func TestUpdateChatModelConfig(t *testing.T) { store.armFailNextUpdate(modelConfig.ID) - _, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + _, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{ DisplayName: "missing in tx", }) requireSDKError(t, err, http.StatusNotFound) @@ -5702,7 +5702,7 @@ func TestUpdateChatModelConfig(t *testing.T) { contextLimit := int64(4096) isDefault := false - candidateConfig, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + candidateConfig, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &aiProvider.ID, Model: "claude-3-5-sonnet", ContextLimit: &contextLimit, @@ -5712,7 +5712,7 @@ func TestUpdateChatModelConfig(t *testing.T) { store.armFailNextUpdate(candidateConfig.ID) - _, err = client.UpdateChatModelConfig(ctx, defaultConfig.ID, codersdk.UpdateChatModelConfigRequest{ + _, err = client.UpdateChatModel(ctx, defaultConfig.ID, codersdk.UpdateChatModelRequest{ IsDefault: ptr.Ref(false), }) sdkErr := requireSDKError(t, err, http.StatusInternalServerError) @@ -5726,7 +5726,7 @@ func TestUpdateChatModelConfig(t *testing.T) { client := newChatClient(t) _ = coderdtest.CreateFirstUser(t, client.Client) - _, err := client.UpdateChatModelConfig(ctx, uuid.New(), codersdk.UpdateChatModelConfigRequest{ + _, err := client.UpdateChatModel(ctx, uuid.New(), codersdk.UpdateChatModelRequest{ DisplayName: "missing", }) requireSDKError(t, err, http.StatusNotFound) @@ -5741,7 +5741,7 @@ func TestUpdateChatModelConfig(t *testing.T) { modelConfig := createChatModelConfig(t, client) contextLimit := int64(0) - _, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + _, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{ ContextLimit: &contextLimit, }) sdkErr := requireSDKError(t, err, http.StatusBadRequest) @@ -5760,7 +5760,7 @@ func TestUpdateChatModelConfig(t *testing.T) { store.armVanishAtLockedRead(modelConfig.ID) - _, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + _, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{ DisplayName: "vanished before update", }) requireSDKError(t, err, http.StatusNotFound) @@ -5777,7 +5777,7 @@ func TestUpdateChatModelConfig(t *testing.T) { const sentinel = "locked-copy-sentinel-model" store.armMutateAtLockedRead(modelConfig.ID, sentinel) - updated, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + updated, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{ DisplayName: "only-display-name", }) require.NoError(t, err) @@ -5799,7 +5799,7 @@ func TestUpdateChatModelConfig(t *testing.T) { }) require.NoError(t, err) - _, err = client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + _, err = client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{ Model: "gpt-4o-different", }) sdkErr := requireSDKError(t, err, http.StatusPreconditionFailed) @@ -5817,7 +5817,7 @@ func TestUpdateChatModelConfig(t *testing.T) { err := client.DeleteAIProvider(ctx, modelConfig.AIProviderID.String()) require.NoError(t, err) - _, err = client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + _, err = client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{ Model: "gpt-4o-different", }) sdkErr := requireSDKError(t, err, http.StatusPreconditionFailed) @@ -5832,7 +5832,7 @@ func TestUpdateChatModelConfig(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client.Client) modelConfig := createChatModelConfig(t, client) - _, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + _, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{ CompressionThreshold: ptr.Ref(int32(150)), }) sdkErr := requireSDKError(t, err, http.StatusBadRequest) @@ -5847,7 +5847,7 @@ func TestUpdateChatModelConfig(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client.Client) modelConfig := createChatModelConfig(t, client) - updated, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + updated, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{ CompressionThreshold: ptr.Ref(int32(55)), }) require.NoError(t, err) @@ -5862,7 +5862,7 @@ func TestUpdateChatModelConfig(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client.Client) modelConfig := createChatModelConfig(t, client) - updated, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + updated, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{ ModelConfig: &codersdk.ChatModelCallConfig{ ReasoningEffort: &codersdk.ChatModelReasoningEffortConfig{ Default: ptr.Ref("high"), @@ -5888,7 +5888,7 @@ func TestUpdateChatModelConfig(t *testing.T) { ctx, http.MethodPatch, "/api/experimental/chats/model-configs/not-a-uuid", - codersdk.UpdateChatModelConfigRequest{DisplayName: "ignored"}, + codersdk.UpdateChatModelRequest{DisplayName: "ignored"}, ) require.NoError(t, err) defer res.Body.Close() @@ -5908,7 +5908,7 @@ func TestUpdateChatModelConfig(t *testing.T) { memberClient := codersdk.NewExperimentalClient(memberClientRaw) modelConfig := createChatModelConfig(t, adminClient) - _, err := memberClient.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + _, err := memberClient.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{ DisplayName: "member update", }) requireSDKError(t, err, http.StatusForbidden) @@ -5926,10 +5926,10 @@ func TestDeleteChatModelConfig(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client.Client) modelConfig := createChatModelConfig(t, client) - err := client.DeleteChatModelConfig(ctx, modelConfig.ID) + err := client.DeleteChatModel(ctx, modelConfig.ID) require.NoError(t, err) - configs, err := client.ListChatModelConfigs(ctx) + configs, err := client.ChatModels(ctx) require.NoError(t, err) for _, config := range configs { require.NotEqual(t, modelConfig.ID, config.ID) @@ -5949,7 +5949,7 @@ func TestDeleteChatModelConfig(t *testing.T) { AIProviderID: uuid.NullUUID{UUID: defaultConfig.AIProviderID, Valid: true}, }) - err := client.DeleteChatModelConfig(ctx, otherConfig.ID) + err := client.DeleteChatModel(ctx, otherConfig.ID) requireSDKError(t, err, http.StatusNotFound) _, err = db.GetChatModelConfigByID(dbauthz.AsSystemRestricted(ctx), otherConfig.ID) require.NoError(t, err) @@ -5976,7 +5976,7 @@ func TestDeleteChatModelConfig(t *testing.T) { IsDefault: true, }) - err := client.DeleteChatModelConfig(ctx, defaultConfig.ID) + err := client.DeleteChatModel(ctx, defaultConfig.ID) require.NoError(t, err) orgDefault, err := db.GetDefaultChatModelConfig(dbauthz.AsSystemRestricted(ctx), firstUser.OrganizationID) @@ -6013,10 +6013,10 @@ func TestDeleteChatModelConfig(t *testing.T) { "z-enabled-model", ) - err := client.DeleteChatModelConfig(ctx, defaultConfig.ID) + err := client.DeleteChatModel(ctx, defaultConfig.ID) require.NoError(t, err) - configs, err := client.ListChatModelConfigs(ctx) + configs, err := client.ChatModels(ctx) require.NoError(t, err) defaultID := uuid.Nil for _, config := range configs { @@ -6034,7 +6034,7 @@ func TestDeleteChatModelConfig(t *testing.T) { client := newChatClient(t) _ = coderdtest.CreateFirstUser(t, client.Client) - err := client.DeleteChatModelConfig(ctx, uuid.New()) + err := client.DeleteChatModel(ctx, uuid.New()) requireSDKError(t, err, http.StatusNotFound) }) @@ -6048,7 +6048,7 @@ func TestDeleteChatModelConfig(t *testing.T) { store.armFailNextDelete(modelConfig.ID) - err := client.DeleteChatModelConfig(ctx, modelConfig.ID) + err := client.DeleteChatModel(ctx, modelConfig.ID) requireSDKError(t, err, http.StatusNotFound) }) @@ -6083,7 +6083,7 @@ func TestDeleteChatModelConfig(t *testing.T) { memberClient := codersdk.NewExperimentalClient(memberClientRaw) modelConfig := createChatModelConfig(t, adminClient) - err := memberClient.DeleteChatModelConfig(ctx, modelConfig.ID) + err := memberClient.DeleteChatModel(ctx, modelConfig.ID) requireSDKError(t, err, http.StatusForbidden) }) } @@ -13307,7 +13307,7 @@ func seedPasteOnlyTitleSourceMessage( func createTitleGenerationModelConfig( t *testing.T, client *codersdk.ExperimentalClient, -) codersdk.ChatModelConfig { +) codersdk.ChatModel { t.Helper() return createAdditionalChatModelConfig(t, client, "openai", "gpt-4.1") } @@ -13339,21 +13339,35 @@ func seedChatWithDeletedModelConfig( return chat } -func createChatModelConfig(t testing.TB, client *codersdk.ExperimentalClient) codersdk.ChatModelConfig { +func createChatModelConfig(t testing.TB, client *codersdk.ExperimentalClient) codersdk.ChatModel { t.Helper() - return coderdtest.CreateOpenAICompatChatModelConfig(t, client, "") + return coderdtest.CreateOpenAICompatChatModel(t, client, "") } -func createChatModelConfigWithBaseURL(t testing.TB, client *codersdk.ExperimentalClient, baseURL string) codersdk.ChatModelConfig { +// createChatModel bridges the SDK-layer name while the later API layer owns +// the full helper rename. +func createChatModel(t testing.TB, client *codersdk.ExperimentalClient) codersdk.ChatModel { t.Helper() - return coderdtest.CreateOpenAICompatChatModelConfig(t, client, baseURL) + return createChatModelConfig(t, client) +} + +// createChatModelWithBaseURL bridges the SDK-layer name while the later API +// layer owns the full helper rename. +func createChatModelWithBaseURL(t testing.TB, client *codersdk.ExperimentalClient, baseURL string) codersdk.ChatModel { + t.Helper() + return createChatModelConfigWithBaseURL(t, client, baseURL) +} + +func createChatModelConfigWithBaseURL(t testing.TB, client *codersdk.ExperimentalClient, baseURL string) codersdk.ChatModel { + t.Helper() + return coderdtest.CreateOpenAICompatChatModel(t, client, baseURL) } // createChatModelConfigWithTitleFailure provisions a model whose streaming chat // responses succeed, while non-streaming requests fail. The non-streaming path // is how quick title generation requests structured output, so tests can fail // title generation without breaking the main assistant response. -func createChatModelConfigWithTitleFailure(t testing.TB, client *codersdk.ExperimentalClient) codersdk.ChatModelConfig { +func createChatModelConfigWithTitleFailure(t testing.TB, client *codersdk.ExperimentalClient) codersdk.ChatModel { t.Helper() baseURL := chattest.NewOpenAI(t, func(req *chattest.OpenAIRequest) chattest.OpenAIResponse { if req.Stream { @@ -13367,7 +13381,7 @@ func createChatModelConfigWithTitleFailure(t testing.TB, client *codersdk.Experi // createChatModelConfigWithTitleQuotaExhausted provisions a model whose // non-streaming responses return a provider insufficient_quota error, which // classifies as a usage limit like an exhausted AI Gateway budget. -func createChatModelConfigWithTitleQuotaExhausted(t testing.TB, client *codersdk.ExperimentalClient) codersdk.ChatModelConfig { +func createChatModelConfigWithTitleQuotaExhausted(t testing.TB, client *codersdk.ExperimentalClient) codersdk.ChatModel { t.Helper() baseURL := chattest.NewOpenAI(t, func(req *chattest.OpenAIRequest) chattest.OpenAIResponse { if req.Stream { @@ -13382,12 +13396,24 @@ func createChatModelConfigWithTitleQuotaExhausted(t testing.TB, client *codersdk return createChatModelConfigWithBaseURL(t, client, baseURL) } +// createAdditionalChatModel bridges the SDK-layer name while the later API +// layer owns the full helper rename. +func createAdditionalChatModel( + t *testing.T, + client *codersdk.ExperimentalClient, + provider string, + model string, +) codersdk.ChatModel { + t.Helper() + return createAdditionalChatModelConfig(t, client, provider, model) +} + func createAdditionalChatModelConfig( t *testing.T, client *codersdk.ExperimentalClient, provider string, model string, -) codersdk.ChatModelConfig { +) codersdk.ChatModel { t.Helper() return createAdditionalChatModelConfigWithModelConfig(t, client, provider, model, nil) } @@ -13399,7 +13425,7 @@ func createAdditionalChatModelConfigWithReasoningEffort( model string, defaultEffort string, maxEffort string, -) codersdk.ChatModelConfig { +) codersdk.ChatModel { t.Helper() return createAdditionalChatModelConfigWithModelConfig(t, client, provider, model, &codersdk.ChatModelCallConfig{ ReasoningEffort: &codersdk.ChatModelReasoningEffortConfig{ @@ -13415,14 +13441,14 @@ func createAdditionalChatModelConfigWithModelConfig( provider string, model string, modelCallConfig *codersdk.ChatModelCallConfig, -) codersdk.ChatModelConfig { +) codersdk.ChatModel { t.Helper() ctx := testutil.Context(t, testutil.WaitLong) aiProvider := createAIProviderForTest(t, client, provider, "test-api-key") contextLimit := int64(4096) isDefault := false - modelConfig, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + modelConfig, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &aiProvider.ID, Model: model, ContextLimit: &contextLimit, @@ -13438,12 +13464,12 @@ func createDisabledChatModelConfig( client *codersdk.ExperimentalClient, provider string, model string, -) codersdk.ChatModelConfig { +) codersdk.ChatModel { t.Helper() modelConfig := createAdditionalChatModelConfig(t, client, provider, model) ctx := testutil.Context(t, testutil.WaitLong) - updated, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + updated, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{ Enabled: ptr.Ref(false), }) require.NoError(t, err) @@ -13457,7 +13483,7 @@ func createProviderDisabledChatModelConfig( client *codersdk.ExperimentalClient, provider string, model string, -) codersdk.ChatModelConfig { +) codersdk.ChatModel { t.Helper() modelConfig := createAdditionalChatModelConfig(t, client, provider, model) @@ -15195,12 +15221,12 @@ func TestUserChatPersonalModelOverrides(t *testing.T) { }) require.NoError(t, err) contextLimit := int64(4096) - modelConfigRequest := codersdk.CreateChatModelConfigRequest{ + modelConfigRequest := codersdk.CreateChatModelRequest{ AIProviderID: &modelProvider.ID, Model: "claude-personal-" + uuid.NewString(), ContextLimit: &contextLimit, } - modelConfig, err := adminClient.CreateChatModelConfig(ctx, modelConfigRequest) + modelConfig, err := adminClient.CreateChatModel(ctx, modelConfigRequest) require.NoError(t, err) modelConfigRequest.Model = "claude-personal-reasoning-" + uuid.NewString() modelConfigRequest.ModelConfig = &codersdk.ChatModelCallConfig{ @@ -15209,7 +15235,7 @@ func TestUserChatPersonalModelOverrides(t *testing.T) { Max: ptr.Ref("high"), }, } - reasoningModelConfig, err := adminClient.CreateChatModelConfig(ctx, modelConfigRequest) + reasoningModelConfig, err := adminClient.CreateChatModel(ctx, modelConfigRequest) require.NoError(t, err) err = adminClient.UpdateChatModelOverride(ctx, codersdk.ChatModelOverrideContextGeneral, codersdk.UpdateChatModelOverrideRequest{ ModelConfigID: modelConfig.ID.String(), @@ -15228,7 +15254,7 @@ func TestUserChatPersonalModelOverrides(t *testing.T) { ) disabledProvider := createAIProviderForTest(t, adminClient, "google", "test-api-key") contextLimit = int64(4096) - disabledProviderModelConfig, err := adminClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + disabledProviderModelConfig, err := adminClient.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &disabledProvider.ID, Model: "gemini-personal-disabled-provider-" + uuid.NewString(), ContextLimit: &contextLimit, @@ -15669,7 +15695,7 @@ func TestCreateChatPersonalModelOverrideRoot(t *testing.T) { }) require.NoError(t, err) contextLimit := int64(4096) - overrideModel, err := adminClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + overrideModel, err := adminClient.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &overrideProvider.ID, Model: "claude-root-personal-" + uuid.NewString(), ContextLimit: &contextLimit, @@ -15776,7 +15802,7 @@ func TestCreateChatPersonalModelOverrideRoot(t *testing.T) { }) t.Run("RootModelOverrideUsesSavedReasoningEffort", func(t *testing.T) { - reasoningModel, err := adminClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + reasoningModel, err := adminClient.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &overrideProvider.ID, Model: "claude-root-personal-reasoning-" + uuid.NewString(), ContextLimit: &contextLimit, diff --git a/coderd/mcp_test.go b/coderd/mcp_test.go index aff79d5fe75..08a10cb167f 100644 --- a/coderd/mcp_test.go +++ b/coderd/mcp_test.go @@ -3249,7 +3249,7 @@ func TestChatWithMCPServerIDs(t *testing.T) { expClient := codersdk.NewExperimentalClient(client) // Create the chat model config required for creating a chat. - _ = createChatModelConfigForMCP(t, expClient) + _ = createChatModelForMCP(t, expClient) // Create enabled MCP server configs. mcpConfigA := createMCPServerConfig(t, client, firstUser.OrganizationID, "chat-mcp-server-a", true) @@ -3284,9 +3284,9 @@ func TestChatWithMCPServerIDs(t *testing.T) { require.Contains(t, fetched.MCPServerIDs, mcpConfigB.ID) } -func createChatModelConfigForMCP(t testing.TB, client *codersdk.ExperimentalClient) codersdk.ChatModelConfig { +func createChatModelForMCP(t testing.TB, client *codersdk.ExperimentalClient) codersdk.ChatModel { t.Helper() - return coderdtest.CreateOpenAICompatChatModelConfig(t, client, "") + return coderdtest.CreateOpenAICompatChatModel(t, client, "") } func TestMCPOAuth2DiscoveryEdgeCases(t *testing.T) { diff --git a/coderd/x/chatd/chatd_test.go b/coderd/x/chatd/chatd_test.go index 47babace29b..5bec32c8de4 100644 --- a/coderd/x/chatd/chatd_test.go +++ b/coderd/x/chatd/chatd_test.go @@ -278,7 +278,7 @@ func TestSubagentChatExcludesWorkspaceProvisioningTools(t *testing.T) { ) }) - coderdtest.CreateOpenAICompatChatModelConfig(t, expClient, openAIURL) + coderdtest.CreateOpenAICompatChatModel(t, expClient, openAIURL) // Create a root chat whose first model call will spawn a subagent. chat, err := expClient.CreateChat(ctx, codersdk.CreateChatRequest{ @@ -446,7 +446,7 @@ func TestPlanModeSubagentChatExcludesAskUserQuestion(t *testing.T) { ) }) - coderdtest.CreateOpenAICompatChatModelConfig(t, expClient, openAIURL) + coderdtest.CreateOpenAICompatChatModel(t, expClient, openAIURL) chat, err := expClient.CreateChat(ctx, codersdk.CreateChatRequest{ OrganizationID: user.OrganizationID, @@ -587,7 +587,7 @@ func TestExploreSubagentIsReadOnly(t *testing.T) { ) }) - coderdtest.CreateOpenAICompatChatModelConfig(t, expClient, openAIURL) + coderdtest.CreateOpenAICompatChatModel(t, expClient, openAIURL) _, err := expClient.CreateChat(ctx, codersdk.CreateChatRequest{ OrganizationID: user.OrganizationID, @@ -4465,7 +4465,7 @@ func TestCreateWorkspaceTool_EndToEnd(t *testing.T) { ) }) - coderdtest.CreateOpenAICompatChatModelConfig(t, expClient, openAIURL) + coderdtest.CreateOpenAICompatChatModel(t, expClient, openAIURL) chat, err := expClient.CreateChat(ctx, codersdk.CreateChatRequest{ OrganizationID: user.OrganizationID, @@ -4620,7 +4620,7 @@ func TestStartWorkspaceTool_EndToEnd(t *testing.T) { ) }) - coderdtest.CreateOpenAICompatChatModelConfig(t, expClient, openAIURL) + coderdtest.CreateOpenAICompatChatModel(t, expClient, openAIURL) // Create a chat with the stopped workspace pre-associated. chat, err := expClient.CreateChat(ctx, codersdk.CreateChatRequest{ @@ -12146,7 +12146,7 @@ func TestAgentContextFilesAndSkillsLoadedIntoChat(t *testing.T) { ) }) - coderdtest.CreateOpenAICompatChatModelConfig(t, expClient, openAIURL) + coderdtest.CreateOpenAICompatChatModel(t, expClient, openAIURL) workspaceID := workspace.ID chat, err := expClient.CreateChat(ctx, codersdk.CreateChatRequest{ diff --git a/coderd/x/chatd/chatprovider/chatprovider.go b/coderd/x/chatd/chatprovider/chatprovider.go index b69d2437c99..0b80dcc398e 100644 --- a/coderd/x/chatd/chatprovider/chatprovider.go +++ b/coderd/x/chatd/chatprovider/chatprovider.go @@ -500,12 +500,12 @@ func (*ModelCatalog) ListConfiguredModels( configuredModels []ConfiguredModel, availabilityByProvider map[string]ProviderAvailability, enabledProviders map[string]struct{}, -) (codersdk.ChatModelsResponse, bool) { +) (codersdk.ChatModelAvailabilityResponse, bool) { if len(configuredModels) == 0 { - return codersdk.ChatModelsResponse{}, false + return codersdk.ChatModelAvailabilityResponse{}, false } - modelsByProvider := make(map[string][]codersdk.ChatModel) + modelsByProvider := make(map[string][]codersdk.ChatModelCatalogEntry) seenByProvider := make(map[string]map[string]struct{}) providerSet := make(map[string]struct{}) @@ -534,16 +534,16 @@ func (*ModelCatalog) ListConfiguredModels( seenByProvider[provider][normalizedModelID] = struct{}{} modelsByProvider[provider] = append( modelsByProvider[provider], - newChatModel(provider, modelID, model.DisplayName), + newChatModelCatalogEntry(provider, modelID, model.DisplayName), ) } providers := orderProviders(providerSet) if len(providers) == 0 { - return codersdk.ChatModelsResponse{}, false + return codersdk.ChatModelAvailabilityResponse{}, false } - response := codersdk.ChatModelsResponse{ + response := codersdk.ChatModelAvailabilityResponse{ Providers: make([]codersdk.ChatModelProvider, 0, len(providers)), } for _, provider := range providers { @@ -552,7 +552,7 @@ func (*ModelCatalog) ListConfiguredModels( } models := modelsByProvider[provider] - sortChatModels(models) + sortChatModelCatalogEntries(models) result := codersdk.ChatModelProvider{ Provider: provider, @@ -579,8 +579,8 @@ func (*ModelCatalog) ListConfiguredModels( func (*ModelCatalog) ListConfiguredProviderAvailability( availabilityByProvider map[string]ProviderAvailability, enabledProviders map[string]struct{}, -) codersdk.ChatModelsResponse { - response := codersdk.ChatModelsResponse{ +) codersdk.ChatModelAvailabilityResponse { + response := codersdk.ChatModelAvailabilityResponse{ Providers: make([]codersdk.ChatModelProvider, 0, len(supportedProviderNames)), } @@ -591,7 +591,7 @@ func (*ModelCatalog) ListConfiguredProviderAvailability( result := codersdk.ChatModelProvider{ Provider: provider, - Models: []codersdk.ChatModel{}, + Models: []codersdk.ChatModelCatalogEntry{}, } if avail, ok := availabilityByProvider[provider]; ok { result.Available = avail.Available @@ -641,13 +641,13 @@ func PruneDisabledProviderKeys(keys *ProviderAPIKeys, enabledProviders map[strin } } -func newChatModel(provider, modelID, displayName string) codersdk.ChatModel { +func newChatModelCatalogEntry(provider, modelID, displayName string) codersdk.ChatModelCatalogEntry { name := strings.TrimSpace(displayName) if name == "" { name = modelID } - return codersdk.ChatModel{ + return codersdk.ChatModelCatalogEntry{ ID: canonicalModelID(provider, modelID), Provider: provider, Model: modelID, @@ -655,8 +655,8 @@ func newChatModel(provider, modelID, displayName string) codersdk.ChatModel { } } -func sortChatModels(models []codersdk.ChatModel) { - slices.SortFunc(models, func(a, b codersdk.ChatModel) int { +func sortChatModelCatalogEntries(models []codersdk.ChatModelCatalogEntry) { + slices.SortFunc(models, func(a, b codersdk.ChatModelCatalogEntry) int { return strings.Compare(a.Model, b.Model) }) } diff --git a/coderd/x/chatd/chatprovider/chatprovider_test.go b/coderd/x/chatd/chatprovider/chatprovider_test.go index 92905513c7e..12751d44fd6 100644 --- a/coderd/x/chatd/chatprovider/chatprovider_test.go +++ b/coderd/x/chatd/chatprovider/chatprovider_test.go @@ -679,7 +679,7 @@ func TestListConfiguredModels_PolicyAwareAvailability(t *testing.T) { configuredModels []chatprovider.ConfiguredModel availabilityByProvider map[string]chatprovider.ProviderAvailability enabledProviders map[string]struct{} - want codersdk.ChatModelsResponse + want codersdk.ChatModelAvailabilityResponse }{ { name: "PolicyUnavailableOverridesConfiguredKey", @@ -697,11 +697,11 @@ func TestListConfiguredModels_PolicyAwareAvailability(t *testing.T) { }, }, enabledProviders: enabledProviders(fantasyopenai.Name), - want: codersdk.ChatModelsResponse{Providers: []codersdk.ChatModelProvider{{ + want: codersdk.ChatModelAvailabilityResponse{Providers: []codersdk.ChatModelProvider{{ Provider: fantasyopenai.Name, Available: false, UnavailableReason: codersdk.ChatModelProviderUnavailableReasonUserAPIKeyRequired, - Models: []codersdk.ChatModel{{ + Models: []codersdk.ChatModelCatalogEntry{{ ID: fantasyopenai.Name + ":gpt-4", Provider: fantasyopenai.Name, Model: "gpt-4", @@ -722,10 +722,10 @@ func TestListConfiguredModels_PolicyAwareAvailability(t *testing.T) { fantasyanthropic.Name: {Available: true}, }, enabledProviders: enabledProviders(fantasyanthropic.Name), - want: codersdk.ChatModelsResponse{Providers: []codersdk.ChatModelProvider{{ + want: codersdk.ChatModelAvailabilityResponse{Providers: []codersdk.ChatModelProvider{{ Provider: fantasyanthropic.Name, Available: true, - Models: []codersdk.ChatModel{{ + Models: []codersdk.ChatModelCatalogEntry{{ ID: fantasyanthropic.Name + ":claude-3-5-sonnet", Provider: fantasyanthropic.Name, Model: "claude-3-5-sonnet", @@ -748,10 +748,10 @@ func TestListConfiguredModels_PolicyAwareAvailability(t *testing.T) { fantasyopenai.Name: {Available: true}, }, enabledProviders: enabledProviders(fantasyopenai.Name), - want: codersdk.ChatModelsResponse{Providers: []codersdk.ChatModelProvider{{ + want: codersdk.ChatModelAvailabilityResponse{Providers: []codersdk.ChatModelProvider{{ Provider: fantasyopenai.Name, Available: true, - Models: []codersdk.ChatModel{{ + Models: []codersdk.ChatModelCatalogEntry{{ ID: fantasyopenai.Name + ":gpt-4", Provider: fantasyopenai.Name, Model: "gpt-4", @@ -769,11 +769,11 @@ func TestListConfiguredModels_PolicyAwareAvailability(t *testing.T) { Model: "gpt-4o", }}, enabledProviders: enabledProviders(fantasyopenai.Name), - want: codersdk.ChatModelsResponse{Providers: []codersdk.ChatModelProvider{{ + want: codersdk.ChatModelAvailabilityResponse{Providers: []codersdk.ChatModelProvider{{ Provider: fantasyopenai.Name, Available: false, UnavailableReason: codersdk.ChatModelProviderUnavailableMissingAPIKey, - Models: []codersdk.ChatModel{{ + Models: []codersdk.ChatModelCatalogEntry{{ ID: fantasyopenai.Name + ":gpt-4o", Provider: fantasyopenai.Name, Model: "gpt-4o", @@ -815,7 +815,7 @@ func TestListConfiguredProviderAvailability_PolicyAwareFiltering(t *testing.T) { name string availabilityByProvider map[string]chatprovider.ProviderAvailability enabledProviders map[string]struct{} - want codersdk.ChatModelsResponse + want codersdk.ChatModelAvailabilityResponse }{ { name: "EnabledProvidersUsePolicyAvailability", @@ -827,17 +827,17 @@ func TestListConfiguredProviderAvailability_PolicyAwareFiltering(t *testing.T) { fantasyopenai.Name: {Available: true}, }, enabledProviders: enabledProviders(fantasyanthropic.Name, fantasyopenai.Name), - want: codersdk.ChatModelsResponse{Providers: []codersdk.ChatModelProvider{ + want: codersdk.ChatModelAvailabilityResponse{Providers: []codersdk.ChatModelProvider{ { Provider: fantasyanthropic.Name, Available: false, UnavailableReason: codersdk.ChatModelProviderUnavailableReasonUserAPIKeyRequired, - Models: []codersdk.ChatModel{}, + Models: []codersdk.ChatModelCatalogEntry{}, }, { Provider: fantasyopenai.Name, Available: true, - Models: []codersdk.ChatModel{}, + Models: []codersdk.ChatModelCatalogEntry{}, }, }}, }, @@ -848,20 +848,20 @@ func TestListConfiguredProviderAvailability_PolicyAwareFiltering(t *testing.T) { fantasyopenai.Name: {Available: true}, }, enabledProviders: enabledProviders(fantasyopenai.Name), - want: codersdk.ChatModelsResponse{Providers: []codersdk.ChatModelProvider{{ + want: codersdk.ChatModelAvailabilityResponse{Providers: []codersdk.ChatModelProvider{{ Provider: fantasyopenai.Name, Available: true, - Models: []codersdk.ChatModel{}, + Models: []codersdk.ChatModelCatalogEntry{}, }}}, }, { name: "MissingAvailabilityDefaultsToMissingAPIKey", enabledProviders: enabledProviders(fantasyopenai.Name), - want: codersdk.ChatModelsResponse{Providers: []codersdk.ChatModelProvider{{ + want: codersdk.ChatModelAvailabilityResponse{Providers: []codersdk.ChatModelProvider{{ Provider: fantasyopenai.Name, Available: false, UnavailableReason: codersdk.ChatModelProviderUnavailableMissingAPIKey, - Models: []codersdk.ChatModel{}, + Models: []codersdk.ChatModelCatalogEntry{}, }}}, }, } diff --git a/coderd/x/chatd/integration_test.go b/coderd/x/chatd/integration_test.go index 8fd89420dc4..2ca2538e81a 100644 --- a/coderd/x/chatd/integration_test.go +++ b/coderd/x/chatd/integration_test.go @@ -97,7 +97,7 @@ func TestAnthropicWebSearchRoundTrip(t *testing.T) { // Create a model config that enables web_search. contextLimit := int64(200000) isDefault := true - _, err := expClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + _, err := expClient.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &provider.ID, Model: "claude-sonnet-4-20250514", ContextLimit: &contextLimit, @@ -355,7 +355,7 @@ func TestOpenAIReasoningRoundTrip(t *testing.T) { contextLimit := int64(200000) isDefault := true reasoningSummary := "auto" - _, err := expClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + _, err := expClient.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &provider.ID, Model: "o4-mini", ContextLimit: &contextLimit, @@ -504,7 +504,7 @@ func TestOpenAIReasoningRoundTripStoreFalse(t *testing.T) { contextLimit := int64(200000) isDefault := true reasoningSummary := "auto" - _, err := expClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + _, err := expClient.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &provider.ID, Model: "o4-mini", ContextLimit: &contextLimit, diff --git a/codersdk/chats.go b/codersdk/chats.go index cc0c495cedd..8fd24e5fef7 100644 --- a/codersdk/chats.go +++ b/codersdk/chats.go @@ -78,8 +78,8 @@ var AllChatAttachmentMediaTypes = []ChatAttachmentMediaType{ // CompactionThresholdKey returns the user-config key for a specific // model configuration's compaction threshold. -func CompactionThresholdKey(modelConfigID uuid.UUID) string { - return ChatCompactionThresholdKeyPrefix + modelConfigID.String() +func CompactionThresholdKey(modelID uuid.UUID) string { + return ChatCompactionThresholdKeyPrefix + modelID.String() } // ChatStatus represents the status of a chat. @@ -722,8 +722,10 @@ const ( ChatModelProviderUnavailableReasonUserAPIKeyRequired ChatModelProviderUnavailableReason = "user_api_key_required" ) -// ChatModel represents a model in the chat model catalog. -type ChatModel struct { +// ChatModelCatalogEntry is a discovery catalog entry for end users. +// Its ID is a synthetic provider:model value, not the UUID in ChatModel.ID. +// It is not an admin-managed record. See ChatModel. +type ChatModelCatalogEntry struct { ID string `json:"id"` Provider string `json:"provider"` Model string `json:"model"` @@ -735,11 +737,12 @@ type ChatModelProvider struct { Provider string `json:"provider"` Available bool `json:"available"` UnavailableReason ChatModelProviderUnavailableReason `json:"unavailable_reason,omitempty"` - Models []ChatModel `json:"models"` + Models []ChatModelCatalogEntry `json:"models"` } -// ChatModelsResponse is the catalog returned from chat model discovery. -type ChatModelsResponse struct { +// ChatModelAvailabilityResponse groups the discovery catalog by provider and +// reports provider availability. +type ChatModelAvailabilityResponse struct { Providers []ChatModelProvider `json:"providers"` // UnsupportedProviders lists configured providers the Agents harness // cannot use, so the UI can explain the empty state. @@ -938,11 +941,11 @@ type AdvisorConfig struct { // MaxOutputTokens caps the advisor model response tokens. 0 means // use the runtime default. MaxOutputTokens int64 `json:"max_output_tokens"` - // ModelConfigID selects a specific chat model config to power the + // ModelConfigID selects a specific admin-managed ChatModel to power the // advisor. uuid.Nil means reuse the outer chat model. The runtime // must fall back to the outer chat model when this ID cannot be - // resolved (e.g. the referenced model config was soft-deleted or - // its provider was disabled after the admin saved this config). + // resolved, such as when the referenced ChatModel was soft-deleted or + // its provider was disabled after the admin saved this configuration. ModelConfigID uuid.UUID `json:"model_config_id" format:"uuid"` // ReasoningEffort overrides the selected advisor model's configured default. // It requires a non-zero ModelConfigID. @@ -1315,8 +1318,9 @@ type CreateUserChatProviderKeyRequest struct { APIKey string `json:"api_key"` } -// ChatModelConfig is an admin-managed model configuration. -type ChatModelConfig struct { +// ChatModel is an admin-managed model record for an organization. +// It is not a discovery catalog entry. See ChatModelCatalogEntry. +type ChatModel struct { ID uuid.UUID `json:"id" format:"uuid"` AIProviderID uuid.UUID `json:"ai_provider_id" format:"uuid"` Model string `json:"model"` @@ -1547,8 +1551,13 @@ func (c *ChatModelCallConfig) UnmarshalStrict(data []byte) error { return nil } -// CreateChatModelConfigRequest creates a chat model config. -type CreateChatModelConfigRequest struct { +// CreateChatModelRequest is the request body for an organization-scoped +// ChatModel. AIProviderID, Model, and a positive ContextLimit are required. +// Enabled defaults to true. IsDefault defaults to false when the organization +// already has a default model. The first model created in an organization is +// automatically promoted to default. CompressionThreshold defaults to 70. An +// omitted ModelConfig uses the provider defaults. +type CreateChatModelRequest struct { AIProviderID *uuid.UUID `json:"ai_provider_id,omitempty" format:"uuid"` Model string `json:"model"` DisplayName string `json:"display_name,omitempty"` @@ -1559,8 +1568,10 @@ type CreateChatModelConfigRequest struct { ModelConfig *ChatModelCallConfig `json:"model_config,omitempty"` } -// UpdateChatModelConfigRequest updates a chat model config. -type UpdateChatModelConfigRequest struct { +// UpdateChatModelRequest updates a ChatModel. Empty Model and DisplayName +// values preserve the stored values. Nil pointer fields preserve their stored +// values. This request cannot clear DisplayName. +type UpdateChatModelRequest struct { AIProviderID *uuid.UUID `json:"ai_provider_id,omitempty" format:"uuid"` Model string `json:"model,omitempty"` DisplayName string `json:"display_name,omitempty"` @@ -2019,18 +2030,18 @@ func (c *ExperimentalClient) ListChats(ctx context.Context, opts *ListChatsOptio return chats, ReadBodyAsJSON(res, &chats) } -// ListChatModels returns the available chat model catalog. -func (c *ExperimentalClient) ListChatModels(ctx context.Context) (ChatModelsResponse, error) { +// ChatModelAvailability returns the discovery catalog and provider availability. +func (c *ExperimentalClient) ChatModelAvailability(ctx context.Context) (ChatModelAvailabilityResponse, error) { res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/models", nil) if err != nil { - return ChatModelsResponse{}, err + return ChatModelAvailabilityResponse{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return ChatModelsResponse{}, ReadBodyAsError(res) + return ChatModelAvailabilityResponse{}, ReadBodyAsError(res) } - var catalog ChatModelsResponse + var catalog ChatModelAvailabilityResponse return catalog, ReadBodyAsJSON(res, &catalog) } @@ -2178,8 +2189,8 @@ func (c *ExperimentalClient) DeleteUserChatProviderKey(ctx context.Context, prov return nil } -// ListChatModelConfigs returns admin-managed chat model configs. -func (c *ExperimentalClient) ListChatModelConfigs(ctx context.Context) ([]ChatModelConfig, error) { +// ChatModels returns admin-managed chat model records. +func (c *ExperimentalClient) ChatModels(ctx context.Context) ([]ChatModel, error) { res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/model-configs", nil) if err != nil { return nil, err @@ -2189,43 +2200,43 @@ func (c *ExperimentalClient) ListChatModelConfigs(ctx context.Context) ([]ChatMo return nil, ReadBodyAsError(res) } - var configs []ChatModelConfig - return configs, ReadBodyAsJSON(res, &configs) + var models []ChatModel + return models, ReadBodyAsJSON(res, &models) } -// CreateChatModelConfig creates an admin-managed chat model config. -func (c *ExperimentalClient) CreateChatModelConfig(ctx context.Context, req CreateChatModelConfigRequest) (ChatModelConfig, error) { +// CreateChatModel creates an admin-managed ChatModel. +func (c *ExperimentalClient) CreateChatModel(ctx context.Context, req CreateChatModelRequest) (ChatModel, error) { res, err := c.Request(ctx, http.MethodPost, "/api/experimental/chats/model-configs", req) if err != nil { - return ChatModelConfig{}, err + return ChatModel{}, err } defer res.Body.Close() if res.StatusCode != http.StatusCreated { - return ChatModelConfig{}, ReadBodyAsError(res) + return ChatModel{}, ReadBodyAsError(res) } - var config ChatModelConfig - return config, ReadBodyAsJSON(res, &config) + var model ChatModel + return model, ReadBodyAsJSON(res, &model) } -// UpdateChatModelConfig updates an admin-managed chat model config. -func (c *ExperimentalClient) UpdateChatModelConfig(ctx context.Context, modelConfigID uuid.UUID, req UpdateChatModelConfigRequest) (ChatModelConfig, error) { - res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/experimental/chats/model-configs/%s", modelConfigID), req) +// UpdateChatModel updates an admin-managed ChatModel. +func (c *ExperimentalClient) UpdateChatModel(ctx context.Context, modelID uuid.UUID, req UpdateChatModelRequest) (ChatModel, error) { + res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/experimental/chats/model-configs/%s", modelID), req) if err != nil { - return ChatModelConfig{}, err + return ChatModel{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return ChatModelConfig{}, ReadBodyAsError(res) + return ChatModel{}, ReadBodyAsError(res) } - var config ChatModelConfig - return config, ReadBodyAsJSON(res, &config) + var model ChatModel + return model, ReadBodyAsJSON(res, &model) } -// DeleteChatModelConfig deletes an admin-managed chat model config. -func (c *ExperimentalClient) DeleteChatModelConfig(ctx context.Context, modelConfigID uuid.UUID) error { - res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/experimental/chats/model-configs/%s", modelConfigID), nil) +// DeleteChatModel deletes an admin-managed ChatModel. +func (c *ExperimentalClient) DeleteChatModel(ctx context.Context, modelID uuid.UUID) error { + res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/experimental/chats/model-configs/%s", modelID), nil) if err != nil { return err } @@ -2613,8 +2624,8 @@ func (c *ExperimentalClient) GetUserChatCompactionThresholds(ctx context.Context // UpdateUserChatCompactionThreshold updates the user's per-model chat // compaction threshold. -func (c *ExperimentalClient) UpdateUserChatCompactionThreshold(ctx context.Context, modelConfigID uuid.UUID, req UpdateUserChatCompactionThresholdRequest) (UserChatCompactionThreshold, error) { - res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/experimental/chats/config/user-compaction-thresholds/%s", modelConfigID), req) +func (c *ExperimentalClient) UpdateUserChatCompactionThreshold(ctx context.Context, modelID uuid.UUID, req UpdateUserChatCompactionThresholdRequest) (UserChatCompactionThreshold, error) { + res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/experimental/chats/config/user-compaction-thresholds/%s", modelID), req) if err != nil { return UserChatCompactionThreshold{}, err } @@ -2628,8 +2639,8 @@ func (c *ExperimentalClient) UpdateUserChatCompactionThreshold(ctx context.Conte // DeleteUserChatCompactionThreshold deletes the user's per-model chat // compaction threshold override. -func (c *ExperimentalClient) DeleteUserChatCompactionThreshold(ctx context.Context, modelConfigID uuid.UUID) error { - res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/experimental/chats/config/user-compaction-thresholds/%s", modelConfigID), nil) +func (c *ExperimentalClient) DeleteUserChatCompactionThreshold(ctx context.Context, modelID uuid.UUID) error { + res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/experimental/chats/config/user-compaction-thresholds/%s", modelID), nil) if err != nil { return err } diff --git a/codersdk/toolsdk/chats.go b/codersdk/toolsdk/chats.go index 947076570f3..8cfc2b0b1c4 100644 --- a/codersdk/toolsdk/chats.go +++ b/codersdk/toolsdk/chats.go @@ -883,7 +883,7 @@ Per-user provider credentials are validated when creating a chat, so coder_creat }, MCPAnnotations: mcpReadOnlyAnnotations, Handler: func(ctx context.Context, deps Deps, _ NoArgs) (ListChatModelConfigsResponse, error) { - configs, err := codersdk.NewExperimentalClient(deps.coderClient).ListChatModelConfigs(ctx) + configs, err := codersdk.NewExperimentalClient(deps.coderClient).ChatModels(ctx) if err != nil { return ListChatModelConfigsResponse{}, xerrors.Errorf("list chat model configs: %w", err) } diff --git a/codersdk/toolsdk/chats_test.go b/codersdk/toolsdk/chats_test.go index c4dbd0c5a98..c40f0087e29 100644 --- a/codersdk/toolsdk/chats_test.go +++ b/codersdk/toolsdk/chats_test.go @@ -83,7 +83,7 @@ func TestChatTools(t *testing.T) { }) firstUser := coderdtest.CreateFirstUser(t, client) expClient := codersdk.NewExperimentalClient(client) - defaultModelConfig := coderdtest.CreateOpenAICompatChatModelConfig(t, expClient, "") + defaultModelConfig := coderdtest.CreateOpenAICompatChatModel(t, expClient, "") aibridgedtest.StartTestAIBridgeDaemon(t.Context(), t, api, nil) tb, err := toolsdk.NewDeps(client) @@ -464,7 +464,7 @@ func TestChatTools(t *testing.T) { } return chattest.OpenAINonStreamingResponse(`{"title": "Await Test"}`) }) - blockingModel := coderdtest.CreateOpenAICompatChatModelConfig(t, expClient, blockingURL) + blockingModel := coderdtest.CreateOpenAICompatChatModel(t, expClient, blockingURL) awaitFile, err := expClient.UploadChatFile(ctx, firstUser.OrganizationID, "text/plain", "await.txt", bytes.NewReader([]byte("await evidence"))) require.NoError(t, err) running, err := expClient.CreateChat(ctx, codersdk.CreateChatRequest{ @@ -544,7 +544,7 @@ func TestChatTools(t *testing.T) { } return chattest.OpenAINonStreamingResponse(`{"title": "Shared Await Test"}`) }) - sharedBlockingModel := coderdtest.CreateOpenAICompatChatModelConfig(t, expClient, sharedBlockingURL) + sharedBlockingModel := coderdtest.CreateOpenAICompatChatModel(t, expClient, sharedBlockingURL) sharedRunning, err := expClient.CreateChat(ctx, codersdk.CreateChatRequest{ OrganizationID: firstUser.OrganizationID, Content: []codersdk.ChatInputPart{{Type: codersdk.ChatInputPartTypeText, Text: "Wait for shared release."}}, @@ -621,7 +621,7 @@ func TestChatTools(t *testing.T) { } return chattest.OpenAINonStreamingResponse(`{"title": "Await Timeout Test"}`) }) - timeoutModel := coderdtest.CreateOpenAICompatChatModelConfig(t, expClient, timeoutURL) + timeoutModel := coderdtest.CreateOpenAICompatChatModel(t, expClient, timeoutURL) busy, err := expClient.CreateChat(ctx, codersdk.CreateChatRequest{ OrganizationID: firstUser.OrganizationID, Content: []codersdk.ChatInputPart{{Type: codersdk.ChatInputPartTypeText, Text: "Stay busy."}}, @@ -664,7 +664,7 @@ func TestChatTools(t *testing.T) { t.Run("ListChatModelConfigsSkipsDisabledProviders", func(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) - disabledProviderConfig := coderdtest.CreateOpenAICompatChatModelConfig(t, expClient, chattest.OpenAI(t)) + disabledProviderConfig := coderdtest.CreateOpenAICompatChatModel(t, expClient, chattest.OpenAI(t)) provider, err := client.UpdateAIProvider(ctx, disabledProviderConfig.AIProviderID.String(), codersdk.UpdateAIProviderRequest{ Enabled: ptr.Ref(false), }) @@ -684,7 +684,7 @@ func TestChatTools(t *testing.T) { t.Run("ListChatModelConfigsSkipsDeletedProviders", func(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) - deletedProviderConfig := coderdtest.CreateOpenAICompatChatModelConfig(t, expClient, chattest.OpenAI(t)) + deletedProviderConfig := coderdtest.CreateOpenAICompatChatModel(t, expClient, chattest.OpenAI(t)) err := client.DeleteAIProvider(ctx, deletedProviderConfig.AIProviderID.String()) require.NoError(t, err) @@ -713,7 +713,7 @@ func TestChatTools(t *testing.T) { } return chattest.OpenAINonStreamingResponse(`{"title": "Interrupt Test"}`) }) - blockingModelConfig := coderdtest.CreateOpenAICompatChatModelConfig(t, expClient, blockingURL) + blockingModelConfig := coderdtest.CreateOpenAICompatChatModel(t, expClient, blockingURL) created, err := testTool(t, toolsdk.CreateChat, tb, toolsdk.CreateChatArgs{ Prompt: "Block forever.", diff --git a/docs/reference/api/chats.md b/docs/reference/api/chats.md index db24aa4c3e0..a47211652f3 100644 --- a/docs/reference/api/chats.md +++ b/docs/reference/api/chats.md @@ -643,9 +643,9 @@ Experimental: this endpoint is subject to change. ### Responses -| Status | Meaning | Description | Schema | -|--------|---------------------------------------------------------|-------------|----------------------------------------------------------------------| -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.ChatModelsResponse](schemas.md#codersdkchatmodelsresponse) | +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|--------------------------------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.ChatModelAvailabilityResponse](schemas.md#codersdkchatmodelavailabilityresponse) | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index d9bb0184714..4bd8af6b070 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -3393,7 +3393,42 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `messages` | array of [codersdk.ChatMessage](#codersdkchatmessage) | false | | | | `queued_messages` | array of [codersdk.ChatQueuedMessage](#codersdkchatqueuedmessage) | false | | | -## codersdk.ChatModel +## codersdk.ChatModelAvailabilityResponse + +```json +{ + "providers": [ + { + "available": true, + "models": [ + { + "display_name": "string", + "id": "string", + "model": "string", + "provider": "string" + } + ], + "provider": "string", + "unavailable_reason": "missing_api_key" + } + ], + "unsupported_providers": [ + { + "display_name": "string", + "provider": "string" + } + ] +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|-------------------------|-------------------------------------------------------------------------------|----------|--------------|------------------------------------------------------------------------------------------------------------------------| +| `providers` | array of [codersdk.ChatModelProvider](#codersdkchatmodelprovider) | false | | | +| `unsupported_providers` | array of [codersdk.ChatUnsupportedProvider](#codersdkchatunsupportedprovider) | false | | Unsupported providers lists configured providers the Agents harness cannot use, so the UI can explain the empty state. | + +## codersdk.ChatModelCatalogEntry ```json { @@ -3436,7 +3471,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | Name | Type | Required | Restrictions | Description | |----------------------|--------------------------------------------------------------------------------------------|----------|--------------|-------------| | `available` | boolean | false | | | -| `models` | array of [codersdk.ChatModel](#codersdkchatmodel) | false | | | +| `models` | array of [codersdk.ChatModelCatalogEntry](#codersdkchatmodelcatalogentry) | false | | | | `provider` | string | false | | | | `unavailable_reason` | [codersdk.ChatModelProviderUnavailableReason](#codersdkchatmodelproviderunavailablereason) | false | | | @@ -3454,41 +3489,6 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in |------------------------------------------------------------| | `fetch_failed`, `missing_api_key`, `user_api_key_required` | -## codersdk.ChatModelsResponse - -```json -{ - "providers": [ - { - "available": true, - "models": [ - { - "display_name": "string", - "id": "string", - "model": "string", - "provider": "string" - } - ], - "provider": "string", - "unavailable_reason": "missing_api_key" - } - ], - "unsupported_providers": [ - { - "display_name": "string", - "provider": "string" - } - ] -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -|-------------------------|-------------------------------------------------------------------------------|----------|--------------|------------------------------------------------------------------------------------------------------------------------| -| `providers` | array of [codersdk.ChatModelProvider](#codersdkchatmodelprovider) | false | | | -| `unsupported_providers` | array of [codersdk.ChatUnsupportedProvider](#codersdkchatunsupportedprovider) | false | | Unsupported providers lists configured providers the Agents harness cannot use, so the UI can explain the empty state. | - ## codersdk.ChatPlanMode ```json diff --git a/enterprise/coderd/exp_chats_test.go b/enterprise/coderd/exp_chats_test.go index 5f6c3ecc3df..e5ea072f272 100644 --- a/enterprise/coderd/exp_chats_test.go +++ b/enterprise/coderd/exp_chats_test.go @@ -46,16 +46,16 @@ func createOpenAIProviderForTest( return provider } -func createOpenAIModelConfigForTest( +func createOpenAIChatModelForTest( ctx context.Context, t testing.TB, client *codersdk.ExperimentalClient, apiKey string, baseURL string, -) codersdk.ChatModelConfig { +) codersdk.ChatModel { t.Helper() provider := createOpenAIProviderForTest(ctx, t, client, apiKey, baseURL) - model, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + model, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &provider.ID, Model: "gpt-4", DisplayName: "GPT-4", @@ -127,7 +127,7 @@ func TestChatStreamRelay(t *testing.T) { }) expClient := codersdk.NewExperimentalClient(firstClient) - model := createOpenAIModelConfigForTest(ctx, t, expClient, "test", openai) + model := createOpenAIChatModelForTest(ctx, t, expClient, "test", openai) // Create a chat on the first replica chat, err := expClient.CreateChat(ctx, codersdk.CreateChatRequest{ @@ -305,7 +305,7 @@ func TestChatStreamRelay(t *testing.T) { }) expClient := codersdk.NewExperimentalClient(firstClient) - model := createOpenAIModelConfigForTest(ctx, t, expClient, "test", openai) + model := createOpenAIChatModelForTest(ctx, t, expClient, "test", openai) // Create a chat on the first replica. chat, err := expClient.CreateChat(ctx, codersdk.CreateChatRequest{ @@ -465,7 +465,7 @@ func TestChatStreamRelay(t *testing.T) { }) expClient := codersdk.NewExperimentalClient(firstClient) - model := createOpenAIModelConfigForTest(ctx, t, expClient, "test", openai) + model := createOpenAIChatModelForTest(ctx, t, expClient, "test", openai) chat, err := expClient.CreateChat(ctx, codersdk.CreateChatRequest{ OrganizationID: firstUser.OrganizationID, @@ -624,7 +624,7 @@ func TestChatStreamRelay(t *testing.T) { }) expClient := codersdk.NewExperimentalClient(firstClient) - model := createOpenAIModelConfigForTest(ctx, t, expClient, "test", openai) + model := createOpenAIChatModelForTest(ctx, t, expClient, "test", openai) chat, err := expClient.CreateChat(ctx, codersdk.CreateChatRequest{ OrganizationID: firstUser.OrganizationID, @@ -760,7 +760,7 @@ func TestChatStreamRelay(t *testing.T) { }) expClient := codersdk.NewExperimentalClient(firstClient) - model := createOpenAIModelConfigForTest(ctx, t, expClient, "test", openai) + model := createOpenAIChatModelForTest(ctx, t, expClient, "test", openai) // Create a chat on the first replica. chat, err := expClient.CreateChat(ctx, codersdk.CreateChatRequest{ @@ -937,7 +937,7 @@ func replicaIDForClientURL( return uuid.Nil } -func TestChatModelConfigDefault(t *testing.T) { +func TestChatModelDefault(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) @@ -951,9 +951,9 @@ func TestChatModelConfigDefault(t *testing.T) { trueValue := true falseValue := false - firstModel, err := expClient.CreateChatModelConfig( + firstModel, err := expClient.CreateChatModel( ctx, - codersdk.CreateChatModelConfigRequest{ + codersdk.CreateChatModelRequest{ AIProviderID: &provider.ID, Model: "gpt-5-a", DisplayName: "GPT 5 A", @@ -965,9 +965,9 @@ func TestChatModelConfigDefault(t *testing.T) { require.NoError(t, err) require.True(t, firstModel.IsDefault) - secondModel, err := expClient.CreateChatModelConfig( + secondModel, err := expClient.CreateChatModel( ctx, - codersdk.CreateChatModelConfigRequest{ + codersdk.CreateChatModelRequest{ AIProviderID: &provider.ID, Model: "gpt-5-b", DisplayName: "GPT 5 B", @@ -979,63 +979,63 @@ func TestChatModelConfigDefault(t *testing.T) { require.NoError(t, err) require.True(t, secondModel.IsDefault) - modelConfigs, err := expClient.ListChatModelConfigs(ctx) + models, err := expClient.ChatModels(ctx) require.NoError(t, err) - firstStored := findChatModelConfigByID(t, modelConfigs, firstModel.ID) - secondStored := findChatModelConfigByID(t, modelConfigs, secondModel.ID) + firstStored := findChatModelByID(t, models, firstModel.ID) + secondStored := findChatModelByID(t, models, secondModel.ID) require.False(t, firstStored.IsDefault) require.True(t, secondStored.IsDefault) - updatedFirst, err := expClient.UpdateChatModelConfig( + updatedFirst, err := expClient.UpdateChatModel( ctx, firstModel.ID, - codersdk.UpdateChatModelConfigRequest{ + codersdk.UpdateChatModelRequest{ IsDefault: &trueValue, }, ) require.NoError(t, err) require.True(t, updatedFirst.IsDefault) - modelConfigs, err = expClient.ListChatModelConfigs(ctx) + models, err = expClient.ChatModels(ctx) require.NoError(t, err) - firstStored = findChatModelConfigByID(t, modelConfigs, firstModel.ID) - secondStored = findChatModelConfigByID(t, modelConfigs, secondModel.ID) + firstStored = findChatModelByID(t, models, firstModel.ID) + secondStored = findChatModelByID(t, models, secondModel.ID) require.True(t, firstStored.IsDefault) require.False(t, secondStored.IsDefault) - updatedFirst, err = expClient.UpdateChatModelConfig( + updatedFirst, err = expClient.UpdateChatModel( ctx, firstModel.ID, - codersdk.UpdateChatModelConfigRequest{ + codersdk.UpdateChatModelRequest{ IsDefault: &falseValue, }, ) require.NoError(t, err) require.False(t, updatedFirst.IsDefault) - modelConfigs, err = expClient.ListChatModelConfigs(ctx) + models, err = expClient.ChatModels(ctx) require.NoError(t, err) - firstStored = findChatModelConfigByID(t, modelConfigs, firstModel.ID) - secondStored = findChatModelConfigByID(t, modelConfigs, secondModel.ID) + firstStored = findChatModelByID(t, models, firstModel.ID) + secondStored = findChatModelByID(t, models, secondModel.ID) require.False(t, firstStored.IsDefault) require.True(t, secondStored.IsDefault) } -func findChatModelConfigByID( +func findChatModelByID( t *testing.T, - modelConfigs []codersdk.ChatModelConfig, + models []codersdk.ChatModel, id uuid.UUID, -) codersdk.ChatModelConfig { +) codersdk.ChatModel { t.Helper() - for _, modelConfig := range modelConfigs { - if modelConfig.ID == id { - return modelConfig + for _, model := range models { + if model.ID == id { + return model } } - require.FailNowf(t, "missing model config", "model config %s not found", id) - return codersdk.ChatModelConfig{} + require.FailNowf(t, "missing chat model", "chat model %s not found", id) + return codersdk.ChatModel{} } // cookieOnlySessionTokenProvider authenticates HTTP requests via the @@ -1096,7 +1096,7 @@ func TestCreateChatUsesOrganizationLocalModel(t *testing.T) { expClient := codersdk.NewExperimentalClient(client) provider := createOpenAIProviderForTest(ctx, t, expClient, "test-key", "https://example.com") - defaultModel, err := expClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + defaultModel, err := expClient.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &provider.ID, Model: "gpt-4o-mini", DisplayName: "Default Organization Model", @@ -1160,7 +1160,7 @@ func TestListChats_OrgAdminOnlySeesOwnChats(t *testing.T) { expClient := codersdk.NewExperimentalClient(client) provider := createOpenAIProviderForTest(ctx, t, expClient, "test-key", "https://example.com") - _, err := expClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + _, err := expClient.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &provider.ID, Model: "gpt-4o-mini", DisplayName: "Test Model", diff --git a/scaletest/chat/client.go b/scaletest/chat/client.go index bb2ad29c746..4649f16b6e6 100644 --- a/scaletest/chat/client.go +++ b/scaletest/chat/client.go @@ -21,9 +21,9 @@ type chatClient interface { var _ chatClient = (*codersdk.ExperimentalClient)(nil) -type chatModelConfigClient interface { - ListChatModelConfigs(ctx context.Context) ([]codersdk.ChatModelConfig, error) - CreateChatModelConfig(ctx context.Context, req codersdk.CreateChatModelConfigRequest) (codersdk.ChatModelConfig, error) +type chatModelClient interface { + ChatModels(ctx context.Context) ([]codersdk.ChatModel, error) + CreateChatModel(ctx context.Context, req codersdk.CreateChatModelRequest) (codersdk.ChatModel, error) } -var _ chatModelConfigClient = (*codersdk.ExperimentalClient)(nil) +var _ chatModelClient = (*codersdk.ExperimentalClient)(nil) diff --git a/scaletest/chat/provider.go b/scaletest/chat/provider.go index 79baf1d4a2e..c0faf98a9f7 100644 --- a/scaletest/chat/provider.go +++ b/scaletest/chat/provider.go @@ -38,11 +38,10 @@ const ( scaletestAIProviderActionReused scaletestAIProviderAction = "reused" ) -// EnsureScaletestModelConfig bootstraps the shared AI provider and model -// config used by chat scaletests. When the provider was created or updated, -// it sleeps for propagationWait so every coderd replica's cached provider -// config expires before chats start. -func EnsureScaletestModelConfig(ctx context.Context, client *codersdk.Client, logger slog.Logger, llmMockURL string, propagationWait time.Duration) (uuid.UUID, error) { +// EnsureScaletestChatModel bootstraps the shared AI provider and ChatModel +// used by chat scaletests. When the provider was created or updated, it waits +// for propagationWait so every coderd replica's cached provider expires. +func EnsureScaletestChatModel(ctx context.Context, client *codersdk.Client, logger slog.Logger, llmMockURL string, propagationWait time.Duration) (uuid.UUID, error) { expClient := codersdk.NewExperimentalClient(client) logger.Info(ctx, "bootstrapping mock LLM provider", slog.F("llm_mock_url", llmMockURL)) @@ -72,7 +71,7 @@ func EnsureScaletestModelConfig(ctx context.Context, client *codersdk.Client, lo ) } - modelConfigID, err := ensureScaletestChatModelConfig(ctx, expClient, logger, provider) + modelID, err := ensureScaletestChatModelRecord(ctx, expClient, logger, provider) if err != nil { return uuid.Nil, err } @@ -89,33 +88,33 @@ func EnsureScaletestModelConfig(ctx context.Context, client *codersdk.Client, lo } } - return modelConfigID, nil + return modelID, nil } -func ensureScaletestChatModelConfig(ctx context.Context, client chatModelConfigClient, logger slog.Logger, provider codersdk.AIProvider) (uuid.UUID, error) { - modelConfigs, err := client.ListChatModelConfigs(ctx) +func ensureScaletestChatModelRecord(ctx context.Context, client chatModelClient, logger slog.Logger, provider codersdk.AIProvider) (uuid.UUID, error) { + models, err := client.ChatModels(ctx) if err != nil { - return uuid.Nil, xerrors.Errorf("list chat model configs: %w", err) + return uuid.Nil, xerrors.Errorf("list chat models: %w", err) } - for i := range modelConfigs { - matchesProvider := modelConfigs[i].AIProviderID == provider.ID - matchesModel := modelConfigs[i].Model == scaletestModelName + for i := range models { + matchesProvider := models[i].AIProviderID == provider.ID + matchesModel := models[i].Model == scaletestModelName if !matchesProvider || !matchesModel { continue } - if !modelConfigs[i].Enabled { - return uuid.Nil, xerrors.Errorf("existing scaletest chat model config %s is disabled; re-enable or delete it before running scaletests", modelConfigs[i].ID) + if !models[i].Enabled { + return uuid.Nil, xerrors.Errorf("existing scaletest ChatModel %s is disabled; re-enable or delete it before running scaletests", models[i].ID) } - modelConfigID := modelConfigs[i].ID - logger.Info(ctx, "reusing scaletest model config", slog.F("model_config_id", modelConfigID)) - return modelConfigID, nil + modelID := models[i].ID + logger.Info(ctx, "reusing scaletest ChatModel", slog.F("model_id", modelID)) + return modelID, nil } enabled := true isDefault := false contextLimit := scaletestModelContextLimit - created, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + created, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{ AIProviderID: &provider.ID, Model: scaletestModelName, DisplayName: scaletestModelDisplayName, @@ -124,9 +123,9 @@ func ensureScaletestChatModelConfig(ctx context.Context, client chatModelConfigC ContextLimit: &contextLimit, }) if err != nil { - return uuid.Nil, xerrors.Errorf("create scaletest chat model config: %w", err) + return uuid.Nil, xerrors.Errorf("create scaletest ChatModel: %w", err) } - logger.Info(ctx, "created scaletest model config", slog.F("model_config_id", created.ID)) + logger.Info(ctx, "created scaletest ChatModel", slog.F("model_id", created.ID)) return created.ID, nil } diff --git a/site/src/api/api.test.ts b/site/src/api/api.test.ts index f832533a53b..63281526d7e 100644 --- a/site/src/api/api.test.ts +++ b/site/src/api/api.test.ts @@ -435,14 +435,14 @@ describe("api.ts", () => { it.each<[string, () => Promise, unknown]>([ [ "/api/experimental/chats/models", - () => API.experimental.getChatModels(), + () => API.experimental.getChatModelAvailability(), { providers: [], }, ], [ "/api/experimental/chats/model-configs", - () => API.experimental.getChatModelConfigs(), + () => API.experimental.getChatModels(), [], ], ])("returns response data for %s", async (path, request, responseData) => { @@ -459,11 +459,11 @@ describe("api.ts", () => { it.each<[string, () => Promise]>([ [ "/api/experimental/chats/models", - () => API.experimental.getChatModels(), + () => API.experimental.getChatModelAvailability(), ], [ "/api/experimental/chats/model-configs", - () => API.experimental.getChatModelConfigs(), + () => API.experimental.getChatModels(), ], ])("rethrows axios errors for %s", async (path, request) => { const expectedError = new Error("request failed"); diff --git a/site/src/api/api.ts b/site/src/api/api.ts index b9dfee28f8c..1ff8068467e 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -352,7 +352,7 @@ const aiSpendBatchSize = 100; const aiProviderConfigsPath = "/api/v2/ai/providers"; const aiGatewayPath = "/api/v2/ai-gateway"; -const chatModelConfigsPath = "/api/experimental/chats/model-configs"; +const chatModelsPath = "/api/experimental/chats/model-configs"; const userSkillsPath = (user: string) => `/api/experimental/users/${encodeURIComponent(user)}/skills`; const userSkillPath = (user: string, name: string) => @@ -3490,12 +3490,14 @@ class ExperimentalApiMethods { return response.data; }; - getChatModels = async (): Promise => { - const response = await this.axios.get( - "/api/experimental/chats/models", - ); - return response.data; - }; + getChatModelAvailability = + async (): Promise => { + const response = + await this.axios.get( + "/api/experimental/chats/models", + ); + return response.data; + }; getAIModelPrices = async (filter: { provider?: string; @@ -3875,54 +3877,51 @@ class ExperimentalApiMethods { return response.data; }; updateUserChatCompactionThreshold = async ( - modelConfigId: string, + modelId: string, req: TypesGen.UpdateUserChatCompactionThresholdRequest, ): Promise => { const response = await this.axios.put( - `/api/experimental/chats/config/user-compaction-thresholds/${encodeURIComponent(modelConfigId)}`, + `/api/experimental/chats/config/user-compaction-thresholds/${encodeURIComponent(modelId)}`, req, ); return response.data; }; deleteUserChatCompactionThreshold = async ( - modelConfigId: string, + modelId: string, ): Promise => { await this.axios.delete( - `/api/experimental/chats/config/user-compaction-thresholds/${encodeURIComponent(modelConfigId)}`, + `/api/experimental/chats/config/user-compaction-thresholds/${encodeURIComponent(modelId)}`, ); }; - getChatModelConfigs = async (): Promise => { - const response = - await this.axios.get(chatModelConfigsPath); + getChatModels = async (): Promise => { + const response = await this.axios.get(chatModelsPath); return response.data; }; - createChatModelConfig = async ( - req: TypesGen.CreateChatModelConfigRequest, - ): Promise => { - const response = await this.axios.post( - chatModelConfigsPath, + createChatModel = async ( + req: TypesGen.CreateChatModelRequest, + ): Promise => { + const response = await this.axios.post( + chatModelsPath, req, ); return response.data; }; - updateChatModelConfig = async ( - modelConfigId: string, - req: TypesGen.UpdateChatModelConfigRequest, - ): Promise => { - const response = await this.axios.patch( - `${chatModelConfigsPath}/${encodeURIComponent(modelConfigId)}`, + updateChatModel = async ( + modelId: string, + req: TypesGen.UpdateChatModelRequest, + ): Promise => { + const response = await this.axios.patch( + `${chatModelsPath}/${encodeURIComponent(modelId)}`, req, ); return response.data; }; - deleteChatModelConfig = async (modelConfigId: string): Promise => { - await this.axios.delete( - `${chatModelConfigsPath}/${encodeURIComponent(modelConfigId)}`, - ); + deleteChatModel = async (modelId: string): Promise => { + await this.axios.delete(`${chatModelsPath}/${encodeURIComponent(modelId)}`); }; getMCPServerConfigs = async ( diff --git a/site/src/api/queries/chats.ts b/site/src/api/queries/chats.ts index 66decae1523..29e7ec054d7 100644 --- a/site/src/api/queries/chats.ts +++ b/site/src/api/queries/chats.ts @@ -2142,13 +2142,10 @@ export const userCompactionThresholds = () => ({ export const updateUserCompactionThreshold = (queryClient: QueryClient) => ({ mutationFn: (vars: { - modelConfigId: string; + modelId: string; req: TypesGen.UpdateUserChatCompactionThresholdRequest; }) => - API.experimental.updateUserChatCompactionThreshold( - vars.modelConfigId, - vars.req, - ), + API.experimental.updateUserChatCompactionThreshold(vars.modelId, vars.req), onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: userCompactionThresholdsKey, @@ -2157,8 +2154,8 @@ export const updateUserCompactionThreshold = (queryClient: QueryClient) => ({ }); export const deleteUserCompactionThreshold = (queryClient: QueryClient) => ({ - mutationFn: (modelConfigId: string) => - API.experimental.deleteUserChatCompactionThreshold(modelConfigId), + mutationFn: (modelId: string) => + API.experimental.deleteUserChatCompactionThreshold(modelId), onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: userCompactionThresholdsKey, @@ -2166,24 +2163,28 @@ export const deleteUserCompactionThreshold = (queryClient: QueryClient) => ({ }, }); -export const chatModelsKey = [...chatConfigKey, "models", "catalog"] as const; +export const chatModelAvailabilityKey = [ + ...chatConfigKey, + "models", + "catalog", +] as const; -export const chatModels = () => ({ - queryKey: chatModelsKey, - queryFn: (): Promise => - API.experimental.getChatModels(), +export const chatModelAvailability = () => ({ + queryKey: chatModelAvailabilityKey, + queryFn: (): Promise => + API.experimental.getChatModelAvailability(), }); -export const chatModelConfigsKey = [ +export const chatModelsKey = [ ...chatConfigKey, "models", "definitions", ] as const; -export const chatModelConfigs = () => ({ - queryKey: chatModelConfigsKey, - queryFn: (): Promise => - API.experimental.getChatModelConfigs(), +export const chatModels = () => ({ + queryKey: chatModelsKey, + queryFn: (): Promise => + API.experimental.getChatModels(), }); export const userChatProviderConfigsKey = [ @@ -2222,7 +2223,7 @@ export const upsertUserChatProviderKey = (queryClient: QueryClient) => ({ queryClient.invalidateQueries({ queryKey: userChatProviderConfigsKey, }), - queryClient.invalidateQueries({ queryKey: chatModelsKey }), + queryClient.invalidateQueries({ queryKey: chatModelAvailabilityKey }), ]); }, }); @@ -2235,15 +2236,15 @@ export const deleteUserChatProviderKey = (queryClient: QueryClient) => ({ queryClient.invalidateQueries({ queryKey: userChatProviderConfigsKey, }), - queryClient.invalidateQueries({ queryKey: chatModelsKey }), + queryClient.invalidateQueries({ queryKey: chatModelAvailabilityKey }), ]); }, }); const invalidateChatConfigurationQueries = async (queryClient: QueryClient) => { await Promise.all([ - queryClient.invalidateQueries({ queryKey: chatModelConfigsKey }), queryClient.invalidateQueries({ queryKey: chatModelsKey }), + queryClient.invalidateQueries({ queryKey: chatModelAvailabilityKey }), ]); }; @@ -2257,30 +2258,29 @@ export const invalidateChatProviderDependentQueries = async ( ]); }; -export const createChatModelConfig = (queryClient: QueryClient) => ({ - mutationFn: (req: TypesGen.CreateChatModelConfigRequest) => - API.experimental.createChatModelConfig(req), +export const createChatModel = (queryClient: QueryClient) => ({ + mutationFn: (req: TypesGen.CreateChatModelRequest) => + API.experimental.createChatModel(req), onSuccess: async () => { await invalidateChatConfigurationQueries(queryClient); }, }); -type UpdateChatModelConfigMutationArgs = { - modelConfigId: string; - req: TypesGen.UpdateChatModelConfigRequest; +type UpdateChatModelMutationArgs = { + modelId: string; + req: TypesGen.UpdateChatModelRequest; }; -export const updateChatModelConfig = (queryClient: QueryClient) => ({ - mutationFn: ({ modelConfigId, req }: UpdateChatModelConfigMutationArgs) => - API.experimental.updateChatModelConfig(modelConfigId, req), +export const updateChatModel = (queryClient: QueryClient) => ({ + mutationFn: ({ modelId, req }: UpdateChatModelMutationArgs) => + API.experimental.updateChatModel(modelId, req), onSuccess: async () => { await invalidateChatConfigurationQueries(queryClient); }, }); -export const deleteChatModelConfig = (queryClient: QueryClient) => ({ - mutationFn: (modelConfigId: string) => - API.experimental.deleteChatModelConfig(modelConfigId), +export const deleteChatModel = (queryClient: QueryClient) => ({ + mutationFn: (modelId: string) => API.experimental.deleteChatModel(modelId), onSuccess: async () => { await invalidateChatConfigurationQueries(queryClient); }, diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index dba610ef882..6e797f677c4 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1214,11 +1214,11 @@ export interface AdvisorConfig { */ readonly max_output_tokens: number; /** - * ModelConfigID selects a specific chat model config to power the + * ModelConfigID selects a specific admin-managed ChatModel to power the * advisor. uuid.Nil means reuse the outer chat model. The runtime * must fall back to the outer chat model when this ID cannot be - * resolved (e.g. the referenced model config was soft-deleted or - * its provider was disabled after the admin saved this config). + * resolved, such as when the referenced ChatModel was soft-deleted or + * its provider was disabled after the admin saved this configuration. */ readonly model_config_id: string; /** @@ -2830,13 +2830,26 @@ export interface ChatMessagesResponse { // From codersdk/chats.go /** - * ChatModel represents a model in the chat model catalog. + * ChatModel is an admin-managed model record for an organization. + * It is not a discovery catalog entry. See ChatModelCatalogEntry. */ export interface ChatModel { readonly id: string; - readonly provider: string; + readonly ai_provider_id: string; readonly model: string; readonly display_name: string; + readonly enabled: boolean; + readonly is_default: boolean; + readonly context_limit: number; + readonly compression_threshold: number; + readonly model_config?: ChatModelCallConfig; + /** + * ReasoningEfforts lists selectable reasoning effort values through + * the model's configured maximum. + */ + readonly reasoning_efforts?: readonly string[]; + readonly created_at: string; + readonly updated_at: string; } // From codersdk/chats.go @@ -2862,6 +2875,20 @@ export interface ChatModelAnthropicThinkingOptions { readonly budget_tokens?: number; } +// From codersdk/chats.go +/** + * ChatModelAvailabilityResponse groups the discovery catalog by provider and + * reports provider availability. + */ +export interface ChatModelAvailabilityResponse { + readonly providers: readonly ChatModelProvider[]; + /** + * UnsupportedProviders lists configured providers the Agents harness + * cannot use, so the UI can explain the empty state. + */ + readonly unsupported_providers: readonly ChatUnsupportedProvider[]; +} + // From codersdk/chats.go /** * ChatModelCallConfig configures per-call model behavior defaults. @@ -2880,25 +2907,15 @@ export interface ChatModelCallConfig { // From codersdk/chats.go /** - * ChatModelConfig is an admin-managed model configuration. + * ChatModelCatalogEntry is a discovery catalog entry for end users. + * Its ID is a synthetic provider:model value, not the UUID in ChatModel.ID. + * It is not an admin-managed record. See ChatModel. */ -export interface ChatModelConfig { +export interface ChatModelCatalogEntry { readonly id: string; - readonly ai_provider_id: string; + readonly provider: string; readonly model: string; readonly display_name: string; - readonly enabled: boolean; - readonly is_default: boolean; - readonly context_limit: number; - readonly compression_threshold: number; - readonly model_config?: ChatModelCallConfig; - /** - * ReasoningEfforts lists selectable reasoning effort values through - * the model's configured maximum. - */ - readonly reasoning_efforts?: readonly string[]; - readonly created_at: string; - readonly updated_at: string; } // From codersdk/chats.go @@ -3045,7 +3062,7 @@ export interface ChatModelProvider { readonly provider: string; readonly available: boolean; readonly unavailable_reason?: ChatModelProviderUnavailableReason; - readonly models: readonly ChatModel[]; + readonly models: readonly ChatModelCatalogEntry[]; } // From codersdk/chats.go @@ -3162,19 +3179,6 @@ export interface ChatModelVercelProviderOptions { readonly extra_body?: Record; } -// From codersdk/chats.go -/** - * ChatModelsResponse is the catalog returned from chat model discovery. - */ -export interface ChatModelsResponse { - readonly providers: readonly ChatModelProvider[]; - /** - * UnsupportedProviders lists configured providers the Agents harness - * cannot use, so the UI can explain the empty state. - */ - readonly unsupported_providers: readonly ChatUnsupportedProvider[]; -} - // From codersdk/chats.go /** * ChatPersonalModelOverride is a resolved user personal model override. @@ -3889,9 +3893,14 @@ export interface CreateChatMessageResponse { // From codersdk/chats.go /** - * CreateChatModelConfigRequest creates a chat model config. + * CreateChatModelRequest is the request body for an organization-scoped + * ChatModel. AIProviderID, Model, and a positive ContextLimit are required. + * Enabled defaults to true. IsDefault defaults to false when the organization + * already has a default model. The first model created in an organization is + * automatically promoted to default. CompressionThreshold defaults to 70. An + * omitted ModelConfig uses the provider defaults. */ -export interface CreateChatModelConfigRequest { +export interface CreateChatModelRequest { readonly ai_provider_id?: string; readonly model: string; readonly display_name?: string; @@ -9704,11 +9713,11 @@ export interface UpdateAdvisorConfigRequest { */ readonly max_output_tokens: number; /** - * ModelConfigID selects a specific chat model config to power the + * ModelConfigID selects a specific admin-managed ChatModel to power the * advisor. uuid.Nil means reuse the outer chat model. The runtime * must fall back to the outer chat model when this ID cannot be - * resolved (e.g. the referenced model config was soft-deleted or - * its provider was disabled after the admin saved this config). + * resolved, such as when the referenced ChatModel was soft-deleted or + * its provider was disabled after the admin saved this configuration. */ readonly model_config_id: string; /** @@ -9773,9 +9782,21 @@ export interface UpdateChatDebugRetentionDaysRequest { // From codersdk/chats.go /** - * UpdateChatModelConfigRequest updates a chat model config. + * UpdateChatModelOverrideRequest is the request body for updating the chat + * model override configuration endpoint. */ -export interface UpdateChatModelConfigRequest { +export interface UpdateChatModelOverrideRequest { + readonly model_config_id: string; + readonly reasoning_effort?: string; +} + +// From codersdk/chats.go +/** + * UpdateChatModelRequest updates a ChatModel. Empty Model and DisplayName + * values preserve the stored values. Nil pointer fields preserve their stored + * values. This request cannot clear DisplayName. + */ +export interface UpdateChatModelRequest { readonly ai_provider_id?: string; readonly model?: string; readonly display_name?: string; @@ -9786,16 +9807,6 @@ export interface UpdateChatModelConfigRequest { readonly model_config?: ChatModelCallConfig; } -// From codersdk/chats.go -/** - * UpdateChatModelOverrideRequest is the request body for updating the chat - * model override configuration endpoint. - */ -export interface UpdateChatModelOverrideRequest { - readonly model_config_id: string; - readonly reasoning_effort?: string; -} - // From codersdk/chats.go /** * UpdateChatPersonalModelOverridesAdminSettingsRequest is the request body for diff --git a/site/src/modules/aiModels/providerStates.test.ts b/site/src/modules/aiModels/providerStates.test.ts index c4c260ba229..1a6629c5c0f 100644 --- a/site/src/modules/aiModels/providerStates.test.ts +++ b/site/src/modules/aiModels/providerStates.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import type * as TypesGen from "#/api/typesGenerated"; import { - MockChatModelConfig, + MockChatModel, MockChatModelProvider, MockChatProviderConfig, } from "#/testHelpers/chatModels"; @@ -16,7 +16,7 @@ const baseProviderState: ProviderState = { provider: "openai", label: "OpenAI", providerConfig: MockChatProviderConfig, - modelConfigs: [], + models: [], catalogModelCount: 0, hasManagedAPIKey: true, hasCatalogAPIKey: false, @@ -36,7 +36,7 @@ describe("deriveProviderStates", () => { display_name: "OpenAI", }, ]; - const catalog: TypesGen.ChatModelsResponse = { + const catalog: TypesGen.ChatModelAvailabilityResponse = { providers: [ { ...MockChatModelProvider, provider: "google" }, { ...MockChatModelProvider, provider: "anthropic" }, @@ -65,20 +65,20 @@ describe("deriveProviderStates", () => { const providerConfigs = [ { ...MockChatProviderConfig, id: "prov-openai", provider: "openai" }, ]; - const modelConfigs = [ + const models = [ { - ...MockChatModelConfig, + ...MockChatModel, id: "m1", ai_provider_id: "prov-openai", }, - { ...MockChatModelConfig, id: "m2", ai_provider_id: "prov-openai" }, + { ...MockChatModel, id: "m2", ai_provider_id: "prov-openai" }, ]; - const states = deriveProviderStates(modelConfigs, providerConfigs, null); + const states = deriveProviderStates(models, providerConfigs, null); expect(states).toHaveLength(1); expect(states[0].key).toBe("prov-openai"); - expect(states[0].modelConfigs.map((m) => m.id)).toEqual(["m1", "m2"]); + expect(states[0].models.map((m) => m.id)).toEqual(["m1", "m2"]); }); it("treats bedrock with central_api_key_enabled as having an effective key", () => { @@ -102,17 +102,15 @@ describe("deriveProviderStates", () => { { ...MockChatProviderConfig, id: "prov-a", provider: "openai" }, { ...MockChatProviderConfig, id: "prov-b", provider: "openai" }, ]; - const modelConfigs = [ - { ...MockChatModelConfig, id: "m1", ai_provider_id: "" }, - ]; + const models = [{ ...MockChatModel, id: "m1", ai_provider_id: "" }]; - const states = deriveProviderStates(modelConfigs, providerConfigs, null); + const states = deriveProviderStates(models, providerConfigs, null); - expect(states.flatMap((s) => s.modelConfigs)).toHaveLength(0); + expect(states.flatMap((s) => s.models)).toHaveLength(0); }); it("detects env-preset providers from the catalog when no config exists", () => { - const catalog: TypesGen.ChatModelsResponse = { + const catalog: TypesGen.ChatModelAvailabilityResponse = { providers: [ { ...MockChatModelProvider, provider: "openai", available: true }, ], @@ -144,7 +142,7 @@ describe("deriveProviderStates", () => { }); it("treats an unavailable catalog provider as having a key unless the api key is missing", () => { - const catalog: TypesGen.ChatModelsResponse = { + const catalog: TypesGen.ChatModelAvailabilityResponse = { providers: [ { ...MockChatModelProvider, @@ -171,7 +169,7 @@ describe("deriveProviderStates", () => { base_url: "https://custom.example.com/v1", }, ]; - const catalog: TypesGen.ChatModelsResponse = { + const catalog: TypesGen.ChatModelAvailabilityResponse = { providers: [ { ...MockChatModelProvider, diff --git a/site/src/modules/aiModels/providerStates.ts b/site/src/modules/aiModels/providerStates.ts index c0f09ce1b81..b57794cfbff 100644 --- a/site/src/modules/aiModels/providerStates.ts +++ b/site/src/modules/aiModels/providerStates.ts @@ -10,7 +10,7 @@ export type ProviderState = { provider: string; label: string; providerConfig: TypesGen.ChatProviderConfig | undefined; - modelConfigs: readonly TypesGen.ChatModelConfig[]; + models: readonly TypesGen.ChatModel[]; catalogModelCount: number; hasManagedAPIKey: boolean; hasCatalogAPIKey: boolean; @@ -20,7 +20,8 @@ export type ProviderState = { baseURL: string; }; -type CatalogProvider = TypesGen.ChatModelsResponse["providers"][number]; +type CatalogProvider = + TypesGen.ChatModelAvailabilityResponse["providers"][number]; const envPresetProviders = new Set(["openai", "anthropic"]); @@ -42,7 +43,7 @@ const isDatabaseProviderConfig = ( }; const getCatalogProviders = ( - catalog: TypesGen.ChatModelsResponse | null | undefined, + catalog: TypesGen.ChatModelAvailabilityResponse | null | undefined, ): readonly CatalogProvider[] => { const providers = catalog?.providers; return Array.isArray(providers) ? providers : []; @@ -86,9 +87,9 @@ type ProviderEntry = { // Returns provider states ordered alphabetically by display label. export const deriveProviderStates = ( - modelConfigs: readonly TypesGen.ChatModelConfig[], + models: readonly TypesGen.ChatModel[], providerConfigs: TypesGen.ChatProviderConfig[] | null | undefined, - catalog: TypesGen.ChatModelsResponse | null | undefined, + catalog: TypesGen.ChatModelAvailabilityResponse | null | undefined, ): readonly ProviderState[] => { const orderedEntries: ProviderEntry[] = []; const seenEntries = new Set(); @@ -120,7 +121,7 @@ export const deriveProviderStates = ( } includeEntry(key, provider); } - const modelStateKey = (modelConfig: TypesGen.ChatModelConfig): string => + const modelStateKey = (modelConfig: TypesGen.ChatModel): string => readOptionalString(modelConfig.ai_provider_id) ?? ""; for (const cp of catalogProviders) { @@ -128,20 +129,20 @@ export const deriveProviderStates = ( if (!provider || providerTypesWithConfigs.has(provider)) continue; includeEntry(provider, provider); } - for (const mc of modelConfigs) { + for (const mc of models) { const key = modelStateKey(mc); includeEntry(key, providerConfigsByKey.get(key)?.provider ?? ""); } - const modelConfigsByKey = new Map(); - for (const mc of modelConfigs) { + const modelsByKey = new Map(); + for (const mc of models) { const key = modelStateKey(mc); if (!key) continue; - const existing = modelConfigsByKey.get(key); + const existing = modelsByKey.get(key); if (existing) { existing.push(mc); } else { - modelConfigsByKey.set(key, [mc]); + modelsByKey.set(key, [mc]); } } @@ -166,7 +167,7 @@ export const deriveProviderStates = ( const hasBedrockAmbientCredentials = provider === "bedrock" && providerConfig?.central_api_key_enabled === true; - const modelConfigsForProvider = modelConfigsByKey.get(key) ?? []; + const modelsForProvider = modelsByKey.get(key) ?? []; const isCatalogEnvPreset = !providerConfig && envPresetProviders.has(provider) && hasCatalogAPIKey; const isEnvPreset = @@ -177,7 +178,7 @@ export const deriveProviderStates = ( provider, label, providerConfig, - modelConfigs: modelConfigsForProvider, + models: modelsForProvider, catalogModelCount: getProviderModels(catalogProvider).length, hasManagedAPIKey, hasCatalogAPIKey, diff --git a/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPage.tsx b/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPage.tsx index 11bd82c236e..9f9fbb80a3b 100644 --- a/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPage.tsx +++ b/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPage.tsx @@ -4,8 +4,8 @@ import { chatProviderConfigs } from "#/api/queries/aiProviders"; import { chatAdvisorConfig, chatComputerUseProvider, - chatModelConfigs, chatModelOverride, + chatModels, chatPersonalModelOverridesAdminSettings, updateChatAdvisorConfig, updateChatComputerUseProvider, @@ -57,7 +57,7 @@ const CoderAgentsPage: FC = () => { ...chatModelOverride(compactionOverrideContext), enabled: canEditDeploymentConfig, }); - const modelConfigsQuery = useQuery(chatModelConfigs()); + const modelsQuery = useQuery(chatModels()); const advisorConfigQuery = useQuery({ ...chatAdvisorConfig(), enabled: canEditDeploymentConfig && showAdvisorSettings, @@ -121,16 +121,14 @@ const CoderAgentsPage: FC = () => { titleGenerationModelOverrideData={titleGenerationModelQuery.data} compactionModelOverrideData={compactionModelQuery.data} exploreModelOverrideData={exploreModelOverrideQuery.data} - modelConfigsData={modelConfigsQuery.data} + models={modelsQuery.data} providerInfoByID={providerInfoByID} - modelConfigsError={ - modelConfigsQuery.error ?? providerConfigsQuery.error + modelsError={modelsQuery.error ?? providerConfigsQuery.error} + isLoadingModels={ + modelsQuery.isLoading || providerConfigsQuery.isLoading } - isLoadingModelConfigs={ - modelConfigsQuery.isLoading || providerConfigsQuery.isLoading - } - isFetchingModelConfigs={ - modelConfigsQuery.isFetching || providerConfigsQuery.isFetching + isFetchingModels={ + modelsQuery.isFetching || providerConfigsQuery.isFetching } onSaveGeneralModelOverride={saveGeneralModelOverrideMutation.mutate} isSavingGeneralModelOverride={ diff --git a/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.stories.tsx b/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.stories.tsx index 49aaea410ea..35cd45fba83 100644 --- a/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.stories.tsx +++ b/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import { expect, fn, userEvent, waitFor, within } from "storybook/test"; import type * as TypesGen from "#/api/typesGenerated"; -import { MockChatModelConfig } from "#/testHelpers/chatModels"; +import { MockChatModel } from "#/testHelpers/chatModels"; import { CoderAgentsPageView, type CoderAgentsPageViewProps, @@ -15,9 +15,9 @@ const TITLE_UNAVAILABLE_SAVED_MODEL_WARNING = "The selected model is currently unavailable. Title generation will be skipped until you choose another model or clear this setting."; const buildModelConfig = ( - overrides: Partial, -): TypesGen.ChatModelConfig => ({ - ...MockChatModelConfig, + overrides: Partial, +): TypesGen.ChatModel => ({ + ...MockChatModel, id: "model-default", model: "gpt-4.1-mini", display_name: "GPT 4.1 Mini", @@ -121,7 +121,7 @@ const providerDisabledModelConfig = buildModelConfig({ context_limit: 128_000, }); -const allModelConfigs: TypesGen.ChatModelConfig[] = [ +const allModels: TypesGen.ChatModel[] = [ generalModelConfig, claudeSonnetModelConfig, advisorReasoningModelConfig, @@ -173,11 +173,11 @@ const buildArgs = ( titleGenerationModelOverrideData: buildTitleGenerationModelOverrideData(), compactionModelOverrideData: buildOverrideData("compaction"), exploreModelOverrideData: buildOverrideData("explore"), - modelConfigsData: allModelConfigs, + models: allModels, providerInfoByID, - modelConfigsError: undefined, - isLoadingModelConfigs: false, - isFetchingModelConfigs: false, + modelsError: undefined, + isLoadingModels: false, + isFetchingModels: false, onSaveGeneralModelOverride: fn(), isSavingGeneralModelOverride: false, isSaveGeneralModelOverrideError: false, diff --git a/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.tsx b/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.tsx index c9e781a8d7c..d11628f5551 100644 --- a/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.tsx +++ b/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.tsx @@ -9,7 +9,7 @@ import { import { AdvisorSettings } from "#/pages/AgentsPage/components/AdvisorSettings"; import { VirtualDesktopSettings } from "#/pages/AgentsPage/components/VirtualDesktopSettings"; import { - filterConfigsWithEnabledProvider, + filterModelsWithEnabledProvider, type ProviderInfo, } from "#/pages/AgentsPage/utils/modelOptions"; import { @@ -38,11 +38,11 @@ export interface CoderAgentsPageViewProps { titleGenerationModelOverrideData?: TypesGen.ChatModelOverrideResponse; compactionModelOverrideData?: TypesGen.ChatModelOverrideResponse; exploreModelOverrideData?: TypesGen.ChatModelOverrideResponse; - modelConfigsData: TypesGen.ChatModelConfig[] | undefined; + models: TypesGen.ChatModel[] | undefined; providerInfoByID: ReadonlyMap; - modelConfigsError: unknown; - isLoadingModelConfigs: boolean; - isFetchingModelConfigs: boolean; + modelsError: unknown; + isLoadingModels: boolean; + isFetchingModels: boolean; onSaveGeneralModelOverride?: SaveModelOverride; isSavingGeneralModelOverride?: boolean; isSaveGeneralModelOverrideError?: boolean; @@ -92,11 +92,11 @@ export const CoderAgentsPageView: FC = ({ titleGenerationModelOverrideData, compactionModelOverrideData, exploreModelOverrideData, - modelConfigsData, + models, providerInfoByID, - modelConfigsError, - isLoadingModelConfigs, - isFetchingModelConfigs, + modelsError, + isLoadingModels, + isFetchingModels, onSaveGeneralModelOverride, isSavingGeneralModelOverride = false, isSaveGeneralModelOverrideError = false, @@ -125,8 +125,8 @@ export const CoderAgentsPageView: FC = ({ isSavingComputerUseProvider, computerUseProviderSaveError, }) => { - const enabledModelConfigs = filterConfigsWithEnabledProvider( - (modelConfigsData ?? []).filter((modelConfig) => modelConfig.enabled), + const enabledModels = filterModelsWithEnabledProvider( + (models ?? []).filter((modelConfig) => modelConfig.enabled), providerInfoByID, ); const showGeneralModelSection = @@ -159,10 +159,10 @@ export const CoderAgentsPageView: FC = ({ title="General model" description="Used by delegated agents that can edit files or run commands." modelOverrideData={generalModelOverrideData} - enabledModelConfigs={enabledModelConfigs} + enabledModels={enabledModels} providerInfoByID={providerInfoByID} - modelConfigsError={modelConfigsError} - isLoading={isLoadingModelConfigs} + modelsError={modelsError} + isLoading={isLoadingModels} onSaveModelOverride={onSaveGeneralModelOverride} isSaving={isSavingGeneralModelOverride} isSaveError={isSaveGeneralModelOverrideError} @@ -173,10 +173,10 @@ export const CoderAgentsPageView: FC = ({ title="Title generation model" description="Leave unset to use Coder's title default, which prefers fast models from configured providers." modelOverrideData={titleGenerationModelOverrideData} - enabledModelConfigs={enabledModelConfigs} + enabledModels={enabledModels} providerInfoByID={providerInfoByID} - modelConfigsError={modelConfigsError} - isLoading={isLoadingModelConfigs} + modelsError={modelsError} + isLoading={isLoadingModels} onSaveModelOverride={onSaveTitleGenerationModel} isSaving={isSavingTitleGenerationModel} isSaveError={isSaveTitleGenerationModelError} @@ -188,10 +188,10 @@ export const CoderAgentsPageView: FC = ({ title="Compaction model" description="Used to summarize long conversations when they approach the model's context limit. Leave unset to summarize with the chat model." modelOverrideData={compactionModelOverrideData} - enabledModelConfigs={enabledModelConfigs} + enabledModels={enabledModels} providerInfoByID={providerInfoByID} - modelConfigsError={modelConfigsError} - isLoading={isLoadingModelConfigs} + modelsError={modelsError} + isLoading={isLoadingModels} onSaveModelOverride={onSaveCompactionModel} isSaving={isSavingCompactionModel} isSaveError={isSaveCompactionModelError} @@ -202,10 +202,10 @@ export const CoderAgentsPageView: FC = ({ title="Explore subagent model" description="Used for read-only codebase exploration before work returns to the main agent." modelOverrideData={exploreModelOverrideData} - enabledModelConfigs={enabledModelConfigs} + enabledModels={enabledModels} providerInfoByID={providerInfoByID} - modelConfigsError={modelConfigsError} - isLoading={isLoadingModelConfigs} + modelsError={modelsError} + isLoading={isLoadingModels} onSaveModelOverride={onSaveExploreModelOverride} isSaving={isSavingExploreModelOverride} isSaveError={isSaveExploreModelOverrideError} @@ -226,11 +226,11 @@ export const CoderAgentsPageView: FC = ({ isAdvisorConfigLoading={isAdvisorConfigLoading} isAdvisorConfigFetching={isAdvisorConfigFetching} isAdvisorConfigLoadError={isAdvisorConfigLoadError} - enabledModelConfigs={enabledModelConfigs} + enabledModels={enabledModels} providerInfoByID={providerInfoByID} - modelConfigsError={modelConfigsError} - isLoadingModelConfigs={isLoadingModelConfigs} - isFetchingModelConfigs={isFetchingModelConfigs} + modelsError={modelsError} + isLoadingModels={isLoadingModels} + isFetchingModels={isFetchingModels} onSaveAdvisorConfig={onSaveAdvisorConfig} isSavingAdvisorConfig={isSavingAdvisorConfig} isSaveAdvisorConfigError={isSaveAdvisorConfigError} diff --git a/site/src/pages/AISettingsPage/CoderAgentsPage/components/SubagentModelOverrideSettings.tsx b/site/src/pages/AISettingsPage/CoderAgentsPage/components/SubagentModelOverrideSettings.tsx index e147646091a..04e548ed15c 100644 --- a/site/src/pages/AISettingsPage/CoderAgentsPage/components/SubagentModelOverrideSettings.tsx +++ b/site/src/pages/AISettingsPage/CoderAgentsPage/components/SubagentModelOverrideSettings.tsx @@ -29,9 +29,9 @@ interface SubagentModelOverrideSettingsProps { title: string; description?: ReactNode; modelOverrideData: ModelOverrideData | undefined; - enabledModelConfigs: readonly TypesGen.ChatModelConfig[]; + enabledModels: readonly TypesGen.ChatModel[]; providerInfoByID: ReadonlyMap; - modelConfigsError: unknown; + modelsError: unknown; isLoading: boolean; onSaveModelOverride: ( req: UpdateModelOverrideRequest, @@ -51,9 +51,9 @@ export const SubagentModelOverrideSettings: FC< title, description, modelOverrideData, - enabledModelConfigs, + enabledModels, providerInfoByID, - modelConfigsError, + modelsError, isLoading, onSaveModelOverride, isSaving, @@ -66,7 +66,7 @@ export const SubagentModelOverrideSettings: FC< const { isSavedVisible, showSavedState } = useTemporarySavedState(); const hasLoadedModelOverride = modelOverrideData !== undefined; const isMalformedOverride = modelOverrideData?.is_malformed ?? false; - const enabledModelOptions = enabledModelConfigs.map((modelConfig) => { + const enabledModelOptions = enabledModels.map((modelConfig) => { const providerInfo = providerInfoByID.get(modelConfig.ai_provider_id); const reasoningEffort = modelConfig.model_config?.reasoning_effort; const reasoningEfforts = modelConfig.reasoning_efforts ?? []; @@ -177,7 +177,7 @@ export const SubagentModelOverrideSettings: FC< unavailableMessage={unavailableModelWarning} isMalformedOverride={isMalformedOverride} malformedMessage="The saved override is malformed and is being treated as unset. Click Save to clear it." - modelConfigsError={modelConfigsError} + modelsError={modelsError} />