Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
92159994
Commit
92159994
authored
Mar 06, 2026
by
shaw
Browse files
feat: /v1/messages端点适配codex账号池
parent
afbe8bf0
Changes
7
Hide whitespace changes
Inline
Side-by-side
backend/internal/handler/openai_gateway_handler.go
View file @
92159994
...
@@ -670,8 +670,14 @@ func (h *OpenAIGatewayHandler) anthropicStreamingAwareError(c *gin.Context, stat
...
@@ -670,8 +670,14 @@ func (h *OpenAIGatewayHandler) anthropicStreamingAwareError(c *gin.Context, stat
if
streamStarted
{
if
streamStarted
{
flusher
,
ok
:=
c
.
Writer
.
(
http
.
Flusher
)
flusher
,
ok
:=
c
.
Writer
.
(
http
.
Flusher
)
if
ok
{
if
ok
{
errorEvent
:=
"event: error
\n
data: "
+
`{"type":"error","error":{"type":`
+
strconv
.
Quote
(
errType
)
+
`,"message":`
+
strconv
.
Quote
(
message
)
+
`}}`
+
"
\n\n
"
errPayload
,
_
:=
json
.
Marshal
(
gin
.
H
{
fmt
.
Fprint
(
c
.
Writer
,
errorEvent
)
//nolint:errcheck
"type"
:
"error"
,
"error"
:
gin
.
H
{
"type"
:
errType
,
"message"
:
message
,
},
})
fmt
.
Fprintf
(
c
.
Writer
,
"event: error
\n
data: %s
\n\n
"
,
errPayload
)
//nolint:errcheck
flusher
.
Flush
()
flusher
.
Flush
()
}
}
return
return
...
...
backend/internal/pkg/apicompat/anthropic_responses_test.go
View file @
92159994
...
@@ -532,3 +532,204 @@ func TestResponsesAnthropicEventToSSE(t *testing.T) {
...
@@ -532,3 +532,204 @@ func TestResponsesAnthropicEventToSSE(t *testing.T) {
assert
.
Contains
(
t
,
sse
,
"data: "
)
assert
.
Contains
(
t
,
sse
,
"data: "
)
assert
.
Contains
(
t
,
sse
,
`"resp_1"`
)
assert
.
Contains
(
t
,
sse
,
`"resp_1"`
)
}
}
// ---------------------------------------------------------------------------
// response.failed tests
// ---------------------------------------------------------------------------
func
TestStreamingFailed
(
t
*
testing
.
T
)
{
state
:=
NewResponsesEventToAnthropicState
()
// 1. response.created
ResponsesEventToAnthropicEvents
(
&
ResponsesStreamEvent
{
Type
:
"response.created"
,
Response
:
&
ResponsesResponse
{
ID
:
"resp_fail_1"
,
Model
:
"gpt-5.2"
},
},
state
)
// 2. Some text output before failure
ResponsesEventToAnthropicEvents
(
&
ResponsesStreamEvent
{
Type
:
"response.output_text.delta"
,
Delta
:
"Partial output before failure"
,
},
state
)
// 3. response.failed
events
:=
ResponsesEventToAnthropicEvents
(
&
ResponsesStreamEvent
{
Type
:
"response.failed"
,
Response
:
&
ResponsesResponse
{
Status
:
"failed"
,
Error
:
&
ResponsesError
{
Code
:
"server_error"
,
Message
:
"Internal error"
},
Usage
:
&
ResponsesUsage
{
InputTokens
:
50
,
OutputTokens
:
10
},
},
},
state
)
// Should close text block + message_delta + message_stop
require
.
Len
(
t
,
events
,
3
)
assert
.
Equal
(
t
,
"content_block_stop"
,
events
[
0
]
.
Type
)
assert
.
Equal
(
t
,
"message_delta"
,
events
[
1
]
.
Type
)
assert
.
Equal
(
t
,
"end_turn"
,
events
[
1
]
.
Delta
.
StopReason
)
assert
.
Equal
(
t
,
50
,
events
[
1
]
.
Usage
.
InputTokens
)
assert
.
Equal
(
t
,
10
,
events
[
1
]
.
Usage
.
OutputTokens
)
assert
.
Equal
(
t
,
"message_stop"
,
events
[
2
]
.
Type
)
}
func
TestStreamingFailedNoOutput
(
t
*
testing
.
T
)
{
state
:=
NewResponsesEventToAnthropicState
()
// 1. response.created
ResponsesEventToAnthropicEvents
(
&
ResponsesStreamEvent
{
Type
:
"response.created"
,
Response
:
&
ResponsesResponse
{
ID
:
"resp_fail_2"
,
Model
:
"gpt-5.2"
},
},
state
)
// 2. response.failed with no prior output
events
:=
ResponsesEventToAnthropicEvents
(
&
ResponsesStreamEvent
{
Type
:
"response.failed"
,
Response
:
&
ResponsesResponse
{
Status
:
"failed"
,
Error
:
&
ResponsesError
{
Code
:
"rate_limit_error"
,
Message
:
"Too many requests"
},
Usage
:
&
ResponsesUsage
{
InputTokens
:
20
,
OutputTokens
:
0
},
},
},
state
)
// Should emit message_delta + message_stop (no block to close)
require
.
Len
(
t
,
events
,
2
)
assert
.
Equal
(
t
,
"message_delta"
,
events
[
0
]
.
Type
)
assert
.
Equal
(
t
,
"end_turn"
,
events
[
0
]
.
Delta
.
StopReason
)
assert
.
Equal
(
t
,
"message_stop"
,
events
[
1
]
.
Type
)
}
func
TestResponsesToAnthropic_Failed
(
t
*
testing
.
T
)
{
resp
:=
&
ResponsesResponse
{
ID
:
"resp_fail_3"
,
Model
:
"gpt-5.2"
,
Status
:
"failed"
,
Error
:
&
ResponsesError
{
Code
:
"server_error"
,
Message
:
"Something went wrong"
},
Output
:
[]
ResponsesOutput
{},
Usage
:
&
ResponsesUsage
{
InputTokens
:
30
,
OutputTokens
:
0
},
}
anth
:=
ResponsesToAnthropic
(
resp
,
"claude-opus-4-6"
)
// Failed status defaults to "end_turn" stop reason
assert
.
Equal
(
t
,
"end_turn"
,
anth
.
StopReason
)
// Should have at least an empty text block
require
.
Len
(
t
,
anth
.
Content
,
1
)
assert
.
Equal
(
t
,
"text"
,
anth
.
Content
[
0
]
.
Type
)
}
// ---------------------------------------------------------------------------
// thinking → reasoning conversion tests
// ---------------------------------------------------------------------------
func
TestAnthropicToResponses_ThinkingEnabled
(
t
*
testing
.
T
)
{
req
:=
&
AnthropicRequest
{
Model
:
"gpt-5.2"
,
MaxTokens
:
1024
,
Messages
:
[]
AnthropicMessage
{{
Role
:
"user"
,
Content
:
json
.
RawMessage
(
`"Hello"`
)}},
Thinking
:
&
AnthropicThinking
{
Type
:
"enabled"
,
BudgetTokens
:
10000
},
}
resp
,
err
:=
AnthropicToResponses
(
req
)
require
.
NoError
(
t
,
err
)
require
.
NotNil
(
t
,
resp
.
Reasoning
)
assert
.
Equal
(
t
,
"high"
,
resp
.
Reasoning
.
Effort
)
assert
.
Equal
(
t
,
"auto"
,
resp
.
Reasoning
.
Summary
)
assert
.
Contains
(
t
,
resp
.
Include
,
"reasoning.encrypted_content"
)
assert
.
NotContains
(
t
,
resp
.
Include
,
"reasoning.summary"
)
}
func
TestAnthropicToResponses_ThinkingAdaptive
(
t
*
testing
.
T
)
{
req
:=
&
AnthropicRequest
{
Model
:
"gpt-5.2"
,
MaxTokens
:
1024
,
Messages
:
[]
AnthropicMessage
{{
Role
:
"user"
,
Content
:
json
.
RawMessage
(
`"Hello"`
)}},
Thinking
:
&
AnthropicThinking
{
Type
:
"adaptive"
,
BudgetTokens
:
5000
},
}
resp
,
err
:=
AnthropicToResponses
(
req
)
require
.
NoError
(
t
,
err
)
require
.
NotNil
(
t
,
resp
.
Reasoning
)
assert
.
Equal
(
t
,
"medium"
,
resp
.
Reasoning
.
Effort
)
assert
.
Equal
(
t
,
"auto"
,
resp
.
Reasoning
.
Summary
)
assert
.
NotContains
(
t
,
resp
.
Include
,
"reasoning.summary"
)
}
func
TestAnthropicToResponses_ThinkingDisabled
(
t
*
testing
.
T
)
{
req
:=
&
AnthropicRequest
{
Model
:
"gpt-5.2"
,
MaxTokens
:
1024
,
Messages
:
[]
AnthropicMessage
{{
Role
:
"user"
,
Content
:
json
.
RawMessage
(
`"Hello"`
)}},
Thinking
:
&
AnthropicThinking
{
Type
:
"disabled"
},
}
resp
,
err
:=
AnthropicToResponses
(
req
)
require
.
NoError
(
t
,
err
)
assert
.
Nil
(
t
,
resp
.
Reasoning
)
assert
.
NotContains
(
t
,
resp
.
Include
,
"reasoning.summary"
)
}
func
TestAnthropicToResponses_NoThinking
(
t
*
testing
.
T
)
{
req
:=
&
AnthropicRequest
{
Model
:
"gpt-5.2"
,
MaxTokens
:
1024
,
Messages
:
[]
AnthropicMessage
{{
Role
:
"user"
,
Content
:
json
.
RawMessage
(
`"Hello"`
)}},
}
resp
,
err
:=
AnthropicToResponses
(
req
)
require
.
NoError
(
t
,
err
)
assert
.
Nil
(
t
,
resp
.
Reasoning
)
}
// ---------------------------------------------------------------------------
// tool_choice conversion tests
// ---------------------------------------------------------------------------
func
TestAnthropicToResponses_ToolChoiceAuto
(
t
*
testing
.
T
)
{
req
:=
&
AnthropicRequest
{
Model
:
"gpt-5.2"
,
MaxTokens
:
1024
,
Messages
:
[]
AnthropicMessage
{{
Role
:
"user"
,
Content
:
json
.
RawMessage
(
`"Hello"`
)}},
ToolChoice
:
json
.
RawMessage
(
`{"type":"auto"}`
),
}
resp
,
err
:=
AnthropicToResponses
(
req
)
require
.
NoError
(
t
,
err
)
var
tc
string
require
.
NoError
(
t
,
json
.
Unmarshal
(
resp
.
ToolChoice
,
&
tc
))
assert
.
Equal
(
t
,
"auto"
,
tc
)
}
func
TestAnthropicToResponses_ToolChoiceAny
(
t
*
testing
.
T
)
{
req
:=
&
AnthropicRequest
{
Model
:
"gpt-5.2"
,
MaxTokens
:
1024
,
Messages
:
[]
AnthropicMessage
{{
Role
:
"user"
,
Content
:
json
.
RawMessage
(
`"Hello"`
)}},
ToolChoice
:
json
.
RawMessage
(
`{"type":"any"}`
),
}
resp
,
err
:=
AnthropicToResponses
(
req
)
require
.
NoError
(
t
,
err
)
var
tc
string
require
.
NoError
(
t
,
json
.
Unmarshal
(
resp
.
ToolChoice
,
&
tc
))
assert
.
Equal
(
t
,
"required"
,
tc
)
}
func
TestAnthropicToResponses_ToolChoiceSpecific
(
t
*
testing
.
T
)
{
req
:=
&
AnthropicRequest
{
Model
:
"gpt-5.2"
,
MaxTokens
:
1024
,
Messages
:
[]
AnthropicMessage
{{
Role
:
"user"
,
Content
:
json
.
RawMessage
(
`"Hello"`
)}},
ToolChoice
:
json
.
RawMessage
(
`{"type":"tool","name":"get_weather"}`
),
}
resp
,
err
:=
AnthropicToResponses
(
req
)
require
.
NoError
(
t
,
err
)
var
tc
map
[
string
]
any
require
.
NoError
(
t
,
json
.
Unmarshal
(
resp
.
ToolChoice
,
&
tc
))
assert
.
Equal
(
t
,
"function"
,
tc
[
"type"
])
fn
,
ok
:=
tc
[
"function"
]
.
(
map
[
string
]
any
)
require
.
True
(
t
,
ok
)
assert
.
Equal
(
t
,
"get_weather"
,
fn
[
"name"
])
}
backend/internal/pkg/apicompat/anthropic_to_responses.go
View file @
92159994
...
@@ -2,6 +2,7 @@ package apicompat
...
@@ -2,6 +2,7 @@ package apicompat
import
(
import
(
"encoding/json"
"encoding/json"
"fmt"
"strings"
"strings"
)
)
...
@@ -44,9 +45,65 @@ func AnthropicToResponses(req *AnthropicRequest) (*ResponsesRequest, error) {
...
@@ -44,9 +45,65 @@ func AnthropicToResponses(req *AnthropicRequest) (*ResponsesRequest, error) {
out
.
Tools
=
convertAnthropicToolsToResponses
(
req
.
Tools
)
out
.
Tools
=
convertAnthropicToolsToResponses
(
req
.
Tools
)
}
}
// Convert thinking → reasoning.
// generate_summary="auto" causes the upstream to emit reasoning_summary_text
// streaming events; the include array only needs reasoning.encrypted_content
// (already set above) for content continuity.
if
req
.
Thinking
!=
nil
{
switch
req
.
Thinking
.
Type
{
case
"enabled"
:
out
.
Reasoning
=
&
ResponsesReasoning
{
Effort
:
"high"
,
Summary
:
"auto"
}
case
"adaptive"
:
out
.
Reasoning
=
&
ResponsesReasoning
{
Effort
:
"medium"
,
Summary
:
"auto"
}
}
// "disabled" or unknown → omit reasoning
}
// Convert tool_choice
if
len
(
req
.
ToolChoice
)
>
0
{
tc
,
err
:=
convertAnthropicToolChoiceToResponses
(
req
.
ToolChoice
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"convert tool_choice: %w"
,
err
)
}
out
.
ToolChoice
=
tc
}
return
out
,
nil
return
out
,
nil
}
}
// convertAnthropicToolChoiceToResponses maps Anthropic tool_choice to Responses format.
//
// {"type":"auto"} → "auto"
// {"type":"any"} → "required"
// {"type":"none"} → "none"
// {"type":"tool","name":"X"} → {"type":"function","function":{"name":"X"}}
func
convertAnthropicToolChoiceToResponses
(
raw
json
.
RawMessage
)
(
json
.
RawMessage
,
error
)
{
var
tc
struct
{
Type
string
`json:"type"`
Name
string
`json:"name"`
}
if
err
:=
json
.
Unmarshal
(
raw
,
&
tc
);
err
!=
nil
{
return
nil
,
err
}
switch
tc
.
Type
{
case
"auto"
:
return
json
.
Marshal
(
"auto"
)
case
"any"
:
return
json
.
Marshal
(
"required"
)
case
"none"
:
return
json
.
Marshal
(
"none"
)
case
"tool"
:
return
json
.
Marshal
(
map
[
string
]
any
{
"type"
:
"function"
,
"function"
:
map
[
string
]
string
{
"name"
:
tc
.
Name
},
})
default
:
// Pass through unknown types as-is
return
raw
,
nil
}
}
// convertAnthropicToResponsesInput builds the Responses API input items array
// convertAnthropicToResponsesInput builds the Responses API input items array
// from the Anthropic system field and message list.
// from the Anthropic system field and message list.
func
convertAnthropicToResponsesInput
(
system
json
.
RawMessage
,
msgs
[]
AnthropicMessage
)
([]
ResponsesInputItem
,
error
)
{
func
convertAnthropicToResponsesInput
(
system
json
.
RawMessage
,
msgs
[]
AnthropicMessage
)
([]
ResponsesInputItem
,
error
)
{
...
...
backend/internal/pkg/apicompat/responses_to_anthropic.go
View file @
92159994
...
@@ -153,7 +153,7 @@ func ResponsesEventToAnthropicEvents(
...
@@ -153,7 +153,7 @@ func ResponsesEventToAnthropicEvents(
return
resToAnthHandleReasoningDelta
(
evt
,
state
)
return
resToAnthHandleReasoningDelta
(
evt
,
state
)
case
"response.reasoning_summary_text.done"
:
case
"response.reasoning_summary_text.done"
:
return
resToAnthHandleBlockDone
(
state
)
return
resToAnthHandleBlockDone
(
state
)
case
"response.completed"
,
"response.incomplete"
:
case
"response.completed"
,
"response.incomplete"
,
"response.failed"
:
return
resToAnthHandleCompleted
(
evt
,
state
)
return
resToAnthHandleCompleted
(
evt
,
state
)
default
:
default
:
return
nil
return
nil
...
...
backend/internal/pkg/apicompat/types.go
View file @
92159994
// Package apicompat provides type definitions and conversion utilities for
// Package apicompat provides type definitions and conversion utilities for
// translating between Anthropic Messages
,
OpenAI
Chat Completions, and OpenAI
// translating between Anthropic Messages
and
OpenAI
Responses API formats.
//
Responses API formats.
It enables multi-protocol support so that clients
// It enables multi-protocol support so that clients
using different API
//
using different API
formats can be served through a unified gateway.
// formats can be served through a unified gateway.
package
apicompat
package
apicompat
import
"encoding/json"
import
"encoding/json"
...
@@ -21,6 +21,14 @@ type AnthropicRequest struct {
...
@@ -21,6 +21,14 @@ type AnthropicRequest struct {
Temperature
*
float64
`json:"temperature,omitempty"`
Temperature
*
float64
`json:"temperature,omitempty"`
TopP
*
float64
`json:"top_p,omitempty"`
TopP
*
float64
`json:"top_p,omitempty"`
StopSeqs
[]
string
`json:"stop_sequences,omitempty"`
StopSeqs
[]
string
`json:"stop_sequences,omitempty"`
Thinking
*
AnthropicThinking
`json:"thinking,omitempty"`
ToolChoice
json
.
RawMessage
`json:"tool_choice,omitempty"`
}
// AnthropicThinking configures extended thinking in the Anthropic API.
type
AnthropicThinking
struct
{
Type
string
`json:"type"`
// "enabled" | "adaptive" | "disabled"
BudgetTokens
int
`json:"budget_tokens,omitempty"`
// max thinking tokens
}
}
// AnthropicMessage is a single message in the Anthropic conversation.
// AnthropicMessage is a single message in the Anthropic conversation.
...
@@ -120,143 +128,29 @@ type AnthropicDelta struct {
...
@@ -120,143 +128,29 @@ type AnthropicDelta struct {
StopSequence
*
string
`json:"stop_sequence,omitempty"`
StopSequence
*
string
`json:"stop_sequence,omitempty"`
}
}
// ---------------------------------------------------------------------------
// OpenAI Chat Completions API types
// ---------------------------------------------------------------------------
// ChatRequest is the request body for POST /v1/chat/completions.
type
ChatRequest
struct
{
Model
string
`json:"model"`
Messages
[]
ChatMessage
`json:"messages"`
MaxTokens
*
int
`json:"max_tokens,omitempty"`
Temperature
*
float64
`json:"temperature,omitempty"`
TopP
*
float64
`json:"top_p,omitempty"`
Stream
bool
`json:"stream,omitempty"`
Tools
[]
ChatTool
`json:"tools,omitempty"`
Stop
json
.
RawMessage
`json:"stop,omitempty"`
// string or []string
}
// ChatMessage is a single message in the Chat Completions conversation.
type
ChatMessage
struct
{
Role
string
`json:"role"`
// "system" | "user" | "assistant" | "tool"
Content
json
.
RawMessage
`json:"content,omitempty"`
// string or []ChatContentPart
// assistant fields
ToolCalls
[]
ChatToolCall
`json:"tool_calls,omitempty"`
// tool fields
ToolCallID
string
`json:"tool_call_id,omitempty"`
// Copilot-specific reasoning passthrough
ReasoningText
string
`json:"reasoning_text,omitempty"`
ReasoningOpaque
string
`json:"reasoning_opaque,omitempty"`
}
// ChatContentPart is a typed content part in a multi-part message.
type
ChatContentPart
struct
{
Type
string
`json:"type"`
// "text" | "image_url"
Text
string
`json:"text,omitempty"`
}
// ChatToolCall represents a tool invocation in an assistant message.
// In streaming deltas, Index identifies which tool call is being updated.
type
ChatToolCall
struct
{
Index
int
`json:"index"`
ID
string
`json:"id,omitempty"`
Type
string
`json:"type,omitempty"`
// "function"
Function
ChatFunctionCall
`json:"function"`
}
// ChatFunctionCall holds the function name and arguments.
type
ChatFunctionCall
struct
{
Name
string
`json:"name"`
Arguments
string
`json:"arguments"`
}
// ChatTool describes a tool available to the model.
type
ChatTool
struct
{
Type
string
`json:"type"`
// "function"
Function
ChatFunction
`json:"function"`
}
// ChatFunction is the function definition inside a ChatTool.
type
ChatFunction
struct
{
Name
string
`json:"name"`
Description
string
`json:"description,omitempty"`
Parameters
json
.
RawMessage
`json:"parameters,omitempty"`
// JSON Schema
}
// ChatResponse is the non-streaming response from POST /v1/chat/completions.
type
ChatResponse
struct
{
ID
string
`json:"id"`
Object
string
`json:"object"`
// "chat.completion"
Created
int64
`json:"created"`
Model
string
`json:"model"`
Choices
[]
ChatChoice
`json:"choices"`
Usage
*
ChatUsage
`json:"usage,omitempty"`
}
// ChatChoice is one completion choice.
type
ChatChoice
struct
{
Index
int
`json:"index"`
Message
ChatMessage
`json:"message"`
FinishReason
string
`json:"finish_reason"`
}
// ChatUsage holds token counts in Chat Completions format.
type
ChatUsage
struct
{
PromptTokens
int
`json:"prompt_tokens"`
CompletionTokens
int
`json:"completion_tokens"`
TotalTokens
int
`json:"total_tokens"`
}
// ---------------------------------------------------------------------------
// Chat Completions SSE types
// ---------------------------------------------------------------------------
// ChatStreamChunk is a single SSE chunk in the Chat Completions streaming protocol.
type
ChatStreamChunk
struct
{
ID
string
`json:"id"`
Object
string
`json:"object"`
// "chat.completion.chunk"
Created
int64
`json:"created"`
Model
string
`json:"model"`
Choices
[]
ChatStreamChoice
`json:"choices"`
Usage
*
ChatUsage
`json:"usage,omitempty"`
}
// ChatStreamChoice is one choice inside a streaming chunk.
type
ChatStreamChoice
struct
{
Index
int
`json:"index"`
Delta
ChatStreamDelta
`json:"delta"`
FinishReason
*
string
`json:"finish_reason"`
}
// ChatStreamDelta carries incremental content in a streaming chunk.
type
ChatStreamDelta
struct
{
Role
string
`json:"role,omitempty"`
Content
string
`json:"content,omitempty"`
ToolCalls
[]
ChatToolCall
`json:"tool_calls,omitempty"`
// Copilot-specific reasoning passthrough (streaming)
ReasoningText
string
`json:"reasoning_text,omitempty"`
ReasoningOpaque
string
`json:"reasoning_opaque,omitempty"`
}
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// OpenAI Responses API types
// OpenAI Responses API types
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ResponsesRequest is the request body for POST /v1/responses.
// ResponsesRequest is the request body for POST /v1/responses.
type
ResponsesRequest
struct
{
type
ResponsesRequest
struct
{
Model
string
`json:"model"`
Model
string
`json:"model"`
Input
json
.
RawMessage
`json:"input"`
// string or []ResponsesInputItem
Input
json
.
RawMessage
`json:"input"`
// string or []ResponsesInputItem
MaxOutputTokens
*
int
`json:"max_output_tokens,omitempty"`
MaxOutputTokens
*
int
`json:"max_output_tokens,omitempty"`
Temperature
*
float64
`json:"temperature,omitempty"`
Temperature
*
float64
`json:"temperature,omitempty"`
TopP
*
float64
`json:"top_p,omitempty"`
TopP
*
float64
`json:"top_p,omitempty"`
Stream
bool
`json:"stream,omitempty"`
Stream
bool
`json:"stream,omitempty"`
Tools
[]
ResponsesTool
`json:"tools,omitempty"`
Tools
[]
ResponsesTool
`json:"tools,omitempty"`
Include
[]
string
`json:"include,omitempty"`
Include
[]
string
`json:"include,omitempty"`
Store
*
bool
`json:"store,omitempty"`
Store
*
bool
`json:"store,omitempty"`
Reasoning
*
ResponsesReasoning
`json:"reasoning,omitempty"`
ToolChoice
json
.
RawMessage
`json:"tool_choice,omitempty"`
}
// ResponsesReasoning configures reasoning effort in the Responses API.
type
ResponsesReasoning
struct
{
Effort
string
`json:"effort"`
// "low" | "medium" | "high"
Summary
string
`json:"summary,omitempty"`
// "auto" | "concise" | "detailed"
}
}
// ResponsesInputItem is one item in the Responses API input array.
// ResponsesInputItem is one item in the Responses API input array.
...
@@ -305,6 +199,15 @@ type ResponsesResponse struct {
...
@@ -305,6 +199,15 @@ type ResponsesResponse struct {
// incomplete_details is present when status="incomplete"
// incomplete_details is present when status="incomplete"
IncompleteDetails
*
ResponsesIncompleteDetails
`json:"incomplete_details,omitempty"`
IncompleteDetails
*
ResponsesIncompleteDetails
`json:"incomplete_details,omitempty"`
// Error is present when status="failed"
Error
*
ResponsesError
`json:"error,omitempty"`
}
// ResponsesError describes an error in a failed response.
type
ResponsesError
struct
{
Code
string
`json:"code"`
Message
string
`json:"message"`
}
}
// ResponsesIncompleteDetails explains why a response is incomplete.
// ResponsesIncompleteDetails explains why a response is incomplete.
...
@@ -349,6 +252,16 @@ type ResponsesUsage struct {
...
@@ -349,6 +252,16 @@ type ResponsesUsage struct {
OutputTokensDetails
*
ResponsesOutputTokensDetails
`json:"output_tokens_details,omitempty"`
OutputTokensDetails
*
ResponsesOutputTokensDetails
`json:"output_tokens_details,omitempty"`
}
}
// ResponsesInputTokensDetails breaks down input token usage.
type
ResponsesInputTokensDetails
struct
{
CachedTokens
int
`json:"cached_tokens,omitempty"`
}
// ResponsesOutputTokensDetails breaks down output token usage.
type
ResponsesOutputTokensDetails
struct
{
ReasoningTokens
int
`json:"reasoning_tokens,omitempty"`
}
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// Responses SSE event types
// Responses SSE event types
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
...
@@ -388,153 +301,6 @@ type ResponsesStreamEvent struct {
...
@@ -388,153 +301,6 @@ type ResponsesStreamEvent struct {
SequenceNumber
int
`json:"sequence_number,omitempty"`
SequenceNumber
int
`json:"sequence_number,omitempty"`
}
}
// ResponsesOutputReasoning is a reasoning output item in the Responses API.
// This type represents the "type":"reasoning" output item that contains
// extended thinking from the model.
type
ResponsesOutputReasoning
struct
{
ID
string
`json:"id,omitempty"`
Type
string
`json:"type"`
// "reasoning"
Status
string
`json:"status,omitempty"`
// "in_progress" | "completed" | "incomplete"
EncryptedContent
string
`json:"encrypted_content,omitempty"`
Summary
[]
ResponsesReasoningSummary
`json:"summary,omitempty"`
}
// ResponsesReasoningSummary is a summary text block inside a reasoning output.
type
ResponsesReasoningSummary
struct
{
Type
string
`json:"type"`
// "summary_text"
Text
string
`json:"text"`
}
// ResponsesStreamState maintains the state for converting Responses streaming
// events to Chat Completions format. It tracks content blocks, tool calls,
// reasoning blocks, and other streaming artifacts.
type
ResponsesStreamState
struct
{
// Response metadata
ID
string
Model
string
Created
int64
// Content tracking
ContentIndex
int
CurrentText
string
CurrentItemID
string
PendingText
[]
string
// Text to accumulate before emitting
// Tool call tracking
ToolCalls
[]
ResponsesToolCallState
CurrentToolCall
*
ResponsesToolCallState
// Reasoning tracking
ReasoningBlocks
[]
ResponsesReasoningState
CurrentReasoning
*
ResponsesReasoningState
// Usage tracking
InputTokens
int
OutputTokens
int
// Status tracking
Status
string
FinishReason
string
}
// ResponsesToolCallState tracks a single tool call during streaming.
type
ResponsesToolCallState
struct
{
Index
int
ItemID
string
CallID
string
Name
string
Arguments
string
Status
string
IsComplete
bool
}
// ResponsesReasoningState tracks a reasoning block during streaming.
type
ResponsesReasoningState
struct
{
ItemID
string
SummaryIndex
int
SummaryText
string
Status
string
IsComplete
bool
}
// ResponsesUsageDetail provides additional token usage details in Responses format.
type
ResponsesUsageDetail
struct
{
InputTokens
int
`json:"input_tokens"`
OutputTokens
int
`json:"output_tokens"`
TotalTokens
int
`json:"total_tokens"`
// Optional detailed breakdown
InputTokensDetails
*
ResponsesInputTokensDetails
`json:"input_tokens_details,omitempty"`
OutputTokensDetails
*
ResponsesOutputTokensDetails
`json:"output_tokens_details,omitempty"`
}
// ResponsesInputTokensDetails breaks down input token usage.
type
ResponsesInputTokensDetails
struct
{
CachedTokens
int
`json:"cached_tokens,omitempty"`
}
// ResponsesOutputTokensDetails breaks down output token usage.
type
ResponsesOutputTokensDetails
struct
{
ReasoningTokens
int
`json:"reasoning_tokens,omitempty"`
}
// ---------------------------------------------------------------------------
// Finish reason mapping helpers
// ---------------------------------------------------------------------------
// ChatFinishToAnthropic maps a Chat Completions finish_reason to an Anthropic stop_reason.
func
ChatFinishToAnthropic
(
reason
string
)
string
{
switch
reason
{
case
"stop"
:
return
"end_turn"
case
"tool_calls"
:
return
"tool_use"
case
"length"
:
return
"max_tokens"
default
:
return
"end_turn"
}
}
// AnthropicStopToChat maps an Anthropic stop_reason to a Chat Completions finish_reason.
func
AnthropicStopToChat
(
reason
string
)
string
{
switch
reason
{
case
"end_turn"
:
return
"stop"
case
"tool_use"
:
return
"tool_calls"
case
"max_tokens"
:
return
"length"
default
:
return
"stop"
}
}
// ResponsesStatusToChat maps a Responses API status to a Chat Completions finish_reason.
func
ResponsesStatusToChat
(
status
string
,
details
*
ResponsesIncompleteDetails
)
string
{
switch
status
{
case
"completed"
:
return
"stop"
case
"incomplete"
:
if
details
!=
nil
&&
details
.
Reason
==
"max_output_tokens"
{
return
"length"
}
return
"stop"
default
:
return
"stop"
}
}
// ChatFinishToResponsesStatus maps a Chat Completions finish_reason to a Responses status.
func
ChatFinishToResponsesStatus
(
reason
string
)
string
{
switch
reason
{
case
"length"
:
return
"incomplete"
default
:
return
"completed"
}
}
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// Shared constants
// Shared constants
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
...
...
backend/internal/service/openai_gateway_messages.go
View file @
92159994
...
@@ -49,7 +49,7 @@ func (s *OpenAIGatewayService) ForwardAsAnthropic(
...
@@ -49,7 +49,7 @@ func (s *OpenAIGatewayService) ForwardAsAnthropic(
mappedModel
:=
account
.
GetMappedModel
(
originalModel
)
mappedModel
:=
account
.
GetMappedModel
(
originalModel
)
responsesReq
.
Model
=
mappedModel
responsesReq
.
Model
=
mappedModel
logger
.
L
()
.
Info
(
"openai messages: model mapping applied"
,
logger
.
L
()
.
Debug
(
"openai messages: model mapping applied"
,
zap
.
Int64
(
"account_id"
,
account
.
ID
),
zap
.
Int64
(
"account_id"
,
account
.
ID
),
zap
.
String
(
"original_model"
,
originalModel
),
zap
.
String
(
"original_model"
,
originalModel
),
zap
.
String
(
"mapped_model"
,
mappedModel
),
zap
.
String
(
"mapped_model"
,
mappedModel
),
...
@@ -67,7 +67,7 @@ func (s *OpenAIGatewayService) ForwardAsAnthropic(
...
@@ -67,7 +67,7 @@ func (s *OpenAIGatewayService) ForwardAsAnthropic(
if
err
:=
json
.
Unmarshal
(
responsesBody
,
&
reqBody
);
err
!=
nil
{
if
err
:=
json
.
Unmarshal
(
responsesBody
,
&
reqBody
);
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"unmarshal for codex transform: %w"
,
err
)
return
nil
,
fmt
.
Errorf
(
"unmarshal for codex transform: %w"
,
err
)
}
}
applyCodexOAuthTransform
(
reqBody
,
false
)
applyCodexOAuthTransform
(
reqBody
,
false
,
false
)
// OAuth codex transform forces stream=true upstream, so always use
// OAuth codex transform forces stream=true upstream, so always use
// the streaming response handler regardless of what the client asked.
// the streaming response handler regardless of what the client asked.
isStream
=
true
isStream
=
true
...
@@ -148,9 +148,9 @@ func (s *OpenAIGatewayService) ForwardAsAnthropic(
...
@@ -148,9 +148,9 @@ func (s *OpenAIGatewayService) ForwardAsAnthropic(
// 9. Handle normal response
// 9. Handle normal response
if
isStream
{
if
isStream
{
return
s
.
handleAnthropicStreamingResponse
(
resp
,
c
,
originalModel
,
startTime
)
return
s
.
handleAnthropicStreamingResponse
(
resp
,
c
,
originalModel
,
mappedModel
,
startTime
)
}
}
return
s
.
handleAnthropicNonStreamingResponse
(
resp
,
c
,
originalModel
,
startTime
)
return
s
.
handleAnthropicNonStreamingResponse
(
resp
,
c
,
originalModel
,
mappedModel
,
startTime
)
}
}
// handleAnthropicErrorResponse reads an upstream error and returns it in
// handleAnthropicErrorResponse reads an upstream error and returns it in
...
@@ -200,6 +200,7 @@ func (s *OpenAIGatewayService) handleAnthropicNonStreamingResponse(
...
@@ -200,6 +200,7 @@ func (s *OpenAIGatewayService) handleAnthropicNonStreamingResponse(
resp
*
http
.
Response
,
resp
*
http
.
Response
,
c
*
gin
.
Context
,
c
*
gin
.
Context
,
originalModel
string
,
originalModel
string
,
mappedModel
string
,
startTime
time
.
Time
,
startTime
time
.
Time
,
)
(
*
OpenAIForwardResult
,
error
)
{
)
(
*
OpenAIForwardResult
,
error
)
{
requestID
:=
resp
.
Header
.
Get
(
"x-request-id"
)
requestID
:=
resp
.
Header
.
Get
(
"x-request-id"
)
...
@@ -233,11 +234,12 @@ func (s *OpenAIGatewayService) handleAnthropicNonStreamingResponse(
...
@@ -233,11 +234,12 @@ func (s *OpenAIGatewayService) handleAnthropicNonStreamingResponse(
c
.
JSON
(
http
.
StatusOK
,
anthropicResp
)
c
.
JSON
(
http
.
StatusOK
,
anthropicResp
)
return
&
OpenAIForwardResult
{
return
&
OpenAIForwardResult
{
RequestID
:
requestID
,
RequestID
:
requestID
,
Usage
:
usage
,
Usage
:
usage
,
Model
:
originalModel
,
Model
:
originalModel
,
Stream
:
false
,
BillingModel
:
mappedModel
,
Duration
:
time
.
Since
(
startTime
),
Stream
:
false
,
Duration
:
time
.
Since
(
startTime
),
},
nil
},
nil
}
}
...
@@ -247,6 +249,7 @@ func (s *OpenAIGatewayService) handleAnthropicStreamingResponse(
...
@@ -247,6 +249,7 @@ func (s *OpenAIGatewayService) handleAnthropicStreamingResponse(
resp
*
http
.
Response
,
resp
*
http
.
Response
,
c
*
gin
.
Context
,
c
*
gin
.
Context
,
originalModel
string
,
originalModel
string
,
mappedModel
string
,
startTime
time
.
Time
,
startTime
time
.
Time
,
)
(
*
OpenAIForwardResult
,
error
)
{
)
(
*
OpenAIForwardResult
,
error
)
{
requestID
:=
resp
.
Header
.
Get
(
"x-request-id"
)
requestID
:=
resp
.
Header
.
Get
(
"x-request-id"
)
...
@@ -293,7 +296,7 @@ func (s *OpenAIGatewayService) handleAnthropicStreamingResponse(
...
@@ -293,7 +296,7 @@ func (s *OpenAIGatewayService) handleAnthropicStreamingResponse(
}
}
// Extract usage from completion events
// Extract usage from completion events
if
(
event
.
Type
==
"response.completed"
||
event
.
Type
==
"response.incomplete"
)
&&
if
(
event
.
Type
==
"response.completed"
||
event
.
Type
==
"response.incomplete"
||
event
.
Type
==
"response.failed"
)
&&
event
.
Response
!=
nil
&&
event
.
Response
.
Usage
!=
nil
{
event
.
Response
!=
nil
&&
event
.
Response
.
Usage
!=
nil
{
usage
=
OpenAIUsage
{
usage
=
OpenAIUsage
{
InputTokens
:
event
.
Response
.
Usage
.
InputTokens
,
InputTokens
:
event
.
Response
.
Usage
.
InputTokens
,
...
@@ -324,6 +327,7 @@ func (s *OpenAIGatewayService) handleAnthropicStreamingResponse(
...
@@ -324,6 +327,7 @@ func (s *OpenAIGatewayService) handleAnthropicStreamingResponse(
RequestID
:
requestID
,
RequestID
:
requestID
,
Usage
:
usage
,
Usage
:
usage
,
Model
:
originalModel
,
Model
:
originalModel
,
BillingModel
:
mappedModel
,
Stream
:
true
,
Stream
:
true
,
Duration
:
time
.
Since
(
startTime
),
Duration
:
time
.
Since
(
startTime
),
FirstTokenMs
:
firstTokenMs
,
FirstTokenMs
:
firstTokenMs
,
...
@@ -360,6 +364,7 @@ func (s *OpenAIGatewayService) handleAnthropicStreamingResponse(
...
@@ -360,6 +364,7 @@ func (s *OpenAIGatewayService) handleAnthropicStreamingResponse(
RequestID
:
requestID
,
RequestID
:
requestID
,
Usage
:
usage
,
Usage
:
usage
,
Model
:
originalModel
,
Model
:
originalModel
,
BillingModel
:
mappedModel
,
Stream
:
true
,
Stream
:
true
,
Duration
:
time
.
Since
(
startTime
),
Duration
:
time
.
Since
(
startTime
),
FirstTokenMs
:
firstTokenMs
,
FirstTokenMs
:
firstTokenMs
,
...
...
backend/internal/service/openai_gateway_service.go
View file @
92159994
...
@@ -207,7 +207,12 @@ type OpenAIUsage struct {
...
@@ -207,7 +207,12 @@ type OpenAIUsage struct {
type
OpenAIForwardResult
struct
{
type
OpenAIForwardResult
struct
{
RequestID
string
RequestID
string
Usage
OpenAIUsage
Usage
OpenAIUsage
Model
string
Model
string
// 原始模型(用于响应和日志显示)
// BillingModel is the model used for cost calculation.
// When non-empty, CalculateCost uses this instead of Model.
// This is set by the Anthropic Messages conversion path where
// the mapped upstream model differs from the client-facing model.
BillingModel
string
// ReasoningEffort is extracted from request body (reasoning.effort) or derived from model suffix.
// ReasoningEffort is extracted from request body (reasoning.effort) or derived from model suffix.
// Stored for usage records display; nil means not provided / not applicable.
// Stored for usage records display; nil means not provided / not applicable.
ReasoningEffort
*
string
ReasoningEffort
*
string
...
@@ -3610,7 +3615,11 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
...
@@ -3610,7 +3615,11 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
multiplier
=
resolver
.
Resolve
(
ctx
,
user
.
ID
,
*
apiKey
.
GroupID
,
apiKey
.
Group
.
RateMultiplier
)
multiplier
=
resolver
.
Resolve
(
ctx
,
user
.
ID
,
*
apiKey
.
GroupID
,
apiKey
.
Group
.
RateMultiplier
)
}
}
cost
,
err
:=
s
.
billingService
.
CalculateCost
(
result
.
Model
,
tokens
,
multiplier
)
billingModel
:=
result
.
Model
if
result
.
BillingModel
!=
""
{
billingModel
=
result
.
BillingModel
}
cost
,
err
:=
s
.
billingService
.
CalculateCost
(
billingModel
,
tokens
,
multiplier
)
if
err
!=
nil
{
if
err
!=
nil
{
cost
=
&
CostBreakdown
{
ActualCost
:
0
}
cost
=
&
CostBreakdown
{
ActualCost
:
0
}
}
}
...
@@ -3630,7 +3639,7 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
...
@@ -3630,7 +3639,7 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
APIKeyID
:
apiKey
.
ID
,
APIKeyID
:
apiKey
.
ID
,
AccountID
:
account
.
ID
,
AccountID
:
account
.
ID
,
RequestID
:
result
.
RequestID
,
RequestID
:
result
.
RequestID
,
Model
:
result
.
Model
,
Model
:
billing
Model
,
ReasoningEffort
:
result
.
ReasoningEffort
,
ReasoningEffort
:
result
.
ReasoningEffort
,
InputTokens
:
actualInputTokens
,
InputTokens
:
actualInputTokens
,
OutputTokens
:
result
.
Usage
.
OutputTokens
,
OutputTokens
:
result
.
Usage
.
OutputTokens
,
...
...
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