Using model profiles and compatibility policies¶
ProviderProfile is deprecated (1.0.0rc2) and removed in 1.0.0
Everything a profile said has one home now. Migrate like this:
| You had | Write instead |
|---|---|
ProviderProfile.inference(provider=..., base_url=URL, compat=OpenAIResponsesCompat.preset("ollama")) + OpenAILM.from_profile(api_key=K, profile=p) |
OpenAILM(api_key=K, compat="ollama") — a preset name supplies its own address; add base_url=URL only if yours differs |
a per-model ModelInfo.compat on the Responses dialect |
Config(extensions={"openai_responses_compat": {...}}) on that model's requests (the chat dialect has OpenAIChatCompat.model_overrides) |
a compat guessed from base_url (openrouter.ai, api.meta.ai) |
compat="openrouter" / compat="meta" — the guess now warns and will be removed |
ModelInfo, ModelRegistry and the lm15.compat policies are not deprecated.
Why: a profile was a second configuration-resolution system beside the
adapter's and the router's; two systems is how a user ends up not knowing
which one picked the URL. Decision record:
lm15-contract/changes/2026-09-11-job-handles-live-turns-profiles.md § 3.
lm15 keeps inference requests simple:
Request(model="gpt-4.1-mini", messages=(...))
Model profiles and compatibility policies are optional helpers for applications that need model metadata, local OpenAI-compatible servers, routing, or exact provider dialect control.
They answer different questions:
Request What the caller wants the model to do.
ModelInfo What a model can do and what it costs.
ProviderProfile Where/how to call provider endpoints.
Compat policy How to serialize for a provider API dialect.
ModelRegistry How to index and resolve model metadata.
Why profiles are separate from Request¶
Provider compatibility is not a semantic user request. For example, whether an
OpenAI-compatible server expects max_tokens or max_output_tokens is a wire
serialization detail, not a different task for the model.
For that reason, these settings live outside lm15.types.Request:
lm15.compat— typed compatibility policy objects.lm15.models— model metadata, pricing, andModelRegistry.lm15.profiles— provider endpoint profiles and compatibility resolution.
Minimal use: no profile¶
Existing code does not change.
import os
from lm15.providers import OpenAILM
from lm15.types import Message, Request
lm = OpenAILM(api_key=os.environ["OPENAI_API_KEY"])
response = lm.complete(
Request(
model="gpt-4.1-mini",
messages=(Message.user("Say hello."),),
)
)
Local OpenAI-compatible server¶
For Chat Completions (/chat/completions), start with
Connect an unlisted OpenAI-compatible server.
It shows OpenAIChatLM with no preset, an unknown-name error, and a custom
OpenAIChatCompat object. You do not need a profile or a registry entry.
The example below is for Responses (/responses), a different API.
Use it only if your server implements that endpoint. Use
ProviderProfile.inference() with an OpenAIResponsesCompat preset.
from lm15.compat import OpenAIResponsesCompat
from lm15.profiles import ProviderProfile
from lm15.providers import OpenAILM
profile = ProviderProfile.inference(
provider="ollama",
api_family="openai_responses",
base_url="http://localhost:11434/v1",
compat=OpenAIResponsesCompat.preset("ollama"),
)
lm = OpenAILM.from_profile(api_key="ollama", profile=profile)
The preset tells the OpenAI Responses serializer to use local-server-friendly
choices such as max_tokens, system for developer instructions, no strict
schema field, and no prompt-cache fields.
Compatibility policy fields¶
OpenAIResponsesCompat is a partial policy. Fields default to None, which
means "inherit from the parent profile". The string value "auto" is different:
it explicitly asks the adapter to use its automatic default for that field.
from lm15.compat import OpenAIResponsesCompat
compat = OpenAIResponsesCompat(
developer_role="system",
max_output_tokens_field="max_tokens",
reasoning_format="qwen",
strict_tools="omit",
cache_control="none",
)
Important fields:
| Field | Meaning |
|---|---|
developer_role |
Serialize Message.developer(...) as developer or system. |
max_output_tokens_field |
Emit max_output_tokens, max_completion_tokens, or max_tokens. |
reasoning_format |
Map Config.reasoning to Responses reasoning, reasoning_effort, Qwen, DeepSeek, etc. |
tool_result_name |
Include or omit name on function call outputs. |
strict_tools |
Include or omit the OpenAI strict field on function tools. |
cache_control |
Emit OpenAI prompt-cache fields, no cache fields, or future dialects. |
routing |
Provider routing object, for example OpenRouter provider selection. |
Available presets include:
OpenAIResponsesCompat.preset("openai")
OpenAIResponsesCompat.preset("openrouter")
OpenAIResponsesCompat.preset("ollama")
OpenAIResponsesCompat.preset("vllm")
OpenAIResponsesCompat.preset("qwen")
OpenAIResponsesCompat.preset("deepseek")
OpenAIResponsesCompat.preset("zai")
Provider-level plus model-level overrides¶
Provider profiles can carry default compatibility, and individual models can override it.
from lm15.compat import OpenAIResponsesCompat
from lm15.models import InferenceModelInfo, ModelInfo
from lm15.profiles import ProviderProfile
profile = ProviderProfile.inference(
provider="local",
api_family="openai_responses",
base_url="http://localhost:8000/v1",
compat=OpenAIResponsesCompat.preset("vllm"),
models=(
ModelInfo(
id="qwen3-coder",
provider="local",
api_family="openai_responses",
aliases=("qwen",),
inference=InferenceModelInfo(
input_modalities=("text",),
output_modalities=("text",),
context_window=128000,
max_output_tokens=8192,
supports_reasoning=True,
reasoning_efforts=("off", "low", "medium", "high"),
),
compat=OpenAIResponsesCompat(reasoning_format="qwen"),
),
),
)
Resolution order for OpenAI Responses requests is:
base URL default
< ProviderProfile endpoint compat
< ModelInfo compat for Request.model
< Config.extensions request-level override
None fields inherit. Non-None fields override, including "auto".
Request-level escape hatch¶
Request-level compatibility is supported for experiments and temporary provider features, but it should not be the normal configuration path.
from lm15.types import Config, Message, Request
request = Request(
model="weird-local-model",
messages=(Message.user("Hi"),),
config=Config(
max_tokens=200,
extensions={
"openai_responses_compat": {
"max_output_tokens_field": "max_tokens",
"reasoning_format": "qwen",
},
"metadata": {"user_id": "u1"},
},
),
)
Compatibility keys are consumed by the adapter and are not passed through to the
provider payload. Other extension keys, such as metadata, still pass through.
Supported request-level shapes:
{"openai_responses_compat": {...}}
{"openai_compat": {...}}
{"compat": {"openai_responses": {...}}}
{"compat": {"openai": {...}}}
Model metadata¶
ModelInfo describes a model without changing the request shape.
from lm15.models import InferenceModelInfo, InferencePricing, ModelInfo
model = ModelInfo(
id="gpt-4.1-mini",
provider="openai",
api_family="openai_responses",
aliases=("mini",),
inference=InferenceModelInfo(
input_modalities=("text", "image"),
output_modalities=("text",),
context_window=1_000_000,
max_output_tokens=32768,
supports_reasoning=False,
pricing=InferencePricing(
input_per_million=0.40,
output_per_million=1.60,
cache_read_per_million=0.10,
),
),
)
Capabilities are endpoint-specific. ModelInfo.inference describes inference
capabilities; new endpoint families can be described later without touching
it. Fine-tune provenance lives on ModelOrigin (type, base_model) —
lm15 does inference with tuned models, not training.
Model registry¶
ModelRegistry is optional. Use it when your application needs discovery,
validation, aliases, or cost estimation.
from lm15.models import ModelRegistry
registry = ModelRegistry.from_profiles([profile])
info = registry.resolve("qwen", provider="local")
assert info is not None
assert info.inference is not None
print(info.inference.context_window)
You can also add generated or fine-tuned models at runtime:
registry.add(
ModelInfo(
id="ft:gpt-4.1-mini:org:project:abc123",
provider="openai",
api_family="openai_responses",
origin=ModelOrigin(
type="fine_tune",
id="ftjob-abc123",
base_model="gpt-4.1-mini",
),
)
)
Cost estimation¶
Pricing is deliberately separate from Usage, because price tables change and
some providers price non-token dimensions differently.
pricing = info.inference.pricing
if pricing is not None:
cost = pricing.estimate(
input_tokens=response.usage.input_tokens,
output_tokens=response.usage.output_tokens,
cache_read_tokens=response.usage.cache_read_tokens,
cache_write_tokens=response.usage.cache_write_tokens,
)
print(cost, pricing.currency)
Design rules¶
- Keep
Requestsemantic and provider-neutral. - Put provider wire quirks in compat policies.
- Put endpoint URLs and defaults in
ProviderProfile. - Put model capabilities and prices in
ModelInfo. - Use
ModelRegistryonly when you need discovery or validation. - Prefer provider/model profiles over request-level compat overrides.