Skip to content

Commit d548d64

Browse files
committed
update
1 parent 0fc6a99 commit d548d64

File tree

17 files changed

+1363
-1134
lines changed

17 files changed

+1363
-1134
lines changed
Lines changed: 103 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,139 +1,141 @@
11
package converters
22

33
import (
4-
"encoding/json"
54
"fmt"
6-
"strings"
5+
6+
"api-key-rotator/backend/internal/converters/formats"
7+
// Import format packages to trigger their init() registration
8+
_ "api-key-rotator/backend/internal/converters/formats/anthropic"
9+
_ "api-key-rotator/backend/internal/converters/formats/gemini"
10+
_ "api-key-rotator/backend/internal/converters/formats/openai"
711
)
812

9-
// ResponseConverter interface for format conversion
10-
type ResponseConverter interface {
11-
// Convert transforms a complete (non-streaming) response body
12-
Convert(body []byte) ([]byte, error)
13-
// ConvertStreamChunk transforms a single SSE chunk's JSON payload
14-
ConvertStreamChunk(chunk []byte) ([]byte, error)
15-
// GetContentType returns the content type for the converted response
16-
GetContentType() string
13+
// Converter handles format conversion between different LLM API formats
14+
type Converter struct {
15+
from formats.FormatHandler
16+
to formats.FormatHandler
17+
fromStream formats.StreamHandler
18+
toStream formats.StreamHandler
1719
}
1820

19-
// PassthroughConverter returns the response as-is without conversion
20-
type PassthroughConverter struct{}
21+
// NewConverter creates a new converter between two formats
22+
// fromFormat and toFormat should be format names like "openai", "anthropic", "gemini"
23+
func NewConverter(fromFormat, toFormat string) (*Converter, error) {
24+
// Normalize format names
25+
fromFormat = NormalizeFormat(fromFormat)
26+
toFormat = NormalizeFormat(toFormat)
2127

22-
func (c *PassthroughConverter) Convert(body []byte) ([]byte, error) {
23-
return body, nil
24-
}
28+
// Get handlers from registry
29+
fromInfo, err := formats.GetFormat(fromFormat)
30+
if err != nil {
31+
return nil, fmt.Errorf("source format error: %w", err)
32+
}
2533

26-
func (c *PassthroughConverter) ConvertStreamChunk(chunk []byte) ([]byte, error) {
27-
return chunk, nil
28-
}
34+
toInfo, err := formats.GetFormat(toFormat)
35+
if err != nil {
36+
return nil, fmt.Errorf("target format error: %w", err)
37+
}
2938

30-
func (c *PassthroughConverter) GetContentType() string {
31-
return "application/json"
39+
return &Converter{
40+
from: fromInfo.Handler,
41+
to: toInfo.Handler,
42+
fromStream: fromInfo.StreamHandler,
43+
toStream: toInfo.StreamHandler,
44+
}, nil
3245
}
3346

34-
// NewResponseConverter creates a converter based on input and output formats
35-
// inputFormat: the format of the upstream API response (openai_compatible, anthropic_native, gemini_native)
36-
// outputFormat: the desired output format (none, openai, anthropic, gemini)
37-
func NewResponseConverter(inputFormat, outputFormat string) ResponseConverter {
38-
// Normalize input format to match output format naming
39-
normalizedInput := NormalizeFormat(inputFormat)
40-
41-
// If no conversion needed or same format, use passthrough
42-
if outputFormat == "none" || outputFormat == "" || normalizedInput == outputFormat {
43-
return &PassthroughConverter{}
47+
// ConvertRequest converts a request from source format to target format
48+
func (c *Converter) ConvertRequest(body []byte) ([]byte, error) {
49+
// Parse source format to universal
50+
universal, err := c.from.ParseRequest(body)
51+
if err != nil {
52+
return nil, fmt.Errorf("parse request error: %w", err)
4453
}
4554

46-
// Select appropriate converter based on input → output combination
47-
switch {
48-
case normalizedInput == "openai" && outputFormat == "anthropic":
49-
return &OpenAIToAnthropicConverter{}
50-
case normalizedInput == "openai" && outputFormat == "gemini":
51-
return &OpenAIToGeminiConverter{}
52-
case normalizedInput == "anthropic" && outputFormat == "openai":
53-
return &AnthropicToOpenAIConverter{}
54-
case normalizedInput == "gemini" && outputFormat == "openai":
55-
return &GeminiToOpenAIConverter{}
56-
case normalizedInput == "anthropic" && outputFormat == "gemini":
57-
// anthropic → gemini: chain through openai
58-
return &ChainedConverter{
59-
first: &AnthropicToOpenAIConverter{},
60-
second: &OpenAIToGeminiConverter{},
61-
}
62-
case normalizedInput == "gemini" && outputFormat == "anthropic":
63-
// gemini → anthropic: chain through openai
64-
return &ChainedConverter{
65-
first: &GeminiToOpenAIConverter{},
66-
second: &OpenAIToAnthropicConverter{},
67-
}
68-
default:
69-
// Unsupported conversion, use passthrough
70-
return &PassthroughConverter{}
55+
// Build target format from universal
56+
result, err := c.to.BuildRequest(universal)
57+
if err != nil {
58+
return nil, fmt.Errorf("build request error: %w", err)
7159
}
60+
61+
return result, nil
7262
}
7363

74-
// NormalizeFormat converts api_format values to client_format naming convention
75-
func NormalizeFormat(format string) string {
76-
switch format {
77-
case "openai_compatible":
78-
return "openai"
79-
case "anthropic_native":
80-
return "anthropic"
81-
case "gemini_native":
82-
return "gemini"
83-
default:
84-
return format
64+
// ConvertResponse converts a response from source format to target format
65+
func (c *Converter) ConvertResponse(body []byte) ([]byte, error) {
66+
// Parse source format to universal
67+
universal, err := c.from.ParseResponse(body)
68+
if err != nil {
69+
return nil, fmt.Errorf("parse response error: %w", err)
70+
}
71+
72+
// Build target format from universal
73+
result, err := c.to.BuildResponse(universal)
74+
if err != nil {
75+
return nil, fmt.Errorf("build response error: %w", err)
8576
}
86-
}
8777

88-
// ChainedConverter chains two converters together
89-
type ChainedConverter struct {
90-
first ResponseConverter
91-
second ResponseConverter
78+
return result, nil
9279
}
9380

94-
func (c *ChainedConverter) Convert(body []byte) ([]byte, error) {
95-
intermediate, err := c.first.Convert(body)
81+
// ConvertStreamChunk converts a streaming chunk from source format to target format
82+
func (c *Converter) ConvertStreamChunk(chunk []byte) ([]byte, error) {
83+
// Parse source format to universal
84+
universal, err := c.fromStream.ParseStreamChunk(chunk)
9685
if err != nil {
97-
return nil, err
86+
return nil, fmt.Errorf("parse stream chunk error: %w", err)
9887
}
99-
return c.second.Convert(intermediate)
100-
}
10188

102-
func (c *ChainedConverter) ConvertStreamChunk(chunk []byte) ([]byte, error) {
103-
intermediate, err := c.first.ConvertStreamChunk(chunk)
89+
// Skip empty chunks
90+
if universal.Delta == "" && universal.StopReason == nil && !universal.IsFirst && !universal.IsLast {
91+
return nil, nil
92+
}
93+
94+
// Build target format from universal
95+
result, err := c.toStream.BuildStreamChunk(universal)
10496
if err != nil {
105-
return nil, err
97+
return nil, fmt.Errorf("build stream chunk error: %w", err)
10698
}
107-
return c.second.ConvertStreamChunk(intermediate)
99+
100+
return result, nil
108101
}
109102

110-
func (c *ChainedConverter) GetContentType() string {
111-
return c.second.GetContentType()
103+
// GetTargetPath converts a client action path to the target API path
104+
func (c *Converter) GetTargetPath(action string) string {
105+
// First convert from client format's perspective
106+
// Then convert to target format's API path
107+
return c.to.GetAPIPath(action)
112108
}
113109

114-
// Helper function to parse SSE data line
115-
func ParseSSEChunk(data []byte) ([]byte, bool) {
116-
str := strings.TrimSpace(string(data))
117-
if strings.HasPrefix(str, "data: ") {
118-
payload := strings.TrimPrefix(str, "data: ")
119-
if payload == "[DONE]" {
120-
return nil, false
121-
}
122-
return []byte(payload), true
123-
}
124-
return nil, false
110+
// GetStreamStartEvents returns the start events needed for the target format
111+
func (c *Converter) GetStreamStartEvents(model, id string) [][]byte {
112+
return c.toStream.BuildStartEvent(model, id)
125113
}
126114

127-
// Helper function to format SSE data line
128-
func FormatSSEChunk(data []byte) []byte {
129-
return []byte(fmt.Sprintf("data: %s\n\n", string(data)))
115+
// GetStreamEndEvents returns the end events needed for the target format
116+
func (c *Converter) GetStreamEndEvents() [][]byte {
117+
return c.toStream.BuildEndEvent()
130118
}
131119

132-
// Common JSON helper
133-
func parseJSON(data []byte, v interface{}) error {
134-
return json.Unmarshal(data, v)
120+
// NormalizeFormat converts api_format values to standard format names
121+
func NormalizeFormat(format string) string {
122+
switch format {
123+
case "openai_compatible":
124+
return "openai"
125+
case "anthropic_native":
126+
return "anthropic"
127+
case "gemini_native":
128+
return "gemini"
129+
default:
130+
return format
131+
}
135132
}
136133

137-
func toJSON(v interface{}) ([]byte, error) {
138-
return json.Marshal(v)
134+
// NeedsConversion checks if conversion is needed between two formats
135+
func NeedsConversion(clientFormat, apiFormat string) bool {
136+
if clientFormat == "none" || clientFormat == "" {
137+
return false
138+
}
139+
normalizedAPI := NormalizeFormat(apiFormat)
140+
return clientFormat != normalizedAPI
139141
}

0 commit comments

Comments
 (0)