Quickstart: Go

This quickstart uses only the Go standard library (net/http, encoding/json, bufio). It needs Go 1.21 or later.

Create a module

mkdir syntic-hello && cd syntic-hello
go mod init example.com/syntic-hello

Write the client

A small package-level helper keeps the request types in one place:

// syntic.go
package main
 
import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"time"
)
 
const baseURL = "https://app.syntic.ai/v1"
 
type Message struct {
	Role       string     `json:"role"`
	Content    string     `json:"content"`
	ToolCalls  []ToolCall `json:"tool_calls,omitempty"`
	ToolCallID string     `json:"tool_call_id,omitempty"`
}
 
type ToolCall struct {
	ID       string `json:"id"`
	Type     string `json:"type"`
	Function struct {
		Name      string `json:"name"`
		Arguments string `json:"arguments"`
	} `json:"function"`
}
 
type Request struct {
	Model     string    `json:"model"`
	Stream    bool      `json:"stream"` // no omitempty: false must be sent
	MaxTokens int       `json:"max_tokens,omitempty"`
	Messages  []Message `json:"messages"`
	Tools     []any     `json:"tools,omitempty"`
}
 
type Completion struct {
	Choices []struct {
		Message      Message `json:"message"`
		FinishReason string  `json:"finish_reason"`
	} `json:"choices"`
	Usage struct {
		PromptTokens     int `json:"prompt_tokens"`
		CompletionTokens int `json:"completion_tokens"`
		TotalTokens      int `json:"total_tokens"`
	} `json:"usage"`
}
 
type APIError struct {
	Status int
	Error  struct {
		Type      string `json:"type"`
		Code      string `json:"code"`
		Message   string `json:"message"`
		RequestID string `json:"request_id"`
	} `json:"error"`
	RequestID string
}
 
func (e *APIError) Error() string {
	return fmt.Sprintf("syntic: %d %s/%s %s (request %s)", e.Status, e.Error.Type, e.Error.Code, e.Error.Message, e.RequestID)
}
 
var httpClient = &http.Client{Timeout: 5 * time.Minute}
 
func post(ctx context.Context, body Request) (*http.Response, error) {
	buf, err := json.Marshal(body)
	if err != nil {
		return nil, err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+"/chat/completions", bytes.NewReader(buf))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("SYNTIC_API_KEY"))
	req.Header.Set("Content-Type", "application/json")
	resp, err := httpClient.Do(req)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode != http.StatusOK {
		defer resp.Body.Close()
		apiErr := &APIError{Status: resp.StatusCode, RequestID: resp.Header.Get("x-syntic-request-id")}
		raw, _ := io.ReadAll(resp.Body)
		_ = json.Unmarshal(raw, apiErr) // the error envelope: type, code, message, request_id
		return nil, apiErr
	}
	return resp, nil
}
 
func Complete(ctx context.Context, body Request) (*Completion, error) {
	body.Stream = false
	resp, err := post(ctx, body)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	var out Completion
	return &out, json.NewDecoder(resp.Body).Decode(&out)
}

Send a request

// main.go
package main
 
import (
	"context"
	"fmt"
	"log"
)
 
func main() {
	out, err := Complete(context.Background(), Request{
		Model: "Amara S1.2",
		Messages: []Message{
			{Role: "system", Content: "Answer in one sentence."},
			{Role: "user", Content: "Why do cats knead blankets?"},
		},
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(out.Choices[0].Message.Content)
	fmt.Printf("tokens: %d\n", out.Usage.TotalTokens)
}

Run it

go run .

The Stream field has no omitempty tag on purpose. The API streams when stream is missing, so false must be sent explicitly.

Stream the reply

Add this function to syntic.go and call it with a Request:

import (
	"bufio"
	"strings"
)
 
type chunk struct {
	Choices []struct {
		Delta struct {
			Content string `json:"content"`
		} `json:"delta"`
	} `json:"choices"`
	Error *struct {
		Code    string `json:"code"`
		Message string `json:"message"`
	} `json:"error"`
}
 
// Stream calls onText for each piece of text as it arrives.
func Stream(ctx context.Context, body Request, onText func(string)) error {
	body.Stream = true
	resp, err := post(ctx, body)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
 
	sc := bufio.NewScanner(resp.Body)
	sc.Buffer(make([]byte, 0, 64*1024), 1<<20) // allow long event lines
	for sc.Scan() {
		line := sc.Text()
		if !strings.HasPrefix(line, "data: ") {
			continue
		}
		payload := strings.TrimPrefix(line, "data: ")
		if payload == "[DONE]" {
			return nil
		}
		var c chunk
		if err := json.Unmarshal([]byte(payload), &c); err != nil {
			return err
		}
		if c.Error != nil {
			return fmt.Errorf("stream failed: %s", c.Error.Code)
		}
		if len(c.Choices) > 0 && c.Choices[0].Delta.Content != "" {
			onText(c.Choices[0].Delta.Content)
		}
	}
	if err := sc.Err(); err != nil {
		return err
	}
	return fmt.Errorf("stream ended without [DONE]")
}
err := Stream(ctx, Request{
	Model:    "Amara Quick",
	Messages: []Message{{Role: "user", Content: "Write a two-line poem about rain."}},
}, func(s string) { fmt.Print(s) })

Cancel the ctx to stop a stream early; closing the connection stops generation.

Call a function

tool := map[string]any{
	"type": "function",
	"function": map[string]any{
		"name":        "store_hours",
		"description": "Opening hours of a store branch for a given weekday.",
		"parameters": map[string]any{
			"type": "object",
			"properties": map[string]any{
				"branch": map[string]any{"type": "string"},
				"day":    map[string]any{"type": "string", "enum": []string{"mon", "tue", "wed", "thu", "fri", "sat", "sun"}},
			},
			"required": []string{"branch", "day"},
		},
	},
}
 
req := Request{Model: "Amara S1.2", Tools: []any{tool},
	Messages: []Message{{Role: "user", Content: "Is the Harbour Street branch open on Sunday?"}}}
out, err := Complete(ctx, req)
if err != nil {
	log.Fatal(err)
}
msg := out.Choices[0].Message
if len(msg.ToolCalls) > 0 {
	req.Messages = append(req.Messages, msg)
	for _, call := range msg.ToolCalls {
		var args struct{ Branch, Day string }
		_ = json.Unmarshal([]byte(call.Function.Arguments), &args)
		result := `{"open":"10:00","close":"16:00"}` // replace with a real lookup
		req.Messages = append(req.Messages, Message{Role: "tool", ToolCallID: call.ID, Content: result})
	}
	out, err = Complete(ctx, req)
	if err != nil {
		log.Fatal(err)
	}
}
fmt.Println(out.Choices[0].Message.Content)

Handle errors

var apiErr *APIError
if errors.As(err, &apiErr) {
	switch {
	case apiErr.Status == 429 || apiErr.Status >= 500:
		// retry with back-off; see the Retries recipe
	case apiErr.Status == 402:
		// quota_exhausted: plan window or credits used up
	default:
		log.Fatalf("fix the request: %v", apiErr)
	}
}

See Retries with back-off and Error codes.

Next steps

Was this page helpful?