fix(translator): add unit tests to validate output_item.done fallback logic for Gemini and Claude
This commit is contained in:
@@ -20,10 +20,11 @@ var (
|
||||
|
||||
// ConvertCodexResponseToGeminiParams holds parameters for response conversion.
|
||||
type ConvertCodexResponseToGeminiParams struct {
|
||||
Model string
|
||||
CreatedAt int64
|
||||
ResponseID string
|
||||
LastStorageOutput []byte
|
||||
Model string
|
||||
CreatedAt int64
|
||||
ResponseID string
|
||||
LastStorageOutput []byte
|
||||
HasOutputTextDelta bool
|
||||
}
|
||||
|
||||
// ConvertCodexResponseToGemini converts Codex streaming response format to Gemini format.
|
||||
@@ -42,10 +43,11 @@ type ConvertCodexResponseToGeminiParams struct {
|
||||
func ConvertCodexResponseToGemini(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
||||
if *param == nil {
|
||||
*param = &ConvertCodexResponseToGeminiParams{
|
||||
Model: modelName,
|
||||
CreatedAt: 0,
|
||||
ResponseID: "",
|
||||
LastStorageOutput: nil,
|
||||
Model: modelName,
|
||||
CreatedAt: 0,
|
||||
ResponseID: "",
|
||||
LastStorageOutput: nil,
|
||||
HasOutputTextDelta: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,18 +60,18 @@ func ConvertCodexResponseToGemini(_ context.Context, modelName string, originalR
|
||||
typeResult := rootResult.Get("type")
|
||||
typeStr := typeResult.String()
|
||||
|
||||
params := (*param).(*ConvertCodexResponseToGeminiParams)
|
||||
|
||||
// Base Gemini response template
|
||||
template := []byte(`{"candidates":[{"content":{"role":"model","parts":[]}}],"usageMetadata":{"trafficType":"PROVISIONED_THROUGHPUT"},"modelVersion":"gemini-2.5-pro","createTime":"2025-08-15T02:52:03.884209Z","responseId":"06CeaPH7NaCU48APvNXDyA4"}`)
|
||||
if len((*param).(*ConvertCodexResponseToGeminiParams).LastStorageOutput) > 0 && typeStr == "response.output_item.done" {
|
||||
template = append([]byte(nil), (*param).(*ConvertCodexResponseToGeminiParams).LastStorageOutput...)
|
||||
} else {
|
||||
template, _ = sjson.SetBytes(template, "modelVersion", (*param).(*ConvertCodexResponseToGeminiParams).Model)
|
||||
{
|
||||
template, _ = sjson.SetBytes(template, "modelVersion", params.Model)
|
||||
createdAtResult := rootResult.Get("response.created_at")
|
||||
if createdAtResult.Exists() {
|
||||
(*param).(*ConvertCodexResponseToGeminiParams).CreatedAt = createdAtResult.Int()
|
||||
template, _ = sjson.SetBytes(template, "createTime", time.Unix((*param).(*ConvertCodexResponseToGeminiParams).CreatedAt, 0).Format(time.RFC3339Nano))
|
||||
params.CreatedAt = createdAtResult.Int()
|
||||
template, _ = sjson.SetBytes(template, "createTime", time.Unix(params.CreatedAt, 0).Format(time.RFC3339Nano))
|
||||
}
|
||||
template, _ = sjson.SetBytes(template, "responseId", (*param).(*ConvertCodexResponseToGeminiParams).ResponseID)
|
||||
template, _ = sjson.SetBytes(template, "responseId", params.ResponseID)
|
||||
}
|
||||
|
||||
// Handle function call completion
|
||||
@@ -101,7 +103,7 @@ func ConvertCodexResponseToGemini(_ context.Context, modelName string, originalR
|
||||
template, _ = sjson.SetRawBytes(template, "candidates.0.content.parts.-1", functionCall)
|
||||
template, _ = sjson.SetBytes(template, "candidates.0.finishReason", "STOP")
|
||||
|
||||
(*param).(*ConvertCodexResponseToGeminiParams).LastStorageOutput = append([]byte(nil), template...)
|
||||
params.LastStorageOutput = append([]byte(nil), template...)
|
||||
|
||||
// Use this return to storage message
|
||||
return [][]byte{}
|
||||
@@ -111,15 +113,45 @@ func ConvertCodexResponseToGemini(_ context.Context, modelName string, originalR
|
||||
if typeStr == "response.created" { // Handle response creation - set model and response ID
|
||||
template, _ = sjson.SetBytes(template, "modelVersion", rootResult.Get("response.model").String())
|
||||
template, _ = sjson.SetBytes(template, "responseId", rootResult.Get("response.id").String())
|
||||
(*param).(*ConvertCodexResponseToGeminiParams).ResponseID = rootResult.Get("response.id").String()
|
||||
params.ResponseID = rootResult.Get("response.id").String()
|
||||
} else if typeStr == "response.reasoning_summary_text.delta" { // Handle reasoning/thinking content delta
|
||||
part := []byte(`{"thought":true,"text":""}`)
|
||||
part, _ = sjson.SetBytes(part, "text", rootResult.Get("delta").String())
|
||||
template, _ = sjson.SetRawBytes(template, "candidates.0.content.parts.-1", part)
|
||||
} else if typeStr == "response.output_text.delta" { // Handle regular text content delta
|
||||
params.HasOutputTextDelta = true
|
||||
part := []byte(`{"text":""}`)
|
||||
part, _ = sjson.SetBytes(part, "text", rootResult.Get("delta").String())
|
||||
template, _ = sjson.SetRawBytes(template, "candidates.0.content.parts.-1", part)
|
||||
} else if typeStr == "response.output_item.done" { // Fallback: emit final message text when no delta chunks were received
|
||||
itemResult := rootResult.Get("item")
|
||||
if itemResult.Get("type").String() != "message" || params.HasOutputTextDelta {
|
||||
return [][]byte{}
|
||||
}
|
||||
contentResult := itemResult.Get("content")
|
||||
if !contentResult.Exists() || !contentResult.IsArray() {
|
||||
return [][]byte{}
|
||||
}
|
||||
wroteText := false
|
||||
contentResult.ForEach(func(_, partResult gjson.Result) bool {
|
||||
if partResult.Get("type").String() != "output_text" {
|
||||
return true
|
||||
}
|
||||
text := partResult.Get("text").String()
|
||||
if text == "" {
|
||||
return true
|
||||
}
|
||||
part := []byte(`{"text":""}`)
|
||||
part, _ = sjson.SetBytes(part, "text", text)
|
||||
template, _ = sjson.SetRawBytes(template, "candidates.0.content.parts.-1", part)
|
||||
wroteText = true
|
||||
return true
|
||||
})
|
||||
if wroteText {
|
||||
params.HasOutputTextDelta = true
|
||||
return [][]byte{template}
|
||||
}
|
||||
return [][]byte{}
|
||||
} else if typeStr == "response.completed" { // Handle response completion with usage metadata
|
||||
template, _ = sjson.SetBytes(template, "usageMetadata.promptTokenCount", rootResult.Get("response.usage.input_tokens").Int())
|
||||
template, _ = sjson.SetBytes(template, "usageMetadata.candidatesTokenCount", rootResult.Get("response.usage.output_tokens").Int())
|
||||
@@ -129,11 +161,10 @@ func ConvertCodexResponseToGemini(_ context.Context, modelName string, originalR
|
||||
return [][]byte{}
|
||||
}
|
||||
|
||||
if len((*param).(*ConvertCodexResponseToGeminiParams).LastStorageOutput) > 0 {
|
||||
return [][]byte{
|
||||
append([]byte(nil), (*param).(*ConvertCodexResponseToGeminiParams).LastStorageOutput...),
|
||||
template,
|
||||
}
|
||||
if len(params.LastStorageOutput) > 0 {
|
||||
stored := append([]byte(nil), params.LastStorageOutput...)
|
||||
params.LastStorageOutput = nil
|
||||
return [][]byte{stored, template}
|
||||
}
|
||||
return [][]byte{template}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user