99 lines
3.3 KiB
Go
99 lines
3.3 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"oci-portal/internal/aiwire"
|
|
)
|
|
|
|
// respRejectStateful 拒绝有状态特性(网关无状态)。
|
|
func respRejectStateful(req aiwire.RespRequest) error {
|
|
if req.PreviousResponseID != "" {
|
|
return fmt.Errorf("previous_response_id 不支持:网关不保存历史响应,请在 input 中自带完整上下文(store:false 模式)")
|
|
}
|
|
if len(req.Conversation) > 0 && string(req.Conversation) != "null" {
|
|
return fmt.Errorf("conversation 不支持:网关不保存对话状态")
|
|
}
|
|
if req.Background != nil && *req.Background {
|
|
return fmt.Errorf("background 模式不支持")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RespServerTools 报告工具列表是否含 xAI 服务端工具(web_search / x_search)。
|
|
func RespServerTools(tools []aiwire.RespTool) bool {
|
|
for _, t := range tools {
|
|
if t.Type == "web_search" || t.Type == "x_search" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// RespPassthroughValidate 校验直通请求:模型必填,有状态特性不支持,工具类型
|
|
// 只放行 function 与已实测的 web_search / x_search;流式仅在含服务端工具时拒绝
|
|
// (工具流式事件形态未实测,不放开)。
|
|
func RespPassthroughValidate(req aiwire.RespRequest) error {
|
|
if strings.TrimSpace(req.Model) == "" {
|
|
return fmt.Errorf("model 不能为空")
|
|
}
|
|
if req.Stream && RespServerTools(req.Tools) {
|
|
return fmt.Errorf("服务端工具暂不支持流式:请去掉 stream 或改用 function 工具")
|
|
}
|
|
if err := respRejectStateful(req); err != nil {
|
|
return err
|
|
}
|
|
for _, t := range req.Tools {
|
|
switch t.Type {
|
|
case "function", "web_search", "x_search":
|
|
default:
|
|
return fmt.Errorf("不支持的工具类型 %q:服务端工具仅支持 web_search / x_search", t.Type)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RespPassthroughBody 以原始请求体为基构造上游 body:强制 store:false(禁上游
|
|
// 存态),stream 原样保留(流式直通);用 json.Number 保真未知字段与数值。
|
|
func RespPassthroughBody(raw []byte) ([]byte, error) {
|
|
dec := json.NewDecoder(strings.NewReader(string(raw)))
|
|
dec.UseNumber()
|
|
var body map[string]any
|
|
if err := dec.Decode(&body); err != nil {
|
|
return nil, fmt.Errorf("解析请求体: %w", err)
|
|
}
|
|
body["store"] = false
|
|
return json.Marshal(body)
|
|
}
|
|
|
|
// RespPassthroughUsage 从直通响应提取用量;缺失时返回 nil(日志记零)。
|
|
func RespPassthroughUsage(payload []byte) *aiwire.Usage {
|
|
var root struct {
|
|
Usage *aiwire.RespUsage `json:"usage"`
|
|
}
|
|
if json.Unmarshal(payload, &root) != nil || root.Usage == nil {
|
|
return nil
|
|
}
|
|
usage := &aiwire.Usage{PromptTokens: root.Usage.InputTokens,
|
|
CompletionTokens: root.Usage.OutputTokens, TotalTokens: root.Usage.TotalTokens}
|
|
if cached := root.Usage.InputTokensDetails.CachedTokens; cached > 0 {
|
|
usage.PromptTokensDetails = &aiwire.PromptTokensDetails{CachedTokens: cached}
|
|
}
|
|
return usage
|
|
}
|
|
|
|
// RespStreamCompletedUsage 从一行 SSE data JSON 中提取 response.completed 事件的
|
|
// usage;非 completed 事件或解析失败返回 nil。流式直通逐行喂入,最后一次非 nil 生效。
|
|
func RespStreamCompletedUsage(data []byte) *aiwire.Usage {
|
|
var ev struct {
|
|
Type string `json:"type"`
|
|
Response json.RawMessage `json:"response"`
|
|
}
|
|
if json.Unmarshal(data, &ev) != nil || ev.Type != "response.completed" || len(ev.Response) == 0 {
|
|
return nil
|
|
}
|
|
return RespPassthroughUsage(ev.Response)
|
|
}
|