Skip to content

Commit 0c8531c

Browse files
authored
feat: Responses API documentation and implementation (#109)
* feat: Responses API documentation and implementation * warning dialog
1 parent e9a2437 commit 0c8531c

File tree

7 files changed

+177
-4
lines changed

7 files changed

+177
-4
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,23 @@ response = completion(
9595
)
9696
print(response.choices[0].message.content)
9797
```
98+
99+
### Responses API
100+
101+
For providers that implement the OpenAI-style Responses API, use [`responses`](https://mozilla-ai.github.io/any-llm/api/responses/) or `aresponses`:
102+
103+
```python
104+
from any_llm import responses
105+
106+
result = responses(
107+
model="openai/gpt-4o-mini",
108+
input_data=[
109+
{"role": "user", "content": [
110+
{"type": "text", "text": "Summarize this in one sentence."}
111+
]}
112+
],
113+
)
114+
115+
# Non-streaming returns an OpenAI-compatible Responses object alias
116+
print(result.output_text)
117+
```

docs/api/responses.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
## Responses
2+
3+
4+
!!! warning
5+
6+
This API is experimental and subject to changes based upon our experience as we integrate additional providers.
7+
Use with caution.
8+
9+
::: any_llm.responses
10+
::: any_llm.aresponses

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Refer to the [Quickstart](./quickstart.md) for instructions on installation and
1212

1313
### Parameters
1414

15-
For a complete list of available functions and their parameters, see the [completion API documentation](./api/completion.md).
15+
For a complete list of available functions and their parameters, see the [completion](./api/completion.md), [embedding](./api/embedding.md), and [responses](./api/responses.md) API documentation.
1616

1717
### Error Handling
1818

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ nav:
1010
- API Reference:
1111
- Completion: api/completion.md
1212
- Embedding: api/embedding.md
13+
- Responses: api/responses.md
1314
- Exceptions: api/exceptions.md
1415
- Helpers: api/helpers.md
1516
theme:

src/any_llm/api.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,9 +252,35 @@ def responses(
252252
user: Optional[str] = None,
253253
**kwargs: Any,
254254
) -> Response | Iterator[ResponseStreamEvent]:
255-
"""Create a response using the Responses API.
255+
"""Create a response using the OpenAI-style Responses API.
256256
257-
This normalizes to the same ChatCompletion/Chunk types for compatibility.
257+
This follows the OpenAI Responses API shape and returns the aliased
258+
`any_llm.types.responses.Response` type. If `stream=True`, an iterator of
259+
`any_llm.types.responses.ResponseStreamEvent` items is returned.
260+
261+
Args:
262+
model: Model identifier in format 'provider/model' (e.g., 'openai/gpt-4o')
263+
input_data: The input payload accepted by provider's Responses API.
264+
For OpenAI-compatible providers, this is typically a list mixing
265+
text, images, and tool instructions, or a dict per OpenAI spec.
266+
tools: Optional tools for tool calling (Python callables or OpenAI tool dicts)
267+
tool_choice: Controls which tools the model can call
268+
max_output_tokens: Maximum number of output tokens to generate
269+
temperature: Controls randomness in the response (0.0 to 2.0)
270+
top_p: Controls diversity via nucleus sampling (0.0 to 1.0)
271+
stream: Whether to stream response events
272+
api_key: API key for the provider
273+
api_base: Base URL for the provider API
274+
timeout: Request timeout in seconds
275+
user: Unique identifier for the end user
276+
**kwargs: Additional provider-specific parameters
277+
278+
Returns:
279+
Either a `Response` object (non-streaming) or an iterator of
280+
`ResponseStreamEvent` (streaming).
281+
282+
Raises:
283+
NotImplementedError: If the selected provider does not support the Responses API.
258284
"""
259285
provider_key, model_name = ProviderFactory.split_model_provider(model)
260286

@@ -304,6 +330,36 @@ async def aresponses(
304330
user: Optional[str] = None,
305331
**kwargs: Any,
306332
) -> Response | Iterator[ResponseStreamEvent]:
333+
"""Create a response using the OpenAI-style Responses API.
334+
335+
This follows the OpenAI Responses API shape and returns the aliased
336+
`any_llm.types.responses.Response` type. If `stream=True`, an iterator of
337+
`any_llm.types.responses.ResponseStreamEvent` items is returned.
338+
339+
Args:
340+
model: Model identifier in format 'provider/model' (e.g., 'openai/gpt-4o')
341+
input_data: The input payload accepted by provider's Responses API.
342+
For OpenAI-compatible providers, this is typically a list mixing
343+
text, images, and tool instructions, or a dict per OpenAI spec.
344+
tools: Optional tools for tool calling (Python callables or OpenAI tool dicts)
345+
tool_choice: Controls which tools the model can call
346+
max_output_tokens: Maximum number of output tokens to generate
347+
temperature: Controls randomness in the response (0.0 to 2.0)
348+
top_p: Controls diversity via nucleus sampling (0.0 to 1.0)
349+
stream: Whether to stream response events
350+
api_key: API key for the provider
351+
api_base: Base URL for the provider API
352+
timeout: Request timeout in seconds
353+
user: Unique identifier for the end user
354+
**kwargs: Additional provider-specific parameters
355+
356+
Returns:
357+
Either a `Response` object (non-streaming) or an iterator of
358+
`ResponseStreamEvent` (streaming).
359+
360+
Raises:
361+
NotImplementedError: If the selected provider does not support the Responses API.
362+
"""
307363
provider_key, model_name = ProviderFactory.split_model_provider(model)
308364

309365
config: dict[str, str] = {}

tests/unit/test_api_signature.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import inspect
2-
from any_llm.api import completion, acompletion
2+
from any_llm.api import completion, acompletion, responses, aresponses
33

44

55
def test_completion_and_acompletion_have_same_signature() -> None:
@@ -62,3 +62,50 @@ def test_completion_and_acompletion_parameter_details() -> None:
6262
assert completion_param.kind == acompletion_param.kind, (
6363
f"Parameter '{param_name}' should have identical parameter kinds"
6464
)
65+
66+
67+
def test_responses_and_aresponses_have_same_signature() -> None:
68+
"""Test that responses and aresponses have identical signatures."""
69+
responses_sig = inspect.signature(responses)
70+
aresponses_sig = inspect.signature(aresponses)
71+
72+
assert responses_sig.parameters == aresponses_sig.parameters, (
73+
"responses and aresponses should have identical parameters"
74+
)
75+
76+
assert responses_sig.return_annotation == aresponses_sig.return_annotation, (
77+
"responses and aresponses should have identical return annotations"
78+
)
79+
80+
81+
def test_responses_and_aresponses_have_same_docstring() -> None:
82+
"""Test that responses and aresponses have identical docstrings."""
83+
responses_doc = responses.__doc__
84+
aresponses_doc = aresponses.__doc__
85+
86+
assert responses_doc is not None, "responses should have a docstring"
87+
assert aresponses_doc is not None, "aresponses should have a docstring"
88+
89+
assert responses_doc == aresponses_doc, "responses and aresponses should have identical docstrings"
90+
91+
92+
def test_responses_and_aresponses_parameter_details() -> None:
93+
"""Test that responses and aresponses parameters have identical details."""
94+
responses_sig = inspect.signature(responses)
95+
aresponses_sig = inspect.signature(aresponses)
96+
97+
for param_name in responses_sig.parameters:
98+
responses_param = responses_sig.parameters[param_name]
99+
aresponses_param = aresponses_sig.parameters[param_name]
100+
101+
assert responses_param.annotation == aresponses_param.annotation, (
102+
f"Parameter '{param_name}' should have identical annotations"
103+
)
104+
105+
assert responses_param.default == aresponses_param.default, (
106+
f"Parameter '{param_name}' should have identical default values"
107+
)
108+
109+
assert responses_param.kind == aresponses_param.kind, (
110+
f"Parameter '{param_name}' should have identical parameter kinds"
111+
)

tests/unit/test_responses.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import pytest
2+
from unittest.mock import Mock, patch
3+
4+
from any_llm import responses
5+
from any_llm.provider import ProviderName
6+
7+
8+
def test_responses_invalid_model_format_no_slash() -> None:
9+
"""Test responses raises ValueError for model without slash."""
10+
with pytest.raises(ValueError, match="Invalid model format. Expected 'provider/model', got 'gpt-5-nano'"):
11+
responses("gpt-5-nano", input_data=[{"role": "user", "content": "Hello"}])
12+
13+
14+
def test_responses_invalid_model_format_empty_provider() -> None:
15+
"""Test responses raises ValueError for model with empty provider."""
16+
with pytest.raises(ValueError, match="Invalid model format"):
17+
responses("/model", input_data=[{"role": "user", "content": "Hello"}])
18+
19+
20+
def test_responses_invalid_model_format_empty_model() -> None:
21+
"""Test responses raises ValueError for model with empty model name."""
22+
with pytest.raises(ValueError, match="Invalid model format"):
23+
responses("provider/", input_data=[{"role": "user", "content": "Hello"}])
24+
25+
26+
def test_responses_invalid_model_format_multiple_slashes() -> None:
27+
"""Test responses handles multiple slashes correctly (should work - takes first split)."""
28+
mock_provider = Mock()
29+
mock_provider.responses.return_value = Mock()
30+
31+
with patch("any_llm.api.ProviderFactory") as mock_factory:
32+
mock_factory.get_supported_providers.return_value = ["provider"]
33+
mock_factory.get_provider_enum.return_value = ProviderName.OPENAI # Using a valid provider
34+
mock_factory.split_model_provider.return_value = (ProviderName.OPENAI, "model/extra")
35+
mock_factory.create_provider.return_value = mock_provider
36+
37+
responses("provider/model/extra", input_data=[{"role": "user", "content": "Hello"}])
38+
39+
mock_provider.responses.assert_called_once_with("model/extra", [{"role": "user", "content": "Hello"}])

0 commit comments

Comments
 (0)