mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-22 13:34:17 +02:00
✨ Fallback to a Completions API when Sampling is not available (#1145)
This commit is contained in:
parent
76eb6dda25
commit
d32a2b953e
18 changed files with 652 additions and 50 deletions
1
.github/workflows/run-static.yml
vendored
1
.github/workflows/run-static.yml
vendored
|
|
@ -39,6 +39,7 @@ jobs:
|
|||
|
||||
- name: Install dependencies
|
||||
run: uv sync
|
||||
|
||||
- name: Check lockfile is up to date
|
||||
run: |
|
||||
if ! uv lock --check; then
|
||||
|
|
|
|||
|
|
@ -150,3 +150,52 @@ client = Client(
|
|||
sampling_handler=basic_sampling_handler
|
||||
)
|
||||
```
|
||||
|
||||
## Sampling fallback
|
||||
|
||||
Client support for sampling is optional, if the client does not support sampling, the server will report an error indicating
|
||||
that the client does not support sampling.
|
||||
|
||||
A `sampling_handler` can also be provided to the FastMCP server, which will be used to handle sampling requests if the client
|
||||
does not support sampling. This sampling handler bypasses the client and sends sampling requests directly to the LLM provider.
|
||||
|
||||
Sampling handlers can be implemented using any LLM provider but a sample implementation for OpenAI is provided as a Contrib
|
||||
module. Sampling lacks the full capabilities of typical LLM completions. For this reason, the OpenAI sampling handler, pointed at
|
||||
a third-party provider's OpenAI-compatible API, is often sufficient to implement a sampling handler.
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from mcp.types import ContentBlock
|
||||
from openai import OpenAI
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.experimental.sampling.handlers.openai import OpenAISamplingHandler
|
||||
from fastmcp.server.context import Context
|
||||
|
||||
|
||||
async def async_main():
|
||||
server = FastMCP(
|
||||
name="OpenAI Sampling Fallback Example",
|
||||
sampling_handler=OpenAISamplingHandler(
|
||||
default_model="gpt-4o-mini",
|
||||
client=OpenAI(
|
||||
api_key=os.getenv("API_KEY"),
|
||||
base_url=os.getenv("BASE_URL"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@server.tool
|
||||
async def test_sample_fallback(ctx: Context) -> ContentBlock:
|
||||
return await ctx.sample(
|
||||
messages=["hello world!"],
|
||||
)
|
||||
|
||||
await server.run_http_async()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(async_main())
|
||||
```
|
||||
34
examples/sampling_fallback.py
Normal file
34
examples/sampling_fallback.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import asyncio
|
||||
import os
|
||||
|
||||
from mcp.types import ContentBlock
|
||||
from openai import OpenAI
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.experimental.sampling.handlers.openai import OpenAISamplingHandler
|
||||
from fastmcp.server.context import Context
|
||||
|
||||
|
||||
async def async_main():
|
||||
server = FastMCP(
|
||||
name="OpenAI Sampling Fallback Example",
|
||||
sampling_handler=OpenAISamplingHandler(
|
||||
default_model=os.getenv("MODEL") or "gpt-4o-mini", # pyright: ignore[reportArgumentType]
|
||||
client=OpenAI(
|
||||
api_key=os.getenv("API_KEY"),
|
||||
base_url=os.getenv("BASE_URL"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@server.tool
|
||||
async def test_sample_fallback(ctx: Context) -> ContentBlock:
|
||||
return await ctx.sample(
|
||||
messages=["hello world!"],
|
||||
)
|
||||
|
||||
await server.run_http_async()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(async_main())
|
||||
|
|
@ -15,6 +15,7 @@ dependencies = [
|
|||
"pydantic[email]>=2.11.7",
|
||||
"pyperclip>=1.9.0",
|
||||
"openapi-core>=0.19.5",
|
||||
"openai>=1.95.1",
|
||||
]
|
||||
requires-python = ">=3.10"
|
||||
readme = "README.md"
|
||||
|
|
|
|||
|
|
@ -30,7 +30,11 @@ from fastmcp.client.roots import (
|
|||
RootsList,
|
||||
create_roots_callback,
|
||||
)
|
||||
from fastmcp.client.sampling import SamplingHandler, create_sampling_callback
|
||||
from fastmcp.client.sampling import (
|
||||
ClientSamplingHandler,
|
||||
SamplingHandler,
|
||||
create_sampling_callback,
|
||||
)
|
||||
from fastmcp.exceptions import ToolError
|
||||
from fastmcp.mcp_config import MCPConfig
|
||||
from fastmcp.server import FastMCP
|
||||
|
|
@ -60,6 +64,7 @@ __all__ = [
|
|||
"RootsList",
|
||||
"LogHandler",
|
||||
"MessageHandler",
|
||||
"ClientSamplingHandler",
|
||||
"SamplingHandler",
|
||||
"ElicitationHandler",
|
||||
"ProgressHandler",
|
||||
|
|
@ -208,7 +213,7 @@ class Client(Generic[ClientTransportT]):
|
|||
| str
|
||||
),
|
||||
roots: RootsList | RootsHandler | None = None,
|
||||
sampling_handler: SamplingHandler | None = None,
|
||||
sampling_handler: ClientSamplingHandler | None = None,
|
||||
elicitation_handler: ElicitationHandler | None = None,
|
||||
log_handler: LogHandler | None = None,
|
||||
message_handler: MessageHandlerT | MessageHandler | None = None,
|
||||
|
|
@ -292,7 +297,7 @@ class Client(Generic[ClientTransportT]):
|
|||
"""Set the roots for the client. This does not automatically call `send_roots_list_changed`."""
|
||||
self._session_kwargs["list_roots_callback"] = create_roots_callback(roots)
|
||||
|
||||
def set_sampling_callback(self, sampling_callback: SamplingHandler) -> None:
|
||||
def set_sampling_callback(self, sampling_callback: ClientSamplingHandler) -> None:
|
||||
"""Set the sampling callback for the client."""
|
||||
self._session_kwargs["sampling_callback"] = create_sampling_callback(
|
||||
sampling_callback
|
||||
|
|
|
|||
|
|
@ -3,16 +3,18 @@ from collections.abc import Awaitable, Callable
|
|||
from typing import TypeAlias
|
||||
|
||||
import mcp.types
|
||||
from mcp import ClientSession, CreateMessageResult
|
||||
from mcp.client.session import SamplingFnT
|
||||
from mcp import CreateMessageResult
|
||||
from mcp.client.session import ClientSession, SamplingFnT
|
||||
from mcp.shared.context import LifespanContextT, RequestContext
|
||||
from mcp.types import CreateMessageRequestParams as SamplingParams
|
||||
from mcp.types import SamplingMessage
|
||||
|
||||
from fastmcp.server.sampling.handler import ServerSamplingHandler
|
||||
|
||||
__all__ = ["SamplingMessage", "SamplingParams", "SamplingHandler"]
|
||||
|
||||
|
||||
SamplingHandler: TypeAlias = Callable[
|
||||
ClientSamplingHandler: TypeAlias = Callable[
|
||||
[
|
||||
list[SamplingMessage],
|
||||
SamplingParams,
|
||||
|
|
@ -21,8 +23,14 @@ SamplingHandler: TypeAlias = Callable[
|
|||
str | CreateMessageResult | Awaitable[str | CreateMessageResult],
|
||||
]
|
||||
|
||||
SamplingHandler: TypeAlias = (
|
||||
ClientSamplingHandler[LifespanContextT] | ServerSamplingHandler[LifespanContextT]
|
||||
)
|
||||
|
||||
def create_sampling_callback(sampling_handler: SamplingHandler) -> SamplingFnT:
|
||||
|
||||
def create_sampling_callback(
|
||||
sampling_handler: ClientSamplingHandler[LifespanContextT],
|
||||
) -> SamplingFnT:
|
||||
async def _sampling_handler(
|
||||
context: RequestContext[ClientSession, LifespanContextT],
|
||||
params: SamplingParams,
|
||||
|
|
|
|||
0
src/fastmcp/experimental/sampling/__init__.py
Normal file
0
src/fastmcp/experimental/sampling/__init__.py
Normal file
3
src/fastmcp/experimental/sampling/handlers/__init__.py
Normal file
3
src/fastmcp/experimental/sampling/handlers/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from .openai import OpenAISamplingHandler
|
||||
|
||||
__all__ = ["OpenAISamplingHandler"]
|
||||
21
src/fastmcp/experimental/sampling/handlers/base.py
Normal file
21
src/fastmcp/experimental/sampling/handlers/base.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Awaitable
|
||||
|
||||
from mcp import ClientSession, CreateMessageResult
|
||||
from mcp.server.session import ServerSession
|
||||
from mcp.shared.context import LifespanContextT, RequestContext
|
||||
from mcp.types import CreateMessageRequestParams as SamplingParams
|
||||
from mcp.types import (
|
||||
SamplingMessage,
|
||||
)
|
||||
|
||||
|
||||
class BaseLLMSamplingHandler(ABC):
|
||||
@abstractmethod
|
||||
def __call__(
|
||||
self,
|
||||
messages: list[SamplingMessage],
|
||||
params: SamplingParams,
|
||||
context: RequestContext[ServerSession, LifespanContextT]
|
||||
| RequestContext[ClientSession, LifespanContextT],
|
||||
) -> str | CreateMessageResult | Awaitable[str | CreateMessageResult]: ...
|
||||
163
src/fastmcp/experimental/sampling/handlers/openai.py
Normal file
163
src/fastmcp/experimental/sampling/handlers/openai.py
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
from collections.abc import Iterator, Sequence
|
||||
from typing import get_args
|
||||
|
||||
from mcp import ClientSession, ServerSession
|
||||
from mcp.shared.context import LifespanContextT, RequestContext
|
||||
from mcp.types import CreateMessageRequestParams as SamplingParams
|
||||
from mcp.types import (
|
||||
CreateMessageResult,
|
||||
ModelPreferences,
|
||||
SamplingMessage,
|
||||
TextContent,
|
||||
)
|
||||
from openai import NOT_GIVEN, OpenAI
|
||||
from openai.types.chat import (
|
||||
ChatCompletion,
|
||||
ChatCompletionAssistantMessageParam,
|
||||
ChatCompletionMessageParam,
|
||||
ChatCompletionSystemMessageParam,
|
||||
ChatCompletionUserMessageParam,
|
||||
)
|
||||
from openai.types.shared.chat_model import ChatModel
|
||||
from typing_extensions import override
|
||||
|
||||
from fastmcp.experimental.sampling.handlers.base import BaseLLMSamplingHandler
|
||||
|
||||
|
||||
class OpenAISamplingHandler(BaseLLMSamplingHandler):
|
||||
def __init__(self, default_model: ChatModel, client: OpenAI | None = None):
|
||||
self.client: OpenAI = client or OpenAI()
|
||||
self.default_model: ChatModel = default_model
|
||||
|
||||
@override
|
||||
async def __call__(
|
||||
self,
|
||||
messages: list[SamplingMessage],
|
||||
params: SamplingParams,
|
||||
context: RequestContext[ServerSession, LifespanContextT]
|
||||
| RequestContext[ClientSession, LifespanContextT],
|
||||
) -> CreateMessageResult:
|
||||
openai_messages: list[ChatCompletionMessageParam] = (
|
||||
self._convert_to_openai_messages(
|
||||
system_prompt=params.systemPrompt,
|
||||
messages=messages,
|
||||
)
|
||||
)
|
||||
|
||||
model: ChatModel = self._select_model_from_preferences(params.modelPreferences)
|
||||
|
||||
response = self.client.chat.completions.create(
|
||||
model=model,
|
||||
messages=openai_messages,
|
||||
temperature=params.temperature or NOT_GIVEN,
|
||||
max_tokens=params.maxTokens,
|
||||
stop=params.stopSequences or NOT_GIVEN,
|
||||
)
|
||||
|
||||
return self._chat_completion_to_create_message_result(response)
|
||||
|
||||
@staticmethod
|
||||
def _iter_models_from_preferences(
|
||||
model_preferences: ModelPreferences | str | list[str] | None,
|
||||
) -> Iterator[str]:
|
||||
if model_preferences is None:
|
||||
return
|
||||
|
||||
if isinstance(model_preferences, str) and model_preferences in get_args(
|
||||
ChatModel
|
||||
):
|
||||
yield model_preferences
|
||||
|
||||
if isinstance(model_preferences, list):
|
||||
yield from model_preferences
|
||||
|
||||
if isinstance(model_preferences, ModelPreferences):
|
||||
if not (hints := model_preferences.hints):
|
||||
return
|
||||
|
||||
for hint in hints:
|
||||
if not (name := hint.name):
|
||||
continue
|
||||
|
||||
yield name
|
||||
|
||||
@staticmethod
|
||||
def _convert_to_openai_messages(
|
||||
system_prompt: str | None, messages: Sequence[SamplingMessage]
|
||||
) -> list[ChatCompletionMessageParam]:
|
||||
openai_messages: list[ChatCompletionMessageParam] = []
|
||||
|
||||
if system_prompt:
|
||||
openai_messages.append(
|
||||
ChatCompletionSystemMessageParam(
|
||||
role="system",
|
||||
content=system_prompt,
|
||||
)
|
||||
)
|
||||
|
||||
if isinstance(messages, str):
|
||||
openai_messages.append(
|
||||
ChatCompletionUserMessageParam(
|
||||
role="user",
|
||||
content=messages,
|
||||
)
|
||||
)
|
||||
|
||||
if isinstance(messages, list):
|
||||
for message in messages:
|
||||
if isinstance(message, str):
|
||||
openai_messages.append(
|
||||
ChatCompletionUserMessageParam(
|
||||
role="user",
|
||||
content=message,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
if not isinstance(message.content, TextContent):
|
||||
raise ValueError("Only text content is supported")
|
||||
|
||||
if message.role == "user":
|
||||
openai_messages.append(
|
||||
ChatCompletionUserMessageParam(
|
||||
role="user",
|
||||
content=message.content.text,
|
||||
)
|
||||
)
|
||||
else:
|
||||
openai_messages.append(
|
||||
ChatCompletionAssistantMessageParam(
|
||||
role="assistant",
|
||||
content=message.content.text,
|
||||
)
|
||||
)
|
||||
|
||||
return openai_messages
|
||||
|
||||
@staticmethod
|
||||
def _chat_completion_to_create_message_result(
|
||||
chat_completion: ChatCompletion,
|
||||
) -> CreateMessageResult:
|
||||
if len(chat_completion.choices) == 0:
|
||||
raise ValueError("No response for completion")
|
||||
|
||||
first_choice = chat_completion.choices[0]
|
||||
|
||||
if content := first_choice.message.content:
|
||||
return CreateMessageResult(
|
||||
content=TextContent(type="text", text=content),
|
||||
role="assistant",
|
||||
model=chat_completion.model,
|
||||
)
|
||||
|
||||
raise ValueError("No content in response from completion")
|
||||
|
||||
def _select_model_from_preferences(
|
||||
self, model_preferences: ModelPreferences | str | list[str] | None
|
||||
) -> ChatModel:
|
||||
for model_option in self._iter_models_from_preferences(model_preferences):
|
||||
if model_option in get_args(ChatModel):
|
||||
chosen_model: ChatModel = model_option # pyright: ignore[reportAssignmentType]
|
||||
return chosen_model
|
||||
|
||||
return self.default_model
|
||||
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
|||
|
||||
import asyncio
|
||||
import copy
|
||||
import inspect
|
||||
import warnings
|
||||
import weakref
|
||||
from collections.abc import Generator, Mapping
|
||||
|
|
@ -16,15 +17,18 @@ from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|||
from mcp.server.lowlevel.server import request_ctx
|
||||
from mcp.shared.context import RequestContext
|
||||
from mcp.types import (
|
||||
ClientCapabilities,
|
||||
ContentBlock,
|
||||
CreateMessageResult,
|
||||
IncludeContext,
|
||||
ModelHint,
|
||||
ModelPreferences,
|
||||
Root,
|
||||
SamplingCapability,
|
||||
SamplingMessage,
|
||||
TextContent,
|
||||
)
|
||||
from mcp.types import CreateMessageRequestParams as SamplingParams
|
||||
from pydantic.networks import AnyUrl
|
||||
from starlette.requests import Request
|
||||
|
||||
|
|
@ -386,13 +390,50 @@ class Context:
|
|||
for m in messages
|
||||
]
|
||||
|
||||
should_fallback = (
|
||||
self.fastmcp.sampling_handler_behavior == "fallback"
|
||||
and not self.session.check_client_capability(
|
||||
capability=ClientCapabilities(sampling=SamplingCapability())
|
||||
)
|
||||
)
|
||||
|
||||
if self.fastmcp.sampling_handler_behavior == "always" or should_fallback:
|
||||
if self.fastmcp.sampling_handler is None:
|
||||
raise ValueError("Client does not support sampling")
|
||||
|
||||
create_message_result = self.fastmcp.sampling_handler(
|
||||
sampling_messages,
|
||||
SamplingParams(
|
||||
systemPrompt=system_prompt,
|
||||
messages=sampling_messages,
|
||||
temperature=temperature,
|
||||
maxTokens=max_tokens,
|
||||
modelPreferences=_parse_model_preferences(model_preferences),
|
||||
),
|
||||
self.request_context,
|
||||
)
|
||||
|
||||
if inspect.isawaitable(create_message_result):
|
||||
create_message_result = await create_message_result
|
||||
|
||||
if isinstance(create_message_result, str):
|
||||
return TextContent(text=create_message_result, type="text")
|
||||
|
||||
if isinstance(create_message_result, CreateMessageResult):
|
||||
return create_message_result.content
|
||||
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unexpected sampling handler result: {create_message_result}"
|
||||
)
|
||||
|
||||
result: CreateMessageResult = await self.session.create_message(
|
||||
messages=sampling_messages,
|
||||
system_prompt=system_prompt,
|
||||
include_context=include_context,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
model_preferences=self._parse_model_preferences(model_preferences),
|
||||
model_preferences=_parse_model_preferences(model_preferences),
|
||||
related_request_id=self.request_id,
|
||||
)
|
||||
|
||||
|
|
@ -592,44 +633,43 @@ class Context:
|
|||
# Don't let notification failures break the request
|
||||
pass
|
||||
|
||||
def _parse_model_preferences(
|
||||
self, model_preferences: ModelPreferences | str | list[str] | None
|
||||
) -> ModelPreferences | None:
|
||||
"""
|
||||
Validates and converts user input for model_preferences into a ModelPreferences object.
|
||||
|
||||
Args:
|
||||
model_preferences (ModelPreferences | str | list[str] | None):
|
||||
The model preferences to use. Accepts:
|
||||
- ModelPreferences (returns as-is)
|
||||
- str (single model hint)
|
||||
- list[str] (multiple model hints)
|
||||
- None (no preferences)
|
||||
def _parse_model_preferences(
|
||||
model_preferences: ModelPreferences | str | list[str] | None,
|
||||
) -> ModelPreferences | None:
|
||||
"""
|
||||
Validates and converts user input for model_preferences into a ModelPreferences object.
|
||||
|
||||
Returns:
|
||||
ModelPreferences | None: The parsed ModelPreferences object, or None if not provided.
|
||||
Args:
|
||||
model_preferences (ModelPreferences | str | list[str] | None):
|
||||
The model preferences to use. Accepts:
|
||||
- ModelPreferences (returns as-is)
|
||||
- str (single model hint)
|
||||
- list[str] (multiple model hints)
|
||||
- None (no preferences)
|
||||
|
||||
Raises:
|
||||
ValueError: If the input is not a supported type or contains invalid values.
|
||||
"""
|
||||
if model_preferences is None:
|
||||
return None
|
||||
elif isinstance(model_preferences, ModelPreferences):
|
||||
return model_preferences
|
||||
elif isinstance(model_preferences, str):
|
||||
# Single model hint
|
||||
return ModelPreferences(hints=[ModelHint(name=model_preferences)])
|
||||
elif isinstance(model_preferences, list):
|
||||
# List of model hints (strings)
|
||||
if not all(isinstance(h, str) for h in model_preferences):
|
||||
raise ValueError(
|
||||
"All elements of model_preferences list must be"
|
||||
" strings (model name hints)."
|
||||
)
|
||||
return ModelPreferences(
|
||||
hints=[ModelHint(name=h) for h in model_preferences]
|
||||
)
|
||||
else:
|
||||
Returns:
|
||||
ModelPreferences | None: The parsed ModelPreferences object, or None if not provided.
|
||||
|
||||
Raises:
|
||||
ValueError: If the input is not a supported type or contains invalid values.
|
||||
"""
|
||||
if model_preferences is None:
|
||||
return None
|
||||
elif isinstance(model_preferences, ModelPreferences):
|
||||
return model_preferences
|
||||
elif isinstance(model_preferences, str):
|
||||
# Single model hint
|
||||
return ModelPreferences(hints=[ModelHint(name=model_preferences)])
|
||||
elif isinstance(model_preferences, list):
|
||||
# List of model hints (strings)
|
||||
if not all(isinstance(h, str) for h in model_preferences):
|
||||
raise ValueError(
|
||||
"model_preferences must be one of: ModelPreferences, str, list[str], or None."
|
||||
"All elements of model_preferences list must be"
|
||||
" strings (model name hints)."
|
||||
)
|
||||
return ModelPreferences(hints=[ModelHint(name=h) for h in model_preferences])
|
||||
else:
|
||||
raise ValueError(
|
||||
"model_preferences must be one of: ModelPreferences, str, list[str], or None."
|
||||
)
|
||||
|
|
|
|||
19
src/fastmcp/server/sampling/handler.py
Normal file
19
src/fastmcp/server/sampling/handler.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
from collections.abc import Awaitable, Callable
|
||||
from typing import TypeAlias
|
||||
|
||||
from mcp import CreateMessageResult
|
||||
from mcp.server.session import ServerSession
|
||||
from mcp.shared.context import LifespanContextT, RequestContext
|
||||
from mcp.types import CreateMessageRequestParams as SamplingParams
|
||||
from mcp.types import (
|
||||
SamplingMessage,
|
||||
)
|
||||
|
||||
ServerSamplingHandler: TypeAlias = Callable[
|
||||
[
|
||||
list[SamplingMessage],
|
||||
SamplingParams,
|
||||
RequestContext[ServerSession, LifespanContextT],
|
||||
],
|
||||
str | CreateMessageResult | Awaitable[str | CreateMessageResult],
|
||||
]
|
||||
|
|
@ -70,6 +70,7 @@ from fastmcp.utilities.types import NotSet, NotSetT
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from fastmcp.client import Client
|
||||
from fastmcp.client.sampling import ServerSamplingHandler
|
||||
from fastmcp.client.transports import ClientTransport, ClientTransportT
|
||||
from fastmcp.experimental.server.openapi import FastMCPOpenAPI as FastMCPOpenAPINew
|
||||
from fastmcp.experimental.server.openapi.routing import (
|
||||
|
|
@ -167,6 +168,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
streamable_http_path: str | None = None,
|
||||
json_response: bool | None = None,
|
||||
stateless_http: bool | None = None,
|
||||
sampling_handler: ServerSamplingHandler[LifespanResultT] | None = None,
|
||||
sampling_handler_behavior: Literal["always", "fallback"] | None = None,
|
||||
):
|
||||
self.resource_prefix_format: Literal["protocol", "path"] = (
|
||||
resource_prefix_format or fastmcp.settings.resource_prefix_format
|
||||
|
|
@ -242,6 +245,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
dependencies or fastmcp.settings.server_dependencies
|
||||
) # TODO: Remove (deprecated in v2.11.4)
|
||||
|
||||
self.sampling_handler = sampling_handler
|
||||
self.sampling_handler_behavior = sampling_handler_behavior or "fallback"
|
||||
|
||||
self.include_fastmcp_meta = (
|
||||
include_fastmcp_meta
|
||||
if include_fastmcp_meta is not None
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from pathlib import Path
|
|||
from types import EllipsisType, UnionType
|
||||
from typing import (
|
||||
Annotated,
|
||||
Protocol,
|
||||
TypeAlias,
|
||||
TypeVar,
|
||||
Union,
|
||||
|
|
@ -19,7 +20,7 @@ from typing import (
|
|||
)
|
||||
|
||||
import mcp.types
|
||||
from mcp.types import Annotations
|
||||
from mcp.types import Annotations, ContentBlock, ModelPreferences, SamplingMessage
|
||||
from pydantic import AnyUrl, BaseModel, ConfigDict, Field, TypeAdapter, UrlConstraints
|
||||
|
||||
T = TypeVar("T")
|
||||
|
|
@ -407,3 +408,14 @@ def replace_type(type_, type_map: dict[type, type]):
|
|||
return Union[new_args] # type: ignore # noqa: UP007
|
||||
else:
|
||||
return origin[new_args]
|
||||
|
||||
|
||||
class ContextSamplingFallbackProtocol(Protocol):
|
||||
async def __call__(
|
||||
self,
|
||||
messages: str | list[str | SamplingMessage],
|
||||
system_prompt: str | None = None,
|
||||
temperature: float | None = None,
|
||||
max_tokens: int | None = None,
|
||||
model_preferences: ModelPreferences | str | list[str] | None = None,
|
||||
) -> ContentBlock: ...
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import json
|
||||
from typing import cast
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
from mcp.types import TextContent
|
||||
|
|
@ -89,8 +91,12 @@ async def test_sampling_with_messages(fastmcp_server: FastMCP):
|
|||
messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext
|
||||
) -> str:
|
||||
assert len(messages) == 2
|
||||
|
||||
assert isinstance(messages[0].content, TextContent)
|
||||
assert messages[0].content.type == "text"
|
||||
assert messages[0].content.text == "Hello!"
|
||||
|
||||
assert isinstance(messages[1].content, TextContent)
|
||||
assert messages[1].content.type == "text"
|
||||
assert messages[1].content.text == "How can I assist you today?"
|
||||
return "I need to think."
|
||||
|
|
@ -102,6 +108,26 @@ async def test_sampling_with_messages(fastmcp_server: FastMCP):
|
|||
assert result.data == "I need to think."
|
||||
|
||||
|
||||
async def test_sampling_with_fallback(fastmcp_server: FastMCP):
|
||||
openai_sampling_handler = AsyncMock(return_value="But I need to think")
|
||||
|
||||
fastmcp_server = FastMCP(
|
||||
sampling_handler=openai_sampling_handler,
|
||||
)
|
||||
|
||||
@fastmcp_server.tool
|
||||
async def sample_with_fallback(context: Context) -> str:
|
||||
sampling_result = await context.sample("Do not think.")
|
||||
return cast(TextContent, sampling_result).text
|
||||
|
||||
client = Client(fastmcp_server)
|
||||
|
||||
async with client:
|
||||
call_tool_result = await client.call_tool("sample_with_fallback")
|
||||
|
||||
assert call_tool_result.data == "But I need to think"
|
||||
|
||||
|
||||
async def test_sampling_with_image(fastmcp_server: FastMCP):
|
||||
def sampling_handler(
|
||||
messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext
|
||||
|
|
|
|||
100
tests/experimental/sampling/test_openai_handler.py
Normal file
100
tests/experimental/sampling/test_openai_handler.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from mcp.types import (
|
||||
CreateMessageResult,
|
||||
ModelHint,
|
||||
ModelPreferences,
|
||||
SamplingMessage,
|
||||
TextContent,
|
||||
)
|
||||
from openai import OpenAI
|
||||
from openai.types.chat import (
|
||||
ChatCompletion,
|
||||
ChatCompletionAssistantMessageParam,
|
||||
ChatCompletionMessage,
|
||||
ChatCompletionSystemMessageParam,
|
||||
ChatCompletionUserMessageParam,
|
||||
)
|
||||
from openai.types.chat.chat_completion import Choice
|
||||
|
||||
from fastmcp.experimental.sampling.handlers.openai import OpenAISamplingHandler
|
||||
|
||||
|
||||
def test_convert_sampling_messages_to_openai_messages():
|
||||
msgs = OpenAISamplingHandler._convert_to_openai_messages(
|
||||
system_prompt="sys",
|
||||
messages=[
|
||||
SamplingMessage(
|
||||
role="user", content=TextContent(type="text", text="hello")
|
||||
),
|
||||
SamplingMessage(
|
||||
role="assistant", content=TextContent(type="text", text="ok")
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
assert msgs == [
|
||||
ChatCompletionSystemMessageParam(content="sys", role="system"),
|
||||
ChatCompletionUserMessageParam(content="hello", role="user"),
|
||||
ChatCompletionAssistantMessageParam(content="ok", role="assistant"),
|
||||
]
|
||||
|
||||
|
||||
def test_convert_to_openai_messages_raises_on_non_text():
|
||||
from fastmcp.utilities.types import Image
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
OpenAISamplingHandler._convert_to_openai_messages(
|
||||
system_prompt=None,
|
||||
messages=[
|
||||
SamplingMessage(
|
||||
role="user",
|
||||
content=Image(data=b"abc").to_image_content(),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"prefs,expected",
|
||||
[
|
||||
("gpt-4o-mini", "gpt-4o-mini"),
|
||||
(ModelPreferences(hints=[ModelHint(name="gpt-4o-mini")]), "gpt-4o-mini"),
|
||||
(["gpt-4o-mini", "other"], "gpt-4o-mini"),
|
||||
(None, "fallback-model"),
|
||||
(["unknown-model"], "fallback-model"),
|
||||
],
|
||||
)
|
||||
def test_select_model_from_preferences(prefs, expected):
|
||||
mock_client = MagicMock(spec=OpenAI)
|
||||
handler = OpenAISamplingHandler(default_model="fallback-model", client=mock_client) # type: ignore[arg-type]
|
||||
assert handler._select_model_from_preferences(prefs) == expected
|
||||
|
||||
|
||||
async def test_chat_completion_to_create_message_result():
|
||||
mock_client = MagicMock(spec=OpenAI)
|
||||
handler = OpenAISamplingHandler(default_model="fallback-model", client=mock_client) # type: ignore[arg-type]
|
||||
mock_client.chat.completions.create.return_value = ChatCompletion(
|
||||
id="123",
|
||||
created=123,
|
||||
model="gpt-4o-mini",
|
||||
object="chat.completion",
|
||||
choices=[
|
||||
Choice(
|
||||
message=ChatCompletionMessage(
|
||||
content="HELPFUL CONTENT FROM A VERY SMART LLM", role="assistant"
|
||||
),
|
||||
finish_reason="stop",
|
||||
index=0,
|
||||
)
|
||||
],
|
||||
)
|
||||
result: CreateMessageResult = handler._chat_completion_to_create_message_result(
|
||||
chat_completion=mock_client.chat.completions.create.return_value
|
||||
)
|
||||
assert result == CreateMessageResult(
|
||||
content=TextContent(type="text", text="HELPFUL CONTENT FROM A VERY SMART LLM"),
|
||||
role="assistant",
|
||||
model="gpt-4o-mini",
|
||||
)
|
||||
|
|
@ -5,7 +5,7 @@ import pytest
|
|||
from mcp.types import ModelPreferences
|
||||
from starlette.requests import Request
|
||||
|
||||
from fastmcp.server.context import Context
|
||||
from fastmcp.server.context import Context, _parse_model_preferences
|
||||
from fastmcp.server.server import FastMCP
|
||||
|
||||
|
||||
|
|
@ -68,24 +68,24 @@ def context():
|
|||
|
||||
class TestParseModelPreferences:
|
||||
def test_parse_model_preferences_string(self, context):
|
||||
mp = context._parse_model_preferences("claude-3-sonnet")
|
||||
mp = _parse_model_preferences("claude-3-sonnet")
|
||||
assert isinstance(mp, ModelPreferences)
|
||||
assert mp.hints is not None
|
||||
assert mp.hints[0].name == "claude-3-sonnet"
|
||||
|
||||
def test_parse_model_preferences_list(self, context):
|
||||
mp = context._parse_model_preferences(["claude-3-sonnet", "claude"])
|
||||
mp = _parse_model_preferences(["claude-3-sonnet", "claude"])
|
||||
assert isinstance(mp, ModelPreferences)
|
||||
assert mp.hints is not None
|
||||
assert [h.name for h in mp.hints] == ["claude-3-sonnet", "claude"]
|
||||
|
||||
def test_parse_model_preferences_object(self, context):
|
||||
obj = ModelPreferences(hints=[])
|
||||
assert context._parse_model_preferences(obj) is obj
|
||||
assert _parse_model_preferences(obj) is obj
|
||||
|
||||
def test_parse_model_preferences_invalid_type(self, context):
|
||||
with pytest.raises(ValueError):
|
||||
context._parse_model_preferences(123)
|
||||
_parse_model_preferences(model_preferences=123) # pyright: ignore[reportArgumentType]
|
||||
|
||||
|
||||
class TestSessionId:
|
||||
|
|
|
|||
114
uv.lock
generated
114
uv.lock
generated
|
|
@ -427,6 +427,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "distro"
|
||||
version = "1.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dnspython"
|
||||
version = "2.7.0"
|
||||
|
|
@ -533,6 +542,7 @@ dependencies = [
|
|||
{ name = "exceptiongroup" },
|
||||
{ name = "httpx" },
|
||||
{ name = "mcp" },
|
||||
{ name = "openai" },
|
||||
{ name = "openapi-core" },
|
||||
{ name = "openapi-pydantic" },
|
||||
{ name = "pydantic", extra = ["email"] },
|
||||
|
|
@ -578,6 +588,7 @@ requires-dist = [
|
|||
{ name = "exceptiongroup", specifier = ">=1.2.2" },
|
||||
{ name = "httpx", specifier = ">=0.28.1" },
|
||||
{ name = "mcp", specifier = ">=1.12.4,<2.0.0" },
|
||||
{ name = "openai", specifier = ">=1.95.1" },
|
||||
{ name = "openapi-core", specifier = ">=0.19.5" },
|
||||
{ name = "openapi-pydantic", specifier = ">=0.5.1" },
|
||||
{ name = "pydantic", extras = ["email"], specifier = ">=2.11.7" },
|
||||
|
|
@ -801,6 +812,78 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiter"
|
||||
version = "0.10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759, upload-time = "2025-05-18T19:04:59.73Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/be/7e/4011b5c77bec97cb2b572f566220364e3e21b51c48c5bd9c4a9c26b41b67/jiter-0.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cd2fb72b02478f06a900a5782de2ef47e0396b3e1f7d5aba30daeb1fce66f303", size = 317215, upload-time = "2025-05-18T19:03:04.303Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/4f/144c1b57c39692efc7ea7d8e247acf28e47d0912800b34d0ad815f6b2824/jiter-0.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:32bb468e3af278f095d3fa5b90314728a6916d89ba3d0ffb726dd9bf7367285e", size = 322814, upload-time = "2025-05-18T19:03:06.433Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/1f/db977336d332a9406c0b1f0b82be6f71f72526a806cbb2281baf201d38e3/jiter-0.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8b3e0068c26ddedc7abc6fac37da2d0af16b921e288a5a613f4b86f050354f", size = 345237, upload-time = "2025-05-18T19:03:07.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/1c/aa30a4a775e8a672ad7f21532bdbfb269f0706b39c6ff14e1f86bdd9e5ff/jiter-0.10.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:286299b74cc49e25cd42eea19b72aa82c515d2f2ee12d11392c56d8701f52224", size = 370999, upload-time = "2025-05-18T19:03:09.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/df/f8257abc4207830cb18880781b5f5b716bad5b2a22fb4330cfd357407c5b/jiter-0.10.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ed5649ceeaeffc28d87fb012d25a4cd356dcd53eff5acff1f0466b831dda2a7", size = 491109, upload-time = "2025-05-18T19:03:11.13Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/76/9e1516fd7b4278aa13a2cc7f159e56befbea9aa65c71586305e7afa8b0b3/jiter-0.10.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2ab0051160cb758a70716448908ef14ad476c3774bd03ddce075f3c1f90a3d6", size = 388608, upload-time = "2025-05-18T19:03:12.911Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/64/67750672b4354ca20ca18d3d1ccf2c62a072e8a2d452ac3cf8ced73571ef/jiter-0.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03997d2f37f6b67d2f5c475da4412be584e1cec273c1cfc03d642c46db43f8cf", size = 352454, upload-time = "2025-05-18T19:03:14.741Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/4d/5c4e36d48f169a54b53a305114be3efa2bbffd33b648cd1478a688f639c1/jiter-0.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c404a99352d839fed80d6afd6c1d66071f3bacaaa5c4268983fc10f769112e90", size = 391833, upload-time = "2025-05-18T19:03:16.426Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/de/ce4a6166a78810bd83763d2fa13f85f73cbd3743a325469a4a9289af6dae/jiter-0.10.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66e989410b6666d3ddb27a74c7e50d0829704ede652fd4c858e91f8d64b403d0", size = 523646, upload-time = "2025-05-18T19:03:17.704Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/a6/3bc9acce53466972964cf4ad85efecb94f9244539ab6da1107f7aed82934/jiter-0.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b532d3af9ef4f6374609a3bcb5e05a1951d3bf6190dc6b176fdb277c9bbf15ee", size = 514735, upload-time = "2025-05-18T19:03:19.44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/d8/243c2ab8426a2a4dea85ba2a2ba43df379ccece2145320dfd4799b9633c5/jiter-0.10.0-cp310-cp310-win32.whl", hash = "sha256:da9be20b333970e28b72edc4dff63d4fec3398e05770fb3205f7fb460eb48dd4", size = 210747, upload-time = "2025-05-18T19:03:21.184Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/7a/8021bd615ef7788b98fc76ff533eaac846322c170e93cbffa01979197a45/jiter-0.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:f59e533afed0c5b0ac3eba20d2548c4a550336d8282ee69eb07b37ea526ee4e5", size = 207484, upload-time = "2025-05-18T19:03:23.046Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/dd/6cefc6bd68b1c3c979cecfa7029ab582b57690a31cd2f346c4d0ce7951b6/jiter-0.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3bebe0c558e19902c96e99217e0b8e8b17d570906e72ed8a87170bc290b1e978", size = 317473, upload-time = "2025-05-18T19:03:25.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/cf/fc33f5159ce132be1d8dd57251a1ec7a631c7df4bd11e1cd198308c6ae32/jiter-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:558cc7e44fd8e507a236bee6a02fa17199ba752874400a0ca6cd6e2196cdb7dc", size = 321971, upload-time = "2025-05-18T19:03:27.255Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/a4/da3f150cf1d51f6c472616fb7650429c7ce053e0c962b41b68557fdf6379/jiter-0.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d613e4b379a07d7c8453c5712ce7014e86c6ac93d990a0b8e7377e18505e98d", size = 345574, upload-time = "2025-05-18T19:03:28.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/34/6e8d412e60ff06b186040e77da5f83bc158e9735759fcae65b37d681f28b/jiter-0.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f62cf8ba0618eda841b9bf61797f21c5ebd15a7a1e19daab76e4e4b498d515b2", size = 371028, upload-time = "2025-05-18T19:03:30.292Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/d9/9ee86173aae4576c35a2f50ae930d2ccb4c4c236f6cb9353267aa1d626b7/jiter-0.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:919d139cdfa8ae8945112398511cb7fca58a77382617d279556b344867a37e61", size = 491083, upload-time = "2025-05-18T19:03:31.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/2c/f955de55e74771493ac9e188b0f731524c6a995dffdcb8c255b89c6fb74b/jiter-0.10.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13ddbc6ae311175a3b03bd8994881bc4635c923754932918e18da841632349db", size = 388821, upload-time = "2025-05-18T19:03:33.184Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/5a/0e73541b6edd3f4aada586c24e50626c7815c561a7ba337d6a7eb0a915b4/jiter-0.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c440ea003ad10927a30521a9062ce10b5479592e8a70da27f21eeb457b4a9c5", size = 352174, upload-time = "2025-05-18T19:03:34.965Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/c0/61eeec33b8c75b31cae42be14d44f9e6fe3ac15a4e58010256ac3abf3638/jiter-0.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc347c87944983481e138dea467c0551080c86b9d21de6ea9306efb12ca8f606", size = 391869, upload-time = "2025-05-18T19:03:36.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/22/5beb5ee4ad4ef7d86f5ea5b4509f680a20706c4a7659e74344777efb7739/jiter-0.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:13252b58c1f4d8c5b63ab103c03d909e8e1e7842d302473f482915d95fefd605", size = 523741, upload-time = "2025-05-18T19:03:38.168Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/10/768e8818538e5817c637b0df52e54366ec4cebc3346108a4457ea7a98f32/jiter-0.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7d1bbf3c465de4a24ab12fb7766a0003f6f9bce48b8b6a886158c4d569452dc5", size = 514527, upload-time = "2025-05-18T19:03:39.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/6d/29b7c2dc76ce93cbedabfd842fc9096d01a0550c52692dfc33d3cc889815/jiter-0.10.0-cp311-cp311-win32.whl", hash = "sha256:db16e4848b7e826edca4ccdd5b145939758dadf0dc06e7007ad0e9cfb5928ae7", size = 210765, upload-time = "2025-05-18T19:03:41.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/c9/d394706deb4c660137caf13e33d05a031d734eb99c051142e039d8ceb794/jiter-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c9c1d5f10e18909e993f9641f12fe1c77b3e9b533ee94ffa970acc14ded3812", size = 209234, upload-time = "2025-05-18T19:03:42.918Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/b5/348b3313c58f5fbfb2194eb4d07e46a35748ba6e5b3b3046143f3040bafa/jiter-0.10.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1e274728e4a5345a6dde2d343c8da018b9d4bd4350f5a472fa91f66fda44911b", size = 312262, upload-time = "2025-05-18T19:03:44.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/4a/6a2397096162b21645162825f058d1709a02965606e537e3304b02742e9b/jiter-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7202ae396446c988cb2a5feb33a543ab2165b786ac97f53b59aafb803fef0744", size = 320124, upload-time = "2025-05-18T19:03:46.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/85/1ce02cade7516b726dd88f59a4ee46914bf79d1676d1228ef2002ed2f1c9/jiter-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23ba7722d6748b6920ed02a8f1726fb4b33e0fd2f3f621816a8b486c66410ab2", size = 345330, upload-time = "2025-05-18T19:03:47.596Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/d0/bb6b4f209a77190ce10ea8d7e50bf3725fc16d3372d0a9f11985a2b23eff/jiter-0.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:371eab43c0a288537d30e1f0b193bc4eca90439fc08a022dd83e5e07500ed026", size = 369670, upload-time = "2025-05-18T19:03:49.334Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/f5/a61787da9b8847a601e6827fbc42ecb12be2c925ced3252c8ffcb56afcaf/jiter-0.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c675736059020365cebc845a820214765162728b51ab1e03a1b7b3abb70f74c", size = 489057, upload-time = "2025-05-18T19:03:50.66Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/e4/6f906272810a7b21406c760a53aadbe52e99ee070fc5c0cb191e316de30b/jiter-0.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c5867d40ab716e4684858e4887489685968a47e3ba222e44cde6e4a2154f959", size = 389372, upload-time = "2025-05-18T19:03:51.98Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/ba/77013b0b8ba904bf3762f11e0129b8928bff7f978a81838dfcc958ad5728/jiter-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395bb9a26111b60141757d874d27fdea01b17e8fac958b91c20128ba8f4acc8a", size = 352038, upload-time = "2025-05-18T19:03:53.703Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/27/c62568e3ccb03368dbcc44a1ef3a423cb86778a4389e995125d3d1aaa0a4/jiter-0.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6842184aed5cdb07e0c7e20e5bdcfafe33515ee1741a6835353bb45fe5d1bd95", size = 391538, upload-time = "2025-05-18T19:03:55.046Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/72/0d6b7e31fc17a8fdce76164884edef0698ba556b8eb0af9546ae1a06b91d/jiter-0.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:62755d1bcea9876770d4df713d82606c8c1a3dca88ff39046b85a048566d56ea", size = 523557, upload-time = "2025-05-18T19:03:56.386Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/09/bc1661fbbcbeb6244bd2904ff3a06f340aa77a2b94e5a7373fd165960ea3/jiter-0.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533efbce2cacec78d5ba73a41756beff8431dfa1694b6346ce7af3a12c42202b", size = 514202, upload-time = "2025-05-18T19:03:57.675Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/84/5a5d5400e9d4d54b8004c9673bbe4403928a00d28529ff35b19e9d176b19/jiter-0.10.0-cp312-cp312-win32.whl", hash = "sha256:8be921f0cadd245e981b964dfbcd6fd4bc4e254cdc069490416dd7a2632ecc01", size = 211781, upload-time = "2025-05-18T19:03:59.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/52/7ec47455e26f2d6e5f2ea4951a0652c06e5b995c291f723973ae9e724a65/jiter-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7c7d785ae9dda68c2678532a5a1581347e9c15362ae9f6e68f3fdbfb64f2e49", size = 206176, upload-time = "2025-05-18T19:04:00.305Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/b0/279597e7a270e8d22623fea6c5d4eeac328e7d95c236ed51a2b884c54f70/jiter-0.10.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0588107ec8e11b6f5ef0e0d656fb2803ac6cf94a96b2b9fc675c0e3ab5e8644", size = 311617, upload-time = "2025-05-18T19:04:02.078Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/e3/0916334936f356d605f54cc164af4060e3e7094364add445a3bc79335d46/jiter-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cafc4628b616dc32530c20ee53d71589816cf385dd9449633e910d596b1f5c8a", size = 318947, upload-time = "2025-05-18T19:04:03.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/8e/fd94e8c02d0e94539b7d669a7ebbd2776e51f329bb2c84d4385e8063a2ad/jiter-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:520ef6d981172693786a49ff5b09eda72a42e539f14788124a07530f785c3ad6", size = 344618, upload-time = "2025-05-18T19:04:04.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/b0/f9f0a2ec42c6e9c2e61c327824687f1e2415b767e1089c1d9135f43816bd/jiter-0.10.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:554dedfd05937f8fc45d17ebdf298fe7e0c77458232bcb73d9fbbf4c6455f5b3", size = 368829, upload-time = "2025-05-18T19:04:06.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/57/5bbcd5331910595ad53b9fd0c610392ac68692176f05ae48d6ce5c852967/jiter-0.10.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bc299da7789deacf95f64052d97f75c16d4fc8c4c214a22bf8d859a4288a1c2", size = 491034, upload-time = "2025-05-18T19:04:08.222Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/be/c393df00e6e6e9e623a73551774449f2f23b6ec6a502a3297aeeece2c65a/jiter-0.10.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5161e201172de298a8a1baad95eb85db4fb90e902353b1f6a41d64ea64644e25", size = 388529, upload-time = "2025-05-18T19:04:09.566Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/3e/df2235c54d365434c7f150b986a6e35f41ebdc2f95acea3036d99613025d/jiter-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e2227db6ba93cb3e2bf67c87e594adde0609f146344e8207e8730364db27041", size = 350671, upload-time = "2025-05-18T19:04:10.98Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/77/71b0b24cbcc28f55ab4dbfe029f9a5b73aeadaba677843fc6dc9ed2b1d0a/jiter-0.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15acb267ea5e2c64515574b06a8bf393fbfee6a50eb1673614aa45f4613c0cca", size = 390864, upload-time = "2025-05-18T19:04:12.722Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/d3/ef774b6969b9b6178e1d1e7a89a3bd37d241f3d3ec5f8deb37bbd203714a/jiter-0.10.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:901b92f2e2947dc6dfcb52fd624453862e16665ea909a08398dde19c0731b7f4", size = 522989, upload-time = "2025-05-18T19:04:14.261Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/41/9becdb1d8dd5d854142f45a9d71949ed7e87a8e312b0bede2de849388cb9/jiter-0.10.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d0cb9a125d5a3ec971a094a845eadde2db0de85b33c9f13eb94a0c63d463879e", size = 513495, upload-time = "2025-05-18T19:04:15.603Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/36/3468e5a18238bdedae7c4d19461265b5e9b8e288d3f86cd89d00cbb48686/jiter-0.10.0-cp313-cp313-win32.whl", hash = "sha256:48a403277ad1ee208fb930bdf91745e4d2d6e47253eedc96e2559d1e6527006d", size = 211289, upload-time = "2025-05-18T19:04:17.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/07/1c96b623128bcb913706e294adb5f768fb7baf8db5e1338ce7b4ee8c78ef/jiter-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:75f9eb72ecb640619c29bf714e78c9c46c9c4eaafd644bf78577ede459f330d4", size = 205074, upload-time = "2025-05-18T19:04:19.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/46/caa2c1342655f57d8f0f2519774c6d67132205909c65e9aa8255e1d7b4f4/jiter-0.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:28ed2a4c05a1f32ef0e1d24c2611330219fed727dae01789f4a335617634b1ca", size = 318225, upload-time = "2025-05-18T19:04:20.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/84/c7d44c75767e18946219ba2d703a5a32ab37b0bc21886a97bc6062e4da42/jiter-0.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a4c418b1ec86a195f1ca69da8b23e8926c752b685af665ce30777233dfe070", size = 350235, upload-time = "2025-05-18T19:04:22.363Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/16/f5a0135ccd968b480daad0e6ab34b0c7c5ba3bc447e5088152696140dcb3/jiter-0.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d7bfed2fe1fe0e4dda6ef682cee888ba444b21e7a6553e03252e4feb6cf0adca", size = 207278, upload-time = "2025-05-18T19:04:23.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/9b/1d646da42c3de6c2188fdaa15bce8ecb22b635904fc68be025e21249ba44/jiter-0.10.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:5e9251a5e83fab8d87799d3e1a46cb4b7f2919b895c6f4483629ed2446f66522", size = 310866, upload-time = "2025-05-18T19:04:24.891Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/0e/26538b158e8a7c7987e94e7aeb2999e2e82b1f9d2e1f6e9874ddf71ebda0/jiter-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:023aa0204126fe5b87ccbcd75c8a0d0261b9abdbbf46d55e7ae9f8e22424eeb8", size = 318772, upload-time = "2025-05-18T19:04:26.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/fb/d302893151caa1c2636d6574d213e4b34e31fd077af6050a9c5cbb42f6fb/jiter-0.10.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c189c4f1779c05f75fc17c0c1267594ed918996a231593a21a5ca5438445216", size = 344534, upload-time = "2025-05-18T19:04:27.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/d8/5780b64a149d74e347c5128d82176eb1e3241b1391ac07935693466d6219/jiter-0.10.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15720084d90d1098ca0229352607cd68256c76991f6b374af96f36920eae13c4", size = 369087, upload-time = "2025-05-18T19:04:28.896Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/5b/f235a1437445160e777544f3ade57544daf96ba7e96c1a5b24a6f7ac7004/jiter-0.10.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4f2fb68e5f1cfee30e2b2a09549a00683e0fde4c6a2ab88c94072fc33cb7426", size = 490694, upload-time = "2025-05-18T19:04:30.183Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/a9/9c3d4617caa2ff89cf61b41e83820c27ebb3f7b5fae8a72901e8cd6ff9be/jiter-0.10.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce541693355fc6da424c08b7edf39a2895f58d6ea17d92cc2b168d20907dee12", size = 388992, upload-time = "2025-05-18T19:04:32.028Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/b1/344fd14049ba5c94526540af7eb661871f9c54d5f5601ff41a959b9a0bbd/jiter-0.10.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31c50c40272e189d50006ad5c73883caabb73d4e9748a688b216e85a9a9ca3b9", size = 351723, upload-time = "2025-05-18T19:04:33.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/89/4c0e345041186f82a31aee7b9d4219a910df672b9fef26f129f0cda07a29/jiter-0.10.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa3402a2ff9815960e0372a47b75c76979d74402448509ccd49a275fa983ef8a", size = 392215, upload-time = "2025-05-18T19:04:34.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/58/ee607863e18d3f895feb802154a2177d7e823a7103f000df182e0f718b38/jiter-0.10.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:1956f934dca32d7bb647ea21d06d93ca40868b505c228556d3373cbd255ce853", size = 522762, upload-time = "2025-05-18T19:04:36.19Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/d0/9123fb41825490d16929e73c212de9a42913d68324a8ce3c8476cae7ac9d/jiter-0.10.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:fcedb049bdfc555e261d6f65a6abe1d5ad68825b7202ccb9692636c70fcced86", size = 513427, upload-time = "2025-05-18T19:04:37.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/b3/2bd02071c5a2430d0b70403a34411fc519c2f227da7b03da9ba6a956f931/jiter-0.10.0-cp314-cp314-win32.whl", hash = "sha256:ac509f7eccca54b2a29daeb516fb95b6f0bd0d0d8084efaf8ed5dfc7b9f0b357", size = 210127, upload-time = "2025-05-18T19:04:38.837Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/0c/5fe86614ea050c3ecd728ab4035534387cd41e7c1855ef6c031f1ca93e3f/jiter-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ed975b83a2b8639356151cef5c0d597c68376fc4922b45d0eb384ac058cfa00", size = 318527, upload-time = "2025-05-18T19:04:40.612Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/4a/4175a563579e884192ba6e81725fc0448b042024419be8d83aa8a80a3f44/jiter-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa96f2abba33dc77f79b4cf791840230375f9534e5fac927ccceb58c5e604a5", size = 354213, upload-time = "2025-05-18T19:04:41.894Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema"
|
||||
version = "4.25.0"
|
||||
|
|
@ -993,6 +1076,25 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "1.95.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "distro" },
|
||||
{ name = "httpx" },
|
||||
{ name = "jiter" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/a3/70cd57c7d71086c532ce90de5fdef4165dc6ae9dbf346da6737ff9ebafaa/openai-1.95.1.tar.gz", hash = "sha256:f089b605282e2a2b6776090b4b46563ac1da77f56402a222597d591e2dcc1086", size = 488271, upload-time = "2025-07-11T20:47:24.437Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/02/1d/0432ea635097f4dbb34641a3650803d8a4aa29d06bafc66583bf1adcceb4/openai-1.95.1-py3-none-any.whl", hash = "sha256:8bbdfeceef231b1ddfabbc232b179d79f8b849aab5a7da131178f8d10e0f162f", size = 755613, upload-time = "2025-07-11T20:47:22.629Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openapi-core"
|
||||
version = "0.19.5"
|
||||
|
|
@ -2079,6 +2181,18 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.67.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "traitlets"
|
||||
version = "5.14.3"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue