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
+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")
}