Compare commits

...

1 Commits

Author SHA1 Message Date
Awni Hannun 1e0c5727b6 add responses API with examples 2025-09-20 12:52:34 -07:00
3 changed files with 143 additions and 7 deletions
+19 -1
View File
@@ -30,6 +30,8 @@ To see a full list of options run:
mlx_lm.server --help
```
## Chat completions API
You can make a request to the model by running:
```shell
@@ -128,7 +130,23 @@ curl localhost:8080/v1/chat/completions \
- `completion_tokens`: The number of tokens generated.
- `total_tokens`: The total number of tokens, i.e. the sum of the above two fields.
### List Models
## Responses API
The responses API follows the [OpenAI responses API
spec](https://platform.openai.com/docs/quickstart?api-mode=responses)
To make a request, use the `/reponses` endpoint. For exapmle:
```shell
curl localhost:8080/responses \
-H "Content-Type: application/json" \
-d '{
"input": [{"role": "user", "content": "Say this is a test!"}],
"temperature": 0.7
}'
```
## Models API
Use the `v1/models` endpoint to list available models:
+59
View File
@@ -0,0 +1,59 @@
# Copyright © 2025 Apple Inc.
"""
Examples using the OpenAI responses endpoint with mlx_lm.server.
To run, first start the server:
>>> mlx_lm.server
Then run this script.
More documentation on the API spec here:
https://platform.openai.com/docs/quickstart?api-mode=responses
"""
from openai import OpenAI
model = "mlx-community/Qwen3-4B-Instruct-2507-4bit"
### Basic response example
client = OpenAI(base_url="http://localhost:8080/v1", api_key="not-needed")
response = client.responses.create(
model=model, input="Write a one-sentence bedtime story about a unicorn."
)
print(response.output_text)
### Input with roles
response = client.responses.create(
model=model,
input=[
{
"role": "user",
"content": [
{
"type": "input_text",
"text": "Write a one-sentence bedtime story about a unicorn.",
},
],
}
],
)
print(response.output_text)
### Streaming
stream = client.responses.create(
model=model,
input=[
{
"role": "user",
"content": "Say 'double bubble bath' ten times fast.",
},
],
stream=True,
)
for event in stream:
print(event)
+65 -6
View File
@@ -279,6 +279,8 @@ class APIHandler(BaseHTTPRequestHandler):
endpoints = {
"/v1/completions": self.handle_text_completions,
"/v1/chat/completions": self.handle_chat_completions,
"/responses": self.handle_responses,
"/v1/responses": self.handle_responses,
"/chat/completions": self.handle_chat_completions,
}
@@ -328,6 +330,7 @@ class APIHandler(BaseHTTPRequestHandler):
self.max_tokens = self.body.get(
"max_tokens", self.model_provider.cli_args.max_tokens
)
self.temperature = self.body.get(
"temperature", self.model_provider.cli_args.temp
)
@@ -492,6 +495,18 @@ class APIHandler(BaseHTTPRequestHandler):
"id": None,
}
if self.stream and self.object_type == "response" and finish_reason is None:
return {
"type": "response.output_text.delta",
"delta": text,
# TODO, these need valid values
"sequence_number": None,
"item_id": None,
"output_index": 1,
"content_index": 0,
"logprobs": [],
}
# Static response
response = {
"id": self.request_id,
@@ -499,14 +514,28 @@ class APIHandler(BaseHTTPRequestHandler):
"object": self.object_type,
"model": self.requested_model,
"created": self.created,
"choices": [
{
"index": 0,
"finish_reason": finish_reason,
},
],
}
if self.object_type == "response":
response["output"] = [
{
"type": "message",
"role": "assistant",
"content": [{"text": text, "type": "output_text"}],
}
]
if self.stream:
return {"response": response, "type": "response.completed"}
return response
response["choices"] = [
{
"index": 0,
"finish_reason": finish_reason,
},
]
if token_logprobs or top_logprobs or tokens:
response["choices"][0]["logprobs"] = {
"token_logprobs": token_logprobs,
@@ -864,6 +893,36 @@ class APIHandler(BaseHTTPRequestHandler):
return prompt
def handle_responses(self) -> List[int]:
body = self.body
system_prompt = body.get("instructions")
prompt = body["input"]
tools = body.get("tools")
messages = []
if system_prompt:
messages = [{"role": "system", "content": system_prompt}]
if isinstance(prompt, list):
for message in prompt:
content = message["content"]
if isinstance(content, list):
if len(content) != 1 or content[0]["type"] != "input_text":
raise ValueError("Unsupported content type.")
message["content"] = content[0]["text"]
messages.append(message)
else:
messages.append({"role": "user", "content": prompt})
# Determine response type
self.request_id = f"resp_{uuid.uuid4()}"
self.object_type = "response"
prompt = self.tokenizer.apply_chat_template(
messages,
tools=tools,
add_generation_prompt=True,
**self.model_provider.cli_args.chat_template_args,
)
return prompt
def handle_text_completions(self) -> List[int]:
"""
Handle a text completion request.