Switch explorer AI provider to Grok
This commit is contained in:
@@ -18,12 +18,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defaultExplorerAIModel = "gpt-5.4-mini"
|
||||
defaultExplorerAIReasoningEffort = "low"
|
||||
maxExplorerAIMessages = 12
|
||||
maxExplorerAIMessageChars = 4000
|
||||
maxExplorerAIContextChars = 22000
|
||||
maxExplorerAIDocSnippets = 6
|
||||
defaultExplorerAIModel = "grok-3"
|
||||
maxExplorerAIMessages = 12
|
||||
maxExplorerAIMessageChars = 4000
|
||||
maxExplorerAIContextChars = 22000
|
||||
maxExplorerAIDocSnippets = 6
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -85,30 +84,31 @@ type AIContextSource struct {
|
||||
Origin string `json:"origin,omitempty"`
|
||||
}
|
||||
|
||||
type openAIResponsesRequest struct {
|
||||
Model string `json:"model"`
|
||||
Input []openAIInputMessage `json:"input"`
|
||||
Reasoning *openAIReasoning `json:"reasoning,omitempty"`
|
||||
type xAIChatCompletionsRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []xAIChatMessageReq `json:"messages"`
|
||||
Stream bool `json:"stream"`
|
||||
}
|
||||
|
||||
type openAIReasoning struct {
|
||||
Effort string `json:"effort,omitempty"`
|
||||
type xAIChatMessageReq struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type openAIInputMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content []openAIInputContent `json:"content"`
|
||||
}
|
||||
|
||||
type openAIInputContent struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type openAIResponsesResponse struct {
|
||||
type xAIChatCompletionsResponse struct {
|
||||
Model string `json:"model"`
|
||||
OutputText string `json:"output_text"`
|
||||
Output []openAIOutputItem `json:"output"`
|
||||
Choices []xAIChoice `json:"choices"`
|
||||
OutputText string `json:"output_text,omitempty"`
|
||||
Output []openAIOutputItem `json:"output,omitempty"`
|
||||
}
|
||||
|
||||
type xAIChoice struct {
|
||||
Message xAIChoiceMessage `json:"message"`
|
||||
}
|
||||
|
||||
type xAIChoiceMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type openAIOutputItem struct {
|
||||
@@ -174,7 +174,7 @@ func (s *Server) handleAIChat(w http.ResponseWriter, r *http.Request) {
|
||||
if !explorerAIEnabled() {
|
||||
s.aiMetrics.Record("chat", http.StatusServiceUnavailable, time.Since(startedAt), "service_unavailable", clientIP)
|
||||
s.logAIRequest("chat", http.StatusServiceUnavailable, time.Since(startedAt), clientIP, explorerAIModel(), "service_unavailable")
|
||||
writeError(w, http.StatusServiceUnavailable, "service_unavailable", "explorer ai is not configured; set OPENAI_API_KEY on the backend")
|
||||
writeError(w, http.StatusServiceUnavailable, "service_unavailable", "explorer ai is not configured; set XAI_API_KEY on the backend")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ func (s *Server) handleAIChat(w http.ResponseWriter, r *http.Request) {
|
||||
latestUser := latestUserMessage(messages)
|
||||
ctxEnvelope, warnings := s.buildAIContext(r.Context(), latestUser, chatReq.PageContext)
|
||||
|
||||
reply, model, err := s.callOpenAIResponses(r.Context(), messages, ctxEnvelope)
|
||||
reply, model, err := s.callXAIChatCompletions(r.Context(), messages, ctxEnvelope)
|
||||
if err != nil {
|
||||
statusCode, code, message, details := mapAIUpstreamError(err)
|
||||
s.aiMetrics.Record("chat", statusCode, time.Since(startedAt), code, clientIP)
|
||||
@@ -219,11 +219,11 @@ func (s *Server) handleAIChat(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func explorerAIEnabled() bool {
|
||||
return strings.TrimSpace(os.Getenv("OPENAI_API_KEY")) != ""
|
||||
return strings.TrimSpace(os.Getenv("XAI_API_KEY")) != ""
|
||||
}
|
||||
|
||||
func explorerAIModel() string {
|
||||
if model := strings.TrimSpace(os.Getenv("OPENAI_MODEL")); model != "" {
|
||||
if model := strings.TrimSpace(os.Getenv("XAI_MODEL")); model != "" {
|
||||
return model
|
||||
}
|
||||
if model := strings.TrimSpace(os.Getenv("EXPLORER_AI_MODEL")); model != "" {
|
||||
@@ -232,16 +232,6 @@ func explorerAIModel() string {
|
||||
return defaultExplorerAIModel
|
||||
}
|
||||
|
||||
func explorerAIReasoningEffort() string {
|
||||
if effort := strings.TrimSpace(os.Getenv("OPENAI_REASONING_EFFORT")); effort != "" {
|
||||
return effort
|
||||
}
|
||||
if effort := strings.TrimSpace(os.Getenv("EXPLORER_AI_REASONING_EFFORT")); effort != "" {
|
||||
return effort
|
||||
}
|
||||
return defaultExplorerAIReasoningEffort
|
||||
}
|
||||
|
||||
func (s *Server) buildAIContext(ctx context.Context, query string, pageContext map[string]string) (AIContextEnvelope, []string) {
|
||||
warnings := []string{}
|
||||
envelope := AIContextEnvelope{
|
||||
@@ -879,60 +869,43 @@ func latestUserMessage(messages []AIChatMessage) string {
|
||||
return messages[len(messages)-1].Content
|
||||
}
|
||||
|
||||
func (s *Server) callOpenAIResponses(ctx context.Context, messages []AIChatMessage, contextEnvelope AIContextEnvelope) (string, string, error) {
|
||||
apiKey := strings.TrimSpace(os.Getenv("OPENAI_API_KEY"))
|
||||
func (s *Server) callXAIChatCompletions(ctx context.Context, messages []AIChatMessage, contextEnvelope AIContextEnvelope) (string, string, error) {
|
||||
apiKey := strings.TrimSpace(os.Getenv("XAI_API_KEY"))
|
||||
if apiKey == "" {
|
||||
return "", "", fmt.Errorf("OPENAI_API_KEY is not configured")
|
||||
return "", "", fmt.Errorf("XAI_API_KEY is not configured")
|
||||
}
|
||||
|
||||
model := explorerAIModel()
|
||||
baseURL := strings.TrimRight(strings.TrimSpace(os.Getenv("OPENAI_BASE_URL")), "/")
|
||||
baseURL := strings.TrimRight(strings.TrimSpace(os.Getenv("XAI_BASE_URL")), "/")
|
||||
if baseURL == "" {
|
||||
baseURL = "https://api.openai.com/v1"
|
||||
baseURL = "https://api.x.ai/v1"
|
||||
}
|
||||
|
||||
contextJSON, _ := json.MarshalIndent(contextEnvelope, "", " ")
|
||||
contextText := clipString(string(contextJSON), maxExplorerAIContextChars)
|
||||
|
||||
input := []openAIInputMessage{
|
||||
input := []xAIChatMessageReq{
|
||||
{
|
||||
Role: "system",
|
||||
Content: []openAIInputContent{
|
||||
{
|
||||
Type: "input_text",
|
||||
Text: "You are the SolaceScanScout ecosystem assistant for Chain 138. Answer using the supplied indexed explorer data, route inventory, and workspace documentation. Be concise, operationally useful, and explicit about uncertainty. Never claim a route, deployment, or production status is live unless the provided context says it is live. If data is missing, say exactly what is missing.",
|
||||
},
|
||||
},
|
||||
Role: "system",
|
||||
Content: "You are the SolaceScanScout ecosystem assistant for Chain 138. Answer using the supplied indexed explorer data, route inventory, and workspace documentation. Be concise, operationally useful, and explicit about uncertainty. Never claim a route, deployment, or production status is live unless the provided context says it is live. If data is missing, say exactly what is missing.",
|
||||
},
|
||||
{
|
||||
Role: "system",
|
||||
Content: []openAIInputContent{
|
||||
{
|
||||
Type: "input_text",
|
||||
Text: "Retrieved ecosystem context:\n" + contextText,
|
||||
},
|
||||
},
|
||||
Role: "system",
|
||||
Content: "Retrieved ecosystem context:\n" + contextText,
|
||||
},
|
||||
}
|
||||
|
||||
for _, message := range messages {
|
||||
input = append(input, openAIInputMessage{
|
||||
Role: message.Role,
|
||||
Content: []openAIInputContent{
|
||||
{
|
||||
Type: "input_text",
|
||||
Text: message.Content,
|
||||
},
|
||||
},
|
||||
input = append(input, xAIChatMessageReq{
|
||||
Role: message.Role,
|
||||
Content: message.Content,
|
||||
})
|
||||
}
|
||||
|
||||
payload := openAIResponsesRequest{
|
||||
Model: model,
|
||||
Input: input,
|
||||
Reasoning: &openAIReasoning{
|
||||
Effort: explorerAIReasoningEffort(),
|
||||
},
|
||||
payload := xAIChatCompletionsRequest{
|
||||
Model: model,
|
||||
Messages: input,
|
||||
Stream: false,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(payload)
|
||||
@@ -940,7 +913,7 @@ func (s *Server) callOpenAIResponses(ctx context.Context, messages []AIChatMessa
|
||||
return "", model, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+"/responses", bytes.NewReader(body))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+"/chat/completions", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return "", model, err
|
||||
}
|
||||
@@ -955,7 +928,7 @@ func (s *Server) callOpenAIResponses(ctx context.Context, messages []AIChatMessa
|
||||
StatusCode: http.StatusGatewayTimeout,
|
||||
Code: "upstream_timeout",
|
||||
Message: "explorer ai upstream timed out",
|
||||
Details: "OpenAI request exceeded the configured timeout",
|
||||
Details: "xAI request exceeded the configured timeout",
|
||||
}
|
||||
}
|
||||
return "", model, &AIUpstreamError{
|
||||
@@ -977,10 +950,10 @@ func (s *Server) callOpenAIResponses(ctx context.Context, messages []AIChatMessa
|
||||
}
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
return "", model, parseOpenAIError(resp.StatusCode, responseBody)
|
||||
return "", model, parseXAIError(resp.StatusCode, responseBody)
|
||||
}
|
||||
|
||||
var response openAIResponsesResponse
|
||||
var response xAIChatCompletionsResponse
|
||||
if err := json.Unmarshal(responseBody, &response); err != nil {
|
||||
return "", model, &AIUpstreamError{
|
||||
StatusCode: http.StatusBadGateway,
|
||||
@@ -990,7 +963,13 @@ func (s *Server) callOpenAIResponses(ctx context.Context, messages []AIChatMessa
|
||||
}
|
||||
}
|
||||
|
||||
reply := strings.TrimSpace(response.OutputText)
|
||||
reply := ""
|
||||
if len(response.Choices) > 0 {
|
||||
reply = strings.TrimSpace(response.Choices[0].Message.Content)
|
||||
}
|
||||
if reply == "" {
|
||||
reply = strings.TrimSpace(response.OutputText)
|
||||
}
|
||||
if reply == "" {
|
||||
reply = strings.TrimSpace(extractOutputText(response.Output))
|
||||
}
|
||||
@@ -999,7 +978,7 @@ func (s *Server) callOpenAIResponses(ctx context.Context, messages []AIChatMessa
|
||||
StatusCode: http.StatusBadGateway,
|
||||
Code: "upstream_bad_response",
|
||||
Message: "explorer ai upstream returned no output text",
|
||||
Details: "OpenAI response did not include output_text or content text",
|
||||
Details: "xAI response did not include choices[0].message.content or output text",
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(response.Model) != "" {
|
||||
@@ -1008,7 +987,7 @@ func (s *Server) callOpenAIResponses(ctx context.Context, messages []AIChatMessa
|
||||
return reply, model, nil
|
||||
}
|
||||
|
||||
func parseOpenAIError(statusCode int, responseBody []byte) error {
|
||||
func parseXAIError(statusCode int, responseBody []byte) error {
|
||||
var parsed struct {
|
||||
Error struct {
|
||||
Message string `json:"message"`
|
||||
|
||||
Reference in New Issue
Block a user