mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-08-06 21:48:39 -04:00
b9cafd67a1
* feat(models): define capability schema and readers * fix(models): harden Google catalog probing Restrict native catalog probing to the Gemini host, keep provider keys out of request URLs, filter non-chat model resources, and preserve the manual refresh default in the built-in Google add flow.
201 lines
6.6 KiB
Python
201 lines
6.6 KiB
Python
"""OpenRouter model catalog capability reader."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping
|
|
from typing import Any
|
|
|
|
from src import model_capabilities as mc
|
|
from src.model_capability_readers import generic_openai
|
|
from src.model_capability_readers.base import (
|
|
ModelCapabilityRecord,
|
|
VENDOR_OPENROUTER,
|
|
as_list,
|
|
as_mapping,
|
|
build_capability,
|
|
compact_str,
|
|
deterministic_controls_from_supported_parameters,
|
|
family_from_modalities,
|
|
int_limit,
|
|
merge_unique,
|
|
model_id_from,
|
|
modalities_from_value,
|
|
openai_model_items,
|
|
split_modality_arrow,
|
|
stable_model_id_for,
|
|
)
|
|
|
|
|
|
vendor = VENDOR_OPENROUTER
|
|
|
|
|
|
_SUPPORTED_PARAMETER_CAPS = {
|
|
"tools": mc.CAP_TOOL_CALL,
|
|
"tool_choice": mc.CAP_TOOL_CALL,
|
|
"function_calling": mc.CAP_TOOL_CALL,
|
|
"parallel_tool_calls": mc.CAP_TOOL_CALL,
|
|
"response_format": mc.CAP_JSON_MODE,
|
|
"structured_outputs": mc.CAP_STRUCTURED_OUTPUT,
|
|
"structured_output": mc.CAP_STRUCTURED_OUTPUT,
|
|
"reasoning": mc.CAP_REASONING,
|
|
"reasoning_effort": mc.CAP_REASONING,
|
|
"include_reasoning": mc.CAP_REASONING,
|
|
"web_search": mc.CAP_WEB_SEARCH,
|
|
"web_search_options": mc.CAP_WEB_SEARCH,
|
|
}
|
|
|
|
|
|
def _capabilities_from_supported_parameters(values: Any) -> tuple[str, ...]:
|
|
iterable = values if isinstance(values, list) else ()
|
|
out: list[str] = []
|
|
for value in iterable:
|
|
cap = _SUPPORTED_PARAMETER_CAPS.get(compact_str(value).lower().replace("-", "_"))
|
|
if cap and cap not in out:
|
|
out.append(cap)
|
|
return tuple(out)
|
|
|
|
|
|
def _limits_from_model(raw: Mapping[str, Any]) -> dict[str, Any]:
|
|
architecture = as_mapping(raw.get("architecture"))
|
|
top_provider = as_mapping(raw.get("top_provider"))
|
|
per_request_limits = as_mapping(raw.get("per_request_limits"))
|
|
limits: dict[str, Any] = {}
|
|
for key, canonical in (
|
|
("context_length", "context_tokens"),
|
|
("max_context_length", "context_tokens"),
|
|
("input_token_limit", "input_tokens"),
|
|
("output_token_limit", "output_tokens"),
|
|
("max_completion_tokens", "output_tokens"),
|
|
):
|
|
value = int_limit(raw.get(key) or architecture.get(key) or top_provider.get(key))
|
|
if value:
|
|
limits[canonical] = value
|
|
for key, value in per_request_limits.items():
|
|
limit = int_limit(value)
|
|
if limit:
|
|
limits[f"per_request_{key}"] = limit
|
|
return limits
|
|
|
|
|
|
def _has_supported_voices(value: Any) -> bool:
|
|
return any(compact_str(item) for item in as_list(value))
|
|
|
|
|
|
def _capabilities_from_modalities(
|
|
input_modalities: tuple[str, ...],
|
|
output_modalities: tuple[str, ...],
|
|
*,
|
|
supported_voices: Any = None,
|
|
) -> tuple[str, ...]:
|
|
input_set = set(input_modalities)
|
|
output_set = set(output_modalities)
|
|
capabilities: list[str] = []
|
|
if mc.MODALITY_IMAGE in input_set and mc.MODALITY_TEXT in output_set:
|
|
capabilities.append(mc.CAP_VISION)
|
|
if mc.MODALITY_FILE in input_set:
|
|
capabilities.append(mc.CAP_FILES)
|
|
if mc.MODALITY_PDF in input_set:
|
|
capabilities.append(mc.CAP_PDF)
|
|
if mc.MODALITY_AUDIO in input_set:
|
|
capabilities.append(mc.CAP_AUDIO_INPUT)
|
|
if mc.MODALITY_AUDIO in output_set:
|
|
capabilities.append(mc.CAP_AUDIO_OUTPUT)
|
|
if _has_supported_voices(supported_voices):
|
|
capabilities.append(mc.CAP_TTS)
|
|
if mc.MODALITY_IMAGE in output_set:
|
|
capabilities.append(mc.CAP_IMAGE_GENERATION)
|
|
if mc.MODALITY_IMAGE in input_set:
|
|
capabilities.append(mc.CAP_IMAGE_EDITING)
|
|
if mc.MODALITY_VIDEO in output_set:
|
|
capabilities.append(mc.CAP_VIDEO_GENERATION)
|
|
return tuple(capabilities)
|
|
|
|
|
|
def _default_parameter_controls(raw: Mapping[str, Any]) -> tuple[str, ...]:
|
|
defaults = as_mapping(raw.get("default_parameters"))
|
|
return tuple(key for key, value in defaults.items() if value is not None)
|
|
|
|
|
|
def _deterministic_controls_from_model(raw: Mapping[str, Any]) -> tuple[mc.DeterministicControl, ...]:
|
|
return deterministic_controls_from_supported_parameters(
|
|
merge_unique(
|
|
as_list(raw.get("supported_parameters")),
|
|
_default_parameter_controls(raw),
|
|
)
|
|
)
|
|
|
|
|
|
def record_from_model(
|
|
raw: Mapping[str, Any],
|
|
*,
|
|
endpoint_id: Any = "",
|
|
base_url: Any = "",
|
|
) -> ModelCapabilityRecord | None:
|
|
model_id = model_id_from(raw, "id", "name")
|
|
if not model_id:
|
|
return None
|
|
|
|
architecture = as_mapping(raw.get("architecture"))
|
|
input_modalities = modalities_from_value(
|
|
raw.get("input_modalities") or architecture.get("input_modalities")
|
|
)
|
|
output_modalities = modalities_from_value(
|
|
raw.get("output_modalities") or architecture.get("output_modalities")
|
|
)
|
|
if not input_modalities or not output_modalities:
|
|
arrow_input, arrow_output = split_modality_arrow(
|
|
raw.get("modality") or architecture.get("modality")
|
|
)
|
|
input_modalities = input_modalities or arrow_input
|
|
output_modalities = output_modalities or arrow_output
|
|
|
|
capabilities = list(_capabilities_from_supported_parameters(raw.get("supported_parameters")))
|
|
capabilities.extend(
|
|
_capabilities_from_modalities(
|
|
input_modalities,
|
|
output_modalities,
|
|
supported_voices=raw.get("supported_voices"),
|
|
)
|
|
)
|
|
|
|
family = family_from_modalities(input_modalities, output_modalities)
|
|
if family == mc.FAMILY_UNKNOWN:
|
|
fallback = generic_openai.record_from_model(
|
|
raw,
|
|
vendor_id=VENDOR_OPENROUTER,
|
|
endpoint_id=endpoint_id,
|
|
base_url=base_url,
|
|
)
|
|
return fallback
|
|
|
|
capability = build_capability(
|
|
family=family,
|
|
input_modalities=input_modalities,
|
|
output_modalities=output_modalities,
|
|
capabilities=merge_unique(capabilities),
|
|
limits=_limits_from_model(raw),
|
|
)
|
|
return ModelCapabilityRecord(
|
|
vendor=VENDOR_OPENROUTER,
|
|
model_id=model_id,
|
|
stable_model_id=stable_model_id_for(VENDOR_OPENROUTER, model_id, endpoint_id=endpoint_id, base_url=base_url),
|
|
display_name=compact_str(raw.get("name")) or model_id,
|
|
capability=capability,
|
|
deterministic_controls=_deterministic_controls_from_model(raw),
|
|
raw=raw,
|
|
)
|
|
|
|
|
|
def records_from_payload(
|
|
payload: Mapping[str, Any],
|
|
*,
|
|
endpoint_id: Any = "",
|
|
base_url: Any = "",
|
|
) -> tuple[ModelCapabilityRecord, ...]:
|
|
records: list[ModelCapabilityRecord] = []
|
|
for item in openai_model_items(payload):
|
|
record = record_from_model(item, endpoint_id=endpoint_id, base_url=base_url)
|
|
if record:
|
|
records.append(record)
|
|
return tuple(records)
|