The way your code is today this should work fine. But if you ever decide to add a RemoveItem method you can have issues. Since you are checking if the key exist with TryAdd then accessing assuming the key is there. This works because nothing can remove a value from the ConcurrentDictionary as it stands. But if you add the ability to remove an item from the ConcurrentDictionary then calling a TryAdd then assuming it's there is dangerous since the RemoveItem could have been processed between those statements. This is why the ConcurrentDictionary has a GetOrAdd method to handle that situation for you.
With using a TaskCompletionSource you would struggle to know if you should call the factory method or not from the GetOrAdd since you wouldn't know if your thread was the one to add it. This is where the Lazy<> version shines as it doesn't matter who added the Lazy<Task<TValue>> both threads can await it since it was passed in the valuefactory.
To me it would be a natural path that RemoveItem might be added later as most caching has a way to invalidate the cache items, for example if the Task threw an Exception maybe I want to remove it from the cache to try again. Someone coming after to add a RemoteItemRemoveItem might not catch the subtle bug they introduced in the AddItem that was working.
In a nutshell it will work today but might not be as future proof as you want it to be.