core/lmproto: add OpenAI-compatible models() list API

Decode GET /models into ModelsResponse and return sorted unique ids.
This commit is contained in:
2026-07-15 17:31:57 +08:00
parent 4ea3362058
commit e1ea43cb8f
4 changed files with 101 additions and 0 deletions
@@ -21,6 +21,8 @@ from .model import FinishReason as FinishReason
from .model import GrammarSyntax as GrammarSyntax
from .model import JsonObjectResponseFormat as JsonObjectResponseFormat
from .model import Modality as Modality
from .model import ModelObject as ModelObject
from .model import ModelsResponse as ModelsResponse
from .model import NamedChatMessage as NamedChatMessage
from .model import NamedRole as NamedRole
from .model import ReasoningEffort as ReasoningEffort
@@ -15,6 +15,7 @@ from .model import (
ChatCompletionChunk,
ChatCompletionResponse,
ChatCompletionsParam,
ModelsResponse,
StreamToolCallDelta,
)
@@ -95,6 +96,7 @@ class BaseOpenAIClient:
encoder: msgspec.json.Encoder
decoder: msgspec.json.Decoder[ChatCompletionResponse]
chunk_decoder: msgspec.json.Decoder[ChatCompletionChunk]
models_decoder: msgspec.json.Decoder[ModelsResponse]
def __init__(self, config: OpenAIConfig) -> None:
self.session = niquests.AsyncSession(
@@ -104,6 +106,25 @@ class BaseOpenAIClient:
self.encoder = msgspec.json.Encoder()
self.decoder = msgspec.json.Decoder(ChatCompletionResponse)
self.chunk_decoder = msgspec.json.Decoder(ChatCompletionChunk)
self.models_decoder = msgspec.json.Decoder(ModelsResponse)
async def models(self) -> list[str]:
"""List model ids via OpenAI-compatible ``GET /models``.
Returns sorted unique ``id`` values from the response ``data`` array.
"""
resp = await self.session.get("/models", stream=False)
await self._ensure_ok(resp)
body = await read_response_body(resp)
if body is None:
msg = "models response body is empty"
raise RuntimeError(msg)
if not isinstance(body, (bytes, bytearray)):
msg = f"models response body has unexpected type {type(body)!r}"
raise TypeError(msg)
parsed = self.models_decoder.decode(bytes(body))
ids = {item.id for item in parsed.data if item.id}
return sorted(ids)
async def _ensure_ok(self, resp: object) -> None:
"""Raise if the HTTP response is an error (stream or non-stream)."""
@@ -258,3 +258,19 @@ class ChatCompletionChunk(Struct):
choices: list[ChunkChoice]
# Final usage chunk may omit or null usage depending on provider.
usage: dict[str, Any] | None | Unset = UNSET
class ModelObject(Struct):
"""One entry from OpenAI-compatible ``GET /models``."""
id: str
object: Literal["model"] | Unset = UNSET
created: int | Unset = UNSET
owned_by: str | Unset = UNSET
class ModelsResponse(Struct):
"""OpenAI-compatible ``GET /models`` list body."""
data: list[ModelObject]
object: Literal["list"] | Unset = UNSET