mirror of
https://github.com/NCBM/plyngent.git
synced 2026-07-23 05:55:16 +08:00
core/lmproto: move Responses API into openai package
openai_compatible stays chat-completions-only; OpenAI provider uses lmproto.openai.OpenAIClient.
This commit is contained in:
@@ -30,13 +30,15 @@ All protocol models use `msgspec.Struct` — not dataclasses, not Pydantic. Opti
|
|||||||
|
|
||||||
```
|
```
|
||||||
lmproto/openai_compatible/ ← Base: model, config, client
|
lmproto/openai_compatible/ ← Base: model, config, client
|
||||||
|
lmproto/openai/ ← OpenAI platform: Responses + chat (extends base)
|
||||||
lmproto/deepseek/openai_compat/ ← Extends base via inheritance + extra fields
|
lmproto/deepseek/openai_compat/ ← Extends base via inheritance + extra fields
|
||||||
```
|
```
|
||||||
|
|
||||||
- **`openai_compatible/model.py`** — tagged chat messages (`SystemChatMessage`, `UserChatMessage`, …), tools, request/response, streaming chunks.
|
- **`openai_compatible/model.py`** — tagged chat messages (`SystemChatMessage`, `UserChatMessage`, …), tools, request/response, streaming chunks.
|
||||||
- **`openai_compatible/responses_model.py`** — OpenAI Responses API (`ResponsesCreateParam`, `Response`, function_call items, stream events).
|
- **`openai_compatible/client.py`** — `BaseOpenAIClient` / `OpenAICompatibleClient` via `niquests` async + SSE; `chat_completions`, `models` only (`OpenAIClient` is a compat alias).
|
||||||
- **`openai_compatible/client.py`** — `BaseOpenAIClient` / `OpenAIClient` via `niquests` async + SSE; `chat_completions`, `models`, `responses` / `get_response` / `delete_response`.
|
|
||||||
- **`openai_compatible/config.py`** — `OpenAIConfig` (token + base URL).
|
- **`openai_compatible/config.py`** — `OpenAIConfig` (token + base URL).
|
||||||
|
- **`openai/model.py`** — OpenAI Responses API (`ResponsesCreateParam`, `Response`, function_call items, stream events).
|
||||||
|
- **`openai/client.py`** — platform `OpenAIClient`: chat completions + `responses` / `get_response` / `delete_response`.
|
||||||
- **DeepSeek** — `DeepseekOpenAIClient`; models add `reasoning_content`, `prefix`, `ThinkingOptions`. Config default model ids: `deepseek-v4-flash`, `deepseek-v4-pro`.
|
- **DeepSeek** — `DeepseekOpenAIClient`; models add `reasoning_content`, `prefix`, `ThinkingOptions`. Config default model ids: `deepseek-v4-flash`, `deepseek-v4-pro`.
|
||||||
|
|
||||||
### Config (`config/`)
|
### Config (`config/`)
|
||||||
@@ -45,7 +47,7 @@ TOML load/store (`ConfigStore`): `[providers]` tagged union presets, `[database]
|
|||||||
|
|
||||||
### Runtime (`runtime/`)
|
### Runtime (`runtime/`)
|
||||||
|
|
||||||
`create_client(provider)` maps config `Provider` → protocol client. OpenAI / openai-compatible / deepseek(openai convention) supported; anthropic and deepseek anthropic convention raise `ProviderNotSupportedError`.
|
`create_client(provider)` maps config `Provider` → protocol client. OpenAI → `lmproto.openai.OpenAIClient` (Responses-capable); openai-compatible / deepseek(openai convention) → chat-completions clients; anthropic and deepseek anthropic convention raise `ProviderNotSupportedError`.
|
||||||
|
|
||||||
### Memory (`memory/`)
|
### Memory (`memory/`)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
from .client import OpenAIClient as OpenAIClient
|
||||||
|
from .model import Response as Response
|
||||||
|
from .model import ResponseDeleted as ResponseDeleted
|
||||||
|
from .model import ResponseFunctionTool as ResponseFunctionTool
|
||||||
|
from .model import ResponseFunctionToolCall as ResponseFunctionToolCall
|
||||||
|
from .model import ResponseFunctionToolCallOutput as ResponseFunctionToolCallOutput
|
||||||
|
from .model import ResponseOutputMessage as ResponseOutputMessage
|
||||||
|
from .model import ResponseOutputText as ResponseOutputText
|
||||||
|
from .model import ResponsesCreateParam as ResponsesCreateParam
|
||||||
|
from .model import ResponseStreamEvent as ResponseStreamEvent
|
||||||
|
from .model import response_function_calls as response_function_calls
|
||||||
|
from .model import response_output_text as response_output_text
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
"""OpenAI platform client: chat completions (compat) + Responses API."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Any, Literal, cast, overload
|
||||||
|
|
||||||
|
import msgspec
|
||||||
|
|
||||||
|
from plyngent.lmproto.openai_compatible.client import BaseOpenAIClient
|
||||||
|
from plyngent.lmproto.openai_compatible.config import OpenAIConfig # noqa: TC001
|
||||||
|
from plyngent.lmproto.openai_compatible.model import ( # noqa: TC001
|
||||||
|
ChatCompletionChunk,
|
||||||
|
ChatCompletionResponse,
|
||||||
|
ChatCompletionsParam,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .model import Response, ResponseDeleted, ResponsesCreateParam, ResponseStreamEvent
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import AsyncIterator
|
||||||
|
|
||||||
|
|
||||||
|
class OpenAIClient(BaseOpenAIClient):
|
||||||
|
"""Full OpenAI HTTP client (``/chat/completions`` + ``/responses`` + ``/models``)."""
|
||||||
|
|
||||||
|
response_decoder: msgspec.json.Decoder[Response]
|
||||||
|
response_deleted_decoder: msgspec.json.Decoder[ResponseDeleted]
|
||||||
|
response_event_decoder: msgspec.json.Decoder[ResponseStreamEvent]
|
||||||
|
|
||||||
|
def __init__(self, config: OpenAIConfig) -> None:
|
||||||
|
super().__init__(config)
|
||||||
|
self.response_decoder = msgspec.json.Decoder(Response)
|
||||||
|
self.response_deleted_decoder = msgspec.json.Decoder(ResponseDeleted)
|
||||||
|
self.response_event_decoder = msgspec.json.Decoder(ResponseStreamEvent)
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def chat_completions(
|
||||||
|
self, param: ChatCompletionsParam, *, stream: Literal[False] = False
|
||||||
|
) -> ChatCompletionResponse: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def chat_completions(
|
||||||
|
self, param: ChatCompletionsParam, *, stream: Literal[True]
|
||||||
|
) -> AsyncIterator[ChatCompletionChunk]: ...
|
||||||
|
|
||||||
|
async def chat_completions(
|
||||||
|
self, param: ChatCompletionsParam, *, stream: bool = False
|
||||||
|
) -> ChatCompletionResponse | AsyncIterator[ChatCompletionChunk]:
|
||||||
|
param = msgspec.structs.replace(param, stream=stream)
|
||||||
|
data = self.encoder.encode(param)
|
||||||
|
if stream:
|
||||||
|
resp = await self.session.post(
|
||||||
|
"/chat/completions",
|
||||||
|
data=data,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
stream=True,
|
||||||
|
)
|
||||||
|
return self._parse_sse(resp)
|
||||||
|
resp = await self.session.post(
|
||||||
|
"/chat/completions",
|
||||||
|
data=data,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
stream=False,
|
||||||
|
)
|
||||||
|
body = await self._read_json_body(resp, what="chat completions")
|
||||||
|
return self.decoder.decode(body)
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def responses(
|
||||||
|
self, param: ResponsesCreateParam, *, stream: Literal[False] = False
|
||||||
|
) -> Response: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def responses(
|
||||||
|
self, param: ResponsesCreateParam, *, stream: Literal[True]
|
||||||
|
) -> AsyncIterator[ResponseStreamEvent]: ...
|
||||||
|
|
||||||
|
async def responses(
|
||||||
|
self, param: ResponsesCreateParam, *, stream: bool = False
|
||||||
|
) -> Response | AsyncIterator[ResponseStreamEvent]:
|
||||||
|
"""Create a model response via OpenAI ``POST /responses``."""
|
||||||
|
param = msgspec.structs.replace(param, stream=stream)
|
||||||
|
data = self.encoder.encode(param)
|
||||||
|
if stream:
|
||||||
|
resp = await self.session.post(
|
||||||
|
"/responses",
|
||||||
|
data=data,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
stream=True,
|
||||||
|
)
|
||||||
|
return self._parse_response_sse(resp)
|
||||||
|
resp = await self.session.post(
|
||||||
|
"/responses",
|
||||||
|
data=data,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
stream=False,
|
||||||
|
)
|
||||||
|
body = await self._read_json_body(resp, what="responses")
|
||||||
|
return self.response_decoder.decode(body)
|
||||||
|
|
||||||
|
async def get_response(self, response_id: str) -> Response:
|
||||||
|
"""Retrieve a stored response via ``GET /responses/{id}``."""
|
||||||
|
resp = await self.session.get(f"/responses/{response_id}", stream=False)
|
||||||
|
body = await self._read_json_body(resp, what="responses")
|
||||||
|
return self.response_decoder.decode(body)
|
||||||
|
|
||||||
|
async def delete_response(self, response_id: str) -> ResponseDeleted:
|
||||||
|
"""Delete a stored response via ``DELETE /responses/{id}``."""
|
||||||
|
resp = await self.session.delete(f"/responses/{response_id}", stream=False)
|
||||||
|
body = await self._read_json_body(resp, what="responses")
|
||||||
|
return self.response_deleted_decoder.decode(body)
|
||||||
|
|
||||||
|
async def _parse_response_sse(self, resp: object) -> AsyncIterator[ResponseStreamEvent]:
|
||||||
|
"""Yield Responses API SSE events; stop at ``data: [DONE]``."""
|
||||||
|
from plyngent.lmproto.openai_compatible.client import (
|
||||||
|
close_async_response,
|
||||||
|
sse_data_payload,
|
||||||
|
)
|
||||||
|
|
||||||
|
typed = cast("Any", resp)
|
||||||
|
try:
|
||||||
|
await self._ensure_ok(typed, what="responses")
|
||||||
|
async for raw in typed.iter_lines():
|
||||||
|
if not raw:
|
||||||
|
continue
|
||||||
|
parsed = sse_data_payload(bytes(raw))
|
||||||
|
if parsed is None:
|
||||||
|
continue
|
||||||
|
if parsed is False:
|
||||||
|
break
|
||||||
|
yield self.response_event_decoder.decode(parsed)
|
||||||
|
finally:
|
||||||
|
await close_async_response(typed)
|
||||||
+2
-3
@@ -13,10 +13,9 @@ from typing import Any, Literal, cast
|
|||||||
import msgspec
|
import msgspec
|
||||||
from msgspec import UNSET, Struct, field
|
from msgspec import UNSET, Struct, field
|
||||||
|
|
||||||
|
from plyngent.lmproto.openai_compatible.model import ServiceTier # noqa: TC001
|
||||||
from plyngent.typedef import JSONSchema, Unset # noqa: TC001
|
from plyngent.typedef import JSONSchema, Unset # noqa: TC001
|
||||||
|
|
||||||
from .model import ServiceTier # noqa: TC001
|
|
||||||
|
|
||||||
type ResponseStatus = Literal[
|
type ResponseStatus = Literal[
|
||||||
"completed",
|
"completed",
|
||||||
"failed",
|
"failed",
|
||||||
@@ -294,7 +293,7 @@ def response_output_text(response: Response) -> str:
|
|||||||
for raw in response.output:
|
for raw in response.output:
|
||||||
if raw.get("type") != "message":
|
if raw.get("type") != "message":
|
||||||
continue
|
continue
|
||||||
content = raw.get("content")
|
content: list[object] | None = raw.get("content")
|
||||||
if not isinstance(content, list):
|
if not isinstance(content, list):
|
||||||
continue
|
continue
|
||||||
for block_obj in content:
|
for block_obj in content:
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from .client import BaseOpenAIClient as BaseOpenAIClient
|
from .client import BaseOpenAIClient as BaseOpenAIClient
|
||||||
from .client import OpenAIClient as OpenAIClient
|
from .client import OpenAIClient as OpenAIClient
|
||||||
|
from .client import OpenAICompatibleClient as OpenAICompatibleClient
|
||||||
from .config import OpenAIConfig as OpenAIConfig
|
from .config import OpenAIConfig as OpenAIConfig
|
||||||
from .model import AnyAssistantToolCall as AnyAssistantToolCall
|
from .model import AnyAssistantToolCall as AnyAssistantToolCall
|
||||||
from .model import AnyChatMessage as AnyChatMessage
|
from .model import AnyChatMessage as AnyChatMessage
|
||||||
@@ -40,14 +41,3 @@ from .model import ToolFunctionItem as ToolFunctionItem
|
|||||||
from .model import UserChatMessage as UserChatMessage
|
from .model import UserChatMessage as UserChatMessage
|
||||||
from .model import Verbosity as Verbosity
|
from .model import Verbosity as Verbosity
|
||||||
from .model import VoiceName as VoiceName
|
from .model import VoiceName as VoiceName
|
||||||
from .responses_model import Response as Response
|
|
||||||
from .responses_model import ResponseDeleted as ResponseDeleted
|
|
||||||
from .responses_model import ResponseFunctionTool as ResponseFunctionTool
|
|
||||||
from .responses_model import ResponseFunctionToolCall as ResponseFunctionToolCall
|
|
||||||
from .responses_model import ResponseFunctionToolCallOutput as ResponseFunctionToolCallOutput
|
|
||||||
from .responses_model import ResponseOutputMessage as ResponseOutputMessage
|
|
||||||
from .responses_model import ResponseOutputText as ResponseOutputText
|
|
||||||
from .responses_model import ResponsesCreateParam as ResponsesCreateParam
|
|
||||||
from .responses_model import ResponseStreamEvent as ResponseStreamEvent
|
|
||||||
from .responses_model import response_function_calls as response_function_calls
|
|
||||||
from .responses_model import response_output_text as response_output_text
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ from .model import (
|
|||||||
ModelsResponse,
|
ModelsResponse,
|
||||||
StreamToolCallDelta,
|
StreamToolCallDelta,
|
||||||
)
|
)
|
||||||
from .responses_model import Response, ResponseDeleted, ResponsesCreateParam, ResponseStreamEvent
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncIterator
|
||||||
@@ -31,7 +30,7 @@ _HTTP_ERROR = 400
|
|||||||
_ERROR_BODY_PREVIEW = 500
|
_ERROR_BODY_PREVIEW = 500
|
||||||
|
|
||||||
|
|
||||||
async def _close_async_response(resp: AsyncResponse) -> None:
|
async def close_async_response(resp: AsyncResponse) -> None:
|
||||||
"""Close a streaming response; support sync or async close()."""
|
"""Close a streaming response; support sync or async close()."""
|
||||||
for name in ("aclose", "close"):
|
for name in ("aclose", "close"):
|
||||||
method = getattr(resp, name, None)
|
method = getattr(resp, name, None)
|
||||||
@@ -43,6 +42,10 @@ async def _close_async_response(resp: AsyncResponse) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
# Internal alias kept for older call sites in this module.
|
||||||
|
_close_async_response = close_async_response
|
||||||
|
|
||||||
|
|
||||||
async def read_response_body(resp: object) -> bytes | str | None:
|
async def read_response_body(resp: object) -> bytes | str | None:
|
||||||
"""Read response body; niquests ``AsyncResponse.content`` is an async property."""
|
"""Read response body; niquests ``AsyncResponse.content`` is an async property."""
|
||||||
content = getattr(resp, "content", None)
|
content = getattr(resp, "content", None)
|
||||||
@@ -98,14 +101,13 @@ def http_error_message(
|
|||||||
|
|
||||||
|
|
||||||
class BaseOpenAIClient:
|
class BaseOpenAIClient:
|
||||||
|
"""Shared OpenAI-compatible HTTP surface (session, models, chat SSE)."""
|
||||||
|
|
||||||
session: AsyncSession
|
session: AsyncSession
|
||||||
encoder: msgspec.json.Encoder
|
encoder: msgspec.json.Encoder
|
||||||
decoder: msgspec.json.Decoder[ChatCompletionResponse]
|
decoder: msgspec.json.Decoder[ChatCompletionResponse]
|
||||||
chunk_decoder: msgspec.json.Decoder[ChatCompletionChunk]
|
chunk_decoder: msgspec.json.Decoder[ChatCompletionChunk]
|
||||||
models_decoder: msgspec.json.Decoder[ModelsResponse]
|
models_decoder: msgspec.json.Decoder[ModelsResponse]
|
||||||
response_decoder: msgspec.json.Decoder[Response]
|
|
||||||
response_deleted_decoder: msgspec.json.Decoder[ResponseDeleted]
|
|
||||||
response_event_decoder: msgspec.json.Decoder[ResponseStreamEvent]
|
|
||||||
|
|
||||||
def __init__(self, config: OpenAIConfig) -> None:
|
def __init__(self, config: OpenAIConfig) -> None:
|
||||||
self.session = niquests.AsyncSession(
|
self.session = niquests.AsyncSession(
|
||||||
@@ -116,9 +118,6 @@ class BaseOpenAIClient:
|
|||||||
self.decoder = msgspec.json.Decoder(ChatCompletionResponse)
|
self.decoder = msgspec.json.Decoder(ChatCompletionResponse)
|
||||||
self.chunk_decoder = msgspec.json.Decoder(ChatCompletionChunk)
|
self.chunk_decoder = msgspec.json.Decoder(ChatCompletionChunk)
|
||||||
self.models_decoder = msgspec.json.Decoder(ModelsResponse)
|
self.models_decoder = msgspec.json.Decoder(ModelsResponse)
|
||||||
self.response_decoder = msgspec.json.Decoder(Response)
|
|
||||||
self.response_deleted_decoder = msgspec.json.Decoder(ResponseDeleted)
|
|
||||||
self.response_event_decoder = msgspec.json.Decoder(ResponseStreamEvent)
|
|
||||||
|
|
||||||
async def models(self) -> list[str]:
|
async def models(self) -> list[str]:
|
||||||
"""List model ids via OpenAI-compatible ``GET /models``.
|
"""List model ids via OpenAI-compatible ``GET /models``.
|
||||||
@@ -164,22 +163,6 @@ class BaseOpenAIClient:
|
|||||||
finally:
|
finally:
|
||||||
await _close_async_response(resp)
|
await _close_async_response(resp)
|
||||||
|
|
||||||
async def _parse_response_sse(self, resp: AsyncResponse) -> AsyncIterator[ResponseStreamEvent]:
|
|
||||||
"""Yield Responses API SSE events; stop at ``data: [DONE]``."""
|
|
||||||
try:
|
|
||||||
await self._ensure_ok(resp, what="responses")
|
|
||||||
async for raw in resp.iter_lines():
|
|
||||||
if not raw:
|
|
||||||
continue
|
|
||||||
parsed = sse_data_payload(bytes(raw))
|
|
||||||
if parsed is None:
|
|
||||||
continue
|
|
||||||
if parsed is False:
|
|
||||||
break
|
|
||||||
yield self.response_event_decoder.decode(parsed)
|
|
||||||
finally:
|
|
||||||
await _close_async_response(resp)
|
|
||||||
|
|
||||||
async def _read_json_body(self, resp: object, *, what: str) -> bytes:
|
async def _read_json_body(self, resp: object, *, what: str) -> bytes:
|
||||||
await self._ensure_ok(resp, what=what)
|
await self._ensure_ok(resp, what=what)
|
||||||
body = await read_response_body(resp)
|
body = await read_response_body(resp)
|
||||||
@@ -192,7 +175,9 @@ class BaseOpenAIClient:
|
|||||||
return bytes(body)
|
return bytes(body)
|
||||||
|
|
||||||
|
|
||||||
class OpenAIClient(BaseOpenAIClient):
|
class OpenAICompatibleClient(BaseOpenAIClient):
|
||||||
|
"""Chat Completions only (``POST /chat/completions`` + ``GET /models``)."""
|
||||||
|
|
||||||
def __init__(self, config: OpenAIConfig) -> None:
|
def __init__(self, config: OpenAIConfig) -> None:
|
||||||
super().__init__(config)
|
super().__init__(config)
|
||||||
|
|
||||||
@@ -229,50 +214,9 @@ class OpenAIClient(BaseOpenAIClient):
|
|||||||
body = await self._read_json_body(resp, what="chat completions")
|
body = await self._read_json_body(resp, what="chat completions")
|
||||||
return self.decoder.decode(body)
|
return self.decoder.decode(body)
|
||||||
|
|
||||||
@overload
|
|
||||||
async def responses(
|
|
||||||
self, param: ResponsesCreateParam, *, stream: Literal[False] = False
|
|
||||||
) -> Response: ...
|
|
||||||
|
|
||||||
@overload
|
# Backward-compatible alias: many call sites still import OpenAIClient from this package.
|
||||||
async def responses(
|
OpenAIClient = OpenAICompatibleClient
|
||||||
self, param: ResponsesCreateParam, *, stream: Literal[True]
|
|
||||||
) -> AsyncIterator[ResponseStreamEvent]: ...
|
|
||||||
|
|
||||||
async def responses(
|
|
||||||
self, param: ResponsesCreateParam, *, stream: bool = False
|
|
||||||
) -> Response | AsyncIterator[ResponseStreamEvent]:
|
|
||||||
"""Create a model response via OpenAI ``POST /responses``."""
|
|
||||||
param = msgspec.structs.replace(param, stream=stream)
|
|
||||||
data = self.encoder.encode(param)
|
|
||||||
if stream:
|
|
||||||
resp = await self.session.post(
|
|
||||||
"/responses",
|
|
||||||
data=data,
|
|
||||||
headers={"Content-Type": "application/json"},
|
|
||||||
stream=True,
|
|
||||||
)
|
|
||||||
return self._parse_response_sse(resp)
|
|
||||||
resp = await self.session.post(
|
|
||||||
"/responses",
|
|
||||||
data=data,
|
|
||||||
headers={"Content-Type": "application/json"},
|
|
||||||
stream=False,
|
|
||||||
)
|
|
||||||
body = await self._read_json_body(resp, what="responses")
|
|
||||||
return self.response_decoder.decode(body)
|
|
||||||
|
|
||||||
async def get_response(self, response_id: str) -> Response:
|
|
||||||
"""Retrieve a stored response via ``GET /responses/{id}``."""
|
|
||||||
resp = await self.session.get(f"/responses/{response_id}", stream=False)
|
|
||||||
body = await self._read_json_body(resp, what="responses")
|
|
||||||
return self.response_decoder.decode(body)
|
|
||||||
|
|
||||||
async def delete_response(self, response_id: str) -> ResponseDeleted:
|
|
||||||
"""Delete a stored response via ``DELETE /responses/{id}``."""
|
|
||||||
resp = await self.session.delete(f"/responses/{response_id}", stream=False)
|
|
||||||
body = await self._read_json_body(resp, what="responses")
|
|
||||||
return self.response_deleted_decoder.decode(body)
|
|
||||||
|
|
||||||
|
|
||||||
def merge_stream_tool_calls(deltas: list[StreamToolCallDelta]) -> list[AssistantFunctionToolCall]:
|
def merge_stream_tool_calls(deltas: list[StreamToolCallDelta]) -> list[AssistantFunctionToolCall]:
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ from plyngent.config.models import (
|
|||||||
Provider,
|
Provider,
|
||||||
)
|
)
|
||||||
from plyngent.lmproto.deepseek import DeepseekOpenAIClient
|
from plyngent.lmproto.deepseek import DeepseekOpenAIClient
|
||||||
from plyngent.lmproto.openai_compatible import OpenAIClient, OpenAIConfig
|
from plyngent.lmproto.openai import OpenAIClient
|
||||||
|
from plyngent.lmproto.openai_compatible import OpenAICompatibleClient, OpenAIConfig
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
@@ -17,7 +18,9 @@ if TYPE_CHECKING:
|
|||||||
DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1"
|
DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1"
|
||||||
DEFAULT_DEEPSEEK_BASE_URL = "https://api.deepseek.com"
|
DEFAULT_DEEPSEEK_BASE_URL = "https://api.deepseek.com"
|
||||||
|
|
||||||
type OpenAICompatibleClient = OpenAIClient | DeepseekOpenAIClient
|
type ProtocolClient = OpenAIClient | OpenAICompatibleClient | DeepseekOpenAIClient
|
||||||
|
# Backward-compatible name used by older imports/tests.
|
||||||
|
type OpenAICompatibleClientUnion = ProtocolClient
|
||||||
|
|
||||||
|
|
||||||
class ProviderNotSupportedError(NotImplementedError):
|
class ProviderNotSupportedError(NotImplementedError):
|
||||||
@@ -45,7 +48,7 @@ def _deepseek_convention(extras: Mapping[str, str]) -> str:
|
|||||||
return extras.get("convention", "openai").lower()
|
return extras.get("convention", "openai").lower()
|
||||||
|
|
||||||
|
|
||||||
def create_client(provider: Provider) -> OpenAICompatibleClient:
|
def create_client(provider: Provider) -> ProtocolClient:
|
||||||
"""Build a protocol client for the given provider config entry.
|
"""Build a protocol client for the given provider config entry.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
@@ -55,7 +58,7 @@ def create_client(provider: Provider) -> OpenAICompatibleClient:
|
|||||||
if isinstance(provider, OpenAIProvider):
|
if isinstance(provider, OpenAIProvider):
|
||||||
return OpenAIClient(provider_to_openai_config(provider))
|
return OpenAIClient(provider_to_openai_config(provider))
|
||||||
if isinstance(provider, OpenAICompatibleProvider):
|
if isinstance(provider, OpenAICompatibleProvider):
|
||||||
return OpenAIClient(provider_to_openai_config(provider))
|
return OpenAICompatibleClient(provider_to_openai_config(provider))
|
||||||
if isinstance(provider, DeepseekProvider):
|
if isinstance(provider, DeepseekProvider):
|
||||||
convention = _deepseek_convention(provider.extras)
|
convention = _deepseek_convention(provider.extras)
|
||||||
if convention in {"openai", "openai_compat", "openai-compatible"}:
|
if convention in {"openai", "openai_compat", "openai-compatible"}:
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ from __future__ import annotations
|
|||||||
import msgspec
|
import msgspec
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from plyngent.lmproto.openai_compatible.client import OpenAIClient
|
from plyngent.lmproto.openai import (
|
||||||
from plyngent.lmproto.openai_compatible.config import OpenAIConfig
|
OpenAIClient,
|
||||||
from plyngent.lmproto.openai_compatible.responses_model import (
|
|
||||||
Response,
|
Response,
|
||||||
ResponseDeleted,
|
ResponseDeleted,
|
||||||
ResponseFunctionTool,
|
ResponseFunctionTool,
|
||||||
@@ -15,6 +14,7 @@ from plyngent.lmproto.openai_compatible.responses_model import (
|
|||||||
response_function_calls,
|
response_function_calls,
|
||||||
response_output_text,
|
response_output_text,
|
||||||
)
|
)
|
||||||
|
from plyngent.lmproto.openai_compatible.config import OpenAIConfig
|
||||||
|
|
||||||
|
|
||||||
def _sample_response_body() -> bytes:
|
def _sample_response_body() -> bytes:
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ from plyngent.config.models import (
|
|||||||
OpenAIProvider,
|
OpenAIProvider,
|
||||||
)
|
)
|
||||||
from plyngent.lmproto.deepseek import DeepseekOpenAIClient
|
from plyngent.lmproto.deepseek import DeepseekOpenAIClient
|
||||||
from plyngent.lmproto.openai_compatible import OpenAIClient
|
from plyngent.lmproto.openai import OpenAIClient
|
||||||
|
from plyngent.lmproto.openai_compatible import OpenAICompatibleClient
|
||||||
from plyngent.runtime import ProviderNotSupportedError, create_client, provider_to_openai_config
|
from plyngent.runtime import ProviderNotSupportedError, create_client, provider_to_openai_config
|
||||||
|
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ def test_openai_provider_defaults_base_url() -> None:
|
|||||||
assert config.base_url == "https://api.openai.com/v1"
|
assert config.base_url == "https://api.openai.com/v1"
|
||||||
client = create_client(provider)
|
client = create_client(provider)
|
||||||
assert isinstance(client, OpenAIClient)
|
assert isinstance(client, OpenAIClient)
|
||||||
|
assert hasattr(client, "responses")
|
||||||
|
|
||||||
|
|
||||||
def test_openai_compatible_requires_url() -> None:
|
def test_openai_compatible_requires_url() -> None:
|
||||||
@@ -34,7 +36,8 @@ def test_openai_compatible_client() -> None:
|
|||||||
url="https://example.com/v1",
|
url="https://example.com/v1",
|
||||||
)
|
)
|
||||||
client = create_client(provider)
|
client = create_client(provider)
|
||||||
assert isinstance(client, OpenAIClient)
|
assert isinstance(client, OpenAICompatibleClient)
|
||||||
|
assert not isinstance(client, OpenAIClient)
|
||||||
assert provider_to_openai_config(provider).base_url == "https://example.com/v1"
|
assert provider_to_openai_config(provider).base_url == "https://example.com/v1"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user