WSv2 egress relays upstream events verbatim without the HTTP-path namespace restore, so flattening requests that take the WSv2 branch would surface flattened tool names the client cannot match. Resolve the WS transport decision before flattening and skip flattening only when the request will actually go WSv2 (passthrough accounts return via HTTP before the WSv2 branch and still flatten). Also check all type assertions in responses_namespace_test.go to satisfy golangci-lint errcheck.
84 lines
2.6 KiB
Go
84 lines
2.6 KiB
Go
package service
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"fmt"
|
||
|
||
"github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
const openAIResponsesNamespaceNamesContextKey = "openai_responses_namespace_names"
|
||
|
||
// shouldFlattenOpenAIResponsesNamespaces 判定原生 Responses 转发前是否摊平
|
||
// Codex namespace 工具。WSv2 上游原生支持 namespace,且 WS 出口
|
||
// (openai_ws_forwarder_v2)原样转发上游事件、不经 HTTP 回程还原,摊平后的
|
||
// 平名无法还原会破坏客户端工具匹配,因此实际走 WSv2 分支的请求保持 namespace
|
||
// 原样。透传账号先于 WSv2 分支经 HTTP 转发返回,仍需摊平。
|
||
func shouldFlattenOpenAIResponsesNamespaces(account *Account, transport OpenAIUpstreamTransport, passthroughEnabled bool) bool {
|
||
if account == nil || account.Type != AccountTypeOAuth {
|
||
return false
|
||
}
|
||
if transport == OpenAIUpstreamTransportResponsesWebsocketV2 && !passthroughEnabled {
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
func flattenOpenAIResponsesNamespaces(c *gin.Context, body []byte) ([]byte, error) {
|
||
if !bytes.Contains(body, []byte(`"namespace"`)) {
|
||
return body, nil
|
||
}
|
||
var requestBody map[string]any
|
||
if err := json.Unmarshal(body, &requestBody); err != nil {
|
||
return body, fmt.Errorf("decode OpenAI namespace body: %w", err)
|
||
}
|
||
names, changed, err := apicompat.FlattenResponsesNamespacesExcept(requestBody, map[string]bool{"image_gen": true})
|
||
if err != nil {
|
||
return body, err
|
||
}
|
||
if !changed {
|
||
return body, nil
|
||
}
|
||
rebuilt, err := marshalOpenAIUpstreamJSON(requestBody)
|
||
if err != nil {
|
||
return body, fmt.Errorf("encode OpenAI namespace body: %w", err)
|
||
}
|
||
setOpenAIResponsesNamespaceNames(c, names)
|
||
return rebuilt, nil
|
||
}
|
||
|
||
func setOpenAIResponsesNamespaceNames(c *gin.Context, names map[string]apicompat.ResponsesNamespaceName) {
|
||
if c != nil && len(names) > 0 {
|
||
c.Set(openAIResponsesNamespaceNamesContextKey, names)
|
||
}
|
||
}
|
||
|
||
func openAIResponsesNamespaceNames(c *gin.Context) map[string]apicompat.ResponsesNamespaceName {
|
||
if c == nil {
|
||
return nil
|
||
}
|
||
value, ok := c.Get(openAIResponsesNamespaceNamesContextKey)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
names, _ := value.(map[string]apicompat.ResponsesNamespaceName)
|
||
return names
|
||
}
|
||
|
||
func restoreOpenAIResponsesNamespacePayload(c *gin.Context, payload []byte) ([]byte, error) {
|
||
names := openAIResponsesNamespaceNames(c)
|
||
if len(names) == 0 || !json.Valid(payload) {
|
||
return payload, nil
|
||
}
|
||
restored, changed, err := apicompat.RestoreResponsesNamespaceCalls(payload, names)
|
||
if err != nil {
|
||
return payload, err
|
||
}
|
||
if changed {
|
||
return restored, nil
|
||
}
|
||
return payload, nil
|
||
}
|