Skip to main content

Dynamic Profiles (BYOK)

A dynamic profile lets each user bring their own key (BYOK): run AI features on their own provider account, with their own API key and model.

Reveal doesn't store users' keys. To offer dynamic profiles, the developer needs to provide:

  1. A screen where users choose a provider and model and enter their API key.
  2. Storage for those connection details. Reveal never stores them itself.
  3. A resolver that retrieves and returns a user's connection details when Reveal asks for them.

A dynamic profile is then a profile clients can request like any other, whose connection details come from that resolver. It appears in the profile list only for users who have a connection, so it fits naturally into a model picker as an option like "Use my own key".

Declare the profile and resolver​

Implement IAIConnectionResolver. Return the user's connection for the requested profile, or null when the user hasn't set one up:

UserConnectionResolver.cs
using Reveal.Sdk;
using Reveal.Sdk.AI;

public sealed class UserConnectionResolver : IAIConnectionResolver
{
private readonly IUserSettingsStore _settings;

public UserConnectionResolver(IUserSettingsStore settings) => _settings = settings;

public async ValueTask<AIConnectionDescriptor?> ResolveAsync(
string profileName, IRVUserContext user, CancellationToken cancellationToken)
{
// Retrieve details from custom storage
var connection = await _settings.GetAIConnectionAsync(user.UserId, profileName, cancellationToken);
if (connection is null)
{
return null;
}

return new AIConnectionDescriptor
{
ProviderType = connection.ProviderType, // for example, "openai"
Model = connection.Model,
ApiKey = connection.DecryptedApiKey,
};
}
}

Declare the dynamic profile and register the resolver in one call:

Program.cs
builder.Services.AddRevealAI()
.AddOpenAI()
.AddProfile("default", p => { p.Provider = "openai"; p.Model = "gpt-4.1"; })
.AddDynamicProfile<UserConnectionResolver>("my-ai", displayName: "Use my own key");

displayName and description are optional, and are what a model picker shows. The profile name is the identifier clients send.

A dynamic profile can't be the default profile, so declare at least one static profile, as the default profile above. Users can only connect to provider types the server makes available.

Connection​

PropertyRequiredDescription
ProviderTypeYesThe provider to connect to: openai, azure-openai, anthropic, or openai-compatible. Google Gemini isn't available for dynamic profiles. The provider type must be available on the server.
ModelYesThe model ID, or the deployment name for Azure OpenAI.
ApiKeyYes, except for openai-compatibleThe user's API key. Without it, the request fails with ai.connection_invalid.
EndpointNoA custom endpoint URL. Required for azure-openai and openai-compatible. Rejected unless the developer allows custom endpoints.
SettingsNoGeneration settings for this user's calls, such as Temperature or MaxOutputTokens.

Reveal creates the provider client from the connection and reuses it for up to 15 minutes. A connection with any different value, such as a new API key, gets a new client immediately.

Available provider types​

Users can only connect to provider types the developer's server registers, and registering a provider type requires a complete connection of the developer's own for that provider: Reveal creates every registered provider at startup, and fails to start if its connection is missing a required value such as the API key. For example, to let users bring Anthropic keys, the server also needs its own Anthropic connection.

Provider typeASP.NETNode.js and Java
openaiAddOpenAI()A providers entry with type: 'OpenAI'
azure-openaiAddAzureOpenAI()A providers entry with type: 'AzureOpenAI'
anthropicAddAnthropic()A providers entry with type: 'Anthropic'
openai-compatibleAddOpenAICompatible("<name>"), with a BaseUrlA providers entry with type: 'OpenAICompatible'

In Java, add the entry with addProvider. Each registration reads its connection, such as the API key, from configuration (RevealAI:Providers:<name> on ASP.NET) or from the options of its providers entry.

Security​

Connections come from users, so Reveal constrains them:

  • Custom endpoints are rejected by default. This prevents users from making the developer's server send requests to arbitrary URLs. Azure OpenAI and OpenAI-compatible connections always need an endpoint, so users can't connect to them until custom endpoints are allowed. To allow them, set RevealAI:DynamicProfiles:AllowCustomEndpoints to true. The setting applies to every provider type: once it's on, users can set an endpoint of their choice for any connection, not only Azure OpenAI and OpenAI-compatible ones.
  • The developer can restrict the provider types users may connect to with RevealAI:DynamicProfiles:AllowedProviderTypes. By default, every provider type the developer's server registers is allowed.
appsettings.json
{
"RevealAI": {
"DynamicProfiles": {
"AllowedProviderTypes": [ "openai", "anthropic" ],
"AllowCustomEndpoints": false,
"CacheTtl": "00:15:00"
}
}
}

CacheTtl sets how long a provider client created from a user's connection is reused.

These settings are available on ASP.NET only. On Node.js and Java the defaults apply: custom endpoints are always rejected, so azure-openai and openai-compatible connections can't be used; every provider type the developer's server registers is allowed; and provider clients are reused for 15 minutes.

API keys are never logged or used directly as cache keys.

How dynamic profiles behave​

  • Users without a connection don't see the profile. The resolver runs when the profile list is requested and when a request uses the profile. When it returns null, the profile is hidden from that user, and requests for it fail with HTTP 403 and the code ai.profile_not_allowed.
  • An invalid connection fails the request. A missing model, a provider type that isn't allowed or registered, a custom endpoint the developer didn't allow, or, on ASP.NET, a resolver that throws fails the user's request with HTTP 400 and the code ai.connection_invalid. When listing profiles, a resolver that throws hides the profile instead.
  • Reveal never falls back to the developer's own keys. If the provider rejects the user's key, for example because it was revoked, the call fails like any other provider error.
  • The developer's intent mappings still apply. Intents the developer pins with MapIntent keep running on the developer's models, and the rest run on the user's connection.
  • The developer's other hooks still apply. The profile authorizer receives dynamic profiles among its candidates and can hide them, and calls on a user's connection reach the quota gate and usage tracking with IsUserConnection set to true. Use it, for example, to skip token billing for users spending their own key.
  • Datasource ranking runs on the developer's models. During a dashboard generation, Reveal ranks datasources as background work, on the developer's default profile or the profile mapped to the datasource-ranker intent, even for users on their own key.
  • In Node.js and Java, a resolver callback that throws or returns a malformed result is treated as null, and a warning is logged: the profile is hidden, and requests for it fail with HTTP 403 and ai.profile_not_allowed.

Several dynamic profiles​

Each dynamic profile is a choice the user can make, so most applications need only one.

  • If the model depends on the user's plan, use one dynamic profile and return a different Model from the resolver.
  • If users choose between several of their own connections, such as a fast one and a premium one, declare one dynamic profile per choice. A single resolver serves them all and receives the profile name; return null for profiles the user's plan doesn't include.

On ASP.NET, register the resolver with the first profile, and declare the others without it. A static default profile is still required:

Program.cs
builder.Services.AddRevealAI()
.AddOpenAI()
.AddProfile("default", p => { p.Provider = "openai"; p.Model = "gpt-4.1"; })
.AddDynamicProfile<UserConnectionResolver>("my-fast", "My key (fast)")
.AddDynamicProfile("my-premium", "My key (premium)");

In Node.js and Java, list every name in dynamicProfiles, or call addDynamicProfile once per name.