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-helloWrite 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
- Guides
- Count cost from usage
- Other languages: the planned official Go client.
Was this page helpful?