Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
253a74dd
authored
Aug 11, 2026
by
Uzuki Shion
Committed by
GitHub
Aug 11, 2026
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix(relay): preserve presence/frequency penalty in Responses conversion (#6654)
parent
9c97e78a
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
172 additions
and
1 deletions
+172
-1
relay/channel/codex/adaptor.go
+2
-0
relay/channel/codex/adaptor_test.go
+30
-0
relaykit/dto/openai_request.go
+5
-0
relaykit/dto/openai_request_zero_value_test.go
+5
-1
relaykit/relayconvert/internal/oai_chat/to_oai_responses_req.go
+10
-0
relaykit/relayconvert/internal/oai_chat/to_oai_responses_req_test.go
+45
-0
relaykit/relayconvert/internal/oai_responses/to_oai_chat_req.go
+20
-0
relaykit/relayconvert/internal/oai_responses/to_oai_chat_req_test.go
+55
-0
No files found.
relay/channel/codex/adaptor.go
View file @
253a74dd
...
@@ -104,6 +104,8 @@ func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommo
...
@@ -104,6 +104,8 @@ func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommo
// rm max_output_tokens
// rm max_output_tokens
request
.
MaxOutputTokens
=
nil
request
.
MaxOutputTokens
=
nil
request
.
Temperature
=
nil
request
.
Temperature
=
nil
request
.
FrequencyPenalty
=
nil
request
.
PresencePenalty
=
nil
return
request
,
nil
return
request
,
nil
}
}
...
...
relay/channel/codex/adaptor_test.go
View file @
253a74dd
package
codex
package
codex
import
(
import
(
"encoding/json"
"testing"
"testing"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/constant"
relaycommon
"github.com/QuantumNous/new-api/relay/common"
relaycommon
"github.com/QuantumNous/new-api/relay/common"
relayconstant
"github.com/QuantumNous/new-api/relay/constant"
relayconstant
"github.com/QuantumNous/new-api/relay/constant"
"github.com/QuantumNous/new-api/relaykit/dto"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/require"
)
)
...
@@ -24,3 +27,30 @@ func TestGetRequestURLAlphaSearch(t *testing.T) {
...
@@ -24,3 +27,30 @@ func TestGetRequestURLAlphaSearch(t *testing.T) {
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
assert
.
Equal
(
t
,
"https://chatgpt.com/backend-api/codex/alpha/search"
,
url
)
assert
.
Equal
(
t
,
"https://chatgpt.com/backend-api/codex/alpha/search"
,
url
)
}
}
// The Codex backend rejects these fields, so the adaptor clears them rather
// than forwarding what the client sent.
func
TestConvertOpenAIResponsesRequestDropsPenalties
(
t
*
testing
.
T
)
{
adaptor
:=
&
Adaptor
{}
info
:=
&
relaycommon
.
RelayInfo
{
ChannelMeta
:
&
relaycommon
.
ChannelMeta
{
ChannelType
:
constant
.
ChannelTypeCodex
},
RelayMode
:
relayconstant
.
RelayModeResponses
,
}
converted
,
err
:=
adaptor
.
ConvertOpenAIResponsesRequest
(
nil
,
info
,
dto
.
OpenAIResponsesRequest
{
Model
:
"gpt-5-codex"
,
Input
:
json
.
RawMessage
(
`"hello"`
),
MaxOutputTokens
:
lo
.
ToPtr
(
uint
(
128
)),
Temperature
:
lo
.
ToPtr
(
1.0
),
FrequencyPenalty
:
json
.
RawMessage
(
`1.5`
),
PresencePenalty
:
json
.
RawMessage
(
`1.5`
),
})
require
.
NoError
(
t
,
err
)
request
,
ok
:=
converted
.
(
dto
.
OpenAIResponsesRequest
)
require
.
True
(
t
,
ok
)
assert
.
Nil
(
t
,
request
.
MaxOutputTokens
)
assert
.
Nil
(
t
,
request
.
Temperature
)
assert
.
Nil
(
t
,
request
.
FrequencyPenalty
)
assert
.
Nil
(
t
,
request
.
PresencePenalty
)
}
relaykit/dto/openai_request.go
View file @
253a74dd
...
@@ -867,6 +867,11 @@ type OpenAIResponsesRequest struct {
...
@@ -867,6 +867,11 @@ type OpenAIResponsesRequest struct {
Metadata
json
.
RawMessage
`json:"metadata,omitempty"`
Metadata
json
.
RawMessage
`json:"metadata,omitempty"`
Moderation
json
.
RawMessage
`json:"moderation,omitempty"`
Moderation
json
.
RawMessage
`json:"moderation,omitempty"`
ParallelToolCalls
json
.
RawMessage
`json:"parallel_tool_calls,omitempty"`
ParallelToolCalls
json
.
RawMessage
`json:"parallel_tool_calls,omitempty"`
// FrequencyPenalty/PresencePenalty are not part of the official OpenAI
// Responses API; they are forwarded verbatim for OpenAI-compatible upstreams
// (e.g. vLLM) that accept them.
FrequencyPenalty
json
.
RawMessage
`json:"frequency_penalty,omitempty"`
PresencePenalty
json
.
RawMessage
`json:"presence_penalty,omitempty"`
PreviousResponseID
string
`json:"previous_response_id,omitempty"`
PreviousResponseID
string
`json:"previous_response_id,omitempty"`
Reasoning
*
Reasoning
`json:"reasoning,omitempty"`
Reasoning
*
Reasoning
`json:"reasoning,omitempty"`
// ServiceTier specifies upstream service level and may affect billing.
// ServiceTier specifies upstream service level and may affect billing.
...
...
relaykit/dto/openai_request_zero_value_test.go
View file @
253a74dd
...
@@ -123,7 +123,9 @@ func TestOpenAIResponsesRequestPreserveExplicitZeroValues(t *testing.T) {
...
@@ -123,7 +123,9 @@ func TestOpenAIResponsesRequestPreserveExplicitZeroValues(t *testing.T) {
"max_output_tokens":0,
"max_output_tokens":0,
"max_tool_calls":0,
"max_tool_calls":0,
"stream":false,
"stream":false,
"top_p":0
"top_p":0,
"frequency_penalty":0,
"presence_penalty":0
}`
)
}`
)
var
req
OpenAIResponsesRequest
var
req
OpenAIResponsesRequest
...
@@ -137,6 +139,8 @@ func TestOpenAIResponsesRequestPreserveExplicitZeroValues(t *testing.T) {
...
@@ -137,6 +139,8 @@ func TestOpenAIResponsesRequestPreserveExplicitZeroValues(t *testing.T) {
require
.
True
(
t
,
gjson
.
GetBytes
(
encoded
,
"max_tool_calls"
)
.
Exists
())
require
.
True
(
t
,
gjson
.
GetBytes
(
encoded
,
"max_tool_calls"
)
.
Exists
())
require
.
True
(
t
,
gjson
.
GetBytes
(
encoded
,
"stream"
)
.
Exists
())
require
.
True
(
t
,
gjson
.
GetBytes
(
encoded
,
"stream"
)
.
Exists
())
require
.
True
(
t
,
gjson
.
GetBytes
(
encoded
,
"top_p"
)
.
Exists
())
require
.
True
(
t
,
gjson
.
GetBytes
(
encoded
,
"top_p"
)
.
Exists
())
require
.
True
(
t
,
gjson
.
GetBytes
(
encoded
,
"frequency_penalty"
)
.
Exists
())
require
.
True
(
t
,
gjson
.
GetBytes
(
encoded
,
"presence_penalty"
)
.
Exists
())
}
}
func
TestOpenAIResponsesRequestPreserveQwenThinkingBudget
(
t
*
testing
.
T
)
{
func
TestOpenAIResponsesRequestPreserveQwenThinkingBudget
(
t
*
testing
.
T
)
{
...
...
relaykit/relayconvert/internal/oai_chat/to_oai_responses_req.go
View file @
253a74dd
...
@@ -372,6 +372,14 @@ func ChatCompletionsRequestToResponsesRequest(req *dto.GeneralOpenAIRequest) (*d
...
@@ -372,6 +372,14 @@ func ChatCompletionsRequestToResponsesRequest(req *dto.GeneralOpenAIRequest) (*d
topP
=
kitutil
.
GetPointer
(
lo
.
FromPtr
(
req
.
TopP
))
topP
=
kitutil
.
GetPointer
(
lo
.
FromPtr
(
req
.
TopP
))
}
}
var
frequencyPenaltyRaw
,
presencePenaltyRaw
json
.
RawMessage
if
req
.
FrequencyPenalty
!=
nil
{
frequencyPenaltyRaw
,
_
=
kitutil
.
Marshal
(
req
.
FrequencyPenalty
)
}
if
req
.
PresencePenalty
!=
nil
{
presencePenaltyRaw
,
_
=
kitutil
.
Marshal
(
req
.
PresencePenalty
)
}
out
:=
&
dto
.
OpenAIResponsesRequest
{
out
:=
&
dto
.
OpenAIResponsesRequest
{
Model
:
req
.
Model
,
Model
:
req
.
Model
,
Input
:
inputRaw
,
Input
:
inputRaw
,
...
@@ -382,6 +390,8 @@ func ChatCompletionsRequestToResponsesRequest(req *dto.GeneralOpenAIRequest) (*d
...
@@ -382,6 +390,8 @@ func ChatCompletionsRequestToResponsesRequest(req *dto.GeneralOpenAIRequest) (*d
ToolChoice
:
toolChoiceRaw
,
ToolChoice
:
toolChoiceRaw
,
Tools
:
toolsRaw
,
Tools
:
toolsRaw
,
TopP
:
topP
,
TopP
:
topP
,
FrequencyPenalty
:
frequencyPenaltyRaw
,
PresencePenalty
:
presencePenaltyRaw
,
User
:
req
.
User
,
User
:
req
.
User
,
ParallelToolCalls
:
parallelToolCallsRaw
,
ParallelToolCalls
:
parallelToolCallsRaw
,
Store
:
req
.
Store
,
Store
:
req
.
Store
,
...
...
relaykit/relayconvert/internal/oai_chat/to_oai_responses_req_test.go
View file @
253a74dd
...
@@ -84,6 +84,51 @@ func TestChatCompletionsRequestToResponsesRequestRejectsMultipleChoices(t *testi
...
@@ -84,6 +84,51 @@ func TestChatCompletionsRequestToResponsesRequestRejectsMultipleChoices(t *testi
assert
.
Contains
(
t
,
err
.
Error
(),
"n>1"
)
assert
.
Contains
(
t
,
err
.
Error
(),
"n>1"
)
}
}
func
TestChatCompletionsRequestToResponsesRequestPreservesPenalties
(
t
*
testing
.
T
)
{
tests
:=
[]
struct
{
name
string
frequency
*
float64
frequencyWant
json
.
RawMessage
presence
*
float64
presenceWant
json
.
RawMessage
}{
{
name
:
"positive values"
,
frequency
:
lo
.
ToPtr
(
0.5
),
frequencyWant
:
json
.
RawMessage
(
`0.5`
),
presence
:
lo
.
ToPtr
(
1.5
),
presenceWant
:
json
.
RawMessage
(
`1.5`
),
},
{
name
:
"explicit zero values"
,
frequency
:
lo
.
ToPtr
(
0.0
),
frequencyWant
:
json
.
RawMessage
(
`0`
),
presence
:
lo
.
ToPtr
(
0.0
),
presenceWant
:
json
.
RawMessage
(
`0`
),
},
{
name
:
"unset stays nil"
,
frequency
:
nil
,
presence
:
nil
,
},
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
got
,
err
:=
ChatCompletionsRequestToResponsesRequest
(
&
dto
.
GeneralOpenAIRequest
{
Model
:
"gpt-test"
,
Messages
:
[]
dto
.
Message
{{
Role
:
"user"
,
Content
:
"hello"
}},
FrequencyPenalty
:
tt
.
frequency
,
PresencePenalty
:
tt
.
presence
,
})
require
.
NoError
(
t
,
err
)
assert
.
Equal
(
t
,
tt
.
frequencyWant
,
got
.
FrequencyPenalty
)
assert
.
Equal
(
t
,
tt
.
presenceWant
,
got
.
PresencePenalty
)
})
}
}
func
assistantMessageWithTool
(
content
string
,
id
string
,
name
string
,
args
string
)
dto
.
Message
{
func
assistantMessageWithTool
(
content
string
,
id
string
,
name
string
,
args
string
)
dto
.
Message
{
msg
:=
dto
.
Message
{
Role
:
"assistant"
,
Content
:
content
}
msg
:=
dto
.
Message
{
Role
:
"assistant"
,
Content
:
content
}
msg
.
SetToolCalls
([]
dto
.
ToolCallRequest
{
msg
.
SetToolCalls
([]
dto
.
ToolCallRequest
{
...
...
relaykit/relayconvert/internal/oai_responses/to_oai_chat_req.go
View file @
253a74dd
...
@@ -76,6 +76,15 @@ func ResponsesRequestToChatCompletionsRequest(req *dto.OpenAIResponsesRequest) (
...
@@ -76,6 +76,15 @@ func ResponsesRequestToChatCompletionsRequest(req *dto.OpenAIResponsesRequest) (
ThinkingBudget
:
req
.
ThinkingBudget
,
ThinkingBudget
:
req
.
ThinkingBudget
,
}
}
out
.
FrequencyPenalty
,
err
=
responsesRawFloat
(
req
.
FrequencyPenalty
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"invalid frequency_penalty: %w"
,
err
)
}
out
.
PresencePenalty
,
err
=
responsesRawFloat
(
req
.
PresencePenalty
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"invalid presence_penalty: %w"
,
err
)
}
if
req
.
Reasoning
!=
nil
{
if
req
.
Reasoning
!=
nil
{
out
.
ReasoningEffort
=
req
.
Reasoning
.
Effort
out
.
ReasoningEffort
=
req
.
Reasoning
.
Effort
}
}
...
@@ -527,6 +536,17 @@ func responseToolOutputToChatContent(value any) any {
...
@@ -527,6 +536,17 @@ func responseToolOutputToChatContent(value any) any {
}
}
}
}
func
responsesRawFloat
(
raw
json
.
RawMessage
)
(
*
float64
,
error
)
{
if
!
rawJSONPresent
(
raw
)
{
return
nil
,
nil
}
var
value
float64
if
err
:=
kitutil
.
Unmarshal
(
raw
,
&
value
);
err
!=
nil
{
return
nil
,
err
}
return
&
value
,
nil
}
func
responsesJSONString
(
raw
json
.
RawMessage
)
(
string
,
error
)
{
func
responsesJSONString
(
raw
json
.
RawMessage
)
(
string
,
error
)
{
if
kitutil
.
GetJsonType
(
raw
)
!=
"string"
{
if
kitutil
.
GetJsonType
(
raw
)
!=
"string"
{
return
string
(
raw
),
nil
return
string
(
raw
),
nil
...
...
relaykit/relayconvert/internal/oai_responses/to_oai_chat_req_test.go
View file @
253a74dd
...
@@ -295,6 +295,61 @@ func TestResponsesRequestToChatCompletionsRequestRejectsStatefulFields(t *testin
...
@@ -295,6 +295,61 @@ func TestResponsesRequestToChatCompletionsRequestRejectsStatefulFields(t *testin
}
}
}
}
func
TestResponsesRequestToChatCompletionsRequestPreservesPenalties
(
t
*
testing
.
T
)
{
tests
:=
[]
struct
{
name
string
frequencyRaw
json
.
RawMessage
frequencyWant
*
float64
presenceRaw
json
.
RawMessage
presenceWant
*
float64
}{
{
name
:
"positive values"
,
frequencyRaw
:
json
.
RawMessage
(
`0.5`
),
frequencyWant
:
lo
.
ToPtr
(
0.5
),
presenceRaw
:
json
.
RawMessage
(
`1.5`
),
presenceWant
:
lo
.
ToPtr
(
1.5
),
},
{
name
:
"explicit zero values"
,
frequencyRaw
:
json
.
RawMessage
(
`0.0`
),
frequencyWant
:
lo
.
ToPtr
(
0.0
),
presenceRaw
:
json
.
RawMessage
(
`0.0`
),
presenceWant
:
lo
.
ToPtr
(
0.0
),
},
{
name
:
"unset stays nil"
,
frequencyRaw
:
nil
,
presenceRaw
:
nil
,
},
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
got
,
err
:=
ResponsesRequestToChatCompletionsRequest
(
&
dto
.
OpenAIResponsesRequest
{
Model
:
"gpt-test"
,
Input
:
mustRawMessage
(
t
,
"hello"
),
FrequencyPenalty
:
tt
.
frequencyRaw
,
PresencePenalty
:
tt
.
presenceRaw
,
})
require
.
NoError
(
t
,
err
)
assert
.
Equal
(
t
,
tt
.
frequencyWant
,
got
.
FrequencyPenalty
)
assert
.
Equal
(
t
,
tt
.
presenceWant
,
got
.
PresencePenalty
)
})
}
}
func
TestResponsesRequestToChatCompletionsRequestRejectsMalformedPenalty
(
t
*
testing
.
T
)
{
_
,
err
:=
ResponsesRequestToChatCompletionsRequest
(
&
dto
.
OpenAIResponsesRequest
{
Model
:
"gpt-test"
,
Input
:
mustRawMessage
(
t
,
"hello"
),
FrequencyPenalty
:
json
.
RawMessage
(
`"not-a-number"`
),
})
require
.
Error
(
t
,
err
)
assert
.
Contains
(
t
,
err
.
Error
(),
"frequency_penalty"
)
}
func
mustRawMessage
(
t
*
testing
.
T
,
value
any
)
[]
byte
{
func
mustRawMessage
(
t
*
testing
.
T
,
value
any
)
[]
byte
{
t
.
Helper
()
t
.
Helper
()
raw
,
err
:=
kitutil
.
Marshal
(
value
)
raw
,
err
:=
kitutil
.
Marshal
(
value
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment