You've already forked ModuleLLM-OpenAI-Plugin
mirror of
https://github.com/m5stack/ModuleLLM-OpenAI-Plugin.git
synced 2026-05-20 11:37:26 -07:00
141 lines
5.1 KiB
Python
141 lines
5.1 KiB
Python
import uuid
|
|
import time
|
|
from .base_model_backend import BaseModelBackend
|
|
from .chat_schemas import ChatCompletionRequest, Message, ContentItem
|
|
from fastapi import HTTPException
|
|
from typing import List
|
|
|
|
class VisionModelBackend(BaseModelBackend):
|
|
MAX_IMAGE_SIZE = 4 * 1024 * 1024
|
|
IMAGE_TIMEOUT = 15
|
|
|
|
async def _process_image_content(self, content_item: ContentItem) -> dict:
|
|
if not content_item.image_url:
|
|
return None
|
|
|
|
url = content_item.image_url.get("url", "")
|
|
if url.startswith("data:image"):
|
|
return {
|
|
"type": "image_url",
|
|
"image_url": {"url": url}
|
|
}
|
|
|
|
# Download external image and convert to base64
|
|
base64_str = await self.download_image(
|
|
url,
|
|
max_size=self.MAX_IMAGE_SIZE,
|
|
timeout=self.IMAGE_TIMEOUT
|
|
)
|
|
if not base64_str:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Failed to load image: {url}"
|
|
)
|
|
|
|
return {
|
|
"type": "image_url",
|
|
"image_url": {
|
|
"url": f"data:image/jpeg;base64,{base64_str}"
|
|
}
|
|
}
|
|
|
|
async def _build_messages(self, messages: List[Message]):
|
|
processed_messages = []
|
|
|
|
for msg in messages:
|
|
content = msg.content
|
|
new_content = []
|
|
|
|
if isinstance(content, list):
|
|
for item in content:
|
|
if item.type == "text":
|
|
new_content.append({
|
|
"type": "text",
|
|
"text": item.text
|
|
})
|
|
elif item.type == "image_url":
|
|
image_content = await self._process_image_content(item)
|
|
if image_content:
|
|
new_content.append(image_content)
|
|
else:
|
|
new_content.append({
|
|
"type": "text",
|
|
"text": str(content)
|
|
})
|
|
|
|
processed_messages.append({
|
|
"role": msg.role,
|
|
"content": new_content
|
|
})
|
|
|
|
return processed_messages
|
|
|
|
async def generate(self, request: ChatCompletionRequest):
|
|
from openai import AsyncOpenAI
|
|
|
|
try:
|
|
client = AsyncOpenAI(
|
|
api_key=self.config["api_key"],
|
|
base_url=self.config["base_url"],
|
|
timeout=30.0
|
|
)
|
|
|
|
messages = await self._build_messages(request.messages)
|
|
|
|
response = await client.chat.completions.create(
|
|
model=self.config["model"],
|
|
messages=messages,
|
|
temperature=request.temperature,
|
|
max_tokens=request.max_tokens,
|
|
stream=request.stream
|
|
)
|
|
|
|
if request.stream:
|
|
async def stream_wrapper():
|
|
async for chunk in response:
|
|
# Unified error handling
|
|
if isinstance(chunk, dict) and "error" in chunk:
|
|
yield chunk
|
|
continue
|
|
|
|
# Convert to compatible format
|
|
yield {
|
|
"id": f"chatcmpl-{uuid.uuid4()}",
|
|
"object": "chat.completion.chunk",
|
|
"created": int(time.time()),
|
|
"model": request.model,
|
|
"choices": [{
|
|
"index": 0,
|
|
"delta": {
|
|
"content": chunk.choices[0].delta.content or "",
|
|
"role": "assistant"
|
|
},
|
|
"finish_reason": chunk.choices[0].finish_reason
|
|
}]
|
|
}
|
|
yield {"choices": [{"delta": {}, "finish_reason": "stop"}]}
|
|
return stream_wrapper()
|
|
|
|
# Add usage info for non-stream response
|
|
return {
|
|
"id": f"chatcmpl-{uuid.uuid4()}",
|
|
"object": "chat.completion",
|
|
"created": int(time.time()),
|
|
"model": request.model,
|
|
"choices": [{
|
|
"message": {
|
|
"role": "assistant",
|
|
"content": response.choices[0].message.content
|
|
}
|
|
}],
|
|
"usage": {
|
|
"prompt_tokens": response.usage.prompt_tokens if response.usage else 0,
|
|
"completion_tokens": response.usage.completion_tokens if response.usage else 0,
|
|
"total_tokens": response.usage.total_tokens if response.usage else 0
|
|
}
|
|
}
|
|
except Exception as e:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"Vision model error: {str(e)}"
|
|
) |