fix: bridge codex image generation over responses
This commit is contained in:
@@ -45,6 +45,11 @@ type codexTransformResult struct {
|
||||
PromptCacheKey string
|
||||
}
|
||||
|
||||
const (
|
||||
codexImageGenerationBridgeMarker = "<sub2api-codex-image-generation>"
|
||||
codexImageGenerationBridgeText = codexImageGenerationBridgeMarker + "\nWhen the user asks for raster image generation or editing, use the OpenAI Responses native `image_generation` tool attached to this request. The local Codex client may not expose an `image_gen` namespace, but that does not mean image generation is unavailable. Do not ask the user to switch to CLI fallback solely because `image_gen` is absent.\n</sub2api-codex-image-generation>"
|
||||
)
|
||||
|
||||
func applyCodexOAuthTransform(reqBody map[string]any, isCodexCLI bool, isCompact bool) codexTransformResult {
|
||||
result := codexTransformResult{}
|
||||
// 工具续链需求会影响存储策略与 input 过滤逻辑。
|
||||
@@ -300,6 +305,61 @@ func normalizeOpenAIResponsesImageGenerationTools(reqBody map[string]any) bool {
|
||||
return modified
|
||||
}
|
||||
|
||||
func ensureOpenAIResponsesImageGenerationTool(reqBody map[string]any) bool {
|
||||
if len(reqBody) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
tool := map[string]any{
|
||||
"type": "image_generation",
|
||||
"output_format": "png",
|
||||
}
|
||||
|
||||
rawTools, ok := reqBody["tools"]
|
||||
if !ok || rawTools == nil {
|
||||
reqBody["tools"] = []any{tool}
|
||||
return true
|
||||
}
|
||||
|
||||
tools, ok := rawTools.([]any)
|
||||
if !ok {
|
||||
reqBody["tools"] = []any{tool}
|
||||
return true
|
||||
}
|
||||
for _, rawTool := range tools {
|
||||
toolMap, ok := rawTool.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(firstNonEmptyString(toolMap["type"])) == "image_generation" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
reqBody["tools"] = append(tools, tool)
|
||||
return true
|
||||
}
|
||||
|
||||
func applyCodexImageGenerationBridgeInstructions(reqBody map[string]any) bool {
|
||||
if len(reqBody) == 0 || !hasOpenAIImageGenerationTool(reqBody) {
|
||||
return false
|
||||
}
|
||||
|
||||
existing, _ := reqBody["instructions"].(string)
|
||||
if strings.Contains(existing, codexImageGenerationBridgeMarker) {
|
||||
return false
|
||||
}
|
||||
|
||||
existing = strings.TrimRight(existing, " \t\r\n")
|
||||
if strings.TrimSpace(existing) == "" {
|
||||
reqBody["instructions"] = codexImageGenerationBridgeText
|
||||
return true
|
||||
}
|
||||
|
||||
reqBody["instructions"] = existing + "\n\n" + codexImageGenerationBridgeText
|
||||
return true
|
||||
}
|
||||
|
||||
func validateOpenAIResponsesImageModel(reqBody map[string]any, model string) error {
|
||||
if !hasOpenAIImageGenerationTool(reqBody) {
|
||||
return nil
|
||||
@@ -311,6 +371,82 @@ func validateOpenAIResponsesImageModel(reqBody map[string]any, model string) err
|
||||
return fmt.Errorf("/v1/responses image_generation requests require a Responses-capable text model; image-only model %q is not allowed", model)
|
||||
}
|
||||
|
||||
func normalizeOpenAIResponsesImageOnlyModel(reqBody map[string]any) bool {
|
||||
if len(reqBody) == 0 {
|
||||
return false
|
||||
}
|
||||
imageModel := strings.TrimSpace(firstNonEmptyString(reqBody["model"]))
|
||||
if !isOpenAIImageGenerationModel(imageModel) {
|
||||
return false
|
||||
}
|
||||
|
||||
modified := false
|
||||
tools, _ := reqBody["tools"].([]any)
|
||||
imageToolIndex := -1
|
||||
for i, rawTool := range tools {
|
||||
toolMap, ok := rawTool.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(firstNonEmptyString(toolMap["type"])) == "image_generation" {
|
||||
imageToolIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if imageToolIndex < 0 {
|
||||
tools = append(tools, map[string]any{
|
||||
"type": "image_generation",
|
||||
"model": imageModel,
|
||||
})
|
||||
imageToolIndex = len(tools) - 1
|
||||
reqBody["tools"] = tools
|
||||
modified = true
|
||||
}
|
||||
|
||||
if toolMap, ok := tools[imageToolIndex].(map[string]any); ok {
|
||||
if strings.TrimSpace(firstNonEmptyString(toolMap["model"])) == "" {
|
||||
toolMap["model"] = imageModel
|
||||
modified = true
|
||||
}
|
||||
for _, key := range []string{
|
||||
"size",
|
||||
"quality",
|
||||
"background",
|
||||
"output_format",
|
||||
"output_compression",
|
||||
"moderation",
|
||||
"style",
|
||||
"partial_images",
|
||||
} {
|
||||
if value, exists := reqBody[key]; exists && value != nil {
|
||||
if _, toolHas := toolMap[key]; !toolHas {
|
||||
toolMap[key] = value
|
||||
}
|
||||
delete(reqBody, key)
|
||||
modified = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if prompt := strings.TrimSpace(firstNonEmptyString(reqBody["prompt"])); prompt != "" {
|
||||
if _, hasInput := reqBody["input"]; !hasInput {
|
||||
reqBody["input"] = prompt
|
||||
}
|
||||
delete(reqBody, "prompt")
|
||||
modified = true
|
||||
}
|
||||
|
||||
if _, ok := reqBody["tool_choice"]; !ok {
|
||||
reqBody["tool_choice"] = map[string]any{"type": "image_generation"}
|
||||
modified = true
|
||||
}
|
||||
if imageModel != openAIImagesResponsesMainModel {
|
||||
modified = true
|
||||
}
|
||||
reqBody["model"] = openAIImagesResponsesMainModel
|
||||
return modified
|
||||
}
|
||||
|
||||
func normalizeOpenAIModelForUpstream(account *Account, model string) string {
|
||||
if account == nil || account.Type == AccountTypeOAuth {
|
||||
return normalizeCodexModel(model)
|
||||
|
||||
Reference in New Issue
Block a user