BYOKchat Blog

How to Handle Provider-Specific AI Request Parameters

Design provider-specific AI settings without bloating a shared request model: capability gates, native extensions, validation, persistence, and migration.

· 5 min read

On this page
  1. Separate portable intent from native controls
  2. A practical request model
  3. Provider extensions should be typed
  4. Do not invent false cross-provider equivalence
  5. Capability-gate settings before display
  6. Validation belongs in more than one layer
  7. UI validation
  8. Capability validation
  9. Adapter validation
  10. Omit unsupported fields instead of sending defaults
  11. Distinguish unset from explicit default
  12. Persist intent with the conversation when reproducibility matters
  13. Version provider-specific settings
  14. Treat unknown saved fields carefully
  15. Provider-specific headers are settings too
  16. Reasoning controls are a good example
  17. Sampling controls can become mutually exclusive
  18. Routing controls are not generation controls
  19. Safety settings should not be normalized carelessly
  20. Storage controls are lifecycle policy
  21. Schema features belong to output requirements
  22. The adapter is a compiler
  23. Keep wire objects out of the UI
  24. Test parameter matrices, not individual fields
  25. Log resolved settings without secrets
  26. Migration should favor intent over wire history
  27. Where BYOKchat fits
  28. Further reading

Multi-provider AI clients eventually hit a design trap: every provider has settings that the others do not.

At first the shared request type looks clean:

interface GenerationOptions {
  temperature?: number;
  maxOutputTokens?: number;
  stream?: boolean;
}

Then requirements arrive:

reasoning effort
thinking budget
provider routing
safety settings
prompt caching controls
service tier
seed
log probabilities
hosted tools
response storage
background execution

The tempting response is to keep adding fields until one giant struct contains every option from every API.

That creates a fragile abstraction.

Separate portable intent from native controls

Some concepts are genuinely cross-provider:

selected model
streaming desired
output budget
user-visible system instructions
tools needed
structured output requirement

Others are provider-specific implementations:

OpenRouter provider routing
Gemini safety settings
Anthropic beta headers
DeepSeek thinking object
OpenAI service tier

Represent those layers separately.

A practical request model

For example:

interface GenerationRequest {
  model: ModelReference;
  conversation: PortableConversation;
  intent: PortableGenerationIntent;
  extensions: ProviderExtensions;
}

where:

interface PortableGenerationIntent {
  streaming: boolean;
  maxOutputTokens?: number;
  tools?: PortableTool[];
  structuredOutput?: PortableSchemaRequirement;
  reasoning?: PortableReasoningIntent;
}

and provider-specific settings live elsewhere.

Provider extensions should be typed

Avoid an unstructured dictionary when possible:

extras: Record<string, any>

It is flexible but creates runtime errors and makes migration difficult.

Prefer typed variants:

type ProviderExtensions =
  | { provider: "anthropic"; value: AnthropicOptions }
  | { provider: "gemini"; value: GeminiOptions }
  | { provider: "openrouter"; value: OpenRouterOptions }
  | { provider: "deepseek"; value: DeepSeekOptions }
  | { provider: "custom"; value: CustomProviderOptions };

The adapter can then compile-time validate its own fields.

Do not invent false cross-provider equivalence

Two fields with similar names may not mean the same thing.

For example:

reasoning_effort = high
thinking_level = high
thinking budget = N tokens

may all influence reasoning, but their provider semantics are not necessarily equivalent.

A generic UI can expose a broad user intent only if the mapping is defensible.

Otherwise show the provider-native control.

Capability-gate settings before display

A settings panel should be driven by:

connection
+ selected model
+ endpoint family
+ mode
→ supported controls

Do not show every advanced field and wait for the API to reject unsupported combinations.

The UI can hide, disable, or explain controls that do not apply.

See Capability Detection in Multi-Model AI Apps.

Validation belongs in more than one layer

Use three levels:

UI validation

Prevent obviously invalid values:

negative token limit
unknown enum
malformed JSON schema

Capability validation

Reject combinations the selected model/API cannot support.

Adapter validation

Ensure the final wire request satisfies provider-specific constraints.

The server remains the final authority, but many errors should be caught before network I/O.

Omit unsupported fields instead of sending defaults

A common compatibility bug is serializing every option with a default value:

{
  "temperature": 1,
  "top_p": 1,
  "reasoning_effort": "medium",
  "seed": 0
}

Even if the user did not choose those settings.

That can:

  • trigger provider validation;
  • disable a model mode;
  • prevent caching;
  • alter behavior;
  • create false assumptions about support.

Prefer sparse encoding: send only fields intentionally active for the selected provider/model.

Distinguish unset from explicit default

