| from __future__ import annotations |
|
|
| import time |
| import uuid |
| from typing import Any, Iterable, Iterator |
|
|
| from fastapi import HTTPException |
|
|
| from services.protocol.conversation import ( |
| ConversationRequest, |
| ImageOutput, |
| collect_image_outputs, |
| collect_text, |
| count_message_tokens, |
| count_text_tokens, |
| encode_images, |
| normalize_messages, |
| stream_image_outputs_with_pool, |
| stream_text_deltas, |
| text_backend, |
| ) |
| from utils.helper import build_chat_image_markdown_content, extract_chat_image, extract_chat_prompt, is_image_chat_request, parse_image_count |
|
|
|
|
| def completion_chunk(model: str, delta: dict[str, Any], finish_reason: str | None = None, completion_id: str = "", created: int | None = None) -> dict[str, Any]: |
| return { |
| "id": completion_id or f"chatcmpl-{uuid.uuid4().hex}", |
| "object": "chat.completion.chunk", |
| "created": created or int(time.time()), |
| "model": model, |
| "choices": [{"index": 0, "delta": delta, "finish_reason": finish_reason}], |
| } |
|
|
|
|
| def completion_response( |
| model: str, |
| content: str, |
| created: int | None = None, |
| messages: list[dict[str, Any]] | None = None, |
| ) -> dict[str, Any]: |
| prompt_tokens = count_message_tokens(messages, model) if messages else 0 |
| completion_tokens = count_text_tokens(content, model) if messages else 0 |
| return { |
| "id": f"chatcmpl-{uuid.uuid4().hex}", |
| "object": "chat.completion", |
| "created": created or int(time.time()), |
| "model": model, |
| "choices": [{ |
| "index": 0, |
| "message": {"role": "assistant", "content": content}, |
| "finish_reason": "stop", |
| }], |
| "usage": { |
| "prompt_tokens": prompt_tokens, |
| "completion_tokens": completion_tokens, |
| "total_tokens": prompt_tokens + completion_tokens, |
| }, |
| } |
|
|
|
|
| def stream_text_chat_completion(backend, messages: list[dict[str, Any]], model: str) -> Iterator[dict[str, Any]]: |
| completion_id = f"chatcmpl-{uuid.uuid4().hex}" |
| created = int(time.time()) |
| sent_role = False |
| request = ConversationRequest(model=model, messages=messages) |
| for delta_text in stream_text_deltas(backend, request): |
| if not sent_role: |
| sent_role = True |
| yield completion_chunk(model, {"role": "assistant", "content": delta_text}, None, completion_id, created) |
| else: |
| yield completion_chunk(model, {"content": delta_text}, None, completion_id, created) |
| if not sent_role: |
| yield completion_chunk(model, {"role": "assistant", "content": ""}, None, completion_id, created) |
| yield completion_chunk(model, {}, "stop", completion_id, created) |
|
|
|
|
| def collect_chat_content(chunks: Iterable[dict[str, Any]]) -> str: |
| parts: list[str] = [] |
| for chunk in chunks: |
| choices = chunk.get("choices") |
| first = choices[0] if isinstance(choices, list) and choices and isinstance(choices[0], dict) else {} |
| delta = first.get("delta") if isinstance(first.get("delta"), dict) else {} |
| content = str(delta.get("content") or "") |
| if content: |
| parts.append(content) |
| return "".join(parts) |
|
|
|
|
| def chat_messages_from_body(body: dict[str, Any]) -> list[dict[str, Any]]: |
| messages = body.get("messages") |
| if isinstance(messages, list) and messages: |
| return [message for message in messages if isinstance(message, dict)] |
| prompt = str(body.get("prompt") or "").strip() |
| if prompt: |
| return [{"role": "user", "content": prompt}] |
| raise HTTPException(status_code=400, detail={"error": "messages or prompt is required"}) |
|
|
|
|
| def chat_image_args(body: dict[str, Any]) -> tuple[str, str, int, list[tuple[bytes, str, str]]]: |
| model = str(body.get("model") or "gpt-image-2").strip() or "gpt-image-2" |
| prompt = extract_chat_prompt(body) |
| if not prompt: |
| raise HTTPException(status_code=400, detail={"error": "prompt is required"}) |
| images = [ |
| (data, f"image_{idx}.png", mime) |
| for idx, (data, mime) in enumerate(extract_chat_image(body), start=1) |
| ] |
| return model, prompt, parse_image_count(body.get("n")), images |
|
|
|
|
| def text_chat_parts(body: dict[str, Any]) -> tuple[str, list[dict[str, Any]]]: |
| model = str(body.get("model") or "auto").strip() or "auto" |
| messages = normalize_messages(chat_messages_from_body(body)) |
| return model, messages |
|
|
|
|
| def image_result_content(result: dict[str, Any]) -> str: |
| data = result.get("data") |
| if isinstance(data, list) and data: |
| return build_chat_image_markdown_content(result) |
| return str(result.get("message") or "Image generation completed.") |
|
|
|
|
| def image_chat_response(body: dict[str, Any]) -> dict[str, Any]: |
| model, prompt, n, images = chat_image_args(body) |
| result = collect_image_outputs(stream_image_outputs_with_pool(ConversationRequest( |
| prompt=prompt, |
| model=model, |
| n=n, |
| response_format="b64_json", |
| images=encode_images(images) or None, |
| ))) |
| return completion_response(model, image_result_content(result), int(result.get("created") or 0) or None) |
|
|
|
|
| def image_chat_events(body: dict[str, Any]) -> Iterator[dict[str, Any]]: |
| model, prompt, n, images = chat_image_args(body) |
| image_outputs = stream_image_outputs_with_pool(ConversationRequest( |
| prompt=prompt, |
| model=model, |
| n=n, |
| response_format="b64_json", |
| images=encode_images(images) or None, |
| )) |
| yield from stream_image_chat_completion(image_outputs, model) |
|
|
|
|
| def stream_image_chat_completion(image_outputs: Iterable[ImageOutput], model: str) -> Iterator[dict[str, Any]]: |
| completion_id = f"chatcmpl-{uuid.uuid4().hex}" |
| created = int(time.time()) |
| sent_role = False |
| sent_text = "" |
| for output in image_outputs: |
| content = "" |
| if output.kind == "progress": |
| content = output.text |
| sent_text += content |
| elif output.kind == "result": |
| content = build_chat_image_markdown_content({"data": output.data}) |
| elif output.kind == "message": |
| content = output.text[len(sent_text):] if output.text.startswith(sent_text) else output.text |
| if not content: |
| continue |
| if not sent_role: |
| sent_role = True |
| yield completion_chunk(model, {"role": "assistant", "content": content}, None, completion_id, created) |
| else: |
| yield completion_chunk(model, {"content": content}, None, completion_id, created) |
| if not sent_role: |
| yield completion_chunk(model, {"role": "assistant", "content": ""}, None, completion_id, created) |
| yield completion_chunk(model, {}, "stop", completion_id, created) |
|
|
|
|
| def handle(body: dict[str, Any]) -> dict[str, Any] | Iterator[dict[str, Any]]: |
| if body.get("stream"): |
| if is_image_chat_request(body): |
| return image_chat_events(body) |
| model, messages = text_chat_parts(body) |
| return stream_text_chat_completion(text_backend(), messages, model) |
| if is_image_chat_request(body): |
| return image_chat_response(body) |
| model, messages = text_chat_parts(body) |
| request = ConversationRequest(model=model, messages=messages) |
| return completion_response(model, collect_text(text_backend(), request), messages=messages) |
|
|