Files
oci-portal/internal/service/airesponses.go
T
wangdefa c7cc5616ed
CI / test (push) Successful in 31s
Release / release (push) Successful in 48s
发布 0.3.0:恢复 Chat 接口并增强云端事件通知
2026-07-13 10:13:44 +08:00

107 lines
3.5 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 {
return nil
}
return usageFromResp(root.Usage)
}
// usageFromResp 换算 Responses usage 为 OpenAI 口径(缓存命中透传);nil 原样返回。
func usageFromResp(u *aiwire.RespUsage) *aiwire.Usage {
if u == nil {
return nil
}
usage := &aiwire.Usage{PromptTokens: u.InputTokens,
CompletionTokens: u.OutputTokens, TotalTokens: u.TotalTokens}
if cached := u.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)
}