|
1 | 1 | package converters |
2 | 2 |
|
3 | 3 | import ( |
4 | | - "encoding/json" |
5 | 4 | "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" |
7 | 11 | ) |
8 | 12 |
|
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 |
17 | 19 | } |
18 | 20 |
|
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) |
21 | 27 |
|
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 | + } |
25 | 33 |
|
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 | + } |
29 | 38 |
|
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 |
32 | 45 | } |
33 | 46 |
|
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) |
44 | 53 | } |
45 | 54 |
|
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) |
71 | 59 | } |
| 60 | + |
| 61 | + return result, nil |
72 | 62 | } |
73 | 63 |
|
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) |
85 | 76 | } |
86 | | -} |
87 | 77 |
|
88 | | -// ChainedConverter chains two converters together |
89 | | -type ChainedConverter struct { |
90 | | - first ResponseConverter |
91 | | - second ResponseConverter |
| 78 | + return result, nil |
92 | 79 | } |
93 | 80 |
|
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) |
96 | 85 | if err != nil { |
97 | | - return nil, err |
| 86 | + return nil, fmt.Errorf("parse stream chunk error: %w", err) |
98 | 87 | } |
99 | | - return c.second.Convert(intermediate) |
100 | | -} |
101 | 88 |
|
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) |
104 | 96 | if err != nil { |
105 | | - return nil, err |
| 97 | + return nil, fmt.Errorf("build stream chunk error: %w", err) |
106 | 98 | } |
107 | | - return c.second.ConvertStreamChunk(intermediate) |
| 99 | + |
| 100 | + return result, nil |
108 | 101 | } |
109 | 102 |
|
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) |
112 | 108 | } |
113 | 109 |
|
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) |
125 | 113 | } |
126 | 114 |
|
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() |
130 | 118 | } |
131 | 119 |
|
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 | + } |
135 | 132 | } |
136 | 133 |
|
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 |
139 | 141 | } |
0 commit comments