Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions internal/translator/claude/gemini/claude_gemini_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
// Create system message in Claude Code format.
systemMessage := []byte(`{"role":"user","content":[{"type":"text","text":""}]}`)
systemMessage, _ = sjson.SetBytes(systemMessage, "content.0.text", systemText.String())
systemMessage = translatorcommon.AttachMessageCacheControl(systemMessage, sysInstr)
messageAccumulator.Append(systemMessage)
messageAccumulator.Flush()
}
Expand Down Expand Up @@ -261,6 +262,7 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
if text := part.Get("text"); text.Exists() {
textContent := []byte(`{"type":"text","text":""}`)
textContent, _ = sjson.SetBytes(textContent, "text", text.String())
textContent = translatorcommon.AttachCacheControl(textContent, part)
contentItems = append(contentItems, textContent)
return true
}
Expand All @@ -283,6 +285,7 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
if args := fc.Get("args"); args.Exists() && args.IsObject() {
toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte(args.Raw))
}
toolUse = translatorcommon.AttachCacheControl(toolUse, part)
contentItems = append(contentItems, toolUse)
return true
}
Expand Down Expand Up @@ -313,13 +316,15 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
} else if response := fr.Get("response"); response.Exists() {
toolResult, _ = sjson.SetBytes(toolResult, "content", response.Raw)
}
toolResult = translatorcommon.AttachCacheControl(toolResult, part)
contentItems = append(contentItems, toolResult)
return true
}

// Inline data conversion to Claude Code content format
if inlineData := geminiClaudeInlineData(part); inlineData.Exists() {
if contentPart, ok := claudeContentPartFromGeminiInlineData(inlineData); ok {
contentPart = translatorcommon.AttachCacheControl(contentPart, part)
contentItems = append(contentItems, contentPart)
}
return true
Expand All @@ -328,6 +333,7 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
// File data conversion to Claude Code content format
if fileData := geminiClaudeFileData(part); fileData.Exists() {
if contentPart, ok := claudeContentPartFromGeminiFileData(fileData); ok {
contentPart = translatorcommon.AttachCacheControl(contentPart, part)
contentItems = append(contentItems, contentPart)
}
return true
Expand All @@ -342,6 +348,7 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
msg := []byte(`{"role":"","content":[]}`)
msg, _ = sjson.SetBytes(msg, "role", role)
msg, _ = sjson.SetRawBytes(msg, "content", translatorcommon.JoinRawArray(contentItems))
msg = translatorcommon.AttachMessageCacheControl(msg, content)
messageAccumulator.Append(msg)
}

Expand Down Expand Up @@ -373,6 +380,7 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
anthropicTool, _ = sjson.SetRawBytes(anthropicTool, "input_schema", cleaned)
}

anthropicTool = translatorcommon.AttachCacheControl(anthropicTool, funcDecl)
anthropicTool = lowercaseClaudeToolSchemaTypes(anthropicTool)
anthropicTools = append(anthropicTools, gjson.ParseBytes(anthropicTool).Value())
return true
Expand Down
59 changes: 41 additions & 18 deletions internal/translator/codex/claude/codex_claude_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func convertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool,
toolNameMap := buildReverseMapFromClaudeOriginalToShort(rawJSON)
template, _ = sjson.SetBytes(template, "model", modelName)
inputItems := translatorcommon.NewRawArrayItems(rootResult.Get("messages.#").Int())
supportsCache := translatorcommon.ModelSupportsExplicitPromptCache(modelName)

// Process system messages and convert them to input content format.
systemsResult := rootResult.Get("system")
Expand All @@ -81,6 +82,10 @@ func convertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool,
systemResult := systemResults[i]
if systemResult.Get("type").String() == "text" {
appendSystemText(systemResult.Get("text").String())
if supportsCache && len(contentItems) > 0 {
last := len(contentItems) - 1
contentItems[last] = translatorcommon.AttachPromptCacheBreakpoint(contentItems[last], systemResult)
}
}
}
}
Expand Down Expand Up @@ -117,6 +122,9 @@ func convertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool,
message := []byte(`{"type":"message","role":""}`)
message, _ = sjson.SetBytes(message, "role", messageRole)
message, _ = sjson.SetRawBytes(message, "content", translatorcommon.JoinRawArray(contentItems))
if supportsCache {
message = translatorcommon.AttachMessagePromptCacheBreakpoint(message, messageResult)
}
inputItems = append(inputItems, message)
contentItems = contentItems[:0]
}
Expand Down Expand Up @@ -181,6 +189,10 @@ func convertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool,
switch contentType {
case "text":
appendTextContent(messageContentResult.Get("text").String())
if supportsCache && len(contentItems) > 0 {
last := len(contentItems) - 1
contentItems[last] = translatorcommon.AttachPromptCacheBreakpoint(contentItems[last], messageContentResult)
}
case "thinking":
appendReasoningContent(messageContentResult)
case "image":
Expand All @@ -200,6 +212,10 @@ func convertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool,
}
dataURL := fmt.Sprintf("data:%s;base64,%s", mediaType, data)
appendImageContent(dataURL)
if supportsCache && len(contentItems) > 0 {
last := len(contentItems) - 1
contentItems[last] = translatorcommon.AttachPromptCacheBreakpoint(contentItems[last], messageContentResult)
}
}
}
case "document":
Expand All @@ -217,6 +233,10 @@ func convertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool,
}
if data != "" {
appendDocumentContent(fmt.Sprintf("data:%s;base64,%s", mediaType, data))
if supportsCache && len(contentItems) > 0 {
last := len(contentItems) - 1
contentItems[last] = translatorcommon.AttachPromptCacheBreakpoint(contentItems[last], messageContentResult)
}
}
case "tool_use":
flushMessage()
Expand Down Expand Up @@ -263,12 +283,18 @@ func convertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool,

