refactor: clarify chat model resource names (#27958)

Rename the admin-managed SDK resource from `ChatModelConfig` to
`ChatModel`. Rename catalog entries from `ChatModel` to
`ChatModelCatalogEntry` and replace `ChatModelsResponse` with
`ChatModelAvailabilityResponse`.

API routes and JSON fields do not change. Experimental SDK and OpenAPI
schema names do change.

Depends on #27957

_This pull request description was generated by Coder Agents._
This commit is contained in:
Ethan
2026-08-24 09:43:45 +00:00
committed by GitHub
parent cbe24a3da5
commit 9bd8899359
90 changed files with 1239 additions and 1239 deletions
+7 -7
View File
@@ -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
+2 -2
View File
@@ -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,
+4 -4
View File
@@ -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
+21 -21
View File
@@ -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": [
+21 -21
View File
@@ -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"],
+7 -7
View File
@@ -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
+7 -7
View File
@@ -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,
+7 -7
View File
@@ -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)
+18 -18
View File
@@ -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{
+6 -6
View File
@@ -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,
+7 -7
View File
@@ -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
})
}
+150 -124
View File
@@ -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: &notDefault,
})
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,
+3 -3
View File
@@ -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) {
+6 -6
View File
@@ -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{
+14 -14
View File
@@ -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)
})
}
@@ -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{},
}}},
},
}
+3 -3
View File
@@ -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,
+56 -45
View File
@@ -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
}
+1 -1
View File
@@ -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)
}
+7 -7
View File
@@ -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.",
+3 -3
View File
@@ -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).
+37 -37
View File
@@ -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
+36 -36
View File
@@ -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",
+4 -4
View File
@@ -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)
+20 -21
View File
@@ -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
}
+4 -4
View File
@@ -435,14 +435,14 @@ describe("api.ts", () => {
it.each<[string, () => Promise<unknown>, 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<unknown>]>([
[
"/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");
+28 -29
View File
@@ -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<TypesGen.ChatModelsResponse> => {
const response = await this.axios.get<TypesGen.ChatModelsResponse>(
"/api/experimental/chats/models",
);
return response.data;
};
getChatModelAvailability =
async (): Promise<TypesGen.ChatModelAvailabilityResponse> => {
const response =
await this.axios.get<TypesGen.ChatModelAvailabilityResponse>(
"/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<TypesGen.UserChatCompactionThreshold> => {
const response = await this.axios.put<TypesGen.UserChatCompactionThreshold>(
`/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<void> => {
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<TypesGen.ChatModelConfig[]> => {
const response =
await this.axios.get<TypesGen.ChatModelConfig[]>(chatModelConfigsPath);
getChatModels = async (): Promise<TypesGen.ChatModel[]> => {
const response = await this.axios.get<TypesGen.ChatModel[]>(chatModelsPath);
return response.data;
};
createChatModelConfig = async (
req: TypesGen.CreateChatModelConfigRequest,
): Promise<TypesGen.ChatModelConfig> => {
const response = await this.axios.post<TypesGen.ChatModelConfig>(
chatModelConfigsPath,
createChatModel = async (
req: TypesGen.CreateChatModelRequest,
): Promise<TypesGen.ChatModel> => {
const response = await this.axios.post<TypesGen.ChatModel>(
chatModelsPath,
req,
);
return response.data;
};
updateChatModelConfig = async (
modelConfigId: string,
req: TypesGen.UpdateChatModelConfigRequest,
): Promise<TypesGen.ChatModelConfig> => {
const response = await this.axios.patch<TypesGen.ChatModelConfig>(
`${chatModelConfigsPath}/${encodeURIComponent(modelConfigId)}`,
updateChatModel = async (
modelId: string,
req: TypesGen.UpdateChatModelRequest,
): Promise<TypesGen.ChatModel> => {
const response = await this.axios.patch<TypesGen.ChatModel>(
`${chatModelsPath}/${encodeURIComponent(modelId)}`,
req,
);
return response.data;
};
deleteChatModelConfig = async (modelConfigId: string): Promise<void> => {
await this.axios.delete(
`${chatModelConfigsPath}/${encodeURIComponent(modelConfigId)}`,
);
deleteChatModel = async (modelId: string): Promise<void> => {
await this.axios.delete(`${chatModelsPath}/${encodeURIComponent(modelId)}`);
};
getMCPServerConfigs = async (
+32 -32
View File
@@ -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<TypesGen.ChatModelsResponse> =>
API.experimental.getChatModels(),
export const chatModelAvailability = () => ({
queryKey: chatModelAvailabilityKey,
queryFn: (): Promise<TypesGen.ChatModelAvailabilityResponse> =>
API.experimental.getChatModelAvailability(),
});
export const chatModelConfigsKey = [
export const chatModelsKey = [
...chatConfigKey,
"models",
"definitions",
] as const;
export const chatModelConfigs = () => ({
queryKey: chatModelConfigsKey,
queryFn: (): Promise<TypesGen.ChatModelConfig[]> =>
API.experimental.getChatModelConfigs(),
export const chatModels = () => ({
queryKey: chatModelsKey,
queryFn: (): Promise<TypesGen.ChatModel[]> =>
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);
},
+62 -51
View File
@@ -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<string, unknown>;
}
// 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
@@ -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,
+14 -13
View File
@@ -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<string>();
@@ -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<string, TypesGen.ChatModelConfig[]>();
for (const mc of modelConfigs) {
const modelsByKey = new Map<string, TypesGen.ChatModel[]>();
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,
@@ -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={
@@ -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>,
): TypesGen.ChatModelConfig => ({
...MockChatModelConfig,
overrides: Partial<TypesGen.ChatModel>,
): 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,
@@ -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<string, ProviderInfo>;
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<CoderAgentsPageViewProps> = ({
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<CoderAgentsPageViewProps> = ({
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<CoderAgentsPageViewProps> = ({
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<CoderAgentsPageViewProps> = ({
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<CoderAgentsPageViewProps> = ({
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<CoderAgentsPageViewProps> = ({
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<CoderAgentsPageViewProps> = ({
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}
@@ -29,9 +29,9 @@ interface SubagentModelOverrideSettingsProps {
title: string;
description?: ReactNode;
modelOverrideData: ModelOverrideData | undefined;
enabledModelConfigs: readonly TypesGen.ChatModelConfig[];
enabledModels: readonly TypesGen.ChatModel[];
providerInfoByID: ReadonlyMap<string, ProviderInfo>;
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}
/>
</div>
<Button
@@ -5,9 +5,9 @@ import { toast } from "sonner";
import { getErrorMessage } from "#/api/errors";
import { chatProviderConfigs } from "#/api/queries/aiProviders";
import {
chatModelConfigs,
chatModelAvailability,
chatModels,
createChatModelConfig,
createChatModel,
} from "#/api/queries/chats";
import { useAuthenticated } from "#/hooks/useAuthenticated";
import {
@@ -27,33 +27,33 @@ const AddModelPage: FC = () => {
const duplicateId = searchParams.get("duplicate");
const providerConfigsQuery = useQuery(chatProviderConfigs());
const modelConfigsQuery = useQuery(chatModelConfigs());
const modelCatalogQuery = useQuery(chatModels());
const modelsQuery = useQuery(chatModels());
const modelCatalogQuery = useQuery(chatModelAvailability());
const createMutation = useMutation(createChatModelConfig(queryClient));
const createMutation = useMutation(createChatModel(queryClient));
const providerStates = useMemo(
() =>
deriveProviderStates(
modelConfigsQuery.data ?? [],
modelsQuery.data ?? [],
providerConfigsQuery.data,
modelCatalogQuery.data,
),
[modelConfigsQuery.data, providerConfigsQuery.data, modelCatalogQuery.data],
[modelsQuery.data, providerConfigsQuery.data, modelCatalogQuery.data],
);
const isLoading =
providerConfigsQuery.isLoading ||
modelConfigsQuery.isLoading ||
modelsQuery.isLoading ||
modelCatalogQuery.isLoading;
const selectedProviderState = providerKey
? (providerStates.find((ps) => ps.key === providerKey) ?? null)
: (providerStates.find(canManageProviderModels) ?? null);
const duplicateSourceModel = duplicateId
? modelConfigsQuery.data?.find((m) => m.id === duplicateId)
? modelsQuery.data?.find((m) => m.id === duplicateId)
: undefined;
const currentDefaultModel = modelConfigsQuery.data?.find((m) => m.is_default);
const currentDefaultModel = modelsQuery.data?.find((m) => m.is_default);
return (
<RequirePermission isFeatureVisible={permissions.editDeploymentConfig}>
@@ -12,13 +12,11 @@ interface AddModelPageViewProps {
isLoading: boolean;
providerStates: readonly ProviderState[];
selectedProviderState: ProviderState | null;
duplicateSourceModel?: TypesGen.ChatModelConfig;
currentDefaultModel?: TypesGen.ChatModelConfig;
duplicateSourceModel?: TypesGen.ChatModel;
currentDefaultModel?: TypesGen.ChatModel;
isSaving: boolean;
onProviderChange: (providerKey: string) => void;
onCreateModel: (
req: TypesGen.CreateChatModelConfigRequest,
) => Promise<unknown>;
onCreateModel: (req: TypesGen.CreateChatModelRequest) => Promise<unknown>;
}
const AddModelPageView: FC<AddModelPageViewProps> = ({
@@ -1,7 +1,7 @@
import type { FC } from "react";
import { useQuery } from "react-query";
import { chatProviderConfigs } from "#/api/queries/aiProviders";
import { chatModelConfigs, chatModels } from "#/api/queries/chats";
import { chatModelAvailability, chatModels } from "#/api/queries/chats";
import { useAuthenticated } from "#/hooks/useAuthenticated";
import { deriveProviderStates } from "#/modules/aiModels/providerStates";
import { RequirePermission } from "#/modules/permissions/RequirePermission";
@@ -16,14 +16,14 @@ const ModelsPage: FC = () => {
...chatProviderConfigs(),
enabled: permissions.editDeploymentConfig,
});
const modelConfigsQuery = useQuery(chatModelConfigs());
const modelCatalogQuery = useQuery(chatModels());
const modelsQuery = useQuery(chatModels());
const modelCatalogQuery = useQuery(chatModelAvailability());
const providerTypeByID = providerTypeByIDFromConfigs(
providerConfigsQuery.data,
);
const models = (modelConfigsQuery.data ?? []).slice().sort((a, b) => {
const models = (modelsQuery.data ?? []).slice().sort((a, b) => {
const aProvider = providerTypeByID.get(a.ai_provider_id) ?? "";
const bProvider = providerTypeByID.get(b.ai_provider_id) ?? "";
const cmp = aProvider.localeCompare(bProvider);
@@ -42,12 +42,12 @@ const ModelsPage: FC = () => {
<ModelsPageView
isLoading={
providerConfigsQuery.isLoading ||
modelConfigsQuery.isLoading ||
modelsQuery.isLoading ||
modelCatalogQuery.isLoading
}
error={
providerConfigsQuery.error ??
modelConfigsQuery.error ??
modelsQuery.error ??
modelCatalogQuery.error
}
models={models}
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, userEvent, within } from "storybook/test";
import { reactRouterParameters } from "storybook-addon-remix-react-router";
import type { ChatModelConfig } from "#/api/typesGenerated";
import type { ChatModel } from "#/api/typesGenerated";
import ModelsPageView from "./ModelsPageView";
import {
MockAnthropicProviderState,
@@ -154,7 +154,7 @@ export const DisabledProviderModelsStillListed: Story = {
// models entirely, so the row reaches "Unset" via a map-miss and the
// `?? false` fallback at ModelsPageView.tsx wiring. Reproduce that shape
// here: the model appears in `models` but is not present in any
// providerState.modelConfigs, so a `?? true` regression would flip this
// providerState.models, so a `?? true` regression would flip this
// story to "Enabled" and be caught.
export const OrphanedModelShowsUnset: Story = {
args: {
@@ -197,7 +197,7 @@ export const LoadError: Story = {
},
};
const manyModels: ChatModelConfig[] = Array.from({ length: 23 }, (_, i) => ({
const manyModels: ChatModel[] = Array.from({ length: 23 }, (_, i) => ({
...mockClaude,
id: `model-${i}`,
model: `model-${i}`,
@@ -1,7 +1,7 @@
import { ChevronDownIcon, PlusIcon, SearchIcon } from "lucide-react";
import { type FC, useMemo, useState } from "react";
import { useNavigate } from "react-router";
import type { ChatModelConfig } from "#/api/typesGenerated";
import type { ChatModel } from "#/api/typesGenerated";
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
import { Button } from "#/components/Button/Button";
import {
@@ -97,7 +97,7 @@ const AddModelDropdown: FC<{
interface ModelsPageViewProps {
isLoading: boolean;
error: unknown;
models: readonly ChatModelConfig[];
models: readonly ChatModel[];
providerStates: readonly ProviderState[];
providerTypeByID: ReadonlyMap<string, string>;
}
@@ -118,7 +118,7 @@ const ModelsPageView: FC<ModelsPageViewProps> = ({
const providerKeyByModelId = useMemo(() => {
const map = new Map<string, string>();
for (const providerState of providerStates) {
for (const providerModel of providerState.modelConfigs) {
for (const providerModel of providerState.models) {
map.set(providerModel.id, providerState.key);
}
}
@@ -128,7 +128,7 @@ const ModelsPageView: FC<ModelsPageViewProps> = ({
const providerLabelByModelId = useMemo(() => {
const map = new Map<string, string>();
for (const providerState of providerStates) {
for (const providerModel of providerState.modelConfigs) {
for (const providerModel of providerState.models) {
map.set(providerModel.id, providerState.label);
}
}
@@ -138,7 +138,7 @@ const ModelsPageView: FC<ModelsPageViewProps> = ({
const hasProviderByModelId = useMemo(() => {
const map = new Map<string, boolean>();
for (const providerState of providerStates) {
for (const providerModel of providerState.modelConfigs) {
for (const providerModel of providerState.models) {
map.set(providerModel.id, Boolean(providerState.providerConfig));
}
}
@@ -148,7 +148,7 @@ const ModelsPageView: FC<ModelsPageViewProps> = ({
const providerEnabledByModelId = useMemo(() => {
const map = new Map<string, boolean>();
for (const providerState of providerStates) {
for (const providerModel of providerState.modelConfigs) {
for (const providerModel of providerState.models) {
map.set(
providerModel.id,
providerState.providerConfig?.enabled === true,
@@ -5,10 +5,10 @@ import { toast } from "sonner";
import { getErrorMessage } from "#/api/errors";
import { chatProviderConfigs } from "#/api/queries/aiProviders";
import {
chatModelConfigs,
chatModelAvailability,
chatModels,
deleteChatModelConfig,
updateChatModelConfig,
deleteChatModel,
updateChatModel,
} from "#/api/queries/chats";
import { Loader } from "#/components/Loader/Loader";
import { useAuthenticated } from "#/hooks/useAuthenticated";
@@ -24,29 +24,29 @@ const UpdateModelPage: FC = () => {
const queryClient = useQueryClient();
const providerConfigsQuery = useQuery(chatProviderConfigs());
const modelConfigsQuery = useQuery(chatModelConfigs());
const modelCatalogQuery = useQuery(chatModels());
const modelsQuery = useQuery(chatModels());
const modelCatalogQuery = useQuery(chatModelAvailability());
const updateMutation = useMutation(updateChatModelConfig(queryClient));
const deleteMutation = useMutation(deleteChatModelConfig(queryClient));
const updateMutation = useMutation(updateChatModel(queryClient));
const deleteMutation = useMutation(deleteChatModel(queryClient));
const providerStates = useMemo(
() =>
deriveProviderStates(
modelConfigsQuery.data ?? [],
modelsQuery.data ?? [],
providerConfigsQuery.data,
modelCatalogQuery.data,
),
[modelConfigsQuery.data, providerConfigsQuery.data, modelCatalogQuery.data],
[modelsQuery.data, providerConfigsQuery.data, modelCatalogQuery.data],
);
const isLoading =
providerConfigsQuery.isLoading ||
modelConfigsQuery.isLoading ||
modelsQuery.isLoading ||
modelCatalogQuery.isLoading;
const model = modelConfigsQuery.data?.find((m) => m.id === modelId);
const currentDefaultModel = modelConfigsQuery.data?.find((m) => m.is_default);
const model = modelsQuery.data?.find((m) => m.id === modelId);
const currentDefaultModel = modelsQuery.data?.find((m) => m.is_default);
const [providerKeyOverride, setProviderKeyOverride] = useState<string | null>(
null,
);
@@ -54,9 +54,7 @@ const UpdateModelPage: FC = () => {
(providerKeyOverride
? providerStates.find((ps) => ps.key === providerKeyOverride)
: undefined) ??
providerStates.find((ps) =>
ps.modelConfigs.some((m) => m.id === modelId),
) ??
providerStates.find((ps) => ps.models.some((m) => m.id === modelId)) ??
null;
return (
@@ -82,7 +80,7 @@ const UpdateModelPage: FC = () => {
onUpdateModel={async (id, req) => {
try {
const updated = await updateMutation.mutateAsync({
modelConfigId: id,
modelId: id,
req,
});
toast.success(
@@ -114,7 +112,7 @@ const UpdateModelPage: FC = () => {
}}
onToggleEnabled={(enabled) => {
updateMutation.mutate(
{ modelConfigId: model.id, req: { enabled } },
{ modelId: model.id, req: { enabled } },
{
onSuccess: () => {
toast.success(
@@ -5,18 +5,18 @@ import { pageTitle } from "#/utils/page";
import { ModelForm } from "../components/ModelForm";
interface UpdateModelPageViewProps {
model: TypesGen.ChatModelConfig;
currentDefaultModel?: TypesGen.ChatModelConfig;
model: TypesGen.ChatModel;
currentDefaultModel?: TypesGen.ChatModel;
providerStates: readonly ProviderState[];
selectedProviderState: ProviderState | null;
onProviderChange: (providerKey: string) => void;
isSaving: boolean;
isDeleting: boolean;
onUpdateModel: (
modelConfigId: string,
req: TypesGen.UpdateChatModelConfigRequest,
modelId: string,
req: TypesGen.UpdateChatModelRequest,
) => Promise<unknown>;
onDeleteModel: (modelConfigId: string) => Promise<void>;
onDeleteModel: (modelId: string) => Promise<void>;
onDuplicate: () => void;
onToggleEnabled: (enabled: boolean) => void;
}
@@ -32,8 +32,8 @@ import { ModelForm } from "./ModelForm";
const onUpdateModel = fn(
async (
_modelConfigId: string,
_req: TypesGen.UpdateChatModelConfigRequest,
_modelId: string,
_req: TypesGen.UpdateChatModelRequest,
): Promise<unknown> => undefined,
);
@@ -44,22 +44,20 @@ const validationSchema = Yup.object({
});
interface ModelFormProps {
editingModel?: TypesGen.ChatModelConfig;
duplicateSourceModel?: TypesGen.ChatModelConfig;
editingModel?: TypesGen.ChatModel;
duplicateSourceModel?: TypesGen.ChatModel;
providerStates: readonly ProviderState[];
selectedProviderState: ProviderState | null;
onProviderChange: (providerKey: string) => void;
isSaving: boolean;
isDeleting: boolean;
onCreateModel: (
req: TypesGen.CreateChatModelConfigRequest,
) => Promise<unknown>;
onCreateModel: (req: TypesGen.CreateChatModelRequest) => Promise<unknown>;
onUpdateModel: (
modelConfigId: string,
req: TypesGen.UpdateChatModelConfigRequest,
modelId: string,
req: TypesGen.UpdateChatModelRequest,
) => Promise<unknown>;
onDeleteModel?: (modelConfigId: string) => Promise<void>;
currentDefaultModel?: TypesGen.ChatModelConfig;
onDeleteModel?: (modelId: string) => Promise<void>;
currentDefaultModel?: TypesGen.ChatModel;
onSetDefault?: () => void;
onDuplicate?: () => void;
onToggleEnabled?: (enabled: boolean) => void;
@@ -148,7 +146,7 @@ export const ModelForm: FC<ModelFormProps> = ({
const editingProviderConfigID = editingModel?.ai_provider_id.trim() ?? "";
if (isEditing && editingModel) {
const req: TypesGen.UpdateChatModelConfigRequest = {
const req: TypesGen.UpdateChatModelRequest = {
...(selectedProviderConfigID &&
selectedProviderConfigID !== editingProviderConfigID && {
ai_provider_id: selectedProviderConfigID,
@@ -181,7 +179,7 @@ export const ModelForm: FC<ModelFormProps> = ({
} else {
if (!selectedProviderState?.providerConfig) return;
const req: TypesGen.CreateChatModelConfigRequest = {
const req: TypesGen.CreateChatModelRequest = {
ai_provider_id: selectedProviderState.providerConfig.id,
model: trimmedModel,
enabled: values.enabled,
@@ -14,8 +14,8 @@ import {
import type { ModelFormValues } from "#/pages/AgentsPage/components/ChatModelAdminPanel/modelConfigFormLogic";
export const ModelFormDialogs: FC<{
editingModel?: TypesGen.ChatModelConfig;
onDeleteModel?: (modelConfigId: string) => Promise<void>;
editingModel?: TypesGen.ChatModel;
onDeleteModel?: (modelId: string) => Promise<void>;
isDeleting: boolean;
confirmingDelete: boolean;
setConfirmingDelete: (open: boolean) => void;
@@ -28,7 +28,7 @@ export const ModelFormDialogs: FC<{
};
confirmingReplaceDefault: boolean;
setConfirmingReplaceDefault: (open: boolean) => void;
currentDefaultModel?: TypesGen.ChatModelConfig;
currentDefaultModel?: TypesGen.ChatModel;
onConfirmReplaceDefault: () => void;
}> = ({
editingModel,
@@ -97,7 +97,7 @@ export const ModelFormFields: FC<{
isEditing: boolean;
isSaving: boolean;
canSubmit: boolean;
initialModel?: TypesGen.ChatModelConfig;
initialModel?: TypesGen.ChatModel;
modelField: FormHelpers;
contextLimitField: FormHelpers;
compressionThresholdField: FormHelpers;
@@ -43,8 +43,8 @@ export const ModelFormHeader: FC<{
title: string;
selectedProviderState: ProviderState;
isEditing: boolean;
editingModel?: TypesGen.ChatModelConfig;
onDeleteModel?: (modelConfigId: string) => Promise<void>;
editingModel?: TypesGen.ChatModel;
onDeleteModel?: (modelId: string) => Promise<void>;
onDuplicate?: () => void;
onToggleEnabled?: (enabled: boolean) => void;
isSaving: boolean;
@@ -1,6 +1,6 @@
import { ChevronRightIcon, InfoIcon } from "lucide-react";
import type { FC } from "react";
import type { ChatModelConfig } from "#/api/typesGenerated";
import type { ChatModel } from "#/api/typesGenerated";
import { Avatar } from "#/components/Avatar/Avatar";
import { Badge } from "#/components/Badge/Badge";
import { TableCell, TableRow } from "#/components/Table/Table";
@@ -13,7 +13,7 @@ import { useClickableTableRow } from "#/hooks/useClickableTableRow";
import { ProviderIcon } from "#/pages/AISettingsPage/ProvidersPage/components/ProviderIcon";
type ModelRowProps = {
model: ChatModelConfig;
model: ChatModel;
providerLabel: string;
providerTypeByID: ReadonlyMap<string, string>;
hasProvider: boolean;
@@ -1,4 +1,4 @@
import type { ChatModelConfig, ChatProviderConfig } from "#/api/typesGenerated";
import type { ChatModel, ChatProviderConfig } from "#/api/typesGenerated";
import type { ProviderState } from "#/modules/aiModels/providerStates";
const now = "2026-02-18T12:00:00.000Z";
@@ -26,7 +26,7 @@ const MockAnthropicProviderConfig: ChatProviderConfig = {
display_name: "Anthropic",
};
export const mockGPT5: ChatModelConfig = {
export const mockGPT5: ChatModel = {
id: "model-gpt5",
ai_provider_id: "prov-openai",
model: "gpt-5",
@@ -39,7 +39,7 @@ export const mockGPT5: ChatModelConfig = {
updated_at: now,
};
export const mockClaude: ChatModelConfig = {
export const mockClaude: ChatModel = {
...mockGPT5,
id: "model-claude",
ai_provider_id: "prov-anthropic",
@@ -48,7 +48,7 @@ export const mockClaude: ChatModelConfig = {
is_default: false,
};
export const mockDisabledModel: ChatModelConfig = {
export const mockDisabledModel: ChatModel = {
...mockGPT5,
id: "model-disabled",
model: "gpt-4o-mini",
@@ -63,7 +63,7 @@ export const MockOpenAIProviderState: ProviderState = {
provider: "openai",
label: "OpenAI",
providerConfig: MockOpenAIProviderConfig,
modelConfigs: [mockGPT5, mockDisabledModel],
models: [mockGPT5, mockDisabledModel],
catalogModelCount: 0,
hasManagedAPIKey: true,
hasCatalogAPIKey: true,
@@ -79,7 +79,7 @@ export const MockAnthropicProviderState: ProviderState = {
provider: "anthropic",
label: "Anthropic",
providerConfig: MockAnthropicProviderConfig,
modelConfigs: [mockClaude],
models: [mockClaude],
};
const MockGoogleProviderConfig: ChatProviderConfig = {
@@ -95,7 +95,7 @@ export const MockGoogleProviderState: ProviderState = {
provider: "google",
label: "Google",
providerConfig: MockGoogleProviderConfig,
modelConfigs: [],
models: [],
};
const MockBedrockProviderConfig: ChatProviderConfig = {
@@ -105,7 +105,7 @@ const MockBedrockProviderConfig: ChatProviderConfig = {
display_name: "AWS Bedrock",
};
export const mockBedrockClaude: ChatModelConfig = {
export const mockBedrockClaude: ChatModel = {
...mockClaude,
id: "model-bedrock-claude",
ai_provider_id: "prov-bedrock",
@@ -119,7 +119,7 @@ export const MockBedrockProviderState: ProviderState = {
provider: "bedrock",
label: "AWS Bedrock",
providerConfig: MockBedrockProviderConfig,
modelConfigs: [mockBedrockClaude],
models: [mockBedrockClaude],
};
export const MockAzureProviderState: ProviderState = {
@@ -133,7 +133,7 @@ export const MockAzureProviderState: ProviderState = {
provider: "azure",
display_name: "Azure OpenAI",
},
modelConfigs: [],
models: [],
};
const MockDisabledProviderConfig: ChatProviderConfig = {
@@ -143,7 +143,7 @@ const MockDisabledProviderConfig: ChatProviderConfig = {
enabled: false,
};
export const mockProviderDisabledModel: ChatModelConfig = {
export const mockProviderDisabledModel: ChatModel = {
...mockGPT5,
id: "model-provider-disabled",
ai_provider_id: "prov-openai-disabled",
@@ -158,7 +158,7 @@ export const MockDisabledProviderState: ProviderState = {
provider: "openai",
label: "OpenAI Secondary",
providerConfig: MockDisabledProviderConfig,
modelConfigs: [mockProviderDisabledModel],
models: [mockProviderDisabledModel],
};
export const MockCopilotProviderState: ProviderState = {
@@ -172,14 +172,14 @@ export const MockCopilotProviderState: ProviderState = {
provider: "copilot",
display_name: "GitHub Copilot",
},
modelConfigs: [],
models: [],
};
// A model whose provider row has been deleted. In production such models
// still appear in the top-level model list, but `deriveProviderStates`
// drops them from every providerState.modelConfigs. Stories should feed
// drops them from every providerState.models. Stories should feed
// this fixture through `models` alone; do not add it to a provider state.
export const mockOrphanedModel: ChatModelConfig = {
export const mockOrphanedModel: ChatModel = {
...mockGPT5,
id: "model-orphaned",
ai_provider_id: "prov-orphaned",
@@ -14,8 +14,8 @@ import {
chatEntityKey,
chatListKey,
chatMessagesKey,
chatModelConfigs,
chatModelsKey,
chatModelAvailabilityKey,
chatModels,
chatPromptsKey,
mcpServerConfigsKey,
toChatListParams,
@@ -28,7 +28,7 @@ import {
MockChatQueuedMessage,
MockMCPServerConfig,
} from "#/testHelpers/chatEntities";
import { MockChatModelConfig } from "#/testHelpers/chatModels";
import { MockChatModel } from "#/testHelpers/chatModels";
import {
MockGroup,
MockOrganizationMember,
@@ -125,7 +125,7 @@ const mockWorkspace: TypesGen.Workspace = {
},
};
const mockModelCatalog: TypesGen.ChatModelsResponse = {
const mockModelCatalog: TypesGen.ChatModelAvailabilityResponse = {
providers: [
{
provider: "openai",
@@ -143,9 +143,9 @@ const mockModelCatalog: TypesGen.ChatModelsResponse = {
unsupported_providers: [],
};
const mockModelConfigs: TypesGen.ChatModelConfig[] = [
const mockModels: TypesGen.ChatModel[] = [
{
...MockChatModelConfig,
...MockChatModel,
id: MODEL_CONFIG_ID,
model: "gpt-4o",
display_name: "GPT-4o",
@@ -307,8 +307,8 @@ const buildQueries = (
key: workspaceByIdKey(mockWorkspace.id),
data: mockWorkspace,
},
{ key: chatModelsKey, data: mockModelCatalog },
{ key: chatModelConfigs().queryKey, data: mockModelConfigs },
{ key: chatModelAvailabilityKey, data: mockModelCatalog },
{ key: chatModels().queryKey, data: mockModels },
{
key: mcpServerConfigsKey(chat.organization_id),
data: opts?.mcpServers ?? [],
+14 -14
View File
@@ -33,7 +33,7 @@ import { buildOptimisticEditedMessage } from "#/api/queries/chatMessageEdits";
import {
chat,
chatMessagesForInfiniteScroll,
chatModelConfigs,
chatModelAvailability,
chatModels,
chatQueueConvergence,
compactChat,
@@ -778,15 +778,15 @@ const getPersistedDetailError = ({
* preferring the user's override when set.
*/
function resolveCompactionThreshold(
modelConfigID: string | undefined,
modelID: string | undefined,
userThresholds: readonly TypesGen.UserChatCompactionThreshold[] | undefined,
modelConfigs: readonly TypesGen.ChatModelConfig[] | null | undefined,
models: readonly TypesGen.ChatModel[] | null | undefined,
): number | undefined {
if (!modelConfigID || !Array.isArray(modelConfigs)) return undefined;
const config = modelConfigs.find((c) => c.id === modelConfigID);
if (!modelID || !Array.isArray(models)) return undefined;
const config = models.find((c) => c.id === modelID);
if (!config) return undefined;
const userOverride = userThresholds?.find(
(threshold) => threshold.model_config_id === modelConfigID,
(threshold) => threshold.model_config_id === modelID,
);
if (userOverride) {
return userOverride.threshold_percent;
@@ -942,8 +942,8 @@ const AgentChatPage: FC = () => {
});
const workspace = workspaceQuery.data;
const chatModelAvailabilityQuery = useQuery(chatModelAvailability());
const chatModelsQuery = useQuery(chatModels());
const chatModelConfigsQuery = useQuery(chatModelConfigs());
const chatProviderConfigsQuery = useQuery({
...chatProviderConfigs(),
enabled: permissions.editDeploymentConfig,
@@ -993,26 +993,26 @@ const AgentChatPage: FC = () => {
modelCatalog,
hasConfiguredModels,
} = resolveModelSelector(
chatModelConfigsQuery,
chatModelsQuery,
chatModelAvailabilityQuery,
userProviderConfigsQuery,
);
const modelConfigs = chatModelConfigsQuery.data ?? [];
const models = chatModelsQuery.data ?? [];
const providerCount =
permissions.editDeploymentConfig &&
chatProviderConfigsQuery.isSuccess &&
chatModelsQuery.isSuccess
chatModelAvailabilityQuery.isSuccess
? countConfiguredProviderConfigs(
chatProviderConfigsQuery.data,
chatModelsQuery.data,
chatModelAvailabilityQuery.data,
)
: undefined;
const modelCount =
chatModelConfigsQuery.isSuccess && chatModelsQuery.isSuccess
chatModelsQuery.isSuccess && chatModelAvailabilityQuery.isSuccess
? modelOptions.length
: undefined;
const unsupportedProviderNames = getUnsupportedProviderNames(
chatModelsQuery.data,
chatModelAvailabilityQuery.data,
);
const agentBindingRefetchKeyRef = useRef<string | undefined>(undefined);
@@ -1372,7 +1372,7 @@ const AgentChatPage: FC = () => {
const compressionThreshold = resolveCompactionThreshold(
chatLastModelConfigID,
userThresholdsQuery.data?.thresholds,
modelConfigs,
models,
);
const hasModelOptions = modelOptions.length > 0;
const hasUserFixableModelProviders = hasUserFixableProviders(modelCatalog);
@@ -50,11 +50,11 @@ import { lastActiveSidebarTabStorageKeyPrefix } from "./utils/sidebarTabStorage"
// ---------------------------------------------------------------------------
const AGENT_ID = "agent-detail-view-1";
const defaultModelConfigID = "model-config-1";
const defaultModelID = "model-config-1";
const defaultModelOptions: ModelSelectorOption[] = [
{
id: defaultModelConfigID,
id: defaultModelID,
provider: "openai",
model: "gpt-4o",
displayName: "GPT-4o",
@@ -70,7 +70,7 @@ const buildChat = (overrides: Partial<TypesGen.Chat> = {}): TypesGen.Chat => ({
owner_username: "owner",
owner_name: "Owner",
title: "Help me refactor",
last_model_config_id: defaultModelConfigID,
last_model_config_id: defaultModelID,
created_at: oneWeekAgo,
updated_at: oneWeekAgo,
...overrides,
@@ -141,7 +141,7 @@ const StoryAgentChatPageView: FC<StoryProps> = ({ editing, ...overrides }) => {
chatOwner: undefined as ComponentProps<
typeof AgentChatPageView
>["chatOwner"],
effectiveSelectedModel: defaultModelConfigID,
effectiveSelectedModel: defaultModelID,
setSelectedModel: fn(),
modelOptions: defaultModelOptions,
modelSelectorPlaceholder: "Select a model",
@@ -893,7 +893,7 @@ export const Loading: Story = {
remountKey={0}
onContentChange={fn()}
isInputDisabled
effectiveSelectedModel={defaultModelConfigID}
effectiveSelectedModel={defaultModelID}
setSelectedModel={fn()}
modelOptions={defaultModelOptions}
modelSelectorPlaceholder="Select a model"
@@ -917,7 +917,7 @@ export const LoadingWithModelOptions: Story = {
remountKey={0}
onContentChange={fn()}
isInputDisabled={false}
effectiveSelectedModel={defaultModelConfigID}
effectiveSelectedModel={defaultModelID}
setSelectedModel={fn()}
modelOptions={defaultModelOptions}
modelSelectorPlaceholder="Select a model"
@@ -940,7 +940,7 @@ export const LoadingWithRightPanel: Story = {
remountKey={0}
onContentChange={fn()}
isInputDisabled
effectiveSelectedModel={defaultModelConfigID}
effectiveSelectedModel={defaultModelID}
setSelectedModel={fn()}
modelOptions={defaultModelOptions}
modelSelectorPlaceholder="Select a model"
@@ -964,7 +964,7 @@ export const LoadingSidebarCollapsed: Story = {
remountKey={0}
onContentChange={fn()}
isInputDisabled
effectiveSelectedModel={defaultModelConfigID}
effectiveSelectedModel={defaultModelID}
setSelectedModel={fn()}
modelOptions={defaultModelOptions}
modelSelectorPlaceholder="Select a model"
+10 -10
View File
@@ -5,7 +5,7 @@ import { toast } from "sonner";
import { getErrorMessage } from "#/api/errors";
import { chatProviderConfigs } from "#/api/queries/aiProviders";
import {
chatModelConfigs,
chatModelAvailability,
chatModels,
createChat,
userChatPersonalModelOverrides,
@@ -42,8 +42,8 @@ const AgentCreatePage: FC = () => {
const { permissions } = useAuthenticated();
const aiGatewayDisabled = !useAIGatewayEnabled();
const chatModelAvailabilityQuery = useQuery(chatModelAvailability());
const chatModelsQuery = useQuery(chatModels());
const chatModelConfigsQuery = useQuery(chatModelConfigs());
const chatProviderConfigsQuery = useQuery({
...chatProviderConfigs(),
enabled: permissions.editDeploymentConfig,
@@ -60,25 +60,25 @@ const AgentCreatePage: FC = () => {
const { options: catalogModelOptions, isModelCatalogLoading } =
resolveModelSelector(
chatModelConfigsQuery,
chatModelsQuery,
chatModelAvailabilityQuery,
userProviderConfigsQuery,
);
const providerCount =
permissions.editDeploymentConfig &&
chatProviderConfigsQuery.isSuccess &&
chatModelsQuery.isSuccess
chatModelAvailabilityQuery.isSuccess
? countConfiguredProviderConfigs(
chatProviderConfigsQuery.data,
chatModelsQuery.data,
chatModelAvailabilityQuery.data,
)
: undefined;
const modelCount =
chatModelConfigsQuery.isSuccess && chatModelsQuery.isSuccess
chatModelsQuery.isSuccess && chatModelAvailabilityQuery.isSuccess
? catalogModelOptions.length
: undefined;
const unsupportedProviderNames = getUnsupportedProviderNames(
chatModelsQuery.data,
chatModelAvailabilityQuery.data,
);
const handleCreateChat = async ({
@@ -165,16 +165,16 @@ const AgentCreatePage: FC = () => {
isCreating={createMutation.isPending}
createError={createMutation.error}
canCreateChat={permissions.createChat}
modelCatalog={chatModelsQuery.data}
modelCatalog={chatModelAvailabilityQuery.data}
modelOptions={catalogModelOptions}
canConfigureAgentSetup={permissions.editDeploymentConfig}
providerCount={providerCount}
modelCount={modelCount}
unsupportedProviderNames={unsupportedProviderNames}
aiGatewayDisabled={aiGatewayDisabled}
modelConfigs={chatModelConfigsQuery.data ?? []}
models={chatModelsQuery.data ?? []}
isModelCatalogLoading={isModelCatalogLoading}
isModelConfigsLoading={chatModelConfigsQuery.isLoading}
isModelsLoading={chatModelsQuery.isLoading}
rootPersonalModelOverride={rootPersonalModelOverride}
isPersonalModelOverridesLoading={personalModelOverridesQuery.isLoading}
workspaceCount={workspacesQuery.data?.count}
@@ -1,10 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, fn, userEvent, waitFor, within } from "storybook/test";
import type {
ChatModelConfig,
UserChatProviderConfig,
} from "#/api/typesGenerated";
import { MockChatModelConfig } from "#/testHelpers/chatModels";
import type { ChatModel, UserChatProviderConfig } from "#/api/typesGenerated";
import { MockChatModel } from "#/testHelpers/chatModels";
import {
AgentSettingsAPIKeysPageView,
type AgentSettingsAPIKeysPageViewProps,
@@ -25,10 +22,10 @@ const createProvider = (
});
const createModel = (
overrides: Partial<ChatModelConfig> &
Pick<ChatModelConfig, "id" | "ai_provider_id" | "model">,
): ChatModelConfig => ({
...MockChatModelConfig,
overrides: Partial<ChatModel> &
Pick<ChatModel, "id" | "ai_provider_id" | "model">,
): ChatModel => ({
...MockChatModel,
created_at: "2026-03-01T00:00:00.000Z",
updated_at: "2026-03-01T00:00:00.000Z",
...overrides,
@@ -4,7 +4,7 @@ import { useMutation, useQuery, useQueryClient } from "react-query";
import { toast } from "sonner";
import { getErrorDetail, getErrorMessage } from "#/api/errors";
import {
chatModelConfigs,
chatModels,
deleteUserChatProviderKey,
upsertUserChatProviderKey,
userChatProviderConfigs,
@@ -26,7 +26,7 @@ const AgentSettingsAPIKeysPage: FC = () => {
>({});
const providersQuery = useQuery(userChatProviderConfigs());
const modelsQuery = useQuery(chatModelConfigs());
const modelsQuery = useQuery(chatModels());
const upsertMutationOptions = upsertUserChatProviderKey(queryClient);
const upsertMutation = useMutation({
@@ -1,9 +1,6 @@
import type { FC, FormEvent } from "react";
import { useId, useState } from "react";
import type {
ChatModelConfig,
UserChatProviderConfig,
} from "#/api/typesGenerated";
import type { ChatModel, UserChatProviderConfig } from "#/api/typesGenerated";
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
import { Badge } from "#/components/Badge/Badge";
import { Button } from "#/components/Button/Button";
@@ -56,7 +53,7 @@ const getProviderStatus = (
interface ProviderKeyPanelProps {
provider: UserChatProviderConfig;
models: readonly ChatModelConfig[];
models: readonly ChatModel[];
isModelsLoading: boolean;
areModelsUnavailable: boolean;
isSaving: boolean;
@@ -246,7 +243,7 @@ export interface AgentSettingsAPIKeysPageViewProps {
error: unknown;
isLoading: boolean;
providerItems: readonly AgentSettingsAPIKeysProviderItem[];
models: readonly ChatModelConfig[];
models: readonly ChatModel[];
isModelsLoading: boolean;
areModelsUnavailable: boolean;
onSave: (providerConfigId: string, apiKey: string) => void;
@@ -1,7 +1,7 @@
import type { FC } from "react";
import { useMutation, useQuery, useQueryClient } from "react-query";
import {
chatModelConfigs,
chatModels,
deleteUserCompactionThreshold,
updateUserCompactionThreshold,
userChatProviderConfigs,
@@ -12,7 +12,7 @@ import { providerTypeByIDFromUserConfigs } from "./utils/modelOptions";
const AgentSettingsCompactionPage: FC = () => {
const queryClient = useQueryClient();
const modelConfigsQuery = useQuery(chatModelConfigs());
const modelsQuery = useQuery(chatModels());
const providerConfigsQuery = useQuery(userChatProviderConfigs());
const thresholdsQuery = useQuery(userCompactionThresholds());
const saveThresholdMutation = useMutation(
@@ -22,17 +22,14 @@ const AgentSettingsCompactionPage: FC = () => {
deleteUserCompactionThreshold(queryClient),
);
const handleSaveThreshold = (
modelConfigId: string,
thresholdPercent: number,
) =>
const handleSaveThreshold = (modelId: string, thresholdPercent: number) =>
saveThresholdMutation.mutateAsync({
modelConfigId,
modelId,
req: { threshold_percent: thresholdPercent },
});
const handleResetThreshold = (modelConfigId: string) =>
resetThresholdMutation.mutateAsync(modelConfigId);
const handleResetThreshold = (modelId: string) =>
resetThresholdMutation.mutateAsync(modelId);
const providerTypeByID = providerTypeByIDFromUserConfigs(
providerConfigsQuery.data,
@@ -40,10 +37,10 @@ const AgentSettingsCompactionPage: FC = () => {
return (
<AgentSettingsCompactionPageView
modelConfigsData={modelConfigsQuery.data}
models={modelsQuery.data}
providerTypeByID={providerTypeByID}
modelConfigsError={modelConfigsQuery.error}
isLoadingModelConfigs={modelConfigsQuery.isLoading}
modelsError={modelsQuery.error}
isLoadingModels={modelsQuery.isLoading}
thresholds={thresholdsQuery.data?.thresholds}
isThresholdsLoading={thresholdsQuery.isLoading}
thresholdsError={thresholdsQuery.error}
@@ -7,7 +7,7 @@ import {
} from "./AgentSettingsCompactionPageView";
const baseArgs: AgentSettingsCompactionPageViewProps = {
modelConfigsData: [
models: [
{
id: "model-config-1",
ai_provider_id: "prov-openai",
@@ -20,10 +20,10 @@ const baseArgs: AgentSettingsCompactionPageViewProps = {
created_at: "2026-03-12T12:00:00.000Z",
updated_at: "2026-03-12T12:00:00.000Z",
},
] as TypesGen.ChatModelConfig[],
] as TypesGen.ChatModel[],
providerTypeByID: new Map<string, string>([["prov-openai", "openai"]]),
modelConfigsError: undefined,
isLoadingModelConfigs: false,
modelsError: undefined,
isLoadingModels: false,
thresholds: [
{
model_config_id: "model-config-1",
@@ -4,27 +4,27 @@ import { SectionHeader } from "./components/SectionHeader";
import { UserCompactionThresholdSettings } from "./components/UserCompactionThresholdSettings";
export interface AgentSettingsCompactionPageViewProps {
modelConfigsData: TypesGen.ChatModelConfig[] | undefined;
models: TypesGen.ChatModel[] | undefined;
providerTypeByID: ReadonlyMap<string, string>;
modelConfigsError: unknown;
isLoadingModelConfigs: boolean;
modelsError: unknown;
isLoadingModels: boolean;
thresholds: readonly TypesGen.UserChatCompactionThreshold[] | undefined;
isThresholdsLoading: boolean;
thresholdsError: unknown;
onSaveThreshold: (
modelConfigId: string,
modelId: string,
thresholdPercent: number,
) => Promise<unknown>;
onResetThreshold: (modelConfigId: string) => Promise<unknown>;
onResetThreshold: (modelId: string) => Promise<unknown>;
}
export const AgentSettingsCompactionPageView: FC<
AgentSettingsCompactionPageViewProps
> = ({
modelConfigsData,
models,
providerTypeByID,
modelConfigsError,
isLoadingModelConfigs,
modelsError,
isLoadingModels,
thresholds,
isThresholdsLoading,
thresholdsError,
@@ -38,10 +38,10 @@ export const AgentSettingsCompactionPageView: FC<
description="Customize when conversations with models are automatically compacted."
/>
<UserCompactionThresholdSettings
modelConfigs={modelConfigsData ?? []}
models={models ?? []}
providerTypeByID={providerTypeByID}
modelConfigsError={modelConfigsError}
isLoadingModelConfigs={isLoadingModelConfigs}
modelsError={modelsError}
isLoadingModels={isLoadingModels}
thresholds={thresholds}
isThresholdsLoading={isThresholdsLoading}
thresholdsError={thresholdsError}
@@ -1,7 +1,7 @@
import type { FC } from "react";
import { useMutation, useQuery, useQueryClient } from "react-query";
import {
chatModelConfigs,
chatModelAvailability,
chatModels,
updateUserChatPersonalModelOverride,
userChatPersonalModelOverrides,
@@ -14,8 +14,8 @@ import { resolveModelSelector } from "./utils/modelOptions";
const AgentSettingsUserAgentsPage: FC = () => {
const queryClient = useQueryClient();
const overridesQuery = useQuery(userChatPersonalModelOverrides());
const chatModelsQuery = useQuery(chatModels());
const modelConfigsQuery = useQuery(chatModelConfigs());
const chatModelAvailabilityQuery = useQuery(chatModelAvailability());
const modelsQuery = useQuery(chatModels());
const providerConfigsQuery = useQuery(userChatProviderConfigs());
const saveRootModelOverrideMutation = useMutation(
updateUserChatPersonalModelOverride(queryClient),
@@ -27,11 +27,11 @@ const AgentSettingsUserAgentsPage: FC = () => {
updateUserChatPersonalModelOverride(queryClient),
);
const { options: modelOptions, isModelCatalogLoading } = resolveModelSelector(
modelConfigsQuery,
chatModelsQuery,
modelsQuery,
chatModelAvailabilityQuery,
providerConfigsQuery,
);
const modelConfigsError = modelConfigsQuery.error ?? chatModelsQuery.error;
const modelsError = modelsQuery.error ?? chatModelAvailabilityQuery.error;
const saveModelOverride = (
context: TypesGen.ChatPersonalModelOverrideContext,
@@ -55,8 +55,8 @@ const AgentSettingsUserAgentsPage: FC = () => {
isRetryingOverrides={overridesQuery.isFetching}
isLoadingOverrides={overridesQuery.isLoading}
modelOptions={modelOptions}
modelConfigs={modelConfigsQuery.data ?? []}
modelConfigsError={modelConfigsError}
models={modelsQuery.data ?? []}
modelsError={modelsError}
isLoadingModels={isModelCatalogLoading}
onSaveRootModelOverride={saveModelOverride(
"root",
@@ -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 {
AgentSettingsUserAgentsPageView,
type AgentSettingsUserAgentsPageViewProps,
@@ -14,9 +14,9 @@ const UNAVAILABLE_WARNING =
"The saved model is unavailable and will be ignored until you choose a valid model override.";
const buildModelConfig = (
overrides: Partial<TypesGen.ChatModelConfig> = {},
): TypesGen.ChatModelConfig => ({
...MockChatModelConfig,
overrides: Partial<TypesGen.ChatModel> = {},
): TypesGen.ChatModel => ({
...MockChatModel,
id: "model-default",
model: "gpt-4.1-mini",
display_name: "GPT 4.1 Mini",
@@ -94,7 +94,7 @@ const inaccessibleModelConfig = buildModelConfig({
display_name: "Bedrock Claude",
});
const modelConfigs = [
const models = [
defaultModelConfig,
claudeModelConfig,
reasoningModelConfig,
@@ -157,8 +157,8 @@ const buildArgs = (
isRetryingOverrides: false,
isLoadingOverrides: false,
modelOptions,
modelConfigs,
modelConfigsError: undefined,
models,
modelsError: undefined,
isLoadingModels: false,
onSaveRootModelOverride: fn(),
isSavingRootModelOverride: false,
@@ -559,9 +559,9 @@ export const UnavailableSavedModels: Story = {
},
};
export const ModelConfigsError: Story = {
export const ModelsError: Story = {
args: buildArgs({
modelConfigsError: new Error("Failed to load model configs."),
modelsError: new Error("Failed to load models."),
overridesData: buildOverridesResponse({
root: buildOverride("root", {
mode: "model",
@@ -593,7 +593,7 @@ export const ModelConfigsError: Story = {
for (const section of [rootSection, generalSection, exploreSection]) {
expect(
within(section).getByText("Failed to load model configs."),
within(section).getByText("Failed to load models."),
).toBeInTheDocument();
expect(
within(section).getByRole("combobox", { name: /behavior/i }),
@@ -17,8 +17,8 @@ export interface AgentSettingsUserAgentsPageViewProps {
isRetryingOverrides?: boolean;
isLoadingOverrides: boolean;
modelOptions: readonly ModelSelectorOption[];
modelConfigs: readonly TypesGen.ChatModelConfig[];
modelConfigsError: unknown;
models: readonly TypesGen.ChatModel[];
modelsError: unknown;
isLoadingModels: boolean;
onSaveRootModelOverride: SavePersonalOverride;
isSavingRootModelOverride: boolean;
@@ -40,8 +40,8 @@ export const AgentSettingsUserAgentsPageView: FC<
isRetryingOverrides = false,
isLoadingOverrides,
modelOptions,
modelConfigs,
modelConfigsError,
models,
modelsError,
isLoadingModels,
onSaveRootModelOverride,
isSavingRootModelOverride,
@@ -93,8 +93,8 @@ export const AgentSettingsUserAgentsPageView: FC<
description="Choose the model behavior for new root agents."
overrideData={overridesData?.root}
modelOptions={modelOptions}
modelConfigs={modelConfigs}
modelConfigsError={modelConfigsError}
models={models}
modelsError={modelsError}
isLoading={isLoading}
onSave={onSaveRootModelOverride}
isSaving={isSavingRootModelOverride}
@@ -109,8 +109,8 @@ export const AgentSettingsUserAgentsPageView: FC<
overrideData={overridesData?.general}
deploymentDefault={overridesData?.deployment_defaults.general}
modelOptions={modelOptions}
modelConfigs={modelConfigs}
modelConfigsError={modelConfigsError}
models={models}
modelsError={modelsError}
isLoading={isLoading}
onSave={onSaveGeneralModelOverride}
isSaving={isSavingGeneralModelOverride}
@@ -125,8 +125,8 @@ export const AgentSettingsUserAgentsPageView: FC<
overrideData={overridesData?.explore}
deploymentDefault={overridesData?.deployment_defaults.explore}
modelOptions={modelOptions}
modelConfigs={modelConfigs}
modelConfigsError={modelConfigsError}
models={models}
modelsError={modelsError}
isLoading={isLoading}
onSave={onSaveExploreModelOverride}
isSaving={isSavingExploreModelOverride}
@@ -57,11 +57,11 @@ import {
} from "./components/ChatsSidebar/sidebarWidth";
import { ChatTopBar } from "./components/ChatTopBar";
const defaultModelConfigID = "model-config-1";
const defaultModelID = "model-config-1";
const defaultModelConfigs: TypesGen.ChatModelConfig[] = [
const defaultModels: TypesGen.ChatModel[] = [
{
id: defaultModelConfigID,
id: defaultModelID,
ai_provider_id: "provider-openai",
model: "gpt-4o",
display_name: "GPT-4o",
@@ -97,7 +97,7 @@ const buildChat = (overrides: Partial<Chat> = {}): Chat => ({
owner_id: "owner-1",
owner_username: "owner",
owner_name: undefined,
last_model_config_id: defaultModelConfigs[0].id,
last_model_config_id: defaultModels[0].id,
created_at: oneWeekAgo,
updated_at: oneWeekAgo,
...overrides,
@@ -119,11 +119,11 @@ const AgentsRouteElement = () => (
model_config_id: "",
is_malformed: false,
}}
modelConfigsData={[]}
models={[]}
providerInfoByID={new Map()}
modelConfigsError={undefined}
isLoadingModelConfigs={false}
isFetchingModelConfigs={false}
modelsError={undefined}
isLoadingModels={false}
isFetchingModels={false}
onSaveTitleGenerationModel={fn()}
isSavingTitleGenerationModel={false}
isSaveTitleGenerationModelError={false}
@@ -347,7 +347,7 @@ const meta: Meta<typeof AgentsPageLayout> = {
custom_prompt: "",
});
// Mocks for child route pages that fetch their own data.
spyOn(API.experimental, "getChatModels").mockResolvedValue({
spyOn(API.experimental, "getChatModelAvailability").mockResolvedValue({
providers: [
{
provider: "openai",
@@ -364,9 +364,9 @@ const meta: Meta<typeof AgentsPageLayout> = {
],
unsupported_providers: [],
});
spyOn(API.experimental, "getChatModelConfigs").mockResolvedValue([
spyOn(API.experimental, "getChatModels").mockResolvedValue([
{
id: defaultModelConfigID,
id: defaultModelID,
ai_provider_id: "provider-openai",
model: "gpt-4o",
display_name: "GPT-4o",
@@ -423,7 +423,7 @@ const meta: Meta<typeof AgentsPageLayout> = {
API.experimental,
"updateUserChatCompactionThreshold",
).mockResolvedValue({
model_config_id: defaultModelConfigID,
model_config_id: defaultModelID,
threshold_percent: 70,
});
spyOn(
@@ -1047,7 +1047,7 @@ const watchedChat = (overrides: Partial<Chat> = {}): Chat => ({
...MockChat,
id: WATCHED_CHAT_ID,
title: "Watched agent",
last_model_config_id: defaultModelConfigID,
last_model_config_id: defaultModelID,
created_at: oneWeekAgo,
updated_at: oneWeekAgo,
...overrides,
@@ -24,7 +24,7 @@ import {
cancelChatListRefetches,
cancelLoadedChatEntityRefetch,
chatEntityKey,
chatModelConfigs,
chatModelAvailability,
chatModels,
infiniteChats,
invalidateChatCostTree,
@@ -89,7 +89,7 @@ import {
} from "./utils/agentWorkspaceUtils";
import { maybePlayChime } from "./utils/chime";
import {
getModelOptionsFromConfigs,
getModelOptionsFromModels,
providerInfoByIDFromUserConfigs,
} from "./utils/modelOptions";
import { clearPersistedRightPanelState } from "./utils/rightPanelTabStorage";
@@ -236,8 +236,8 @@ const AgentsPageLayout: FC = () => {
// model info alongside each chat. Child routes that need models
// subscribe to the same queries independently, and react-query
// deduplicates the requests.
const chatModelAvailabilityQuery = useQuery(chatModelAvailability());
const chatModelsQuery = useQuery(chatModels());
const chatModelConfigsQuery = useQuery(chatModelConfigs());
const chatProviderConfigsQuery = useQuery(userChatProviderConfigs());
const personalModelOverridesQuery = useQuery(
userChatPersonalModelOverrides(),
@@ -388,9 +388,9 @@ const AgentsPageLayout: FC = () => {
},
});
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const catalogModelOptions = getModelOptionsFromConfigs(
chatModelConfigsQuery.data,
const catalogModelOptions = getModelOptionsFromModels(
chatModelsQuery.data,
chatModelAvailabilityQuery.data,
providerInfoByIDFromUserConfigs(chatProviderConfigsQuery.data),
);
const chatList = chatsQuery.data?.pages.flat() ?? [];
@@ -769,7 +769,7 @@ const AgentsPageLayout: FC = () => {
currentUserId={user.id}
chatErrorReasons={sidebarChatErrorReasons}
modelOptions={catalogModelOptions}
modelConfigs={chatModelConfigsQuery.data ?? []}
models={chatModelsQuery.data ?? []}
onArchiveAgent={requestArchiveAgent}
onUnarchiveAgent={requestUnarchiveAgent}
onArchiveAndDeleteWorkspace={requestArchiveAndDeleteWorkspace}
@@ -3,7 +3,7 @@ import { type FC, useId } from "react";
import { getErrorMessage } from "#/api/errors";
import type {
AdvisorConfig,
ChatModelConfig,
ChatModel,
UpdateAdvisorConfigRequest,
} from "#/api/typesGenerated";
import { Button } from "#/components/Button/Button";
@@ -26,11 +26,11 @@ interface AdvisorSettingsProps {
isAdvisorConfigLoading: boolean;
isAdvisorConfigFetching: boolean;
isAdvisorConfigLoadError: boolean;
enabledModelConfigs: readonly ChatModelConfig[];
enabledModels: readonly ChatModel[];
providerInfoByID: ReadonlyMap<string, ProviderInfo>;
modelConfigsError: unknown;
isLoadingModelConfigs: boolean;
isFetchingModelConfigs: boolean;
modelsError: unknown;
isLoadingModels: boolean;
isFetchingModels: boolean;
onSaveAdvisorConfig: (
req: UpdateAdvisorConfigRequest,
options?: MutationCallbacks,
@@ -120,11 +120,11 @@ export const AdvisorSettings: FC<AdvisorSettingsProps> = ({
isAdvisorConfigLoading,
isAdvisorConfigFetching,
isAdvisorConfigLoadError,
enabledModelConfigs,
enabledModels,
providerInfoByID,
modelConfigsError,
isLoadingModelConfigs,
isFetchingModelConfigs,
modelsError,
isLoadingModels,
isFetchingModels,
onSaveAdvisorConfig,
isSavingAdvisorConfig,
isSaveAdvisorConfigError,
@@ -134,7 +134,7 @@ export const AdvisorSettings: FC<AdvisorSettingsProps> = ({
const maxOutputTokensId = useId();
const { isSavedVisible, showSavedState } = useTemporarySavedState();
const hasLoadedAdvisorConfig = advisorConfigData !== undefined;
const enabledModelOptions = enabledModelConfigs.map((config) => {
const enabledModelOptions = enabledModels.map((config) => {
const providerInfo = providerInfoByID.get(config.ai_provider_id);
const reasoningEffort = config.model_config?.reasoning_effort;
const reasoningEfforts = config.reasoning_efforts ?? [];
@@ -168,12 +168,10 @@ export const AdvisorSettings: FC<AdvisorSettingsProps> = ({
let source = values;
if (
!isUnsetModelConfigId(source.model_config_id) &&
!isLoadingModelConfigs &&
!isFetchingModelConfigs &&
!modelConfigsError &&
!enabledModelConfigs.some(
(config) => config.id === source.model_config_id,
)
!isLoadingModels &&
!isFetchingModels &&
!modelsError &&
!enabledModels.some((config) => config.id === source.model_config_id)
) {
source = { ...source, model_config_id: "", reasoning_effort: "" };
}
@@ -199,11 +197,7 @@ export const AdvisorSettings: FC<AdvisorSettingsProps> = ({
submitOption.reasoningEffortDefault,
) ?? "",
};
} else if (
!isLoadingModelConfigs &&
!isFetchingModelConfigs &&
!modelConfigsError
) {
} else if (!isLoadingModels && !isFetchingModels && !modelsError) {
source = { ...source, reasoning_effort: "" };
}
}
@@ -224,7 +218,7 @@ export const AdvisorSettings: FC<AdvisorSettingsProps> = ({
isAdvisorConfigFetching ||
!hasLoadedAdvisorConfig;
const isModelSelectDisabled =
isFormDisabled || isLoadingModelConfigs || Boolean(modelConfigsError);
isFormDisabled || isLoadingModels || Boolean(modelsError);
const selectedModelOption = enabledModelOptions.find(
(option) => option.id === form.values.model_config_id,
);
@@ -236,7 +230,7 @@ export const AdvisorSettings: FC<AdvisorSettingsProps> = ({
)
: undefined;
const hasUnavailableSelectedModel =
!isLoadingModelConfigs &&
!isLoadingModels &&
!isUnsetModelConfigId(form.values.model_config_id) &&
selectedModelOption === undefined;
const canSave = hasLoadedAdvisorConfig && form.dirty && form.isValid;
@@ -317,9 +311,7 @@ export const AdvisorSettings: FC<AdvisorSettingsProps> = ({
}
unsetLabel="Use chat model"
emptyMessage={
isLoadingModelConfigs
? "Loading models..."
: "No enabled models found."
isLoadingModels ? "Loading models..." : "No enabled models found."
}
className="h-10 w-[22rem] max-w-full justify-between rounded-md border border-border border-solid bg-transparent px-3 text-sm"
contentClassName="min-w-[18rem]"
@@ -18,11 +18,11 @@ import {
} from "./AgentChatInput";
import type { ChatMessageInputRef } from "./ChatMessageInput/ChatMessageInput";
const defaultModelConfigID = "model-config-1";
const defaultModelID = "model-config-1";
const defaultModelOptions = [
{
id: defaultModelConfigID,
id: defaultModelID,
provider: "openai",
model: "gpt-4o",
displayName: "GPT-4o",
@@ -15,7 +15,7 @@ import { API } from "#/api/api";
import { permittedOrganizationsKey } from "#/api/queries/organizations";
import type * as TypesGen from "#/api/typesGenerated";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { MockChatModelConfig } from "#/testHelpers/chatModels";
import { MockChatModel } from "#/testHelpers/chatModels";
import {
MockDefaultOrganization,
MockOrganization2,
@@ -36,12 +36,12 @@ const permittedOrgsKey = permittedOrganizationsKey({
action: "create",
});
const modelConfigID = "model-config-1";
const modelID = "model-config-1";
const claudeModelConfigID = "model-config-claude";
const modelOptions = [
{
id: modelConfigID,
id: modelID,
provider: "openai",
model: "gpt-4o",
displayName: "GPT-4o",
@@ -55,10 +55,10 @@ const modelOptions = [
] as const;
const buildModelConfig = (
overrides: Partial<TypesGen.ChatModelConfig> = {},
): TypesGen.ChatModelConfig => ({
...MockChatModelConfig,
id: modelConfigID,
overrides: Partial<TypesGen.ChatModel> = {},
): TypesGen.ChatModel => ({
...MockChatModel,
id: modelID,
model: "gpt-4o",
display_name: "GPT-4o",
created_at: "2026-02-18T00:00:00.000Z",
@@ -66,7 +66,7 @@ const buildModelConfig = (
...overrides,
});
const defaultModelConfigs: TypesGen.ChatModelConfig[] = [
const defaultModels: TypesGen.ChatModel[] = [
buildModelConfig({ is_default: true }),
buildModelConfig({
id: claudeModelConfigID,
@@ -120,8 +120,8 @@ const meta: Meta<typeof AgentCreateForm> = {
modelCatalog: null,
modelOptions: [...modelOptions],
isModelCatalogLoading: false,
modelConfigs: [],
isModelConfigsLoading: false,
models: [],
isModelsLoading: false,
workspaceCount: 0,
workspaceOptions: [],
workspacesError: undefined,
@@ -182,7 +182,7 @@ export const RootPersonalModelOverrideModelSelected: Story = {
args: {
...defaultArgs,
onCreateChat: fn().mockResolvedValue(undefined),
modelConfigs: defaultModelConfigs,
models: defaultModels,
rootPersonalModelOverride: buildRootPersonalModelOverride({
mode: "model",
model_config_id: claudeModelConfigID,
@@ -205,7 +205,7 @@ export const RootChatDefaultSubmitsDisplayedModel: Story = {
args: {
...defaultArgs,
onCreateChat: fn().mockResolvedValue(undefined),
modelConfigs: defaultModelConfigs,
models: defaultModels,
rootPersonalModelOverride: buildRootPersonalModelOverride({
mode: "chat_default",
model_config_id: "",
@@ -220,7 +220,7 @@ export const RootChatDefaultSubmitsDisplayedModel: Story = {
await waitFor(() => {
expect(args.onCreateChat).toHaveBeenCalled();
});
expect(getCreateOptions(args.onCreateChat).model).toBe(modelConfigID);
expect(getCreateOptions(args.onCreateChat).model).toBe(modelID);
},
};
@@ -228,7 +228,7 @@ export const RootOverrideMissingFromCatalog: Story = {
args: {
...defaultArgs,
onCreateChat: fn().mockResolvedValue(undefined),
modelConfigs: defaultModelConfigs,
models: defaultModels,
rootPersonalModelOverride: buildRootPersonalModelOverride({
mode: "model",
model_config_id: "model-does-not-exist",
@@ -245,7 +245,7 @@ export const RootOverrideMissingFromCatalog: Story = {
await waitFor(() => {
expect(args.onCreateChat).toHaveBeenCalled();
});
expect(getCreateOptions(args.onCreateChat).model).toBe(modelConfigID);
expect(getCreateOptions(args.onCreateChat).model).toBe(modelID);
},
};
@@ -253,7 +253,7 @@ export const MalformedRootOverrideUsesDefaultModel: Story = {
args: {
...defaultArgs,
onCreateChat: fn().mockResolvedValue(undefined),
modelConfigs: defaultModelConfigs,
models: defaultModels,
rootPersonalModelOverride: buildRootPersonalModelOverride({
mode: "model",
model_config_id: claudeModelConfigID,
@@ -272,7 +272,7 @@ export const MalformedRootOverrideUsesDefaultModel: Story = {
await waitFor(() => {
expect(args.onCreateChat).toHaveBeenCalled();
});
expect(getCreateOptions(args.onCreateChat).model).toBe(modelConfigID);
expect(getCreateOptions(args.onCreateChat).model).toBe(modelID);
},
};
@@ -280,7 +280,7 @@ export const LastUsedModelFallbackWithoutRootOverride: Story = {
args: {
...defaultArgs,
onCreateChat: fn().mockResolvedValue(undefined),
modelConfigs: defaultModelConfigs,
models: defaultModels,
},
beforeEach: () => {
localStorage.clear();
@@ -303,7 +303,7 @@ export const ManualSelectionOverridesRootChatDefault: Story = {
args: {
...defaultArgs,
onCreateChat: fn().mockResolvedValue(undefined),
modelConfigs: defaultModelConfigs,
models: defaultModels,
rootPersonalModelOverride: buildRootPersonalModelOverride({
mode: "chat_default",
model_config_id: "",
@@ -354,7 +354,7 @@ export const RemembersReasoningEffortByModel: Story = {
},
beforeEach: () => {
localStorage.clear();
saveReasoningEffortForModel(modelConfigID, "high");
saveReasoningEffortForModel(modelID, "high");
saveReasoningEffortForModel(claudeModelConfigID, "medium");
},
play: async ({ canvasElement }) => {
@@ -386,7 +386,7 @@ export const RemembersReasoningEffortByModel: Story = {
restoredSlider.focus();
await userEvent.keyboard("{ArrowRight}");
await waitFor(() => {
expect(getReasoningEffortForModel(modelConfigID)).toBe("xhigh");
expect(getReasoningEffortForModel(modelID)).toBe("xhigh");
});
await userEvent.keyboard("{Escape}");
},
@@ -397,16 +397,16 @@ export const PersistedReasoningEffortOutranksRootOverride: Story = {
...defaultArgs,
onCreateChat: fn().mockResolvedValue(undefined),
modelOptions: [...effortModelOptions],
modelConfigs: defaultModelConfigs,
models: defaultModels,
rootPersonalModelOverride: buildRootPersonalModelOverride({
mode: "model",
model_config_id: modelConfigID,
model_config_id: modelID,
reasoning_effort: "high",
}),
},
beforeEach: () => {
localStorage.clear();
saveReasoningEffortForModel(modelConfigID, "low");
saveReasoningEffortForModel(modelID, "low");
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
@@ -432,10 +432,10 @@ export const ManualReselectKeepsRootOverrideEffort: Story = {
args: {
...defaultArgs,
modelOptions: [...effortModelOptions],
modelConfigs: defaultModelConfigs,
models: defaultModels,
rootPersonalModelOverride: buildRootPersonalModelOverride({
mode: "model",
model_config_id: modelConfigID,
model_config_id: modelID,
reasoning_effort: "high",
}),
},
@@ -466,16 +466,16 @@ export const StalePersistedEffortFallsThroughToRootOverride: Story = {
reasoningEfforts: ["low", "medium"],
},
],
modelConfigs: defaultModelConfigs,
models: defaultModels,
rootPersonalModelOverride: buildRootPersonalModelOverride({
mode: "model",
model_config_id: modelConfigID,
model_config_id: modelID,
reasoning_effort: "medium",
}),
},
beforeEach: () => {
localStorage.clear();
saveReasoningEffortForModel(modelConfigID, "max");
saveReasoningEffortForModel(modelID, "max");
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
@@ -535,7 +535,7 @@ export const SubmitsReasoningEffort: Story = {
expect(args.onCreateChat).toHaveBeenCalled();
});
const options = getCreateOptions(args.onCreateChat);
expect(options.model).toBe(modelConfigID);
expect(options.model).toBe(modelID);
expect(options.reasoningEffort).toBe("high");
},
};
@@ -673,7 +673,7 @@ export const LoadingModelCatalog: Story = {
modelCatalog: null,
modelOptions: [],
isModelCatalogLoading: true,
isModelConfigsLoading: true,
isModelsLoading: true,
},
};
@@ -697,7 +697,7 @@ export const NoModelsConfigured: Story = {
modelCatalog: { providers: [], unsupported_providers: [] },
modelOptions: [],
isModelCatalogLoading: false,
isModelConfigsLoading: false,
isModelsLoading: false,
},
};
@@ -710,7 +710,7 @@ export const MissingProviderAndModelSetup: Story = {
modelCatalog: { providers: [], unsupported_providers: [] },
modelOptions: [],
isModelCatalogLoading: false,
isModelConfigsLoading: false,
isModelsLoading: false,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
@@ -128,7 +128,7 @@ interface AgentCreateFormProps {
isCreating: boolean;
createError: unknown;
canCreateChat: boolean;
modelCatalog: TypesGen.ChatModelsResponse | null | undefined;
modelCatalog: TypesGen.ChatModelAvailabilityResponse | null | undefined;
modelOptions: readonly ChatModelOption[];
canConfigureAgentSetup: boolean;
providerCount?: number;
@@ -136,8 +136,8 @@ interface AgentCreateFormProps {
unsupportedProviderNames?: readonly string[];
aiGatewayDisabled?: boolean;
isModelCatalogLoading: boolean;
modelConfigs: readonly TypesGen.ChatModelConfig[];
isModelConfigsLoading: boolean;
models: readonly TypesGen.ChatModel[];
isModelsLoading: boolean;
rootPersonalModelOverride?: TypesGen.ChatPersonalModelOverride;
isPersonalModelOverridesLoading?: boolean;
workspaceCount: number | undefined;
@@ -159,9 +159,9 @@ export const AgentCreateForm: FC<AgentCreateFormProps> = ({
modelCount,
unsupportedProviderNames,
aiGatewayDisabled,
modelConfigs,
models,
isModelCatalogLoading,
isModelConfigsLoading,
isModelsLoading,
rootPersonalModelOverride,
isPersonalModelOverridesLoading = false,
workspaceCount: _workspaceCount,
@@ -190,8 +190,8 @@ export const AgentCreateForm: FC<AgentCreateFormProps> = ({
? initialLastModelConfigID
: "";
const defaultModelID = (() => {
const defaultModelConfig = Array.isArray(modelConfigs)
? modelConfigs.find((config) => config.is_default)
const defaultModelConfig = Array.isArray(models)
? models.find((config) => config.is_default)
: undefined;
if (!defaultModelConfig) {
return "";
@@ -392,7 +392,7 @@ export const AgentCreateForm: FC<AgentCreateFormProps> = ({
if (!initialLastModelConfigID) {
return;
}
if (isModelCatalogLoading || isModelConfigsLoading) {
if (isModelCatalogLoading || isModelsLoading) {
return;
}
if (lastUsedModelID) {
@@ -402,7 +402,7 @@ export const AgentCreateForm: FC<AgentCreateFormProps> = ({
}, [
initialLastModelConfigID,
isModelCatalogLoading,
isModelConfigsLoading,
isModelsLoading,
lastUsedModelID,
]);
@@ -10,7 +10,7 @@ import type React from "react";
import { useState } from "react";
import { useQuery } from "react-query";
import { Link, useLocation } from "react-router";
import { chatModelConfigs } from "#/api/queries/chats";
import { chatModels } from "#/api/queries/chats";
import { ScrollArea } from "#/components/ScrollArea/ScrollArea";
import { safeBuildAgentChatPath } from "../../../utils/navigation";
import { Response } from "../Response";
@@ -193,15 +193,15 @@ export const SubagentTool: React.FC<{
const [expanded, setExpanded] = useState(false);
const { desktopChatId, onOpenDesktop } = useDesktopPanel();
const wantsModelDisplay =
descriptor.action === "spawn" && Boolean(descriptor.modelConfigId);
const modelConfigsQuery = useQuery({
...chatModelConfigs(),
descriptor.action === "spawn" && Boolean(descriptor.modelId);
const modelsQuery = useQuery({
...chatModels(),
enabled: wantsModelDisplay,
});
const modelDisplay: SpawnModelDisplay = wantsModelDisplay
? resolveSpawnModelDisplay({
configs: modelConfigsQuery.data,
modelConfigId: descriptor.modelConfigId,
models: modelsQuery.data,
modelId: descriptor.modelId,
reasoningEffort: descriptor.reasoningEffort,
})
: {};
@@ -1,11 +1,11 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, fn, userEvent, waitFor, within } from "storybook/test";
import { reactRouterParameters } from "storybook-addon-remix-react-router";
import { chatModelConfigsKey } from "#/api/queries/chats";
import { chatModelsKey } from "#/api/queries/chats";
import { workspaceBuildLogs } from "#/api/queries/workspaceBuilds";
import { workspaceByIdKey } from "#/api/queries/workspaces";
import type * as TypesGen from "#/api/typesGenerated";
import { MockChatModelConfig } from "#/testHelpers/chatModels";
import { MockChatModel } from "#/testHelpers/chatModels";
import { MockWorkspace, MockWorkspaceBuild } from "#/testHelpers/entities";
import { ChatWorkspaceContext } from "../../../context/ChatWorkspaceContext";
import { BlockList } from "../../ChatConversation/MessageBlocks";
@@ -858,8 +858,8 @@ export const SubagentMalformedChatIdLinksToRecoverableChatId: Story = {
},
};
const mockChatModelConfig = {
...MockChatModelConfig,
const mockChatModel = {
...MockChatModel,
id: "8b29eba2-53a9-4c9a-95bb-b0326ac0a2fe",
model: "claude-sonnet-4-6",
display_name: "Claude Sonnet 4.6",
@@ -885,7 +885,7 @@ export const SubagentSpawnWithModelAndEffort: Story = {
},
},
parameters: {
queries: [{ key: chatModelConfigsKey, data: [mockChatModelConfig] }],
queries: [{ key: chatModelsKey, data: [mockChatModel] }],
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
@@ -915,7 +915,7 @@ export const SubagentSpawnWithModelDefaultEffort: Story = {
},
},
parameters: {
queries: [{ key: chatModelConfigsKey, data: [mockChatModelConfig] }],
queries: [{ key: chatModelsKey, data: [mockChatModel] }],
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
@@ -947,7 +947,7 @@ export const SubagentSpawnWithEffortOnly: Story = {
},
},
parameters: {
queries: [{ key: chatModelConfigsKey, data: [mockChatModelConfig] }],
queries: [{ key: chatModelsKey, data: [mockChatModel] }],
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
@@ -975,7 +975,7 @@ export const SubagentSpawnWithUnknownModelConfig: Story = {
},
},
parameters: {
queries: [{ key: chatModelConfigsKey, data: [mockChatModelConfig] }],
queries: [{ key: chatModelsKey, data: [mockChatModel] }],
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
@@ -1,10 +1,10 @@
import { describe, expect, it } from "vitest";
import type { ChatModelConfig } from "#/api/typesGenerated";
import { MockChatModelConfig } from "#/testHelpers/chatModels";
import type { ChatModel } from "#/api/typesGenerated";
import { MockChatModel } from "#/testHelpers/chatModels";
import { resolveSpawnModelDisplay } from "./spawnModelDisplay";
const config = (overrides: Partial<ChatModelConfig>): ChatModelConfig => ({
...MockChatModelConfig,
const config = (overrides: Partial<ChatModel>): ChatModel => ({
...MockChatModel,
...overrides,
});
@@ -26,15 +26,15 @@ const noEffortModel = config({
describe("resolveSpawnModelDisplay", () => {
it("returns nothing without explicit args", () => {
expect(
resolveSpawnModelDisplay({ configs: [sonnet], modelConfigId: undefined }),
resolveSpawnModelDisplay({ models: [sonnet], modelId: undefined }),
).toEqual({});
});
it("resolves model and explicit effort", () => {
expect(
resolveSpawnModelDisplay({
configs: [sonnet],
modelConfigId: "cfg-sonnet",
models: [sonnet],
modelId: "cfg-sonnet",
reasoningEffort: "low",
}),
).toEqual({ modelLabel: "Claude Sonnet 4.6", effortLabel: "low" });
@@ -43,8 +43,8 @@ describe("resolveSpawnModelDisplay", () => {
it("falls back to the config default effort when not requested", () => {
expect(
resolveSpawnModelDisplay({
configs: [sonnet],
modelConfigId: "cfg-sonnet",
models: [sonnet],
modelId: "cfg-sonnet",
}),
).toEqual({ modelLabel: "Claude Sonnet 4.6", effortLabel: "medium" });
});
@@ -52,8 +52,8 @@ describe("resolveSpawnModelDisplay", () => {
it("clamps a requested effort above the config max", () => {
expect(
resolveSpawnModelDisplay({
configs: [sonnet],
modelConfigId: "cfg-sonnet",
models: [sonnet],
modelId: "cfg-sonnet",
reasoningEffort: "max",
}),
).toEqual({ modelLabel: "Claude Sonnet 4.6", effortLabel: "high" });
@@ -62,8 +62,8 @@ describe("resolveSpawnModelDisplay", () => {
it("omits effort for models without effort settings", () => {
expect(
resolveSpawnModelDisplay({
configs: [noEffortModel],
modelConfigId: "cfg-plain",
models: [noEffortModel],
modelId: "cfg-plain",
reasoningEffort: "high",
}),
).toEqual({ modelLabel: "GPT-4.1" });
@@ -72,8 +72,8 @@ describe("resolveSpawnModelDisplay", () => {
it("shows nothing when the config is unknown", () => {
expect(
resolveSpawnModelDisplay({
configs: [sonnet],
modelConfigId: "deleted-config",
models: [sonnet],
modelId: "deleted-config",
reasoningEffort: "xhigh",
}),
).toEqual({});
@@ -82,7 +82,7 @@ describe("resolveSpawnModelDisplay", () => {
it("shows nothing for effort-only spawns", () => {
expect(
resolveSpawnModelDisplay({
configs: [sonnet],
models: [sonnet],
reasoningEffort: "high",
}),
).toEqual({});
@@ -91,14 +91,14 @@ describe("resolveSpawnModelDisplay", () => {
it("omits an effective effort of none", () => {
expect(
resolveSpawnModelDisplay({
configs: [sonnet],
modelConfigId: "cfg-sonnet",
models: [sonnet],
modelId: "cfg-sonnet",
reasoningEffort: "none",
}),
).toEqual({ modelLabel: "Claude Sonnet 4.6" });
expect(
resolveSpawnModelDisplay({
configs: [sonnet],
models: [sonnet],
reasoningEffort: "none",
}),
).toEqual({});
@@ -107,13 +107,13 @@ describe("resolveSpawnModelDisplay", () => {
it("ignores values outside the global scale", () => {
expect(
resolveSpawnModelDisplay({
configs: [sonnet],
modelConfigId: "cfg-sonnet",
models: [sonnet],
modelId: "cfg-sonnet",
reasoningEffort: "ultra",
}),
).toEqual({ modelLabel: "Claude Sonnet 4.6", effortLabel: "medium" });
expect(
resolveSpawnModelDisplay({ configs: [sonnet], reasoningEffort: "ultra" }),
resolveSpawnModelDisplay({ models: [sonnet], reasoningEffort: "ultra" }),
).toEqual({});
});
@@ -125,17 +125,17 @@ describe("resolveSpawnModelDisplay", () => {
});
expect(
resolveSpawnModelDisplay({
configs: [blankName],
modelConfigId: "cfg-blank",
models: [blankName],
modelId: "cfg-blank",
}),
).toEqual({ modelLabel: "o4-mini" });
});
it("returns nothing while configs are still loading", () => {
it("returns nothing while models are still loading", () => {
expect(
resolveSpawnModelDisplay({
configs: undefined,
modelConfigId: "cfg-sonnet",
models: undefined,
modelId: "cfg-sonnet",
}),
).toEqual({});
});
@@ -30,25 +30,25 @@ export type SpawnModelDisplay = {
};
export const resolveSpawnModelDisplay = ({
configs,
modelConfigId,
models,
modelId,
reasoningEffort,
}: {
configs: readonly TypesGen.ChatModelConfig[] | undefined;
modelConfigId?: string;
models: readonly TypesGen.ChatModel[] | undefined;
modelId?: string;
reasoningEffort?: string;
}): SpawnModelDisplay => {
const requestedRank = effortRank(reasoningEffort);
const requested = requestedRank >= 0 ? effortScale[requestedRank] : undefined;
const config = modelConfigId
? configs?.find((c) => c.id === modelConfigId)
const model = modelId
? models?.find((model) => model.id === modelId)
: undefined;
if (!config) {
if (!model) {
return {};
}
const modelLabel = config.display_name.trim() || config.model.trim();
const effortConfig = config.model_config?.reasoning_effort;
const modelLabel = model.display_name.trim() || model.model.trim();
const effortConfig = model.model_config?.reasoning_effort;
if (!effortConfig) {
return { modelLabel };
}
@@ -13,7 +13,7 @@ export type SubagentDescriptor = {
fallbackTitle: string;
supportsDesktopAffordance: boolean;
/** Set only when the spawn args explicitly selected a model. */
modelConfigId?: string;
modelId?: string;
/** Set only when the spawn args explicitly pinned a reasoning effort. */
reasoningEffort?: string;
};
@@ -145,7 +145,7 @@ export const getSubagentDescriptor = ({
const title =
getProvidedSubagentTitle({ args: argsRecord, result: resultRecord }) ||
catalogEntry.fallbackTitle;
const modelConfigId =
const modelId =
action === "spawn" ? asString(argsRecord?.model_config_id).trim() : "";
const reasoningEffort =
action === "spawn"
@@ -159,7 +159,7 @@ export const getSubagentDescriptor = ({
title,
fallbackTitle: catalogEntry.fallbackTitle,
supportsDesktopAffordance: catalogEntry.supportsDesktopAffordance,
...(modelConfigId ? { modelConfigId } : {}),
...(modelId ? { modelId } : {}),
...(reasoningEffort ? { reasoningEffort } : {}),
};
};
@@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest";
import type { FieldSchema } from "#/api/chatModelOptions";
import type * as TypesGen from "#/api/typesGenerated";
import { MockChatModelConfig } from "#/testHelpers/chatModels";
import { MockChatModel } from "#/testHelpers/chatModels";
import {
buildInitialModelFormValues,
buildModelConfigFromForm,
@@ -81,8 +81,8 @@ function deepGet(obj: unknown, path: string[]): unknown {
return current;
}
const baseChatModelConfig: TypesGen.ChatModelConfig = {
...MockChatModelConfig,
const baseChatModel: TypesGen.ChatModel = {
...MockChatModel,
id: "test-id",
model: "gpt-4",
display_name: "GPT-4",
@@ -108,13 +108,13 @@ describe("buildInitialModelFormValues", () => {
});
it("preserves enabled=true when editing an enabled model", () => {
expect(buildInitialModelFormValues(baseChatModelConfig).enabled).toBe(true);
expect(buildInitialModelFormValues(baseChatModel).enabled).toBe(true);
});
it("preserves enabled=false when editing a disabled model", () => {
expect(
buildInitialModelFormValues({
...baseChatModelConfig,
...baseChatModel,
enabled: false,
}).enabled,
).toBe(false);
@@ -206,18 +206,18 @@ describe("parseThresholdInteger", () => {
describe("extractModelConfigFormState", () => {
it("returns empty form state when model_config is undefined", () => {
const result = extractModelConfigFormState(baseChatModelConfig);
const result = extractModelConfigFormState(baseChatModel);
expect(result).toEqual(emptyModelConfigFormState);
});
it("returns a copy, not a reference to emptyModelConfigFormState", () => {
const result = extractModelConfigFormState(baseChatModelConfig);
const result = extractModelConfigFormState(baseChatModel);
expect(result).not.toBe(emptyModelConfigFormState);
});
it("extracts top-level numeric fields", () => {
const model: TypesGen.ChatModelConfig = {
...baseChatModelConfig,
const model: TypesGen.ChatModel = {
...baseChatModel,
model_config: {
max_output_tokens: 4096,
temperature: 0.7,
@@ -237,8 +237,8 @@ describe("extractModelConfigFormState", () => {
});
it("extracts reasoning effort bounds", () => {
const model: TypesGen.ChatModelConfig = {
...baseChatModelConfig,
const model: TypesGen.ChatModel = {
...baseChatModel,
model_config: {
reasoning_effort: {
default: "medium",
@@ -252,8 +252,8 @@ describe("extractModelConfigFormState", () => {
});
it("extracts OpenAI provider options", () => {
const model: TypesGen.ChatModelConfig = {
...baseChatModelConfig,
const model: TypesGen.ChatModel = {
...baseChatModel,
model_config: {
provider_options: {
openai: {
@@ -278,8 +278,8 @@ describe("extractModelConfigFormState", () => {
});
it("extracts Anthropic provider options with thinking", () => {
const model: TypesGen.ChatModelConfig = {
...baseChatModelConfig,
const model: TypesGen.ChatModel = {
...baseChatModel,
model_config: {
provider_options: {
anthropic: {
@@ -298,8 +298,8 @@ describe("extractModelConfigFormState", () => {
});
it("extracts Anthropic 1M context window option", () => {
const model: TypesGen.ChatModelConfig = {
...baseChatModelConfig,
const model: TypesGen.ChatModel = {
...baseChatModel,
model_config: {
provider_options: {
anthropic: {
@@ -315,8 +315,8 @@ describe("extractModelConfigFormState", () => {
it("extracts Google provider options with safety settings", () => {
const safetySettings = [{ category: "harm", threshold: "block" }];
const model: TypesGen.ChatModelConfig = {
...baseChatModelConfig,
const model: TypesGen.ChatModel = {
...baseChatModel,
model_config: {
provider_options: {
google: {
@@ -339,8 +339,8 @@ describe("extractModelConfigFormState", () => {
});
it("returns empty string for google safety settings when absent", () => {
const model: TypesGen.ChatModelConfig = {
...baseChatModelConfig,
const model: TypesGen.ChatModel = {
...baseChatModel,
model_config: {
provider_options: {
google: {},
@@ -353,8 +353,8 @@ describe("extractModelConfigFormState", () => {
});
it("extracts OpenAI-compatible provider options", () => {
const model: TypesGen.ChatModelConfig = {
...baseChatModelConfig,
const model: TypesGen.ChatModel = {
...baseChatModel,
model_config: {
provider_options: {
openaicompat: {
@@ -369,8 +369,8 @@ describe("extractModelConfigFormState", () => {
});
it("extracts OpenRouter provider options", () => {
const model: TypesGen.ChatModelConfig = {
...baseChatModelConfig,
const model: TypesGen.ChatModel = {
...baseChatModel,
model_config: {
provider_options: {
openrouter: {
@@ -397,8 +397,8 @@ describe("extractModelConfigFormState", () => {
});
it("extracts Vercel provider options", () => {
const model: TypesGen.ChatModelConfig = {
...baseChatModelConfig,
const model: TypesGen.ChatModel = {
...baseChatModel,
model_config: {
provider_options: {
vercel: {
@@ -423,8 +423,8 @@ describe("extractModelConfigFormState", () => {
});
it("handles missing provider_options gracefully", () => {
const model: TypesGen.ChatModelConfig = {
...baseChatModelConfig,
const model: TypesGen.ChatModel = {
...baseChatModel,
model_config: {
temperature: 0.5,
},
@@ -441,7 +441,7 @@ describe("extractModelConfigFormState", () => {
});
it("returns deep copies of provider sub-objects", () => {
const result = extractModelConfigFormState(baseChatModelConfig);
const result = extractModelConfigFormState(baseChatModel);
const empty = emptyModelConfigFormState;
expect(result.openai).not.toBe(empty.openai);
expect(result.anthropic).not.toBe(empty.anthropic);
@@ -204,7 +204,7 @@ export const emptyModelConfigFormState: ModelConfigFormState = (() => {
// ── Extract form state from an existing API model ──────────────
export const extractModelConfigFormState = (
model: TypesGen.ChatModelConfig,
model: TypesGen.ChatModel,
): ModelConfigFormState => {
const config = model.model_config;
if (!config) {
@@ -250,7 +250,7 @@ export const extractModelConfigFormState = (
// ── Build initial model form values ────────────────────────────
export const buildInitialModelFormValues = (
sourceModel?: TypesGen.ChatModelConfig,
sourceModel?: TypesGen.ChatModel,
): ModelFormValues => ({
model: sourceModel?.model ?? "",
displayName: sourceModel?.display_name ?? "",
@@ -49,7 +49,7 @@ const defaultModelOptions: ModelSelectorOption[] = [
},
];
const defaultModelConfigs: TypesGen.ChatModelConfig[] = [
const defaultModels: TypesGen.ChatModel[] = [
{
id: "config-openai-gpt-4o",
ai_provider_id: "prov-1",
@@ -69,7 +69,7 @@ const oneWeekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
const buildChat = (overrides: Partial<Chat> = {}): Chat => ({
...MockChat,
id: "chat-default",
last_model_config_id: defaultModelConfigs[0].id,
last_model_config_id: defaultModels[0].id,
created_at: oneWeekAgo,
updated_at: oneWeekAgo,
...overrides,
@@ -100,7 +100,7 @@ const meta: Meta<typeof ChatsSidebar> = {
args: {
chatErrorReasons: {},
modelOptions: defaultModelOptions,
modelConfigs: defaultModelConfigs,
models: defaultModels,
onArchiveAgent: fn(),
onUnarchiveAgent: fn(),
onArchiveAndDeleteWorkspace: fn(),
@@ -104,7 +104,7 @@ const defaultProps: React.ComponentProps<typeof ChatsSidebar> = {
chats: [buildChat({ id: "chat-1", title: "Chat One" })],
chatErrorReasons: {},
modelOptions: [],
modelConfigs: [],
models: [],
onArchiveAgent: vi.fn(),
onUnarchiveAgent: vi.fn(),
onArchiveAndDeleteWorkspace: vi.fn(),
@@ -580,7 +580,7 @@ describe("ChatsSidebar model display names", () => {
displayName: "GPT-4o (Quality)",
},
];
const modelConfigs: TypesGen.ChatModelConfig[] = [
const models: TypesGen.ChatModel[] = [
{
id: "config-fast",
ai_provider_id: "prov-openai",
@@ -619,7 +619,7 @@ describe("ChatsSidebar model display names", () => {
}),
]}
modelOptions={modelOptions}
modelConfigs={modelConfigs}
models={models}
/>
</Wrapper>,
);
@@ -2,7 +2,7 @@ import { type FC, useState } from "react";
import { useQuery } from "react-query";
import { useLocation, useParams } from "react-router";
import { userChatProviderConfigs } from "#/api/queries/chats";
import type { Chat, ChatModelConfig } from "#/api/typesGenerated";
import type { Chat, ChatModel } from "#/api/typesGenerated";
import type { AgentSidebarFilters } from "../../utils/agentSidebarFilters";
import type { ModelSelectorOption } from "../ChatElements";
import { ChatsPanel } from "./chats/ChatsPanel";
@@ -16,7 +16,7 @@ interface ChatsSidebarProps {
chats: readonly Chat[];
chatErrorReasons: Record<string, string>;
modelOptions: readonly ModelSelectorOption[];
modelConfigs: readonly ChatModelConfig[];
models: readonly ChatModel[];
onArchiveAgent: (chatId: string) => void;
onUnarchiveAgent: (chatId: string) => void;
onArchiveAndDeleteWorkspace: (chatId: string, workspaceId: string) => void;
@@ -58,7 +58,7 @@ export const ChatsSidebar: FC<ChatsSidebarProps> = (props) => {
chats,
chatErrorReasons,
modelOptions,
modelConfigs,
models,
onArchiveAgent,
onUnarchiveAgent,
onArchiveAndDeleteWorkspace,
@@ -124,7 +124,7 @@ export const ChatsSidebar: FC<ChatsSidebarProps> = (props) => {
chats={chats}
chatErrorReasons={chatErrorReasons}
modelOptions={modelOptions}
modelConfigs={modelConfigs}
models={models}
onArchiveAgent={onArchiveAgent}
onUnarchiveAgent={onUnarchiveAgent}
onArchiveAndDeleteWorkspace={onArchiveAndDeleteWorkspace}
@@ -22,7 +22,7 @@ import {
} from "lucide-react";
import { type FC, useEffect, useRef, useState } from "react";
import { Link, type Location, NavLink } from "react-router";
import type { Chat, ChatModelConfig } from "#/api/typesGenerated";
import type { Chat, ChatModel } from "#/api/typesGenerated";
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
import { Button } from "#/components/Button/Button";
import { ProductLogo } from "#/components/Icons/ProductLogo";
@@ -68,7 +68,7 @@ interface ChatsPanelProps {
readonly chats: readonly Chat[];
readonly chatErrorReasons: Record<string, string>;
readonly modelOptions: readonly ModelSelectorOption[];
readonly modelConfigs: readonly ChatModelConfig[];
readonly models: readonly ChatModel[];
readonly onArchiveAgent: (chatId: string) => void;
readonly onUnarchiveAgent: (chatId: string) => void;
readonly onArchiveAndDeleteWorkspace: (
@@ -104,7 +104,7 @@ export const ChatsPanel: FC<ChatsPanelProps> = ({
chats,
chatErrorReasons,
modelOptions,
modelConfigs,
models,
onArchiveAgent,
onUnarchiveAgent,
onArchiveAndDeleteWorkspace,
@@ -297,7 +297,7 @@ export const ChatsPanel: FC<ChatsPanelProps> = ({
normalizedSearch: "",
expandedById,
modelOptions,
modelConfigs,
models,
chatErrorReasons,
activeChatId,
isArchiving,
@@ -1,5 +1,5 @@
import { createContext, useContext } from "react";
import type { Chat, ChatModelConfig } from "#/api/typesGenerated";
import type { Chat, ChatModel } from "#/api/typesGenerated";
import type { ModelSelectorOption } from "../../ChatElements";
import type { ChatTree } from "./chatTree";
@@ -10,7 +10,7 @@ export interface ChatTreeContextValue {
readonly normalizedSearch: string;
readonly expandedById: Record<string, boolean>;
readonly modelOptions: readonly ModelSelectorOption[];
readonly modelConfigs: readonly ChatModelConfig[];
readonly models: readonly ChatModel[];
readonly chatErrorReasons: Record<string, string>;
readonly activeChatId: string | undefined;
readonly isArchiving: boolean;
@@ -59,7 +59,7 @@ export const ChatTreeNode: FC<ChatTreeNodeProps> = ({
normalizedSearch,
expandedById,
modelOptions,
modelConfigs,
models,
chatErrorReasons,
activeChatId,
isArchiving,
@@ -82,7 +82,7 @@ export const ChatTreeNode: FC<ChatTreeNodeProps> = ({
const isDelegatedExecuting = isDelegated && chat.status === "running";
const modelName = getModelDisplayName(
chat.last_model_config_id,
modelConfigs,
models,
modelOptions,
);
const errorReason =
@@ -1,10 +1,10 @@
import type { Chat, ChatModelConfig } from "#/api/typesGenerated";
import type { Chat, ChatModel } from "#/api/typesGenerated";
import type { ModelSelectorOption } from "../../ChatElements";
import { asString } from "../../ChatElements/runtimeTypeUtils";
export const getModelDisplayName = (
lastModelConfigID: Chat["last_model_config_id"] | undefined,
modelConfigs: readonly ChatModelConfig[],
models: readonly ChatModel[],
modelOptions: readonly ModelSelectorOption[],
) => {
const normalizedModelConfigID = asString(lastModelConfigID).trim();
@@ -19,7 +19,7 @@ export const getModelDisplayName = (
return modelOption.displayName;
}
const modelConfig = modelConfigs.find(
const modelConfig = models.find(
(config) => config.id === normalizedModelConfigID,
);
if (!modelConfig) {
@@ -6,7 +6,7 @@ interface ModelOverrideAlertsProps {
unavailableMessage: ReactNode;
isMalformedOverride: boolean;
malformedMessage: ReactNode;
modelConfigsError: unknown;
modelsError: unknown;
children?: ReactNode;
}
@@ -15,7 +15,7 @@ export const ModelOverrideAlerts: FC<ModelOverrideAlertsProps> = ({
unavailableMessage,
isMalformedOverride,
malformedMessage,
modelConfigsError,
modelsError,
children,
}) => {
return (
@@ -31,9 +31,9 @@ export const ModelOverrideAlerts: FC<ModelOverrideAlertsProps> = ({
</Alert>
)}
{children}
{Boolean(modelConfigsError) && (
{Boolean(modelsError) && (
<p className="m-0 text-xs text-content-destructive">
Failed to load model configs.
Failed to load models.
</p>
)}
</>
@@ -37,8 +37,8 @@ interface PersonalModelOverrideRowProps {
overrideData: PersonalOverride | undefined;
deploymentDefault?: TypesGen.ChatModelOverrideResponse;
modelOptions: readonly ModelSelectorOption[];
modelConfigs: readonly TypesGen.ChatModelConfig[];
modelConfigsError: unknown;
models: readonly TypesGen.ChatModel[];
modelsError: unknown;
isLoading: boolean;
onSave: SavePersonalOverride;
isSaving: boolean;
@@ -90,29 +90,27 @@ const toUpdateRequest = (
return { mode: values.mode, model_config_id: "" };
};
const getModelConfigLabel = (modelConfig: TypesGen.ChatModelConfig): string => {
return modelConfig.display_name.trim() || modelConfig.model || modelConfig.id;
const getModelLabel = (model: TypesGen.ChatModel): string => {
return model.display_name.trim() || model.model || model.id;
};
const getModelConfigLabelByID = (
modelConfigID: string,
modelConfigs: readonly TypesGen.ChatModelConfig[],
const getModelLabelByID = (
modelID: string,
models: readonly TypesGen.ChatModel[],
): string | undefined => {
const modelConfig = modelConfigs.find(
(config) => config.id === modelConfigID,
);
return modelConfig ? getModelConfigLabel(modelConfig) : undefined;
const model = models.find((model) => model.id === modelID);
return model ? getModelLabel(model) : undefined;
};
const getUnavailableModelLabel = (
modelConfigID: string,
modelConfigs: readonly TypesGen.ChatModelConfig[],
modelID: string,
models: readonly TypesGen.ChatModel[],
): string => {
const modelConfigLabel = getModelConfigLabelByID(modelConfigID, modelConfigs);
if (!modelConfigLabel) {
return `Unavailable model (${modelConfigID})`;
const modelLabel = getModelLabelByID(modelID, models);
if (!modelLabel) {
return `Unavailable model (${modelID})`;
}
return `Unavailable: ${modelConfigLabel}`;
return `Unavailable: ${modelLabel}`;
};
const getDefaultModeOptions = (
@@ -125,20 +123,20 @@ const getDefaultModeOptions = (
const getChatDefaultDescription = (
context: PersonalOverrideContext,
modelConfigs: readonly TypesGen.ChatModelConfig[],
models: readonly TypesGen.ChatModel[],
): string => {
if (context !== "root") {
return "Your current chat model";
}
const defaultModel = modelConfigs.find((config) => config.is_default);
const defaultModel = models.find((model) => model.is_default);
return defaultModel
? getModelConfigLabel(defaultModel)
? getModelLabel(defaultModel)
: "Model definition default";
};
const getDeploymentDefaultDescription = (
deploymentDefault: TypesGen.ChatModelOverrideResponse | undefined,
modelConfigs: readonly TypesGen.ChatModelConfig[],
models: readonly TypesGen.ChatModel[],
): string => {
if (!deploymentDefault) {
return "Loading deployment default";
@@ -146,14 +144,11 @@ const getDeploymentDefaultDescription = (
if (deploymentDefault.is_malformed) {
return "Invalid deployment default";
}
const modelConfigID = deploymentDefault.model_config_id.trim();
if (modelConfigID === "") {
const modelID = deploymentDefault.model_config_id.trim();
if (modelID === "") {
return "Chat default fallback";
}
return (
getModelConfigLabelByID(modelConfigID, modelConfigs) ??
`Unavailable model (${modelConfigID})`
);
return getModelLabelByID(modelID, models) ?? `Unavailable model (${modelID})`;
};
const isDefaultModeOption = (
@@ -169,8 +164,8 @@ export const PersonalModelOverrideRow: FC<PersonalModelOverrideRowProps> = ({
overrideData,
deploymentDefault,
modelOptions,
modelConfigs,
modelConfigsError,
models,
modelsError,
isLoading,
onSave,
isSaving,
@@ -198,8 +193,8 @@ export const PersonalModelOverrideRow: FC<PersonalModelOverrideRowProps> = ({
mode === "deployment_default" ? "Deployment default" : "Chat default";
const modeDescription =
mode === "deployment_default"
? getDeploymentDefaultDescription(deploymentDefault, modelConfigs)
: getChatDefaultDescription(context, modelConfigs);
? getDeploymentDefaultDescription(deploymentDefault, models)
: getChatDefaultDescription(context, models);
return {
id: mode,
provider: "defaults",
@@ -277,10 +272,7 @@ export const PersonalModelOverrideRow: FC<PersonalModelOverrideRowProps> = ({
isInvalidRootDeploymentDefault
? "Invalid deployment default"
: isUnavailableSelectedModel
? getUnavailableModelLabel(
form.values.model_config_id,
modelConfigs,
)
? getUnavailableModelLabel(form.values.model_config_id, models)
: "Select..."
}
triggerAriaLabel={`${title} behavior`}
@@ -303,7 +295,7 @@ export const PersonalModelOverrideRow: FC<PersonalModelOverrideRowProps> = ({
unavailableMessage="The saved model is unavailable and will be ignored until you choose a valid model override."
isMalformedOverride={isMalformedOverride}
malformedMessage="The saved override is malformed. Choose a valid value and save to replace it."
modelConfigsError={modelConfigsError}
modelsError={modelsError}
>
{isInvalidRootDeploymentDefault && (
<Alert severity="warning">
@@ -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 { MockUserOwner } from "#/testHelpers/entities";
import {
withAuthProvider,
@@ -9,9 +9,9 @@ import {
} from "#/testHelpers/storybook";
import { UserCompactionThresholdSettings } from "./UserCompactionThresholdSettings";
const mockModelConfigs: TypesGen.ChatModelConfig[] = [
const mockModels: TypesGen.ChatModel[] = [
{
...MockChatModelConfig,
...MockChatModel,
id: "model-1",
model: "gpt-4o",
display_name: "GPT-4o",
@@ -22,7 +22,7 @@ const mockModelConfigs: TypesGen.ChatModelConfig[] = [
updated_at: "2025-01-01T00:00:00Z",
},
{
...MockChatModelConfig,
...MockChatModel,
id: "model-2",
ai_provider_id: "provider-anthropic",
model: "claude-sonnet",
@@ -31,7 +31,7 @@ const mockModelConfigs: TypesGen.ChatModelConfig[] = [
updated_at: "2025-01-01T00:00:00Z",
},
{
...MockChatModelConfig,
...MockChatModel,
id: "model-3",
model: "gpt-3.5",
display_name: "GPT-3.5 (Disabled)",
@@ -48,7 +48,7 @@ const meta = {
component: UserCompactionThresholdSettings,
decorators: [withAuthProvider, withDashboardProvider],
args: {
modelConfigs: mockModelConfigs,
models: mockModels,
providerTypeByID: new Map<string, string>([
["provider-1", "openai"],
["provider-anthropic", "anthropic"],
@@ -237,8 +237,8 @@ export const Loading: Story = {
export const PartialSaveFailure: Story = {
name: "Partial Save Failure",
args: {
onSaveThreshold: fn(async (modelConfigId: string) => {
if (modelConfigId === "model-2") {
onSaveThreshold: fn(async (modelId: string) => {
if (modelId === "model-2") {
throw new globalThis.Error("Network error");
}
}),
@@ -29,18 +29,18 @@ import { cn } from "#/utils/cn";
import { ProviderIcon } from "./ChatModelAdminPanel/ProviderIcon";
interface UserCompactionThresholdSettingsProps {
modelConfigs: readonly TypesGen.ChatModelConfig[];
models: readonly TypesGen.ChatModel[];
providerTypeByID: ReadonlyMap<string, string>;
modelConfigsError?: unknown;
isLoadingModelConfigs?: boolean;
modelsError?: unknown;
isLoadingModels?: boolean;
thresholds: readonly TypesGen.UserChatCompactionThreshold[] | undefined;
isThresholdsLoading: boolean;
thresholdsError: unknown;
onSaveThreshold: (
modelConfigId: string,
modelId: string,
thresholdPercent: number,
) => Promise<unknown>;
onResetThreshold: (modelConfigId: string) => Promise<unknown>;
onResetThreshold: (modelId: string) => Promise<unknown>;
}
const parseThresholdDraft = (value: string): number | null => {
@@ -72,10 +72,10 @@ const ContextCompactionHeader: FC = () => (
export const UserCompactionThresholdSettings: FC<
UserCompactionThresholdSettingsProps
> = ({
modelConfigs,
models,
providerTypeByID,
modelConfigsError,
isLoadingModelConfigs,
modelsError,
isLoadingModels,
thresholds,
isThresholdsLoading,
thresholdsError,
@@ -87,7 +87,7 @@ export const UserCompactionThresholdSettings: FC<
const [pendingModels, setPendingModels] = useState<Set<string>>(new Set());
const { isSavedVisible, showSavedState } = useTemporarySavedState();
const enabledModelConfigs = modelConfigs.filter((config) => config.enabled);
const enabledModels = models.filter((config) => config.enabled);
const overridesByModelID = new Map(
(thresholds ?? []).map(
(threshold: TypesGen.UserChatCompactionThreshold) => [
@@ -97,21 +97,21 @@ export const UserCompactionThresholdSettings: FC<
),
);
const clearDraft = (modelConfigID: string) => {
const clearDraft = (modelID: string) => {
setDrafts((currentDrafts) => {
const nextDrafts = { ...currentDrafts };
delete nextDrafts[modelConfigID];
delete nextDrafts[modelID];
return nextDrafts;
});
};
const clearRowError = (modelConfigID: string) => {
const clearRowError = (modelID: string) => {
setRowErrors((currentErrors) => {
if (!(modelConfigID in currentErrors)) {
if (!(modelID in currentErrors)) {
return currentErrors;
}
const nextErrors = { ...currentErrors };
delete nextErrors[modelConfigID];
delete nextErrors[modelID];
return nextErrors;
});
};
@@ -128,55 +128,55 @@ export const UserCompactionThresholdSettings: FC<
});
};
const handleReset = (modelConfigId: string) => {
clearRowError(modelConfigId);
addPending(modelConfigId);
onResetThreshold(modelConfigId)
const handleReset = (modelId: string) => {
clearRowError(modelId);
addPending(modelId);
onResetThreshold(modelId)
.then(() => {
clearDraft(modelConfigId);
clearRowError(modelConfigId);
clearDraft(modelId);
clearRowError(modelId);
})
.catch((error: unknown) => {
setRowErrors((currentErrors) => ({
...currentErrors,
[modelConfigId]: getErrorMessage(
[modelId]: getErrorMessage(
error,
"Failed to reset compaction threshold.",
),
}));
})
.finally(() => {
removePending(modelConfigId);
removePending(modelId);
});
};
// Compute dirty rows: rows where the user has typed a valid value
// that differs from the current server-side override.
const dirtyRows: Array<{ modelConfigId: string; value: number }> = [];
for (const modelConfig of enabledModelConfigs) {
const dirtyRows: Array<{ modelId: string; value: number }> = [];
for (const modelConfig of enabledModels) {
const draft = drafts[modelConfig.id];
if (draft === undefined) continue;
const parsed = parseThresholdDraft(draft);
if (parsed === null) continue;
const existingOverride = overridesByModelID.get(modelConfig.id);
if (parsed === existingOverride) continue;
dirtyRows.push({ modelConfigId: modelConfig.id, value: parsed });
dirtyRows.push({ modelId: modelConfig.id, value: parsed });
}
const handleSaveAll = () => {
const saves = dirtyRows.map(({ modelConfigId, value }) => {
clearRowError(modelConfigId);
addPending(modelConfigId);
return onSaveThreshold(modelConfigId, value)
const saves = dirtyRows.map(({ modelId, value }) => {
clearRowError(modelId);
addPending(modelId);
return onSaveThreshold(modelId, value)
.then(() => {
clearDraft(modelConfigId);
clearRowError(modelConfigId);
clearDraft(modelId);
clearRowError(modelId);
return true;
})
.catch((error: unknown) => {
setRowErrors((currentErrors) => ({
...currentErrors,
[modelConfigId]: getErrorMessage(
[modelId]: getErrorMessage(
error,
"Failed to save compaction threshold.",
),
@@ -184,7 +184,7 @@ export const UserCompactionThresholdSettings: FC<
return false;
})
.finally(() => {
removePending(modelConfigId);
removePending(modelId);
});
});
void Promise.all(saves).then((results) => {
@@ -234,19 +234,16 @@ export const UserCompactionThresholdSettings: FC<
return (
<div className="flex flex-col gap-3">
<ContextCompactionHeader />
{isLoadingModelConfigs ? (
{isLoadingModels ? (
<div className="flex items-center gap-2 text-sm text-content-secondary">
<Spinner loading className="size-4" />
Loading models...
</div>
) : modelConfigsError ? (
) : modelsError ? (
<p className="m-0 text-xs text-content-destructive">
{getErrorMessage(
modelConfigsError,
"Failed to load model configurations.",
)}
{getErrorMessage(modelsError, "Failed to load model configurations.")}
</p>
) : enabledModelConfigs.length === 0 ? (
) : enabledModels.length === 0 ? (
<p className="m-0 text-xs text-content-secondary">
No enabled chat models available. An administrator must configure chat
models before compaction thresholds can be set.
@@ -261,7 +258,7 @@ export const UserCompactionThresholdSettings: FC<
</TableRow>
</TableHeader>
<TableBody>
{enabledModelConfigs.map((modelConfig) => {
{enabledModels.map((modelConfig) => {
const existingOverride = overridesByModelID.get(modelConfig.id);
const hasOverride = overridesByModelID.has(modelConfig.id);
const draftValue =
@@ -1,18 +1,18 @@
import { describe, expect, it } from "vitest";
import type {
ChatModelConfig,
ChatModelsResponse,
ChatModel,
ChatModelAvailabilityResponse,
ChatProviderConfig,
} from "#/api/typesGenerated";
import {
MockChatModelConfig,
MockChatModel,
MockChatProviderConfig,
} from "#/testHelpers/chatModels";
import {
countConfiguredProviderConfigs,
filterConfigsWithEnabledProvider,
filterModelsWithEnabledProvider,
formatProviderLabel,
getModelOptionsFromConfigs,
getModelOptionsFromModels,
getModelSelectorPlaceholder,
getUnsupportedProviderNames,
hasConfiguredProviderConfigs,
@@ -26,10 +26,10 @@ import {
} from "./modelOptions";
const createConfig = (
overrides: Partial<ChatModelConfig> &
Pick<ChatModelConfig, "id" | "ai_provider_id" | "model">,
): ChatModelConfig => ({
...MockChatModelConfig,
overrides: Partial<ChatModel> &
Pick<ChatModel, "id" | "ai_provider_id" | "model">,
): ChatModel => ({
...MockChatModel,
context_limit: 0,
compression_threshold: 0,
created_at: "",
@@ -50,9 +50,9 @@ const providerInfoByID = new Map([
]);
const createCatalog = (
providers: ChatModelsResponse["providers"],
unsupportedProviders: ChatModelsResponse["unsupported_providers"] = [],
): ChatModelsResponse => ({
providers: ChatModelAvailabilityResponse["providers"],
unsupportedProviders: ChatModelAvailabilityResponse["unsupported_providers"] = [],
): ChatModelAvailabilityResponse => ({
providers,
unsupported_providers: unsupportedProviders,
});
@@ -113,7 +113,7 @@ describe("hasConfiguredProviderConfigs", () => {
).toBe(false);
});
it("returns true for database and env preset provider configs", () => {
it("returns true for database and env preset provider models", () => {
const catalog = createCatalog([
{ provider: "openai", available: true, models: [] },
]);
@@ -132,7 +132,7 @@ describe("hasConfiguredProviderConfigs", () => {
).toBe(true);
});
it("excludes disabled and unavailable provider configs", () => {
it("excludes disabled and unavailable provider models", () => {
const catalog = createCatalog([
{ provider: "openai", available: true, models: [] },
{
@@ -163,7 +163,7 @@ describe("hasConfiguredProviderConfigs", () => {
});
describe("countConfiguredProviderConfigs", () => {
it("counts only enabled provider configs available in the catalog", () => {
it("counts only enabled provider models available in the catalog", () => {
const catalog = createCatalog([
{ provider: "openai", available: true, models: [] },
{ provider: "anthropic", available: true, models: [] },
@@ -281,9 +281,9 @@ describe("resolveModelOptionId", () => {
});
});
describe("getModelOptionsFromConfigs", () => {
it("returns distinct options for configs with the same provider and model", () => {
const configs = [
describe("getModelOptionsFromModels", () => {
it("returns distinct options for models with the same provider and model", () => {
const models = [
createConfig({
id: "config-1",
ai_provider_id: "prov-openai",
@@ -308,7 +308,7 @@ describe("getModelOptionsFromConfigs", () => {
]);
expect(
getModelOptionsFromConfigs(configs, catalog, providerInfoByID),
getModelOptionsFromModels(models, catalog, providerInfoByID),
).toEqual([
{
id: "config-1",
@@ -325,7 +325,7 @@ describe("getModelOptionsFromConfigs", () => {
});
it("populates reasoning effort bounds from the model config", () => {
const configs = [
const models = [
createConfig({
id: "config-effort",
ai_provider_id: "prov-openai",
@@ -349,7 +349,7 @@ describe("getModelOptionsFromConfigs", () => {
]);
expect(
getModelOptionsFromConfigs(configs, catalog, providerInfoByID),
getModelOptionsFromModels(models, catalog, providerInfoByID),
).toEqual([
{
id: "config-no-effort",
@@ -376,8 +376,8 @@ describe("getModelOptionsFromConfigs", () => {
]);
});
it("excludes configs whose providers are unavailable", () => {
const configs = [
it("excludes models whose providers are unavailable", () => {
const models = [
createConfig({
id: "config-1",
ai_provider_id: "prov-anthropic",
@@ -395,12 +395,12 @@ describe("getModelOptionsFromConfigs", () => {
]);
expect(
getModelOptionsFromConfigs(configs, catalog, providerInfoByID),
getModelOptionsFromModels(models, catalog, providerInfoByID),
).toEqual([]);
});
it("excludes disabled configs", () => {
const configs = [
it("excludes disabled models", () => {
const models = [
createConfig({
id: "config-1",
ai_provider_id: "prov-openai",
@@ -426,14 +426,14 @@ describe("getModelOptionsFromConfigs", () => {
]);
expect(
getModelOptionsFromConfigs(configs, catalog, providerInfoByID).map(
getModelOptionsFromModels(models, catalog, providerInfoByID).map(
(option) => option.id,
),
).toEqual(["config-2"]);
});
it("falls back to the model name when display_name is blank", () => {
const configs = [
const models = [
createConfig({
id: "config-1",
ai_provider_id: "prov-openai",
@@ -451,7 +451,7 @@ describe("getModelOptionsFromConfigs", () => {
]);
expect(
getModelOptionsFromConfigs(configs, catalog, providerInfoByID),
getModelOptionsFromModels(models, catalog, providerInfoByID),
).toEqual([
expect.objectContaining({
id: "config-1",
@@ -462,16 +462,14 @@ describe("getModelOptionsFromConfigs", () => {
});
it("returns an empty array for null and undefined inputs", () => {
expect(getModelOptionsFromConfigs(null, null, providerInfoByID)).toEqual(
[],
);
expect(getModelOptionsFromModels(null, null, providerInfoByID)).toEqual([]);
expect(
getModelOptionsFromConfigs(undefined, undefined, providerInfoByID),
getModelOptionsFromModels(undefined, undefined, providerInfoByID),
).toEqual([]);
});
it("sorts options by provider and display name", () => {
const configs = [
const models = [
createConfig({
id: "config-openai-zeta",
ai_provider_id: "prov-openai",
@@ -508,7 +506,7 @@ describe("getModelOptionsFromConfigs", () => {
]);
expect(
getModelOptionsFromConfigs(configs, catalog, providerInfoByID).map(
getModelOptionsFromModels(models, catalog, providerInfoByID).map(
(option) => option.id,
),
).toEqual([
@@ -519,7 +517,7 @@ describe("getModelOptionsFromConfigs", () => {
});
it("keeps canonical wrapper-provider model strings distinct", () => {
const configs = [
const models = [
createConfig({
id: "config-1",
ai_provider_id: "prov-openrouter",
@@ -544,14 +542,14 @@ describe("getModelOptionsFromConfigs", () => {
]);
expect(
getModelOptionsFromConfigs(configs, catalog, providerInfoByID).map(
getModelOptionsFromModels(models, catalog, providerInfoByID).map(
(option) => option.id,
),
).toEqual(["config-2", "config-1"]);
});
it("drops configs whose ai_provider_id is absent from the provider map", () => {
const configs = [
it("drops models whose ai_provider_id is absent from the provider map", () => {
const models = [
createConfig({
id: "config-1",
ai_provider_id: "prov-openai",
@@ -564,11 +562,11 @@ describe("getModelOptionsFromConfigs", () => {
{ provider: "openai", available: true, models: [] },
]);
expect(getModelOptionsFromConfigs(configs, catalog, new Map())).toEqual([]);
expect(getModelOptionsFromModels(models, catalog, new Map())).toEqual([]);
});
it("keeps only configs whose ai_provider_id resolves in the provider map", () => {
const configs = [
it("keeps only models whose ai_provider_id resolves in the provider map", () => {
const models = [
createConfig({
id: "config-openai",
ai_provider_id: "prov-openai",
@@ -593,14 +591,14 @@ describe("getModelOptionsFromConfigs", () => {
]);
expect(
getModelOptionsFromConfigs(configs, catalog, partialMap).map(
getModelOptionsFromModels(models, catalog, partialMap).map(
(option) => option.id,
),
).toEqual(["config-openai"]);
});
it("preserves provider instance metadata for same-type providers", () => {
const configs = [
const models = [
createConfig({
id: "config-primary",
ai_provider_id: "prov-anthropic-primary",
@@ -631,7 +629,7 @@ describe("getModelOptionsFromConfigs", () => {
]);
expect(
getModelOptionsFromConfigs(configs, catalog, sameTypeProviders),
getModelOptionsFromModels(models, catalog, sameTypeProviders),
).toEqual([
expect.objectContaining({
id: "config-primary",
@@ -647,8 +645,8 @@ describe("getModelOptionsFromConfigs", () => {
]);
});
it("drops configs whose provider row is disabled", () => {
const configs = [
it("drops models whose provider row is disabled", () => {
const models = [
createConfig({
id: "config-enabled",
ai_provider_id: "prov-enabled",
@@ -680,14 +678,14 @@ describe("getModelOptionsFromConfigs", () => {
]);
expect(
getModelOptionsFromConfigs(configs, catalog, providers).map(
getModelOptionsFromModels(models, catalog, providers).map(
(option) => option.id,
),
).toEqual(["config-enabled"]);
});
it("keeps configs when the provider enabled flag is undefined", () => {
const configs = [
it("keeps models when the provider enabled flag is undefined", () => {
const models = [
createConfig({
id: "config-openai",
ai_provider_id: "prov-openai",
@@ -699,7 +697,7 @@ describe("getModelOptionsFromConfigs", () => {
]);
expect(
getModelOptionsFromConfigs(configs, catalog, providerInfoByID).map(
getModelOptionsFromModels(models, catalog, providerInfoByID).map(
(option) => option.id,
),
).toEqual(["config-openai"]);
@@ -708,7 +706,7 @@ describe("getModelOptionsFromConfigs", () => {
it("excludes only the disabled instance for same-type providers", () => {
// The catalog marks the type as available because of the enabled
// instance, so only the per-row flag can exclude the disabled one.
const configs = [
const models = [
createConfig({
id: "config-primary",
ai_provider_id: "prov-anthropic-primary",
@@ -745,15 +743,15 @@ describe("getModelOptionsFromConfigs", () => {
]);
expect(
getModelOptionsFromConfigs(configs, catalog, sameTypeProviders).map(
getModelOptionsFromModels(models, catalog, sameTypeProviders).map(
(option) => option.id,
),
).toEqual(["config-primary"]);
});
});
describe("filterConfigsWithEnabledProvider", () => {
const configs = [
describe("filterModelsWithEnabledProvider", () => {
const models = [
createConfig({
id: "config-enabled",
ai_provider_id: "prov-enabled",
@@ -786,24 +784,24 @@ describe("filterConfigsWithEnabledProvider", () => {
],
]);
it("drops configs of disabled or unknown providers", () => {
it("drops models of disabled or unknown providers", () => {
expect(
filterConfigsWithEnabledProvider(configs, providers).map(
filterModelsWithEnabledProvider(models, providers).map(
(config) => config.id,
),
).toEqual(["config-enabled"]);
});
it("keeps configs whose provider rows lack enabled flags", () => {
it("keeps models whose provider rows lack enabled flags", () => {
const flaglessProviders = new Map(
["prov-enabled", "prov-disabled", "prov-unknown"].map((id) => [
id,
{ provider: "openai", displayName: "OpenAI", icon: "" },
]),
);
expect(
filterConfigsWithEnabledProvider(configs, flaglessProviders),
).toEqual(configs);
expect(filterModelsWithEnabledProvider(models, flaglessProviders)).toEqual(
models,
);
});
});
@@ -902,12 +900,13 @@ describe("providerTypeByIDFromUserConfigs", () => {
});
describe("getUnsupportedProviderNames", () => {
const unsupportedCopilot: ChatModelsResponse["unsupported_providers"] = [
{
provider: "copilot",
display_name: "GitHub Copilot",
},
];
const unsupportedCopilot: ChatModelAvailabilityResponse["unsupported_providers"] =
[
{
provider: "copilot",
display_name: "GitHub Copilot",
},
];
it("returns names when no supported provider is configured", () => {
const catalog = createCatalog([], unsupportedCopilot);
@@ -956,7 +955,7 @@ describe("resolveModelSelector", () => {
const catalog = createCatalog([
{ provider: "openai", available: true, models: [] },
]);
const userProviderConfigs = [
const userProviderModels = [
{
provider_id: "prov-openai",
provider: "openai",
@@ -970,7 +969,7 @@ describe("resolveModelSelector", () => {
];
it("stays loading and drops options while the provider query is pending", () => {
// Catalog + configs have resolved, but provider identity has not, so
// Catalog + models have resolved, but provider identity has not, so
// the provider map is empty. Options must be dropped and the flag must
// stay loading rather than flashing "No Models".
const state = resolveModelSelector(
@@ -987,7 +986,7 @@ describe("resolveModelSelector", () => {
const state = resolveModelSelector(
{ data: [config], isLoading: false },
{ data: catalog, isLoading: false },
{ data: userProviderConfigs, isLoading: false },
{ data: userProviderModels, isLoading: false },
);
expect(state.isModelCatalogLoading).toBe(false);
+44 -48
View File
@@ -7,7 +7,7 @@ import {
} from "../components/ChatElements/runtimeTypeUtils";
type CatalogModelLike =
| TypesGen.ChatModel
| TypesGen.ChatModelCatalogEntry
| {
readonly id?: unknown;
readonly display_name?: unknown;
@@ -23,14 +23,14 @@ type ModelCatalogLike = {
export const hasConfiguredProviderConfigs = (
providerConfigs: readonly TypesGen.ChatProviderConfig[] | null | undefined,
catalog: TypesGen.ChatModelsResponse | null | undefined,
catalog: TypesGen.ChatModelAvailabilityResponse | null | undefined,
): boolean => {
return countConfiguredProviderConfigs(providerConfigs, catalog) > 0;
};
export const countConfiguredProviderConfigs = (
providerConfigs: readonly TypesGen.ChatProviderConfig[] | null | undefined,
catalog: TypesGen.ChatModelsResponse | null | undefined,
catalog: TypesGen.ChatModelAvailabilityResponse | null | undefined,
): number => {
const availableProviders = getAvailableProviders(catalog);
return (
@@ -81,7 +81,7 @@ export const hasConfiguredModelsInCatalog = (
};
export const hasUserFixableProviders = (
catalog: TypesGen.ChatModelsResponse | null | undefined,
catalog: TypesGen.ChatModelAvailabilityResponse | null | undefined,
): boolean => {
if (!catalog?.providers) {
return false;
@@ -92,7 +92,7 @@ export const hasUserFixableProviders = (
};
const getCatalogUnsupportedProviders = (
catalog: TypesGen.ChatModelsResponse | null | undefined,
catalog: TypesGen.ChatModelAvailabilityResponse | null | undefined,
): readonly TypesGen.ChatUnsupportedProvider[] => {
const unsupported = catalog?.unsupported_providers;
return Array.isArray(unsupported) ? unsupported : [];
@@ -104,7 +104,7 @@ const getCatalogUnsupportedProviders = (
* missing its API key returns an empty list, keeping normal setup guidance.
*/
export const getUnsupportedProviderNames = (
catalog: TypesGen.ChatModelsResponse | null | undefined,
catalog: TypesGen.ChatModelAvailabilityResponse | null | undefined,
): readonly string[] => {
const unsupported = getCatalogUnsupportedProviders(catalog);
if (unsupported.length === 0) {
@@ -121,7 +121,7 @@ export const getUnsupportedProviderNames = (
};
const getAvailableProviders = (
catalog: TypesGen.ChatModelsResponse | null | undefined,
catalog: TypesGen.ChatModelAvailabilityResponse | null | undefined,
): ReadonlySet<string> => {
const availableProviders = new Set<string>();
for (const provider of getCatalogProviders(catalog)) {
@@ -137,7 +137,7 @@ const getAvailableProviders = (
};
/**
* Resolves a stored model config ID to the ID of a matching model
* Resolves a stored ChatModel ID to the ID of a matching model
* option. Returns the matched option ID, or an empty string when the
* stored ID is blank or no longer matches an available option.
*/
@@ -168,7 +168,7 @@ export type ProviderInfo = {
// providerInfoByIDFromConfigs and providerInfoByIDFromUserConfigs build
// the ai_provider_id -> provider metadata lookup that
// getModelOptionsFromConfigs needs. The admin and user provider endpoints
// getModelOptionsFromModels needs. The admin and user provider endpoints
// expose the provider id under different field names (id vs provider_id), so
// each source has its own helper to bake in the correct field.
export const providerInfoByIDFromConfigs = (
@@ -228,25 +228,25 @@ export const providerTypeByIDFromUserConfigs = (
);
/**
* Drops model configs whose provider row is disabled or missing. Both
* Drops models whose provider row is disabled or missing. Both
* provider-info sources include every enabled provider, so a missing row
* means the provider is disabled or deleted.
*/
export const filterConfigsWithEnabledProvider = (
configs: readonly TypesGen.ChatModelConfig[],
export const filterModelsWithEnabledProvider = (
models: readonly TypesGen.ChatModel[],
providerInfoByID: ReadonlyMap<string, ProviderInfo>,
): readonly TypesGen.ChatModelConfig[] =>
configs.filter((config) => {
const info = providerInfoByID.get(config.ai_provider_id);
): readonly TypesGen.ChatModel[] =>
models.filter((model) => {
const info = providerInfoByID.get(model.ai_provider_id);
return info !== undefined && info.enabled !== false;
});
export const getModelOptionsFromConfigs = (
configs: readonly TypesGen.ChatModelConfig[] | null | undefined,
catalog: TypesGen.ChatModelsResponse | null | undefined,
export const getModelOptionsFromModels = (
models: readonly TypesGen.ChatModel[] | null | undefined,
catalog: TypesGen.ChatModelAvailabilityResponse | null | undefined,
providerInfoByID: ReadonlyMap<string, ProviderInfo>,
): readonly ModelSelectorOption[] => {
if (!configs || !catalog) {
if (!models || !catalog) {
return [];
}
@@ -255,37 +255,37 @@ export const getModelOptionsFromConfigs = (
// The catalog check below is keyed by provider type, so it cannot
// exclude a disabled provider when another of the same type is enabled.
for (const config of filterConfigsWithEnabledProvider(
configs,
for (const model of filterModelsWithEnabledProvider(
models,
providerInfoByID,
)) {
if (!config.enabled) {
if (!model.enabled) {
continue;
}
const configID = config.id.trim();
const providerInfo = providerInfoByID.get(config.ai_provider_id);
const modelID = model.id.trim();
const providerInfo = providerInfoByID.get(model.ai_provider_id);
const provider = asString(providerInfo?.provider).trim().toLowerCase();
const model = config.model.trim();
if (!configID || !providerInfo || !provider || !model) {
const modelName = model.model.trim();
if (!modelID || !providerInfo || !provider || !modelName) {
continue;
}
if (!availableProviders.has(provider)) {
continue;
}
const displayName = config.display_name.trim() || model;
const contextLimit = asNumber(config.context_limit);
const reasoningEffort = config.model_config?.reasoning_effort;
const displayName = model.display_name.trim() || modelName;
const contextLimit = asNumber(model.context_limit);
const reasoningEffort = model.model_config?.reasoning_effort;
const reasoningEffortDefault = asString(reasoningEffort?.default).trim();
const reasoningEfforts = config.reasoning_efforts ?? [];
const reasoningEfforts = model.reasoning_efforts ?? [];
options.push({
id: configID,
id: modelID,
provider,
providerId: config.ai_provider_id,
providerId: model.ai_provider_id,
providerLabel: providerInfo.displayName,
providerIcon: providerInfo.icon,
model,
model: modelName,
displayName,
...(contextLimit !== undefined ? { contextLimit } : {}),
...(reasoningEffortDefault ? { reasoningEffortDefault } : {}),
@@ -315,30 +315,26 @@ type SelectorQuery<T> = {
interface ModelSelectorState {
readonly options: readonly ModelSelectorOption[];
readonly isModelCatalogLoading: boolean;
readonly modelCatalog: TypesGen.ChatModelsResponse | undefined;
readonly modelCatalog: TypesGen.ChatModelAvailabilityResponse | undefined;
readonly hasConfiguredModels: boolean;
}
// Provider identity comes from a separate query (userChatProviderConfigs).
// Provider identity comes from a separate query (userChatProviderModels).
// Folding all three loading states into one flag here spares every caller the
// "configs loaded but providers still pending" window that would otherwise
// "models loaded but providers still pending" window that would otherwise
// build an empty provider map, drop every option, and flash "No Models".
export const resolveModelSelector = (
modelConfigs: SelectorQuery<readonly TypesGen.ChatModelConfig[]>,
catalog: SelectorQuery<TypesGen.ChatModelsResponse>,
userProviderConfigs: SelectorQuery<
readonly TypesGen.UserChatProviderConfig[]
>,
models: SelectorQuery<readonly TypesGen.ChatModel[]>,
catalog: SelectorQuery<TypesGen.ChatModelAvailabilityResponse>,
userProviderModels: SelectorQuery<readonly TypesGen.UserChatProviderConfig[]>,
): ModelSelectorState => ({
options: getModelOptionsFromConfigs(
modelConfigs.data,
options: getModelOptionsFromModels(
models.data,
catalog.data,
providerInfoByIDFromUserConfigs(userProviderConfigs.data),
providerInfoByIDFromUserConfigs(userProviderModels.data),
),
isModelCatalogLoading:
modelConfigs.isLoading ||
catalog.isLoading ||
userProviderConfigs.isLoading,
models.isLoading || catalog.isLoading || userProviderModels.isLoading,
modelCatalog: catalog.data,
hasConfiguredModels: hasConfiguredModelsInCatalog(catalog.data),
});
@@ -359,7 +355,7 @@ export const getModelSelectorPlaceholder = (
modelOptions: readonly ModelSelectorOption[],
isModelCatalogLoading: boolean,
hasConfiguredModels: boolean,
catalog?: TypesGen.ChatModelsResponse | null,
catalog?: TypesGen.ChatModelAvailabilityResponse | null,
): string => {
if (modelOptions.length > 0) {
return "Select model";
@@ -1,16 +1,15 @@
const reasoningEffortStorageKeyPrefix = "agents.reasoning-effort.";
const reasoningEffortStorageKey = (modelConfigID: string) =>
`${reasoningEffortStorageKeyPrefix}${modelConfigID}`;
const reasoningEffortStorageKey = (modelID: string) =>
`${reasoningEffortStorageKeyPrefix}${modelID}`;
/** Reads the persisted effort for a model, or undefined when none is stored or storage is unavailable. */
export const getReasoningEffortForModel = (
modelConfigID: string,
modelID: string,
): string | undefined => {
try {
return (
localStorage.getItem(reasoningEffortStorageKey(modelConfigID)) ??
undefined
localStorage.getItem(reasoningEffortStorageKey(modelID)) ?? undefined
);
} catch {
return undefined;
@@ -19,14 +18,11 @@ export const getReasoningEffortForModel = (
/** Persists the effort for a model. Swallows storage errors (private mode, quota) so the caller's in-memory selection is unaffected. */
export const saveReasoningEffortForModel = (
modelConfigID: string,
modelID: string,
reasoningEffort: string,
): void => {
try {
localStorage.setItem(
reasoningEffortStorageKey(modelConfigID),
reasoningEffort,
);
localStorage.setItem(reasoningEffortStorageKey(modelID), reasoningEffort);
} catch {
// Keep the in-memory selection when storage is unavailable.
}
+2 -2
View File
@@ -1,12 +1,12 @@
import type {
AIModelPrice,
ChatModelConfig,
ChatModel,
ChatModelProvider,
ChatProviderConfig,
} from "#/api/typesGenerated";
import { MOCK_TIMESTAMP } from "./chatEntities";
export const MockChatModelConfig: ChatModelConfig = {
export const MockChatModel: ChatModel = {
id: "model-1",
ai_provider_id: "provider-1",
model: "gpt-5",