feat: improve OpenAI messages compatibility for Claude Code
This commit is contained in:
@@ -69,6 +69,13 @@ type codexTransformResult struct {
|
||||
PromptCacheKey string
|
||||
}
|
||||
|
||||
type codexOAuthTransformOptions struct {
|
||||
IsCodexCLI bool
|
||||
IsCompact bool
|
||||
SkipDefaultInstructions bool
|
||||
PreserveToolCallIDs bool
|
||||
}
|
||||
|
||||
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>"
|
||||
@@ -94,6 +101,13 @@ var openAICodexOAuthUnsupportedFields = append([]string{
|
||||
}, openAIChatGPTInternalUnsupportedFields...)
|
||||
|
||||
func applyCodexOAuthTransform(reqBody map[string]any, isCodexCLI bool, isCompact bool) codexTransformResult {
|
||||
return applyCodexOAuthTransformWithOptions(reqBody, codexOAuthTransformOptions{
|
||||
IsCodexCLI: isCodexCLI,
|
||||
IsCompact: isCompact,
|
||||
})
|
||||
}
|
||||
|
||||
func applyCodexOAuthTransformWithOptions(reqBody map[string]any, opts codexOAuthTransformOptions) codexTransformResult {
|
||||
result := codexTransformResult{}
|
||||
// 工具续链需求会影响存储策略与 input 过滤逻辑。
|
||||
needsToolContinuation := NeedsToolContinuation(reqBody)
|
||||
@@ -111,7 +125,7 @@ func applyCodexOAuthTransform(reqBody map[string]any, isCodexCLI bool, isCompact
|
||||
result.NormalizedModel = normalizedModel
|
||||
}
|
||||
|
||||
if isCompact {
|
||||
if opts.IsCompact {
|
||||
if _, ok := reqBody["store"]; ok {
|
||||
delete(reqBody, "store")
|
||||
result.Modified = true
|
||||
@@ -183,6 +197,10 @@ func applyCodexOAuthTransform(reqBody map[string]any, isCodexCLI bool, isCompact
|
||||
|
||||
if v, ok := reqBody["prompt_cache_key"].(string); ok {
|
||||
result.PromptCacheKey = strings.TrimSpace(v)
|
||||
if isOpenAICompatMessagesBridgeRequestBody(reqBody) {
|
||||
delete(reqBody, "prompt_cache_key")
|
||||
result.Modified = true
|
||||
}
|
||||
}
|
||||
|
||||
// 提取 input 中 role:"system" 消息至 instructions(OAuth 上游不支持 system role)。
|
||||
@@ -191,7 +209,7 @@ func applyCodexOAuthTransform(reqBody map[string]any, isCodexCLI bool, isCompact
|
||||
}
|
||||
|
||||
// instructions 处理逻辑:根据是否是 Codex CLI 分别调用不同方法
|
||||
if applyInstructions(reqBody, isCodexCLI) {
|
||||
if !opts.SkipDefaultInstructions && applyInstructions(reqBody, opts.IsCodexCLI) {
|
||||
result.Modified = true
|
||||
}
|
||||
if isCodexSparkModel(normalizedModel) && applyCodexSparkImageUnsupportedInstructions(reqBody) {
|
||||
@@ -208,7 +226,10 @@ func applyCodexOAuthTransform(reqBody map[string]any, isCodexCLI bool, isCompact
|
||||
input = normalizedInput
|
||||
result.Modified = true
|
||||
}
|
||||
input = filterCodexInput(input, needsToolContinuation)
|
||||
input = filterCodexInputWithOptions(input, codexInputFilterOptions{
|
||||
PreserveReferences: needsToolContinuation,
|
||||
PreserveCallIDs: opts.PreserveToolCallIDs,
|
||||
})
|
||||
reqBody["input"] = input
|
||||
result.Modified = true
|
||||
} else if inputStr, ok := reqBody["input"].(string); ok {
|
||||
@@ -853,7 +874,7 @@ func getNormalizedCodexModel(modelID string) string {
|
||||
}
|
||||
|
||||
// extractTextFromContent extracts plain text from a content value that is either
|
||||
// a Go string or a []any of content-part maps with type:"text".
|
||||
// a Go string or a []any of text-like content-part maps.
|
||||
func extractTextFromContent(content any) string {
|
||||
switch v := content.(type) {
|
||||
case string:
|
||||
@@ -865,7 +886,8 @@ func extractTextFromContent(content any) string {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if t, _ := m["type"].(string); t == "text" {
|
||||
switch t, _ := m["type"].(string); t {
|
||||
case "text", "input_text", "output_text":
|
||||
if text, ok := m["text"].(string); ok {
|
||||
parts = append(parts, text)
|
||||
}
|
||||
@@ -919,6 +941,28 @@ func extractSystemMessagesFromInput(reqBody map[string]any) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func extractPromptLikeInstructionsFromInput(reqBody map[string]any) string {
|
||||
input, ok := reqBody["input"].([]any)
|
||||
if !ok || len(input) == 0 {
|
||||
return ""
|
||||
}
|
||||
var texts []string
|
||||
for _, item := range input {
|
||||
m, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
role, _ := m["role"].(string)
|
||||
switch role {
|
||||
case "developer", "system":
|
||||
if text := strings.TrimSpace(extractTextFromContent(m["content"])); text != "" {
|
||||
texts = append(texts, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.Join(texts, "\n\n")
|
||||
}
|
||||
|
||||
// applyInstructions 处理 instructions 字段:仅在 instructions 为空时填充默认值。
|
||||
func applyInstructions(reqBody map[string]any, isCodexCLI bool) bool {
|
||||
if !isInstructionsEmpty(reqBody) {
|
||||
@@ -945,9 +989,20 @@ func isInstructionsEmpty(reqBody map[string]any) bool {
|
||||
return strings.TrimSpace(str) == ""
|
||||
}
|
||||
|
||||
type codexInputFilterOptions struct {
|
||||
PreserveReferences bool
|
||||
PreserveCallIDs bool
|
||||
}
|
||||
|
||||
// filterCodexInput 按需过滤 item_reference 与 id。
|
||||
// preserveReferences 为 true 时保持引用与 id,以满足续链请求对上下文的依赖。
|
||||
func filterCodexInput(input []any, preserveReferences bool) []any {
|
||||
return filterCodexInputWithOptions(input, codexInputFilterOptions{
|
||||
PreserveReferences: preserveReferences,
|
||||
})
|
||||
}
|
||||
|
||||
func filterCodexInputWithOptions(input []any, opts codexInputFilterOptions) []any {
|
||||
filtered := make([]any, 0, len(input))
|
||||
for _, item := range input {
|
||||
m, ok := item.(map[string]any)
|
||||
@@ -968,6 +1023,9 @@ func filterCodexInput(input []any, preserveReferences bool) []any {
|
||||
// 仅修正真正的 tool/function call 标识,避免误改普通 message/reasoning id;
|
||||
// 若 item_reference 指向 legacy call_* 标识,则仅修正该引用本身。
|
||||
fixCallIDPrefix := func(id string) string {
|
||||
if opts.PreserveCallIDs {
|
||||
return id
|
||||
}
|
||||
if id == "" || strings.HasPrefix(id, "fc") {
|
||||
return id
|
||||
}
|
||||
@@ -978,7 +1036,7 @@ func filterCodexInput(input []any, preserveReferences bool) []any {
|
||||
}
|
||||
|
||||
if typ == "item_reference" {
|
||||
if !preserveReferences {
|
||||
if !opts.PreserveReferences {
|
||||
continue
|
||||
}
|
||||
newItem := make(map[string]any, len(m))
|
||||
@@ -1046,7 +1104,7 @@ func filterCodexInput(input []any, preserveReferences bool) []any {
|
||||
}
|
||||
}
|
||||
|
||||
if !preserveReferences {
|
||||
if !opts.PreserveReferences {
|
||||
ensureCopy()
|
||||
delete(newItem, "id")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user