openai_gateway_grok_chat_bridge.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. package service
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "net/http"
  8. "strings"
  9. "time"
  10. "github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
  11. "github.com/Wei-Shaw/sub2api/internal/pkg/xai"
  12. "github.com/gin-gonic/gin"
  13. "github.com/tidwall/gjson"
  14. )
  15. const (
  16. grokChatResponsesEndpoint = "/v1/responses"
  17. grokChatRawEndpoint = "/v1/chat/completions"
  18. )
  19. var grokChatResponsesBridgeTopLevelFields = map[string]struct{}{
  20. "model": {},
  21. "messages": {},
  22. "instructions": {},
  23. "stream": {},
  24. "stream_options": {},
  25. "max_tokens": {},
  26. "max_completion_tokens": {},
  27. "temperature": {},
  28. "top_p": {},
  29. "stop": {},
  30. "reasoning_effort": {},
  31. "prompt_cache_key": {},
  32. "tools": {},
  33. "tool_choice": {},
  34. "functions": {},
  35. "function_call": {},
  36. "parallel_tool_calls": {},
  37. "response_format": {},
  38. "service_tier": {},
  39. }
  40. // grokChatResponsesBridgeEligibility deliberately accepts only request shapes
  41. // whose Chat Completions semantics are preserved by the Responses bridge.
  42. // Everything else stays on raw Chat Completions rather than being silently
  43. // dropped or rewritten.
  44. func grokChatResponsesBridgeEligibility(body []byte) (bool, string) {
  45. var root map[string]json.RawMessage
  46. if err := json.Unmarshal(body, &root); err != nil || root == nil {
  47. return false, "invalid_json"
  48. }
  49. // These fields have no effect when explicitly set to JSON null. Accepting
  50. // that common SDK representation keeps the request on the bridge path,
  51. // while non-null values remain unsupported because the Responses converter
  52. // cannot preserve their Chat Completions semantics.
  53. for _, field := range []string{"stop", "reasoning_effort"} {
  54. if raw, exists := root[field]; exists && !grokChatJSONNull(raw) {
  55. return false, "unsupported_" + field
  56. }
  57. }
  58. if raw, exists := root["instructions"]; exists {
  59. var instructions string
  60. if !grokChatJSONNull(raw) && json.Unmarshal(raw, &instructions) != nil {
  61. return false, "invalid_instructions"
  62. }
  63. }
  64. if raw, exists := root["response_format"]; exists {
  65. var responseFormat map[string]json.RawMessage
  66. if !grokChatJSONNull(raw) && (json.Unmarshal(raw, &responseFormat) != nil || responseFormat == nil) {
  67. return false, "invalid_response_format"
  68. }
  69. }
  70. if raw, exists := root["service_tier"]; exists {
  71. var serviceTier string
  72. if !grokChatJSONNull(raw) && json.Unmarshal(raw, &serviceTier) != nil {
  73. return false, "invalid_service_tier"
  74. }
  75. }
  76. if raw, exists := root["tools"]; exists {
  77. if ok, reason := grokChatFunctionDeclarationsBridgeable(raw); !ok {
  78. return false, reason
  79. }
  80. }
  81. if raw, exists := root["functions"]; exists && !grokChatNullOrEmptyArray(raw) {
  82. return false, "unsupported_functions"
  83. }
  84. if raw, exists := root["tool_choice"]; exists {
  85. if ok, reason := grokChatToolChoiceBridgeable(raw); !ok {
  86. return false, reason
  87. }
  88. var choice string
  89. if json.Unmarshal(raw, &choice) == nil && choice == "required" && !grokChatHasFunctionDeclarations(root) {
  90. return false, "required_tool_choice_without_tools"
  91. }
  92. }
  93. if raw, exists := root["function_call"]; exists && !grokChatNullOrNone(raw) {
  94. return false, "unsupported_function_call"
  95. }
  96. for field := range root {
  97. if _, supported := grokChatResponsesBridgeTopLevelFields[field]; !supported {
  98. return false, "unknown_field_" + field
  99. }
  100. }
  101. var model string
  102. if raw, ok := root["model"]; !ok || json.Unmarshal(raw, &model) != nil || strings.TrimSpace(model) == "" {
  103. return false, "invalid_model"
  104. }
  105. if raw, ok := root["stream"]; ok {
  106. var stream *bool
  107. if json.Unmarshal(raw, &stream) != nil || stream == nil {
  108. return false, "invalid_stream"
  109. }
  110. }
  111. if raw, ok := root["parallel_tool_calls"]; ok {
  112. var parallelToolCalls *bool
  113. if json.Unmarshal(raw, &parallelToolCalls) != nil || parallelToolCalls == nil {
  114. return false, "invalid_parallel_tool_calls"
  115. }
  116. }
  117. if raw, ok := root["stream_options"]; ok {
  118. var options map[string]json.RawMessage
  119. if json.Unmarshal(raw, &options) != nil || options == nil {
  120. return false, "invalid_stream_options"
  121. }
  122. for field, value := range options {
  123. if field != "include_usage" {
  124. return false, "unknown_stream_option_" + field
  125. }
  126. var includeUsage *bool
  127. if json.Unmarshal(value, &includeUsage) != nil || includeUsage == nil {
  128. return false, "invalid_stream_include_usage"
  129. }
  130. }
  131. }
  132. for _, field := range []string{"max_tokens", "max_completion_tokens"} {
  133. if raw, ok := root[field]; ok {
  134. var value *int
  135. if json.Unmarshal(raw, &value) != nil || value == nil || *value < 128 {
  136. return false, "unsafe_" + field
  137. }
  138. }
  139. }
  140. if _, hasMaxTokens := root["max_tokens"]; hasMaxTokens {
  141. if _, hasMaxCompletionTokens := root["max_completion_tokens"]; hasMaxCompletionTokens {
  142. return false, "conflicting_max_tokens"
  143. }
  144. }
  145. for _, field := range []string{"temperature", "top_p"} {
  146. if raw, ok := root[field]; ok {
  147. var value *float64
  148. if json.Unmarshal(raw, &value) != nil || value == nil {
  149. return false, "invalid_" + field
  150. }
  151. }
  152. }
  153. if raw, ok := root["prompt_cache_key"]; ok {
  154. var key string
  155. if json.Unmarshal(raw, &key) != nil {
  156. return false, "invalid_prompt_cache_key"
  157. }
  158. }
  159. var messages []map[string]json.RawMessage
  160. rawMessages, ok := root["messages"]
  161. if !ok || json.Unmarshal(rawMessages, &messages) != nil || len(messages) == 0 {
  162. return false, "invalid_messages"
  163. }
  164. for _, message := range messages {
  165. var role string
  166. if raw, exists := message["role"]; !exists || json.Unmarshal(raw, &role) != nil {
  167. return false, "invalid_message_role"
  168. }
  169. switch role {
  170. case "system", "user":
  171. if ok, reason := grokChatMessageFieldsBridgeable(message, "role", "content"); !ok {
  172. return false, reason
  173. }
  174. raw, exists := message["content"]
  175. if !exists {
  176. return false, "non_text_message_content"
  177. }
  178. if ok, reason := grokChatRequiredMessageContentBridgeable(raw); !ok {
  179. return false, reason
  180. }
  181. case "assistant":
  182. if ok, reason := grokChatMessageFieldsBridgeable(message, "role", "content", "reasoning_content", "tool_calls"); !ok {
  183. return false, reason
  184. }
  185. reasoningContent := ""
  186. if raw, exists := message["reasoning_content"]; exists {
  187. if !grokChatJSONNull(raw) && json.Unmarshal(raw, &reasoningContent) != nil {
  188. return false, "invalid_reasoning_content"
  189. }
  190. }
  191. hasReasoningContent := strings.TrimSpace(reasoningContent) != ""
  192. toolCallCount := 0
  193. if raw, exists := message["tool_calls"]; exists {
  194. var reason string
  195. toolCallCount, reason = grokChatAssistantToolCallsBridgeable(raw)
  196. if reason != "" {
  197. return false, reason
  198. }
  199. }
  200. raw, hasContent := message["content"]
  201. if !hasContent || strings.TrimSpace(string(raw)) == "null" {
  202. if toolCallCount == 0 && !hasReasoningContent {
  203. return false, "non_text_message_content"
  204. }
  205. continue
  206. }
  207. var content string
  208. if json.Unmarshal(raw, &content) == nil {
  209. if strings.TrimSpace(content) == "" && toolCallCount == 0 && !hasReasoningContent {
  210. return false, "empty_message_content"
  211. }
  212. continue
  213. }
  214. if ok, reason := grokChatStructuredContentBridgeable(raw); !ok {
  215. // The converter can still emit a standalone reasoning part when
  216. // an otherwise empty content array accompanies reasoning_content.
  217. // Do not broaden this exception to unsupported/malformed parts.
  218. if !hasReasoningContent || reason != "empty_message_content" {
  219. return false, reason
  220. }
  221. }
  222. case "tool":
  223. if ok, reason := grokChatMessageFieldsBridgeable(message, "role", "content", "tool_call_id"); !ok {
  224. return false, reason
  225. }
  226. var callID string
  227. if raw, exists := message["tool_call_id"]; !exists || json.Unmarshal(raw, &callID) != nil || strings.TrimSpace(callID) == "" {
  228. return false, "invalid_tool_call_id"
  229. }
  230. var output string
  231. if raw, exists := message["content"]; !exists || json.Unmarshal(raw, &output) != nil || output == "" {
  232. return false, "invalid_tool_message_content"
  233. }
  234. default:
  235. return false, "unsupported_message_role_" + role
  236. }
  237. }
  238. return true, ""
  239. }
  240. func grokChatFunctionDeclarationsBridgeable(raw json.RawMessage) (bool, string) {
  241. if strings.TrimSpace(string(raw)) == "null" {
  242. return true, ""
  243. }
  244. var declarations []json.RawMessage
  245. if json.Unmarshal(raw, &declarations) != nil {
  246. return false, "invalid_tools"
  247. }
  248. for _, declaration := range declarations {
  249. var tool map[string]json.RawMessage
  250. if json.Unmarshal(declaration, &tool) != nil || tool == nil {
  251. return false, "invalid_tool"
  252. }
  253. for field := range tool {
  254. if field != "type" && field != "function" {
  255. return false, "unsafe_tool_field_" + field
  256. }
  257. }
  258. var toolType string
  259. if rawType, exists := tool["type"]; !exists || json.Unmarshal(rawType, &toolType) != nil || toolType != "function" {
  260. return false, "unsupported_tool_type"
  261. }
  262. functionRaw, exists := tool["function"]
  263. if !exists {
  264. return false, "invalid_tool_function"
  265. }
  266. var function map[string]json.RawMessage
  267. if json.Unmarshal(functionRaw, &function) != nil || function == nil {
  268. return false, "invalid_tool_function"
  269. }
  270. for field := range function {
  271. switch field {
  272. case "name", "description", "parameters", "strict":
  273. default:
  274. return false, "unsafe_tool_function_field_" + field
  275. }
  276. }
  277. var name string
  278. if rawName, exists := function["name"]; !exists || json.Unmarshal(rawName, &name) != nil || strings.TrimSpace(name) == "" {
  279. return false, "invalid_tool_function_name"
  280. }
  281. if rawDescription, exists := function["description"]; exists {
  282. var description string
  283. if json.Unmarshal(rawDescription, &description) != nil {
  284. return false, "invalid_tool_function_description"
  285. }
  286. }
  287. var parameters map[string]json.RawMessage
  288. if rawParameters, exists := function["parameters"]; !exists || json.Unmarshal(rawParameters, &parameters) != nil || parameters == nil {
  289. return false, "invalid_tool_function_parameters"
  290. }
  291. if rawStrict, exists := function["strict"]; exists {
  292. var strict bool
  293. if json.Unmarshal(rawStrict, &strict) != nil {
  294. return false, "invalid_tool_function_strict"
  295. }
  296. }
  297. }
  298. return true, ""
  299. }
  300. func grokChatToolChoiceBridgeable(raw json.RawMessage) (bool, string) {
  301. if strings.TrimSpace(string(raw)) == "null" {
  302. return true, ""
  303. }
  304. var choice string
  305. if json.Unmarshal(raw, &choice) != nil {
  306. return false, "unsupported_tool_choice"
  307. }
  308. switch choice {
  309. case "auto", "none", "required":
  310. return true, ""
  311. default:
  312. return false, "unsupported_tool_choice"
  313. }
  314. }
  315. func grokChatHasFunctionDeclarations(root map[string]json.RawMessage) bool {
  316. for _, field := range []string{"tools", "functions"} {
  317. raw, exists := root[field]
  318. if !exists {
  319. continue
  320. }
  321. var declarations []json.RawMessage
  322. if json.Unmarshal(raw, &declarations) == nil && len(declarations) > 0 {
  323. return true
  324. }
  325. }
  326. return false
  327. }
  328. func grokChatMessageFieldsBridgeable(message map[string]json.RawMessage, allowedFields ...string) (bool, string) {
  329. allowed := make(map[string]struct{}, len(allowedFields))
  330. for _, field := range allowedFields {
  331. allowed[field] = struct{}{}
  332. }
  333. for field := range message {
  334. if _, ok := allowed[field]; !ok {
  335. return false, "unsafe_message_field_" + field
  336. }
  337. }
  338. return true, ""
  339. }
  340. func grokChatRequiredMessageContentBridgeable(raw json.RawMessage) (bool, string) {
  341. var content string
  342. if json.Unmarshal(raw, &content) == nil {
  343. if strings.TrimSpace(content) == "" {
  344. return false, "empty_message_content"
  345. }
  346. return true, ""
  347. }
  348. // Structured content: only allow arrays whose parts are text or
  349. // image_url. These are losslessly convertible to Responses input_text/
  350. // input_image parts, so the bridge preserves Chat Completions semantics.
  351. return grokChatStructuredContentBridgeable(raw)
  352. }
  353. func grokChatAssistantToolCallsBridgeable(raw json.RawMessage) (int, string) {
  354. if strings.TrimSpace(string(raw)) == "null" {
  355. return 0, ""
  356. }
  357. var calls []map[string]json.RawMessage
  358. if json.Unmarshal(raw, &calls) != nil {
  359. return 0, "invalid_tool_calls"
  360. }
  361. for _, call := range calls {
  362. if call == nil {
  363. return 0, "invalid_tool_call"
  364. }
  365. for field := range call {
  366. switch field {
  367. case "id", "type", "function", "index":
  368. default:
  369. return 0, "unsafe_tool_call_field_" + field
  370. }
  371. }
  372. if rawIndex, exists := call["index"]; exists {
  373. var index *int
  374. if json.Unmarshal(rawIndex, &index) != nil || (index != nil && *index < 0) {
  375. return 0, "invalid_tool_call_index"
  376. }
  377. }
  378. var callID string
  379. if rawID, exists := call["id"]; !exists || json.Unmarshal(rawID, &callID) != nil || strings.TrimSpace(callID) == "" {
  380. return 0, "invalid_tool_call_id"
  381. }
  382. var callType string
  383. if rawType, exists := call["type"]; !exists || json.Unmarshal(rawType, &callType) != nil || callType != "function" {
  384. return 0, "unsupported_tool_call_type"
  385. }
  386. var function map[string]json.RawMessage
  387. if rawFunction, exists := call["function"]; !exists || json.Unmarshal(rawFunction, &function) != nil || function == nil {
  388. return 0, "invalid_tool_call_function"
  389. }
  390. for field := range function {
  391. if field != "name" && field != "arguments" {
  392. return 0, "unsafe_tool_call_function_field_" + field
  393. }
  394. }
  395. var name string
  396. if rawName, exists := function["name"]; !exists || json.Unmarshal(rawName, &name) != nil || strings.TrimSpace(name) == "" {
  397. return 0, "invalid_tool_call_function_name"
  398. }
  399. var arguments string
  400. if rawArguments, exists := function["arguments"]; !exists || json.Unmarshal(rawArguments, &arguments) != nil || !json.Valid([]byte(arguments)) {
  401. return 0, "invalid_tool_call_arguments"
  402. }
  403. }
  404. return len(calls), ""
  405. }
  406. func grokChatStructuredContentBridgeable(raw json.RawMessage) (bool, string) {
  407. var parts []map[string]json.RawMessage
  408. if err := json.Unmarshal(raw, &parts); err != nil {
  409. return false, "non_text_message_content"
  410. }
  411. if len(parts) == 0 {
  412. return false, "empty_message_content"
  413. }
  414. hasContent := false
  415. for _, part := range parts {
  416. var partType string
  417. rawType, ok := part["type"]
  418. if !ok || json.Unmarshal(rawType, &partType) != nil {
  419. return false, "non_text_message_content"
  420. }
  421. switch strings.TrimSpace(partType) {
  422. case "text":
  423. var text string
  424. if raw, ok := part["text"]; ok && json.Unmarshal(raw, &text) == nil {
  425. if strings.TrimSpace(text) != "" {
  426. hasContent = true
  427. }
  428. }
  429. case "image_url", "input_image":
  430. hasContent = true
  431. default:
  432. return false, "unsupported_content_part_" + strings.TrimSpace(partType)
  433. }
  434. }
  435. if !hasContent {
  436. return false, "empty_message_content"
  437. }
  438. return true, ""
  439. }
  440. func grokChatNullOrNone(raw json.RawMessage) bool {
  441. if strings.TrimSpace(string(raw)) == "null" {
  442. return true
  443. }
  444. var value string
  445. return json.Unmarshal(raw, &value) == nil && strings.EqualFold(strings.TrimSpace(value), "none")
  446. }
  447. func grokChatJSONNull(raw json.RawMessage) bool {
  448. return strings.TrimSpace(string(raw)) == "null"
  449. }
  450. func grokChatNullOrEmptyArray(raw json.RawMessage) bool {
  451. if strings.TrimSpace(string(raw)) == "null" {
  452. return true
  453. }
  454. var values []json.RawMessage
  455. return json.Unmarshal(raw, &values) == nil && len(values) == 0
  456. }
  457. func grokChatResponsesCacheIntentBody(body []byte) ([]byte, error) {
  458. // An empty Chat tools array is omitted by the Responses converter. In that
  459. // case auto/none is also a semantic no-op and must not suppress the normal
  460. // tool-free cache route. Non-empty converted tools are always kept intact.
  461. if gjson.GetBytes(body, "tools").Exists() {
  462. return append([]byte(nil), body...), nil
  463. }
  464. choice := gjson.GetBytes(body, "tool_choice")
  465. if !choice.Exists() || choice.Type != gjson.String || (choice.String() != "auto" && choice.String() != "none") {
  466. return append([]byte(nil), body...), nil
  467. }
  468. var root map[string]json.RawMessage
  469. if err := json.Unmarshal(body, &root); err != nil {
  470. return nil, err
  471. }
  472. delete(root, "tool_choice")
  473. return json.Marshal(root)
  474. }
  475. func grokChatResponsesBridgeModel(model string) bool {
  476. switch strings.ToLower(xai.StripGrokProviderPrefix(strings.TrimSpace(model))) {
  477. case "grok-4.5", "grok-4.6", "grok-4.6-latest":
  478. return true
  479. default:
  480. return false
  481. }
  482. }
  483. func grokChatResponsesRuntimeEligible(upstreamModel, cacheIdentity string) bool {
  484. return grokChatResponsesBridgeModel(upstreamModel) && strings.TrimSpace(cacheIdentity) != ""
  485. }
  486. // forwardGrokChatCompletionsViaResponses converts a strictly compatible Chat
  487. // request into xAI Responses format and reuses the established Responses-to-
  488. // Chat response translators. It intentionally does not run the Codex OAuth
  489. // transform because Grok CLI is a separate upstream protocol.
  490. func (s *OpenAIGatewayService) forwardGrokChatCompletionsViaResponses(
  491. ctx context.Context,
  492. c *gin.Context,
  493. account *Account,
  494. body []byte,
  495. promptCacheKey string,
  496. defaultMappedModel string,
  497. ) (*OpenAIForwardResult, error) {
  498. startTime := time.Now()
  499. var chatReq apicompat.ChatCompletionsRequest
  500. if err := json.Unmarshal(body, &chatReq); err != nil {
  501. return nil, fmt.Errorf("parse grok chat completions request: %w", err)
  502. }
  503. originalModel := chatReq.Model
  504. clientStream := chatReq.Stream
  505. billingModel := resolveOpenAIForwardModel(account, originalModel, defaultMappedModel)
  506. upstreamModel := normalizeOpenAIModelForUpstream(account, billingModel)
  507. cacheIdentity := resolveGrokCacheIdentity(c, body, promptCacheKey, upstreamModel)
  508. // Image inputs must go through the Responses bridge: the raw Chat
  509. // Completions path cannot forward image_url parts to Grok's native vision
  510. // for non-composer models, so they would be silently dropped. Route them to
  511. // Responses even when no prompt-cache identity is available.
  512. hasImageInput := openAIJSONValueMayContainImageInput(gjson.GetBytes(body, "messages"))
  513. if !grokChatResponsesRuntimeEligible(upstreamModel, cacheIdentity) && (!hasImageInput || !grokChatResponsesBridgeModel(upstreamModel)) {
  514. return s.forwardAsRawChatCompletions(ctx, c, account, body, defaultMappedModel)
  515. }
  516. responsesReq, err := apicompat.ChatCompletionsToResponses(&chatReq)
  517. if err != nil {
  518. return nil, fmt.Errorf("convert grok chat completions to responses: %w", err)
  519. }
  520. responsesReq.Model = upstreamModel
  521. responsesReq.Stream = true
  522. // Keep Chat and native Responses paths aligned for OpenAI-compatible
  523. // service_tier aliases (for example, "fast" -> "priority"). Unknown
  524. // values are omitted by the shared normalizer instead of reaching xAI.
  525. normalizeResponsesRequestServiceTier(responsesReq)
  526. // These fields are useful to Codex but are not needed by the Grok CLI
  527. // protocol. Keep the bridge request as close as possible to native Grok.
  528. responsesReq.Include = nil
  529. responsesReq.Store = nil
  530. responsesBody, err := json.Marshal(responsesReq)
  531. if err != nil {
  532. return nil, fmt.Errorf("marshal grok responses bridge request: %w", err)
  533. }
  534. // Preserve the converted Responses intent before Grok capability
  535. // sanitization. Cache routing must see the actual client function tools,
  536. // not the nested Chat Completions declarations and not a tool-free copy.
  537. intentBody, err := grokChatResponsesCacheIntentBody(responsesBody)
  538. if err != nil {
  539. return nil, fmt.Errorf("normalize grok responses bridge cache intent: %w", err)
  540. }
  541. responsesBody, err = patchGrokResponsesBody(responsesBody, upstreamModel)
  542. if err != nil {
  543. return nil, fmt.Errorf("patch grok responses bridge request: %w", err)
  544. }
  545. responsesBody, err = applyGrokResponsesCacheIdentity(responsesBody, intentBody, cacheIdentity, true)
  546. if err != nil {
  547. return nil, fmt.Errorf("apply grok responses bridge cache identity: %w", err)
  548. }
  549. responsesBody, err = applyGrokFreeRequestToolCacheRoute(c, responsesBody, intentBody, account, cacheIdentity)
  550. if err != nil {
  551. return nil, fmt.Errorf("apply grok responses bridge function-tool cache route: %w", err)
  552. }
  553. updatedBody, policyErr := s.applyOpenAIFastPolicyToBody(ctx, account, upstreamModel, responsesBody)
  554. if policyErr != nil {
  555. var blocked *OpenAIFastBlockedError
  556. if errors.As(policyErr, &blocked) {
  557. MarkOpsClientBusinessLimited(c, OpsClientBusinessLimitedReasonLocalPolicyDenied)
  558. writeChatCompletionsError(c, http.StatusForbidden, "permission_error", blocked.Message)
  559. }
  560. return nil, policyErr
  561. }
  562. responsesBody = updatedBody
  563. token, _, err := s.getRequestCredential(ctx, c, account)
  564. if err != nil {
  565. return nil, fmt.Errorf("get grok access token: %w", err)
  566. }
  567. upstreamCtx, releaseUpstreamCtx := detachUpstreamContext(ctx)
  568. upstreamReq, err := buildGrokResponsesRequest(upstreamCtx, c, account, responsesBody, token, cacheIdentity, s.cfg, s.settingService)
  569. releaseUpstreamCtx()
  570. if err != nil {
  571. return nil, fmt.Errorf("build grok responses bridge request: %w", err)
  572. }
  573. SetActualOpenAIUpstreamEndpoint(c, grokChatResponsesEndpoint)
  574. proxyURL := ""
  575. if account.ProxyID != nil && account.Proxy != nil {
  576. proxyURL = account.Proxy.URL()
  577. }
  578. resp, err := s.doOpenAIUpstream(upstreamReq, proxyURL, account)
  579. if err != nil {
  580. return nil, s.handleOpenAIUpstreamTransportError(ctx, c, account, err, false)
  581. }
  582. defer func() { _ = resp.Body.Close() }()
  583. if resp.StatusCode >= http.StatusBadRequest {
  584. respBody, upstreamMsg := s.readOpenAIUpstreamError(resp)
  585. if upstreamMsg == "" {
  586. upstreamMsg = fmt.Sprintf("xAI upstream returned status %d", resp.StatusCode)
  587. }
  588. kind := "http_error"
  589. if s.shouldFailoverGrokUpstreamError(resp.StatusCode, respBody) {
  590. kind = "failover"
  591. }
  592. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  593. ProxyID: opsUpstreamProxyID(account),
  594. ProxyName: opsUpstreamProxyName(account),
  595. Platform: account.Platform,
  596. AccountID: account.ID,
  597. AccountName: account.Name,
  598. UpstreamStatusCode: resp.StatusCode,
  599. UpstreamRequestID: firstNonEmpty(resp.Header.Get("x-request-id"), resp.Header.Get("xai-request-id")),
  600. Kind: kind,
  601. Message: upstreamMsg,
  602. })
  603. s.handleGrokAccountUpstreamError(withGrokTeamRateLimitModel(ctx, upstreamModel), account, resp.StatusCode, resp.Header, respBody)
  604. if s.shouldFailoverGrokUpstreamError(resp.StatusCode, respBody) {
  605. retryable, retryDelay, retryDeadline, retryMax := grokSameAccountRetryMetadata(account, resp.StatusCode, respBody)
  606. return nil, &UpstreamFailoverError{
  607. StatusCode: resp.StatusCode,
  608. ResponseBody: respBody,
  609. ResponseHeaders: resp.Header.Clone(),
  610. RetryableOnSameAccount: retryable,
  611. RequestScopedTransient: retryable && resp.StatusCode == http.StatusTooManyRequests,
  612. SameAccountRetryDelay: retryDelay,
  613. SameAccountRetryDeadline: retryDeadline,
  614. SameAccountRetryMax: retryMax,
  615. }
  616. }
  617. return s.handleChatCompletionsErrorResponse(resp, c, account, billingModel)
  618. }
  619. s.updateGrokUsageFromResponse(withGrokTeamRateLimitModel(ctx, upstreamModel), account, resp.Header, resp.StatusCode)
  620. var result *OpenAIForwardResult
  621. if clientStream {
  622. result, err = s.handleChatStreamingResponse(resp, c, account, originalModel, billingModel, upstreamModel, startTime, len(body))
  623. } else {
  624. result, err = s.handleChatBufferedStreamingResponse(resp, c, account, originalModel, billingModel, upstreamModel, startTime)
  625. }
  626. if result != nil {
  627. result.UpstreamEndpoint = grokChatResponsesEndpoint
  628. result.ResponseHeaders = resp.Header.Clone()
  629. if result.RequestID == "" {
  630. result.RequestID = firstNonEmpty(resp.Header.Get("x-request-id"), resp.Header.Get("xai-request-id"))
  631. }
  632. result.ReasoningEffort = extractOpenAIReasoningEffortFromBody(body, upstreamModel, billingModel, originalModel)
  633. }
  634. return result, err
  635. }