mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
Refactor codebase
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
// Package codex provides utilities to translate OpenAI Chat Completions
|
||||
// Package openai provides utilities to translate OpenAI Chat Completions
|
||||
// request JSON into OpenAI Responses API request JSON using gjson/sjson.
|
||||
// It supports tools, multimodal text/image inputs, and Structured Outputs.
|
||||
// The package handles the conversion of OpenAI API requests into the format
|
||||
// expected by the OpenAI Responses API, including proper mapping of messages,
|
||||
// tools, and generation parameters.
|
||||
package openai
|
||||
|
||||
import (
|
||||
@@ -9,19 +12,25 @@ import (
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
// ConvertOpenAIChatRequestToCodex converts an OpenAI Chat Completions request JSON
|
||||
// ConvertOpenAIRequestToCodex converts an OpenAI Chat Completions request JSON
|
||||
// into an OpenAI Responses API request JSON. The transformation follows the
|
||||
// examples defined in docs/2.md exactly, including tools, multi-turn dialog,
|
||||
// multimodal text/image handling, and Structured Outputs mapping.
|
||||
func ConvertOpenAIChatRequestToCodex(rawJSON []byte) string {
|
||||
//
|
||||
// Parameters:
|
||||
// - modelName: The name of the model to use for the request
|
||||
// - rawJSON: The raw JSON request data from the OpenAI Chat Completions API
|
||||
// - stream: A boolean indicating if the request is for a streaming response
|
||||
//
|
||||
// Returns:
|
||||
// - []byte: The transformed request data in OpenAI Responses API format
|
||||
func ConvertOpenAIRequestToCodex(modelName string, rawJSON []byte, stream bool) []byte {
|
||||
// Start with empty JSON object
|
||||
out := `{}`
|
||||
store := false
|
||||
|
||||
// Stream must be set to true
|
||||
if v := gjson.GetBytes(rawJSON, "stream"); v.Exists() {
|
||||
out, _ = sjson.Set(out, "stream", true)
|
||||
}
|
||||
out, _ = sjson.Set(out, "stream", stream)
|
||||
|
||||
// Codex not support temperature, top_p, top_k, max_output_tokens, so comment them
|
||||
// if v := gjson.GetBytes(rawJSON, "temperature"); v.Exists() {
|
||||
@@ -49,9 +58,7 @@ func ConvertOpenAIChatRequestToCodex(rawJSON []byte) string {
|
||||
}
|
||||
|
||||
// Model
|
||||
if v := gjson.GetBytes(rawJSON, "model"); v.Exists() {
|
||||
out, _ = sjson.Set(out, "model", v.Value())
|
||||
}
|
||||
out, _ = sjson.Set(out, "model", modelName)
|
||||
|
||||
// Extract system instructions from first system message (string or text object)
|
||||
messages := gjson.GetBytes(rawJSON, "messages")
|
||||
@@ -257,5 +264,5 @@ func ConvertOpenAIChatRequestToCodex(rawJSON []byte) string {
|
||||
}
|
||||
|
||||
out, _ = sjson.Set(out, "store", store)
|
||||
return out
|
||||
return []byte(out)
|
||||
}
|
||||
|
||||
@@ -1,27 +1,59 @@
|
||||
// Package codex provides response translation functionality for converting between
|
||||
// Codex API response formats and OpenAI-compatible formats. It handles both
|
||||
// streaming and non-streaming responses, transforming backend client responses
|
||||
// into OpenAI Server-Sent Events (SSE) format and standard JSON response formats.
|
||||
// The package supports content translation, function calls, reasoning content,
|
||||
// usage metadata, and various response attributes while maintaining compatibility
|
||||
// with OpenAI API specifications.
|
||||
// Package openai provides response translation functionality for Codex to OpenAI API compatibility.
|
||||
// This package handles the conversion of Codex API responses into OpenAI Chat Completions-compatible
|
||||
// JSON format, transforming streaming events and non-streaming responses into the format
|
||||
// expected by OpenAI API clients. It supports both streaming and non-streaming modes,
|
||||
// handling text content, tool calls, reasoning content, and usage metadata appropriately.
|
||||
package openai
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
var (
|
||||
dataTag = []byte("data: ")
|
||||
)
|
||||
|
||||
// ConvertCliToOpenAIParams holds parameters for response conversion.
|
||||
type ConvertCliToOpenAIParams struct {
|
||||
ResponseID string
|
||||
CreatedAt int64
|
||||
Model string
|
||||
}
|
||||
|
||||
// ConvertCodexResponseToOpenAIChat translates a single chunk of a streaming response from the
|
||||
// Codex backend client format to the OpenAI Server-Sent Events (SSE) format.
|
||||
// It returns an empty string if the chunk contains no useful data.
|
||||
func ConvertCodexResponseToOpenAIChat(rawJSON []byte, params *ConvertCliToOpenAIParams) (*ConvertCliToOpenAIParams, string) {
|
||||
// ConvertCodexResponseToOpenAI translates a single chunk of a streaming response from the
|
||||
// Codex API format to the OpenAI Chat Completions streaming format.
|
||||
// It processes various Codex event types and transforms them into OpenAI-compatible JSON responses.
|
||||
// The function handles text content, tool calls, reasoning content, and usage metadata, outputting
|
||||
// responses that match the OpenAI API format. It supports incremental updates for streaming responses.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: The context for the request, used for cancellation and timeout handling
|
||||
// - modelName: The name of the model being used for the response
|
||||
// - rawJSON: The raw JSON response from the Codex API
|
||||
// - param: A pointer to a parameter object for maintaining state between calls
|
||||
//
|
||||
// Returns:
|
||||
// - []string: A slice of strings, each containing an OpenAI-compatible JSON response
|
||||
func ConvertCodexResponseToOpenAI(_ context.Context, modelName string, rawJSON []byte, param *any) []string {
|
||||
if *param == nil {
|
||||
*param = &ConvertCliToOpenAIParams{
|
||||
Model: modelName,
|
||||
CreatedAt: 0,
|
||||
ResponseID: "",
|
||||
}
|
||||
}
|
||||
|
||||
if !bytes.HasPrefix(rawJSON, dataTag) {
|
||||
return []string{}
|
||||
}
|
||||
rawJSON = rawJSON[6:]
|
||||
|
||||
// Initialize the OpenAI SSE template.
|
||||
template := `{"id":"","object":"chat.completion.chunk","created":12345,"model":"model","choices":[{"index":0,"delta":{"role":null,"content":null,"reasoning_content":null,"tool_calls":null},"finish_reason":null,"native_finish_reason":null}]}`
|
||||
|
||||
@@ -30,15 +62,10 @@ func ConvertCodexResponseToOpenAIChat(rawJSON []byte, params *ConvertCliToOpenAI
|
||||
typeResult := rootResult.Get("type")
|
||||
dataType := typeResult.String()
|
||||
if dataType == "response.created" {
|
||||
return &ConvertCliToOpenAIParams{
|
||||
ResponseID: rootResult.Get("response.id").String(),
|
||||
CreatedAt: rootResult.Get("response.created_at").Int(),
|
||||
Model: rootResult.Get("response.model").String(),
|
||||
}, ""
|
||||
}
|
||||
|
||||
if params == nil {
|
||||
return params, ""
|
||||
(*param).(*ConvertCliToOpenAIParams).ResponseID = rootResult.Get("response.id").String()
|
||||
(*param).(*ConvertCliToOpenAIParams).CreatedAt = rootResult.Get("response.created_at").Int()
|
||||
(*param).(*ConvertCliToOpenAIParams).Model = rootResult.Get("response.model").String()
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// Extract and set the model version.
|
||||
@@ -46,10 +73,10 @@ func ConvertCodexResponseToOpenAIChat(rawJSON []byte, params *ConvertCliToOpenAI
|
||||
template, _ = sjson.Set(template, "model", modelResult.String())
|
||||
}
|
||||
|
||||
template, _ = sjson.Set(template, "created", params.CreatedAt)
|
||||
template, _ = sjson.Set(template, "created", (*param).(*ConvertCliToOpenAIParams).CreatedAt)
|
||||
|
||||
// Extract and set the response ID.
|
||||
template, _ = sjson.Set(template, "id", params.ResponseID)
|
||||
template, _ = sjson.Set(template, "id", (*param).(*ConvertCliToOpenAIParams).ResponseID)
|
||||
|
||||
// Extract and set usage metadata (token counts).
|
||||
if usageResult := gjson.GetBytes(rawJSON, "response.usage"); usageResult.Exists() {
|
||||
@@ -88,7 +115,7 @@ func ConvertCodexResponseToOpenAIChat(rawJSON []byte, params *ConvertCliToOpenAI
|
||||
itemResult := rootResult.Get("item")
|
||||
if itemResult.Exists() {
|
||||
if itemResult.Get("type").String() != "function_call" {
|
||||
return params, ""
|
||||
return []string{}
|
||||
}
|
||||
template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls", `[]`)
|
||||
functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "id", itemResult.Get("call_id").String())
|
||||
@@ -99,133 +126,166 @@ func ConvertCodexResponseToOpenAIChat(rawJSON []byte, params *ConvertCliToOpenAI
|
||||
}
|
||||
|
||||
} else {
|
||||
return params, ""
|
||||
return []string{}
|
||||
}
|
||||
|
||||
return params, template
|
||||
return []string{template}
|
||||
}
|
||||
|
||||
// ConvertCodexResponseToOpenAIChatNonStream aggregates response from the Codex backend client
|
||||
// convert a single, non-streaming OpenAI-compatible JSON response.
|
||||
func ConvertCodexResponseToOpenAIChatNonStream(rawJSON string, unixTimestamp int64) string {
|
||||
template := `{"id":"","object":"chat.completion","created":123456,"model":"model","choices":[{"index":0,"message":{"role":"assistant","content":null,"reasoning_content":null,"tool_calls":null},"finish_reason":null,"native_finish_reason":null}]}`
|
||||
|
||||
// Extract and set the model version.
|
||||
if modelResult := gjson.Get(rawJSON, "model"); modelResult.Exists() {
|
||||
template, _ = sjson.Set(template, "model", modelResult.String())
|
||||
}
|
||||
|
||||
// Extract and set the creation timestamp.
|
||||
if createdAtResult := gjson.Get(rawJSON, "created_at"); createdAtResult.Exists() {
|
||||
template, _ = sjson.Set(template, "created", createdAtResult.Int())
|
||||
} else {
|
||||
template, _ = sjson.Set(template, "created", unixTimestamp)
|
||||
}
|
||||
|
||||
// Extract and set the response ID.
|
||||
if idResult := gjson.Get(rawJSON, "id"); idResult.Exists() {
|
||||
template, _ = sjson.Set(template, "id", idResult.String())
|
||||
}
|
||||
|
||||
// Extract and set usage metadata (token counts).
|
||||
if usageResult := gjson.Get(rawJSON, "usage"); usageResult.Exists() {
|
||||
if outputTokensResult := usageResult.Get("output_tokens"); outputTokensResult.Exists() {
|
||||
template, _ = sjson.Set(template, "usage.completion_tokens", outputTokensResult.Int())
|
||||
// ConvertCodexResponseToOpenAINonStream converts a non-streaming Codex response to a non-streaming OpenAI response.
|
||||
// This function processes the complete Codex response and transforms it into a single OpenAI-compatible
|
||||
// JSON response. It handles message content, tool calls, reasoning content, and usage metadata, combining all
|
||||
// the information into a single response that matches the OpenAI API format.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: The context for the request, used for cancellation and timeout handling
|
||||
// - modelName: The name of the model being used for the response (unused in current implementation)
|
||||
// - rawJSON: The raw JSON response from the Codex API
|
||||
// - param: A pointer to a parameter object for the conversion (unused in current implementation)
|
||||
//
|
||||
// Returns:
|
||||
// - string: An OpenAI-compatible JSON response containing all message content and metadata
|
||||
func ConvertCodexResponseToOpenAINonStream(_ context.Context, _ string, rawJSON []byte, _ *any) string {
|
||||
scanner := bufio.NewScanner(bytes.NewReader(rawJSON))
|
||||
buffer := make([]byte, 10240*1024)
|
||||
scanner.Buffer(buffer, 10240*1024)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Bytes()
|
||||
// log.Debug(string(line))
|
||||
if !bytes.HasPrefix(line, dataTag) {
|
||||
continue
|
||||
}
|
||||
if totalTokensResult := usageResult.Get("total_tokens"); totalTokensResult.Exists() {
|
||||
template, _ = sjson.Set(template, "usage.total_tokens", totalTokensResult.Int())
|
||||
rawJSON = line[6:]
|
||||
|
||||
rootResult := gjson.ParseBytes(rawJSON)
|
||||
// Verify this is a response.completed event
|
||||
if rootResult.Get("type").String() != "response.completed" {
|
||||
continue
|
||||
}
|
||||
if inputTokensResult := usageResult.Get("input_tokens"); inputTokensResult.Exists() {
|
||||
template, _ = sjson.Set(template, "usage.prompt_tokens", inputTokensResult.Int())
|
||||
unixTimestamp := time.Now().Unix()
|
||||
|
||||
responseResult := rootResult.Get("response")
|
||||
|
||||
template := `{"id":"","object":"chat.completion","created":123456,"model":"model","choices":[{"index":0,"message":{"role":"assistant","content":null,"reasoning_content":null,"tool_calls":null},"finish_reason":null,"native_finish_reason":null}]}`
|
||||
|
||||
// Extract and set the model version.
|
||||
if modelResult := responseResult.Get("model"); modelResult.Exists() {
|
||||
template, _ = sjson.Set(template, "model", modelResult.String())
|
||||
}
|
||||
if reasoningTokensResult := usageResult.Get("output_tokens_details.reasoning_tokens"); reasoningTokensResult.Exists() {
|
||||
template, _ = sjson.Set(template, "usage.completion_tokens_details.reasoning_tokens", reasoningTokensResult.Int())
|
||||
|
||||
// Extract and set the creation timestamp.
|
||||
if createdAtResult := responseResult.Get("created_at"); createdAtResult.Exists() {
|
||||
template, _ = sjson.Set(template, "created", createdAtResult.Int())
|
||||
} else {
|
||||
template, _ = sjson.Set(template, "created", unixTimestamp)
|
||||
}
|
||||
}
|
||||
|
||||
// Process the output array for content and function calls
|
||||
outputResult := gjson.Get(rawJSON, "output")
|
||||
if outputResult.IsArray() {
|
||||
outputArray := outputResult.Array()
|
||||
var contentText string
|
||||
var reasoningText string
|
||||
var toolCalls []string
|
||||
// Extract and set the response ID.
|
||||
if idResult := responseResult.Get("id"); idResult.Exists() {
|
||||
template, _ = sjson.Set(template, "id", idResult.String())
|
||||
}
|
||||
|
||||
for _, outputItem := range outputArray {
|
||||
outputType := outputItem.Get("type").String()
|
||||
|
||||
switch outputType {
|
||||
case "reasoning":
|
||||
// Extract reasoning content from summary
|
||||
if summaryResult := outputItem.Get("summary"); summaryResult.IsArray() {
|
||||
summaryArray := summaryResult.Array()
|
||||
for _, summaryItem := range summaryArray {
|
||||
if summaryItem.Get("type").String() == "summary_text" {
|
||||
reasoningText = summaryItem.Get("text").String()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
case "message":
|
||||
// Extract message content
|
||||
if contentResult := outputItem.Get("content"); contentResult.IsArray() {
|
||||
contentArray := contentResult.Array()
|
||||
for _, contentItem := range contentArray {
|
||||
if contentItem.Get("type").String() == "output_text" {
|
||||
contentText = contentItem.Get("text").String()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
case "function_call":
|
||||
// Handle function call content
|
||||
functionCallTemplate := `{"id": "","type": "function","function": {"name": "","arguments": ""}}`
|
||||
|
||||
if callIdResult := outputItem.Get("call_id"); callIdResult.Exists() {
|
||||
functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", callIdResult.String())
|
||||
}
|
||||
|
||||
if nameResult := outputItem.Get("name"); nameResult.Exists() {
|
||||
functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", nameResult.String())
|
||||
}
|
||||
|
||||
if argsResult := outputItem.Get("arguments"); argsResult.Exists() {
|
||||
functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.arguments", argsResult.String())
|
||||
}
|
||||
|
||||
toolCalls = append(toolCalls, functionCallTemplate)
|
||||
// Extract and set usage metadata (token counts).
|
||||
if usageResult := responseResult.Get("usage"); usageResult.Exists() {
|
||||
if outputTokensResult := usageResult.Get("output_tokens"); outputTokensResult.Exists() {
|
||||
template, _ = sjson.Set(template, "usage.completion_tokens", outputTokensResult.Int())
|
||||
}
|
||||
if totalTokensResult := usageResult.Get("total_tokens"); totalTokensResult.Exists() {
|
||||
template, _ = sjson.Set(template, "usage.total_tokens", totalTokensResult.Int())
|
||||
}
|
||||
if inputTokensResult := usageResult.Get("input_tokens"); inputTokensResult.Exists() {
|
||||
template, _ = sjson.Set(template, "usage.prompt_tokens", inputTokensResult.Int())
|
||||
}
|
||||
if reasoningTokensResult := usageResult.Get("output_tokens_details.reasoning_tokens"); reasoningTokensResult.Exists() {
|
||||
template, _ = sjson.Set(template, "usage.completion_tokens_details.reasoning_tokens", reasoningTokensResult.Int())
|
||||
}
|
||||
}
|
||||
|
||||
// Set content and reasoning content if found
|
||||
if contentText != "" {
|
||||
template, _ = sjson.Set(template, "choices.0.message.content", contentText)
|
||||
template, _ = sjson.Set(template, "choices.0.message.role", "assistant")
|
||||
}
|
||||
// Process the output array for content and function calls
|
||||
outputResult := responseResult.Get("output")
|
||||
if outputResult.IsArray() {
|
||||
outputArray := outputResult.Array()
|
||||
var contentText string
|
||||
var reasoningText string
|
||||
var toolCalls []string
|
||||
|
||||
if reasoningText != "" {
|
||||
template, _ = sjson.Set(template, "choices.0.message.reasoning_content", reasoningText)
|
||||
template, _ = sjson.Set(template, "choices.0.message.role", "assistant")
|
||||
}
|
||||
for _, outputItem := range outputArray {
|
||||
outputType := outputItem.Get("type").String()
|
||||
|
||||
// Add tool calls if any
|
||||
if len(toolCalls) > 0 {
|
||||
template, _ = sjson.SetRaw(template, "choices.0.message.tool_calls", `[]`)
|
||||
for _, toolCall := range toolCalls {
|
||||
template, _ = sjson.SetRaw(template, "choices.0.message.tool_calls.-1", toolCall)
|
||||
switch outputType {
|
||||
case "reasoning":
|
||||
// Extract reasoning content from summary
|
||||
if summaryResult := outputItem.Get("summary"); summaryResult.IsArray() {
|
||||
summaryArray := summaryResult.Array()
|
||||
for _, summaryItem := range summaryArray {
|
||||
if summaryItem.Get("type").String() == "summary_text" {
|
||||
reasoningText = summaryItem.Get("text").String()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
case "message":
|
||||
// Extract message content
|
||||
if contentResult := outputItem.Get("content"); contentResult.IsArray() {
|
||||
contentArray := contentResult.Array()
|
||||
for _, contentItem := range contentArray {
|
||||
if contentItem.Get("type").String() == "output_text" {
|
||||
contentText = contentItem.Get("text").String()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
case "function_call":
|
||||
// Handle function call content
|
||||
functionCallTemplate := `{"id": "","type": "function","function": {"name": "","arguments": ""}}`
|
||||
|
||||
if callIdResult := outputItem.Get("call_id"); callIdResult.Exists() {
|
||||
functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", callIdResult.String())
|
||||
}
|
||||
|
||||
if nameResult := outputItem.Get("name"); nameResult.Exists() {
|
||||
functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", nameResult.String())
|
||||
}
|
||||
|
||||
if argsResult := outputItem.Get("arguments"); argsResult.Exists() {
|
||||
functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.arguments", argsResult.String())
|
||||
}
|
||||
|
||||
toolCalls = append(toolCalls, functionCallTemplate)
|
||||
}
|
||||
}
|
||||
template, _ = sjson.Set(template, "choices.0.message.role", "assistant")
|
||||
}
|
||||
}
|
||||
|
||||
// Extract and set the finish reason based on status
|
||||
if statusResult := gjson.Get(rawJSON, "status"); statusResult.Exists() {
|
||||
status := statusResult.String()
|
||||
if status == "completed" {
|
||||
template, _ = sjson.Set(template, "choices.0.finish_reason", "stop")
|
||||
template, _ = sjson.Set(template, "choices.0.native_finish_reason", "stop")
|
||||
}
|
||||
}
|
||||
// Set content and reasoning content if found
|
||||
if contentText != "" {
|
||||
template, _ = sjson.Set(template, "choices.0.message.content", contentText)
|
||||
template, _ = sjson.Set(template, "choices.0.message.role", "assistant")
|
||||
}
|
||||
|
||||
return template
|
||||
if reasoningText != "" {
|
||||
template, _ = sjson.Set(template, "choices.0.message.reasoning_content", reasoningText)
|
||||
template, _ = sjson.Set(template, "choices.0.message.role", "assistant")
|
||||
}
|
||||
|
||||
// Add tool calls if any
|
||||
if len(toolCalls) > 0 {
|
||||
template, _ = sjson.SetRaw(template, "choices.0.message.tool_calls", `[]`)
|
||||
for _, toolCall := range toolCalls {
|
||||
template, _ = sjson.SetRaw(template, "choices.0.message.tool_calls.-1", toolCall)
|
||||
}
|
||||
template, _ = sjson.Set(template, "choices.0.message.role", "assistant")
|
||||
}
|
||||
}
|
||||
|
||||
// Extract and set the finish reason based on status
|
||||
if statusResult := responseResult.Get("status"); statusResult.Exists() {
|
||||
status := statusResult.String()
|
||||
if status == "completed" {
|
||||
template, _ = sjson.Set(template, "choices.0.finish_reason", "stop")
|
||||
template, _ = sjson.Set(template, "choices.0.native_finish_reason", "stop")
|
||||
}
|
||||
}
|
||||
|
||||
return template
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
19
internal/translator/codex/openai/init.go
Normal file
19
internal/translator/codex/openai/init.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package openai
|
||||
|
||||
import (
|
||||
. "github.com/luispater/CLIProxyAPI/internal/constant"
|
||||
"github.com/luispater/CLIProxyAPI/internal/interfaces"
|
||||
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
translator.Register(
|
||||
OPENAI,
|
||||
CODEX,
|
||||
ConvertOpenAIRequestToCodex,
|
||||
interfaces.TranslateResponse{
|
||||
Stream: ConvertCodexResponseToOpenAI,
|
||||
NonStream: ConvertCodexResponseToOpenAINonStream,
|
||||
},
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user