toolResultContent := []byte(`{"type":"input_image","image_url":""}`)
toolResultContent, _ = sjson.SetBytes(toolResultContent, "image_url", dataURL)
if supportsCache {
toolResultContent = translatorcommon.AttachPromptCacheBreakpoint(toolResultContent, contentResults[k])
}
toolResultContentItems = append(toolResultContentItems, toolResultContent)
}
}
} else if toolResultContentType == "text" {
toolResultContent := []byte(`{"type":"input_text","text":""}`)
toolResultContent, _ = sjson.SetBytes(toolResultContent, "text", contentResults[k].Get("text").String())
if supportsCache {
toolResultContent = translatorcommon.AttachPromptCacheBreakpoint(toolResultContent, contentResults[k])
}
toolResultContentItems = append(toolResultContentItems, toolResultContent)
}
}
Expand All @@ -287,6 +313,10 @@ func convertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool,
flushMessage()
} else if messageContentsResult.Type == gjson.String {
appendTextContent(messageContentsResult.String())
if supportsCache && len(contentItems) > 0 {
last := len(contentItems) - 1
contentItems[last] = translatorcommon.AttachPromptCacheBreakpoint(contentItems[last], messageContentsResult)
}
flushMessage()
}
}
Expand Down Expand Up @@ -380,13 +410,22 @@ func convertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool,
// OpenAI documents reasoning summaries as explicit opt-in output. Leave
// reasoning.summary to the source request's canonical summary intent instead
// of coupling it to reasoning effort.
serviceTier := normalizeCodexServiceTier(rootResult.Get("service_tier"))
if speed := rootResult.Get("speed"); speed.Type == gjson.String && speed.String() == "fast" {
serviceTier := translatorcommon.NormalizeCodexServiceTier(rootResult.Get("service_tier"))
if speed := rootResult.Get("speed"); speed.Type == gjson.String && strings.ToLower(strings.TrimSpace(speed.String())) == "fast" {
serviceTier = "priority"
}
if serviceTier != "" {
template, _ = sjson.SetBytes(template, "service_tier", serviceTier)
}
if v := rootResult.Get("prompt_cache_key"); v.Exists() {
template, _ = sjson.SetBytes(template, "prompt_cache_key", v.String())
}
if v := rootResult.Get("prompt_cache_retention"); v.Exists() {
template, _ = sjson.SetBytes(template, "prompt_cache_retention", v.String())
}
if v := rootResult.Get("prompt_cache_options"); v.Exists() && supportsCache {
template, _ = sjson.SetRawBytes(template, "prompt_cache_options", []byte(v.Raw))
}
template, _ = sjson.SetBytes(template, "stream", true)
template, _ = sjson.SetBytes(template, "store", false)
template, _ = sjson.SetBytes(template, "include", []string{"reasoning.encrypted_content"})
Expand All @@ -403,22 +442,6 @@ func codexClaudeTargetAcceptsGrokSignature(modelName string) bool {
return strings.Contains(baseModel, "grok")
}

// normalizeCodexServiceTier maps a requested service_tier to the value Codex
// accepts. "fast" and "priority" (case-insensitive, trimmed) both resolve to
// "priority"; any other value yields an empty string so the field is omitted.
func normalizeCodexServiceTier(result gjson.Result) string {
if !result.Exists() || result.Type != gjson.String {
return ""
}

switch strings.ToLower(strings.TrimSpace(result.String())) {
case "fast", "priority":
return "priority"
default:
return ""
}
}

// shortenCodexCallIDIfNeeded keeps Claude tool IDs within the OpenAI Responses
// API call_id limit while preserving a stable, low-collision mapping.
func shortenCodexCallIDIfNeeded(id string) string {
Expand Down
6 changes: 4 additions & 2 deletions internal/translator/codex/claude/codex_claude_request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,10 @@ func TestConvertClaudeRequestToCodex_ServiceTier(t *testing.T) {
wantExists: true,
},
{
name: "Unsupported tier is omitted",
name: "Default tier passes through",
serviceTierJSON: `"default"`,
want: "default",
wantExists: true,
},
{
name: "Non-string tier is omitted",
Expand All @@ -226,7 +228,7 @@ func TestConvertClaudeRequestToCodex_ServiceTier(t *testing.T) {
speedJSON: `true`,
},
{
name: "Fast speed overrides unsupported Anthropic tier",
name: "Fast speed overrides auto tier",
serviceTierJSON: `"auto"`,
speedJSON: `"fast"`,
want: "priority",
Expand Down
25 changes: 16 additions & 9 deletions internal/translator/codex/gemini/codex_gemini_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool)

root := gjson.ParseBytes(rawJSON)
inputItems := translatorcommon.NewRawArrayItems(root.Get("contents.#").Int())
supportsCache := translatorcommon.ModelSupportsExplicitPromptCache(modelName)

// Pre-compute tool name shortening map from declared functionDeclarations
shortMap := map[string]string{}
Expand Down Expand Up @@ -104,9 +105,22 @@ func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool)

// Model
out, _ = sjson.SetBytes(out, "model", modelName)
if serviceTier := normalizeGeminiCodexServiceTier(root.Get("service_tier")); serviceTier != "" {
serviceTier := translatorcommon.NormalizeCodexServiceTier(root.Get("service_tier"))
if speed := root.Get("speed"); speed.Type == gjson.String && strings.ToLower(strings.TrimSpace(speed.String())) == "fast" {
serviceTier = "priority"
}
if serviceTier != "" {
out, _ = sjson.SetBytes(out, "service_tier", serviceTier)
}
if v := root.Get("prompt_cache_key"); v.Exists() {
out, _ = sjson.SetBytes(out, "prompt_cache_key", v.String())
}
if v := root.Get("prompt_cache_retention"); v.Exists() {
out, _ = sjson.SetBytes(out, "prompt_cache_retention", v.String())
}
if v := root.Get("prompt_cache_options"); v.Exists() && supportsCache {
out, _ = sjson.SetRawBytes(out, "prompt_cache_options", []byte(v.Raw))
}

// System instruction -> as a user message with input_text parts
sysParts := root.Get("system_instruction.parts")
Expand Down Expand Up @@ -401,14 +415,7 @@ func codexMessageWithPart(role string, part []byte) []byte {
}

func normalizeGeminiCodexServiceTier(serviceTier gjson.Result) string {
if !serviceTier.Exists() || serviceTier.Type != gjson.String {
return ""
}
switch strings.ToLower(strings.TrimSpace(serviceTier.String())) {
case "priority", "fast":
return "priority"
}
return ""
return translatorcommon.NormalizeCodexServiceTier(serviceTier)
}

func codexContentPartFromGeminiInlineData(part gjson.Result) ([]byte, bool) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,24 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b
// Model
out, _ = sjson.SetBytes(out, "model", modelName)

// Carry cache hints and service tier when present. prompt_cache_options is
// only valid for gpt-5.6+ / daybreak; strip it for earlier models.
supportsExplicitCache := translatorcommon.ModelSupportsExplicitPromptCache(modelName)
if v := root.Get("prompt_cache_key"); v.Exists() {
out, _ = sjson.SetBytes(out, "prompt_cache_key", v.String())
}
if v := root.Get("prompt_cache_retention"); v.Exists() {
out, _ = sjson.SetBytes(out, "prompt_cache_retention", v.String())
}
if v := root.Get("prompt_cache_options"); v.Exists() && supportsExplicitCache {
out, _ = sjson.SetRawBytes(out, "prompt_cache_options", []byte(v.Raw))
}
if v := root.Get("service_tier"); v.Exists() {
if normalized := translatorcommon.NormalizeCodexServiceTier(v); normalized != "" {
out, _ = sjson.SetBytes(out, "service_tier", normalized)
}
}

// Build request-local tool metadata and name shortening map.
originalToolNameMap := map[string]string{}
customToolNames := map[string]struct{}{}
Expand Down Expand Up @@ -238,6 +256,9 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b
part := []byte(`{}`)
part, _ = sjson.SetBytes(part, "type", partType)
part, _ = sjson.SetBytes(part, "text", it.Get("text").String())
if supportsExplicitCache {
part = translatorcommon.CopyPromptCacheBreakpoint(part, it)
}
contentItems = append(contentItems, part)
case "image_url":
// Map image inputs to input_image for Responses API
Expand All @@ -247,6 +268,9 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b
if u := it.Get("image_url.url"); u.Exists() {
part, _ = sjson.SetBytes(part, "image_url", u.String())
}
if supportsExplicitCache {
part = translatorcommon.CopyPromptCacheBreakpoint(part, it)
}
contentItems = append(contentItems, part)
}
case "file":
Expand All @@ -260,6 +284,9 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b
if filename != "" {
part, _ = sjson.SetBytes(part, "filename", filename)
}
if supportsExplicitCache {
part = translatorcommon.CopyPromptCacheBreakpoint(part, it)
}
contentItems = append(contentItems, part)
}
}
Expand All @@ -274,6 +301,9 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b
if audioFormat != "" {
part, _ = sjson.SetBytes(part, "format", audioFormat)
}
if supportsExplicitCache {
part = translatorcommon.CopyPromptCacheBreakpoint(part, it)
}
contentItems = append(contentItems, part)
}
}
Expand Down
Loading
Loading