feat(auth): disallow free-tier Codex auth during selection process

- Introduced `disallowFreeAuthFromMetadata` and `isFreeCodexAuth` to enforce skipping free-tier credentials.
- Modified scheduler logic to honor `DisallowFreeAuthMetadataKey` during auth selection.
- Updated `ensureImageGenerationTool` to skip tool injection for free-tier Codex auth.
- Added context utility `WithDisallowFreeAuth` and integrated with image handlers.
- Augmented relevant tests to cover free-tier exclusion scenarios.
This commit is contained in:
Luis Pater
2026-04-24 23:18:56 +08:00
parent 36cc762fc9
commit a7e92e2639
7 changed files with 200 additions and 52 deletions

View File

@@ -180,7 +180,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
body, _ = sjson.DeleteBytes(body, "safety_identifier")
body, _ = sjson.DeleteBytes(body, "stream_options")
body = normalizeCodexInstructions(body)
body = ensureImageGenerationTool(body, baseModel)
body = ensureImageGenerationTool(body, baseModel, auth)
url := strings.TrimSuffix(baseURL, "/") + "/responses"
httpReq, err := e.cacheHelper(ctx, from, url, req, body)
@@ -327,7 +327,7 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A
body, _ = sjson.SetBytes(body, "model", baseModel)
body, _ = sjson.DeleteBytes(body, "stream")
body = normalizeCodexInstructions(body)
body = ensureImageGenerationTool(body, baseModel)
body = ensureImageGenerationTool(body, baseModel, auth)
url := strings.TrimSuffix(baseURL, "/") + "/responses/compact"
httpReq, err := e.cacheHelper(ctx, from, url, req, body)
@@ -422,7 +422,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
body, _ = sjson.DeleteBytes(body, "stream_options")
body, _ = sjson.SetBytes(body, "model", baseModel)
body = normalizeCodexInstructions(body)
body = ensureImageGenerationTool(body, baseModel)
body = ensureImageGenerationTool(body, baseModel, auth)
url := strings.TrimSuffix(baseURL, "/") + "/responses"
httpReq, err := e.cacheHelper(ctx, from, url, req, body)
@@ -827,10 +827,23 @@ func normalizeCodexInstructions(body []byte) []byte {
var imageGenToolJSON = []byte(`{"type":"image_generation","output_format":"png"}`)
var imageGenToolArrayJSON = []byte(`[{"type":"image_generation","output_format":"png"}]`)
func ensureImageGenerationTool(body []byte, baseModel string) []byte {
func isCodexFreePlanAuth(auth *cliproxyauth.Auth) bool {
if auth == nil || auth.Attributes == nil {
return false
}
if !strings.EqualFold(strings.TrimSpace(auth.Provider), "codex") {
return false
}
return strings.EqualFold(strings.TrimSpace(auth.Attributes["plan_type"]), "free")
}
func ensureImageGenerationTool(body []byte, baseModel string, auth *cliproxyauth.Auth) []byte {
if strings.HasSuffix(baseModel, "spark") {
return body
}
if isCodexFreePlanAuth(auth) {
return body
}
tools := gjson.GetBytes(body, "tools")
if !tools.Exists() || !tools.IsArray() {

View File

@@ -3,12 +3,13 @@ package executor
import (
"testing"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
"github.com/tidwall/gjson"
)
func TestEnsureImageGenerationTool_NoTools(t *testing.T) {
body := []byte(`{"model":"gpt-5.4","input":"draw a cat"}`)
result := ensureImageGenerationTool(body, "gpt-5.4")
result := ensureImageGenerationTool(body, "gpt-5.4", nil)
tools := gjson.GetBytes(result, "tools")
if !tools.IsArray() {
@@ -28,7 +29,7 @@ func TestEnsureImageGenerationTool_NoTools(t *testing.T) {
func TestEnsureImageGenerationTool_ExistingToolsWithoutImageGen(t *testing.T) {
body := []byte(`{"model":"gpt-5.4","tools":[{"type":"function","name":"get_weather","parameters":{}}]}`)
result := ensureImageGenerationTool(body, "gpt-5.4")
result := ensureImageGenerationTool(body, "gpt-5.4", nil)
tools := gjson.GetBytes(result, "tools")
arr := tools.Array()
@@ -45,7 +46,7 @@ func TestEnsureImageGenerationTool_ExistingToolsWithoutImageGen(t *testing.T) {
func TestEnsureImageGenerationTool_AlreadyPresent(t *testing.T) {
body := []byte(`{"model":"gpt-5.4","tools":[{"type":"image_generation","output_format":"webp"},{"type":"function","name":"f1"}]}`)
result := ensureImageGenerationTool(body, "gpt-5.4")
result := ensureImageGenerationTool(body, "gpt-5.4", nil)
tools := gjson.GetBytes(result, "tools")
arr := tools.Array()
@@ -59,7 +60,7 @@ func TestEnsureImageGenerationTool_AlreadyPresent(t *testing.T) {
func TestEnsureImageGenerationTool_EmptyToolsArray(t *testing.T) {
body := []byte(`{"model":"gpt-5.4","tools":[]}`)
result := ensureImageGenerationTool(body, "gpt-5.4")
result := ensureImageGenerationTool(body, "gpt-5.4", nil)
tools := gjson.GetBytes(result, "tools")
arr := tools.Array()
@@ -73,7 +74,7 @@ func TestEnsureImageGenerationTool_EmptyToolsArray(t *testing.T) {
func TestEnsureImageGenerationTool_WebSearchAndImageGen(t *testing.T) {
body := []byte(`{"model":"gpt-5.4","tools":[{"type":"web_search"}]}`)
result := ensureImageGenerationTool(body, "gpt-5.4")
result := ensureImageGenerationTool(body, "gpt-5.4", nil)
tools := gjson.GetBytes(result, "tools")
arr := tools.Array()
@@ -90,7 +91,7 @@ func TestEnsureImageGenerationTool_WebSearchAndImageGen(t *testing.T) {
func TestEnsureImageGenerationTool_GPT53CodexSparkDoesNotInjectTool(t *testing.T) {
body := []byte(`{"model":"gpt-5.3-codex-spark","input":"draw a cat"}`)
result := ensureImageGenerationTool(body, "gpt-5.3-codex-spark")
result := ensureImageGenerationTool(body, "gpt-5.3-codex-spark", nil)
if string(result) != string(body) {
t.Fatalf("expected body to be unchanged, got %s", string(result))
@@ -99,3 +100,19 @@ func TestEnsureImageGenerationTool_GPT53CodexSparkDoesNotInjectTool(t *testing.T
t.Fatalf("expected no tools for gpt-5.3-codex-spark, got %s", gjson.GetBytes(result, "tools").Raw)
}
}
func TestEnsureImageGenerationTool_FreeCodexAuthDoesNotInjectTool(t *testing.T) {
body := []byte(`{"model":"gpt-5.4","input":"draw a cat"}`)
freeAuth := &cliproxyauth.Auth{
Provider: "codex",
Attributes: map[string]string{"plan_type": "free"},
}
result := ensureImageGenerationTool(body, "gpt-5.4", freeAuth)
if string(result) != string(body) {
t.Fatalf("expected body to be unchanged, got %s", string(result))
}
if gjson.GetBytes(result, "tools").Exists() {
t.Fatalf("expected no tools for free codex auth, got %s", gjson.GetBytes(result, "tools").Raw)
}
}