These are semantically different:

user did not specify temperature
user explicitly selected provider default
user explicitly selected 1.0

Your persistence model may need an automatic state rather than forcing a number.

That lets provider defaults evolve without old chats silently pinning stale values.

Persist intent with the conversation when reproducibility matters

If a chat turn used a special setting, store enough information to explain or regenerate it later.

Useful request metadata can include:

provider
model ID
endpoint family
portable intent
provider extension version
resolved native settings

Do not store credentials with that record.

Version provider-specific settings

Settings schemas evolve.

A persisted object can include:

{
  "provider": "example",
  "version": 2,
  "options": {
    "someNewField": true
  }
}

On app upgrade, migrate old data deliberately.

This is safer than decoding arbitrary dictionaries forever.

Treat unknown saved fields carefully

Forward compatibility is useful.

If a newer app writes an extension field and an older app opens the same backup, the older app should not necessarily destroy it on save.

Possible strategies:

  • preserve unknown JSON alongside typed fields;
  • version backups and reject unsupported destructive edits;
  • keep raw provider metadata separate from editable settings.

Choose intentionally.

Provider-specific headers are settings too

Routing or feature headers can be part of provider configuration.

Separate:

non-secret feature header
secret credential header

Protected header values belong in secure storage and should never be copied into conversation metadata.

Reasoning controls are a good example

A portable UI might expose:

Reasoning: Auto / Off / Low / Medium / High

The adapter can map this only when the provider/model supports a meaningful equivalent.

If a provider has a unique mode such as a token budget or special beta toggle, preserve that natively rather than pretending it is exactly High.

See Reasoning Effort Explained.

Sampling controls can become mutually exclusive

Some reasoning modes ignore or reject normal sampling parameters.

Capability rules can express this:

if model.reasoningMode == .thinking {
  disable(.temperature)
  disable(.topP)
}

The adapter should enforce the same rule even if the UI somehow sends an old saved combination.

Routing controls are not generation controls

OpenRouter-style provider ordering/fallback settings affect where a request runs, not directly what the model should generate.

Keep them in a routing policy object:

request.routing

rather than mixing them with:

request.generation

This separation makes analytics and debugging much clearer.

Safety settings should not be normalized carelessly

Provider safety controls differ in taxonomy, defaults, and enforcement.

Do not create a universal slider like:

Safety: 0–100

unless you can define exactly what it means across providers.

It is usually better to expose provider-native advanced settings with clear labels.

Storage controls are lifecycle policy

Fields such as:

store response
background execution
previous response/interaction ID

change persistence and continuation semantics.

They belong closer to request lifecycle/state policy than to creative generation settings.

Your architecture should reflect that distinction.

Schema features belong to output requirements

Structured output is not merely another numeric knob.

Represent it as a requirement:

structuredOutput: {
  schema,
  strictness
}

Then capability validation can decide whether the selected provider can satisfy it.

If not, fail before sending rather than quietly falling back to unstructured text.

The adapter is a compiler

A useful mental model is:

portable intent + native extension + capabilities

             provider adapter

               wire request

The adapter behaves like a compiler:

  • validates inputs;
  • resolves defaults;
  • rejects impossible combinations;
  • translates semantic intent;
  • emits provider-specific JSON/headers.

This is stronger than a serializer.

Keep wire objects out of the UI

Avoid binding forms directly to SDK request types.

SDK types change and often contain fields irrelevant to your product.

Instead:

UI model → product settings → adapter → SDK/wire model

That keeps provider SDK updates from rippling through every view.

Test parameter matrices, not individual fields

Most bugs appear in combinations:

reasoning + temperature
tools + structured output
streaming + tool arguments
background + storage disabled
fallback routing + required parameter
image input + unsupported model

Build table-driven tests for valid and invalid matrices.

Log resolved settings without secrets

For diagnostics, it can be useful to record:

model = x
reasoning = high
stream = true
tools = enabled
routing = fallback allowed

Do not log:

  • API keys;
  • protected headers;
  • prompts;
  • tool arguments/results unless the user explicitly opts into sensitive diagnostics.

Migration should favor intent over wire history

Provider APIs evolve.

If you only persist old raw request JSON, future clients may struggle to reinterpret it.

Persist the user/product intent plus optional raw metadata.

Then a newer adapter can generate a current request while historical wire data remains available for audit/debugging.

Where BYOKchat fits

A multi-provider client can keep common chat controls consistent while giving each provider room for native advanced settings. Provider/model capability profiles decide which controls are visible, and adapters compile those choices into correct requests.

That prevents the common outcome where a supposedly “unified” settings panel becomes a union of every API field ever invented.

Further reading

Keep reading