first commit

This commit is contained in:
Beyhan Ogur
2026-05-11 15:08:50 +03:00
commit a408821410
47 changed files with 4670 additions and 0 deletions
+73
View File
@@ -0,0 +1,73 @@
package logger
import (
"fmt"
"log"
"strings"
)
// Level represents the logging verbosity level.
type Level int
const (
LevelDebug Level = iota
LevelInfo
LevelWarn
)
var currentLevel = LevelInfo
// SetLevel sets the global log level from a string: "debug", "info", or "warn".
func SetLevel(s string) {
switch strings.ToLower(s) {
case "debug":
currentLevel = LevelDebug
case "info":
currentLevel = LevelInfo
case "warn":
currentLevel = LevelWarn
default:
currentLevel = LevelInfo
}
}
// Debug logs a message only when LOG_LEVEL=debug.
func Debug(format string, args ...interface{}) {
if currentLevel <= LevelDebug {
log.Printf(format, args...)
}
}
// Info logs a message when LOG_LEVEL is debug or info.
func Info(format string, args ...interface{}) {
if currentLevel <= LevelInfo {
log.Printf(format, args...)
}
}
// Warn logs a message at any log level (always visible).
func Warn(format string, args ...interface{}) {
if currentLevel <= LevelWarn {
log.Printf(format, args...)
}
}
// Fatal logs and exits. Always visible regardless of level.
func Fatal(format string, args ...interface{}) {
log.Fatalf(format, args...)
}
// formatKV formats key=value pairs for structured console output.
func formatKV(pairs ...interface{}) string {
if len(pairs)%2 != 0 {
return fmt.Sprint(pairs...)
}
var b strings.Builder
for i := 0; i < len(pairs); i += 2 {
if i > 0 {
b.WriteString(" | ")
}
b.WriteString(fmt.Sprintf("%v=%v", pairs[i], pairs[i+1]))
}
return b.String()
}
+103
View File
@@ -0,0 +1,103 @@
package proxy
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"strings"
"time"
"optoant/internal/logger"
)
// ForwardResult holds the result of a forwarded request.
type ForwardResult struct {
StatusCode int
Body []byte
Headers http.Header
}
// Forward proxies an HTTP request to targetURL, preserving method, headers,
// and body. Timeout is applied via context. Sensitive headers are forwarded
// but never logged.
func Forward(ctx context.Context, method, targetURL string, headers http.Header, body io.Reader, timeoutSec int) (*ForwardResult, error) {
if timeoutSec <= 0 {
timeoutSec = 30
}
ctx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSec)*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, method, targetURL, body)
if err != nil {
return nil, fmt.Errorf("proxy: build request: %w", err)
}
// Copy allowed headers — never modify; just forward as-is.
for key, vals := range headers {
lk := strings.ToLower(key)
// Skip hop-by-hop headers that would break the proxied connection.
switch lk {
case "connection", "te", "trailers", "transfer-encoding", "upgrade":
continue
}
for _, v := range vals {
req.Header.Add(key, v)
}
}
// Read body for logging (then re-wrap for actual request)
bodyBytes, _ := io.ReadAll(body)
if len(bodyBytes) > 0 {
req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
req.ContentLength = int64(len(bodyBytes))
}
logHeader := MaskSensitiveHeaders(req.Header)
logger.Debug("[PROXY] --> %s %s | Headers: %v | Body: %s",
method, targetURL, logHeader, truncateForLog(string(bodyBytes), 2000))
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
logger.Warn("[PROXY] <-- ERROR from %s: %v", targetURL, err)
return nil, fmt.Errorf("proxy: upstream error: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("proxy: read response body: %w", err)
}
logger.Debug("[PROXY] <-- RESPONSE %d from %s (%d bytes) | Body: %s",
resp.StatusCode, targetURL, len(respBody), truncateForLog(string(respBody), 2000))
return &ForwardResult{
StatusCode: resp.StatusCode,
Body: respBody,
Headers: resp.Header,
}, nil
}
// MaskSensitiveHeaders returns a copy of headers with Authorization values masked.
// Use this copy only for logging purposes.
func MaskSensitiveHeaders(h http.Header) http.Header {
masked := h.Clone()
if masked.Get("Authorization") != "" {
masked.Set("Authorization", "Bearer [REDACTED]")
}
if masked.Get("X-Api-Key") != "" {
masked.Set("X-Api-Key", "[REDACTED]")
}
return masked
}
func truncateForLog(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen] + "...[TRUNCATED]"
}
+345
View File
@@ -0,0 +1,345 @@
package transform
import (
"encoding/json"
"fmt"
"log"
"strings"
)
// AnthropicRequest represents a standard Anthropic Messages API request.
type AnthropicRequest struct {
Model string `json:"model"`
MaxTokens int `json:"max_tokens"`
Messages []AnthropicMessage `json:"messages"`
System interface{} `json:"system,omitempty"`
Stream bool `json:"stream,omitempty"`
}
// AnthropicMessage is a single message in Anthropic format.
// Content can be a plain string or an array of content blocks.
type AnthropicMessage struct {
Role string `json:"role"`
Content interface{} `json:"content"`
}
// extractTextContent converts the message content (string or array) to a plain string.
func extractTextContent(content interface{}) string {
if content == nil {
return ""
}
switch v := content.(type) {
case string:
return v
case []interface{}:
var parts []string
for _, block := range v {
if b, ok := block.(map[string]interface{}); ok {
if t, ok := b["text"].(string); ok {
parts = append(parts, t)
}
}
}
return strings.Join(parts, "\n")
default:
return fmt.Sprintf("%v", v)
}
}
// BifrostRequest is the OpenAI-compatible format that Bifrost expects.
type BifrostRequest struct {
Model string `json:"model"`
Messages []BifrostMessage `json:"messages"`
MaxTokens int `json:"max_tokens,omitempty"`
Stream bool `json:"stream,omitempty"`
}
// BifrostMessage mirrors OpenAI chat message format.
type BifrostMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}
// BifrostResponse is the OpenAI-compatible response from Bifrost.
type BifrostResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Usage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
} `json:"usage"`
Choices []struct {
Message struct {
Role string `json:"role"`
Content string `json:"content"`
Reasoning string `json:"reasoning"`
} `json:"message"`
FinishReason string `json:"finish_reason"`
Index int `json:"index"`
} `json:"choices"`
}
// getContent returns the message content, falling back to reasoning field
// when content is empty (DeepSeek V4 reasoning/thinking mode).
func (b BifrostResponse) getContent(idx int) string {
if idx < len(b.Choices) {
c := b.Choices[idx].Message.Content
if c != "" {
return c
}
return b.Choices[idx].Message.Reasoning
}
return ""
}
// AnthropicResponse is returned to Anthropic-format clients.
type AnthropicResponse struct {
ID string `json:"id"`
Type string `json:"type"`
Role string `json:"role"`
Content []AnthropicContent `json:"content"`
Model string `json:"model"`
StopReason string `json:"stop_reason"`
StopSequence *string `json:"stop_sequence,omitempty"`
Usage AnthropicUsage `json:"usage"`
}
// AnthropicUsage mirrors the Anthropic usage block.
type AnthropicUsage struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
}
// AnthropicContent is a content block in Anthropic response format.
type AnthropicContent struct {
Type string `json:"type"`
Text string `json:"text"`
}
// AnthropicToBifrost converts an Anthropic Messages API request body (raw JSON)
// to Bifrost / OpenAI-compatible format.
func AnthropicToBifrost(rawBody []byte) ([]byte, error) {
var antReq AnthropicRequest
if err := json.Unmarshal(rawBody, &antReq); err != nil {
return nil, fmt.Errorf("transform: parse anthropic request: %w", err)
}
bfMessages := make([]BifrostMessage, 0, len(antReq.Messages)+1)
if sysContent := extractTextContent(antReq.System); sysContent != "" {
bfMessages = append(bfMessages, BifrostMessage{Role: "system", Content: sysContent})
}
for _, m := range antReq.Messages {
bfMessages = append(bfMessages, BifrostMessage{Role: m.Role, Content: extractTextContent(m.Content)})
}
// Auto-prefix model with provider if missing provider/model format
model := antReq.Model
if !strings.Contains(model, "/") {
prefix := guessProvider(model)
model = prefix + "/" + model
log.Printf("[TRANSFORM] Model auto-prefixed: %s -> %s", antReq.Model, model)
}
// Force stream=false — streaming SSE responses can't be transformed back to Anthropic format.
if antReq.Stream {
log.Printf("[TRANSFORM] Stream forced to false (SSE not supported for transform)")
}
bfReq := BifrostRequest{
Model: model,
Messages: bfMessages,
MaxTokens: antReq.MaxTokens,
Stream: false,
}
return json.Marshal(bfReq)
}
// guessProvider maps model names to their likely provider prefix for Bifrost.
func guessProvider(model string) string {
lower := strings.ToLower(model)
switch {
case strings.Contains(lower, "deepseek"):
return "DeepSeek"
case strings.Contains(lower, "gpt") || strings.Contains(lower, "openai"):
return "OpenAI"
case strings.Contains(lower, "claude") || strings.Contains(lower, "anthropic"):
return "Anthropic"
case strings.Contains(lower, "gemini"):
return "Google"
default:
return "DeepSeek"
}
}
// BifrostToAnthropic converts a Bifrost / OpenAI-compatible response body (raw JSON)
// back to Anthropic Messages API format.
func BifrostToAnthropic(rawBody []byte) ([]byte, error) {
var bfResp BifrostResponse
if err := json.Unmarshal(rawBody, &bfResp); err != nil {
return nil, fmt.Errorf("transform: parse bifrost response: %w", err)
}
contents := make([]AnthropicContent, 0, len(bfResp.Choices))
stopReason := "end_turn"
for i := range bfResp.Choices {
ch := bfResp.Choices[i]
contents = append(contents, AnthropicContent{Type: "text", Text: bfResp.getContent(i)})
if ch.FinishReason != "" {
stopReason = ch.FinishReason
}
}
antResp := AnthropicResponse{
ID: bfResp.ID,
Type: "message",
Role: "assistant",
Content: contents,
Model: bfResp.Model,
StopReason: stopReason,
Usage: AnthropicUsage{
InputTokens: bfResp.Usage.PromptTokens,
OutputTokens: bfResp.Usage.CompletionTokens,
},
}
return json.Marshal(antResp)
}
// ──────────────────────────────────────────────────────────────
// Streaming SSE transform: OpenAI chunks → Anthropic events
// ──────────────────────────────────────────────────────────────
// StreamTransformer converts OpenAI streaming chunks to Anthropic SSE events.
type StreamTransformer struct {
msgID string
model string
started bool
blockOpened bool
finishSent bool
}
// NewStreamTransformer creates a new streaming transformer.
func NewStreamTransformer(msgID, model string) *StreamTransformer {
return &StreamTransformer{msgID: msgID, model: model}
}
// TransformChunk converts a single OpenAI SSE data chunk (JSON bytes) into
// one or more Anthropic SSE event strings. Returns empty string if the
// chunk produced no output. On [DONE], emits final events.
func (s *StreamTransformer) TransformChunk(raw []byte) string {
if string(raw) == "[DONE]" {
return s.finish()
}
var chunk struct {
ID string `json:"id"`
Model string `json:"model"`
Choices []struct {
Delta struct {
Content string `json:"content"`
} `json:"delta"`
FinishReason *string `json:"finish_reason"`
} `json:"choices"`
}
if err := json.Unmarshal(raw, &chunk); err != nil {
return ""
}
if len(chunk.Choices) == 0 {
return ""
}
if chunk.ID != "" && chunk.Model != "" {
s.msgID = chunk.ID
s.model = chunk.Model
}
var out strings.Builder
// message_start on first chunk
if !s.started {
s.started = true
s.writeEvent(&out, "message_start", map[string]interface{}{
"type": "message_start",
"message": map[string]interface{}{
"id": s.msgID,
"type": "message",
"role": "assistant",
"model": s.model,
"content": []interface{}{},
"usage": map[string]int{
"input_tokens": 0,
"output_tokens": 0,
},
},
})
}
delta := chunk.Choices[0].Delta.Content
finish := chunk.Choices[0].FinishReason
if delta != "" {
if !s.blockOpened {
s.blockOpened = true
s.writeEvent(&out, "content_block_start", map[string]interface{}{
"type": "content_block_start",
"index": 0,
"content_block": map[string]interface{}{"type": "text", "text": ""},
})
}
s.writeEvent(&out, "content_block_delta", map[string]interface{}{
"type": "content_block_delta",
"index": 0,
"delta": map[string]interface{}{"type": "text_delta", "text": delta},
})
}
if finish != nil {
if s.blockOpened {
s.writeEvent(&out, "content_block_stop", map[string]interface{}{
"type": "content_block_stop",
"index": 0,
})
}
out.WriteString(s.finish())
s.finishSent = true
}
return out.String()
}
func (s *StreamTransformer) finish() string {
if s.finishSent {
return ""
}
var out strings.Builder
if s.blockOpened {
s.writeEvent(&out, "content_block_stop", map[string]interface{}{
"type": "content_block_stop",
"index": 0,
})
}
s.writeEvent(&out, "message_delta", map[string]interface{}{
"type": "message_delta",
"delta": map[string]interface{}{
"stop_reason": "end_turn",
"stop_sequence": nil,
},
"usage": map[string]int{"output_tokens": 0},
})
s.writeEvent(&out, "message_stop", map[string]interface{}{
"type": "message_stop",
})
s.finishSent = true
return out.String()
}
func (s *StreamTransformer) writeEvent(out *strings.Builder, event string, data interface{}) {
out.WriteString("event: ")
out.WriteString(event)
out.WriteString("\ndata: ")
body, _ := json.Marshal(data)
out.Write(body)
out.WriteString("\n\n")
}