Samfy454 / main.py
Samfy001's picture
Update main.py
c99c9d3 verified
#!/usr/bin/env python3
"""
OpenAI-compatible API wrapper for v0.dev
Provides a drop-in replacement for OpenAI's API using v0.dev as the backend
"""
import os
import json
import asyncio
from typing import List, Dict, Any, Optional, AsyncGenerator
from datetime import datetime
import uuid
import httpx
from fastapi import FastAPI, HTTPException, Depends, Request
from fastapi.responses import StreamingResponse
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from dotenv import load_dotenv
load_dotenv()
# Configuration
V0_API_KEY = os.getenv("V0_API_KEY", "v1:348jgN2Fu5eebFqZDMgEr0qm:u7FitMJwSu8Vi0AhUOgjlo7p")
V0_API_BASE_URL = os.getenv("V0_API_BASE_URL", "https://api.v0.dev/v1")
HOST = os.getenv("HOST", "0.0.0.0")
PORT = int(os.getenv("PORT", 8000))
# FastAPI app
app = FastAPI(
title="v0.dev OpenAI Compatible API",
description="Drop-in replacement for OpenAI API using v0.dev as backend",
version="1.0.0"
)
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Pydantic models
class Message(BaseModel):
role: str = Field(..., description="Role of the message sender (user, assistant, system)")
content: str = Field(..., description="Content of the message")
class ChatCompletionRequest(BaseModel):
model: str = Field(..., description="Model to use for completion")
messages: List[Message] = Field(..., description="List of messages")
max_tokens: Optional[int] = Field(None, description="Maximum tokens to generate")
temperature: Optional[float] = Field(0.7, description="Sampling temperature")
stream: Optional[bool] = Field(False, description="Whether to stream the response")
project_id: Optional[str] = Field(None, description="v0.dev project ID")
class Choice(BaseModel):
index: int
message: Message
finish_reason: str
class Usage(BaseModel):
prompt_tokens: int
completion_tokens: int
total_tokens: int
class ChatCompletionResponse(BaseModel):
id: str
object: str = "chat.completion"
created: int
model: str
choices: List[Choice]
usage: Usage
class ChatCompletionStreamResponse(BaseModel):
id: str
object: str = "chat.completion.chunk"
created: int
model: str
choices: List[Dict[str, Any]]
# v0.dev API client
class V0APIClient:
def __init__(self, api_key: str, base_url: str):
self.api_key = api_key
self.base_url = base_url
self.headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
async def create_chat(self, messages: List[Message], model_config: Dict[str, Any], project_id: Optional[str] = None) -> Dict[str, Any]:
"""Create a new chat with v0.dev"""
url = f"{self.base_url}/chats"
# Use the formatting function to properly format messages
formatted_messages = [
{"role": msg.role, "content": msg.content}
for msg in messages
]
# Extract system message and user messages using the formatting function
system_message = ""
user_messages = []
for msg in messages:
if msg.role == "system":
system_message = msg.content
else:
user_messages.append(msg)
# Use the last user message
if not user_messages:
raise HTTPException(status_code=400, detail="No user message found")
last_user_message = user_messages[-1].content
payload = {
"system": system_message,
"message": last_user_message,
"modelConfiguration": model_config,
"projectId": project_id
}
# Remove None values
payload = {k: v for k, v in payload.items() if v is not None}
async with httpx.AsyncClient() as client:
response = await client.post(url, headers=self.headers, json=payload)
response.raise_for_status()
return response.json()
# Initialize v0 client
v0_client = V0APIClient(V0_API_KEY, V0_API_BASE_URL)
# Helper functions
def format_prompt(messages: List[Dict[str, Any]], add_special_tokens: bool = False,
do_continue: bool = False, include_system: bool = True) -> str:
"""
Format a series of messages into a single string, optionally adding special tokens.
Args:
messages: A list of message dictionaries, each containing 'role' and 'content'.
add_special_tokens: Whether to add special formatting tokens.
do_continue: If True, don't add the final "Assistant:" prompt.
include_system: Whether to include system messages in the formatted output.
Returns:
A formatted string containing all messages.
"""
# Helper function to convert content to string
def to_string(value) -> str:
if isinstance(value, str):
return value
elif isinstance(value, dict):
if "text" in value:
return value.get("text", "")
return ""
elif isinstance(value, list):
return "".join([to_string(v) for v in value])
return str(value)
# If there's only one message and no special tokens needed, just return its content
if not add_special_tokens and len(messages) <= 1:
return to_string(messages[0]["content"])
# Filter and process messages
processed_messages = [
(message["role"], to_string(message["content"]))
for message in messages
if include_system or message.get("role") != "system"
]
# Format each message as "Role: Content"
formatted = "\n".join([
f'{role.capitalize()}: {content}'
for role, content in processed_messages
if content.strip()
])
# Add final prompt for assistant if needed
if do_continue:
return formatted
return f"{formatted}\nAssistant:"
def create_openai_response(v0_response: Dict[str, Any], model: str) -> ChatCompletionResponse:
"""Convert v0.dev response to OpenAI format"""
messages = v0_response.get("messages", [])
assistant_message = None
for msg in messages:
if msg.get("role") == "assistant":
assistant_message = msg
break
if not assistant_message:
raise HTTPException(status_code=500, detail="No assistant message found in v0 response")
# Generate a unique ID
response_id = f"chatcmpl-{uuid.uuid4().hex}"
created = int(datetime.now().timestamp())
# Create choices
choice = Choice(
index=0,
message=Message(
role="assistant",
content=assistant_message.get("content", "")
),
finish_reason="stop"
)
# Create usage (estimated)
content = assistant_message.get("content", "")
usage = Usage(
prompt_tokens=len(str(v0_response)),
completion_tokens=len(content),
total_tokens=len(str(v0_response)) + len(content)
)
return ChatCompletionResponse(
id=response_id,
created=created,
model=model,
choices=[choice],
usage=usage
)
async def create_streaming_response(v0_response: Dict[str, Any], model: str) -> AsyncGenerator[str, None]:
"""Create streaming response in OpenAI format"""
response_id = f"chatcmpl-{uuid.uuid4().hex}"
created = int(datetime.now().timestamp())
messages = v0_response.get("messages", [])
assistant_message = ""
for msg in messages:
if msg.get("role") == "assistant":
assistant_message = msg.get("content", "")
break
# Simulate streaming by breaking the response into chunks
words = assistant_message.split()
# Send initial response
initial_chunk = {
'id': response_id,
'object': 'chat.completion.chunk',
'created': created,
'model': model,
'choices': [{
'index': 0,
'delta': {'role': 'assistant'},
'finish_reason': None
}]
}
yield f"data: {json.dumps(initial_chunk)}\n\n"
# Send content in chunks
current_text = ""
for word in words:
current_text += word + " "
content_chunk = {
'id': response_id,
'object': 'chat.completion.chunk',
'created': created,
'model': model,
'choices': [{
'index': 0,
'delta': {'content': word + " "},
'finish_reason': None
}]
}
yield f"data: {json.dumps(content_chunk)}\n\n"
await asyncio.sleep(0.01) # Small delay for streaming effect
# Send final response
final_chunk = {
'id': response_id,
'object': 'chat.completion.chunk',
'created': created,
'model': model,
'choices': [{
'index': 0,
'delta': {},
'finish_reason': 'stop'
}]
}
yield f"data: {json.dumps(final_chunk)}\n\n"
yield "data: [DONE]\n\n"
# API endpoints
@app.get("/")
async def root():
return {"message": "v0.dev OpenAI Compatible API", "version": "1.0.0"}
@app.get("/v1/models")
async def list_models():
"""List available models (mock OpenAI format)"""
return {
"object": "list",
"data": [
{
"id": "v0-gpt-5",
"object": "model",
"created": int(datetime.now().timestamp()),
"owned_by": "v0.dev"
},
{
"id": "v0-gpt-4",
"object": "model",
"created": int(datetime.now().timestamp()),
"owned_by": "v0.dev"
}
]
}
@app.post("/v1/chat/completions")
async def create_chat_completion(request: ChatCompletionRequest):
"""Create chat completion (OpenAI compatible)"""
try:
# Map OpenAI model to v0.dev model
model_config = {
"modelId": request.model,
"imageGenerations": True,
"thinking": True
}
# Create chat with v0.dev
v0_response = await v0_client.create_chat(
messages=request.messages,
model_config=model_config,
project_id=request.project_id
)
if request.stream:
return StreamingResponse(
create_streaming_response(v0_response, request.model),
media_type="text/plain"
)
else:
return create_openai_response(v0_response, request.model)
except httpx.HTTPStatusError as e:
raise HTTPException(status_code=e.response.status_code, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/health")
async def health_check():
return {"status": "healthy", "timestamp": datetime.now().isoformat()}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host=HOST, port=PORT)