mirror of
https://github.com/coder/coder.git
synced 2026-10-31 02:43:21 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+21
-21
@@ -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": [
|
||||
|
||||
Generated
+21
-21
@@ -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"],
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -2490,7 +2490,7 @@ func TestListChatModels(t *testing.T) {
|
||||
_ = coderdtest.CreateFirstUser(t, client.Client)
|
||||
modelConfig := createChatModelConfig(t, client)
|
||||
|
||||
models, err := client.ListChatModels(ctx)
|
||||
models, err := client.ChatModelAvailability(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
var openAIProvider *codersdk.ChatModelProvider
|
||||
@@ -2521,7 +2521,7 @@ func TestListChatModels(t *testing.T) {
|
||||
_ = coderdtest.CreateFirstUser(t, client.Client)
|
||||
|
||||
unauthenticatedClient := codersdk.NewExperimentalClient(codersdk.New(client.URL))
|
||||
_, err := unauthenticatedClient.ListChatModels(ctx)
|
||||
_, err := unauthenticatedClient.ChatModelAvailability(ctx)
|
||||
requireSDKError(t, err, http.StatusUnauthorized)
|
||||
})
|
||||
|
||||
@@ -2537,7 +2537,7 @@ func TestListChatModels(t *testing.T) {
|
||||
// than vanish, so the empty state can explain why.
|
||||
_ = createAIProviderForTest(t, client, string(codersdk.AIProviderTypeCopilot), "")
|
||||
|
||||
models, err := client.ListChatModels(ctx)
|
||||
models, err := client.ChatModelAvailability(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.False(t, slices.ContainsFunc(models.Providers, func(p codersdk.ChatModelProvider) bool {
|
||||
@@ -2560,7 +2560,7 @@ func TestListChatModels(t *testing.T) {
|
||||
_ = coderdtest.CreateFirstUser(t, client.Client)
|
||||
_ = createChatModelConfig(t, client)
|
||||
|
||||
models, err := client.ListChatModels(ctx)
|
||||
models, err := client.ChatModelAvailability(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, models.UnsupportedProviders)
|
||||
})
|
||||
@@ -2573,7 +2573,7 @@ func TestListChatModels(t *testing.T) {
|
||||
_ = coderdtest.CreateFirstUser(t, client.Client)
|
||||
_ = createChatModelConfig(t, client)
|
||||
|
||||
models, err := client.ListChatModels(ctx)
|
||||
models, err := client.ChatModelAvailability(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
var openAIProvider *codersdk.ChatModelProvider
|
||||
@@ -2598,14 +2598,14 @@ func TestListChatModels(t *testing.T) {
|
||||
provider := createAIProviderForTest(t, client, string(providerType), "")
|
||||
|
||||
contextLimit := int64(4096)
|
||||
_, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
_, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &provider.ID,
|
||||
Model: "claude-sonnet",
|
||||
ContextLimit: &contextLimit,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
models, err := client.ListChatModels(ctx)
|
||||
models, err := client.ChatModelAvailability(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
var anthropicProvider *codersdk.ChatModelProvider
|
||||
@@ -2624,7 +2624,7 @@ func TestListChatModels(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
models, err = client.ListChatModels(ctx)
|
||||
models, err = client.ChatModelAvailability(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
anthropicProvider = nil
|
||||
@@ -2648,14 +2648,14 @@ func TestListChatModels(t *testing.T) {
|
||||
provider := createAIProviderForTest(t, client, "google", "provider-api-key")
|
||||
|
||||
contextLimit := int64(4096)
|
||||
_, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
_, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &provider.ID,
|
||||
Model: "gemini-1.5-pro",
|
||||
ContextLimit: &contextLimit,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
models, err := client.ListChatModels(ctx)
|
||||
models, err := client.ChatModelAvailability(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
var googleProvider *codersdk.ChatModelProvider
|
||||
@@ -2673,7 +2673,7 @@ func TestListChatModels(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
models, err = client.ListChatModels(ctx)
|
||||
models, err = client.ChatModelAvailability(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
googleProvider = nil
|
||||
@@ -2695,7 +2695,7 @@ func TestListChatModels(t *testing.T) {
|
||||
_ = coderdtest.CreateFirstUser(t, client.Client)
|
||||
provider := createAIProviderForTest(t, client, "openai-compat", "test-api-key")
|
||||
contextLimit := int64(4096)
|
||||
defaultConfig, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
defaultConfig, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &provider.ID,
|
||||
Model: "default-" + uuid.NewString(),
|
||||
ContextLimit: &contextLimit,
|
||||
@@ -2708,7 +2708,7 @@ func TestListChatModels(t *testing.T) {
|
||||
OrganizationID: otherOrganization.ID,
|
||||
})
|
||||
|
||||
models, err := client.ListChatModels(ctx)
|
||||
models, err := client.ChatModelAvailability(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
var catalogModels []string
|
||||
@@ -2733,14 +2733,14 @@ func TestListChatModels(t *testing.T) {
|
||||
provider := createAIProviderForTest(t, client, "openai", "test-key")
|
||||
|
||||
contextLimit := int64(4096)
|
||||
_, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
_, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &provider.ID,
|
||||
Model: "gpt-4o-mini",
|
||||
ContextLimit: &contextLimit,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
models, err := client.ListChatModels(ctx)
|
||||
models, err := client.ChatModelAvailability(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, models.Providers, 1)
|
||||
require.Equal(t, "openai", models.Providers[0].Provider)
|
||||
@@ -2753,7 +2753,7 @@ func TestListChatModels(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
models, err = client.ListChatModels(ctx)
|
||||
models, err = client.ChatModelAvailability(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, models.Providers)
|
||||
})
|
||||
@@ -2762,7 +2762,7 @@ func TestListChatModels(t *testing.T) {
|
||||
func TestListChats_Search(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
setup := func(t *testing.T) (context.Context, *codersdk.ExperimentalClient, database.Store, codersdk.CreateFirstUserResponse, codersdk.ChatModelConfig) {
|
||||
setup := func(t *testing.T) (context.Context, *codersdk.ExperimentalClient, database.Store, codersdk.CreateFirstUserResponse, codersdk.ChatModel) {
|
||||
t.Helper()
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
client, db := newChatClientWithDatabase(t)
|
||||
@@ -4539,7 +4539,7 @@ func TestListChatModelConfigs(t *testing.T) {
|
||||
_ = coderdtest.CreateFirstUser(t, client.Client)
|
||||
modelConfig := createChatModelConfig(t, client)
|
||||
|
||||
configs, err := client.ListChatModelConfigs(ctx)
|
||||
configs, err := client.ChatModels(ctx)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, configs)
|
||||
|
||||
@@ -4566,7 +4566,7 @@ func TestListChatModelConfigs(t *testing.T) {
|
||||
|
||||
contextLimit := int64(4096)
|
||||
enabled := false
|
||||
disabledConfig, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
disabledConfig, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &aiProvider.ID,
|
||||
Model: "gpt-4o-disabled",
|
||||
DisplayName: "GPT-4o Disabled",
|
||||
@@ -4576,7 +4576,7 @@ func TestListChatModelConfigs(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.False(t, disabledConfig.Enabled)
|
||||
|
||||
configs, err := client.ListChatModelConfigs(ctx)
|
||||
configs, err := client.ChatModels(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
found := false
|
||||
@@ -4602,7 +4602,7 @@ func TestListChatModelConfigs(t *testing.T) {
|
||||
|
||||
contextLimit := int64(4096)
|
||||
enabled := false
|
||||
_, err := adminClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
_, err := adminClient.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &enabledConfig.AIProviderID,
|
||||
Model: "gpt-4o-disabled",
|
||||
DisplayName: "GPT-4o Disabled",
|
||||
@@ -4611,13 +4611,13 @@ func TestListChatModelConfigs(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
configs, err := memberClient.ListChatModelConfigs(ctx)
|
||||
configs, err := memberClient.ChatModels(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, configs, 2)
|
||||
require.True(t, slices.ContainsFunc(configs, func(config codersdk.ChatModelConfig) bool {
|
||||
require.True(t, slices.ContainsFunc(configs, func(config codersdk.ChatModel) bool {
|
||||
return config.ID == enabledConfig.ID && config.Enabled
|
||||
}))
|
||||
require.True(t, slices.ContainsFunc(configs, func(config codersdk.ChatModelConfig) bool {
|
||||
require.True(t, slices.ContainsFunc(configs, func(config codersdk.ChatModel) bool {
|
||||
return config.Model == "gpt-4o-disabled" && !config.Enabled
|
||||
}))
|
||||
})
|
||||
@@ -4639,7 +4639,7 @@ func TestListChatModelConfigs(t *testing.T) {
|
||||
memberClientRaw, _ := coderdtest.CreateAnotherUser(t, adminClient.Client, firstUser.OrganizationID)
|
||||
memberClient := codersdk.NewExperimentalClient(memberClientRaw)
|
||||
|
||||
adminConfigs, err := adminClient.ListChatModelConfigs(ctx)
|
||||
adminConfigs, err := adminClient.ChatModels(ctx)
|
||||
require.NoError(t, err)
|
||||
adminIDs := make([]uuid.UUID, 0, len(adminConfigs))
|
||||
for _, config := range adminConfigs {
|
||||
@@ -4647,7 +4647,7 @@ func TestListChatModelConfigs(t *testing.T) {
|
||||
}
|
||||
require.Contains(t, adminIDs, providerDisabledConfig.ID)
|
||||
|
||||
memberConfigs, err := memberClient.ListChatModelConfigs(ctx)
|
||||
memberConfigs, err := memberClient.ChatModels(ctx)
|
||||
require.NoError(t, err)
|
||||
memberIDs := make([]uuid.UUID, 0, len(memberConfigs))
|
||||
for _, config := range memberConfigs {
|
||||
@@ -4670,7 +4670,7 @@ func TestListChatModelConfigs(t *testing.T) {
|
||||
AIProviderID: uuid.NullUUID{UUID: defaultConfig.AIProviderID, Valid: true},
|
||||
})
|
||||
|
||||
configs, err := client.ListChatModelConfigs(ctx)
|
||||
configs, err := client.ChatModels(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, configs, defaultConfig)
|
||||
for _, config := range configs {
|
||||
@@ -4689,7 +4689,7 @@ func TestListChatModelConfigs(t *testing.T) {
|
||||
memberClient := codersdk.NewExperimentalClient(memberClientRaw)
|
||||
|
||||
// The management list returns all rows allowed by the row ACL.
|
||||
configs, err := memberClient.ListChatModelConfigs(ctx)
|
||||
configs, err := memberClient.ChatModels(ctx)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, configs)
|
||||
|
||||
@@ -4719,7 +4719,7 @@ func TestCreateChatModelConfig(t *testing.T) {
|
||||
|
||||
contextLimit := int64(4096)
|
||||
isDefault := true
|
||||
modelConfig, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
modelConfig, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &aiProvider.ID,
|
||||
Model: "gpt-4o-mini",
|
||||
ContextLimit: &contextLimit,
|
||||
@@ -4732,7 +4732,7 @@ func TestCreateChatModelConfig(t *testing.T) {
|
||||
require.EqualValues(t, 4096, modelConfig.ContextLimit)
|
||||
require.True(t, modelConfig.IsDefault)
|
||||
|
||||
configs, err := client.ListChatModelConfigs(ctx)
|
||||
configs, err := client.ChatModels(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, configs, 1)
|
||||
})
|
||||
@@ -4748,7 +4748,7 @@ func TestCreateChatModelConfig(t *testing.T) {
|
||||
store.armFailProviderReferenceLock(aiProvider.ID)
|
||||
|
||||
contextLimit := int64(4096)
|
||||
_, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
_, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &aiProvider.ID,
|
||||
Model: "gpt-4o-mini",
|
||||
ContextLimit: &contextLimit,
|
||||
@@ -4773,11 +4773,11 @@ func TestCreateChatModelConfig(t *testing.T) {
|
||||
// the losers 409 on the single-default unique index.
|
||||
const creators = 10
|
||||
contextLimit := int64(4096)
|
||||
var claimed codersdk.ChatModelConfig
|
||||
var claimed codersdk.ChatModel
|
||||
var eg errgroup.Group
|
||||
for i := range creators - 1 {
|
||||
eg.Go(func() error {
|
||||
_, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
_, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &aiProvider.ID,
|
||||
Model: fmt.Sprintf("gpt-4o-mini-%d", i),
|
||||
ContextLimit: &contextLimit,
|
||||
@@ -4786,7 +4786,7 @@ func TestCreateChatModelConfig(t *testing.T) {
|
||||
})
|
||||
}
|
||||
eg.Go(func() error {
|
||||
created, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
created, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &aiProvider.ID,
|
||||
Model: "gpt-4o",
|
||||
ContextLimit: &contextLimit,
|
||||
@@ -4794,7 +4794,7 @@ func TestCreateChatModelConfig(t *testing.T) {
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create claimed config: %w", err)
|
||||
}
|
||||
claimed, err = client.UpdateChatModelConfig(ctx, created.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
claimed, err = client.UpdateChatModel(ctx, created.ID, codersdk.UpdateChatModelRequest{
|
||||
IsDefault: ptr.Ref(true),
|
||||
})
|
||||
if err != nil {
|
||||
@@ -4804,7 +4804,7 @@ func TestCreateChatModelConfig(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, eg.Wait())
|
||||
|
||||
configs, err := client.ListChatModelConfigs(ctx)
|
||||
configs, err := client.ChatModels(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, configs, creators)
|
||||
var defaults []uuid.UUID
|
||||
@@ -4849,7 +4849,7 @@ func TestCreateChatModelConfig(t *testing.T) {
|
||||
aiProvider := createAIProviderForTest(t, client, "openai", "test-api-key")
|
||||
|
||||
contextLimit := int64(4096)
|
||||
modelConfig, err := adminClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
modelConfig, err := adminClient.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &aiProvider.ID,
|
||||
Model: "gpt-4o-mini",
|
||||
ContextLimit: &contextLimit,
|
||||
@@ -4907,26 +4907,26 @@ func TestCreateChatModelConfig(t *testing.T) {
|
||||
defaultConfig := createChatModelConfig(t, client)
|
||||
|
||||
contextLimit := int64(4096)
|
||||
modelConfig, err := writerClient.CreateChatModelConfig(setupCtx, codersdk.CreateChatModelConfigRequest{
|
||||
modelConfig, err := writerClient.CreateChatModel(setupCtx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &aiProvider.ID,
|
||||
Model: "gpt-4o-mini",
|
||||
ContextLimit: &contextLimit,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
updated, err := writerClient.UpdateChatModelConfig(setupCtx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
updated, err := writerClient.UpdateChatModel(setupCtx, modelConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
DisplayName: "Window Write",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Window Write", updated.DisplayName)
|
||||
|
||||
require.NoError(t, writerClient.DeleteChatModelConfig(setupCtx, modelConfig.ID))
|
||||
require.NoError(t, writerClient.DeleteChatModel(setupCtx, modelConfig.ID))
|
||||
|
||||
// TODO(mafredri): remove these default-transition checks after
|
||||
// CODAGT-709 M3 (org-scoping cutover).
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
notDefault := false
|
||||
updated, err = writerClient.UpdateChatModelConfig(ctx, defaultConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
updated, err = writerClient.UpdateChatModel(ctx, defaultConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
IsDefault: ¬Default,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -4935,27 +4935,27 @@ func TestCreateChatModelConfig(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.True(t, row.IsDefault, "the sole config must remain default")
|
||||
|
||||
replacement, err := writerClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
replacement, err := writerClient.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &aiProvider.ID,
|
||||
Model: "gpt-4o-mini-replacement",
|
||||
ContextLimit: &contextLimit,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, writerClient.DeleteChatModelConfig(ctx, defaultConfig.ID))
|
||||
require.NoError(t, writerClient.DeleteChatModel(ctx, defaultConfig.ID))
|
||||
_, err = rawDB.GetChatModelConfigByID(ctx, defaultConfig.ID)
|
||||
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||
row, err = rawDB.GetChatModelConfigByID(ctx, replacement.ID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, row.IsDefault, "replacement must be promoted")
|
||||
|
||||
candidate, err := writerClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
candidate, err := writerClient.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &aiProvider.ID,
|
||||
Model: "gpt-4o-mini-promote",
|
||||
ContextLimit: &contextLimit,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
isDefault := true
|
||||
updated, err = writerClient.UpdateChatModelConfig(ctx, candidate.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
updated, err = writerClient.UpdateChatModel(ctx, candidate.ID, codersdk.UpdateChatModelRequest{
|
||||
IsDefault: &isDefault,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -4978,7 +4978,7 @@ func TestCreateChatModelConfig(t *testing.T) {
|
||||
aiProvider := createAIProviderForTest(t, client, "openai", "test-api-key")
|
||||
|
||||
contextLimit := int64(4096)
|
||||
modelConfig, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
modelConfig, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &aiProvider.ID,
|
||||
Model: "gpt-4o-mini",
|
||||
ContextLimit: &contextLimit,
|
||||
@@ -5009,7 +5009,7 @@ func TestCreateChatModelConfig(t *testing.T) {
|
||||
aiProvider := createAIProviderForTest(t, client, "openai", "test-api-key")
|
||||
|
||||
contextLimit := int64(4096)
|
||||
_, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
_, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &aiProvider.ID,
|
||||
Model: "gpt-4o-mini",
|
||||
ContextLimit: &contextLimit,
|
||||
@@ -5034,7 +5034,7 @@ func TestCreateChatModelConfig(t *testing.T) {
|
||||
aiProvider := createAIProviderForTest(t, client, "openai", "test-api-key")
|
||||
|
||||
contextLimit := int64(4096)
|
||||
_, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
_, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &aiProvider.ID,
|
||||
Model: "gpt-4o-mini",
|
||||
ContextLimit: &contextLimit,
|
||||
@@ -5064,7 +5064,7 @@ func TestCreateChatModelConfig(t *testing.T) {
|
||||
aiProvider := createAIProviderForTest(t, client, "openai", "test-api-key")
|
||||
|
||||
contextLimit := int64(4096)
|
||||
_, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
_, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &aiProvider.ID,
|
||||
Model: "gpt-4o-mini",
|
||||
ContextLimit: &contextLimit,
|
||||
@@ -5088,7 +5088,7 @@ func TestCreateChatModelConfig(t *testing.T) {
|
||||
_ = coderdtest.CreateFirstUser(t, client.Client)
|
||||
aiProvider := createAIProviderForTest(t, client, "openai", "test-api-key")
|
||||
|
||||
_, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
_, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &aiProvider.ID,
|
||||
Model: "gpt-4o-mini",
|
||||
})
|
||||
@@ -5104,7 +5104,7 @@ func TestCreateChatModelConfig(t *testing.T) {
|
||||
_ = coderdtest.CreateFirstUser(t, client.Client)
|
||||
|
||||
contextLimit := int64(4096)
|
||||
_, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
_, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
Model: "gpt-4o-mini",
|
||||
ContextLimit: &contextLimit,
|
||||
})
|
||||
@@ -5121,7 +5121,7 @@ func TestCreateChatModelConfig(t *testing.T) {
|
||||
|
||||
contextLimit := int64(4096)
|
||||
missingProviderID := uuid.New()
|
||||
_, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
_, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &missingProviderID,
|
||||
Model: "gpt-4o-mini",
|
||||
ContextLimit: &contextLimit,
|
||||
@@ -5145,7 +5145,7 @@ func TestCreateChatModelConfig(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
contextLimit := int64(4096)
|
||||
modelConfig, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
modelConfig, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &provider.ID,
|
||||
Model: "gpt-4o-mini",
|
||||
ContextLimit: &contextLimit,
|
||||
@@ -5164,7 +5164,7 @@ func TestCreateChatModelConfig(t *testing.T) {
|
||||
|
||||
missingProviderID := uuid.New()
|
||||
contextLimit := int64(4096)
|
||||
_, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
_, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &missingProviderID,
|
||||
Model: "gpt-4o-mini",
|
||||
ContextLimit: &contextLimit,
|
||||
@@ -5188,7 +5188,7 @@ func TestCreateChatModelConfig(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
contextLimit := int64(4096)
|
||||
_, err = client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
_, err = client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &provider.ID,
|
||||
Model: "gpt-4o-mini",
|
||||
ContextLimit: &contextLimit,
|
||||
@@ -5214,7 +5214,7 @@ func TestCreateChatModelConfig(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
contextLimit := int64(4096)
|
||||
_, err = client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
_, err = client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &aiProvider.ID,
|
||||
Model: "anthropic/claude-opus-4.6",
|
||||
ContextLimit: &contextLimit,
|
||||
@@ -5236,7 +5236,7 @@ func TestCreateChatModelConfig(t *testing.T) {
|
||||
aiProvider := createAIProviderForTest(t, adminClient, "openai", "test-api-key")
|
||||
|
||||
contextLimit := int64(4096)
|
||||
_, err := memberClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
_, err := memberClient.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &aiProvider.ID,
|
||||
Model: "gpt-4o-mini",
|
||||
ContextLimit: &contextLimit,
|
||||
@@ -5257,7 +5257,7 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
modelConfig := createChatModelConfig(t, client)
|
||||
|
||||
contextLimit := int64(8192)
|
||||
updated, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
updated, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
DisplayName: "GPT-4o Mini Updated",
|
||||
ContextLimit: &contextLimit,
|
||||
})
|
||||
@@ -5266,7 +5266,7 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
require.Equal(t, "GPT-4o Mini Updated", updated.DisplayName)
|
||||
require.EqualValues(t, 8192, updated.ContextLimit)
|
||||
|
||||
configs, err := client.ListChatModelConfigs(ctx)
|
||||
configs, err := client.ChatModels(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, configs, 1)
|
||||
})
|
||||
@@ -5284,7 +5284,7 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
AIProviderID: uuid.NullUUID{UUID: defaultConfig.AIProviderID, Valid: true},
|
||||
})
|
||||
|
||||
_, err := client.UpdateChatModelConfig(ctx, otherConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
_, err := client.UpdateChatModel(ctx, otherConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
DisplayName: "not allowed",
|
||||
})
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
@@ -5302,7 +5302,7 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
firstUser := coderdtest.CreateFirstUser(t, client.Client)
|
||||
modelConfig := createChatModelConfig(t, client)
|
||||
|
||||
updated, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
updated, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
IsDefault: ptr.Ref(false),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -5334,7 +5334,7 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
IsDefault: true,
|
||||
})
|
||||
|
||||
updated, err := client.UpdateChatModelConfig(ctx, candidateConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
updated, err := client.UpdateChatModel(ctx, candidateConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
IsDefault: ptr.Ref(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -5356,7 +5356,7 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
_ = coderdtest.CreateFirstUser(t, client.Client)
|
||||
modelConfig := createChatModelConfig(t, client)
|
||||
|
||||
updated, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
updated, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
Model: "gpt-4o-mini-updated",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -5383,14 +5383,14 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
contextLimit := int64(4096)
|
||||
modelConfig, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
modelConfig, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &aiProvider.ID,
|
||||
Model: "gpt-4o-mini",
|
||||
ContextLimit: &contextLimit,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
_, err = client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
Model: "anthropic/claude-opus-4.6",
|
||||
})
|
||||
sdkErr := requireSDKError(t, err, http.StatusBadRequest)
|
||||
@@ -5419,7 +5419,7 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
AIProviderID: uuid.NullUUID{UUID: aiProvider.ID, Valid: true},
|
||||
})
|
||||
|
||||
updated, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
updated, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
DisplayName: "Existing OpenRouter Config",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -5452,14 +5452,14 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
contextLimit := int64(4096)
|
||||
modelConfig, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
modelConfig, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &validProvider.ID,
|
||||
Model: "anthropic/claude-opus-4.6",
|
||||
ContextLimit: &contextLimit,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
_, err = client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
AIProviderID: &misconfiguredProvider.ID,
|
||||
})
|
||||
sdkErr := requireSDKError(t, err, http.StatusBadRequest)
|
||||
@@ -5478,14 +5478,14 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
modelConfig := createChatModelConfig(t, adminClient)
|
||||
|
||||
enabled := false
|
||||
updated, err := adminClient.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
updated, err := adminClient.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
Enabled: &enabled,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, modelConfig.ID, updated.ID)
|
||||
require.False(t, updated.Enabled)
|
||||
|
||||
adminConfigs, err := adminClient.ListChatModelConfigs(ctx)
|
||||
adminConfigs, err := adminClient.ChatModels(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
foundForAdmin := false
|
||||
@@ -5497,7 +5497,7 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
}
|
||||
require.True(t, foundForAdmin)
|
||||
|
||||
memberConfigs, err := memberClient.ListChatModelConfigs(ctx)
|
||||
memberConfigs, err := memberClient.ChatModels(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
foundForMember := false
|
||||
@@ -5523,7 +5523,7 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
|
||||
contextLimit := int64(4096)
|
||||
enabled := false
|
||||
modelConfig, err := adminClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
modelConfig, err := adminClient.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &aiProvider.ID,
|
||||
Model: "gpt-4o-reenable",
|
||||
DisplayName: "GPT-4o Re-enable",
|
||||
@@ -5533,7 +5533,7 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.False(t, modelConfig.Enabled)
|
||||
|
||||
memberConfigs, err := memberClient.ListChatModelConfigs(ctx)
|
||||
memberConfigs, err := memberClient.ChatModels(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
foundForMember := false
|
||||
@@ -5546,14 +5546,14 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
require.True(t, foundForMember)
|
||||
|
||||
enabled = true
|
||||
updated, err := adminClient.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
updated, err := adminClient.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
Enabled: &enabled,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, modelConfig.ID, updated.ID)
|
||||
require.True(t, updated.Enabled)
|
||||
|
||||
memberConfigs, err = memberClient.ListChatModelConfigs(ctx)
|
||||
memberConfigs, err = memberClient.ChatModels(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
foundForMember = false
|
||||
@@ -5581,7 +5581,7 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
updated, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
updated, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
AIProviderID: &provider.ID,
|
||||
Model: "claude-3-5-sonnet-latest",
|
||||
})
|
||||
@@ -5605,14 +5605,14 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
updated, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
updated, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
AIProviderID: &provider.ID,
|
||||
Model: "claude-3-5-sonnet-latest",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, uuid.Nil, updated.AIProviderID)
|
||||
|
||||
updated, err = client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
updated, err = client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
Model: "claude-3-5-haiku-latest",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -5629,7 +5629,7 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
modelConfig := createChatModelConfig(t, client)
|
||||
|
||||
missingProviderID := uuid.New()
|
||||
_, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
_, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
AIProviderID: &missingProviderID,
|
||||
})
|
||||
sdkErr := requireSDKError(t, err, http.StatusPreconditionFailed)
|
||||
@@ -5651,7 +5651,7 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
_, err = client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
AIProviderID: &provider.ID,
|
||||
})
|
||||
sdkErr := requireSDKError(t, err, http.StatusPreconditionFailed)
|
||||
@@ -5667,7 +5667,7 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
modelConfig := createChatModelConfig(t, client)
|
||||
|
||||
missingProviderID := uuid.New()
|
||||
_, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
_, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
AIProviderID: &missingProviderID,
|
||||
})
|
||||
sdkErr := requireSDKError(t, err, http.StatusPreconditionFailed)
|
||||
@@ -5684,7 +5684,7 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
|
||||
store.armFailNextUpdate(modelConfig.ID)
|
||||
|
||||
_, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
_, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
DisplayName: "missing in tx",
|
||||
})
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
@@ -5702,7 +5702,7 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
|
||||
contextLimit := int64(4096)
|
||||
isDefault := false
|
||||
candidateConfig, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
candidateConfig, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &aiProvider.ID,
|
||||
Model: "claude-3-5-sonnet",
|
||||
ContextLimit: &contextLimit,
|
||||
@@ -5712,7 +5712,7 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
|
||||
store.armFailNextUpdate(candidateConfig.ID)
|
||||
|
||||
_, err = client.UpdateChatModelConfig(ctx, defaultConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
_, err = client.UpdateChatModel(ctx, defaultConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
IsDefault: ptr.Ref(false),
|
||||
})
|
||||
sdkErr := requireSDKError(t, err, http.StatusInternalServerError)
|
||||
@@ -5726,7 +5726,7 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
client := newChatClient(t)
|
||||
_ = coderdtest.CreateFirstUser(t, client.Client)
|
||||
|
||||
_, err := client.UpdateChatModelConfig(ctx, uuid.New(), codersdk.UpdateChatModelConfigRequest{
|
||||
_, err := client.UpdateChatModel(ctx, uuid.New(), codersdk.UpdateChatModelRequest{
|
||||
DisplayName: "missing",
|
||||
})
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
@@ -5741,7 +5741,7 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
modelConfig := createChatModelConfig(t, client)
|
||||
|
||||
contextLimit := int64(0)
|
||||
_, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
_, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
ContextLimit: &contextLimit,
|
||||
})
|
||||
sdkErr := requireSDKError(t, err, http.StatusBadRequest)
|
||||
@@ -5760,7 +5760,7 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
|
||||
store.armVanishAtLockedRead(modelConfig.ID)
|
||||
|
||||
_, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
_, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
DisplayName: "vanished before update",
|
||||
})
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
@@ -5777,7 +5777,7 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
const sentinel = "locked-copy-sentinel-model"
|
||||
store.armMutateAtLockedRead(modelConfig.ID, sentinel)
|
||||
|
||||
updated, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
updated, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
DisplayName: "only-display-name",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -5799,7 +5799,7 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
_, err = client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
Model: "gpt-4o-different",
|
||||
})
|
||||
sdkErr := requireSDKError(t, err, http.StatusPreconditionFailed)
|
||||
@@ -5817,7 +5817,7 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
err := client.DeleteAIProvider(ctx, modelConfig.AIProviderID.String())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
_, err = client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
Model: "gpt-4o-different",
|
||||
})
|
||||
sdkErr := requireSDKError(t, err, http.StatusPreconditionFailed)
|
||||
@@ -5832,7 +5832,7 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
_ = coderdtest.CreateFirstUser(t, client.Client)
|
||||
modelConfig := createChatModelConfig(t, client)
|
||||
|
||||
_, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
_, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
CompressionThreshold: ptr.Ref(int32(150)),
|
||||
})
|
||||
sdkErr := requireSDKError(t, err, http.StatusBadRequest)
|
||||
@@ -5847,7 +5847,7 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
_ = coderdtest.CreateFirstUser(t, client.Client)
|
||||
modelConfig := createChatModelConfig(t, client)
|
||||
|
||||
updated, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
updated, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
CompressionThreshold: ptr.Ref(int32(55)),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -5862,7 +5862,7 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
_ = coderdtest.CreateFirstUser(t, client.Client)
|
||||
modelConfig := createChatModelConfig(t, client)
|
||||
|
||||
updated, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
updated, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
ModelConfig: &codersdk.ChatModelCallConfig{
|
||||
ReasoningEffort: &codersdk.ChatModelReasoningEffortConfig{
|
||||
Default: ptr.Ref("high"),
|
||||
@@ -5888,7 +5888,7 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
ctx,
|
||||
http.MethodPatch,
|
||||
"/api/experimental/chats/model-configs/not-a-uuid",
|
||||
codersdk.UpdateChatModelConfigRequest{DisplayName: "ignored"},
|
||||
codersdk.UpdateChatModelRequest{DisplayName: "ignored"},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer res.Body.Close()
|
||||
@@ -5908,7 +5908,7 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
||||
memberClient := codersdk.NewExperimentalClient(memberClientRaw)
|
||||
|
||||
modelConfig := createChatModelConfig(t, adminClient)
|
||||
_, err := memberClient.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
_, err := memberClient.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
DisplayName: "member update",
|
||||
})
|
||||
requireSDKError(t, err, http.StatusForbidden)
|
||||
@@ -5926,10 +5926,10 @@ func TestDeleteChatModelConfig(t *testing.T) {
|
||||
_ = coderdtest.CreateFirstUser(t, client.Client)
|
||||
modelConfig := createChatModelConfig(t, client)
|
||||
|
||||
err := client.DeleteChatModelConfig(ctx, modelConfig.ID)
|
||||
err := client.DeleteChatModel(ctx, modelConfig.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
configs, err := client.ListChatModelConfigs(ctx)
|
||||
configs, err := client.ChatModels(ctx)
|
||||
require.NoError(t, err)
|
||||
for _, config := range configs {
|
||||
require.NotEqual(t, modelConfig.ID, config.ID)
|
||||
@@ -5949,7 +5949,7 @@ func TestDeleteChatModelConfig(t *testing.T) {
|
||||
AIProviderID: uuid.NullUUID{UUID: defaultConfig.AIProviderID, Valid: true},
|
||||
})
|
||||
|
||||
err := client.DeleteChatModelConfig(ctx, otherConfig.ID)
|
||||
err := client.DeleteChatModel(ctx, otherConfig.ID)
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
_, err = db.GetChatModelConfigByID(dbauthz.AsSystemRestricted(ctx), otherConfig.ID)
|
||||
require.NoError(t, err)
|
||||
@@ -5976,7 +5976,7 @@ func TestDeleteChatModelConfig(t *testing.T) {
|
||||
IsDefault: true,
|
||||
})
|
||||
|
||||
err := client.DeleteChatModelConfig(ctx, defaultConfig.ID)
|
||||
err := client.DeleteChatModel(ctx, defaultConfig.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
orgDefault, err := db.GetDefaultChatModelConfig(dbauthz.AsSystemRestricted(ctx), firstUser.OrganizationID)
|
||||
@@ -6013,10 +6013,10 @@ func TestDeleteChatModelConfig(t *testing.T) {
|
||||
"z-enabled-model",
|
||||
)
|
||||
|
||||
err := client.DeleteChatModelConfig(ctx, defaultConfig.ID)
|
||||
err := client.DeleteChatModel(ctx, defaultConfig.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
configs, err := client.ListChatModelConfigs(ctx)
|
||||
configs, err := client.ChatModels(ctx)
|
||||
require.NoError(t, err)
|
||||
defaultID := uuid.Nil
|
||||
for _, config := range configs {
|
||||
@@ -6034,7 +6034,7 @@ func TestDeleteChatModelConfig(t *testing.T) {
|
||||
client := newChatClient(t)
|
||||
_ = coderdtest.CreateFirstUser(t, client.Client)
|
||||
|
||||
err := client.DeleteChatModelConfig(ctx, uuid.New())
|
||||
err := client.DeleteChatModel(ctx, uuid.New())
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
})
|
||||
|
||||
@@ -6048,7 +6048,7 @@ func TestDeleteChatModelConfig(t *testing.T) {
|
||||
|
||||
store.armFailNextDelete(modelConfig.ID)
|
||||
|
||||
err := client.DeleteChatModelConfig(ctx, modelConfig.ID)
|
||||
err := client.DeleteChatModel(ctx, modelConfig.ID)
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
})
|
||||
|
||||
@@ -6083,7 +6083,7 @@ func TestDeleteChatModelConfig(t *testing.T) {
|
||||
memberClient := codersdk.NewExperimentalClient(memberClientRaw)
|
||||
|
||||
modelConfig := createChatModelConfig(t, adminClient)
|
||||
err := memberClient.DeleteChatModelConfig(ctx, modelConfig.ID)
|
||||
err := memberClient.DeleteChatModel(ctx, modelConfig.ID)
|
||||
requireSDKError(t, err, http.StatusForbidden)
|
||||
})
|
||||
}
|
||||
@@ -13307,7 +13307,7 @@ func seedPasteOnlyTitleSourceMessage(
|
||||
func createTitleGenerationModelConfig(
|
||||
t *testing.T,
|
||||
client *codersdk.ExperimentalClient,
|
||||
) codersdk.ChatModelConfig {
|
||||
) codersdk.ChatModel {
|
||||
t.Helper()
|
||||
return createAdditionalChatModelConfig(t, client, "openai", "gpt-4.1")
|
||||
}
|
||||
@@ -13339,21 +13339,35 @@ func seedChatWithDeletedModelConfig(
|
||||
return chat
|
||||
}
|
||||
|
||||
func createChatModelConfig(t testing.TB, client *codersdk.ExperimentalClient) codersdk.ChatModelConfig {
|
||||
func createChatModelConfig(t testing.TB, client *codersdk.ExperimentalClient) codersdk.ChatModel {
|
||||
t.Helper()
|
||||
return coderdtest.CreateOpenAICompatChatModelConfig(t, client, "")
|
||||
return coderdtest.CreateOpenAICompatChatModel(t, client, "")
|
||||
}
|
||||
|
||||
func createChatModelConfigWithBaseURL(t testing.TB, client *codersdk.ExperimentalClient, baseURL string) codersdk.ChatModelConfig {
|
||||
// createChatModel bridges the SDK-layer name while the later API layer owns
|
||||
// the full helper rename.
|
||||
func createChatModel(t testing.TB, client *codersdk.ExperimentalClient) codersdk.ChatModel {
|
||||
t.Helper()
|
||||
return coderdtest.CreateOpenAICompatChatModelConfig(t, client, baseURL)
|
||||
return createChatModelConfig(t, client)
|
||||
}
|
||||
|
||||
// createChatModelWithBaseURL bridges the SDK-layer name while the later API
|
||||
// layer owns the full helper rename.
|
||||
func createChatModelWithBaseURL(t testing.TB, client *codersdk.ExperimentalClient, baseURL string) codersdk.ChatModel {
|
||||
t.Helper()
|
||||
return createChatModelConfigWithBaseURL(t, client, baseURL)
|
||||
}
|
||||
|
||||
func createChatModelConfigWithBaseURL(t testing.TB, client *codersdk.ExperimentalClient, baseURL string) codersdk.ChatModel {
|
||||
t.Helper()
|
||||
return coderdtest.CreateOpenAICompatChatModel(t, client, baseURL)
|
||||
}
|
||||
|
||||
// createChatModelConfigWithTitleFailure provisions a model whose streaming chat
|
||||
// responses succeed, while non-streaming requests fail. The non-streaming path
|
||||
// is how quick title generation requests structured output, so tests can fail
|
||||
// title generation without breaking the main assistant response.
|
||||
func createChatModelConfigWithTitleFailure(t testing.TB, client *codersdk.ExperimentalClient) codersdk.ChatModelConfig {
|
||||
func createChatModelConfigWithTitleFailure(t testing.TB, client *codersdk.ExperimentalClient) codersdk.ChatModel {
|
||||
t.Helper()
|
||||
baseURL := chattest.NewOpenAI(t, func(req *chattest.OpenAIRequest) chattest.OpenAIResponse {
|
||||
if req.Stream {
|
||||
@@ -13367,7 +13381,7 @@ func createChatModelConfigWithTitleFailure(t testing.TB, client *codersdk.Experi
|
||||
// createChatModelConfigWithTitleQuotaExhausted provisions a model whose
|
||||
// non-streaming responses return a provider insufficient_quota error, which
|
||||
// classifies as a usage limit like an exhausted AI Gateway budget.
|
||||
func createChatModelConfigWithTitleQuotaExhausted(t testing.TB, client *codersdk.ExperimentalClient) codersdk.ChatModelConfig {
|
||||
func createChatModelConfigWithTitleQuotaExhausted(t testing.TB, client *codersdk.ExperimentalClient) codersdk.ChatModel {
|
||||
t.Helper()
|
||||
baseURL := chattest.NewOpenAI(t, func(req *chattest.OpenAIRequest) chattest.OpenAIResponse {
|
||||
if req.Stream {
|
||||
@@ -13382,12 +13396,24 @@ func createChatModelConfigWithTitleQuotaExhausted(t testing.TB, client *codersdk
|
||||
return createChatModelConfigWithBaseURL(t, client, baseURL)
|
||||
}
|
||||
|
||||
// createAdditionalChatModel bridges the SDK-layer name while the later API
|
||||
// layer owns the full helper rename.
|
||||
func createAdditionalChatModel(
|
||||
t *testing.T,
|
||||
client *codersdk.ExperimentalClient,
|
||||
provider string,
|
||||
model string,
|
||||
) codersdk.ChatModel {
|
||||
t.Helper()
|
||||
return createAdditionalChatModelConfig(t, client, provider, model)
|
||||
}
|
||||
|
||||
func createAdditionalChatModelConfig(
|
||||
t *testing.T,
|
||||
client *codersdk.ExperimentalClient,
|
||||
provider string,
|
||||
model string,
|
||||
) codersdk.ChatModelConfig {
|
||||
) codersdk.ChatModel {
|
||||
t.Helper()
|
||||
return createAdditionalChatModelConfigWithModelConfig(t, client, provider, model, nil)
|
||||
}
|
||||
@@ -13399,7 +13425,7 @@ func createAdditionalChatModelConfigWithReasoningEffort(
|
||||
model string,
|
||||
defaultEffort string,
|
||||
maxEffort string,
|
||||
) codersdk.ChatModelConfig {
|
||||
) codersdk.ChatModel {
|
||||
t.Helper()
|
||||
return createAdditionalChatModelConfigWithModelConfig(t, client, provider, model, &codersdk.ChatModelCallConfig{
|
||||
ReasoningEffort: &codersdk.ChatModelReasoningEffortConfig{
|
||||
@@ -13415,14 +13441,14 @@ func createAdditionalChatModelConfigWithModelConfig(
|
||||
provider string,
|
||||
model string,
|
||||
modelCallConfig *codersdk.ChatModelCallConfig,
|
||||
) codersdk.ChatModelConfig {
|
||||
) codersdk.ChatModel {
|
||||
t.Helper()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
aiProvider := createAIProviderForTest(t, client, provider, "test-api-key")
|
||||
contextLimit := int64(4096)
|
||||
isDefault := false
|
||||
modelConfig, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
modelConfig, err := client.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &aiProvider.ID,
|
||||
Model: model,
|
||||
ContextLimit: &contextLimit,
|
||||
@@ -13438,12 +13464,12 @@ func createDisabledChatModelConfig(
|
||||
client *codersdk.ExperimentalClient,
|
||||
provider string,
|
||||
model string,
|
||||
) codersdk.ChatModelConfig {
|
||||
) codersdk.ChatModel {
|
||||
t.Helper()
|
||||
|
||||
modelConfig := createAdditionalChatModelConfig(t, client, provider, model)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
updated, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{
|
||||
updated, err := client.UpdateChatModel(ctx, modelConfig.ID, codersdk.UpdateChatModelRequest{
|
||||
Enabled: ptr.Ref(false),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -13457,7 +13483,7 @@ func createProviderDisabledChatModelConfig(
|
||||
client *codersdk.ExperimentalClient,
|
||||
provider string,
|
||||
model string,
|
||||
) codersdk.ChatModelConfig {
|
||||
) codersdk.ChatModel {
|
||||
t.Helper()
|
||||
|
||||
modelConfig := createAdditionalChatModelConfig(t, client, provider, model)
|
||||
@@ -15195,12 +15221,12 @@ func TestUserChatPersonalModelOverrides(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
contextLimit := int64(4096)
|
||||
modelConfigRequest := codersdk.CreateChatModelConfigRequest{
|
||||
modelConfigRequest := codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &modelProvider.ID,
|
||||
Model: "claude-personal-" + uuid.NewString(),
|
||||
ContextLimit: &contextLimit,
|
||||
}
|
||||
modelConfig, err := adminClient.CreateChatModelConfig(ctx, modelConfigRequest)
|
||||
modelConfig, err := adminClient.CreateChatModel(ctx, modelConfigRequest)
|
||||
require.NoError(t, err)
|
||||
modelConfigRequest.Model = "claude-personal-reasoning-" + uuid.NewString()
|
||||
modelConfigRequest.ModelConfig = &codersdk.ChatModelCallConfig{
|
||||
@@ -15209,7 +15235,7 @@ func TestUserChatPersonalModelOverrides(t *testing.T) {
|
||||
Max: ptr.Ref("high"),
|
||||
},
|
||||
}
|
||||
reasoningModelConfig, err := adminClient.CreateChatModelConfig(ctx, modelConfigRequest)
|
||||
reasoningModelConfig, err := adminClient.CreateChatModel(ctx, modelConfigRequest)
|
||||
require.NoError(t, err)
|
||||
err = adminClient.UpdateChatModelOverride(ctx, codersdk.ChatModelOverrideContextGeneral, codersdk.UpdateChatModelOverrideRequest{
|
||||
ModelConfigID: modelConfig.ID.String(),
|
||||
@@ -15228,7 +15254,7 @@ func TestUserChatPersonalModelOverrides(t *testing.T) {
|
||||
)
|
||||
disabledProvider := createAIProviderForTest(t, adminClient, "google", "test-api-key")
|
||||
contextLimit = int64(4096)
|
||||
disabledProviderModelConfig, err := adminClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
disabledProviderModelConfig, err := adminClient.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &disabledProvider.ID,
|
||||
Model: "gemini-personal-disabled-provider-" + uuid.NewString(),
|
||||
ContextLimit: &contextLimit,
|
||||
@@ -15669,7 +15695,7 @@ func TestCreateChatPersonalModelOverrideRoot(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
contextLimit := int64(4096)
|
||||
overrideModel, err := adminClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
overrideModel, err := adminClient.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &overrideProvider.ID,
|
||||
Model: "claude-root-personal-" + uuid.NewString(),
|
||||
ContextLimit: &contextLimit,
|
||||
@@ -15776,7 +15802,7 @@ func TestCreateChatPersonalModelOverrideRoot(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("RootModelOverrideUsesSavedReasoningEffort", func(t *testing.T) {
|
||||
reasoningModel, err := adminClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
reasoningModel, err := adminClient.CreateChatModel(ctx, codersdk.CreateChatModelRequest{
|
||||
AIProviderID: &overrideProvider.ID,
|
||||
Model: "claude-root-personal-reasoning-" + uuid.NewString(),
|
||||
ContextLimit: &contextLimit,
|
||||
|
||||
+3
-3
@@ -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) {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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{},
|
||||
}}},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Generated
+3
-3
@@ -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).
|
||||
|
||||
|
||||
Generated
+37
-37
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
Generated
+62
-51
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
+6
-6
@@ -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 ?? [],
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
+30
-30
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user