Files
EvoScientist/EvoScientist/model_registry/adapters.py
T

831 lines
28 KiB
Python

"""Adapter parameter contracts and request mapping (design doc 6.1-6.3).
This module is the single authority for how unified registry parameters are
validated, normalized, and mapped onto LangChain constructor options and
provider request fields:
- ``adapter_specs`` holds the versioned built-in contracts: a generic
``model_selector: "*"`` contract for each phase-1 adapter plus the
``openai-compatible``/``glm-5.2`` model-specific contract.
- ``find_adapter_spec`` implements the matching order — exact
``upstream_model_id`` first, longest glob next, generic contract last.
- ``resolve_parameters`` applies the section 6.1 inheritance semantics and
the save-time contract checks (stable error codes from section 9.5).
- ``Adapter.build_request`` is the only entry point that turns a frozen
``ResolvedModelConfig`` into ``{client_options, request_options}``;
parameters the contract does not declare never enter the request.
A ``target_name`` of ``""`` declares a parameter the contract validates but
enforces outside the request payload (for example ollama retries, which live
in the SafeHttpTransport). Dotted ``client_option`` target names (for example
``client_kwargs.timeout``) build nested option dicts.
"""
from __future__ import annotations
import fnmatch
from typing import Any
from pydantic import BaseModel, ConfigDict
from .errors import (
ADAPTER_NOT_SUPPORTED,
AUTH_MODE_UNSUPPORTED,
CAPABILITY_UNSUPPORTED_BY_ADAPTER,
CREDENTIAL_NOT_CONFIGURED,
UNSUPPORTED_RUNTIME_PARAMETER,
ModelRegistryError,
)
from .schemas import (
AdapterParameterSpec,
AuthSpec,
Capabilities,
ConnectionSpec,
ModelConfig,
ParameterRule,
ProviderConfig,
ReasoningEffort,
ResolvedModelConfig,
SamplingOverride,
)
SUPPORTED_ADAPTER_IDS = (
"openai",
"anthropic",
"openai-compatible",
"anthropic-compatible",
"ollama",
)
# Known adapter IDs that phase 1 deliberately does not open (section 6.2).
UNOPENED_ADAPTER_IDS = ("google-genai", "grok", "openrouter", "nvidia", "antigravity")
# Unified parameter keys the mapping engine knows how to source (section 6.3).
_UNIFIED_PARAMETERS = (
"timeout_seconds",
"max_retries",
"max_output_tokens",
"temperature",
"top_p",
"reasoning_effort",
)
_CAPABILITY_NAMES = ("tools", "vision", "structured_output")
_REASONING_VALUES = ["low", "medium", "high"]
class BuiltRequest(BaseModel):
"""The output of ``Adapter.build_request`` (section 6.4)."""
model_config = ConfigDict(frozen=True)
client_options: dict[str, Any]
request_options: dict[str, Any]
class ResolvedParameters(BaseModel):
"""Save-time resolution result; the Resolver freezes it into a snapshot."""
model_config = ConfigDict(frozen=True)
timeout_seconds: int
max_retries: int
max_output_tokens: int
temperature: float | None
top_p: float | None
# "auto" means the field is omitted from the provider request.
reasoning_effort: ReasoningEffort
# --- Normalizers (named server-side functions referenced by contracts) ---
class _Omit:
"""Sentinel: the parameter is omitted from the outbound request."""
def __repr__(self) -> str: # pragma: no cover - debugging aid
return "OMIT"
OMIT = _Omit()
def _reject_parameter(name: str, message: str) -> ModelRegistryError:
return ModelRegistryError(
UNSUPPORTED_RUNTIME_PARAMETER,
message,
details=[{"path": f"runtime.{name}", "code": UNSUPPORTED_RUNTIME_PARAMETER}],
)
def _normalize_identity(name: str, rule: ParameterRule, value: Any) -> Any:
return value
def _normalize_clamp_to_model_limit(name: str, rule: ParameterRule, value: Any) -> Any:
if value is None:
return OMIT
if rule.maximum is not None and value > rule.maximum:
return rule.maximum
if rule.minimum is not None and value < rule.minimum:
raise _reject_parameter(
name,
f"{name}={value} is below the contract minimum {rule.minimum}.",
)
return value
def _normalize_omit_when_none(name: str, rule: ParameterRule, value: Any) -> Any:
return OMIT if value is None else value
def _normalize_omit_when_auto(name: str, rule: ParameterRule, value: Any) -> Any:
return OMIT if value is None or value == "auto" else value
def _normalize_reject_non_auto(name: str, rule: ParameterRule, value: Any) -> Any:
if value is None or value == "auto":
return OMIT
raise _reject_parameter(
name,
f"{name} is not supported by this adapter contract; only the "
"inheriting 'auto' value is accepted.",
)
_NORMALIZERS = {
"identity": _normalize_identity,
"clamp_to_model_limit": _normalize_clamp_to_model_limit,
"omit_when_none": _normalize_omit_when_none,
"omit_when_auto": _normalize_omit_when_auto,
"reject_non_auto": _normalize_reject_non_auto,
}
def _check_rule_value(name: str, rule: ParameterRule, value: Any) -> None:
"""Type/range/enum validation for a non-null value against the contract.
With the ``clamp_to_model_limit`` normalizer an out-of-range high value is
clamped by the normalizer instead of rejected, so the maximum check is
left to it.
"""
if rule.value_type == "integer":
if isinstance(value, bool) or not isinstance(value, int):
raise _reject_parameter(name, f"{name} must be an integer.")
elif rule.value_type == "number":
if isinstance(value, bool) or not isinstance(value, (int, float)):
raise _reject_parameter(name, f"{name} must be a number.")
elif rule.value_type == "enum":
if rule.enum_values is not None and value not in rule.enum_values:
raise _reject_parameter(name, f"{name} must be one of {rule.enum_values}.")
if rule.value_type in ("integer", "number"):
if rule.minimum is not None and value < rule.minimum:
raise _reject_parameter(
name,
f"{name}={value} is below the contract minimum {rule.minimum}.",
)
if (
rule.normalizer != "clamp_to_model_limit"
and rule.maximum is not None
and value > rule.maximum
):
raise _reject_parameter(
name,
f"{name}={value} exceeds the contract maximum {rule.maximum}.",
)
# --- Built-in contracts (section 6.2) ---
def _connection(chat_model: str) -> ConnectionSpec:
return ConnectionSpec(
chat_model=chat_model, model_field="model", base_url_field="base_url"
)
def _timeout_rule(target_name: str) -> ParameterRule:
return ParameterRule(
supported=True,
value_type="integer",
minimum=10,
maximum=600,
nullable="forbidden",
target="client_option",
target_name=target_name,
normalizer="identity",
)
def _retries_rule(target_name: str) -> ParameterRule:
return ParameterRule(
supported=True,
value_type="integer",
minimum=0,
maximum=5,
nullable="forbidden",
target="client_option",
target_name=target_name,
normalizer="identity",
)
def _max_output_tokens_rule(
target_name: str, maximum: float | None = None
) -> ParameterRule:
return ParameterRule(
supported=True,
value_type="integer",
minimum=1,
maximum=maximum,
nullable="forbidden",
target="request_option",
target_name=target_name,
normalizer="clamp_to_model_limit",
)
def _temperature_rule(maximum: float) -> ParameterRule:
return ParameterRule(
supported=True,
value_type="number",
minimum=0,
maximum=maximum,
nullable="omit",
target="request_option",
target_name="temperature",
normalizer="omit_when_none",
)
def _top_p_rule() -> ParameterRule:
return ParameterRule(
supported=True,
value_type="number",
minimum=0.000001,
maximum=1,
nullable="omit",
target="request_option",
target_name="top_p",
normalizer="omit_when_none",
)
def _reasoning_supported_rule() -> ParameterRule:
return ParameterRule(
supported=True,
value_type="enum",
nullable="omit",
enum_values=list(_REASONING_VALUES),
target="request_option",
target_name="reasoning_effort",
normalizer="omit_when_auto",
)
def _reasoning_unsupported_rule() -> ParameterRule:
return ParameterRule(
supported=False,
value_type="enum",
nullable="omit",
enum_values=list(_REASONING_VALUES),
target="request_option",
target_name="",
normalizer="reject_non_auto",
)
def _api_key_auth() -> AuthSpec:
return AuthSpec(
credential_required=True,
credential_kind="api_key",
target="client_option",
target_name="api_key",
)
def _bearer_auth() -> AuthSpec:
return AuthSpec(
credential_required=True,
credential_kind="bearer_token",
target="request_header",
target_name="Authorization",
)
def _openai_style_parameters() -> dict[str, ParameterRule]:
return {
"timeout_seconds": _timeout_rule("timeout"),
"max_retries": _retries_rule("max_retries"),
"max_output_tokens": _max_output_tokens_rule("max_tokens"),
"temperature": _temperature_rule(2),
"top_p": _top_p_rule(),
"reasoning_effort": _reasoning_supported_rule(),
}
def _anthropic_style_parameters() -> dict[str, ParameterRule]:
return {
"timeout_seconds": _timeout_rule("timeout"),
"max_retries": _retries_rule("max_retries"),
"max_output_tokens": _max_output_tokens_rule("max_tokens"),
"temperature": _temperature_rule(1),
"top_p": _top_p_rule(),
"reasoning_effort": _reasoning_unsupported_rule(),
}
def _build_builtin_specs() -> tuple[AdapterParameterSpec, ...]:
openai_generic = AdapterParameterSpec(
adapter_id="openai",
spec_revision=1,
model_selector="*",
auth_specs={"api_key": _api_key_auth()},
parameters=_openai_style_parameters(),
protocol_capabilities=Capabilities(
tools=True, vision=True, structured_output=True
),
connection=_connection("ChatOpenAI"),
)
openai_compatible_generic = AdapterParameterSpec(
adapter_id="openai-compatible",
spec_revision=1,
model_selector="*",
auth_specs={"api_key": _api_key_auth(), "bearer": _bearer_auth()},
parameters=_openai_style_parameters(),
protocol_capabilities=Capabilities(
tools=True, vision=True, structured_output=True
),
connection=_connection("ChatOpenAI"),
)
anthropic_generic = AdapterParameterSpec(
adapter_id="anthropic",
spec_revision=1,
model_selector="*",
auth_specs={"api_key": _api_key_auth()},
parameters=_anthropic_style_parameters(),
protocol_capabilities=Capabilities(
tools=True, vision=True, structured_output=False
),
connection=_connection("ChatAnthropic"),
)
anthropic_compatible_generic = anthropic_generic.model_copy(
update={"adapter_id": "anthropic-compatible"}
)
ollama_generic = AdapterParameterSpec(
adapter_id="ollama",
spec_revision=1,
model_selector="*",
auth_specs={
"none": AuthSpec(
credential_required=False,
credential_kind="none",
target="adapter_internal",
target_name=None,
)
},
parameters={
"timeout_seconds": _timeout_rule("client_kwargs.timeout"),
# Retries are enforced by the SafeHttpTransport built in Task 2,
# not by an ollama client option; the contract still validates
# the registry value (empty target_name = validated, not sent).
"max_retries": _retries_rule(""),
"max_output_tokens": _max_output_tokens_rule("num_predict"),
"temperature": _temperature_rule(2),
"top_p": _top_p_rule(),
"reasoning_effort": _reasoning_unsupported_rule(),
},
protocol_capabilities=Capabilities(
tools=True, vision=True, structured_output=True
),
connection=_connection("ChatOllama"),
)
# The first model-specific contract (section 6.2 YAML, verbatim values).
glm_52 = AdapterParameterSpec(
adapter_id="openai-compatible",
spec_revision=1,
model_selector="glm-5.2",
auth_specs={"api_key": _api_key_auth()},
parameters={
"timeout_seconds": _timeout_rule("timeout"),
"max_retries": _retries_rule("max_retries"),
"max_output_tokens": _max_output_tokens_rule("max_tokens", maximum=32768),
"temperature": _temperature_rule(1),
"top_p": _top_p_rule(),
"reasoning_effort": _reasoning_unsupported_rule(),
},
protocol_capabilities=Capabilities(
tools=True, vision=False, structured_output=True
),
connection=_connection("ChatOpenAI"),
)
return (
openai_generic,
openai_compatible_generic,
anthropic_generic,
anthropic_compatible_generic,
ollama_generic,
glm_52,
)
_BUILTIN_SPECS: tuple[AdapterParameterSpec, ...] = _build_builtin_specs()
def adapter_specs() -> tuple[AdapterParameterSpec, ...]:
"""Return the built-in versioned adapter contracts."""
return _BUILTIN_SPECS
def find_adapter_spec(
adapter_id: str,
upstream_model_id: str,
*,
spec_revision: int | None = None,
specs: list[AdapterParameterSpec] | tuple[AdapterParameterSpec, ...] | None = None,
) -> AdapterParameterSpec | None:
"""Match a contract: exact ID first, longest glob next, ``*`` last.
Unopened or unknown adapter IDs raise ``ADAPTER_NOT_SUPPORTED``. ``None``
is returned when no contract matches (or the pinned ``spec_revision`` is
gone); such configurations may only be saved as ``configured``.
"""
if adapter_id not in SUPPORTED_ADAPTER_IDS:
note = (
"a known but not yet opened adapter"
if adapter_id in UNOPENED_ADAPTER_IDS
else "an unknown adapter"
)
raise ModelRegistryError(
ADAPTER_NOT_SUPPORTED,
f"Adapter {adapter_id!r} is {note}; phase 1 supports "
f"{list(SUPPORTED_ADAPTER_IDS)}.",
)
candidates = [
spec
for spec in (adapter_specs() if specs is None else specs)
if spec.adapter_id == adapter_id
and (spec_revision is None or spec.spec_revision == spec_revision)
]
for spec in candidates:
if spec.model_selector == upstream_model_id:
return spec
globs = [
spec
for spec in candidates
if spec.model_selector != "*"
and fnmatch.fnmatchcase(upstream_model_id, spec.model_selector)
]
if globs:
return max(globs, key=lambda spec: len(spec.model_selector))
for spec in candidates:
if spec.model_selector == "*":
return spec
return None
def get_adapter(
adapter_id: str,
upstream_model_id: str,
*,
spec_revision: int | None = None,
) -> Adapter:
"""Return the matched Adapter, failing loudly when no contract applies."""
spec = find_adapter_spec(adapter_id, upstream_model_id, spec_revision=spec_revision)
if spec is None:
revision_note = (
f" at spec_revision {spec_revision}" if spec_revision is not None else ""
)
raise ModelRegistryError(
ADAPTER_NOT_SUPPORTED,
f"No adapter contract matches {adapter_id!r}/{upstream_model_id!r}"
f"{revision_note}; the configuration may only remain 'configured'.",
)
return Adapter(spec)
def compute_effective_capabilities(
protocol_capabilities: Capabilities,
declared_capabilities: Capabilities,
verified_capabilities: Capabilities,
) -> Capabilities:
"""The single capability rule: protocol AND declared AND verified."""
return Capabilities(
**{
name: (
getattr(protocol_capabilities, name)
and getattr(declared_capabilities, name)
and getattr(verified_capabilities, name)
)
for name in _CAPABILITY_NAMES
}
)
def _resolve_nullable_parameter(
name: str,
rule: ParameterRule,
value: Any,
) -> Any:
"""Validate one already-inherited value; returns the value or ``OMIT``."""
if not rule.supported:
if value is None or value == "auto":
return OMIT
# A concrete value for an unsupported parameter is handed to the
# contract-declared normalizer: reject_non_auto raises here. Any
# normalizer that would let the value through still rejects, because
# an unsupported parameter must never enter the request.
resolved = _NORMALIZERS[rule.normalizer](name, rule, value)
if resolved is not OMIT:
raise _reject_parameter(
name, f"{name} is not supported by this adapter contract."
)
return OMIT
if value is None or (name == "reasoning_effort" and value == "auto"):
if rule.nullable == "forbidden":
raise _reject_parameter(name, f"{name} must have a concrete value.")
return OMIT
_check_rule_value(name, rule, value)
return _NORMALIZERS[rule.normalizer](name, rule, value)
def _resolve_required_parameter(
name: str,
rule: ParameterRule,
value: Any,
) -> Any:
"""Validate a registry-sourced value that must always resolve."""
if not rule.supported:
raise _reject_parameter(
name, f"{name} is not supported by this adapter contract."
)
if value is None:
raise _reject_parameter(name, f"{name} must have a concrete value.")
_check_rule_value(name, rule, value)
resolved = _NORMALIZERS[rule.normalizer](name, rule, value)
if resolved is OMIT:
raise _reject_parameter(name, f"{name} must have a concrete value.")
return resolved
def _parameter_rule(spec: AdapterParameterSpec, name: str) -> ParameterRule:
rule = spec.parameters.get(name)
if rule is None:
raise _reject_parameter(
name, f"Adapter contract {spec.adapter_id!r} does not declare {name}."
)
return rule
def resolve_parameters(
provider: ProviderConfig,
model: ModelConfig,
spec: AdapterParameterSpec,
*,
reasoning_effort_override: ReasoningEffort | None = None,
sampling_override: SamplingOverride | None = None,
) -> ResolvedParameters:
"""Resolve a model's runtime parameters against the matched contract.
Applies the section 6.1 inheritance semantics (model value overrides the
provider default; provider ``null`` means the field is omitted, never
zero; ``reasoning_effort=auto`` inherits and is omitted when still auto)
and the save-time contract checks. Raises ``ModelRegistryError`` with a
stable section 9.5 code on any violation.
The sampling override (snapshot creation only) replaces the model's
configured value for its ``kind`` and omits the other sampling
parameter entirely — registry defaults included — because temperature
and top_p must not be sent together. Overrides flow through the same
contract rules, so unsupported adapters still reject them.
"""
auth_spec = spec.auth_specs.get(provider.auth.mode)
if auth_spec is None:
raise ModelRegistryError(
AUTH_MODE_UNSUPPORTED,
f"Adapter {spec.adapter_id!r} does not support auth mode "
f"{provider.auth.mode!r}.",
details=[{"path": "auth.mode", "code": AUTH_MODE_UNSUPPORTED}],
)
if auth_spec.credential_required and provider.auth.credential_id is None:
raise ModelRegistryError(
CREDENTIAL_NOT_CONFIGURED,
f"Auth mode {provider.auth.mode!r} requires a credential reference.",
details=[{"path": "auth.credential_id", "code": CREDENTIAL_NOT_CONFIGURED}],
)
for name in _CAPABILITY_NAMES:
if getattr(model.runtime.declared_capabilities, name) and not getattr(
spec.protocol_capabilities, name
):
raise ModelRegistryError(
CAPABILITY_UNSUPPORTED_BY_ADAPTER,
f"Adapter {spec.adapter_id!r} protocol does not support the "
f"declared capability {name!r}.",
details=[
{
"path": f"runtime.declared_capabilities.{name}",
"code": CAPABILITY_UNSUPPORTED_BY_ADAPTER,
}
],
)
timeout_seconds = _resolve_required_parameter(
"timeout_seconds",
_parameter_rule(spec, "timeout_seconds"),
provider.runtime.timeout_seconds,
)
max_retries = _resolve_required_parameter(
"max_retries",
_parameter_rule(spec, "max_retries"),
provider.runtime.max_retries,
)
max_output_tokens = _resolve_required_parameter(
"max_output_tokens",
_parameter_rule(spec, "max_output_tokens"),
model.runtime.max_output_tokens,
)
if sampling_override is not None and sampling_override.kind == "temperature":
temperature = _resolve_nullable_parameter(
"temperature", _parameter_rule(spec, "temperature"), sampling_override.value
)
top_p = _resolve_nullable_parameter(
"top_p", _parameter_rule(spec, "top_p"), None
)
elif sampling_override is not None:
top_p = _resolve_nullable_parameter(
"top_p", _parameter_rule(spec, "top_p"), sampling_override.value
)
temperature = _resolve_nullable_parameter(
"temperature", _parameter_rule(spec, "temperature"), None
)
else:
temperature = model.runtime.temperature
if temperature is None:
temperature = provider.runtime.default_temperature
temperature = _resolve_nullable_parameter(
"temperature", _parameter_rule(spec, "temperature"), temperature
)
top_p = model.runtime.top_p
if top_p is None:
top_p = provider.runtime.default_top_p
top_p = _resolve_nullable_parameter("top_p", _parameter_rule(spec, "top_p"), top_p)
reasoning_effort: ReasoningEffort = (
reasoning_effort_override
if reasoning_effort_override not in (None, "auto")
else model.runtime.reasoning_effort
)
if reasoning_effort == "auto":
reasoning_effort = provider.runtime.default_reasoning_effort
reasoning_effort = _resolve_nullable_parameter(
"reasoning_effort",
_parameter_rule(spec, "reasoning_effort"),
reasoning_effort,
)
resolved = {
"timeout_seconds": timeout_seconds,
"max_retries": max_retries,
"max_output_tokens": max_output_tokens,
"temperature": None if temperature is OMIT else temperature,
"top_p": None if top_p is OMIT else top_p,
"reasoning_effort": ("auto" if reasoning_effort is OMIT else reasoning_effort),
}
_check_conflicts(spec, resolved)
return ResolvedParameters(**resolved)
def _check_conflicts(spec: AdapterParameterSpec, resolved: dict[str, Any]) -> None:
present = {
name
for name, value in resolved.items()
if value is not None and value != "auto"
}
for name in present:
rule = spec.parameters.get(name)
if rule is None:
continue
for other in rule.conflicts_with:
if other in present:
raise _reject_parameter(
name, f"{name} conflicts with {other} in this contract."
)
def _assign_option(options: dict[str, Any], dotted_name: str, value: Any) -> None:
parts = dotted_name.split(".")
target = options
for part in parts[:-1]:
existing = target.get(part)
if not isinstance(existing, dict):
existing = {}
target[part] = existing
target = existing
target[parts[-1]] = value
class Adapter:
"""A versioned parameter contract bound to one ``AdapterParameterSpec``."""
def __init__(self, spec: AdapterParameterSpec) -> None:
for name, rule in spec.parameters.items():
if name not in _UNIFIED_PARAMETERS:
raise ValueError(f"Unknown unified parameter {name!r} in contract.")
if rule.normalizer not in _NORMALIZERS:
raise ValueError(f"Unknown normalizer {rule.normalizer!r}.")
self._spec = spec
@property
def spec(self) -> AdapterParameterSpec:
return self._spec
def build_request(
self,
resolved_config: ResolvedModelConfig,
*,
credential: str | None = None,
) -> BuiltRequest:
"""Map a frozen config into ``{client_options, request_options}``.
This is the only entry point that constructs LangChain parameters and
provider request parameters. Contract validation is re-applied so a
stale snapshot fails loudly instead of silently sending values the
current contract would reject.
"""
spec = self._spec
auth_spec = spec.auth_specs.get(resolved_config.auth_ref.mode)
if auth_spec is None:
raise ModelRegistryError(
AUTH_MODE_UNSUPPORTED,
f"Adapter {spec.adapter_id!r} does not support auth mode "
f"{resolved_config.auth_ref.mode!r}.",
)
if auth_spec.credential_required and credential is None:
raise ModelRegistryError(
CREDENTIAL_NOT_CONFIGURED,
"A credential is required to build the request but none was "
"resolved for this run.",
)
client_options: dict[str, Any] = {}
if spec.connection is not None:
client_options[spec.connection.model_field] = (
resolved_config.upstream_model_id
)
client_options[spec.connection.base_url_field] = resolved_config.base_url
sources: dict[str, Any] = {
"timeout_seconds": resolved_config.client_options.timeout_seconds,
"max_retries": resolved_config.client_options.max_retries,
"max_output_tokens": resolved_config.request_options.max_output_tokens,
"temperature": resolved_config.request_options.temperature,
"top_p": resolved_config.request_options.top_p,
"reasoning_effort": resolved_config.request_options.reasoning_effort,
}
request_options: dict[str, Any] = {}
for name, rule in spec.parameters.items():
value = _resolve_nullable_parameter(name, rule, sources[name])
if value is OMIT or rule.target_name == "":
continue
if rule.target == "client_option":
_assign_option(client_options, rule.target_name, value)
elif rule.target == "request_option":
_assign_option(request_options, rule.target_name, value)
else: # extra_body_path
extra_body = request_options.get("extra_body")
if not isinstance(extra_body, dict):
extra_body = {}
request_options["extra_body"] = extra_body
_assign_option(extra_body, rule.target_name, value)
if resolved_config.auth_ref.mode != "none" and credential is not None:
self._apply_auth(auth_spec, client_options, credential)
return BuiltRequest(
client_options=client_options, request_options=request_options
)
@staticmethod
def _apply_auth(
auth_spec: AuthSpec, client_options: dict[str, Any], credential: str
) -> None:
if auth_spec.target == "client_option" and auth_spec.target_name:
client_options[auth_spec.target_name] = credential
elif auth_spec.target == "request_header" and auth_spec.target_name:
header_value = (
f"Bearer {credential}"
if auth_spec.credential_kind == "bearer_token"
else credential
)
headers = client_options.get("default_headers")
if not isinstance(headers, dict):
headers = {}
client_options["default_headers"] = headers
headers[auth_spec.target_name] = header_value
# adapter_internal credentials are handled by the adapter itself and
# never appear in client or request options.