"""GitHub Models provider using OpenAI-compatible chat completions.""" from collections.abc import Mapping, Sequence from typing import Any import httpx from free_claude_code.application.model_metadata import ProviderModelInfo from free_claude_code.core.anthropic import ReasoningReplayMode from free_claude_code.providers.admission import ProviderAdmissionController from free_claude_code.providers.base import ProviderConfig from free_claude_code.providers.http import maybe_await_aclose from free_claude_code.providers.model_listing import ( ModelListResponseError, model_infos_from_ids, ) from free_claude_code.providers.openai_chat import ( NO_REASONING, OpenAIChatProfile, OpenAIChatProvider, OpenAIChatRequestPolicy, ) GITHUB_MODELS_CATALOG_URL = "https://models.github.ai/catalog/models" GITHUB_MODELS_API_VERSION = "2026-03-10" _REQUEST_POLICY = OpenAIChatRequestPolicy( provider_name="GITHUB_MODELS", reasoning_replay=ReasoningReplayMode.THINK_TAGS, ) _PROFILE = OpenAIChatProfile(_REQUEST_POLICY, NO_REASONING) _REQUIRED_MODEL_CAPABILITIES = frozenset({"streaming", "tool-calling"}) class GitHubModelsProvider(OpenAIChatProvider): """GitHub Models OpenAI-compatible inference provider.""" def __init__( self, config: ProviderConfig, *, admission: ProviderAdmissionController ): self._catalog_url = GITHUB_MODELS_CATALOG_URL self._model_list_client = httpx.AsyncClient( proxy=config.proxy or None, timeout=httpx.Timeout( config.http_read_timeout, connect=config.http_connect_timeout, read=config.http_read_timeout, write=config.http_write_timeout, ), ) super().__init__( config, profile=_PROFILE, admission=admission, default_headers=_github_models_default_headers(), ) async def cleanup(self) -> None: """Release provider client resources.""" await super().cleanup() await self._model_list_client.aclose() async def list_model_infos(self) -> frozenset[ProviderModelInfo]: """Return stream/tool-capable GitHub Models catalog ids.""" async def request() -> httpx.Response: response = await self._model_list_client.get( self._catalog_url, headers=self._model_list_headers(), ) try: response.raise_for_status() except Exception: await maybe_await_aclose(response) raise return response response = await self._admission.run_with_retry(request) try: try: payload = response.json() except ValueError as exc: raise ModelListResponseError( "GITHUB_MODELS model-list response is malformed: invalid JSON" ) from exc return model_infos_from_ids( _extract_supported_github_model_ids(payload), ) finally: await maybe_await_aclose(response) def _model_list_headers(self) -> dict[str, str]: return _github_models_api_headers(self._api_key) def _github_models_default_headers() -> dict[str, str]: return { "Accept": "application/vnd.github+json", "X-GitHub-Api-Version": GITHUB_MODELS_API_VERSION, } def _github_models_api_headers(api_key: str) -> dict[str, str]: return { **_github_models_default_headers(), "Authorization": f"Bearer {api_key}", } def _extract_supported_github_model_ids(payload: Any) -> frozenset[str]: """Extract stream/tool-capable model ids from GitHub's catalog array.""" if not _is_sequence(payload): raise ModelListResponseError( "GITHUB_MODELS model-list response is malformed: expected top-level array" ) model_ids: set[str] = set() for item in payload: if not isinstance(item, Mapping): raise ModelListResponseError( "GITHUB_MODELS model-list response is malformed: expected every item to be an object" ) model_id = item.get("id") if not isinstance(model_id, str) or not model_id.strip(): raise ModelListResponseError( "GITHUB_MODELS model-list response is malformed: expected every item to include id" ) capabilities = item.get("capabilities") if not _supports_streaming_tools(capabilities): continue model_ids.add(model_id) return frozenset(model_ids) def _supports_streaming_tools(capabilities: Any) -> bool: if not _is_sequence(capabilities): return False capability_names = {item for item in capabilities if isinstance(item, str)} return capability_names >= _REQUIRED_MODEL_CAPABILITIES def _is_sequence(value: Any) -> bool: return isinstance(value, Sequence) and not isinstance( value, str | bytes | bytearray )