| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670 |
- package service
- import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "net/http"
- "strings"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
- "github.com/Wei-Shaw/sub2api/internal/pkg/xai"
- "github.com/gin-gonic/gin"
- "github.com/tidwall/gjson"
- )
- const (
- grokChatResponsesEndpoint = "/v1/responses"
- grokChatRawEndpoint = "/v1/chat/completions"
- )
- var grokChatResponsesBridgeTopLevelFields = map[string]struct{}{
- "model": {},
- "messages": {},
- "instructions": {},
- "stream": {},
- "stream_options": {},
- "max_tokens": {},
- "max_completion_tokens": {},
- "temperature": {},
- "top_p": {},
- "stop": {},
- "reasoning_effort": {},
- "prompt_cache_key": {},
- "tools": {},
- "tool_choice": {},
- "functions": {},
- "function_call": {},
- "parallel_tool_calls": {},
- "response_format": {},
- "service_tier": {},
- }
- // grokChatResponsesBridgeEligibility deliberately accepts only request shapes
- // whose Chat Completions semantics are preserved by the Responses bridge.
- // Everything else stays on raw Chat Completions rather than being silently
- // dropped or rewritten.
- func grokChatResponsesBridgeEligibility(body []byte) (bool, string) {
- var root map[string]json.RawMessage
- if err := json.Unmarshal(body, &root); err != nil || root == nil {
- return false, "invalid_json"
- }
- // These fields have no effect when explicitly set to JSON null. Accepting
- // that common SDK representation keeps the request on the bridge path,
- // while non-null values remain unsupported because the Responses converter
- // cannot preserve their Chat Completions semantics.
- for _, field := range []string{"stop", "reasoning_effort"} {
- if raw, exists := root[field]; exists && !grokChatJSONNull(raw) {
- return false, "unsupported_" + field
- }
- }
- if raw, exists := root["instructions"]; exists {
- var instructions string
- if !grokChatJSONNull(raw) && json.Unmarshal(raw, &instructions) != nil {
- return false, "invalid_instructions"
- }
- }
- if raw, exists := root["response_format"]; exists {
- var responseFormat map[string]json.RawMessage
- if !grokChatJSONNull(raw) && (json.Unmarshal(raw, &responseFormat) != nil || responseFormat == nil) {
- return false, "invalid_response_format"
- }
- }
- if raw, exists := root["service_tier"]; exists {
- var serviceTier string
- if !grokChatJSONNull(raw) && json.Unmarshal(raw, &serviceTier) != nil {
- return false, "invalid_service_tier"
- }
- }
- if raw, exists := root["tools"]; exists {
- if ok, reason := grokChatFunctionDeclarationsBridgeable(raw); !ok {
- return false, reason
- }
- }
- if raw, exists := root["functions"]; exists && !grokChatNullOrEmptyArray(raw) {
- return false, "unsupported_functions"
- }
- if raw, exists := root["tool_choice"]; exists {
- if ok, reason := grokChatToolChoiceBridgeable(raw); !ok {
- return false, reason
- }
- var choice string
- if json.Unmarshal(raw, &choice) == nil && choice == "required" && !grokChatHasFunctionDeclarations(root) {
- return false, "required_tool_choice_without_tools"
- }
- }
- if raw, exists := root["function_call"]; exists && !grokChatNullOrNone(raw) {
- return false, "unsupported_function_call"
- }
- for field := range root {
- if _, supported := grokChatResponsesBridgeTopLevelFields[field]; !supported {
- return false, "unknown_field_" + field
- }
- }
- var model string
- if raw, ok := root["model"]; !ok || json.Unmarshal(raw, &model) != nil || strings.TrimSpace(model) == "" {
- return false, "invalid_model"
- }
- if raw, ok := root["stream"]; ok {
- var stream *bool
- if json.Unmarshal(raw, &stream) != nil || stream == nil {
- return false, "invalid_stream"
- }
- }
- if raw, ok := root["parallel_tool_calls"]; ok {
- var parallelToolCalls *bool
- if json.Unmarshal(raw, ¶llelToolCalls) != nil || parallelToolCalls == nil {
- return false, "invalid_parallel_tool_calls"
- }
- }
- if raw, ok := root["stream_options"]; ok {
- var options map[string]json.RawMessage
- if json.Unmarshal(raw, &options) != nil || options == nil {
- return false, "invalid_stream_options"
- }
- for field, value := range options {
- if field != "include_usage" {
- return false, "unknown_stream_option_" + field
- }
- var includeUsage *bool
- if json.Unmarshal(value, &includeUsage) != nil || includeUsage == nil {
- return false, "invalid_stream_include_usage"
- }
- }
- }
- for _, field := range []string{"max_tokens", "max_completion_tokens"} {
- if raw, ok := root[field]; ok {
- var value *int
- if json.Unmarshal(raw, &value) != nil || value == nil || *value < 128 {
- return false, "unsafe_" + field
- }
- }
- }
- if _, hasMaxTokens := root["max_tokens"]; hasMaxTokens {
- if _, hasMaxCompletionTokens := root["max_completion_tokens"]; hasMaxCompletionTokens {
- return false, "conflicting_max_tokens"
- }
- }
- for _, field := range []string{"temperature", "top_p"} {
- if raw, ok := root[field]; ok {
- var value *float64
- if json.Unmarshal(raw, &value) != nil || value == nil {
- return false, "invalid_" + field
- }
- }
- }
- if raw, ok := root["prompt_cache_key"]; ok {
- var key string
- if json.Unmarshal(raw, &key) != nil {
- return false, "invalid_prompt_cache_key"
- }
- }
- var messages []map[string]json.RawMessage
- rawMessages, ok := root["messages"]
- if !ok || json.Unmarshal(rawMessages, &messages) != nil || len(messages) == 0 {
- return false, "invalid_messages"
- }
- for _, message := range messages {
- var role string
- if raw, exists := message["role"]; !exists || json.Unmarshal(raw, &role) != nil {
- return false, "invalid_message_role"
- }
- switch role {
- case "system", "user":
- if ok, reason := grokChatMessageFieldsBridgeable(message, "role", "content"); !ok {
- return false, reason
- }
- raw, exists := message["content"]
- if !exists {
- return false, "non_text_message_content"
- }
- if ok, reason := grokChatRequiredMessageContentBridgeable(raw); !ok {
- return false, reason
- }
- case "assistant":
- if ok, reason := grokChatMessageFieldsBridgeable(message, "role", "content", "reasoning_content", "tool_calls"); !ok {
- return false, reason
- }
- reasoningContent := ""
- if raw, exists := message["reasoning_content"]; exists {
- if !grokChatJSONNull(raw) && json.Unmarshal(raw, &reasoningContent) != nil {
- return false, "invalid_reasoning_content"
- }
- }
- hasReasoningContent := strings.TrimSpace(reasoningContent) != ""
- toolCallCount := 0
- if raw, exists := message["tool_calls"]; exists {
- var reason string
- toolCallCount, reason = grokChatAssistantToolCallsBridgeable(raw)
- if reason != "" {
- return false, reason
- }
- }
- raw, hasContent := message["content"]
- if !hasContent || strings.TrimSpace(string(raw)) == "null" {
- if toolCallCount == 0 && !hasReasoningContent {
- return false, "non_text_message_content"
- }
- continue
- }
- var content string
- if json.Unmarshal(raw, &content) == nil {
- if strings.TrimSpace(content) == "" && toolCallCount == 0 && !hasReasoningContent {
- return false, "empty_message_content"
- }
- continue
- }
- if ok, reason := grokChatStructuredContentBridgeable(raw); !ok {
- // The converter can still emit a standalone reasoning part when
- // an otherwise empty content array accompanies reasoning_content.
- // Do not broaden this exception to unsupported/malformed parts.
- if !hasReasoningContent || reason != "empty_message_content" {
- return false, reason
- }
- }
- case "tool":
- if ok, reason := grokChatMessageFieldsBridgeable(message, "role", "content", "tool_call_id"); !ok {
- return false, reason
- }
- var callID string
- if raw, exists := message["tool_call_id"]; !exists || json.Unmarshal(raw, &callID) != nil || strings.TrimSpace(callID) == "" {
- return false, "invalid_tool_call_id"
- }
- var output string
- if raw, exists := message["content"]; !exists || json.Unmarshal(raw, &output) != nil || output == "" {
- return false, "invalid_tool_message_content"
- }
- default:
- return false, "unsupported_message_role_" + role
- }
- }
- return true, ""
- }
- func grokChatFunctionDeclarationsBridgeable(raw json.RawMessage) (bool, string) {
- if strings.TrimSpace(string(raw)) == "null" {
- return true, ""
- }
- var declarations []json.RawMessage
- if json.Unmarshal(raw, &declarations) != nil {
- return false, "invalid_tools"
- }
- for _, declaration := range declarations {
- var tool map[string]json.RawMessage
- if json.Unmarshal(declaration, &tool) != nil || tool == nil {
- return false, "invalid_tool"
- }
- for field := range tool {
- if field != "type" && field != "function" {
- return false, "unsafe_tool_field_" + field
- }
- }
- var toolType string
- if rawType, exists := tool["type"]; !exists || json.Unmarshal(rawType, &toolType) != nil || toolType != "function" {
- return false, "unsupported_tool_type"
- }
- functionRaw, exists := tool["function"]
- if !exists {
- return false, "invalid_tool_function"
- }
- var function map[string]json.RawMessage
- if json.Unmarshal(functionRaw, &function) != nil || function == nil {
- return false, "invalid_tool_function"
- }
- for field := range function {
- switch field {
- case "name", "description", "parameters", "strict":
- default:
- return false, "unsafe_tool_function_field_" + field
- }
- }
- var name string
- if rawName, exists := function["name"]; !exists || json.Unmarshal(rawName, &name) != nil || strings.TrimSpace(name) == "" {
- return false, "invalid_tool_function_name"
- }
- if rawDescription, exists := function["description"]; exists {
- var description string
- if json.Unmarshal(rawDescription, &description) != nil {
- return false, "invalid_tool_function_description"
- }
- }
- var parameters map[string]json.RawMessage
- if rawParameters, exists := function["parameters"]; !exists || json.Unmarshal(rawParameters, ¶meters) != nil || parameters == nil {
- return false, "invalid_tool_function_parameters"
- }
- if rawStrict, exists := function["strict"]; exists {
- var strict bool
- if json.Unmarshal(rawStrict, &strict) != nil {
- return false, "invalid_tool_function_strict"
- }
- }
- }
- return true, ""
- }
- func grokChatToolChoiceBridgeable(raw json.RawMessage) (bool, string) {
- if strings.TrimSpace(string(raw)) == "null" {
- return true, ""
- }
- var choice string
- if json.Unmarshal(raw, &choice) != nil {
- return false, "unsupported_tool_choice"
- }
- switch choice {
- case "auto", "none", "required":
- return true, ""
- default:
- return false, "unsupported_tool_choice"
- }
- }
- func grokChatHasFunctionDeclarations(root map[string]json.RawMessage) bool {
- for _, field := range []string{"tools", "functions"} {
- raw, exists := root[field]
- if !exists {
- continue
- }
- var declarations []json.RawMessage
- if json.Unmarshal(raw, &declarations) == nil && len(declarations) > 0 {
- return true
- }
- }
- return false
- }
- func grokChatMessageFieldsBridgeable(message map[string]json.RawMessage, allowedFields ...string) (bool, string) {
- allowed := make(map[string]struct{}, len(allowedFields))
- for _, field := range allowedFields {
- allowed[field] = struct{}{}
- }
- for field := range message {
- if _, ok := allowed[field]; !ok {
- return false, "unsafe_message_field_" + field
- }
- }
- return true, ""
- }
- func grokChatRequiredMessageContentBridgeable(raw json.RawMessage) (bool, string) {
- var content string
- if json.Unmarshal(raw, &content) == nil {
- if strings.TrimSpace(content) == "" {
- return false, "empty_message_content"
- }
- return true, ""
- }
- // Structured content: only allow arrays whose parts are text or
- // image_url. These are losslessly convertible to Responses input_text/
- // input_image parts, so the bridge preserves Chat Completions semantics.
- return grokChatStructuredContentBridgeable(raw)
- }
- func grokChatAssistantToolCallsBridgeable(raw json.RawMessage) (int, string) {
- if strings.TrimSpace(string(raw)) == "null" {
- return 0, ""
- }
- var calls []map[string]json.RawMessage
- if json.Unmarshal(raw, &calls) != nil {
- return 0, "invalid_tool_calls"
- }
- for _, call := range calls {
- if call == nil {
- return 0, "invalid_tool_call"
- }
- for field := range call {
- switch field {
- case "id", "type", "function", "index":
- default:
- return 0, "unsafe_tool_call_field_" + field
- }
- }
- if rawIndex, exists := call["index"]; exists {
- var index *int
- if json.Unmarshal(rawIndex, &index) != nil || (index != nil && *index < 0) {
- return 0, "invalid_tool_call_index"
- }
- }
- var callID string
- if rawID, exists := call["id"]; !exists || json.Unmarshal(rawID, &callID) != nil || strings.TrimSpace(callID) == "" {
- return 0, "invalid_tool_call_id"
- }
- var callType string
- if rawType, exists := call["type"]; !exists || json.Unmarshal(rawType, &callType) != nil || callType != "function" {
- return 0, "unsupported_tool_call_type"
- }
- var function map[string]json.RawMessage
- if rawFunction, exists := call["function"]; !exists || json.Unmarshal(rawFunction, &function) != nil || function == nil {
- return 0, "invalid_tool_call_function"
- }
- for field := range function {
- if field != "name" && field != "arguments" {
- return 0, "unsafe_tool_call_function_field_" + field
- }
- }
- var name string
- if rawName, exists := function["name"]; !exists || json.Unmarshal(rawName, &name) != nil || strings.TrimSpace(name) == "" {
- return 0, "invalid_tool_call_function_name"
- }
- var arguments string
- if rawArguments, exists := function["arguments"]; !exists || json.Unmarshal(rawArguments, &arguments) != nil || !json.Valid([]byte(arguments)) {
- return 0, "invalid_tool_call_arguments"
- }
- }
- return len(calls), ""
- }
- func grokChatStructuredContentBridgeable(raw json.RawMessage) (bool, string) {
- var parts []map[string]json.RawMessage
- if err := json.Unmarshal(raw, &parts); err != nil {
- return false, "non_text_message_content"
- }
- if len(parts) == 0 {
- return false, "empty_message_content"
- }
- hasContent := false
- for _, part := range parts {
- var partType string
- rawType, ok := part["type"]
- if !ok || json.Unmarshal(rawType, &partType) != nil {
- return false, "non_text_message_content"
- }
- switch strings.TrimSpace(partType) {
- case "text":
- var text string
- if raw, ok := part["text"]; ok && json.Unmarshal(raw, &text) == nil {
- if strings.TrimSpace(text) != "" {
- hasContent = true
- }
- }
- case "image_url", "input_image":
- hasContent = true
- default:
- return false, "unsupported_content_part_" + strings.TrimSpace(partType)
- }
- }
- if !hasContent {
- return false, "empty_message_content"
- }
- return true, ""
- }
- func grokChatNullOrNone(raw json.RawMessage) bool {
- if strings.TrimSpace(string(raw)) == "null" {
- return true
- }
- var value string
- return json.Unmarshal(raw, &value) == nil && strings.EqualFold(strings.TrimSpace(value), "none")
- }
- func grokChatJSONNull(raw json.RawMessage) bool {
- return strings.TrimSpace(string(raw)) == "null"
- }
- func grokChatNullOrEmptyArray(raw json.RawMessage) bool {
- if strings.TrimSpace(string(raw)) == "null" {
- return true
- }
- var values []json.RawMessage
- return json.Unmarshal(raw, &values) == nil && len(values) == 0
- }
- func grokChatResponsesCacheIntentBody(body []byte) ([]byte, error) {
- // An empty Chat tools array is omitted by the Responses converter. In that
- // case auto/none is also a semantic no-op and must not suppress the normal
- // tool-free cache route. Non-empty converted tools are always kept intact.
- if gjson.GetBytes(body, "tools").Exists() {
- return append([]byte(nil), body...), nil
- }
- choice := gjson.GetBytes(body, "tool_choice")
- if !choice.Exists() || choice.Type != gjson.String || (choice.String() != "auto" && choice.String() != "none") {
- return append([]byte(nil), body...), nil
- }
- var root map[string]json.RawMessage
- if err := json.Unmarshal(body, &root); err != nil {
- return nil, err
- }
- delete(root, "tool_choice")
- return json.Marshal(root)
- }
- func grokChatResponsesBridgeModel(model string) bool {
- switch strings.ToLower(xai.StripGrokProviderPrefix(strings.TrimSpace(model))) {
- case "grok-4.5", "grok-4.6", "grok-4.6-latest":
- return true
- default:
- return false
- }
- }
- func grokChatResponsesRuntimeEligible(upstreamModel, cacheIdentity string) bool {
- return grokChatResponsesBridgeModel(upstreamModel) && strings.TrimSpace(cacheIdentity) != ""
- }
- // forwardGrokChatCompletionsViaResponses converts a strictly compatible Chat
- // request into xAI Responses format and reuses the established Responses-to-
- // Chat response translators. It intentionally does not run the Codex OAuth
- // transform because Grok CLI is a separate upstream protocol.
- func (s *OpenAIGatewayService) forwardGrokChatCompletionsViaResponses(
- ctx context.Context,
- c *gin.Context,
- account *Account,
- body []byte,
- promptCacheKey string,
- defaultMappedModel string,
- ) (*OpenAIForwardResult, error) {
- startTime := time.Now()
- var chatReq apicompat.ChatCompletionsRequest
- if err := json.Unmarshal(body, &chatReq); err != nil {
- return nil, fmt.Errorf("parse grok chat completions request: %w", err)
- }
- originalModel := chatReq.Model
- clientStream := chatReq.Stream
- billingModel := resolveOpenAIForwardModel(account, originalModel, defaultMappedModel)
- upstreamModel := normalizeOpenAIModelForUpstream(account, billingModel)
- cacheIdentity := resolveGrokCacheIdentity(c, body, promptCacheKey, upstreamModel)
- // Image inputs must go through the Responses bridge: the raw Chat
- // Completions path cannot forward image_url parts to Grok's native vision
- // for non-composer models, so they would be silently dropped. Route them to
- // Responses even when no prompt-cache identity is available.
- hasImageInput := openAIJSONValueMayContainImageInput(gjson.GetBytes(body, "messages"))
- if !grokChatResponsesRuntimeEligible(upstreamModel, cacheIdentity) && (!hasImageInput || !grokChatResponsesBridgeModel(upstreamModel)) {
- return s.forwardAsRawChatCompletions(ctx, c, account, body, defaultMappedModel)
- }
- responsesReq, err := apicompat.ChatCompletionsToResponses(&chatReq)
- if err != nil {
- return nil, fmt.Errorf("convert grok chat completions to responses: %w", err)
- }
- responsesReq.Model = upstreamModel
- responsesReq.Stream = true
- // Keep Chat and native Responses paths aligned for OpenAI-compatible
- // service_tier aliases (for example, "fast" -> "priority"). Unknown
- // values are omitted by the shared normalizer instead of reaching xAI.
- normalizeResponsesRequestServiceTier(responsesReq)
- // These fields are useful to Codex but are not needed by the Grok CLI
- // protocol. Keep the bridge request as close as possible to native Grok.
- responsesReq.Include = nil
- responsesReq.Store = nil
- responsesBody, err := json.Marshal(responsesReq)
- if err != nil {
- return nil, fmt.Errorf("marshal grok responses bridge request: %w", err)
- }
- // Preserve the converted Responses intent before Grok capability
- // sanitization. Cache routing must see the actual client function tools,
- // not the nested Chat Completions declarations and not a tool-free copy.
- intentBody, err := grokChatResponsesCacheIntentBody(responsesBody)
- if err != nil {
- return nil, fmt.Errorf("normalize grok responses bridge cache intent: %w", err)
- }
- responsesBody, err = patchGrokResponsesBody(responsesBody, upstreamModel)
- if err != nil {
- return nil, fmt.Errorf("patch grok responses bridge request: %w", err)
- }
- responsesBody, err = applyGrokResponsesCacheIdentity(responsesBody, intentBody, cacheIdentity, true)
- if err != nil {
- return nil, fmt.Errorf("apply grok responses bridge cache identity: %w", err)
- }
- responsesBody, err = applyGrokFreeRequestToolCacheRoute(c, responsesBody, intentBody, account, cacheIdentity)
- if err != nil {
- return nil, fmt.Errorf("apply grok responses bridge function-tool cache route: %w", err)
- }
- updatedBody, policyErr := s.applyOpenAIFastPolicyToBody(ctx, account, upstreamModel, responsesBody)
- if policyErr != nil {
- var blocked *OpenAIFastBlockedError
- if errors.As(policyErr, &blocked) {
- MarkOpsClientBusinessLimited(c, OpsClientBusinessLimitedReasonLocalPolicyDenied)
- writeChatCompletionsError(c, http.StatusForbidden, "permission_error", blocked.Message)
- }
- return nil, policyErr
- }
- responsesBody = updatedBody
- token, _, err := s.getRequestCredential(ctx, c, account)
- if err != nil {
- return nil, fmt.Errorf("get grok access token: %w", err)
- }
- upstreamCtx, releaseUpstreamCtx := detachUpstreamContext(ctx)
- upstreamReq, err := buildGrokResponsesRequest(upstreamCtx, c, account, responsesBody, token, cacheIdentity, s.cfg, s.settingService)
- releaseUpstreamCtx()
- if err != nil {
- return nil, fmt.Errorf("build grok responses bridge request: %w", err)
- }
- SetActualOpenAIUpstreamEndpoint(c, grokChatResponsesEndpoint)
- proxyURL := ""
- if account.ProxyID != nil && account.Proxy != nil {
- proxyURL = account.Proxy.URL()
- }
- resp, err := s.doOpenAIUpstream(upstreamReq, proxyURL, account)
- if err != nil {
- return nil, s.handleOpenAIUpstreamTransportError(ctx, c, account, err, false)
- }
- defer func() { _ = resp.Body.Close() }()
- if resp.StatusCode >= http.StatusBadRequest {
- respBody, upstreamMsg := s.readOpenAIUpstreamError(resp)
- if upstreamMsg == "" {
- upstreamMsg = fmt.Sprintf("xAI upstream returned status %d", resp.StatusCode)
- }
- kind := "http_error"
- if s.shouldFailoverGrokUpstreamError(resp.StatusCode, respBody) {
- kind = "failover"
- }
- appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
- ProxyID: opsUpstreamProxyID(account),
- ProxyName: opsUpstreamProxyName(account),
- Platform: account.Platform,
- AccountID: account.ID,
- AccountName: account.Name,
- UpstreamStatusCode: resp.StatusCode,
- UpstreamRequestID: firstNonEmpty(resp.Header.Get("x-request-id"), resp.Header.Get("xai-request-id")),
- Kind: kind,
- Message: upstreamMsg,
- })
- s.handleGrokAccountUpstreamError(withGrokTeamRateLimitModel(ctx, upstreamModel), account, resp.StatusCode, resp.Header, respBody)
- if s.shouldFailoverGrokUpstreamError(resp.StatusCode, respBody) {
- retryable, retryDelay, retryDeadline, retryMax := grokSameAccountRetryMetadata(account, resp.StatusCode, respBody)
- return nil, &UpstreamFailoverError{
- StatusCode: resp.StatusCode,
- ResponseBody: respBody,
- ResponseHeaders: resp.Header.Clone(),
- RetryableOnSameAccount: retryable,
- RequestScopedTransient: retryable && resp.StatusCode == http.StatusTooManyRequests,
- SameAccountRetryDelay: retryDelay,
- SameAccountRetryDeadline: retryDeadline,
- SameAccountRetryMax: retryMax,
- }
- }
- return s.handleChatCompletionsErrorResponse(resp, c, account, billingModel)
- }
- s.updateGrokUsageFromResponse(withGrokTeamRateLimitModel(ctx, upstreamModel), account, resp.Header, resp.StatusCode)
- var result *OpenAIForwardResult
- if clientStream {
- result, err = s.handleChatStreamingResponse(resp, c, account, originalModel, billingModel, upstreamModel, startTime, len(body))
- } else {
- result, err = s.handleChatBufferedStreamingResponse(resp, c, account, originalModel, billingModel, upstreamModel, startTime)
- }
- if result != nil {
- result.UpstreamEndpoint = grokChatResponsesEndpoint
- result.ResponseHeaders = resp.Header.Clone()
- if result.RequestID == "" {
- result.RequestID = firstNonEmpty(resp.Header.Get("x-request-id"), resp.Header.Get("xai-request-id"))
- }
- result.ReasoningEffort = extractOpenAIReasoningEffortFromBody(body, upstreamModel, billingModel, originalModel)
- }
- return result, err
- }
|