mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-09 08:58:23 +00:00
146 lines
4.9 KiB
Go
146 lines
4.9 KiB
Go
package service
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
type openAICompactFallbackSignal struct {
|
|
payload []byte
|
|
message string
|
|
}
|
|
|
|
func (e *openAICompactFallbackSignal) Error() string {
|
|
if e == nil || strings.TrimSpace(e.message) == "" {
|
|
return "upstream compact request failed"
|
|
}
|
|
return e.message
|
|
}
|
|
|
|
func asOpenAICompactFallbackSignal(err error) (*openAICompactFallbackSignal, bool) {
|
|
var signal *openAICompactFallbackSignal
|
|
return signal, errors.As(err, &signal) && signal != nil
|
|
}
|
|
|
|
func isExplicitOpenAICompactContext(c *gin.Context) bool {
|
|
return isOpenAIResponsesCompactPath(c) || isOpenAINativeCompactionV2(c)
|
|
}
|
|
|
|
func newOpenAICompactFallbackSignal(c *gin.Context, payload []byte, message string) error {
|
|
if !isExplicitOpenAICompactContext(c) ||
|
|
!isOpenAICompactModelFailure(http.StatusBadRequest, message, payload) {
|
|
return nil
|
|
}
|
|
return &openAICompactFallbackSignal{
|
|
payload: append([]byte(nil), payload...),
|
|
message: sanitizeUpstreamErrorMessage(strings.TrimSpace(message)),
|
|
}
|
|
}
|
|
|
|
func isExplicitOpenAICompactRequest(c *gin.Context, body []byte) bool {
|
|
return isOpenAIResponsesCompactPath(c) || HasCompactionTriggerInInput(body)
|
|
}
|
|
|
|
// resolveOpenAICompactFallbackModel prefers the account's compact-only rule
|
|
// for the client-visible model. The process-wide fallback is used only when
|
|
// that account has no matching compact rule.
|
|
func (s *OpenAIGatewayService) resolveOpenAICompactFallbackModel(account *Account, requestedModel string) string {
|
|
requestedModel = strings.TrimSpace(requestedModel)
|
|
if account != nil {
|
|
if mapped, matched := account.ResolveCompactMappedModel(requestedModel); matched {
|
|
if mapped = strings.TrimSpace(mapped); mapped != "" {
|
|
return mapped
|
|
}
|
|
}
|
|
}
|
|
if s == nil || s.cfg == nil {
|
|
return ""
|
|
}
|
|
fallback := strings.TrimSpace(s.cfg.Gateway.OpenAICompactModel)
|
|
if fallback == "" {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(resolveOpenAIAccountUpstreamModelForRequest(account, fallback, false))
|
|
}
|
|
|
|
func isOpenAICompactModelFailure(statusCode int, upstreamMsg string, upstreamBody []byte) bool {
|
|
if isOpenAIContextWindowError(upstreamMsg, upstreamBody) {
|
|
return true
|
|
}
|
|
if statusCode != http.StatusBadRequest && statusCode != http.StatusNotFound {
|
|
return false
|
|
}
|
|
|
|
values := []string{
|
|
extractUpstreamErrorCode(upstreamBody),
|
|
upstreamMsg,
|
|
gjson.GetBytes(upstreamBody, "error.type").String(),
|
|
gjson.GetBytes(upstreamBody, "response.error.code").String(),
|
|
gjson.GetBytes(upstreamBody, "response.error.type").String(),
|
|
}
|
|
for _, value := range values {
|
|
value = strings.ToLower(strings.TrimSpace(value))
|
|
switch value {
|
|
case "model_not_found", "model_not_available", "unsupported_model", "invalid_model":
|
|
return true
|
|
}
|
|
if strings.Contains(value, "model") && (strings.Contains(value, "not found") ||
|
|
strings.Contains(value, "does not exist") ||
|
|
strings.Contains(value, "unavailable") ||
|
|
strings.Contains(value, "unsupported") ||
|
|
strings.Contains(value, "not supported")) {
|
|
return true
|
|
}
|
|
}
|
|
// Some compact providers return only a failed response shell. It is safe to
|
|
// retry that shape for an explicit compact request, but a populated error is
|
|
// left untouched so business and policy failures keep their original wire.
|
|
if strings.EqualFold(strings.TrimSpace(gjson.GetBytes(upstreamBody, "response.status").String()), "failed") ||
|
|
strings.EqualFold(strings.TrimSpace(gjson.GetBytes(upstreamBody, "status").String()), "failed") {
|
|
for _, path := range []string{
|
|
"error.message", "error.code", "error.type",
|
|
"response.error.message", "response.error.code", "response.error.type",
|
|
} {
|
|
if strings.TrimSpace(gjson.GetBytes(upstreamBody, path).String()) != "" {
|
|
return false
|
|
}
|
|
}
|
|
return strings.TrimSpace(upstreamMsg) == ""
|
|
}
|
|
return false
|
|
}
|
|
|
|
// prepareOpenAICompactFallbackRetry returns a body for one safe, same-account
|
|
// retry. Callers invoke it only before any downstream response has been
|
|
// written; it changes the model and deliberately leaves path, trigger, and
|
|
// native-v2 context state untouched.
|
|
func (s *OpenAIGatewayService) prepareOpenAICompactFallbackRetry(
|
|
c *gin.Context,
|
|
account *Account,
|
|
requestedModel string,
|
|
currentBody []byte,
|
|
statusCode int,
|
|
upstreamMsg string,
|
|
upstreamBody []byte,
|
|
alreadyRetried bool,
|
|
) ([]byte, string, bool) {
|
|
if alreadyRetried || !isExplicitOpenAICompactRequest(c, currentBody) ||
|
|
!isOpenAICompactModelFailure(statusCode, upstreamMsg, upstreamBody) {
|
|
return currentBody, "", false
|
|
}
|
|
fallbackModel := s.resolveOpenAICompactFallbackModel(account, requestedModel)
|
|
currentModel := strings.TrimSpace(gjson.GetBytes(currentBody, "model").String())
|
|
if fallbackModel == "" || strings.EqualFold(fallbackModel, currentModel) {
|
|
return currentBody, "", false
|
|
}
|
|
retryBody := ReplaceModelInBody(currentBody, fallbackModel)
|
|
if strings.EqualFold(strings.TrimSpace(gjson.GetBytes(retryBody, "model").String()), currentModel) {
|
|
return currentBody, "", false
|
|
}
|
|
return retryBody, fallbackModel, true
|
|
}
|