Files
2026-07-13 13:22:34 +08:00

548 lines
16 KiB
Python

import argparse
import base64
import json
from typing import Any
import fastapi
from pydantic import BaseModel
from starlette.responses import StreamingResponse
from mlflow.types.chat import ChatCompletionRequest
EMPTY_CHOICES = "EMPTY_CHOICES"
LIST_CONTENT = "LIST_CONTENT"
AZURE_ANNOTATIONS = "AZURE_ANNOTATIONS"
app = fastapi.FastAPI()
@app.get("/health")
def health():
return {"status": "healthy"}
def chat_response(payload: ChatCompletionRequest):
dumped_input = json.dumps([m.model_dump(exclude_unset=True) for m in payload.messages])
return {
"id": "chatcmpl-123",
"object": "chat.completion",
"created": 1677652288,
"model": "gpt-4o-mini",
"system_fingerprint": "fp_44709d6fcb",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": dumped_input,
},
"logprobs": None,
"finish_reason": "stop",
}
],
"usage": {
"prompt_tokens": 9,
"completion_tokens": 12,
"total_tokens": 21,
},
}
def _make_chat_stream_chunk(content, include_usage: bool = False):
return {
"id": "chatcmpl-123",
"object": "chat.completion.chunk",
"created": 1677652288,
"model": "gpt-4o-mini",
"system_fingerprint": "fp_44709d6fcb",
"choices": [
{
"delta": {
"content": content,
"function_call": None,
"role": None,
"tool_calls": None,
},
"finish_reason": None,
"index": 0,
"logprobs": None,
}
],
"usage": {
"prompt_tokens": 9,
"completion_tokens": 12,
"total_tokens": 21,
}
if include_usage
else None,
}
def _make_chat_stream_chunk_empty_choices():
return {
"id": "chatcmpl-123",
"object": "chat.completion.chunk",
"created": 1677652288,
"model": "gpt-4o-mini",
"system_fingerprint": "fp_44709d6fcb",
"choices": [],
"usage": None,
}
def _make_chat_stream_annotation_chunk():
return {
"id": "",
"object": "",
"created": 0,
"model": "",
"system_fingerprint": None,
"choices": [],
"usage": None,
}
def _make_chat_stream_usage_chunk():
return {
"id": "chatcmpl-123",
"object": "chat.completion.chunk",
"created": 1677652288,
"model": "gpt-4o-mini",
"system_fingerprint": "fp_44709d6fcb",
"choices": [],
"usage": {
"prompt_tokens": 9,
"completion_tokens": 12,
"total_tokens": 21,
},
}
def _make_chat_stream_chunk_with_list_content(content_list, include_usage: bool = False):
# Create a streaming chunk with list content (Databricks format).
return {
"id": "chatcmpl-123",
"object": "chat.completion.chunk",
"created": 1677652288,
"model": "gpt-4o-mini",
"system_fingerprint": "fp_44709d6fcb",
"choices": [
{
"delta": {
"content": content_list,
"function_call": None,
"role": None,
"tool_calls": None,
},
"finish_reason": None,
"index": 0,
"logprobs": None,
}
],
"usage": {
"prompt_tokens": 9,
"completion_tokens": 12,
"total_tokens": 21,
}
if include_usage
else None,
}
async def chat_response_stream(include_usage: bool = False):
# OpenAI Chat Completion stream only includes usage in the last chunk
# if {"stream_options": {"include_usage": True}} is specified in the request.
yield _make_chat_stream_chunk("Hello", include_usage=False)
yield _make_chat_stream_chunk(" world", include_usage=include_usage)
async def chat_response_stream_empty_choices():
yield _make_chat_stream_chunk_empty_choices()
yield _make_chat_stream_chunk("Hello")
async def chat_response_stream_with_azure_annotations(include_usage: bool = False):
yield _make_chat_stream_annotation_chunk()
yield _make_chat_stream_chunk("Hello")
yield _make_chat_stream_chunk(" world")
if include_usage:
yield _make_chat_stream_usage_chunk()
yield _make_chat_stream_annotation_chunk()
async def chat_response_stream_with_list_content(include_usage: bool = False):
# Simulate Databricks streaming format with list content.
yield _make_chat_stream_chunk_with_list_content(
[{"type": "text", "text": "Hello"}], include_usage=False
)
yield _make_chat_stream_chunk_with_list_content(
[{"type": "text", "text": " world"}], include_usage=include_usage
)
@app.post("/chat/completions", response_model_exclude_unset=True)
async def chat(payload: ChatCompletionRequest):
if payload.stream:
# SSE stream
if EMPTY_CHOICES == payload.messages[0].content:
content = (
f"data: {json.dumps(d)}\n\n" async for d in chat_response_stream_empty_choices()
)
elif AZURE_ANNOTATIONS == payload.messages[0].content:
content = (
f"data: {json.dumps(d)}\n\n"
async for d in chat_response_stream_with_azure_annotations(
include_usage=(payload.stream_options or {}).get("include_usage", False)
)
)
elif LIST_CONTENT == payload.messages[0].content:
content = (
f"data: {json.dumps(d)}\n\n"
async for d in chat_response_stream_with_list_content(
include_usage=(payload.stream_options or {}).get("include_usage", False)
)
)
else:
content = (
f"data: {json.dumps(d)}\n\n"
async for d in chat_response_stream(
include_usage=(payload.stream_options or {}).get("include_usage", False)
)
)
return StreamingResponse(
content,
media_type="text/event-stream",
)
else:
return chat_response(payload)
def _make_responses_payload(outputs, tools=None):
return {
"id": "responses-123",
"object": "response",
"created": 1589478378,
"status": "completed",
"error": None,
"incomplete_details": None,
"max_output_tokens": None,
"model": "gpt-4o-mini",
"output": outputs,
"parallel_tool_calls": True,
"previous_response_id": None,
"reasoning": {"effort": None, "generate_summary": None},
"store": True,
"temperature": 1.0,
"text": {"format": {"type": "text"}},
"tool_choice": "auto",
"tools": tools or [],
"top_p": 1.0,
"truncation": "disabled",
"usage": {
"input_tokens": 36,
"input_tokens_details": {"cached_tokens": 0},
"output_tokens": 87,
"output_tokens_details": {"reasoning_tokens": 0},
"total_tokens": 123,
},
"user": None,
"metadata": {},
}
_DUMMY_TEXT_OUTPUTS = [
{
"type": "message",
"id": "test",
"status": "completed",
"role": "assistant",
"content": [
{
"type": "output_text",
"text": "Dummy output",
}
],
}
]
_DUMMY_WEB_SEARCH_OUTPUTS = [
{"type": "web_search_call", "id": "tool_call_1", "status": "completed"},
{
"type": "message",
"id": "msg",
"status": "completed",
"role": "assistant",
"content": [
{
"type": "output_text",
"text": "As of today, March 9, 2025, one notable positive news story...",
"annotations": [
{
"type": "url_citation",
"start_index": 442,
"end_index": 557,
"url": "https://.../?utm_source=chatgpt.com",
"title": "...",
},
],
}
],
},
]
_DUMMY_FILE_SEARCH_OUTPUTS = [
{
"type": "file_search_call",
"id": "file_search_1",
"status": "completed",
"queries": ["attributes of an ancient brown dragon"],
"results": None,
},
{
"type": "message",
"id": "file_search_1",
"status": "completed",
"role": "assistant",
"content": [
{
"type": "output_text",
"text": "The attributes of an ancient brown dragon include...",
"annotations": [
{
"type": "file_citation",
"index": 320,
"file_id": "file-4wDz5b167pAf72nx1h9eiN",
"filename": "dragons.pdf",
},
{
"type": "file_citation",
"index": 576,
"file_id": "file-4wDz5b167pAf72nx1h9eiN",
"filename": "dragons.pdf",
},
],
}
],
},
]
_DUMMY_COMPUTER_USE_OUTPUTS = [
{
"type": "reasoning",
"id": "rs_67cc...",
"summary": [{"type": "summary_text", "text": "Clicking on the browser address bar."}],
},
{
"type": "computer_call",
"id": "cu_67cc...",
"call_id": "computer_call_1",
"action": {"type": "click", "button": "left", "x": 156, "y": 50},
"pending_safety_checks": [],
"status": "completed",
},
]
_DUMMY_FUNCTION_CALL_OUTPUTS = [
{
"type": "function_call",
"id": "fc_67ca09c6bedc8190a7abfec07b1a1332096610f474011cc0",
"call_id": "function_call_1",
"name": "get_current_weather",
"arguments": '{"location":"Boston, MA","unit":"celsius"}',
"status": "completed",
}
]
_DUMMY_RESPONSES_STREAM_EVENTS = [
{
"type": "response.created",
"response": _make_responses_payload(outputs=[]),
},
{
"content_index": 0,
"delta": "Hello ",
"item_id": 0,
"output_index": 0,
"type": "response.output_text.delta",
},
{
"content_index": 0,
"delta": "World",
"item_id": 0,
"output_index": 0,
"type": "response.output_text.delta",
},
{
"response": _make_responses_payload(outputs=_DUMMY_TEXT_OUTPUTS),
"type": "response.completed",
},
]
class ResponsesPayload(BaseModel):
input: Any
tools: list[Any] | None = None
stream: bool = False
@app.post("/responses", response_model_exclude_unset=True)
async def responses(payload: ResponsesPayload):
if payload.stream:
content = (
f"event: {d['type']}\ndata: {json.dumps(d)}\n\n" for d in _DUMMY_RESPONSES_STREAM_EVENTS
)
return StreamingResponse(content, media_type="text/event-stream")
if tools := payload.tools or []:
if tools[0]["type"] == "web_search_preview":
outputs = _DUMMY_WEB_SEARCH_OUTPUTS
elif tools[0]["type"] == "file_search":
outputs = _DUMMY_FILE_SEARCH_OUTPUTS
elif tools[0]["type"] == "computer_use_preview":
outputs = _DUMMY_COMPUTER_USE_OUTPUTS
elif tools[0]["type"] == "function":
outputs = _DUMMY_FUNCTION_CALL_OUTPUTS
else:
raise fastapi.HTTPException(
status_code=400,
detail=f"Unsupported tool type: {tools[0]['type']}",
)
return _make_responses_payload(outputs, tools)
return _make_responses_payload(outputs=_DUMMY_TEXT_OUTPUTS)
class CompletionsPayload(BaseModel):
prompt: str | list[str]
stream: bool = False
def completions_response(payload: CompletionsPayload):
return {
"id": "cmpl-uqkvlQyYK7bGYrRHQ0eXlWi7",
"object": "text_completion",
"created": 1589478378,
"model": "gpt-4o-mini",
"choices": [
{
"text": text,
"index": 0,
"logprobs": None,
"finish_reason": "length",
}
for text in ([payload.prompt] if isinstance(payload.prompt, str) else payload.prompt)
],
"usage": {"prompt_tokens": 5, "completion_tokens": 7, "total_tokens": 12},
}
def _make_completions_stream_chunk(content):
return {
"id": "cmpl-uqkvlQyYK7bGYrRHQ0eXlWi7",
"object": "text_completion",
"created": 1589478378,
"model": "gpt-4o-mini",
"choices": [{"finish_reason": None, "index": 0, "logprobs": None, "text": content}],
"system_fingerprint": None,
"usage": {"prompt_tokens": 5, "completion_tokens": 7, "total_tokens": 12},
}
def _make_completions_stream_chunk_empty_choices():
return {
"id": "cmpl-uqkvlQyYK7bGYrRHQ0eXlWi7",
"object": "text_completion",
"created": 1589478378,
"model": "gpt-4o-mini",
"choices": [],
"system_fingerprint": None,
"usage": None,
}
async def completions_response_stream():
yield _make_completions_stream_chunk("Hello")
yield _make_completions_stream_chunk(" world")
async def completions_response_stream_empty_choices():
yield _make_completions_stream_chunk_empty_choices()
yield _make_completions_stream_chunk("Hello")
@app.post("/completions")
def completions(payload: CompletionsPayload):
if payload.stream:
if EMPTY_CHOICES == payload.prompt:
content = (
f"data: {json.dumps(d)}\n\n"
async for d in completions_response_stream_empty_choices()
)
else:
content = (f"data: {json.dumps(d)}\n\n" async for d in completions_response_stream())
return StreamingResponse(
content,
media_type="text/event-stream",
)
else:
return completions_response(payload)
class EmbeddingsPayload(BaseModel):
input: str | list[str]
@app.post("/embeddings")
def embeddings(payload: EmbeddingsPayload):
return {
"object": "list",
"data": [
{
"object": "embedding",
"embedding": list(range(1536)),
"index": 0,
}
for _ in range(1 if isinstance(payload.input, str) else len(payload.input))
],
"model": "text-embedding-ada-002",
"usage": {"prompt_tokens": 8, "total_tokens": 8},
}
@app.post("/images/generations")
def images_generate():
# Return a minimal DALL-E-style response with a tiny PNG
png_b64 = base64.b64encode(b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01").decode()
return {
"created": 1677652288,
"data": [
{
"b64_json": png_b64,
"revised_prompt": "a test image",
}
],
}
@app.get("/models/{model}")
def models(model: str):
return {
"id": model,
"object": "model",
"created": 1686935002,
"owned_by": "openai",
}
if __name__ == "__main__":
import uvicorn
parser = argparse.ArgumentParser()
parser.add_argument("--host", type=str)
parser.add_argument("--port", type=int)
args = parser.parse_args()
uvicorn.run(app, host=args.host, port=args.port)