| package cli |
|
|
| import ( |
| "bytes" |
| "context" |
| "encoding/json" |
| "io" |
| "log/slog" |
| "net/http" |
| "strconv" |
| "strings" |
|
|
| "github.com/chenyme/grok2api/backend/internal/domain/account" |
| "github.com/chenyme/grok2api/backend/internal/infra/config" |
| "github.com/chenyme/grok2api/backend/internal/infra/provider" |
| ) |
|
|
| |
| |
| type FallbackMarker interface { |
| MarkBuildAPIFallback(ctx context.Context, accountID uint64, enabled bool) error |
| } |
|
|
| |
| type VideoUploadIssuer interface { |
| |
| |
| IssueVideoUpload(ctx context.Context, jobID string) (uploadURL, assetID string, err error) |
| |
| WaitVideoUpload(ctx context.Context, assetID string) (contentType string, err error) |
| } |
|
|
| func (a *Adapter) SetFallbackMarker(marker FallbackMarker) { |
| a.cfgMu.Lock() |
| a.fallbackMarker = marker |
| a.cfgMu.Unlock() |
| } |
|
|
| func (a *Adapter) SetVideoUploadIssuer(issuer VideoUploadIssuer) { |
| a.cfgMu.Lock() |
| a.uploadIssuer = issuer |
| a.cfgMu.Unlock() |
| } |
|
|
| func (a *Adapter) fallbackMarkerRef() FallbackMarker { |
| a.cfgMu.RLock() |
| defer a.cfgMu.RUnlock() |
| return a.fallbackMarker |
| } |
|
|
| func (a *Adapter) uploadIssuerRef() VideoUploadIssuer { |
| a.cfgMu.RLock() |
| defer a.cfgMu.RUnlock() |
| return a.uploadIssuer |
| } |
|
|
| func (a *Adapter) primaryBaseURL() string { |
| return strings.TrimRight(strings.TrimSpace(a.config().BaseURL), "/") |
| } |
|
|
| func (a *Adapter) fallbackBaseURL() string { |
| return strings.TrimRight(config.NormalizeBuildFallbackBaseURL(a.config().FallbackBaseURL), "/") |
| } |
|
|
| |
| |
| |
| |
| |
| |
| func isXAIInferenceFallbackCapable(method, path string) bool { |
| method = strings.ToUpper(strings.TrimSpace(method)) |
| path = normalizeBuildAPIPath(path) |
| switch { |
| case method == http.MethodPost && path == "/responses": |
| |
| return true |
| case method == http.MethodPost && path == "/responses/compact": |
| return true |
| case method == http.MethodPost && path == "/videos/generations": |
| return true |
| case method == http.MethodGet && strings.HasPrefix(path, "/videos/") && path != "/videos" && path != "/videos/generations": |
| |
| return true |
| default: |
| |
| return false |
| } |
| } |
|
|
| func normalizeBuildAPIPath(path string) string { |
| path = strings.TrimSpace(path) |
| if path == "" { |
| return "/" |
| } |
| if !strings.HasPrefix(path, "/") { |
| path = "/" + path |
| } |
| if i := strings.IndexByte(path, '?'); i >= 0 { |
| path = path[:i] |
| } |
| if len(path) > 1 { |
| path = strings.TrimRight(path, "/") |
| } |
| return path |
| } |
|
|
| func normalizedBuildRouteMode(credential account.Credential) account.BuildRouteMode { |
| if credential.Provider == account.ProviderBuild && credential.BuildRouteMode.IsValid() { |
| return credential.BuildRouteMode |
| } |
| return account.BuildRouteAuto |
| } |
|
|
| |
| |
| func (a *Adapter) inferenceBaseForOperation(credential account.Credential, billing *account.Billing, method, path string) string { |
| if !isXAIInferenceFallbackCapable(method, path) { |
| return a.primaryBaseURL() |
| } |
| switch normalizedBuildRouteMode(credential) { |
| case account.BuildRouteBuild: |
| return a.primaryBaseURL() |
| case account.BuildRouteXAI: |
| return a.fallbackBaseURL() |
| } |
| if !account.IsBuildSuper(credential, billing) { |
| return a.primaryBaseURL() |
| } |
| if a.CredentialMetadata(credential).BuildBotFlagged { |
| return a.fallbackBaseURL() |
| } |
| return a.primaryBaseURL() |
| } |
|
|
| |
| |
| func shouldProbeXAIInferenceFallback(credential account.Credential, billing *account.Billing, method, path string, primaryStatus int) bool { |
| return account.IsBuildSuper(credential, billing) && normalizedBuildRouteMode(credential) == account.BuildRouteAuto && isHTTPForbidden(primaryStatus) && isXAIInferenceFallbackCapable(method, path) |
| } |
|
|
| func (a *Adapter) urlWithBase(base, path string) string { |
| return strings.TrimRight(base, "/") + "/" + strings.TrimLeft(path, "/") |
| } |
|
|
| |
| |
| func (a *Adapter) activateBuildAPIFallback(ctx context.Context, credential *account.Credential) { |
| if credential == nil || credential.ID == 0 || credential.BuildAPIFallback { |
| return |
| } |
| credential.BuildAPIFallback = true |
| marker := a.fallbackMarkerRef() |
| if marker == nil { |
| slog.Error("build_api_fallback_mark_skipped", "account_id", credential.ID, "reason", "marker_unavailable") |
| return |
| } |
| if err := marker.MarkBuildAPIFallback(ctx, credential.ID, true); err != nil { |
| |
| slog.Error("build_api_fallback_mark_failed", "account_id", credential.ID, "error", err.Error()) |
| } |
| } |
|
|
| func isHTTPForbidden(status int) bool { |
| return status == http.StatusForbidden |
| } |
|
|
| func isDefinitiveAccountBlockBody(body []byte) bool { |
| var payload map[string]any |
| if json.Unmarshal(body, &payload) != nil { |
| return false |
| } |
| code := fallbackStringField(payload, "code") |
| message := firstNonEmpty(fallbackStringField(payload, "error"), fallbackStringField(payload, "message")) |
| if nested, ok := payload["error"].(map[string]any); ok { |
| code = firstNonEmpty(fallbackStringField(nested, "code"), code) |
| message = firstNonEmpty(fallbackStringField(nested, "message"), message) |
| } |
| code = strings.ToLower(strings.TrimSpace(code)) |
| message = strings.ToLower(strings.Trim(strings.TrimSpace(message), " .!\t\r\n")) |
| return strings.Contains(code, "blocked-user") || message == "user is blocked" |
| } |
|
|
| |
| |
| func isSafetyRejectionBody(body []byte) bool { |
| lower := strings.ToLower(string(body)) |
| return strings.Contains(lower, "content violates usage guidelines") || |
| strings.Contains(lower, "safety_check_type_") |
| } |
|
|
| |
| func shouldSkipXAIFallback(body []byte) bool { |
| return isDefinitiveAccountBlockBody(body) || isSafetyRejectionBody(body) |
| } |
|
|
| func fallbackStringField(values map[string]any, key string) string { |
| value, _ := values[key].(string) |
| return value |
| } |
|
|
| func bufferedFailureDiagnostic(response *http.Response, body []byte, truncated bool) *provider.DiagnosticResponse { |
| if response == nil { |
| return &provider.DiagnosticResponse{StatusCode: http.StatusForbidden, Status: "403 Forbidden", Header: make(http.Header), Body: append([]byte(nil), body...), BodyTruncated: truncated} |
| } |
| return &provider.DiagnosticResponse{ |
| StatusCode: response.StatusCode, Status: response.Status, Header: response.Header.Clone(), |
| Body: append([]byte(nil), body...), BodyTruncated: truncated, |
| } |
| } |
|
|
| func isHTTPSuccess(status int) bool { |
| return status >= 200 && status < 300 |
| } |
|
|
| |
| func cloneBufferedResponse(source *http.Response, body []byte, truncated bool) *http.Response { |
| if source == nil { |
| return &http.Response{ |
| StatusCode: http.StatusForbidden, |
| Status: "403 Forbidden", |
| Header: make(http.Header), |
| Body: io.NopCloser(bytes.NewReader(body)), |
| ContentLength: int64(len(body)), |
| } |
| } |
| header := source.Header.Clone() |
| if header == nil { |
| header = make(http.Header) |
| } |
| if truncated { |
| header.Set("X-Grok2API-Body-Truncated", "1") |
| } |
| header.Set("Content-Length", strconv.Itoa(len(body))) |
| return &http.Response{ |
| StatusCode: source.StatusCode, |
| Status: source.Status, |
| Proto: source.Proto, |
| ProtoMajor: source.ProtoMajor, |
| ProtoMinor: source.ProtoMinor, |
| Header: header, |
| Body: io.NopCloser(bytes.NewReader(body)), |
| ContentLength: int64(len(body)), |
| TransferEncoding: append([]string(nil), source.TransferEncoding...), |
| Uncompressed: source.Uncompressed, |
| Trailer: source.Trailer.Clone(), |
| Request: source.Request, |
| TLS: source.TLS, |
| } |
| } |
|
|