Skip to content
2 changes: 2 additions & 0 deletions internal/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ func (s *Server) setupRoutes() {
{
v1.GET("/models", s.unifiedModelsHandler(openaiHandlers, claudeCodeHandlers))
v1.POST("/chat/completions", openaiHandlers.ChatCompletions)
v1.POST("/audio/transcriptions", openaiHandlers.AudioTranscriptions)
v1.POST("/completions", openaiHandlers.Completions)
v1.POST("/messages", claudeCodeHandlers.ClaudeMessages)
v1.POST("/messages/count_tokens", claudeCodeHandlers.ClaudeCountTokens)
Expand All @@ -353,6 +354,7 @@ func (s *Server) setupRoutes() {
"message": "CLI Proxy API Server",
"endpoints": []string{
"POST /v1/chat/completions",
"POST /v1/audio/transcriptions",
"POST /v1/completions",
"GET /v1/models",
},
Expand Down
15 changes: 15 additions & 0 deletions internal/api/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,18 @@ func TestDefaultRequestLoggerFactory_UsesResolvedLogDirectory(t *testing.T) {
}
}
}

func TestRootEndpointIncludesAudioTranscriptions(t *testing.T) {
server := newTestServer(t)

req := httptest.NewRequest(http.MethodGet, "/", nil)
resp := httptest.NewRecorder()
server.engine.ServeHTTP(resp, req)

if resp.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body=%s", resp.Code, http.StatusOK, resp.Body.String())
}
if body := resp.Body.String(); !strings.Contains(body, "POST /v1/audio/transcriptions") {
t.Fatalf("response body missing audio transcription endpoint: %s", body)
}
}
58 changes: 57 additions & 1 deletion internal/registry/models/models.json
Original file line number Diff line number Diff line change
Expand Up @@ -1371,6 +1371,20 @@
"xhigh"
]
}
},
{
"id": "gpt-4o-mini-transcribe",
"object": "model",
"created": 1729728000,
"owned_by": "openai",
"type": "openai",
"display_name": "GPT-4o Mini Transcribe",
"version": "gpt-4o-mini-transcribe",
"description": "Fast speech-to-text model for audio transcription.",
"supported_parameters": [
"language",
"prompt"
]
}
],
"codex-team": [
Expand Down Expand Up @@ -1623,6 +1637,20 @@
"xhigh"
]
}
},
{
"id": "gpt-4o-mini-transcribe",
"object": "model",
"created": 1729728000,
"owned_by": "openai",
"type": "openai",
"display_name": "GPT-4o Mini Transcribe",
"version": "gpt-4o-mini-transcribe",
"description": "Fast speech-to-text model for audio transcription.",
"supported_parameters": [
"language",
"prompt"
]
}
],
"codex-plus": [
Expand Down Expand Up @@ -1898,6 +1926,20 @@
"xhigh"
]
}
},
{
"id": "gpt-4o-mini-transcribe",
"object": "model",
"created": 1729728000,
"owned_by": "openai",
"type": "openai",
"display_name": "GPT-4o Mini Transcribe",
"version": "gpt-4o-mini-transcribe",
"description": "Fast speech-to-text model for audio transcription.",
"supported_parameters": [
"language",
"prompt"
]
}
],
"codex-pro": [
Expand Down Expand Up @@ -2173,6 +2215,20 @@
"xhigh"
]
}
},
{
"id": "gpt-4o-mini-transcribe",
"object": "model",
"created": 1729728000,
"owned_by": "openai",
"type": "openai",
"display_name": "GPT-4o Mini Transcribe",
"version": "gpt-4o-mini-transcribe",
"description": "Fast speech-to-text model for audio transcription.",
"supported_parameters": [
"language",
"prompt"
]
}
],
"qwen": [
Expand Down Expand Up @@ -2680,4 +2736,4 @@
"max_completion_tokens": 32768
}
]
}
}
40 changes: 26 additions & 14 deletions internal/runtime/executor/codex_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,7 @@ func (e *CodexExecutor) PrepareRequest(req *http.Request, auth *cliproxyauth.Aut
return nil
}
apiKey, _ := codexCreds(auth)
if strings.TrimSpace(apiKey) != "" {
req.Header.Set("Authorization", "Bearer "+apiKey)
}
var attrs map[string]string
if auth != nil {
attrs = auth.Attributes
}
util.ApplyCustomHeadersFromAttrs(req, attrs)
applyCodexPreparedHeaders(req, auth, apiKey, e.cfg)
return nil
}

Expand Down Expand Up @@ -637,8 +630,28 @@ func (e *CodexExecutor) cacheHelper(ctx context.Context, from sdktranslator.Form
}

func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, stream bool, cfg *config.Config) {
applyCodexPreparedHeaders(r, auth, token, cfg)
if r == nil {
return
}
r.Header.Set("Content-Type", "application/json")
r.Header.Set("Authorization", "Bearer "+token)
if stream {
r.Header.Set("Accept", "text/event-stream")
} else {
r.Header.Set("Accept", "application/json")
}
}

func applyCodexPreparedHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, cfg *config.Config) {
if r == nil {
return
}
if r.Header == nil {
r.Header = make(http.Header)
}
if strings.TrimSpace(token) != "" {
r.Header.Set("Authorization", "Bearer "+token)
}

var ginHeaders http.Header
if ginCtx, ok := r.Context().Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil {
Expand All @@ -649,13 +662,12 @@ func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, s
misc.EnsureHeader(r.Header, ginHeaders, "Session_id", uuid.NewString())
cfgUserAgent, _ := codexHeaderDefaults(cfg, auth)
ensureHeaderWithConfigPrecedence(r.Header, ginHeaders, "User-Agent", cfgUserAgent, codexUserAgent)

if stream {
r.Header.Set("Accept", "text/event-stream")
} else {
if strings.TrimSpace(r.Header.Get("Accept")) == "" {
r.Header.Set("Accept", "application/json")
}
r.Header.Set("Connection", "Keep-Alive")
if strings.TrimSpace(r.Header.Get("Connection")) == "" {
r.Header.Set("Connection", "Keep-Alive")
}

isAPIKey := false
if auth != nil && auth.Attributes != nil {
Expand Down
42 changes: 42 additions & 0 deletions internal/runtime/executor/codex_executor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package executor

import (
"net/http"
"testing"

cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
)

func TestCodexPrepareRequestPreservesMultipartContentType(t *testing.T) {
req, err := http.NewRequest(http.MethodPost, "https://example.com/backend-api/transcribe", nil)
if err != nil {
t.Fatalf("NewRequest() error = %v", err)
}
req.Header.Set("Content-Type", "multipart/form-data; boundary=test-boundary")

executor := NewCodexExecutor(nil)
auth := &cliproxyauth.Auth{
Provider: "codex",
Metadata: map[string]any{
"account_id": "account-123",
"email": "user@example.com",
},
}

if err := executor.PrepareRequest(req, auth); err != nil {
t.Fatalf("PrepareRequest() error = %v", err)
}

if got := req.Header.Get("Content-Type"); got != "multipart/form-data; boundary=test-boundary" {
t.Fatalf("Content-Type = %q, want %q", got, "multipart/form-data; boundary=test-boundary")
}
if got := req.Header.Get("Chatgpt-Account-Id"); got != "account-123" {
t.Fatalf("Chatgpt-Account-Id = %q, want %q", got, "account-123")
}
if got := req.Header.Get("Originator"); got != "codex_cli_rs" {
t.Fatalf("Originator = %q, want %q", got, "codex_cli_rs")
}
if got := req.Header.Get("Accept"); got != "application/json" {
t.Fatalf("Accept = %q, want %q", got, "application/json")
}
}
29 changes: 29 additions & 0 deletions sdk/api/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,35 @@ func (h *BaseAPIHandler) ExecuteWithAuthManager(ctx context.Context, handlerType
return resp.Payload, FilterUpstreamHeaders(resp.Headers), nil
}

// ExecuteHTTPRequestWithAuthManager executes a raw HTTP request builder via the core auth manager.
// This path is intended for provider-native endpoints that do not go through the JSON translator stack.
func (h *BaseAPIHandler) ExecuteHTTPRequestWithAuthManager(ctx context.Context, modelName string, build coreauth.HTTPRequestBuilder) (*http.Response, *coreauth.Auth, *interfaces.ErrorMessage) {
providers, normalizedModel, errMsg := h.getRequestDetails(modelName)
if errMsg != nil {
return nil, nil, errMsg
}
reqMeta := requestExecutionMetadata(ctx)
reqMeta[coreexecutor.RequestedModelMetadataKey] = normalizedModel
opts := coreexecutor.Options{Metadata: reqMeta}
resp, auth, err := h.AuthManager.ExecuteHTTPRequest(ctx, providers, normalizedModel, normalizedModel, opts, build)
if err != nil {
status := http.StatusInternalServerError
if se, ok := err.(interface{ StatusCode() int }); ok && se != nil {
if code := se.StatusCode(); code > 0 {
status = code
}
}
var addon http.Header
if he, ok := err.(interface{ Headers() http.Header }); ok && he != nil {
if hdr := he.Headers(); hdr != nil {
addon = hdr.Clone()
}
}
return nil, nil, &interfaces.ErrorMessage{StatusCode: status, Error: err, Addon: addon}
}
return resp, auth, nil
}

// ExecuteCountWithAuthManager executes a non-streaming request via the core auth manager.
// This path is the only supported execution route.
func (h *BaseAPIHandler) ExecuteCountWithAuthManager(ctx context.Context, handlerType, modelName string, rawJSON []byte, alt string) ([]byte, http.Header, *interfaces.ErrorMessage) {
Expand Down
Loading
Loading