Skip to content
Closed
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions contextual_orchestrator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,8 @@ def main() -> None:
"--temperature",
dest="sampling_temperature",
type=float,
default=0.2,
help="Default provider sampling temperature (default: 0.2; --temperature is a compatibility alias).",
default=None,
help="Optional provider sampling temperature; omitted by default so model capabilities decide (the alias is retained for compatibility).",
)
parser.add_argument("--max-output-tokens", type=int, default=2048,
help="Default provider output token cap (default: 2048).")
Expand Down
133 changes: 102 additions & 31 deletions contextual_orchestrator/orchestrator.py
Comment thread
seonghobae marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,16 @@ def complete_structured(
if not isinstance(response_format, dict):
raise TypeError("response_format must be a mapping")
agent = self._agent()
response = self.orchestrator.proxy_completion({
payload: dict[str, Any] = {
"model": agent.model,
"messages": messages,
"temperature": self.orchestrator.client.temperature,
"max_tokens": self.orchestrator.client.max_output_tokens,
"max_tokens": self.orchestrator.client._effective_max_output_tokens(),
"response_format": response_format,
})
}
effective_temperature = self.orchestrator.client._effective_temperature()
if effective_temperature is not None:
payload["temperature"] = effective_temperature
response = self.orchestrator.proxy_completion(payload)
Comment thread
seonghobae marked this conversation as resolved.
output = ModelClient._response_content(agent, response)
usage = response.get("usage") if isinstance(response.get("usage"), dict) else None
return self._completion_payload(output, agent.id, usage, self.mode if mode is None else mode)
Expand Down Expand Up @@ -686,7 +689,7 @@ def __init__(
local_max_retries: int = 0,
retry_backoff: float = 0.5,
retry_backoff_cap: float = 8.0,
temperature: float = 0.2,
temperature: float | None = None,
Comment thread
seonghobae marked this conversation as resolved.
local_concurrency: int = 1,
chat_template_args: dict[str, Any] | None = None,
ca_bundle: str | None = None,
Expand All @@ -697,7 +700,9 @@ def __init__(
self.max_output_tokens = max_output_tokens
if isinstance(max_retries, bool) or max_retries < 0:
raise ValueError("max_retries must be >= 0")
self.default_temperature = 0.2
# Do not invent a sampling parameter for a provider/model that may not
# support it. Explicit caller values are still forwarded after validation.
self.default_temperature = temperature
Comment thread
seonghobae marked this conversation as resolved.
self.default_top_p: float | None = None
self.default_presence_penalty: float | None = None
self.default_frequency_penalty: float | None = None
Expand All @@ -707,7 +712,6 @@ def __init__(
self.local_max_retries = int(local_max_retries)
self.retry_backoff = retry_backoff
self.retry_backoff_cap = retry_backoff_cap
self.temperature = temperature
if type(local_concurrency) is not int or not 1 <= local_concurrency <= MAX_LOCAL_CONCURRENCY:
raise ValueError(
f"local_concurrency must be an integer in 1..{MAX_LOCAL_CONCURRENCY}"
Expand Down Expand Up @@ -763,6 +767,48 @@ def take_usage(self) -> dict[str, Any] | None:
self._local.usage = None
return usage

@contextmanager
def request_options(
self,
*,
max_output_tokens: int | None = None,
temperature: float | None = None,
top_p: float | None = None,
presence_penalty: float | None = None,
frequency_penalty: float | None = None,
):
"""Apply HTTP request controls without mutating shared client defaults."""
missing = object()
options = {
"request_max_output_tokens": max_output_tokens,
"request_temperature": temperature,
"request_top_p": top_p,
"request_presence_penalty": presence_penalty,
"request_frequency_penalty": frequency_penalty,
}
previous = {name: getattr(self._local, name, missing) for name in options}
for name, value in options.items():
if value is not None:
setattr(self._local, name, value)
try:
yield
finally:
for name, value in previous.items():
if value is missing:
self._local.__dict__.pop(name, None)
else:
setattr(self._local, name, value)
Comment thread
seonghobae marked this conversation as resolved.

def _effective_temperature(self, requested: float | None = None) -> float | None:
"""Resolve request, request-scoped, and constructor sampling values in order."""
if requested is not None:
return requested
return getattr(self._local, "request_temperature", self.default_temperature)

def _effective_max_output_tokens(self) -> int:
"""Resolve the request-scoped output limit or the constructor default."""
return getattr(self._local, "request_max_output_tokens", self.max_output_tokens)
Comment thread
seonghobae marked this conversation as resolved.

def chat(
self,
agent: ModelAgent,
Expand All @@ -772,16 +818,24 @@ def chat(
) -> str:
"""Send messages to a mock or OpenAI-compatible chat endpoint with retries.

When ``temperature``/``top_p`` are omitted, ``default_temperature`` and
``default_top_p`` are used so request-scoped Completions sampling can be
applied without threading kwargs through every orchestrator hop.
When sampling knobs are omitted, they are omitted from the provider
request so the selected model's capability contract remains authoritative.
Explicit request-scoped values are still forwarded after validation.
"""
self._local.usage = None
# Expose the effective sampling knobs for request-path tests / diagnostics.
effective_temperature = self.default_temperature if temperature is None else temperature
effective_top_p = self.default_top_p if top_p is None else top_p
effective_presence = self.default_presence_penalty
effective_frequency = self.default_frequency_penalty
effective_temperature = self._effective_temperature(temperature)
effective_top_p = (
getattr(self._local, "request_top_p", self.default_top_p)
if top_p is None
else top_p
)
effective_presence = getattr(
self._local, "request_presence_penalty", self.default_presence_penalty
)
effective_frequency = getattr(
self._local, "request_frequency_penalty", self.default_frequency_penalty
)
self._local.last_temperature = effective_temperature
self._local.last_top_p = effective_top_p
self._local.last_presence_penalty = effective_presence
Expand All @@ -800,10 +854,11 @@ def chat(
payload = { # pragma: no cover
"model": agent.model,
"messages": messages,
"temperature": effective_temperature,
"stream": False,
"max_tokens": self.max_output_tokens,
"max_tokens": self._effective_max_output_tokens(),
}
if effective_temperature is not None: # pragma: no cover
payload["temperature"] = effective_temperature
if effective_top_p is not None: # pragma: no cover
payload["top_p"] = effective_top_p
if effective_presence is not None: # pragma: no cover
Expand Down Expand Up @@ -857,7 +912,6 @@ def probe(self, agent: ModelAgent, *, timeout: float = DEFAULT_PROVIDER_PROBE_TI
payload: dict[str, Any] = {
"model": agent.model,
"messages": [{"role": "user", "content": "Reply with exactly OK."}],
"temperature": 0.0,
"stream": False,
"max_tokens": 1,
}
Expand Down Expand Up @@ -1080,10 +1134,25 @@ def stream_chat(self, agent: ModelAgent, messages: list[ChatMessage], temperatur
payload = { # pragma: no cover
"model": agent.model,
"messages": messages,
"temperature": self.temperature if temperature is None else temperature,
"stream": True,
"max_tokens": self.max_output_tokens,
"max_tokens": self._effective_max_output_tokens(),
}
effective_temperature = self._effective_temperature(temperature)
if effective_temperature is not None: # pragma: no cover
payload["temperature"] = effective_temperature
effective_top_p = getattr(self._local, "request_top_p", self.default_top_p)
if effective_top_p is not None: # pragma: no cover
payload["top_p"] = effective_top_p
effective_presence = getattr(
self._local, "request_presence_penalty", self.default_presence_penalty
)
if effective_presence is not None: # pragma: no cover
payload["presence_penalty"] = effective_presence
effective_frequency = getattr(
self._local, "request_frequency_penalty", self.default_frequency_penalty
)
if effective_frequency is not None: # pragma: no cover
payload["frequency_penalty"] = effective_frequency
if _is_direct_mlx_provider_url(agent.base_url) and self.chat_template_args:
payload["chat_template_kwargs"] = self.chat_template_args
with _local_provider_slot(agent, self.local_concurrency, self.timeout): # pragma: no cover
Expand Down Expand Up @@ -1351,26 +1420,28 @@ def _batch_run(
self,
agent: ModelAgent,
requests: dict[str, list[ChatMessage]],
temperature: float,
temperature: float | None,
poll_interval: float,
poll_timeout: float,
destination: ProviderDestination | None = None,
) -> dict[str, dict[str, Any]]:
"""Upload, create, poll, and parse one batch (isolated so the flow stays testable)."""
lines = [
json.dumps({
lines = []
for custom_id, messages in requests.items():
body: dict[str, Any] = {
"model": agent.model,
"messages": messages,
"max_tokens": self._effective_max_output_tokens(),
}
effective_temperature = self._effective_temperature(temperature)
if effective_temperature is not None:
body["temperature"] = effective_temperature
Comment thread
seonghobae marked this conversation as resolved.
lines.append(json.dumps({
"custom_id": custom_id,
"method": "POST",
"url": "/v1/chat/completions",
"body": {
"model": agent.model,
"messages": messages,
"temperature": self.temperature if temperature is None else temperature,
"max_tokens": self.max_output_tokens,
},
}, ensure_ascii=False)
for custom_id, messages in requests.items()
]
"body": body,
}, ensure_ascii=False))
input_file_id = self._batch_upload(agent, "\n".join(lines).encode("utf-8"), destination)
batch_id = self._batch_json(agent, "POST", "/batches", {
"input_file_id": input_file_id,
Expand Down
59 changes: 14 additions & 45 deletions contextual_orchestrator/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -4886,24 +4886,14 @@ def do_POST(self) -> None: # noqa: N802
attribution["service"] = "completions_api"
routing = _validate_routing(body.get("routing"))
started_at = time.perf_counter()
# Apply request sampling knobs to the provider client for this call.
model_client = orchestrator.client
previous_max_tokens = model_client.max_output_tokens
previous_temperature = model_client.default_temperature
previous_top_p = model_client.default_top_p
previous_presence = model_client.default_presence_penalty
previous_frequency = model_client.default_frequency_penalty
if max_tokens is not None:
model_client.max_output_tokens = max_tokens
if temperature is not None:
model_client.default_temperature = temperature
if top_p is not None:
model_client.default_top_p = top_p
if presence_penalty is not None:
model_client.default_presence_penalty = presence_penalty
if frequency_penalty is not None:
model_client.default_frequency_penalty = frequency_penalty
try:
with model_client.request_options(
max_output_tokens=max_tokens,
temperature=temperature,
top_p=top_p,
presence_penalty=presence_penalty,
frequency_penalty=frequency_penalty,
):
result = self._run(lambda: coordinator.complete(
messages,
mode="route",
Expand All @@ -4912,12 +4902,6 @@ def do_POST(self) -> None: # noqa: N802
model_name=model_name,
workflow_run_id=f"run_{uuid.uuid4().hex}",
))
finally:
model_client.max_output_tokens = previous_max_tokens
model_client.default_temperature = previous_temperature
model_client.default_top_p = previous_top_p
model_client.default_presence_penalty = previous_presence
model_client.default_frequency_penalty = previous_frequency
# Batch-channel Completions return a job handle (202), not a
# text_completion body — match chat Completions honesty so
# clients never receive a 500 on a valid batch routing hint.
Expand Down Expand Up @@ -5132,22 +5116,13 @@ def do_POST(self) -> None: # noqa: N802
_validate_openai_metadata(body)
started_at = time.perf_counter()
model_client = orchestrator.client
previous_max_tokens = model_client.max_output_tokens
previous_temperature = model_client.default_temperature
previous_top_p = model_client.default_top_p
previous_presence = model_client.default_presence_penalty
previous_frequency = model_client.default_frequency_penalty
if max_tokens is not None:
model_client.max_output_tokens = max_tokens
if temperature is not None:
model_client.default_temperature = temperature
if top_p is not None:
model_client.default_top_p = top_p
if presence_penalty is not None:
model_client.default_presence_penalty = presence_penalty
if frequency_penalty is not None:
model_client.default_frequency_penalty = frequency_penalty
try:
with model_client.request_options(
max_output_tokens=max_tokens,
temperature=temperature,
top_p=top_p,
presence_penalty=presence_penalty,
frequency_penalty=frequency_penalty,
):
if stream and orchestrator.would_route(messages, mode):
self._stream_route_completion(orchestrator, security, messages, model_name)
orchestrator.record_analytics_event(
Expand All @@ -5170,12 +5145,6 @@ def do_POST(self) -> None: # noqa: N802
model_name=model_name,
workflow_run_id=f"run_{uuid.uuid4().hex}",
))
finally:
model_client.max_output_tokens = previous_max_tokens
model_client.default_temperature = previous_temperature
model_client.default_top_p = previous_top_p
model_client.default_presence_penalty = previous_presence
model_client.default_frequency_penalty = previous_frequency
# Latency-tolerant requests get dispatched to the batch backend.
if result.get("channel") == "batch":
orchestrator.record_analytics_event(
Expand Down
51 changes: 51 additions & 0 deletions docs/adr/0123-omit-unsupported-sampling-defaults.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# ADR 0123: Omit Unsupported Sampling Defaults

- Status: Accepted
- Date: 2026-08-21

## Context

Reasoning-capable providers can reject a `temperature` field even when the
value is a conventional default such as `0.2`. A gateway that invents this
field before capability negotiation makes an otherwise valid discovered model
unusable and can turn failover into a false provider outage. Azure's current
reasoning-model documentation explicitly lists `temperature` among the
unsupported parameters (Microsoft, 2026).

## Decision

The orchestrator omits `temperature` and other optional sampling controls when
the caller did not explicitly provide them. The provider and selected model
therefore own their documented defaults and capability handling. Explicit
values remain validated at the public request boundary and are forwarded
unchanged through chat, streaming, batch, and structured-output transport
paths. A client-level explicit value, including the CLI option, applies to
ordinary chat as well as streaming and batch calls; a request-scoped value
temporarily overrides it for both streamed and non-streamed requests. HTTP
request overrides are isolated from concurrent requests; the server must not
mutate shared client defaults while a request is in flight.

The command-line sampling option is optional and defaults to omission. Health
probes also omit sampling controls; a probe must test reachability without
assuming that the model accepts a sampling parameter.

This is a transport-capability contract, not a model-ranking heuristic. Model
selection and reasoning-effort policy remain governed by the paper-grounded
model policy ADRs.

## Consequences

- Reasoning models that reject `temperature` can be selected without a local
model-name exception.
- Callers that require a sampling value must state it explicitly and accept the
selected provider's capability response.
- One request's explicit controls cannot become another request's implicit
defaults.
- Provider request assertions must distinguish omitted fields from explicit
values.

## References

Microsoft. (2026, August 6). *Azure OpenAI reasoning models: GPT-5 series,
o3-mini, o1, o1-mini*. Microsoft Learn.
https://learn.microsoft.com/azure/foundry/openai/how-to/reasoning
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ db = [
"SQLAlchemy>=2.0",
"alembic>=1.17",
"psycopg[binary]>=3.2",
"greenlet>=1",
]
fuzz = [
"atheris==3.1.0; python_version >= '3.12'",
Expand Down
Loading
Loading