Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
28 changes: 26 additions & 2 deletions contextual_orchestrator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,8 @@ def _discover_models_command(argv: list[str]) -> None:
"model": model.model_id,
"agent_id": agent_id_for(model),
"is_free": model.is_free,
"max_output_tokens": model.max_output_tokens,
"context_window": model.context_window,
"data_privacy": {
"zero_data_retention": (
"supported" if model.supports_zero_data_retention is True else
Expand Down Expand Up @@ -427,7 +429,27 @@ def _auto_discover_runtime_agents(orchestrator: TaskOrchestrator) -> dict[str, l
agents.append(replace(agent_from_discovered(model), disabled=not routable))
elif "discovered" not in existing.tags:
continue
elif not routable:
else:
max_output_tokens = (
None
if model.max_output_tokens_conflicted
else model.max_output_tokens or existing.max_output_tokens
)
context_window = (
None
if model.context_window_conflicted
else model.context_window or existing.context_window
)
Comment thread
seonghobae marked this conversation as resolved.
limits_changed = (
existing.max_output_tokens != max_output_tokens
or existing.context_window != context_window
)
existing = replace(
existing,
max_output_tokens=max_output_tokens,
context_window=context_window,
)
Comment thread
seonghobae marked this conversation as resolved.
if existing is not None and not routable:
tags = (*existing.tags, "spend:blocked")
if existing.disabled and "spend:blocked" not in existing.tags:
tags = (*tags, "spend:blocked:preserve-disabled")
Expand All @@ -438,7 +460,7 @@ def _auto_discover_runtime_agents(orchestrator: TaskOrchestrator) -> dict[str, l
tags=tuple(dict.fromkeys(tags)),
)
)
elif "spend:blocked" in existing.tags:
elif existing is not None and "spend:blocked" in existing.tags:
agents.append(
replace(
existing,
Expand All @@ -450,6 +472,8 @@ def _auto_discover_runtime_agents(orchestrator: TaskOrchestrator) -> dict[str, l
),
)
)
elif existing is not None and limits_changed:
agents.append(existing)
result = (
orchestrator.sync_discovered_agents(agents)
if agents
Expand Down
22 changes: 22 additions & 0 deletions contextual_orchestrator/api_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from __future__ import annotations

_AGENT_POOL_INTEGER_MAX = 9_223_372_036_854_775_807

OPENAPI_SPEC = {
"openapi": "3.1.0",
"info": {
Expand Down Expand Up @@ -437,6 +439,26 @@
"tags": {"type": "array", "items": {"type": "string"}},
"provider_exclusions": {"type": "array", "items": {"type": "string"}},
"group_name": {"type": "string"},
"max_output_tokens": {
"anyOf": [
{
"type": "integer",
"minimum": 1,
"maximum": _AGENT_POOL_INTEGER_MAX,
},
{"type": "null"},
]
},
"context_window": {
"anyOf": [
{
"type": "integer",
"minimum": 1,
"maximum": _AGENT_POOL_INTEGER_MAX,
},
{"type": "null"},
]
},
"stream_usage_supported": {"type": "boolean"},
},
},
Expand Down
99 changes: 98 additions & 1 deletion contextual_orchestrator/model_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import urllib.request
import certifi
from dataclasses import dataclass, replace
from typing import TYPE_CHECKING, Any, Literal, Mapping
from typing import TYPE_CHECKING, Any, Literal, Mapping, Sequence
from urllib.parse import quote, urlsplit, urlunsplit

from .chat_capability import (
Expand Down Expand Up @@ -273,6 +273,10 @@ class DiscoveredModel:
capabilities: tuple[str, ...] = ()
input_modalities: tuple[str, ...] = ()
output_modalities: tuple[str, ...] = ()
max_output_tokens: int | None = None
context_window: int | None = None
max_output_tokens_conflicted: bool = False
context_window_conflicted: bool = False
prompt_price_per_1k: float | None = None
completion_price_per_1k: float | None = None
currency_code: str = "USD"
Expand All @@ -296,6 +300,29 @@ def __init__(self, provider_name: str, error_code: str) -> None:
super().__init__(f"model discovery failed for provider {provider_name!r}: {error_code}")


def _positive_int_metadata(value: object) -> int | None:
"""Return one exact positive integer metadata field or ``None`` when unknown."""
if isinstance(value, bool):
return None
if isinstance(value, int):
return value if 0 < value <= 9_223_372_036_854_775_807 else None
if isinstance(value, float):
if not math.isfinite(value) or not value.is_integer():
return None
integer = int(value)
return integer if 0 < integer <= 9_223_372_036_854_775_807 else None
if isinstance(value, str):
stripped = value.strip()
if not stripped.isdigit():
return None
try:
integer = int(stripped)
except ValueError:
return None
return integer if 0 < integer <= 9_223_372_036_854_775_807 else None
return None
Comment thread
seonghobae marked this conversation as resolved.


def _fetch_json(url: str, *, api_key: str = "", auth_scheme: str = "Bearer", timeout: float) -> Any:
if not url.startswith("https://"):
# Every caller passes one of the hardcoded PROVIDER_SOURCES chat_base_url
Expand Down Expand Up @@ -503,6 +530,8 @@ def _deduplicate_discovered_models(
chosen = min((previous, model), key=_source_tiebreaker)
unique[identity] = replace(
chosen,
max_output_tokens=None,
context_window=None,
Comment thread
seonghobae marked this conversation as resolved.
prompt_price_per_1k=None,
completion_price_per_1k=None,
unit_prices=(),
Expand Down Expand Up @@ -625,6 +654,7 @@ def _merge_models_dev_metadata(payload: Any, metadata: Any, provider: str) -> An
if _valid_price_component(value):
pricing[target_key] = str(Decimal(str(value)) / Decimal(1_000_000))
modalities = model.get("modalities") if isinstance(model.get("modalities"), dict) else {}
limits = model.get("limit") if isinstance(model.get("limit"), dict) else {}
enriched.append(
{
**row,
Expand All @@ -633,6 +663,8 @@ def _merge_models_dev_metadata(payload: Any, metadata: Any, provider: str) -> An
"input_modalities": modalities.get("input"),
"output_modalities": modalities.get("output"),
},
"max_output_tokens": _positive_int_metadata(limits.get("output")),
"context_window": _positive_int_metadata(limits.get("context")),
"is_free": _models_dev_cost_is_free(cost),
}
)
Expand Down Expand Up @@ -676,6 +708,10 @@ def _merge_configured_gateway_metadata(payload: Any, metadata: Any) -> Any:
row.pop("pricing", None)
row.pop("architecture", None)
row.pop("unit_pricing", None)
row.pop("max_output_tokens", None)
row.pop("context_window", None)
Comment thread
seonghobae marked this conversation as resolved.
row.pop("context_length", None)
row.pop("max_completion_tokens", None)
Comment thread
seonghobae marked this conversation as resolved.
for key in (
"supports_zero_data_retention",
"supports_no_training",
Expand All @@ -689,6 +725,8 @@ def _merge_configured_gateway_metadata(payload: Any, metadata: Any) -> Any:
prices: set[tuple[object, object]] = set()
pricing_complete = bool(model_details)
unit_price_maps: list[tuple[tuple[str, object], ...]] = []
completion_limits: list[int | None] = []
context_windows: list[int | None] = []
privacy_values = {
key: []
for key in (
Expand Down Expand Up @@ -746,6 +784,22 @@ def _merge_configured_gateway_metadata(payload: Any, metadata: Any) -> Any:
completion = info.get(
"output_cost_per_token", params.get("output_cost_per_token")
)
completion_limits.append(
_positive_int_metadata(
info.get("max_output_tokens", params.get("max_output_tokens"))
)
or _positive_int_metadata(
info.get("max_completion_tokens", params.get("max_completion_tokens"))
)
)
context_windows.append(
_positive_int_metadata(
info.get("context_window", params.get("context_window"))
)
or _positive_int_metadata(
info.get("context_length", params.get("context_length"))
)
)
if _valid_price_component(prompt) and _valid_price_component(completion):
prices.add((prompt, completion))
else:
Expand Down Expand Up @@ -777,6 +831,18 @@ def _merge_configured_gateway_metadata(payload: Any, metadata: Any) -> Any:
"input_modalities": list(deployment_inputs[0]),
"output_modalities": list(deployment_outputs[0]),
}
if completion_limits and all(value is not None for value in completion_limits):
unique_completion_limits = {value for value in completion_limits if value is not None}
if len(unique_completion_limits) == 1:
row["max_output_tokens"] = unique_completion_limits.pop()
else:
row["_max_output_tokens_conflicted"] = True
if context_windows and all(value is not None for value in context_windows):
unique_context_windows = {value for value in context_windows if value is not None}
if len(unique_context_windows) == 1:
row["context_window"] = unique_context_windows.pop()
Comment thread
seonghobae marked this conversation as resolved.
else:
row["_context_window_conflicted"] = True
if pricing_complete and len(prices) == 1:
prompt, completion = prices.pop()
if prompt is not None and completion is not None:
Expand Down Expand Up @@ -941,10 +1007,29 @@ def _parse_openai_compatible(payload: Any, source: ProviderModelSource) -> list[
if isinstance(row.get("supported_parameters"), list)
else []
)
top_provider = row.get("top_provider") if isinstance(row.get("top_provider"), dict) else {}
raw_inputs = architecture.get("input_modalities")
raw_outputs = architecture.get("output_modalities")
inputs = tuple(value for value in raw_inputs if isinstance(value, str)) if isinstance(raw_inputs, list) else ()
outputs = tuple(value for value in raw_outputs if isinstance(value, str)) if isinstance(raw_outputs, list) else ()
raw_output_limits = [
value
for container, key in (
(row, "max_output_tokens"),
(top_provider, "max_completion_tokens"),
)
if key in container
for value in (container[key],)
]
raw_context_limits = [
row[key]
for key in ("context_window", "context_length")
if key in row
]
output_limits = [_positive_int_metadata(value) for value in raw_output_limits]
context_limits = [_positive_int_metadata(value) for value in raw_context_limits]
max_output_tokens = next((value for value in output_limits if value is not None), None)
context_window = next((value for value in context_limits if value is not None), None)
if (
not outputs
and not any(capability != "chat" for capability in source.capabilities)
Expand Down Expand Up @@ -1004,6 +1089,16 @@ def _parse_openai_compatible(payload: Any, source: ProviderModelSource) -> list[
capabilities=capabilities,
input_modalities=inputs,
output_modalities=outputs,
max_output_tokens=max_output_tokens,
context_window=context_window,
max_output_tokens_conflicted=(
row.get("_max_output_tokens_conflicted") is True
or any(value is None for value in output_limits)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Missing enrichment clears saved limits

When Models.dev omits a limit, _merge_models_dev_metadata inserts None, which becomes a conflict. A successful refresh then deletes the saved limit.

Prompt for agents
Distinguish absent Models.dev limit metadata from explicitly invalid provider metadata. _merge_models_dev_metadata currently always writes max_output_tokens and context_window, using None when limit.output or limit.context is absent; _parse_openai_compatible now interprets any present invalid value as a conflict, and provider catalog refreshes use that conflict to clear persisted limits. Preserve the conflict signal for explicit invalid values, but omit candidate keys when the upstream source supplied no value. Add coverage for a previously saved limit followed by Models.dev metadata that omits one or both limits, alongside the existing explicit zero/negative invalid-metadata cases.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

),
context_window_conflicted=(
row.get("_context_window_conflicted") is True
or any(value is None for value in context_limits)
),
prompt_price_per_1k=prompt_price,
completion_price_per_1k=completion_price,
unit_prices=unit_prices,
Expand Down Expand Up @@ -1484,6 +1579,8 @@ def agent_from_discovered(discovered: DiscoveredModel, *, priority: int = 0) ->
),
priority=priority,
disabled=True,
max_output_tokens=discovered.max_output_tokens,
context_window=discovered.context_window,
Comment thread
seonghobae marked this conversation as resolved.
)


Expand Down
Loading
Loading