diff --git a/go.mod b/go.mod index 842cb74f..91e344da 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/tidwall/gjson v1.18.0 github.com/tidwall/sjson v1.2.5 + golang.org/x/crypto v0.36.0 golang.org/x/net v0.37.1-0.20250305215238-2914f4677317 golang.org/x/oauth2 v0.30.0 gopkg.in/yaml.v3 v3.0.1 @@ -39,7 +40,6 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect golang.org/x/arch v0.8.0 // indirect - golang.org/x/crypto v0.36.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.23.0 // indirect google.golang.org/protobuf v1.34.1 // indirect diff --git a/internal/api/handlers/openai/openai_responses_handlers.go b/internal/api/handlers/openai/openai_responses_handlers.go new file mode 100644 index 00000000..bc92e4da --- /dev/null +++ b/internal/api/handlers/openai/openai_responses_handlers.go @@ -0,0 +1,474 @@ +// Package openai provides HTTP handlers for OpenAIResponses API endpoints. +// This package implements the OpenAIResponses-compatible API interface, including model listing +// and chat completion functionality. It supports both streaming and non-streaming responses, +// and manages a pool of clients to interact with backend services. +// The handlers translate OpenAIResponses API requests to the appropriate backend format and +// convert responses back to OpenAIResponses-compatible format. +package openai + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/luispater/CLIProxyAPI/internal/api/handlers" + . "github.com/luispater/CLIProxyAPI/internal/constant" + "github.com/luispater/CLIProxyAPI/internal/interfaces" + "github.com/luispater/CLIProxyAPI/internal/registry" + "github.com/luispater/CLIProxyAPI/internal/util" + log "github.com/sirupsen/logrus" + "github.com/tidwall/gjson" +) + +// OpenAIResponsesAPIHandler contains the handlers for OpenAIResponses API endpoints. +// It holds a pool of clients to interact with the backend service. +type OpenAIResponsesAPIHandler struct { + *handlers.BaseAPIHandler +} + +// NewOpenAIResponsesAPIHandler creates a new OpenAIResponses API handlers instance. +// It takes an BaseAPIHandler instance as input and returns an OpenAIResponsesAPIHandler. +// +// Parameters: +// - apiHandlers: The base API handlers instance +// +// Returns: +// - *OpenAIResponsesAPIHandler: A new OpenAIResponses API handlers instance +func NewOpenAIResponsesAPIHandler(apiHandlers *handlers.BaseAPIHandler) *OpenAIResponsesAPIHandler { + return &OpenAIResponsesAPIHandler{ + BaseAPIHandler: apiHandlers, + } +} + +// HandlerType returns the identifier for this handler implementation. +func (h *OpenAIResponsesAPIHandler) HandlerType() string { + return OPENAI_RESPONSE +} + +// Models returns the OpenAIResponses-compatible model metadata supported by this handler. +func (h *OpenAIResponsesAPIHandler) Models() []map[string]any { + // Get dynamic models from the global registry + modelRegistry := registry.GetGlobalRegistry() + return modelRegistry.GetAvailableModels("openai") +} + +// OpenAIResponsesModels handles the /v1/models endpoint. +// It returns a list of available AI models with their capabilities +// and specifications in OpenAIResponses-compatible format. +func (h *OpenAIResponsesAPIHandler) OpenAIResponsesModels(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "object": "list", + "data": h.Models(), + }) +} + +// ChatCompletions handles the /v1/chat/completions endpoint. +// It determines whether the request is for a streaming or non-streaming response +// and calls the appropriate handler based on the model provider. +// +// Parameters: +// - c: The Gin context containing the HTTP request and response +func (h *OpenAIResponsesAPIHandler) ChatCompletions(c *gin.Context) { + rawJSON, err := c.GetRawData() + // If data retrieval fails, return a 400 Bad Request error. + if err != nil { + c.JSON(http.StatusBadRequest, handlers.ErrorResponse{ + Error: handlers.ErrorDetail{ + Message: fmt.Sprintf("Invalid request: %v", err), + Type: "invalid_request_error", + }, + }) + return + } + + // Check if the client requested a streaming response. + streamResult := gjson.GetBytes(rawJSON, "stream") + if streamResult.Type == gjson.True { + h.handleStreamingResponse(c, rawJSON) + } else { + h.handleNonStreamingResponse(c, rawJSON) + } + +} + +// Completions handles the /v1/completions endpoint. +// It determines whether the request is for a streaming or non-streaming response +// and calls the appropriate handler based on the model provider. +// This endpoint follows the OpenAIResponses completions API specification. +// +// Parameters: +// - c: The Gin context containing the HTTP request and response +func (h *OpenAIResponsesAPIHandler) Completions(c *gin.Context) { + rawJSON, err := c.GetRawData() + // If data retrieval fails, return a 400 Bad Request error. + if err != nil { + c.JSON(http.StatusBadRequest, handlers.ErrorResponse{ + Error: handlers.ErrorDetail{ + Message: fmt.Sprintf("Invalid request: %v", err), + Type: "invalid_request_error", + }, + }) + return + } + + // Check if the client requested a streaming response. + streamResult := gjson.GetBytes(rawJSON, "stream") + if streamResult.Type == gjson.True { + h.handleCompletionsStreamingResponse(c, rawJSON) + } else { + h.handleCompletionsNonStreamingResponse(c, rawJSON) + } + +} + +// handleNonStreamingResponse handles non-streaming chat completion responses +// for Gemini models. It selects a client from the pool, sends the request, and +// aggregates the response before sending it back to the client in OpenAIResponses format. +// +// Parameters: +// - c: The Gin context containing the HTTP request and response +// - rawJSON: The raw JSON bytes of the OpenAIResponses-compatible request +func (h *OpenAIResponsesAPIHandler) handleNonStreamingResponse(c *gin.Context, rawJSON []byte) { + c.Header("Content-Type", "application/json") + + modelName := gjson.GetBytes(rawJSON, "model").String() + cliCtx, cliCancel := h.GetContextWithCancel(h, c, context.Background()) + + var cliClient interfaces.Client + defer func() { + if cliClient != nil { + if mutex := cliClient.GetRequestMutex(); mutex != nil { + mutex.Unlock() + } + } + }() + + retryCount := 0 + for retryCount <= h.Cfg.RequestRetry { + var errorResponse *interfaces.ErrorMessage + cliClient, errorResponse = h.GetClient(modelName) + if errorResponse != nil { + c.Status(errorResponse.StatusCode) + _, _ = fmt.Fprint(c.Writer, errorResponse.Error.Error()) + cliCancel() + return + } + + resp, err := cliClient.SendRawMessage(cliCtx, modelName, rawJSON, "") + if err != nil { + switch err.StatusCode { + case 429: + if h.Cfg.QuotaExceeded.SwitchProject { + log.Debugf("quota exceeded, switch client") + continue // Restart the client selection process + } + case 403, 408, 500, 502, 503, 504: + log.Debugf("http status code %d, switch client", err.StatusCode) + retryCount++ + continue + case 401: + log.Debugf("unauthorized request, try to refresh token, %s", util.HideAPIKey(cliClient.GetEmail())) + errRefreshTokens := cliClient.RefreshTokens(cliCtx) + if errRefreshTokens != nil { + log.Debugf("refresh token failed, switch client, %s", util.HideAPIKey(cliClient.GetEmail())) + } + retryCount++ + continue + default: + // Forward other errors directly to the client + c.Status(err.StatusCode) + _, _ = c.Writer.Write([]byte(err.Error.Error())) + cliCancel(err.Error) + } + break + } else { + _, _ = c.Writer.Write(resp) + cliCancel(resp) + break + } + } +} + +// handleStreamingResponse handles streaming responses for Gemini models. +// It establishes a streaming connection with the backend service and forwards +// the response chunks to the client in real-time using Server-Sent Events. +// +// Parameters: +// - c: The Gin context containing the HTTP request and response +// - rawJSON: The raw JSON bytes of the OpenAIResponses-compatible request +func (h *OpenAIResponsesAPIHandler) handleStreamingResponse(c *gin.Context, rawJSON []byte) { + c.Header("Content-Type", "text/event-stream") + c.Header("Cache-Control", "no-cache") + c.Header("Connection", "keep-alive") + c.Header("Access-Control-Allow-Origin", "*") + + // Get the http.Flusher interface to manually flush the response. + flusher, ok := c.Writer.(http.Flusher) + if !ok { + c.JSON(http.StatusInternalServerError, handlers.ErrorResponse{ + Error: handlers.ErrorDetail{ + Message: "Streaming not supported", + Type: "server_error", + }, + }) + return + } + + modelName := gjson.GetBytes(rawJSON, "model").String() + cliCtx, cliCancel := h.GetContextWithCancel(h, c, context.Background()) + + var cliClient interfaces.Client + defer func() { + // Ensure the client's mutex is unlocked on function exit. + if cliClient != nil { + if mutex := cliClient.GetRequestMutex(); mutex != nil { + mutex.Unlock() + } + } + }() + + retryCount := 0 +outLoop: + for retryCount <= h.Cfg.RequestRetry { + var errorResponse *interfaces.ErrorMessage + cliClient, errorResponse = h.GetClient(modelName) + if errorResponse != nil { + c.Status(errorResponse.StatusCode) + _, _ = fmt.Fprint(c.Writer, errorResponse.Error.Error()) + flusher.Flush() + cliCancel() + return + } + + // Send the message and receive response chunks and errors via channels. + respChan, errChan := cliClient.SendRawMessageStream(cliCtx, modelName, rawJSON, "") + + for { + select { + // Handle client disconnection. + case <-c.Request.Context().Done(): + if c.Request.Context().Err().Error() == "context canceled" { + log.Debugf("openai client disconnected: %v", c.Request.Context().Err()) + cliCancel() // Cancel the backend request. + return + } + // Process incoming response chunks. + case chunk, okStream := <-respChan: + if !okStream { + // Stream is closed, send the final [DONE] message. + _, _ = fmt.Fprintf(c.Writer, "data: [DONE]\n\n") + flusher.Flush() + cliCancel() + return + } + + _, _ = fmt.Fprintf(c.Writer, "data: %s\n\n", string(chunk)) + flusher.Flush() + // Handle errors from the backend. + case err, okError := <-errChan: + if okError { + switch err.StatusCode { + case 429: + if h.Cfg.QuotaExceeded.SwitchProject { + log.Debugf("quota exceeded, switch client") + continue outLoop // Restart the client selection process + } + case 403, 408, 500, 502, 503, 504: + log.Debugf("http status code %d, switch client", err.StatusCode) + retryCount++ + continue outLoop + default: + // Forward other errors directly to the client + c.Status(err.StatusCode) + _, _ = fmt.Fprint(c.Writer, err.Error.Error()) + flusher.Flush() + cliCancel(err.Error) + } + return + } + // Send a keep-alive signal to the client. + case <-time.After(500 * time.Millisecond): + } + } + } +} + +// handleCompletionsNonStreamingResponse handles non-streaming completions responses. +// It converts completions request to chat completions format, sends to backend, +// then converts the response back to completions format before sending to client. +// +// Parameters: +// - c: The Gin context containing the HTTP request and response +// - rawJSON: The raw JSON bytes of the OpenAIResponses-compatible completions request +func (h *OpenAIResponsesAPIHandler) handleCompletionsNonStreamingResponse(c *gin.Context, rawJSON []byte) { + c.Header("Content-Type", "application/json") + + // Convert completions request to chat completions format + chatCompletionsJSON := convertCompletionsRequestToChatCompletions(rawJSON) + + modelName := gjson.GetBytes(chatCompletionsJSON, "model").String() + cliCtx, cliCancel := h.GetContextWithCancel(h, c, context.Background()) + + var cliClient interfaces.Client + defer func() { + if cliClient != nil { + if mutex := cliClient.GetRequestMutex(); mutex != nil { + mutex.Unlock() + } + } + }() + + retryCount := 0 + for retryCount <= h.Cfg.RequestRetry { + var errorResponse *interfaces.ErrorMessage + cliClient, errorResponse = h.GetClient(modelName) + if errorResponse != nil { + c.Status(errorResponse.StatusCode) + _, _ = fmt.Fprint(c.Writer, errorResponse.Error.Error()) + cliCancel() + return + } + + // Send the converted chat completions request + resp, err := cliClient.SendRawMessage(cliCtx, modelName, chatCompletionsJSON, "") + if err != nil { + switch err.StatusCode { + case 429: + if h.Cfg.QuotaExceeded.SwitchProject { + log.Debugf("quota exceeded, switch client") + continue // Restart the client selection process + } + case 403, 408, 500, 502, 503, 504: + log.Debugf("http status code %d, switch client", err.StatusCode) + retryCount++ + continue + default: + // Forward other errors directly to the client + c.Status(err.StatusCode) + _, _ = c.Writer.Write([]byte(err.Error.Error())) + cliCancel(err.Error) + } + break + } else { + // Convert chat completions response back to completions format + completionsResp := convertChatCompletionsResponseToCompletions(resp) + _, _ = c.Writer.Write(completionsResp) + cliCancel(completionsResp) + break + } + } +} + +// handleCompletionsStreamingResponse handles streaming completions responses. +// It converts completions request to chat completions format, streams from backend, +// then converts each response chunk back to completions format before sending to client. +// +// Parameters: +// - c: The Gin context containing the HTTP request and response +// - rawJSON: The raw JSON bytes of the OpenAIResponses-compatible completions request +func (h *OpenAIResponsesAPIHandler) handleCompletionsStreamingResponse(c *gin.Context, rawJSON []byte) { + c.Header("Content-Type", "text/event-stream") + c.Header("Cache-Control", "no-cache") + c.Header("Connection", "keep-alive") + c.Header("Access-Control-Allow-Origin", "*") + + // Get the http.Flusher interface to manually flush the response. + flusher, ok := c.Writer.(http.Flusher) + if !ok { + c.JSON(http.StatusInternalServerError, handlers.ErrorResponse{ + Error: handlers.ErrorDetail{ + Message: "Streaming not supported", + Type: "server_error", + }, + }) + return + } + + // Convert completions request to chat completions format + chatCompletionsJSON := convertCompletionsRequestToChatCompletions(rawJSON) + + modelName := gjson.GetBytes(chatCompletionsJSON, "model").String() + cliCtx, cliCancel := h.GetContextWithCancel(h, c, context.Background()) + + var cliClient interfaces.Client + defer func() { + // Ensure the client's mutex is unlocked on function exit. + if cliClient != nil { + if mutex := cliClient.GetRequestMutex(); mutex != nil { + mutex.Unlock() + } + } + }() + + retryCount := 0 +outLoop: + for retryCount <= h.Cfg.RequestRetry { + var errorResponse *interfaces.ErrorMessage + cliClient, errorResponse = h.GetClient(modelName) + if errorResponse != nil { + c.Status(errorResponse.StatusCode) + _, _ = fmt.Fprint(c.Writer, errorResponse.Error.Error()) + flusher.Flush() + cliCancel() + return + } + + // Send the converted chat completions request and receive response chunks + respChan, errChan := cliClient.SendRawMessageStream(cliCtx, modelName, chatCompletionsJSON, "") + + for { + select { + // Handle client disconnection. + case <-c.Request.Context().Done(): + if c.Request.Context().Err().Error() == "context canceled" { + log.Debugf("client disconnected: %v", c.Request.Context().Err()) + cliCancel() // Cancel the backend request. + return + } + // Process incoming response chunks. + case chunk, okStream := <-respChan: + if !okStream { + // Stream is closed, send the final [DONE] message. + _, _ = fmt.Fprintf(c.Writer, "data: [DONE]\n\n") + flusher.Flush() + cliCancel() + return + } + + // Convert chat completions chunk to completions chunk format + completionsChunk := convertChatCompletionsStreamChunkToCompletions(chunk) + // Skip this chunk if it has no meaningful content (empty text) + if completionsChunk != nil { + _, _ = fmt.Fprintf(c.Writer, "data: %s\n\n", string(completionsChunk)) + flusher.Flush() + } + // Handle errors from the backend. + case err, okError := <-errChan: + if okError { + switch err.StatusCode { + case 429: + if h.Cfg.QuotaExceeded.SwitchProject { + log.Debugf("quota exceeded, switch client") + continue outLoop // Restart the client selection process + } + case 403, 408, 500, 502, 503, 504: + log.Debugf("http status code %d, switch client", err.StatusCode) + retryCount++ + continue outLoop + default: + // Forward other errors directly to the client + c.Status(err.StatusCode) + _, _ = fmt.Fprint(c.Writer, err.Error.Error()) + flusher.Flush() + cliCancel(err.Error) + } + return + } + // Send a keep-alive signal to the client. + case <-time.After(500 * time.Millisecond): + } + } + } +} diff --git a/internal/constant/constant.go b/internal/constant/constant.go index 30e3644e..4e39d93f 100644 --- a/internal/constant/constant.go +++ b/internal/constant/constant.go @@ -1,10 +1,10 @@ package constant const ( - GEMINI = "gemini" - GEMINICLI = "gemini-cli" - CODEX = "codex" - CLAUDE = "claude" - OPENAI = "openai" - OPENAI_COMPATIBILITY = "openai-compatibility" + GEMINI = "gemini" + GEMINICLI = "gemini-cli" + CODEX = "codex" + CLAUDE = "claude" + OPENAI = "openai" + OPENAI_RESPONSE = "openai-response" ) diff --git a/internal/translator/claude/openai/claude_openai_request.go b/internal/translator/claude/openai/chat-completions/claude_openai_request.go similarity index 99% rename from internal/translator/claude/openai/claude_openai_request.go rename to internal/translator/claude/openai/chat-completions/claude_openai_request.go index b65334bf..dce6f248 100644 --- a/internal/translator/claude/openai/claude_openai_request.go +++ b/internal/translator/claude/openai/chat-completions/claude_openai_request.go @@ -3,7 +3,7 @@ // extracting model information, system instructions, message contents, and tool declarations. // The package performs JSON data transformation to ensure compatibility // between OpenAI API format and Claude Code API's expected format. -package openai +package chat_completions import ( "crypto/rand" diff --git a/internal/translator/claude/openai/claude_openai_response.go b/internal/translator/claude/openai/chat-completions/claude_openai_response.go similarity index 99% rename from internal/translator/claude/openai/claude_openai_response.go rename to internal/translator/claude/openai/chat-completions/claude_openai_response.go index 9b4fd8c9..f6b6b3da 100644 --- a/internal/translator/claude/openai/claude_openai_response.go +++ b/internal/translator/claude/openai/chat-completions/claude_openai_response.go @@ -3,7 +3,7 @@ // 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 +package chat_completions import ( "bufio" diff --git a/internal/translator/claude/openai/init.go b/internal/translator/claude/openai/chat-completions/init.go similarity index 94% rename from internal/translator/claude/openai/init.go rename to internal/translator/claude/openai/chat-completions/init.go index b8ea73d3..e4c53a42 100644 --- a/internal/translator/claude/openai/init.go +++ b/internal/translator/claude/openai/chat-completions/init.go @@ -1,4 +1,4 @@ -package openai +package chat_completions import ( . "github.com/luispater/CLIProxyAPI/internal/constant" diff --git a/internal/translator/codex/openai/codex_openai_request.go b/internal/translator/codex/openai/chat-completions/codex_openai_request.go similarity index 99% rename from internal/translator/codex/openai/codex_openai_request.go rename to internal/translator/codex/openai/chat-completions/codex_openai_request.go index 842cb7e3..60f71b41 100644 --- a/internal/translator/codex/openai/codex_openai_request.go +++ b/internal/translator/codex/openai/chat-completions/codex_openai_request.go @@ -4,7 +4,7 @@ // 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 +package chat_completions import ( "github.com/luispater/CLIProxyAPI/internal/misc" diff --git a/internal/translator/codex/openai/codex_openai_response.go b/internal/translator/codex/openai/chat-completions/codex_openai_response.go similarity index 99% rename from internal/translator/codex/openai/codex_openai_response.go rename to internal/translator/codex/openai/chat-completions/codex_openai_response.go index 51ab5d09..1ba78601 100644 --- a/internal/translator/codex/openai/codex_openai_response.go +++ b/internal/translator/codex/openai/chat-completions/codex_openai_response.go @@ -3,7 +3,7 @@ // 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 +package chat_completions import ( "bufio" diff --git a/internal/translator/codex/openai/init.go b/internal/translator/codex/openai/chat-completions/init.go similarity index 94% rename from internal/translator/codex/openai/init.go rename to internal/translator/codex/openai/chat-completions/init.go index 7c734cd9..062b413f 100644 --- a/internal/translator/codex/openai/init.go +++ b/internal/translator/codex/openai/chat-completions/init.go @@ -1,4 +1,4 @@ -package openai +package chat_completions import ( . "github.com/luispater/CLIProxyAPI/internal/constant" diff --git a/internal/translator/gemini-cli/openai/cli_openai_request.go b/internal/translator/gemini-cli/openai/chat-completions/cli_openai_request.go similarity index 99% rename from internal/translator/gemini-cli/openai/cli_openai_request.go rename to internal/translator/gemini-cli/openai/chat-completions/cli_openai_request.go index b21a0f8f..9dc080ed 100644 --- a/internal/translator/gemini-cli/openai/cli_openai_request.go +++ b/internal/translator/gemini-cli/openai/chat-completions/cli_openai_request.go @@ -1,6 +1,6 @@ // Package openai provides request translation functionality for OpenAI to Gemini CLI API compatibility. // It converts OpenAI Chat Completions requests into Gemini CLI compatible JSON using gjson/sjson only. -package openai +package chat_completions import ( "fmt" diff --git a/internal/translator/gemini-cli/openai/cli_openai_response.go b/internal/translator/gemini-cli/openai/chat-completions/cli_openai_response.go similarity index 99% rename from internal/translator/gemini-cli/openai/cli_openai_response.go rename to internal/translator/gemini-cli/openai/chat-completions/cli_openai_response.go index 0bbbed5a..06d68651 100644 --- a/internal/translator/gemini-cli/openai/cli_openai_response.go +++ b/internal/translator/gemini-cli/openai/chat-completions/cli_openai_response.go @@ -3,7 +3,7 @@ // 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 +package chat_completions import ( "bytes" @@ -11,7 +11,7 @@ import ( "fmt" "time" - . "github.com/luispater/CLIProxyAPI/internal/translator/gemini/openai" + . "github.com/luispater/CLIProxyAPI/internal/translator/gemini/openai/chat-completions" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/internal/translator/gemini-cli/openai/init.go b/internal/translator/gemini-cli/openai/chat-completions/init.go similarity index 94% rename from internal/translator/gemini-cli/openai/init.go rename to internal/translator/gemini-cli/openai/chat-completions/init.go index 2203eb57..5ee21657 100644 --- a/internal/translator/gemini-cli/openai/init.go +++ b/internal/translator/gemini-cli/openai/chat-completions/init.go @@ -1,4 +1,4 @@ -package openai +package chat_completions import ( . "github.com/luispater/CLIProxyAPI/internal/constant" diff --git a/internal/translator/gemini/openai/gemini_openai_request.go b/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go similarity index 99% rename from internal/translator/gemini/openai/gemini_openai_request.go rename to internal/translator/gemini/openai/chat-completions/gemini_openai_request.go index 71590fdd..5b207240 100644 --- a/internal/translator/gemini/openai/gemini_openai_request.go +++ b/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go @@ -1,6 +1,6 @@ // Package openai provides request translation functionality for OpenAI to Gemini API compatibility. // It converts OpenAI Chat Completions requests into Gemini compatible JSON using gjson/sjson only. -package openai +package chat_completions import ( "fmt" diff --git a/internal/translator/gemini/openai/gemini_openai_response.go b/internal/translator/gemini/openai/chat-completions/gemini_openai_response.go similarity index 99% rename from internal/translator/gemini/openai/gemini_openai_response.go rename to internal/translator/gemini/openai/chat-completions/gemini_openai_response.go index 4fd11d0c..28782135 100644 --- a/internal/translator/gemini/openai/gemini_openai_response.go +++ b/internal/translator/gemini/openai/chat-completions/gemini_openai_response.go @@ -3,7 +3,7 @@ // 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 +package chat_completions import ( "bytes" diff --git a/internal/translator/gemini/openai/init.go b/internal/translator/gemini/openai/chat-completions/init.go similarity index 94% rename from internal/translator/gemini/openai/init.go rename to internal/translator/gemini/openai/chat-completions/init.go index 376b485c..53a4c131 100644 --- a/internal/translator/gemini/openai/init.go +++ b/internal/translator/gemini/openai/chat-completions/init.go @@ -1,4 +1,4 @@ -package openai +package chat_completions import ( . "github.com/luispater/CLIProxyAPI/internal/constant" diff --git a/internal/translator/init.go b/internal/translator/init.go index 7e3f2b60..f74e70a4 100644 --- a/internal/translator/init.go +++ b/internal/translator/init.go @@ -3,18 +3,18 @@ package translator import ( _ "github.com/luispater/CLIProxyAPI/internal/translator/claude/gemini" _ "github.com/luispater/CLIProxyAPI/internal/translator/claude/gemini-cli" - _ "github.com/luispater/CLIProxyAPI/internal/translator/claude/openai" + _ "github.com/luispater/CLIProxyAPI/internal/translator/claude/openai/chat-completions" _ "github.com/luispater/CLIProxyAPI/internal/translator/codex/claude" _ "github.com/luispater/CLIProxyAPI/internal/translator/codex/gemini" _ "github.com/luispater/CLIProxyAPI/internal/translator/codex/gemini-cli" - _ "github.com/luispater/CLIProxyAPI/internal/translator/codex/openai" + _ "github.com/luispater/CLIProxyAPI/internal/translator/codex/openai/chat-completions" _ "github.com/luispater/CLIProxyAPI/internal/translator/gemini-cli/claude" _ "github.com/luispater/CLIProxyAPI/internal/translator/gemini-cli/gemini" - _ "github.com/luispater/CLIProxyAPI/internal/translator/gemini-cli/openai" + _ "github.com/luispater/CLIProxyAPI/internal/translator/gemini-cli/openai/chat-completions" _ "github.com/luispater/CLIProxyAPI/internal/translator/gemini/claude" _ "github.com/luispater/CLIProxyAPI/internal/translator/gemini/gemini" _ "github.com/luispater/CLIProxyAPI/internal/translator/gemini/gemini-cli" - _ "github.com/luispater/CLIProxyAPI/internal/translator/gemini/openai" + _ "github.com/luispater/CLIProxyAPI/internal/translator/gemini/openai/chat-completions" _ "github.com/luispater/CLIProxyAPI/internal/translator/openai/claude" _ "github.com/luispater/CLIProxyAPI/internal/translator/openai/gemini" _ "github.com/luispater/CLIProxyAPI/internal/translator/openai/gemini-cli"