This guide walks you through installing Twilight AI and making your first LLM request.
- Go 1.25 or later
- An API key from OpenAI, Anthropic, Google, or any OpenAI-compatible provider
go get github.com/felinics/twilightA Provider is the bridge between the SDK and a specific AI backend. The SDK ships with two OpenAI providers:
Chat Completions — broad compatibility with OpenAI and all OpenAI-compatible APIs:
import "github.com/felinics/twilight/provider/openai/completions"
provider := completions.New(
completions.WithAPIKey("sk-..."),
)Responses — OpenAI's newer API with native reasoning and citation support:
import "github.com/felinics/twilight/provider/openai/responses"
provider := responses.New(
responses.WithAPIKey("sk-..."),
)For OpenAI-compatible endpoints, add a custom base URL:
// Completions API
provider := completions.New(
completions.WithAPIKey("your-key"),
completions.WithBaseURL("https://api.deepseek.com/v1"),
)
// Responses API (e.g. via OpenRouter)
provider := responses.New(
responses.WithAPIKey("sk-or-v1-..."),
responses.WithBaseURL("https://openrouter.ai/api/v1"),
)model := provider.ChatModel("gpt-4o-mini")The model carries a reference to its provider, so the SDK knows which backend to call.
One model call takes a sdk.Request and returns a sdk.ModelResult:
import "github.com/felinics/twilight/sdk"
result, err := model.Generate(ctx, sdk.Request{
Messages: []sdk.Message{
sdk.UserMessage("What is the capital of France?"),
},
})
// result.Text == "The capital of France is Paris."The result carries the usage, the finish reason and the response metadata next to the text:
result, err := model.Generate(ctx, sdk.Request{
System: "You are a helpful assistant.",
Messages: []sdk.Message{
sdk.UserMessage("What is the capital of France?"),
},
})
fmt.Println(result.Text) // response text
fmt.Println(result.Usage.TotalTokens) // token count
fmt.Println(result.FinishReason) // "stop"
fmt.Println(result.Response.ModelID) // "gpt-4o-mini"Every sampling parameter is a field of the Request: Temperature, MaxTokens, Tools, ToolChoice, ResponseFormat and the rest. See the API Reference.
For real-time output, Model.Stream returns a sdk.ModelStream: a channel of typed parts, and the assembled result once the channel is drained.
stream, err := model.Stream(ctx, sdk.Request{
Messages: []sdk.Message{
sdk.UserMessage("Tell me a short story."),
},
})
if err != nil {
log.Fatal(err)
}
for part := range stream.Parts {
switch p := part.(type) {
case *sdk.TextDeltaPart:
fmt.Print(p.Text)
case *sdk.ErrorPart:
log.Fatal(p.Error)
}
}
fmt.Println() // newline after streaming
result, err := stream.Result() // the same ModelResult Generate would have returnedTo collect the whole stream without handling parts, sdk.CollectStream(ctx, stream.Parts) returns the ModelResult directly.
Before making generation requests, you can verify that your API key and endpoint are working:
err := provider.Test(ctx)
var apiErr *sdk.APIError
switch {
case err == nil:
fmt.Println("Ready to go!")
case sdk.KindOf(err) == sdk.KindAuthentication:
fmt.Printf("Connected but auth failed: %v\n", err)
case errors.As(err, &apiErr):
fmt.Printf("Connected but the check failed (HTTP %d): %v\n", apiErr.StatusCode, err)
default:
fmt.Printf("Cannot reach endpoint: %v\n", err)
}List all models your API key has access to:
models, err := provider.ListModels(ctx)
for _, m := range models {
fmt.Println(m.ID)
}Or check a single model:
model := provider.ChatModel("gpt-4o")
testResult, _ := model.Test(ctx)
fmt.Println(testResult.Supported) // true or falseSpeech synthesis converts text to audio. Speech providers are separate from chat providers:
import "github.com/felinics/twilight/provider/edge/speech"
speechProvider := speech.New()
speechModel := speechProvider.SpeechModel("edge-read-aloud")
result, err := sdk.GenerateSpeech(ctx,
sdk.WithSpeechModel(speechModel),
sdk.WithText("Hello, this is a speech test."),
sdk.WithSpeechConfig(map[string]any{
"voice": "en-US-EmmaMultilingualNeural",
}),
)
// result.Audio is []byte (raw audio data)
// result.ContentType is "audio/mpeg"
// Or stream for low-latency playback:
sr, err := sdk.StreamSpeech(ctx,
sdk.WithSpeechModel(speechModel),
sdk.WithText("Streaming speech synthesis."),
)
for chunk := range sr.Stream {
// write each chunk...
}Edge TTS is free and requires no API key. See Speech for all available voices and configuration options.
Embeddings convert text into numeric vectors for search, retrieval, and similarity comparison. Embedding providers are separate from chat providers:
import "github.com/felinics/twilight/provider/openai/embedding"
embProvider := embedding.New(
embedding.WithAPIKey("sk-..."),
)
embModel := embProvider.EmbeddingModel("text-embedding-3-small")
vec, err := sdk.Embed(ctx, "What is the capital of France?",
sdk.WithEmbeddingModel(embModel),
)
// vec is a []float64 vector
// Embed multiple texts at once
result, err := sdk.EmbedMany(ctx, []string{"Paris", "London", "Berlin"},
sdk.WithEmbeddingModel(embModel),
)
// result.Embeddings[0] is the vector for "Paris"
// result.Usage.Tokens reports total tokens consumedFor Google embeddings, use the Google embedding provider:
import "github.com/felinics/twilight/provider/google/embedding"
embProvider := embedding.New(
embedding.WithAPIKey("AIza..."),
embedding.WithTaskType("RETRIEVAL_QUERY"),
)
embModel := embProvider.EmbeddingModel("gemini-embedding-001")
vec, err := sdk.Embed(ctx, "search query here",
sdk.WithEmbeddingModel(embModel),
)Model.Generate and Model.Stream are the only chat entry points. Embedding, image, speech, transcription and video calls are package-level functions (sdk.Embed, sdk.GenerateImage, ...) and, equivalently, methods of a sdk.Client:
client := sdk.NewClient()
vec, err := client.Embed(ctx, "search query here", sdk.WithEmbeddingModel(embModel))The SDK itself does not read environment variables.
go test ./... runs only offline tests against local HTTP fixtures; it never contacts a real provider, even when API keys are set. Tests that call real APIs are compiled only with the integration build tag:
go test -tags integration -count=1 -run '^TestIntegration_' ./...These send real requests that may be billed. Each test skips when its credentials are missing. Credentials are read from the environment, or from a .env file in the repository root (see .env.example):
OPENAI_API_KEY=sk-...
OPENAI_BASE_URL=https://api.openai.com/v1
OPENAI_MODEL=gpt-4o-mini
- Providers — learn about the Provider interface and OpenAI options
- Embeddings — generate vector embeddings with OpenAI and Google
- Speech — speech synthesis with Edge TTS and custom providers
- Tool Calling — define tools, run the calls the model makes, build the next request
- Streaming — understand StreamPart types and advanced patterns
- API Reference — complete type and function reference