|
|
package main
|
|
|
|
|
|
import (
|
|
|
"bytes"
|
|
|
"context"
|
|
|
"encoding/json"
|
|
|
"errors"
|
|
|
"fmt"
|
|
|
"io"
|
|
|
"log/slog"
|
|
|
"net/http"
|
|
|
"os"
|
|
|
"strings"
|
|
|
"time"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
var JulepApiBaseURL string
|
|
|
var apiClient *http.Client
|
|
|
|
|
|
const (
|
|
|
defaultTimeout = 30 * time.Second
|
|
|
chatTimeout = 90 * time.Second
|
|
|
)
|
|
|
|
|
|
func init() {
|
|
|
JulepApiBaseURL = os.Getenv("JULEP_API_BASE_URL")
|
|
|
if JulepApiBaseURL == "" {
|
|
|
slog.Error("Fatal: JULEP_API_BASE_URL environment variable not set.")
|
|
|
os.Exit(1)
|
|
|
}
|
|
|
JulepApiBaseURL = strings.TrimSuffix(JulepApiBaseURL, "/")
|
|
|
slog.Info("Julep API Base URL configured", "url", JulepApiBaseURL)
|
|
|
|
|
|
|
|
|
apiClient = &http.Client{
|
|
|
Timeout: defaultTimeout,
|
|
|
Transport: &http.Transport{
|
|
|
MaxIdleConns: 100,
|
|
|
MaxIdleConnsPerHost: 10,
|
|
|
IdleConnTimeout: 90 * time.Second,
|
|
|
},
|
|
|
}
|
|
|
slog.Info("HTTP client initialized")
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type CreateAgentPayload struct {
|
|
|
Name string `json:"name"`
|
|
|
About string `json:"about"`
|
|
|
|
|
|
}
|
|
|
|
|
|
type CreateSessionPayload struct {
|
|
|
AgentID string `json:"agent"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
type JulepMessage struct {
|
|
|
Role string `json:"role"`
|
|
|
Content string `json:"content"`
|
|
|
Name *string `json:"name,omitempty"`
|
|
|
ToolCallID *string `json:"tool_call_id,omitempty"`
|
|
|
ToolCalls []JulepToolCall `json:"tool_calls,omitempty"`
|
|
|
}
|
|
|
|
|
|
|
|
|
type JulepToolCall struct {
|
|
|
ID string `json:"id"`
|
|
|
Type string `json:"type"`
|
|
|
Function JulepFunction `json:"function,omitempty"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
type JulepFunction struct {
|
|
|
Name string `json:"name"`
|
|
|
Arguments string `json:"arguments"`
|
|
|
}
|
|
|
|
|
|
|
|
|
type JulepChatPayload struct {
|
|
|
Messages []JulepMessage `json:"messages"`
|
|
|
Model *string `json:"model,omitempty"`
|
|
|
Stream bool `json:"stream"`
|
|
|
MaxTokens *int `json:"max_tokens,omitempty"`
|
|
|
Temperature *float64 `json:"temperature,omitempty"`
|
|
|
TopP *float64 `json:"top_p,omitempty"`
|
|
|
Stop []string `json:"stop,omitempty"`
|
|
|
PresencePenalty *float64 `json:"presence_penalty,omitempty"`
|
|
|
FrequencyPenalty *float64 `json:"frequency_penalty,omitempty"`
|
|
|
Tools []OpenAITool `json:"tools,omitempty"`
|
|
|
ToolChoice any `json:"tool_choice,omitempty"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
type JulepChatResponse struct {
|
|
|
ID string `json:"id"`
|
|
|
CreatedAt time.Time `json:"created_at"`
|
|
|
Choices []JulepChoice `json:"choices"`
|
|
|
Usage *JulepUsage `json:"usage,omitempty"`
|
|
|
|
|
|
}
|
|
|
|
|
|
type JulepChoice struct {
|
|
|
Index int `json:"index"`
|
|
|
Message JulepMessage `json:"message"`
|
|
|
FinishReason string `json:"finish_reason"`
|
|
|
}
|
|
|
|
|
|
type JulepUsage struct {
|
|
|
PromptTokens int `json:"prompt_tokens"`
|
|
|
CompletionTokens int `json:"completion_tokens"`
|
|
|
TotalTokens int `json:"total_tokens"`
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func convertOpenaiToJulep(openaiReq OpenAIRequest) JulepChatPayload {
|
|
|
julepMessages := make([]JulepMessage, len(openaiReq.Messages))
|
|
|
for i, msg := range openaiReq.Messages {
|
|
|
julepToolCalls := make([]JulepToolCall, len(msg.ToolCalls))
|
|
|
for j, tc := range msg.ToolCalls {
|
|
|
julepToolCalls[j] = JulepToolCall{
|
|
|
ID: tc.ID,
|
|
|
Type: tc.Type,
|
|
|
Function: JulepFunction{
|
|
|
Name: tc.Function.Name,
|
|
|
Arguments: tc.Function.Arguments,
|
|
|
},
|
|
|
}
|
|
|
}
|
|
|
julepMessages[i] = JulepMessage{
|
|
|
Role: msg.Role,
|
|
|
Content: msg.Content,
|
|
|
Name: msg.Name,
|
|
|
ToolCallID: msg.ToolCallID,
|
|
|
ToolCalls: julepToolCalls,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
payload := JulepChatPayload{
|
|
|
Messages: julepMessages,
|
|
|
Model: &openaiReq.Model,
|
|
|
Stream: false,
|
|
|
MaxTokens: openaiReq.MaxTokens,
|
|
|
Temperature: openaiReq.Temperature,
|
|
|
TopP: openaiReq.TopP,
|
|
|
Stop: openaiReq.Stop,
|
|
|
PresencePenalty: openaiReq.PresencePenalty,
|
|
|
FrequencyPenalty: openaiReq.FrequencyPenalty,
|
|
|
Tools: openaiReq.Tools,
|
|
|
ToolChoice: openaiReq.ToolChoice,
|
|
|
}
|
|
|
|
|
|
if openaiReq.Model == "" {
|
|
|
payload.Model = nil
|
|
|
}
|
|
|
return payload
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func convertJulepToOpenai(julepResp *JulepChatResponse, modelName string, sessionID string) *OpenAIResponse {
|
|
|
openaiChoices := make([]OpenAIChoice, len(julepResp.Choices))
|
|
|
for i, choice := range julepResp.Choices {
|
|
|
openaiToolCalls := make([]OpenAIToolCall, len(choice.Message.ToolCalls))
|
|
|
for j, tc := range choice.Message.ToolCalls {
|
|
|
openaiToolCalls[j] = OpenAIToolCall{
|
|
|
ID: tc.ID,
|
|
|
Type: tc.Type,
|
|
|
Function: OpenAIFunction{
|
|
|
Name: tc.Function.Name,
|
|
|
Arguments: tc.Function.Arguments,
|
|
|
},
|
|
|
}
|
|
|
}
|
|
|
openaiChoices[i] = OpenAIChoice{
|
|
|
Index: choice.Index,
|
|
|
Message: OpenAIMessage{
|
|
|
Role: choice.Message.Role,
|
|
|
Content: choice.Message.Content,
|
|
|
ToolCalls: openaiToolCalls,
|
|
|
},
|
|
|
FinishReason: choice.FinishReason,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
var openaiUsage *OpenAIUsage
|
|
|
if julepResp.Usage != nil {
|
|
|
openaiUsage = &OpenAIUsage{
|
|
|
PromptTokens: julepResp.Usage.PromptTokens,
|
|
|
CompletionTokens: julepResp.Usage.CompletionTokens,
|
|
|
TotalTokens: julepResp.Usage.TotalTokens,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return &OpenAIResponse{
|
|
|
ID: sessionID,
|
|
|
Object: "chat.completion",
|
|
|
Created: julepResp.CreatedAt.Unix(),
|
|
|
Model: modelName,
|
|
|
Choices: openaiChoices,
|
|
|
Usage: openaiUsage,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func makeJulepRequest(ctx context.Context, logger *slog.Logger, method, url string, headers http.Header, requestBody any, responseTarget any, reqID string) (int, error) {
|
|
|
logAttrs := []any{"request_id", reqID, "method", method, "url", url}
|
|
|
logger.Debug("Making Julep API request...", logAttrs...)
|
|
|
|
|
|
var reqBodyReader io.Reader
|
|
|
if requestBody != nil {
|
|
|
jsonBody, err := json.Marshal(requestBody)
|
|
|
if err != nil {
|
|
|
logger.Error("Failed to marshal Julep request body", append(logAttrs, "error", err)...)
|
|
|
return 0, fmt.Errorf("failed to marshal request body: %w", err)
|
|
|
}
|
|
|
reqBodyReader = bytes.NewBuffer(jsonBody)
|
|
|
logAttrs = append(logAttrs, "body_size", len(jsonBody))
|
|
|
}
|
|
|
|
|
|
httpReq, err := http.NewRequestWithContext(ctx, method, url, reqBodyReader)
|
|
|
if err != nil {
|
|
|
logger.Error("Failed to create Julep HTTP request", append(logAttrs, "error", err)...)
|
|
|
return 0, fmt.Errorf("failed to create HTTP request: %w", err)
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if auth := headers.Get("Authorization"); auth != "" {
|
|
|
httpReq.Header.Set("Authorization", auth)
|
|
|
}
|
|
|
|
|
|
if requestBody != nil {
|
|
|
httpReq.Header.Set("Content-Type", "application/json")
|
|
|
}
|
|
|
|
|
|
|
|
|
startTime := time.Now()
|
|
|
httpResp, err := apiClient.Do(httpReq)
|
|
|
duration := time.Since(startTime)
|
|
|
logAttrs = append(logAttrs, "duration_ms", duration.Milliseconds())
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
if errors.Is(err, context.DeadlineExceeded) {
|
|
|
logger.Error("Julep API request timed out", append(logAttrs, "error", err)...)
|
|
|
return http.StatusGatewayTimeout, fmt.Errorf("request to %s timed out: %w", url, err)
|
|
|
}
|
|
|
logger.Error("Julep API request failed", append(logAttrs, "error", err)...)
|
|
|
return 0, fmt.Errorf("failed to send request to %s: %w", url, err)
|
|
|
}
|
|
|
defer httpResp.Body.Close()
|
|
|
|
|
|
logAttrs = append(logAttrs, "status_code", httpResp.StatusCode)
|
|
|
|
|
|
|
|
|
respBodyBytes, readErr := io.ReadAll(httpResp.Body)
|
|
|
if readErr != nil {
|
|
|
logger.Warn("Failed to read Julep response body", append(logAttrs, "read_error", readErr)...)
|
|
|
|
|
|
} else {
|
|
|
logAttrs = append(logAttrs, "response_size", len(respBodyBytes))
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
|
|
errMsg := fmt.Sprintf("Julep API returned error status %d", httpResp.StatusCode)
|
|
|
if len(respBodyBytes) > 0 {
|
|
|
errMsg = fmt.Sprintf("%s: %s", errMsg, string(respBodyBytes))
|
|
|
}
|
|
|
logger.Error("Julep API request returned non-2xx status", logAttrs...)
|
|
|
|
|
|
return httpResp.StatusCode, fmt.Errorf(errMsg)
|
|
|
}
|
|
|
|
|
|
|
|
|
if responseTarget != nil && len(respBodyBytes) > 0 {
|
|
|
if err := json.Unmarshal(respBodyBytes, responseTarget); err != nil {
|
|
|
logger.Error("Failed to unmarshal Julep response body", append(logAttrs, "error", err, "body_preview", string(respBodyBytes[:min(len(respBodyBytes), 100)]))...)
|
|
|
return httpResp.StatusCode, fmt.Errorf("failed to decode Julep response: %w", err)
|
|
|
}
|
|
|
logger.Debug("Successfully decoded Julep response", logAttrs...)
|
|
|
} else {
|
|
|
logger.Debug("Julep request successful, no response body expected or decoded.", logAttrs...)
|
|
|
}
|
|
|
|
|
|
return httpResp.StatusCode, nil
|
|
|
}
|
|
|
|
|
|
|
|
|
func callJulepChat(ctx context.Context, logger *slog.Logger, headers http.Header, openaiReq OpenAIRequest, requestID string) (*OpenAIResponse, int, error) {
|
|
|
reqLogger := logger.With("request_id", requestID)
|
|
|
|
|
|
|
|
|
agentID := generateUUID()
|
|
|
agentURL := fmt.Sprintf("%s/agents/%s", JulepApiBaseURL, agentID)
|
|
|
agentPayload := CreateAgentPayload{
|
|
|
Name: fmt.Sprintf("temp-agent-%s", agentID),
|
|
|
About: "Temporary agent created for a chat session via proxy.",
|
|
|
}
|
|
|
reqLogger.Info("Creating temporary Julep agent", "agent_id", agentID)
|
|
|
statusCode, err := makeJulepRequest(ctx, reqLogger, http.MethodPost, agentURL, headers, agentPayload, nil, requestID)
|
|
|
if err != nil {
|
|
|
reqLogger.Error("Failed to create Julep agent", "error", err, "status_code", statusCode)
|
|
|
|
|
|
if statusCode == 0 || statusCode >= 500 {
|
|
|
return nil, http.StatusBadGateway, fmt.Errorf("failed to initialize session (agent creation failed): %w", err)
|
|
|
}
|
|
|
return nil, statusCode, fmt.Errorf("failed to create agent: %w", err)
|
|
|
}
|
|
|
reqLogger.Info("Julep agent created successfully", "agent_id", agentID)
|
|
|
|
|
|
|
|
|
sessionID := generateUUID()
|
|
|
sessionURL := fmt.Sprintf("%s/sessions/%s", JulepApiBaseURL, sessionID)
|
|
|
sessionPayload := CreateSessionPayload{
|
|
|
AgentID: agentID,
|
|
|
}
|
|
|
reqLogger.Info("Creating temporary Julep session", "session_id", sessionID, "linked_agent_id", agentID)
|
|
|
statusCode, err = makeJulepRequest(ctx, reqLogger, http.MethodPost, sessionURL, headers, sessionPayload, nil, requestID)
|
|
|
if err != nil {
|
|
|
reqLogger.Error("Failed to create Julep session", "error", err, "status_code", statusCode)
|
|
|
|
|
|
if statusCode == 0 || statusCode >= 500 {
|
|
|
return nil, http.StatusBadGateway, fmt.Errorf("failed to initialize session (session creation failed): %w", err)
|
|
|
}
|
|
|
return nil, statusCode, fmt.Errorf("failed to create session: %w", err)
|
|
|
}
|
|
|
reqLogger.Info("Julep session created successfully", "session_id", sessionID)
|
|
|
|
|
|
|
|
|
chatURL := fmt.Sprintf("%s/sessions/%s/chat", JulepApiBaseURL, sessionID)
|
|
|
julepPayload := convertOpenaiToJulep(openaiReq)
|
|
|
reqLogger.Info("Calling Julep chat endpoint", "url", chatURL)
|
|
|
|
|
|
|
|
|
chatCtx := ctx
|
|
|
if _, ok := ctx.Deadline(); !ok {
|
|
|
var cancel context.CancelFunc
|
|
|
chatCtx, cancel = context.WithTimeout(context.Background(), chatTimeout)
|
|
|
defer cancel()
|
|
|
reqLogger.Debug("Applying specific timeout for chat request", "timeout", chatTimeout)
|
|
|
}
|
|
|
|
|
|
var julepResponse JulepChatResponse
|
|
|
statusCode, err = makeJulepRequest(chatCtx, reqLogger, http.MethodPost, chatURL, headers, julepPayload, &julepResponse, requestID)
|
|
|
if err != nil {
|
|
|
reqLogger.Error("Julep chat request failed", "error", err, "status_code", statusCode)
|
|
|
|
|
|
if statusCode == 0 || statusCode >= 500 || statusCode == http.StatusGatewayTimeout || statusCode == http.StatusServiceUnavailable {
|
|
|
return nil, http.StatusBadGateway, fmt.Errorf("upstream API error during chat: %w", err)
|
|
|
}
|
|
|
|
|
|
return nil, statusCode, fmt.Errorf("julep chat API error: %w", err)
|
|
|
}
|
|
|
reqLogger.Info("Julep chat request successful")
|
|
|
|
|
|
|
|
|
openaiResponse := convertJulepToOpenai(&julepResponse, openaiReq.Model, sessionID)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return openaiResponse, http.StatusOK, nil
|
|
|
}
|
|
|
|
|
|
|
|
|
func min(a, b int) int {
|
|
|
if a < b {
|
|
|
return a
|
|
|
}
|
|
|
return b
|
|
|
}
|
|
|
|