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)