Skip to content

Commit 5460bf2

Browse files
Bowl42claudeymkiux
authored
feat: add reasoning and service_tier params to Codex provider (#365)
* feat: add reasoning and service_tier params to Codex provider Add support for `reasoning` (effort) and `service_tier` parameters in Codex requests, with provider-level overrides that force these values for all requests routed through a given provider. - Add `ServiceTier` field to CodexRequest and OpenAIRequest types - Forward `service_tier` through codex↔openai converters - Forward `reasoning.effort` to Claude `output_config` in codex→claude - Add `BaseURL`, `Reasoning`, `ServiceTier` to ProviderConfigCodex - Apply provider overrides in both codex and cliproxyapi_codex adapters - Add dropdown selects in provider UI (reasoning / service_tier) - Add E2E test with mock Codex server (5 scenarios) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve TS18047 'user' possibly null in admin-route Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address CodeRabbit review feedback - Guard against empty service_tier in openai_to_codex converter - Add headed mode script for codex test - Use i18n for dropdown option labels - Add cleanup of test providers/routes after E2E tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove isAuthenticated from AdminRoute Removed isAuthenticated from AdminRoute component. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: ymkiux <44744442+ymkiux@users.noreply.github.com>
1 parent 4fb81ec commit 5460bf2

File tree

15 files changed

+719
-15
lines changed

15 files changed

+719
-15
lines changed

internal/adapter/provider/cliproxyapi_codex/adapter.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/awsl-project/maxx/internal/domain"
1818
"github.com/awsl-project/maxx/internal/flow"
1919
"github.com/awsl-project/maxx/internal/usage"
20+
"github.com/tidwall/sjson"
2021
"github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
2122
"github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
2223
"github.com/router-for-me/CLIProxyAPI/v6/sdk/exec"
@@ -212,6 +213,19 @@ func (a *CLIProxyAPICodexAdapter) Execute(c *flow.Ctx, p *domain.Provider) error
212213
stream := flow.GetIsStream(c)
213214
model := flow.GetMappedModel(c)
214215

216+
// Apply provider-level overrides for reasoning and service_tier
217+
cfg := a.codexConfig()
218+
if cfg.Reasoning != "" {
219+
if updated, err := sjson.SetBytes(requestBody, "reasoning.effort", cfg.Reasoning); err == nil {
220+
requestBody = updated
221+
}
222+
}
223+
if cfg.ServiceTier != "" {
224+
if updated, err := sjson.SetBytes(requestBody, "service_tier", cfg.ServiceTier); err == nil {
225+
requestBody = updated
226+
}
227+
}
228+
215229
// Codex CLI 请求体本质是 OpenAI Responses schema;保持与 CLIProxyAPI 一致。
216230
sourceFormat := translator.FormatOpenAIResponse
217231

internal/adapter/provider/codex/adapter.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,28 @@ func (a *CodexAdapter) Execute(c *flow.Ctx, provider *domain.Provider) error {
121121
cacheID, updatedBody := applyCodexRequestTuning(c, requestBody)
122122
requestBody = updatedBody
123123

124+
// Apply provider-level overrides for reasoning and service_tier
125+
config := provider.Config.Codex
126+
if config.Reasoning != "" {
127+
if updated, err := sjson.SetBytes(requestBody, "reasoning.effort", config.Reasoning); err == nil {
128+
requestBody = updated
129+
}
130+
}
131+
if config.ServiceTier != "" {
132+
if updated, err := sjson.SetBytes(requestBody, "service_tier", config.ServiceTier); err == nil {
133+
requestBody = updated
134+
}
135+
}
136+
124137
// Build upstream URL and stream mode
125-
upstreamURL := CodexBaseURL + "/responses"
138+
baseURL := CodexBaseURL
139+
if config.BaseURL != "" {
140+
baseURL = strings.TrimRight(config.BaseURL, "/")
141+
}
142+
upstreamURL := baseURL + "/responses"
126143
upstreamStream := true
127144
if !clientWantsStream {
128-
upstreamURL = CodexBaseURL + "/responses/compact"
145+
upstreamURL = baseURL + "/responses/compact"
129146
upstreamStream = false
130147
}
131148
if len(requestBody) > 0 {
@@ -141,7 +158,6 @@ func (a *CodexAdapter) Execute(c *flow.Ctx, provider *domain.Provider) error {
141158
}
142159

143160
// Apply headers with passthrough support (client headers take priority)
144-
config := provider.Config.Codex
145161
a.applyCodexHeaders(upstreamReq, request, accessToken, config.AccountID, upstreamStream, cacheID)
146162

147163
// Send request info via EventChannel

internal/converter/codex_to_claude.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ func (c *codexToClaudeRequest) Transform(body []byte, model string, stream bool)
3636
TopP: req.TopP,
3737
}
3838

39+
// Convert reasoning effort to Claude output_config
40+
if req.Reasoning != nil && req.Reasoning.Effort != "" {
41+
claudeReq.OutputConfig = &ClaudeOutputConfig{
42+
Effort: req.Reasoning.Effort,
43+
}
44+
}
45+
3946
// Convert instructions to system prompt
4047
if req.Instructions != "" {
4148
claudeReq.System = req.Instructions

internal/converter/codex_to_openai.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ func (c *codexToOpenAIRequest) Transform(body []byte, model string, stream bool)
5151
if req.Reasoning != nil && req.Reasoning.Effort != "" {
5252
openaiReq.ReasoningEffort = req.Reasoning.Effort
5353
}
54+
if req.ServiceTier != "" {
55+
openaiReq.ServiceTier = req.ServiceTier
56+
}
5457

5558
// Convert instructions to system message
5659
if req.Instructions != "" {

internal/converter/openai_to_codex.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ func (c *openaiToCodexRequest) Transform(body []byte, model string, stream bool)
3636
} else {
3737
out, _ = sjson.Set(out, "reasoning.effort", "medium")
3838
}
39+
if v := gjson.GetBytes(rawJSON, "service_tier"); v.Exists() && v.String() != "" {
40+
out, _ = sjson.Set(out, "service_tier", v.Value())
41+
}
3942
out, _ = sjson.Set(out, "parallel_tool_calls", true)
4043
out, _ = sjson.Set(out, "reasoning.summary", "auto")
4144
out, _ = sjson.Set(out, "include", []string{"reasoning.encrypted_content"})
@@ -901,6 +904,7 @@ func applyRequestEchoToResponse(responseJSON string, prefix string, requestRaw [
901904
"previous_response_id",
902905
"text",
903906
"truncation",
907+
"service_tier",
904908
}
905909
for _, path := range paths {
906910
val := req.Get(path)

internal/converter/types_codex.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type CodexRequest struct {
1313
Tools []CodexTool `json:"tools,omitempty"`
1414
ToolChoice interface{} `json:"tool_choice,omitempty"`
1515
Reasoning *CodexReasoning `json:"reasoning,omitempty"`
16+
ServiceTier string `json:"service_tier,omitempty"`
1617
ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"`
1718
Include []string `json:"include,omitempty"`
1819
Metadata map[string]interface{} `json:"metadata,omitempty"`

internal/converter/types_openai.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ type OpenAIRequest struct {
99
MaxTokens int `json:"max_tokens,omitempty"`
1010
MaxCompletionTokens int `json:"max_completion_tokens,omitempty"`
1111
ReasoningEffort string `json:"reasoning_effort,omitempty"`
12+
ServiceTier string `json:"service_tier,omitempty"`
1213
Temperature *float64 `json:"temperature,omitempty"`
1314
TopP *float64 `json:"top_p,omitempty"`
1415
N int `json:"n,omitempty"`

internal/domain/model.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,17 @@ type ProviderConfigCodex struct {
148148

149149
// 使用 CLIProxyAPI 转发
150150
UseCLIProxyAPI bool `json:"useCLIProxyAPI,omitempty"`
151+
152+
// 自定义 Codex API Base URL(默认使用官方地址)
153+
BaseURL string `json:"baseURL,omitempty"`
154+
155+
// 强制 reasoning effort(覆盖请求中的值)
156+
// 可选值: "low", "medium", "high"
157+
Reasoning string `json:"reasoning,omitempty"`
158+
159+
// 强制 service_tier(覆盖请求中的值)
160+
// 可选值: "auto", "default", "flex", "priority"
161+
ServiceTier string `json:"serviceTier,omitempty"`
151162
}
152163

153164
// ProviderConfigCLIProxyAPIAntigravity CLIProxyAPI Antigravity 内部配置

tests/e2e/playwright/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
"private": true,
44
"type": "module",
55
"scripts": {
6-
"test": "node test-passkey-discoverable.mjs && node test-requests-project-filter.mjs && node test-stats-chart-resize.mjs",
6+
"test": "node test-passkey-discoverable.mjs && node test-requests-project-filter.mjs && node test-stats-chart-resize.mjs && node test-codex-reasoning-servicetier.mjs",
7+
"test:codex-overrides": "node test-codex-reasoning-servicetier.mjs",
8+
"test:codex-overrides:headed": "HEADED=1 node test-codex-reasoning-servicetier.mjs",
79
"test:passkey": "node test-passkey-discoverable.mjs",
810
"test:project-filter": "node test-requests-project-filter.mjs",
911
"test:stats-chart": "node test-stats-chart-resize.mjs",

0 commit comments

Comments
 (0)