mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add configurable reasoning effort to Coder agents (#26974)
This commit is contained in:
+280
-102
@@ -71,6 +71,8 @@ const (
|
||||
maxSystemPromptLenBytes = 131072 // 128 KiB
|
||||
)
|
||||
|
||||
var allowedReasoningEffortValues = strings.Join(codersdk.ChatModelReasoningEffortValues(), ", ")
|
||||
|
||||
// chatGitRef holds the branch, remote origin, and optional chat
|
||||
// ID reported by the workspace agent during a git operation.
|
||||
type chatGitRef struct {
|
||||
@@ -519,24 +521,40 @@ func validateChatPlanMode(mode codersdk.ChatPlanMode) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func parseChatModelOverride(raw string) (*uuid.UUID, error) {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if trimmed == "" {
|
||||
//nolint:nilnil // Empty site-config value means the override is unset.
|
||||
return nil, nil
|
||||
}
|
||||
modelConfigID, err := uuid.Parse(trimmed)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("parse chat model override: %w", err)
|
||||
}
|
||||
return &modelConfigID, nil
|
||||
type parsedChatModelOverride struct {
|
||||
modelConfigID *uuid.UUID
|
||||
reasoningEffort *string
|
||||
}
|
||||
|
||||
func formatChatModelOverride(id *uuid.UUID) string {
|
||||
func parseChatModelOverride(raw string) (parsedChatModelOverride, error) {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if trimmed == "" {
|
||||
return parsedChatModelOverride{}, nil
|
||||
}
|
||||
rawID, rawEffort, hasEffort := strings.Cut(trimmed, ":")
|
||||
modelConfigID, err := uuid.Parse(rawID)
|
||||
if err != nil {
|
||||
return parsedChatModelOverride{}, xerrors.Errorf("parse chat model override: %w", err)
|
||||
}
|
||||
if hasEffort && rawEffort == "" {
|
||||
return parsedChatModelOverride{}, xerrors.New("parse chat model override: reasoning effort is empty")
|
||||
}
|
||||
parsed := parsedChatModelOverride{modelConfigID: &modelConfigID}
|
||||
if hasEffort {
|
||||
parsed.reasoningEffort = &rawEffort
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func formatChatModelOverride(id *uuid.UUID, effort *string) string {
|
||||
if id == nil {
|
||||
return ""
|
||||
}
|
||||
return id.String()
|
||||
formatted := id.String()
|
||||
if effort != nil {
|
||||
formatted += ":" + *effort
|
||||
}
|
||||
return formatted
|
||||
}
|
||||
|
||||
func lookupEnabledChatModelConfigByID(
|
||||
@@ -549,12 +567,65 @@ func lookupEnabledChatModelConfigByID(
|
||||
return db.GetEnabledChatModelConfigByID(dbauthz.AsChatd(ctx), id)
|
||||
}
|
||||
|
||||
func validateChatModelOverrideID(
|
||||
func parseChatModelCallConfig(options json.RawMessage) (*codersdk.ChatModelCallConfig, error) {
|
||||
callConfig := &codersdk.ChatModelCallConfig{}
|
||||
if len(options) == 0 {
|
||||
return callConfig, nil
|
||||
}
|
||||
if err := json.Unmarshal(options, callConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return callConfig, nil
|
||||
}
|
||||
|
||||
func validateChatModelOverrideEffort(
|
||||
modelConfig database.ChatModelConfig,
|
||||
effort *string,
|
||||
) (int, *codersdk.Response) {
|
||||
if effort == nil {
|
||||
return 0, nil
|
||||
}
|
||||
if !chatprovider.IsValidReasoningEffort(*effort) {
|
||||
return http.StatusBadRequest, &codersdk.Response{
|
||||
Message: "Invalid reasoning_effort value.",
|
||||
Detail: "Must be one of none, minimal, low, medium, high, xhigh, max.",
|
||||
}
|
||||
}
|
||||
callConfig, err := parseChatModelCallConfig(modelConfig.Options)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, &codersdk.Response{
|
||||
Message: "Internal error validating reasoning effort.",
|
||||
Detail: err.Error(),
|
||||
}
|
||||
}
|
||||
selectableEfforts := chatprovider.SelectableReasoningEfforts(callConfig.ReasoningEffort)
|
||||
if len(selectableEfforts) == 0 {
|
||||
return http.StatusBadRequest, &codersdk.Response{
|
||||
Message: "Invalid reasoning_effort value.",
|
||||
Detail: "This model does not support reasoning effort.",
|
||||
}
|
||||
}
|
||||
if !slices.Contains(selectableEfforts, *effort) {
|
||||
return http.StatusBadRequest, &codersdk.Response{
|
||||
Message: "Invalid reasoning_effort value.",
|
||||
Detail: "Must be one of " + strings.Join(selectableEfforts, ", ") + ".",
|
||||
}
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func validateChatModelOverride(
|
||||
ctx context.Context,
|
||||
db database.Store,
|
||||
id *uuid.UUID,
|
||||
effort *string,
|
||||
) (int, *codersdk.Response) {
|
||||
if id == nil {
|
||||
if effort != nil {
|
||||
return http.StatusBadRequest, &codersdk.Response{
|
||||
Message: "reasoning_effort requires model_config_id.",
|
||||
}
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
if *id == uuid.Nil {
|
||||
@@ -562,31 +633,31 @@ func validateChatModelOverrideID(
|
||||
Message: "Invalid model_config_id.",
|
||||
}
|
||||
}
|
||||
_, err := lookupEnabledChatModelConfigByID(ctx, db, *id)
|
||||
if err == nil {
|
||||
return 0, nil
|
||||
}
|
||||
if xerrors.Is(err, sql.ErrNoRows) {
|
||||
return http.StatusBadRequest, &codersdk.Response{
|
||||
Message: "Invalid model_config_id.",
|
||||
modelConfig, err := lookupEnabledChatModelConfigByID(ctx, db, *id)
|
||||
if err != nil {
|
||||
if xerrors.Is(err, sql.ErrNoRows) {
|
||||
return http.StatusBadRequest, &codersdk.Response{
|
||||
Message: "Invalid model_config_id.",
|
||||
}
|
||||
}
|
||||
return http.StatusInternalServerError, &codersdk.Response{
|
||||
Message: "Internal error validating model config override.",
|
||||
Detail: err.Error(),
|
||||
}
|
||||
}
|
||||
return http.StatusInternalServerError, &codersdk.Response{
|
||||
Message: "Internal error validating model config override.",
|
||||
Detail: err.Error(),
|
||||
}
|
||||
return validateChatModelOverrideEffort(modelConfig, effort)
|
||||
}
|
||||
|
||||
func (api *API) getChatModelOverrideConfig(
|
||||
ctx context.Context,
|
||||
settingName string,
|
||||
getter func(context.Context) (string, error),
|
||||
) (*uuid.UUID, bool, error) {
|
||||
) (*uuid.UUID, *string, bool, error) {
|
||||
raw, err := getter(ctx)
|
||||
if err != nil {
|
||||
return nil, false, xerrors.Errorf("get %s model override: %w", settingName, err)
|
||||
return nil, nil, false, xerrors.Errorf("get %s model override: %w", settingName, err)
|
||||
}
|
||||
id, err := parseChatModelOverride(raw)
|
||||
parsed, err := parseChatModelOverride(raw)
|
||||
if err != nil {
|
||||
// Degrade malformed values to unset so the admin settings page
|
||||
// remains accessible and the bad value can be cleared.
|
||||
@@ -597,9 +668,9 @@ func (api *API) getChatModelOverrideConfig(
|
||||
slog.F("raw_value", raw),
|
||||
slog.Error(err),
|
||||
)
|
||||
return nil, true, nil
|
||||
return nil, nil, true, nil
|
||||
}
|
||||
return id, false, nil
|
||||
return parsed.modelConfigID, parsed.reasoningEffort, false, nil
|
||||
}
|
||||
|
||||
func parseChatModelOverrideContext(raw string) (codersdk.ChatModelOverrideContext, error) {
|
||||
@@ -649,25 +720,26 @@ func (api *API) chatModelOverrideSiteConfig(
|
||||
func (api *API) readChatModelOverrideConfig(
|
||||
ctx context.Context,
|
||||
overrideContext codersdk.ChatModelOverrideContext,
|
||||
) (*uuid.UUID, bool, string, error) {
|
||||
) (*uuid.UUID, *string, bool, string, error) {
|
||||
siteConfig, err := api.chatModelOverrideSiteConfig(overrideContext)
|
||||
if err != nil {
|
||||
return nil, false, "", err
|
||||
return nil, nil, false, "", err
|
||||
}
|
||||
id, isMalformed, err := api.getChatModelOverrideConfig(ctx, siteConfig.label, siteConfig.getter)
|
||||
return id, isMalformed, siteConfig.label, err
|
||||
id, effort, isMalformed, err := api.getChatModelOverrideConfig(ctx, siteConfig.label, siteConfig.getter)
|
||||
return id, effort, isMalformed, siteConfig.label, err
|
||||
}
|
||||
|
||||
func (api *API) upsertChatModelOverrideConfig(
|
||||
ctx context.Context,
|
||||
overrideContext codersdk.ChatModelOverrideContext,
|
||||
modelConfigID *uuid.UUID,
|
||||
reasoningEffort *string,
|
||||
) (string, error) {
|
||||
siteConfig, err := api.chatModelOverrideSiteConfig(overrideContext)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return siteConfig.label, siteConfig.upsert(ctx, formatChatModelOverride(modelConfigID))
|
||||
return siteConfig.label, siteConfig.upsert(ctx, formatChatModelOverride(modelConfigID, reasoningEffort))
|
||||
}
|
||||
|
||||
var chatPersonalModelOverrideContexts = []codersdk.ChatPersonalModelOverrideContext{
|
||||
@@ -717,9 +789,14 @@ func parseChatPersonalModelOverrideValue(
|
||||
func formatChatPersonalModelOverrideValue(
|
||||
mode codersdk.ChatPersonalModelOverrideMode,
|
||||
modelConfigID string,
|
||||
reasoningEffort *string,
|
||||
) string {
|
||||
if mode == codersdk.ChatPersonalModelOverrideModeModel {
|
||||
return string(mode) + ":" + strings.TrimSpace(modelConfigID)
|
||||
value := string(mode) + ":" + strings.TrimSpace(modelConfigID)
|
||||
if reasoningEffort != nil {
|
||||
value += ":" + *reasoningEffort
|
||||
}
|
||||
return value
|
||||
}
|
||||
return string(mode)
|
||||
}
|
||||
@@ -731,15 +808,18 @@ func chatPersonalModelOverrideResponse(
|
||||
) codersdk.ChatPersonalModelOverride {
|
||||
parsed := parseChatPersonalModelOverrideValue(raw, overrideContext)
|
||||
modelConfigID := ""
|
||||
var reasoningEffort *string
|
||||
if parsed.Mode == codersdk.ChatPersonalModelOverrideModeModel {
|
||||
modelConfigID = parsed.ModelConfigID.String()
|
||||
reasoningEffort = parsed.ReasoningEffort
|
||||
}
|
||||
return codersdk.ChatPersonalModelOverride{
|
||||
Context: overrideContext,
|
||||
Mode: parsed.Mode,
|
||||
ModelConfigID: modelConfigID,
|
||||
IsSet: isSet,
|
||||
IsMalformed: parsed.Malformed,
|
||||
Context: overrideContext,
|
||||
Mode: parsed.Mode,
|
||||
ModelConfigID: modelConfigID,
|
||||
ReasoningEffort: reasoningEffort,
|
||||
IsSet: isSet,
|
||||
IsMalformed: parsed.Malformed,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -751,7 +831,7 @@ func (api *API) chatPersonalModelOverrideDeploymentDefaultResponse(
|
||||
// resources. Users may read these values here because the personal settings
|
||||
// UI must explain what deployment_default resolves to.
|
||||
//nolint:gocritic // System context is required to read deployment config.
|
||||
modelConfigID, isMalformed, _, err := api.readChatModelOverrideConfig(
|
||||
modelConfigID, reasoningEffort, isMalformed, _, err := api.readChatModelOverrideConfig(
|
||||
dbauthz.AsSystemRestricted(ctx),
|
||||
overrideContext,
|
||||
)
|
||||
@@ -759,9 +839,10 @@ func (api *API) chatPersonalModelOverrideDeploymentDefaultResponse(
|
||||
return codersdk.ChatModelOverrideResponse{}, err
|
||||
}
|
||||
return codersdk.ChatModelOverrideResponse{
|
||||
Context: overrideContext,
|
||||
ModelConfigID: formatChatModelOverride(modelConfigID),
|
||||
IsMalformed: isMalformed,
|
||||
Context: overrideContext,
|
||||
ModelConfigID: formatChatModelOverride(modelConfigID, nil),
|
||||
ReasoningEffort: reasoningEffort,
|
||||
IsMalformed: isMalformed,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -946,9 +1027,9 @@ func (api *API) userCanUseChatModelConfig(
|
||||
ctx context.Context,
|
||||
userID uuid.UUID,
|
||||
modelConfigID uuid.UUID,
|
||||
) (chatModelConfigUnavailableReason, error) {
|
||||
) (database.ChatModelConfig, chatModelConfigUnavailableReason, error) {
|
||||
if modelConfigID == uuid.Nil {
|
||||
return chatModelConfigUnavailableModelNotFoundOrDisabled, nil
|
||||
return database.ChatModelConfig{}, chatModelConfigUnavailableModelNotFoundOrDisabled, nil
|
||||
}
|
||||
//nolint:gocritic // Non-admin users need deployment config validation.
|
||||
model, err := api.Database.GetChatModelConfigByID(
|
||||
@@ -957,63 +1038,63 @@ func (api *API) userCanUseChatModelConfig(
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) || httpapi.Is404Error(err) {
|
||||
return chatModelConfigUnavailableModelNotFoundOrDisabled, nil
|
||||
return database.ChatModelConfig{}, chatModelConfigUnavailableModelNotFoundOrDisabled, nil
|
||||
}
|
||||
return chatModelConfigAvailable, err
|
||||
return database.ChatModelConfig{}, chatModelConfigAvailable, err
|
||||
}
|
||||
if !model.Enabled {
|
||||
return chatModelConfigUnavailableModelNotFoundOrDisabled, nil
|
||||
return database.ChatModelConfig{}, chatModelConfigUnavailableModelNotFoundOrDisabled, nil
|
||||
}
|
||||
|
||||
availability, err := api.getUserChatProviderAvailability(ctx, userID)
|
||||
if err != nil {
|
||||
return chatModelConfigAvailable, err
|
||||
return database.ChatModelConfig{}, chatModelConfigAvailable, err
|
||||
}
|
||||
if model.AIProviderID.Valid {
|
||||
providerID := model.AIProviderID.UUID
|
||||
if _, ok := availability.enabledProviderIDs[providerID]; !ok {
|
||||
return chatModelConfigUnavailableProviderDisabled, nil
|
||||
return database.ChatModelConfig{}, chatModelConfigUnavailableProviderDisabled, nil
|
||||
}
|
||||
providerStatus, ok := availability.providerStatusByID[providerID]
|
||||
if !ok {
|
||||
return chatModelConfigUnavailableProviderDisabled, nil
|
||||
return database.ChatModelConfig{}, chatModelConfigUnavailableProviderDisabled, nil
|
||||
}
|
||||
if !providerStatus.Available {
|
||||
return chatModelConfigUnavailableCredentialsMissing, nil
|
||||
return database.ChatModelConfig{}, chatModelConfigUnavailableCredentialsMissing, nil
|
||||
}
|
||||
return chatModelConfigAvailable, nil
|
||||
return model, chatModelConfigAvailable, nil
|
||||
}
|
||||
// Active configs always carry a provider FK (CHECK
|
||||
// chat_model_configs_ai_provider_required_when_active), so an unset FK
|
||||
// means the config is not usable.
|
||||
return chatModelConfigUnavailableModelNotFoundOrDisabled, nil
|
||||
return database.ChatModelConfig{}, chatModelConfigUnavailableModelNotFoundOrDisabled, nil
|
||||
}
|
||||
|
||||
func (api *API) validateUserChatModelConfigAvailable(
|
||||
ctx context.Context,
|
||||
userID uuid.UUID,
|
||||
modelConfigID uuid.UUID,
|
||||
) (int, *codersdk.Response) {
|
||||
reason, err := api.userCanUseChatModelConfig(ctx, userID, modelConfigID)
|
||||
) (database.ChatModelConfig, int, *codersdk.Response) {
|
||||
modelConfig, reason, err := api.userCanUseChatModelConfig(ctx, userID, modelConfigID)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, &codersdk.Response{
|
||||
return database.ChatModelConfig{}, http.StatusInternalServerError, &codersdk.Response{
|
||||
Message: "Internal error validating model config override.",
|
||||
Detail: err.Error(),
|
||||
}
|
||||
}
|
||||
switch reason {
|
||||
case chatModelConfigAvailable:
|
||||
return 0, nil
|
||||
return modelConfig, 0, nil
|
||||
case chatModelConfigUnavailableModelNotFoundOrDisabled:
|
||||
return http.StatusBadRequest, &codersdk.Response{
|
||||
return database.ChatModelConfig{}, http.StatusBadRequest, &codersdk.Response{
|
||||
Message: "Invalid model_config_id: model config not found or disabled.",
|
||||
}
|
||||
case chatModelConfigUnavailableCredentialsMissing:
|
||||
return http.StatusBadRequest, &codersdk.Response{
|
||||
return database.ChatModelConfig{}, http.StatusBadRequest, &codersdk.Response{
|
||||
Message: "Invalid model_config_id: provider credentials unavailable for this model.",
|
||||
}
|
||||
case chatModelConfigUnavailableProviderDisabled:
|
||||
return http.StatusBadRequest, &codersdk.Response{
|
||||
return database.ChatModelConfig{}, http.StatusBadRequest, &codersdk.Response{
|
||||
Message: "Invalid model_config_id: provider is not enabled for this model.",
|
||||
}
|
||||
default:
|
||||
@@ -1023,7 +1104,7 @@ func (api *API) validateUserChatModelConfigAvailable(
|
||||
slog.F("model_config_id", modelConfigID),
|
||||
slog.F("reason", reason),
|
||||
)
|
||||
return http.StatusBadRequest, &codersdk.Response{
|
||||
return database.ChatModelConfig{}, http.StatusBadRequest, &codersdk.Response{
|
||||
Message: "Invalid model_config_id.",
|
||||
}
|
||||
}
|
||||
@@ -1120,7 +1201,7 @@ func (api *API) postChats(rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
title := chatprompt.FallbackTitle(titleSource)
|
||||
|
||||
modelConfigID, modelConfigStatus, modelConfigError := api.resolveCreateChatModelConfigID(ctx, apiKey.UserID, req)
|
||||
modelConfigID, personalOverrideEffort, modelConfigStatus, modelConfigError := api.resolveCreateChatModelConfigID(ctx, apiKey.UserID, req)
|
||||
if modelConfigError != nil {
|
||||
httpapi.Write(ctx, rw, modelConfigStatus, *modelConfigError)
|
||||
return
|
||||
@@ -1236,12 +1317,22 @@ func (api *API) postChats(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
reasoningEffort := req.ReasoningEffort
|
||||
if reasoningEffort == nil {
|
||||
reasoningEffort = personalOverrideEffort
|
||||
}
|
||||
if reasoningEffort != nil && !chatprovider.IsValidReasoningEffort(*reasoningEffort) {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, invalidReasoningEffortResponse(*reasoningEffort))
|
||||
return
|
||||
}
|
||||
|
||||
chat, err := api.chatDaemon.CreateChat(ctx, chatd.CreateOptions{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OwnerID: apiKey.UserID,
|
||||
WorkspaceID: workspaceSelection.WorkspaceID,
|
||||
Title: title,
|
||||
ModelConfigID: modelConfigID,
|
||||
ReasoningEffort: reasoningEffort,
|
||||
PlanMode: planModeToNullChatPlanMode(req.PlanMode),
|
||||
ClientType: clientType,
|
||||
SystemPrompt: req.SystemPrompt,
|
||||
@@ -3155,17 +3246,24 @@ func (api *API) postChatMessages(rw http.ResponseWriter, r *http.Request) {
|
||||
modelConfigID = *req.ModelConfigID
|
||||
}
|
||||
|
||||
reasoningEffort := req.ReasoningEffort
|
||||
if reasoningEffort != nil && !chatprovider.IsValidReasoningEffort(*reasoningEffort) {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, invalidReasoningEffortResponse(*reasoningEffort))
|
||||
return
|
||||
}
|
||||
|
||||
sendResult, sendErr := api.chatDaemon.SendMessage(
|
||||
ctx,
|
||||
chatd.SendMessageOptions{
|
||||
ChatID: chatID,
|
||||
CreatedBy: apiKey.UserID,
|
||||
Content: contentBlocks,
|
||||
ModelConfigID: modelConfigID,
|
||||
APIKeyID: apiKey.ID,
|
||||
BusyBehavior: busyBehavior,
|
||||
PlanMode: sendPlanMode,
|
||||
MCPServerIDs: req.MCPServerIDs,
|
||||
ChatID: chatID,
|
||||
CreatedBy: apiKey.UserID,
|
||||
Content: contentBlocks,
|
||||
ModelConfigID: modelConfigID,
|
||||
ReasoningEffort: reasoningEffort,
|
||||
APIKeyID: apiKey.ID,
|
||||
BusyBehavior: busyBehavior,
|
||||
PlanMode: sendPlanMode,
|
||||
MCPServerIDs: req.MCPServerIDs,
|
||||
},
|
||||
)
|
||||
if sendErr != nil {
|
||||
@@ -3313,6 +3411,12 @@ func (api *API) patchChatMessage(rw http.ResponseWriter, r *http.Request) {
|
||||
editModelConfigID = *req.ModelConfigID
|
||||
}
|
||||
|
||||
editReasoningEffort := req.ReasoningEffort
|
||||
if editReasoningEffort != nil && !chatprovider.IsValidReasoningEffort(*editReasoningEffort) {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, invalidReasoningEffortResponse(*editReasoningEffort))
|
||||
return
|
||||
}
|
||||
|
||||
editResult, editErr := api.chatDaemon.EditMessage(ctx, chatd.EditMessageOptions{
|
||||
ChatID: chat.ID,
|
||||
CreatedBy: apiKey.UserID,
|
||||
@@ -3320,6 +3424,7 @@ func (api *API) patchChatMessage(rw http.ResponseWriter, r *http.Request) {
|
||||
Content: contentBlocks,
|
||||
APIKeyID: apiKey.ID,
|
||||
ModelConfigID: editModelConfigID,
|
||||
ReasoningEffort: editReasoningEffort,
|
||||
})
|
||||
if editErr != nil {
|
||||
if maybeWriteLimitErr(ctx, rw, editErr) {
|
||||
@@ -4615,25 +4720,26 @@ func (api *API) resolveCreateChatModelConfigID(
|
||||
ctx context.Context,
|
||||
userID uuid.UUID,
|
||||
req codersdk.CreateChatRequest,
|
||||
) (uuid.UUID, int, *codersdk.Response) {
|
||||
) (uuid.UUID, *string, int, *codersdk.Response) {
|
||||
if req.ModelConfigID != nil {
|
||||
if *req.ModelConfigID == uuid.Nil {
|
||||
return uuid.Nil, http.StatusBadRequest, &codersdk.Response{
|
||||
return uuid.Nil, nil, http.StatusBadRequest, &codersdk.Response{
|
||||
Message: "Invalid model config ID.",
|
||||
}
|
||||
}
|
||||
return *req.ModelConfigID, 0, nil
|
||||
return *req.ModelConfigID, nil, 0, nil
|
||||
}
|
||||
|
||||
personalOverridesEnabled, err := api.Database.GetChatPersonalModelOverridesEnabled(ctx)
|
||||
if err != nil {
|
||||
return uuid.Nil, http.StatusInternalServerError, &codersdk.Response{
|
||||
return uuid.Nil, nil, http.StatusInternalServerError, &codersdk.Response{
|
||||
Message: "Failed to resolve chat model config.",
|
||||
Detail: err.Error(),
|
||||
}
|
||||
}
|
||||
if !personalOverridesEnabled {
|
||||
return api.defaultCreateChatModelConfigID(ctx)
|
||||
id, status, resp := api.defaultCreateChatModelConfigID(ctx)
|
||||
return id, nil, status, resp
|
||||
}
|
||||
|
||||
raw, err := api.Database.GetUserChatPersonalModelOverride(ctx, database.GetUserChatPersonalModelOverrideParams{
|
||||
@@ -4641,7 +4747,7 @@ func (api *API) resolveCreateChatModelConfigID(
|
||||
Key: chatd.ChatPersonalModelOverrideKey(codersdk.ChatPersonalModelOverrideContextRoot),
|
||||
})
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return uuid.Nil, http.StatusInternalServerError, &codersdk.Response{
|
||||
return uuid.Nil, nil, http.StatusInternalServerError, &codersdk.Response{
|
||||
Message: "Failed to resolve chat model config.",
|
||||
Detail: err.Error(),
|
||||
}
|
||||
@@ -4664,19 +4770,19 @@ func (api *API) resolveCreateChatModelConfigID(
|
||||
// For root context, chat_default and the defensive default
|
||||
// case both fall through to the deployment default model below.
|
||||
case codersdk.ChatPersonalModelOverrideModeModel:
|
||||
reason, err := api.userCanUseChatModelConfig(
|
||||
_, reason, err := api.userCanUseChatModelConfig(
|
||||
ctx,
|
||||
userID,
|
||||
parsed.ModelConfigID,
|
||||
)
|
||||
if err != nil {
|
||||
return uuid.Nil, http.StatusInternalServerError, &codersdk.Response{
|
||||
return uuid.Nil, nil, http.StatusInternalServerError, &codersdk.Response{
|
||||
Message: "Failed to resolve chat model config.",
|
||||
Detail: err.Error(),
|
||||
}
|
||||
}
|
||||
if reason == chatModelConfigAvailable {
|
||||
return parsed.ModelConfigID, 0, nil
|
||||
return parsed.ModelConfigID, parsed.ReasoningEffort, 0, nil
|
||||
}
|
||||
api.Logger.Debug(
|
||||
ctx,
|
||||
@@ -4695,7 +4801,8 @@ func (api *API) resolveCreateChatModelConfigID(
|
||||
}
|
||||
}
|
||||
|
||||
return api.defaultCreateChatModelConfigID(ctx)
|
||||
id, status, resp := api.defaultCreateChatModelConfigID(ctx)
|
||||
return id, nil, status, resp
|
||||
}
|
||||
|
||||
func (api *API) defaultCreateChatModelConfigID(
|
||||
@@ -4923,7 +5030,7 @@ func (api *API) getChatModelOverride(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
modelConfigID, isMalformed, label, err := api.readChatModelOverrideConfig(ctx, overrideContext)
|
||||
modelConfigID, reasoningEffort, isMalformed, label, err := api.readChatModelOverrideConfig(ctx, overrideContext)
|
||||
if err != nil {
|
||||
if label == "" {
|
||||
label = string(overrideContext)
|
||||
@@ -4936,9 +5043,10 @@ func (api *API) getChatModelOverride(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
resp := codersdk.ChatModelOverrideResponse{
|
||||
Context: overrideContext,
|
||||
ModelConfigID: formatChatModelOverride(modelConfigID),
|
||||
IsMalformed: isMalformed,
|
||||
Context: overrideContext,
|
||||
ModelConfigID: formatChatModelOverride(modelConfigID, nil),
|
||||
ReasoningEffort: reasoningEffort,
|
||||
IsMalformed: isMalformed,
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, resp)
|
||||
@@ -4961,22 +5069,34 @@ func (api *API) putChatModelOverride(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
modelConfigID, err := parseChatModelOverride(req.ModelConfigID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid model_config_id.",
|
||||
Detail: fmt.Sprintf("Value %q is not a valid UUID.", req.ModelConfigID),
|
||||
})
|
||||
return
|
||||
var modelConfigID *uuid.UUID
|
||||
trimmedModelConfigID := strings.TrimSpace(req.ModelConfigID)
|
||||
if trimmedModelConfigID != "" {
|
||||
if strings.Contains(trimmedModelConfigID, ":") {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid model_config_id.",
|
||||
Detail: fmt.Sprintf("Value %q is not a valid UUID.", req.ModelConfigID),
|
||||
})
|
||||
return
|
||||
}
|
||||
parsedModelConfigID, err := uuid.Parse(trimmedModelConfigID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid model_config_id.",
|
||||
Detail: fmt.Sprintf("Value %q is not a valid UUID.", req.ModelConfigID),
|
||||
})
|
||||
return
|
||||
}
|
||||
modelConfigID = &parsedModelConfigID
|
||||
}
|
||||
|
||||
status, resp := validateChatModelOverrideID(ctx, api.Database, modelConfigID)
|
||||
status, resp := validateChatModelOverride(ctx, api.Database, modelConfigID, req.ReasoningEffort)
|
||||
if resp != nil {
|
||||
httpapi.Write(ctx, rw, status, *resp)
|
||||
return
|
||||
}
|
||||
|
||||
label, err := api.upsertChatModelOverrideConfig(ctx, overrideContext, modelConfigID)
|
||||
label, err := api.upsertChatModelOverrideConfig(ctx, overrideContext, modelConfigID, req.ReasoningEffort)
|
||||
if err != nil {
|
||||
if label == "" {
|
||||
label = string(overrideContext)
|
||||
@@ -5154,6 +5274,7 @@ func (api *API) putUserChatPersonalModelOverride(rw http.ResponseWriter, r *http
|
||||
}
|
||||
|
||||
modelConfigID := ""
|
||||
reasoningEffort := req.ReasoningEffort
|
||||
rawModelConfigID := strings.TrimSpace(req.ModelConfigID)
|
||||
switch req.Mode {
|
||||
case codersdk.ChatPersonalModelOverrideModeChatDefault:
|
||||
@@ -5163,6 +5284,12 @@ func (api *API) putUserChatPersonalModelOverride(rw http.ResponseWriter, r *http
|
||||
})
|
||||
return
|
||||
}
|
||||
if reasoningEffort != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "reasoning_effort requires mode model.",
|
||||
})
|
||||
return
|
||||
}
|
||||
case codersdk.ChatPersonalModelOverrideModeDeploymentDefault:
|
||||
if overrideContext == codersdk.ChatPersonalModelOverrideContextRoot {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
@@ -5176,6 +5303,12 @@ func (api *API) putUserChatPersonalModelOverride(rw http.ResponseWriter, r *http
|
||||
})
|
||||
return
|
||||
}
|
||||
if reasoningEffort != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "reasoning_effort requires mode model.",
|
||||
})
|
||||
return
|
||||
}
|
||||
case codersdk.ChatPersonalModelOverrideModeModel:
|
||||
if rawModelConfigID == "" {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
@@ -5197,7 +5330,12 @@ func (api *API) putUserChatPersonalModelOverride(rw http.ResponseWriter, r *http
|
||||
})
|
||||
return
|
||||
}
|
||||
status, resp := api.validateUserChatModelConfigAvailable(ctx, apiKey.UserID, parsedModelConfigID)
|
||||
modelConfig, status, resp := api.validateUserChatModelConfigAvailable(ctx, apiKey.UserID, parsedModelConfigID)
|
||||
if resp != nil {
|
||||
httpapi.Write(ctx, rw, status, *resp)
|
||||
return
|
||||
}
|
||||
status, resp = validateChatModelOverrideEffort(modelConfig, reasoningEffort)
|
||||
if resp != nil {
|
||||
httpapi.Write(ctx, rw, status, *resp)
|
||||
return
|
||||
@@ -5213,7 +5351,7 @@ func (api *API) putUserChatPersonalModelOverride(rw http.ResponseWriter, r *http
|
||||
if err := api.Database.UpsertUserChatPersonalModelOverride(ctx, database.UpsertUserChatPersonalModelOverrideParams{
|
||||
UserID: apiKey.UserID,
|
||||
Key: chatd.ChatPersonalModelOverrideKey(overrideContext),
|
||||
Value: formatChatPersonalModelOverrideValue(req.Mode, modelConfigID),
|
||||
Value: formatChatPersonalModelOverrideValue(req.Mode, modelConfigID, reasoningEffort),
|
||||
}); err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error updating user personal model override.",
|
||||
@@ -7473,6 +7611,12 @@ func parseChatModelConfigID(rw http.ResponseWriter, r *http.Request) (uuid.UUID,
|
||||
}
|
||||
|
||||
func convertChatModelConfig(config database.ChatModelConfig) codersdk.ChatModelConfig {
|
||||
modelConfig := unmarshalChatModelCallConfig(config.Options)
|
||||
var reasoningEffortConfig *codersdk.ChatModelReasoningEffortConfig
|
||||
if modelConfig != nil {
|
||||
reasoningEffortConfig = modelConfig.ReasoningEffort
|
||||
}
|
||||
|
||||
// Active configs always carry a non-null ai_provider_id (CHECK
|
||||
// chat_model_configs_ai_provider_required_when_active).
|
||||
return codersdk.ChatModelConfig{
|
||||
@@ -7484,15 +7628,14 @@ func convertChatModelConfig(config database.ChatModelConfig) codersdk.ChatModelC
|
||||
IsDefault: config.IsDefault,
|
||||
ContextLimit: config.ContextLimit,
|
||||
CompressionThreshold: config.CompressionThreshold,
|
||||
ModelConfig: unmarshalChatModelCallConfig(config.Options),
|
||||
ModelConfig: modelConfig,
|
||||
ReasoningEfforts: chatprovider.SelectableReasoningEfforts(reasoningEffortConfig),
|
||||
CreatedAt: config.CreatedAt,
|
||||
UpdatedAt: config.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func marshalChatModelCallConfig(
|
||||
modelConfig *codersdk.ChatModelCallConfig,
|
||||
) (json.RawMessage, error) {
|
||||
func marshalChatModelCallConfig(modelConfig *codersdk.ChatModelCallConfig) (json.RawMessage, error) {
|
||||
if modelConfig == nil {
|
||||
return json.RawMessage("{}"), nil
|
||||
}
|
||||
@@ -7508,6 +7651,13 @@ func marshalChatModelCallConfig(
|
||||
return encoded, nil
|
||||
}
|
||||
|
||||
func invalidReasoningEffortResponse(value string) codersdk.Response {
|
||||
return codersdk.Response{
|
||||
Message: "Invalid reasoning_effort value.",
|
||||
Detail: fmt.Sprintf("Invalid value %q, must be one of %s", value, allowedReasoningEffortValues),
|
||||
}
|
||||
}
|
||||
|
||||
func validateChatModelCallConfig(modelConfig *codersdk.ChatModelCallConfig) error {
|
||||
if modelConfig == nil {
|
||||
return nil
|
||||
@@ -7533,9 +7683,36 @@ func validateChatModelCallConfig(modelConfig *codersdk.ChatModelCallConfig) erro
|
||||
}
|
||||
}
|
||||
|
||||
if err := validateChatModelReasoningEffortConfig(modelConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return validateChatModelProviderOptions(modelConfig.ProviderOptions)
|
||||
}
|
||||
|
||||
// validateChatModelReasoningEffortConfig validates the reasoning_effort
|
||||
// config. Values must exactly match the global effort scale, and default
|
||||
// must not exceed max.
|
||||
func validateChatModelReasoningEffortConfig(modelConfig *codersdk.ChatModelCallConfig) error {
|
||||
config := modelConfig.ReasoningEffort
|
||||
if config == nil {
|
||||
return nil
|
||||
}
|
||||
if config.Default == nil || config.Max == nil {
|
||||
return xerrors.New("reasoning_effort.default and reasoning_effort.max must both be set")
|
||||
}
|
||||
if !chatprovider.IsValidReasoningEffort(*config.Default) {
|
||||
return xerrors.Errorf("reasoning_effort.default %q must be one of %s", *config.Default, allowedReasoningEffortValues)
|
||||
}
|
||||
if !chatprovider.IsValidReasoningEffort(*config.Max) {
|
||||
return xerrors.Errorf("reasoning_effort.max %q must be one of %s", *config.Max, allowedReasoningEffortValues)
|
||||
}
|
||||
if !chatprovider.ReasoningEffortLessOrEqual(*config.Default, *config.Max) {
|
||||
return xerrors.New("reasoning_effort.default must not exceed reasoning_effort.max")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChatModelProviderOptions(options *codersdk.ChatModelProviderOptions) error {
|
||||
if options == nil || options.Anthropic == nil || options.Anthropic.ThinkingDisplay == nil {
|
||||
return nil
|
||||
@@ -7586,6 +7763,7 @@ func isZeroChatModelCallConfig(config *codersdk.ChatModelCallConfig) bool {
|
||||
config.TopK == nil &&
|
||||
config.PresencePenalty == nil &&
|
||||
config.FrequencyPenalty == nil &&
|
||||
config.ReasoningEffort == nil &&
|
||||
isZeroModelCostConfig(config.Cost) &&
|
||||
isZeroChatModelProviderOptions(config.ProviderOptions)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user