mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-12 04:54:45 +00:00
122 lines
4.5 KiB
Go
122 lines
4.5 KiB
Go
package traefikllmgateway
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
)
|
|
|
|
// geminiImageAspectRatioBySize maps OpenAI's images.generations "size"
|
|
// values to the Imagen aspectRatio parameter this translator understands,
|
|
// per spec §3's mapping table. A size string absent from this map is one
|
|
// Imagen has no known aspect ratio for — geminiImageAspectRatio rejects it
|
|
// as a *translateError rather than guessing.
|
|
var geminiImageAspectRatioBySize = map[string]string{
|
|
"1024x1024": "1:1",
|
|
"1792x1024": "16:9",
|
|
"1024x1792": "9:16",
|
|
"512x512": "1:1",
|
|
}
|
|
|
|
// geminiImageAspectRatio resolves req's OpenAI "size" field to an Imagen
|
|
// aspectRatio parameter: an absent or empty size defaults to "1:1" (spec
|
|
// §3), and any other value must be one of geminiImageAspectRatioBySize's
|
|
// keys. An unrecognized size is a *translateError, not a silent fallback
|
|
// — the client asked for a specific size the gateway cannot honor, so
|
|
// letting Imagen apply its own default instead would silently ignore the
|
|
// request rather than fail loudly.
|
|
func geminiImageAspectRatio(size string) (string, error) {
|
|
if size == "" {
|
|
return "1:1", nil
|
|
}
|
|
ratio, ok := geminiImageAspectRatioBySize[size]
|
|
if !ok {
|
|
return "", &translateError{msg: fmt.Sprintf("size %q is not supported for gemini image generation", size)}
|
|
}
|
|
return ratio, nil
|
|
}
|
|
|
|
// geminiImagesRequestFromOpenAI maps an OpenAI images.generations request
|
|
// body ({"model","prompt","n","size","response_format",...}) to Imagen's
|
|
// :predict request body ({"instances":[{"prompt"}],"parameters":
|
|
// {"sampleCount","aspectRatio"}}), per spec §3. response_format:"url" is
|
|
// rejected outright — the gateway stores nothing, so it cannot honor a
|
|
// client asking for a hosted URL back; the response this translator
|
|
// always produces is b64_json (translate_gemini_images.go's
|
|
// openAIImagesResponseFromGemini). A truthy "quality" or "style" field is
|
|
// rejected the same way chat's translators reject an unsupported field
|
|
// (isTruthy, translate_anthropic.go): Imagen has no equivalent for
|
|
// either. n defaults to 1 when absent or non-positive, matching OpenAI's
|
|
// own default. The request's "model" field is deliberately never read
|
|
// here — the adapter reads it separately to build the :predict URL, the
|
|
// same convention chatCompletion/embeddings already follow.
|
|
func geminiImagesRequestFromOpenAI(req map[string]any) (map[string]any, error) {
|
|
if rf, ok := req["response_format"].(string); ok && rf == "url" {
|
|
return nil, &translateError{msg: `response_format "url" is not supported for gemini image generation (the gateway stores nothing to host a URL from)`}
|
|
}
|
|
if v, ok := req["quality"]; ok && isTruthy(v) {
|
|
return nil, geminiUnsupportedFieldError("quality")
|
|
}
|
|
if v, ok := req["style"]; ok && isTruthy(v) {
|
|
return nil, geminiUnsupportedFieldError("style")
|
|
}
|
|
|
|
prompt, _ := req["prompt"].(string)
|
|
|
|
n := int64(1)
|
|
if v, ok := req["n"]; ok {
|
|
if parsed, ok2 := toInt64(v); ok2 && parsed > 0 {
|
|
n = parsed
|
|
}
|
|
}
|
|
|
|
size, _ := req["size"].(string)
|
|
aspectRatio, err := geminiImageAspectRatio(size)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return map[string]any{
|
|
"instances": []any{map[string]any{"prompt": prompt}},
|
|
"parameters": map[string]any{
|
|
"sampleCount": n,
|
|
"aspectRatio": aspectRatio,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// geminiImagePrediction is one entry of Imagen's :predict response
|
|
// "predictions" array this translator reads.
|
|
type geminiImagePrediction struct {
|
|
BytesBase64Encoded string `json:"bytesBase64Encoded"`
|
|
}
|
|
|
|
// geminiImagePredictResponse is the subset of Imagen's :predict response
|
|
// body this translator reads.
|
|
type geminiImagePredictResponse struct {
|
|
Predictions []geminiImagePrediction `json:"predictions"`
|
|
}
|
|
|
|
// openAIImagesResponseFromGemini maps an Imagen :predict response body to
|
|
// an OpenAI images.generations response ({"created","data":[{"b64_json"}]}),
|
|
// per spec §3's response mapping. created is the caller-supplied response
|
|
// timestamp — Imagen's response carries none. An empty predictions array
|
|
// decodes to an empty (non-nil) data array, not an error: Imagen
|
|
// answering with zero images is a valid, if unusual, response to forward
|
|
// as-is.
|
|
func openAIImagesResponseFromGemini(body []byte, created int64) (map[string]any, error) {
|
|
var resp geminiImagePredictResponse
|
|
if err := json.Unmarshal(body, &resp); err != nil {
|
|
return nil, fmt.Errorf("%w: decode gemini images response: %w", errUpstream, err)
|
|
}
|
|
|
|
data := make([]any, len(resp.Predictions))
|
|
for i, p := range resp.Predictions {
|
|
data[i] = map[string]any{"b64_json": p.BytesBase64Encoded}
|
|
}
|
|
|
|
return map[string]any{
|
|
"created": created,
|
|
"data": data,
|
|
}, nil
|
|
}
|