AI Integration
Grit integrates with Vercel AI Gateway — one API key gives you access to hundreds of models from all major providers (Anthropic, OpenAI, Google, and more) through a single OpenAI-compatible endpoint. Generate completions, run multi-turn conversations, and stream responses via SSE.
Configuration
AI is configured via three environment variables. Vercel AI Gateway uses a provider/model format, so switching models is just a string change -- no code changes required.
# AI Configuration (Vercel AI Gateway)AI_GATEWAY_API_KEY= # Get from vercel.com/ai-gatewayAI_GATEWAY_MODEL=anthropic/claude-sonnet-4-6 # provider/model formatAI_GATEWAY_URL=https://ai-gateway.vercel.sh/v1
Model format: Models use provider/model format. Examples: anthropic/claude-sonnet-4-6, openai/gpt-5.4, google/gemini-2.5-pro. See the full list at vercel.com/ai-gateway.
AI Service
The AI service at internal/ai/ai.go provides a unified interface powered by Vercel AI Gateway. It sends requests to a single OpenAI-compatible endpoint, which routes to the correct provider based on your model string.
// Message represents a chat message.type Message struct {Role string `json:"role"` // "user" or "assistant"Content string `json:"content"`}// CompletionRequest holds the input for a completion.type CompletionRequest struct {Prompt string `json:"prompt"`Messages []Message `json:"messages,omitempty"`MaxTokens int `json:"max_tokens,omitempty"`Temperature float64 `json:"temperature,omitempty"`}// CompletionResponse holds the AI response.type CompletionResponse struct {Content string `json:"content"`Model string `json:"model"`Usage *Usage `json:"usage,omitempty"`}// Usage contains token usage information.type Usage struct {InputTokens int `json:"input_tokens"`OutputTokens int `json:"output_tokens"`}// StreamHandler is called for each chunk of a streamed response.type StreamHandler func(chunk string) error
// New creates a new AI service instance.func New(apiKey, model, gatewayURL string) *AI// Complete generates a response from a single prompt or message history.// Routes through Vercel AI Gateway to the provider specified in the model string.func (a *AI) Complete(ctx context.Context, req CompletionRequest) (*CompletionResponse, error)// Stream generates a streaming response, calling handler for each text chunk.// Uses SSE (Server-Sent Events) from the upstream API.func (a *AI) Stream(ctx context.Context, req CompletionRequest, handler StreamHandler) error
Complete: Single Prompt
The simplest way to use the AI service. Send a prompt, get a response.
aiService := ai.New(apiKey, "anthropic/claude-sonnet-4-6", "https://ai-gateway.vercel.sh/v1")resp, err := aiService.Complete(ctx, ai.CompletionRequest{Prompt: "Explain the Go concurrency model in 3 sentences.",MaxTokens: 256,})if err != nil {return fmt.Errorf("AI completion failed: %w", err)}fmt.Println(resp.Content) // "Go uses goroutines..."fmt.Println(resp.Model) // "anthropic/claude-sonnet-4-6"fmt.Println(resp.Usage.InputTokens) // 12fmt.Println(resp.Usage.OutputTokens) // 87
API Endpoint
Chat: Multi-Turn Conversations
For multi-turn conversations, send an array of messages with alternating user/assistant roles. The AI service passes the full conversation history to the provider.
resp, err := aiService.Complete(ctx, ai.CompletionRequest{Messages: []ai.Message{{Role: "user", Content: "I'm building a SaaS with Go and React."},{Role: "assistant", Content: "That's a great stack! Go handles the backend..."},{Role: "user", Content: "How should I structure my API?"},},MaxTokens: 512,Temperature: 0.7,})
API Endpoint
// Request body:{"messages": [{ "role": "user", "content": "What is Grit?" },{ "role": "assistant", "content": "Grit is a full-stack framework..." },{ "role": "user", "content": "How do I generate a resource?" }],"max_tokens": 512,"temperature": 0.7}// Response:{"data": {"content": "To generate a resource in Grit, use the CLI...","model": "anthropic/claude-sonnet-4-6","usage": {"input_tokens": 45,"output_tokens": 120}}}
Stream: Server-Sent Events
The streaming endpoint sends response chunks as SSE events in real-time. This enables typewriter-style output in chat interfaces. The handler function receives each text chunk as it arrives from the AI provider.
// In a Go service:err := aiService.Stream(ctx, ai.CompletionRequest{Prompt: "Write a haiku about Go programming",MaxTokens: 100,}, func(chunk string) error {fmt.Print(chunk) // Prints each word/token as it arrivesreturn nil})
How Streaming Works via Gin
The AI handler at POST /api/ai/stream sets SSE headers and uses Gin's c.SSEvent() to send each chunk to the client. The connection stays open until the AI response is complete.
func (h *AIHandler) Stream(c *gin.Context) {var req chatRequestif err := c.ShouldBindJSON(&req); err != nil {c.JSON(http.StatusUnprocessableEntity, gin.H{...})return}// Set SSE headersc.Header("Content-Type", "text/event-stream")c.Header("Cache-Control", "no-cache")c.Header("Connection", "keep-alive")// Stream chunks to clienterr := h.AI.Stream(c.Request.Context(), ai.CompletionRequest{Messages: req.Messages,MaxTokens: req.MaxTokens,Temperature: req.Temperature,}, func(chunk string) error {c.SSEvent("message", chunk)c.Writer.Flush()return nil})if err != nil {c.SSEvent("error", fmt.Sprintf("Stream error: %v", err))c.Writer.Flush()}c.SSEvent("done", "[DONE]")c.Writer.Flush()}
Consuming the Stream (Frontend)
async function streamCompletion(messages: Message[]) {const response = await fetch("/api/ai/stream", {method: "POST",headers: {"Content-Type": "application/json",Authorization: `Bearer ${token}`,},body: JSON.stringify({ messages, max_tokens: 1024 }),});const reader = response.body?.getReader();const decoder = new TextDecoder();while (true) {const { done, value } = await reader!.read();if (done) break;const text = decoder.decode(value);const lines = text.split("\n");for (const line of lines) {if (line.startsWith("data: ")) {const data = JSON.parse(line.slice(6));if (data === "[DONE]") return;// Append chunk to the UIsetResponse((prev) => prev + data);}}}}
API Endpoints
| Endpoint | Method | Description |
|---|---|---|
| /api/ai/complete | POST | Single prompt completion |
| /api/ai/chat | POST | Multi-turn conversation |
| /api/ai/stream | POST | Streaming response via SSE |
Switching Models
With Vercel AI Gateway, switching between providers is just a model string change. The gateway handles all differences in API formats, authentication, and streaming protocols behind a single OpenAI-compatible endpoint.
AI_GATEWAY_MODEL=anthropic/claude-sonnet-4-6
AI_GATEWAY_MODEL=openai/gpt-5.4
AI_GATEWAY_MODEL=google/gemini-2.5-pro
One key, all providers. You do not need separate API keys for each provider. Your single AI_GATEWAY_API_KEY works with every model available on Vercel AI Gateway. The gateway URL stays the same: https://ai-gateway.vercel.sh/v1.
Initialization in main.go
The AI service is created in main.go and passed to the AI handler. If no API key is configured, the handler gracefully returns a 503 "AI service not configured" response.
// Initialize AI service (optional -- graceful if not configured)var aiService *ai.AIif cfg.AIGatewayAPIKey != "" {aiService = ai.New(cfg.AIGatewayAPIKey, cfg.AIGatewayModel, cfg.AIGatewayURL)log.Printf("AI service initialized: %s", cfg.AIGatewayModel)}// Register AI routesaiHandler := &handlers.AIHandler{AI: aiService}aiGroup := api.Group("/ai", authMiddleware){aiGroup.POST("/complete", aiHandler.Complete)aiGroup.POST("/chat", aiHandler.Chat)aiGroup.POST("/stream", aiHandler.Stream)}