1126 lines
32 KiB
Go
1126 lines
32 KiB
Go
package proxy
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/freeflowuniverse/heroagent/pkg/jobsmanager"
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/gofiber/fiber/v2/middleware/logger"
|
|
)
|
|
|
|
// Server represents the OpenAI proxy server
|
|
type Server struct {
|
|
Factory *Factory
|
|
App *fiber.App
|
|
}
|
|
|
|
// NewServer creates a new proxy server with the given factory
|
|
func NewServer(factory *Factory) *Server {
|
|
app := fiber.New(fiber.Config{
|
|
ReadTimeout: 60 * time.Second,
|
|
WriteTimeout: 60 * time.Second,
|
|
BodyLimit: 10 * 1024 * 1024, // 10MB
|
|
})
|
|
|
|
// Add middleware
|
|
app.Use(logger.New())
|
|
|
|
server := &Server{
|
|
Factory: factory,
|
|
App: app,
|
|
}
|
|
|
|
// Setup routes
|
|
server.setupRoutes()
|
|
|
|
return server
|
|
}
|
|
|
|
// setupRoutes configures all the API routes for the proxy server
|
|
func (s *Server) setupRoutes() {
|
|
// API Routes
|
|
api := s.App.Group("/ai")
|
|
|
|
// Models endpoints
|
|
api.Get("/models", s.handleModels)
|
|
api.Get("/models/:model", s.handleGetModel)
|
|
|
|
// Chat completions endpoint
|
|
api.Post("/chat/completions", s.handleChatCompletions)
|
|
|
|
// Completions endpoint
|
|
api.Post("/completions", s.handleCompletions)
|
|
|
|
// Embeddings endpoint
|
|
api.Post("/embeddings", s.handleEmbeddings)
|
|
|
|
// Images endpoints
|
|
api.Post("/images/generations", s.handleImagesGenerations)
|
|
api.Post("/images/edits", s.handleImagesEdits)
|
|
api.Post("/images/variations", s.handleImagesVariations)
|
|
|
|
// Files endpoints
|
|
api.Get("/files", s.handleListFiles)
|
|
api.Post("/files", s.handleUploadFile)
|
|
api.Get("/files/:file_id", s.handleGetFile)
|
|
api.Delete("/files/:file_id", s.handleDeleteFile)
|
|
}
|
|
|
|
// Start starts the proxy server on the configured port
|
|
func (s *Server) Start() error {
|
|
return s.App.Listen(fmt.Sprintf(":%d", s.Factory.Config.Port))
|
|
}
|
|
|
|
// extractAPIKey extracts the API key from the Authorization header
|
|
func extractAPIKey(c *fiber.Ctx) string {
|
|
auth := c.Get("Authorization")
|
|
if auth == "" {
|
|
return "" //TODO: give error, needs to be set
|
|
}
|
|
|
|
//TODO: why we have following???
|
|
parts := strings.SplitN(auth, " ", 2)
|
|
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
|
return ""
|
|
}
|
|
|
|
return parts[1]
|
|
}
|
|
|
|
// registerUserIfNeeded registers a new user if the API key is not already registered
|
|
func (s *Server) registerUserIfNeeded(apiKey string) {
|
|
// Check if the user already exists
|
|
_, err := s.Factory.GetUserConfig(apiKey)
|
|
if err == nil {
|
|
// User already exists, nothing to do
|
|
return
|
|
}
|
|
|
|
// Create a new user configuration with default settings
|
|
userConfig := UserConfig{
|
|
Budget: 10000, // Default budget
|
|
ModelGroups: []string{"all"}, // Default access to all models
|
|
OpenAIKey: "", // Empty OpenAI key means we'll use the server's default key
|
|
}
|
|
|
|
// Add the user to the factory
|
|
s.Factory.AddUserConfig(apiKey, userConfig)
|
|
}
|
|
|
|
// createJob creates a new job for tracking the request
|
|
func createJob(c *fiber.Ctx, apiKey string, endpoint string, requestBody interface{}) (*jobsmanager.Job, error) {
|
|
// Create a new job
|
|
job := jobsmanager.NewJob()
|
|
job.ParamsType = jobsmanager.ParamsTypeAI
|
|
job.Topic = "openai-proxy"
|
|
job.CircleID = "ai"
|
|
|
|
// Serialize request body to JSON
|
|
paramsBytes, err := json.Marshal(map[string]interface{}{
|
|
"endpoint": endpoint,
|
|
"api_key": apiKey,
|
|
"request": requestBody,
|
|
"client_ip": c.IP(),
|
|
"user_agent": c.Get("User-Agent"),
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error serializing request: %w", err)
|
|
}
|
|
|
|
job.Params = string(paramsBytes)
|
|
job.Status = jobsmanager.JobStatusActive
|
|
job.TimeStart = time.Now().Unix()
|
|
|
|
// Save the job to store it
|
|
if err := job.Save(); err != nil {
|
|
return nil, fmt.Errorf("error saving job: %w", err)
|
|
}
|
|
|
|
return job, nil
|
|
}
|
|
|
|
// finishJob updates the job with the result or error
|
|
func finishJob(job *jobsmanager.Job, result interface{}, err error) {
|
|
if err != nil {
|
|
job.Finish(jobsmanager.JobStatusError, "", err)
|
|
return
|
|
}
|
|
|
|
// Serialize the result to JSON
|
|
resultBytes, jsonErr := json.Marshal(result)
|
|
if jsonErr != nil {
|
|
job.Finish(jobsmanager.JobStatusError, "", jsonErr)
|
|
return
|
|
}
|
|
|
|
job.Finish(jobsmanager.JobStatusDone, string(resultBytes), nil)
|
|
}
|
|
|
|
// handleModels handles GET /v1/models
|
|
func (s *Server) handleModels(c *fiber.Ctx) error {
|
|
apiKey := extractAPIKey(c)
|
|
if apiKey == "" {
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
|
"error": "missing API key",
|
|
})
|
|
}
|
|
|
|
// Create a job to track this request
|
|
job, err := createJob(c, apiKey, "models.list", nil)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error creating job: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Forward request to OpenAI
|
|
url := s.Factory.Config.OpenAIBaseURL + "/v1/models"
|
|
if url == "/v1/models" {
|
|
url = "https://api.openai.com/v1/models"
|
|
}
|
|
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "failed to create request: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Set headers from original request
|
|
req.Header.Set("Authorization", "Bearer "+s.Factory.GetOpenAIKey(apiKey))
|
|
|
|
// Perform the request
|
|
httpClient := &http.Client{}
|
|
resp, err := httpClient.Do(req)
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error from OpenAI: " + err.Error(),
|
|
})
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Parse response
|
|
var result map[string]interface{}
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
finishJob(job, nil, err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error parsing OpenAI response: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Finish the job with the result
|
|
finishJob(job, result, nil)
|
|
|
|
return c.Status(resp.StatusCode).JSON(result)
|
|
}
|
|
|
|
// handleGetModel handles GET /v1/models/:model
|
|
func (s *Server) handleGetModel(c *fiber.Ctx) error {
|
|
apiKey := extractAPIKey(c)
|
|
if apiKey == "" {
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
|
"error": "missing API key",
|
|
})
|
|
}
|
|
|
|
// Register the user if they don't exist yet
|
|
s.registerUserIfNeeded(apiKey)
|
|
|
|
modelID := c.Params("model")
|
|
|
|
// Create a job to track this request
|
|
job, err := createJob(c, apiKey, "models.get", map[string]string{
|
|
"model_id": modelID,
|
|
})
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error creating job: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Forward request to OpenAI
|
|
url := s.Factory.Config.OpenAIBaseURL + "/v1/models/" + modelID
|
|
if strings.HasPrefix(url, "/v1/models/") {
|
|
url = "https://api.openai.com/v1/models/" + modelID
|
|
}
|
|
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "failed to create request: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Set headers from original request
|
|
req.Header.Set("Authorization", "Bearer "+s.Factory.GetOpenAIKey(apiKey))
|
|
|
|
// Perform the request
|
|
httpClient := &http.Client{}
|
|
resp, err := httpClient.Do(req)
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error from OpenAI: " + err.Error(),
|
|
})
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Parse response
|
|
var result map[string]interface{}
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
finishJob(job, nil, err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error parsing OpenAI response: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Finish the job with the result
|
|
finishJob(job, result, nil)
|
|
|
|
return c.Status(resp.StatusCode).JSON(result)
|
|
}
|
|
|
|
// handleChatCompletions handles POST /v1/chat/completions
|
|
func (s *Server) handleChatCompletions(c *fiber.Ctx) error {
|
|
apiKey := extractAPIKey(c)
|
|
if apiKey == "" {
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
|
"error": "missing API key",
|
|
})
|
|
}
|
|
|
|
// Register the user if they don't exist yet
|
|
s.registerUserIfNeeded(apiKey)
|
|
|
|
// Log the raw request body for debugging
|
|
rawBody := c.Body()
|
|
log.Printf("Received chat completions request with API key: %s", apiKey)
|
|
log.Printf("Raw request body: %s", string(rawBody))
|
|
|
|
// Parse the request body
|
|
var requestBody map[string]interface{}
|
|
if err := json.Unmarshal(rawBody, &requestBody); err != nil {
|
|
log.Printf("Error parsing request body: %v", err)
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
|
"error": "invalid request body: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Extract model from request
|
|
model, _ := requestBody["model"].(string)
|
|
|
|
// Check user budget and model access
|
|
if !s.Factory.CanAccessModel(apiKey, model) {
|
|
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
|
"error": "access to this model is not allowed for your API key",
|
|
})
|
|
}
|
|
|
|
// Create a job to track this request
|
|
job, err := createJob(c, apiKey, "chat.completions", requestBody)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error creating job: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Forward request to OpenAI
|
|
url := s.Factory.Config.OpenAIBaseURL + "/v1/chat/completions"
|
|
if url == "/v1/chat/completions" {
|
|
url = "https://api.openai.com/v1/chat/completions"
|
|
}
|
|
|
|
// Convert the request body back to JSON
|
|
jsonBody, err := json.Marshal(requestBody)
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "failed to marshal request body: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
log.Printf("Forwarding request to OpenAI at URL: %s", url)
|
|
log.Printf("Request body being sent to OpenAI: %s", string(jsonBody))
|
|
|
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBody))
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "failed to create request: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Set headers from original request
|
|
req.Header.Set("Content-Type", "application/json")
|
|
openaiKey := s.Factory.GetOpenAIKey(apiKey)
|
|
req.Header.Set("Authorization", "Bearer "+openaiKey)
|
|
log.Printf("Using OpenAI key: %s (first 5 chars)", openaiKey[:5])
|
|
|
|
// Perform the request
|
|
httpClient := &http.Client{}
|
|
resp, err := httpClient.Do(req)
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
log.Printf("Error from OpenAI: %v", err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error from OpenAI: " + err.Error(),
|
|
})
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Read the response body
|
|
respBody, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
log.Printf("Error reading response body: %v", err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error reading OpenAI response: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
log.Printf("Response from OpenAI (status %d): %s", resp.StatusCode, string(respBody))
|
|
|
|
// Parse response
|
|
var result map[string]interface{}
|
|
if err := json.Unmarshal(respBody, &result); err != nil {
|
|
finishJob(job, nil, err)
|
|
log.Printf("Error parsing OpenAI response: %v", err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error parsing OpenAI response: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Finish the job with the result
|
|
finishJob(job, result, nil)
|
|
|
|
// Update user budget based on usage if available
|
|
if usage, ok := result["usage"].(map[string]interface{}); ok {
|
|
if totalTokens, ok := usage["total_tokens"].(float64); ok {
|
|
// For simplicity, we're using 1 token = 1 budget unit
|
|
_ = s.Factory.DecreaseBudget(apiKey, uint32(totalTokens))
|
|
}
|
|
}
|
|
|
|
return c.Status(resp.StatusCode).JSON(result)
|
|
}
|
|
|
|
// handleCompletions handles POST /v1/completions
|
|
func (s *Server) handleCompletions(c *fiber.Ctx) error {
|
|
apiKey := extractAPIKey(c)
|
|
if apiKey == "" {
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
|
"error": "missing API key",
|
|
})
|
|
}
|
|
|
|
// Register the user if they don't exist yet
|
|
s.registerUserIfNeeded(apiKey)
|
|
|
|
// Log the raw request body for debugging
|
|
rawBody := c.Body()
|
|
log.Printf("Received completions request with API key: %s", apiKey)
|
|
log.Printf("Raw request body: %s", string(rawBody))
|
|
|
|
// Parse the request body
|
|
var requestBody map[string]interface{}
|
|
if err := json.Unmarshal(rawBody, &requestBody); err != nil {
|
|
log.Printf("Error parsing request body: %v", err)
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
|
"error": "invalid request body: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Extract model from request
|
|
model, _ := requestBody["model"].(string)
|
|
|
|
// Check user budget and model access
|
|
if !s.Factory.CanAccessModel(apiKey, model) {
|
|
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
|
"error": "access to this model is not allowed for your API key",
|
|
})
|
|
}
|
|
|
|
// Create a job to track this request
|
|
job, err := createJob(c, apiKey, "completions", requestBody)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error creating job: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Forward request to OpenAI
|
|
url := s.Factory.Config.OpenAIBaseURL + "/v1/completions"
|
|
if url == "/v1/completions" {
|
|
url = "https://api.openai.com/v1/completions"
|
|
}
|
|
|
|
// Convert the request body back to JSON
|
|
jsonBody, err := json.Marshal(requestBody)
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "failed to marshal request body: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
log.Printf("Forwarding completions request to OpenAI at URL: %s", url)
|
|
log.Printf("Request body being sent to OpenAI: %s", string(jsonBody))
|
|
|
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBody))
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "failed to create request: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Set headers from original request
|
|
req.Header.Set("Content-Type", "application/json")
|
|
openaiKey := s.Factory.GetOpenAIKey(apiKey)
|
|
req.Header.Set("Authorization", "Bearer "+openaiKey)
|
|
log.Printf("Using OpenAI key: %s (first 5 chars)", openaiKey[:5])
|
|
|
|
// Perform the request
|
|
httpClient := &http.Client{}
|
|
resp, err := httpClient.Do(req)
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
log.Printf("Error from OpenAI: %v", err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error from OpenAI: " + err.Error(),
|
|
})
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Read the response body
|
|
respBody, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
log.Printf("Error reading response body: %v", err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error reading OpenAI response: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
log.Printf("Response from OpenAI (status %d): %s", resp.StatusCode, string(respBody))
|
|
|
|
// Parse response
|
|
var result map[string]interface{}
|
|
if err := json.Unmarshal(respBody, &result); err != nil {
|
|
finishJob(job, nil, err)
|
|
log.Printf("Error parsing OpenAI response: %v", err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error parsing OpenAI response: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Finish the job with the result
|
|
finishJob(job, result, nil)
|
|
|
|
// Update user budget based on usage if available
|
|
if usage, ok := result["usage"].(map[string]interface{}); ok {
|
|
if totalTokens, ok := usage["total_tokens"].(float64); ok {
|
|
// For simplicity, we're using 1 token = 1 budget unit
|
|
_ = s.Factory.DecreaseBudget(apiKey, uint32(totalTokens))
|
|
}
|
|
}
|
|
|
|
return c.Status(resp.StatusCode).JSON(result)
|
|
}
|
|
|
|
// handleEmbeddings handles POST /v1/embeddings
|
|
func (s *Server) handleEmbeddings(c *fiber.Ctx) error {
|
|
apiKey := extractAPIKey(c)
|
|
if apiKey == "" {
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
|
"error": "missing API key",
|
|
})
|
|
}
|
|
|
|
// Register the user if they don't exist yet
|
|
s.registerUserIfNeeded(apiKey)
|
|
|
|
// Log the raw request body for debugging
|
|
rawBody := c.Body()
|
|
log.Printf("Received embeddings request with API key: %s", apiKey)
|
|
log.Printf("Raw request body: %s", string(rawBody))
|
|
|
|
// Parse the request body
|
|
var requestBody map[string]interface{}
|
|
if err := json.Unmarshal(rawBody, &requestBody); err != nil {
|
|
log.Printf("Error parsing request body: %v", err)
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
|
"error": "invalid request body: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Extract model from request
|
|
model, _ := requestBody["model"].(string)
|
|
|
|
// Check user budget and model access
|
|
if !s.Factory.CanAccessModel(apiKey, model) {
|
|
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
|
"error": "access to this model is not allowed for your API key",
|
|
})
|
|
}
|
|
|
|
// Create a job to track this request
|
|
job, err := createJob(c, apiKey, "embeddings", requestBody)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error creating job: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Forward request to OpenAI
|
|
url := s.Factory.Config.OpenAIBaseURL + "/v1/embeddings"
|
|
if url == "/v1/embeddings" {
|
|
url = "https://api.openai.com/v1/embeddings"
|
|
}
|
|
|
|
// Convert the request body back to JSON
|
|
jsonBody, err := json.Marshal(requestBody)
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "failed to marshal request body: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
log.Printf("Forwarding embeddings request to OpenAI at URL: %s", url)
|
|
log.Printf("Request body being sent to OpenAI: %s", string(jsonBody))
|
|
|
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBody))
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "failed to create request: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Set headers from original request
|
|
req.Header.Set("Content-Type", "application/json")
|
|
openaiKey := s.Factory.GetOpenAIKey(apiKey)
|
|
req.Header.Set("Authorization", "Bearer "+openaiKey)
|
|
log.Printf("Using OpenAI key: %s (first 5 chars)", openaiKey[:5])
|
|
|
|
// Perform the request
|
|
httpClient := &http.Client{}
|
|
resp, err := httpClient.Do(req)
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
log.Printf("Error from OpenAI: %v", err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error from OpenAI: " + err.Error(),
|
|
})
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Read the response body
|
|
respBody, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
log.Printf("Error reading response body: %v", err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error reading OpenAI response: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
log.Printf("Response from OpenAI (status %d): %s", resp.StatusCode, string(respBody))
|
|
|
|
// Parse response
|
|
var result map[string]interface{}
|
|
if err := json.Unmarshal(respBody, &result); err != nil {
|
|
finishJob(job, nil, err)
|
|
log.Printf("Error parsing OpenAI response: %v", err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error parsing OpenAI response: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Finish the job with the result
|
|
finishJob(job, result, nil)
|
|
|
|
// Update user budget based on usage if available
|
|
if usage, ok := result["usage"].(map[string]interface{}); ok {
|
|
if totalTokens, ok := usage["total_tokens"].(float64); ok {
|
|
// For simplicity, we're using 1 token = 1 budget unit
|
|
_ = s.Factory.DecreaseBudget(apiKey, uint32(totalTokens))
|
|
}
|
|
}
|
|
|
|
return c.Status(resp.StatusCode).JSON(result)
|
|
}
|
|
|
|
// handleImagesGenerations handles POST /v1/images/generations
|
|
func (s *Server) handleImagesGenerations(c *fiber.Ctx) error {
|
|
apiKey := extractAPIKey(c)
|
|
if apiKey == "" {
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
|
"error": "missing API key",
|
|
})
|
|
}
|
|
|
|
// Register the user if they don't exist yet
|
|
s.registerUserIfNeeded(apiKey)
|
|
|
|
// Log the raw request body for debugging
|
|
rawBody := c.Body()
|
|
log.Printf("Received images generations request with API key: %s", apiKey)
|
|
log.Printf("Raw request body: %s", string(rawBody))
|
|
|
|
// Parse the request body
|
|
var requestBody map[string]interface{}
|
|
if err := json.Unmarshal(rawBody, &requestBody); err != nil {
|
|
log.Printf("Error parsing request body: %v", err)
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
|
"error": "invalid request body: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Create a job to track this request
|
|
job, err := createJob(c, apiKey, "images.generations", requestBody)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error creating job: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Check budget for image generation
|
|
// For image generation we use a standard cost based on size and quantity
|
|
n := 1 // Default to 1 image
|
|
if nValue, ok := requestBody["n"].(float64); ok {
|
|
n = int(nValue)
|
|
}
|
|
|
|
costPerImage := uint32(1000) // Base cost - adjust as needed
|
|
totalCost := costPerImage * uint32(n)
|
|
|
|
userConfig, err := s.Factory.GetUserConfig(apiKey)
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
|
"error": "invalid API key",
|
|
})
|
|
}
|
|
|
|
if userConfig.Budget < totalCost {
|
|
finishJob(job, nil, fmt.Errorf("insufficient budget"))
|
|
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
|
"error": "insufficient budget for this request",
|
|
})
|
|
}
|
|
|
|
// Forward request to OpenAI
|
|
url := s.Factory.Config.OpenAIBaseURL + "/v1/images/generations"
|
|
if url == "/v1/images/generations" {
|
|
url = "https://api.openai.com/v1/images/generations"
|
|
}
|
|
|
|
// Convert the request body back to JSON
|
|
jsonBody, err := json.Marshal(requestBody)
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "failed to marshal request body: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
log.Printf("Forwarding images generations request to OpenAI at URL: %s", url)
|
|
log.Printf("Request body being sent to OpenAI: %s", string(jsonBody))
|
|
|
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBody))
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "failed to create request: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Set headers from original request
|
|
req.Header.Set("Content-Type", "application/json")
|
|
openaiKey := s.Factory.GetOpenAIKey(apiKey)
|
|
req.Header.Set("Authorization", "Bearer "+openaiKey)
|
|
log.Printf("Using OpenAI key: %s (first 5 chars)", openaiKey[:5])
|
|
|
|
// Perform the request
|
|
httpClient := &http.Client{}
|
|
resp, err := httpClient.Do(req)
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
log.Printf("Error from OpenAI: %v", err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error from OpenAI: " + err.Error(),
|
|
})
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Read the response body
|
|
respBody, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
log.Printf("Error reading response body: %v", err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error reading OpenAI response: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
log.Printf("Response from OpenAI (status %d): %s", resp.StatusCode, string(respBody))
|
|
|
|
// Parse response
|
|
var result map[string]interface{}
|
|
if err := json.Unmarshal(respBody, &result); err != nil {
|
|
finishJob(job, nil, err)
|
|
log.Printf("Error parsing OpenAI response: %v", err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error parsing OpenAI response: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Finish the job with the result
|
|
finishJob(job, result, nil)
|
|
|
|
// Deduct the cost if successful
|
|
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
|
_ = s.Factory.DecreaseBudget(apiKey, totalCost)
|
|
}
|
|
|
|
return c.Status(resp.StatusCode).JSON(result)
|
|
}
|
|
|
|
// handleImagesEdits handles POST /v1/images/edits
|
|
func (s *Server) handleImagesEdits(c *fiber.Ctx) error {
|
|
apiKey := extractAPIKey(c)
|
|
if apiKey == "" {
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
|
"error": "missing API key",
|
|
})
|
|
}
|
|
|
|
// Register the user if they don't exist yet
|
|
s.registerUserIfNeeded(apiKey)
|
|
|
|
// Create a job to track this request
|
|
job, err := createJob(c, apiKey, "images.edits", map[string]string{
|
|
"info": "multipart form data - see raw request",
|
|
})
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error creating job: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// For this example, we'll charge a fixed amount
|
|
costPerEdit := uint32(2000) // Base cost - adjust as needed
|
|
|
|
userConfig, err := s.Factory.GetUserConfig(apiKey)
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
|
"error": "invalid API key",
|
|
})
|
|
}
|
|
|
|
if userConfig.Budget < costPerEdit {
|
|
finishJob(job, nil, fmt.Errorf("insufficient budget"))
|
|
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
|
"error": "insufficient budget for this request",
|
|
})
|
|
}
|
|
|
|
// For multipart requests, we need to handle them differently
|
|
// This is a simplified implementation
|
|
finishJob(job, map[string]string{"status": "processed"}, nil)
|
|
|
|
return c.Status(fiber.StatusNotImplemented).JSON(fiber.Map{
|
|
"error": "image edits endpoint not fully implemented in this proxy",
|
|
})
|
|
}
|
|
|
|
// handleImagesVariations handles POST /v1/images/variations
|
|
func (s *Server) handleImagesVariations(c *fiber.Ctx) error {
|
|
apiKey := extractAPIKey(c)
|
|
if apiKey == "" {
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
|
"error": "missing API key",
|
|
})
|
|
}
|
|
|
|
// Register the user if they don't exist yet
|
|
s.registerUserIfNeeded(apiKey)
|
|
|
|
// Create a job to track this request
|
|
job, err := createJob(c, apiKey, "images.variations", map[string]string{
|
|
"info": "multipart form data - see raw request",
|
|
})
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error creating job: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// For this example, we'll charge a fixed amount
|
|
costPerVariation := uint32(1500) // Base cost - adjust as needed
|
|
|
|
userConfig, err := s.Factory.GetUserConfig(apiKey)
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
|
"error": "invalid API key",
|
|
})
|
|
}
|
|
|
|
if userConfig.Budget < costPerVariation {
|
|
finishJob(job, nil, fmt.Errorf("insufficient budget"))
|
|
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
|
"error": "insufficient budget for this request",
|
|
})
|
|
}
|
|
|
|
// For multipart requests, we need to handle them differently
|
|
// This is a simplified implementation
|
|
finishJob(job, map[string]string{"status": "processed"}, nil)
|
|
|
|
return c.Status(fiber.StatusNotImplemented).JSON(fiber.Map{
|
|
"error": "image variations endpoint not fully implemented in this proxy",
|
|
})
|
|
}
|
|
|
|
// handleListFiles handles GET /v1/files
|
|
func (s *Server) handleListFiles(c *fiber.Ctx) error {
|
|
apiKey := extractAPIKey(c)
|
|
if apiKey == "" {
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
|
"error": "missing API key",
|
|
})
|
|
}
|
|
|
|
// Register the user if they don't exist yet
|
|
s.registerUserIfNeeded(apiKey)
|
|
|
|
// Create a job to track this request
|
|
job, err := createJob(c, apiKey, "files.list", nil)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error creating job: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Forward request to OpenAI
|
|
url := s.Factory.Config.OpenAIBaseURL + "/v1/files"
|
|
if url == "/v1/files" {
|
|
url = "https://api.openai.com/v1/files"
|
|
}
|
|
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "failed to create request: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Set headers from original request
|
|
req.Header.Set("Authorization", "Bearer "+s.Factory.GetOpenAIKey(apiKey))
|
|
|
|
// Perform the request
|
|
httpClient := &http.Client{}
|
|
resp, err := httpClient.Do(req)
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error from OpenAI: " + err.Error(),
|
|
})
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Parse response
|
|
var result map[string]interface{}
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
finishJob(job, nil, err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error parsing OpenAI response: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Finish the job with the result
|
|
finishJob(job, result, nil)
|
|
|
|
return c.Status(resp.StatusCode).JSON(result)
|
|
}
|
|
|
|
// handleUploadFile handles POST /v1/files
|
|
func (s *Server) handleUploadFile(c *fiber.Ctx) error {
|
|
apiKey := extractAPIKey(c)
|
|
if apiKey == "" {
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
|
"error": "missing API key",
|
|
})
|
|
}
|
|
|
|
// Register the user if they don't exist yet
|
|
s.registerUserIfNeeded(apiKey)
|
|
|
|
// Create a job to track this request
|
|
job, err := createJob(c, apiKey, "files.upload", map[string]string{
|
|
"info": "multipart form data - see raw request",
|
|
})
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error creating job: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// For multipart requests, we need to handle them differently
|
|
// This is a simplified implementation
|
|
finishJob(job, map[string]string{"status": "processed"}, nil)
|
|
|
|
return c.Status(fiber.StatusNotImplemented).JSON(fiber.Map{
|
|
"error": "file upload endpoint not fully implemented in this proxy",
|
|
})
|
|
}
|
|
|
|
// handleGetFile handles GET /v1/files/:file_id
|
|
func (s *Server) handleGetFile(c *fiber.Ctx) error {
|
|
apiKey := extractAPIKey(c)
|
|
if apiKey == "" {
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
|
"error": "missing API key",
|
|
})
|
|
}
|
|
|
|
// Register the user if they don't exist yet
|
|
s.registerUserIfNeeded(apiKey)
|
|
|
|
fileID := c.Params("file_id")
|
|
|
|
// Create a job to track this request
|
|
job, err := createJob(c, apiKey, "files.get", map[string]string{
|
|
"file_id": fileID,
|
|
})
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error creating job: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Forward request to OpenAI
|
|
url := s.Factory.Config.OpenAIBaseURL + "/v1/files/" + fileID
|
|
if strings.HasPrefix(url, "/v1/files/") {
|
|
url = "https://api.openai.com/v1/files/" + fileID
|
|
}
|
|
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "failed to create request: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Set headers from original request
|
|
req.Header.Set("Authorization", "Bearer "+s.Factory.GetOpenAIKey(apiKey))
|
|
|
|
// Perform the request
|
|
httpClient := &http.Client{}
|
|
resp, err := httpClient.Do(req)
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error from OpenAI: " + err.Error(),
|
|
})
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Parse response
|
|
var result map[string]interface{}
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
finishJob(job, nil, err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error parsing OpenAI response: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Finish the job with the result
|
|
finishJob(job, result, nil)
|
|
|
|
return c.Status(resp.StatusCode).JSON(result)
|
|
}
|
|
|
|
// handleDeleteFile handles DELETE /v1/files/:file_id
|
|
func (s *Server) handleDeleteFile(c *fiber.Ctx) error {
|
|
apiKey := extractAPIKey(c)
|
|
if apiKey == "" {
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
|
"error": "missing API key",
|
|
})
|
|
}
|
|
|
|
// Register the user if they don't exist yet
|
|
s.registerUserIfNeeded(apiKey)
|
|
|
|
fileID := c.Params("file_id")
|
|
|
|
// Create a job to track this request
|
|
job, err := createJob(c, apiKey, "files.delete", map[string]string{
|
|
"file_id": fileID,
|
|
})
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error creating job: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Forward request to OpenAI
|
|
url := s.Factory.Config.OpenAIBaseURL + "/v1/files/" + fileID
|
|
if strings.HasPrefix(url, "/v1/files/") {
|
|
url = "https://api.openai.com/v1/files/" + fileID
|
|
}
|
|
|
|
req, err := http.NewRequest("DELETE", url, nil)
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "failed to create request: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Set headers from original request
|
|
req.Header.Set("Authorization", "Bearer "+s.Factory.GetOpenAIKey(apiKey))
|
|
|
|
// Perform the request
|
|
httpClient := &http.Client{}
|
|
resp, err := httpClient.Do(req)
|
|
if err != nil {
|
|
finishJob(job, nil, err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error from OpenAI: " + err.Error(),
|
|
})
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Parse response
|
|
var result map[string]interface{}
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
finishJob(job, nil, err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "error parsing OpenAI response: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Finish the job with the result
|
|
finishJob(job, result, nil)
|
|
|
|
return c.Status(resp.StatusCode).JSON(result)
|
|
}
|