(rw http.ResponseWriter, r *http.Request)
| 6568 | } |
| 6569 | |
| 6570 | func (api *API) upsertUserAIProviderKey(rw http.ResponseWriter, r *http.Request) { |
| 6571 | ctx := r.Context() |
| 6572 | if !api.DeploymentValues.AI.BridgeConfig.AllowBYOK.Value() { |
| 6573 | httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{Message: "BYOK is disabled."}) |
| 6574 | return |
| 6575 | } |
| 6576 | targetUser := httpmw.UserParam(r) |
| 6577 | providerID, err := parseUserAIProviderID(r) |
| 6578 | if err != nil { |
| 6579 | httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{Message: "Invalid AI provider ID."}) |
| 6580 | return |
| 6581 | } |
| 6582 | //nolint:gocritic // Users can attach their own key to an enabled provider without AI provider admin permissions. |
| 6583 | metadataCtx := dbauthz.AsAIProviderMetadataReader(ctx) |
| 6584 | provider, err := api.Database.GetAIProviderByID(metadataCtx, providerID) |
| 6585 | if err != nil { |
| 6586 | if errors.Is(err, sql.ErrNoRows) { |
| 6587 | httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{Message: "AI provider not found."}) |
| 6588 | return |
| 6589 | } |
| 6590 | api.Logger.Error(ctx, "failed to get AI provider", slog.Error(err), slog.F("ai_provider_id", providerID)) |
| 6591 | httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{Message: "Failed to get AI provider."}) |
| 6592 | return |
| 6593 | } |
| 6594 | if !provider.Enabled { |
| 6595 | httpapi.Write(ctx, rw, http.StatusPreconditionFailed, codersdk.Response{Message: "AI provider is disabled."}) |
| 6596 | return |
| 6597 | } |
| 6598 | var req codersdk.CreateUserAIProviderKeyRequest |
| 6599 | if !httpapi.Read(ctx, rw, r, &req) { |
| 6600 | return |
| 6601 | } |
| 6602 | if err := validateChatProviderAPIKeySize(req.APIKey); err != nil { |
| 6603 | httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ |
| 6604 | Message: "API key too large.", |
| 6605 | Detail: err.Error(), |
| 6606 | }) |
| 6607 | return |
| 6608 | } |
| 6609 | if req.APIKey == "" { |
| 6610 | httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{Message: "API key is required."}) |
| 6611 | return |
| 6612 | } |
| 6613 | if strings.TrimSpace(req.APIKey) != req.APIKey { |
| 6614 | httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{Message: "API key must not contain leading or trailing whitespace."}) |
| 6615 | return |
| 6616 | } |
| 6617 | providerKeys, err := api.Database.GetAIProviderKeyPresence(metadataCtx, []uuid.UUID{providerID}) |
| 6618 | if err != nil { |
| 6619 | api.Logger.Error(ctx, "failed to list AI provider key presence", slog.Error(err), slog.F("ai_provider_id", providerID)) |
| 6620 | httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{Message: "Failed to list AI provider keys."}) |
| 6621 | return |
| 6622 | } |
| 6623 | now := api.Clock.Now() |
| 6624 | _, err = api.Database.UpsertUserAIProviderKey(ctx, database.UpsertUserAIProviderKeyParams{ |
| 6625 | ID: uuid.New(), |
| 6626 | UserID: targetUser.ID, |
| 6627 | AIProviderID: providerID, |
no test coverage detected