A lightweight, idiomatic AI SDK for Go β inspired by Vercel AI SDK.
- One call, one result β
Model.GenerateandModel.Streamtake ansdk.Requestand return aModelResultor a stream of typed parts.Embed,EmbedMany,GenerateImage,EditImage,GenerateVideo,GenerateSpeechandStreamSpeechcover the other modalities - Provider-agnostic β swap between OpenAI, Anthropic, Google, GitHub Copilot, Edge TTS, or any OpenAI-compatible endpoint
- Model discovery β
ListModelsfetches available models,Testchecks provider connectivity and model support - Tool calling β describe tools with
ToolDefinition(or infer the schema from a Go struct withNewToolDefinition[T]); the model's calls come back as typedToolCalls withToolArguments - Streaming β first-class channel-based streaming with fine-grained
StreamParttypes - Rich message types β text, images, files, reasoning content, tool calls/results
- Embeddings β generate embeddings with
Embed/EmbedMany, supports OpenAI and Google providers - Image generation β generate and edit images with
GenerateImage/EditImage, supports OpenAI (dall-e, gpt-image) and Alibaba Cloud DashScope (Qwen-Image, Wan) models - Video generation β create, poll, and download video jobs with OpenRouter and Ark/ModelArk providers
- Speech synthesis β generate speech with
GenerateSpeech/StreamSpeech, supports Edge TTS with an open provider model
go get github.com/felinics/twilightRequires Go 1.25+.
package main
import (
"context"
"fmt"
"log"
"github.com/felinics/twilight/provider/openai/completions"
"github.com/felinics/twilight/sdk"
)
func main() {
provider := completions.New(
completions.WithAPIKey("sk-..."),
)
model := provider.ChatModel("gpt-4o-mini")
result, err := model.Generate(context.Background(), sdk.Request{
Messages: []sdk.Message{
sdk.UserMessage("Explain Go channels in 3 sentences."),
},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Text)
}import "github.com/felinics/twilight/provider/openai/responses"
provider := responses.New(
responses.WithAPIKey("sk-..."),
)
model := provider.ChatModel("gpt-4o-mini")
result, err := model.Generate(context.Background(), sdk.Request{
Messages: []sdk.Message{
sdk.UserMessage("Explain Go channels in 3 sentences."),
},
})
fmt.Println(result.Text)The Responses API is OpenAI's newer API with first-class support for reasoning models (o3, o4-mini), URL citation annotations, and a flat input format. See Providers for details.
import "github.com/felinics/twilight/provider/anthropic/messages"
provider := messages.New(
messages.WithAPIKey("sk-ant-..."),
)
model := provider.ChatModel("claude-sonnet-4-20250514")
maxTokens := 1024
result, err := model.Generate(context.Background(), sdk.Request{
MaxTokens: &maxTokens,
Messages: []sdk.Message{
sdk.UserMessage("Explain Go channels in 3 sentences."),
},
})
fmt.Println(result.Text)For extended thinking (reasoning), configure the provider with WithThinking:
provider := messages.New(
messages.WithAPIKey("sk-ant-..."),
messages.WithThinking(messages.ThinkingConfig{
Type: "enabled",
BudgetTokens: 4000,
}),
)import "github.com/felinics/twilight/provider/google/generativeai"
provider := generativeai.New(
generativeai.WithAPIKey("AIza..."),
)
model := provider.ChatModel("gemini-2.5-flash")
result, err := model.Generate(context.Background(), sdk.Request{
Messages: []sdk.Message{
sdk.UserMessage("Explain Go channels in 3 sentences."),
},
})
fmt.Println(result.Text)import "github.com/felinics/twilight/provider/github/copilot"
provider := copilot.New(
// Use the inbound X-GitHub-Token value from your Copilot agent request.
copilot.WithGitHubToken("ghu_..."),
)
model := provider.ChatModel(copilot.AutoModel)
result, err := model.Generate(context.Background(), sdk.Request{
Messages: []sdk.Message{
sdk.UserMessage("Explain Go channels in 3 sentences."),
},
})
fmt.Println(result.Text)This provider targets GitHub Copilot agent / extension runtimes that can call api.githubcopilot.com/chat/completions. GitHub currently does not expose a public Copilot models discovery endpoint, so copilot.AutoModel tells the provider to let GitHub choose the backing model instead of inventing an undocumented model ID.
stream, err := model.Stream(ctx, sdk.Request{
Messages: []sdk.Message{
sdk.UserMessage("Write a haiku about concurrency."),
},
})
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)
}
}
// Once Parts is drained, the assembled result is available: text, usage,
// finish reason and any tool calls, identical to what Generate returns.
result, err := stream.Result()Describe the tool with a Go struct β the SDK infers the JSON Schema. The model asks for the call; the caller runs it and replays the step:
type WeatherParams struct {
City string `json:"city" jsonschema:"City name"`
}
weather, err := sdk.NewToolDefinition[WeatherParams]("get_weather", "Get current weather for a city")
if err != nil {
log.Fatal(err)
}
messages := []sdk.Message{sdk.UserMessage("What's the weather in Tokyo?")}
for {
result, err := model.Generate(ctx, sdk.Request{Messages: messages, Tools: []sdk.ToolDefinition{weather}})
if err != nil {
log.Fatal(err)
}
if len(result.ToolCalls) == 0 {
fmt.Println(result.Text)
break
}
var assistant []sdk.MessagePart
for _, rp := range result.ReasoningParts {
assistant = append(assistant, rp) // reasoning first, with the provider's tokens
}
if result.Text != "" {
assistant = append(assistant, sdk.TextPart{Text: result.Text, ProviderMetadata: result.TextProviderMetadata})
}
var results []sdk.ToolResultPart
for _, call := range result.ToolCalls {
assistant = append(assistant, sdk.ToolCallPart{ToolCallID: call.ToolCallID, ToolName: call.ToolName, Input: call.Input, ProviderMetadata: call.ProviderMetadata})
var params WeatherParams
if err := call.Input.Unmarshal(¶ms); err != nil { // not a JSON document: tell the model
results = append(results, sdk.ToolResultPart{ToolCallID: call.ToolCallID, ToolName: call.ToolName, Result: sdk.TextOutput(err.Error()), IsError: true})
continue
}
out, _ := sdk.JSONOutput(map[string]any{"city": params.City, "temp": "22Β°C"})
results = append(results, sdk.ToolResultPart{ToolCallID: call.ToolCallID, ToolName: call.ToolName, Result: out})
}
messages = append(messages, sdk.Message{Role: sdk.MessageRoleAssistant, Content: assistant}, sdk.ToolMessage(results...))
}Each iteration is one model call. See Tool Calling.
Generate images from text prompts using OpenAI's image models:
import "github.com/felinics/twilight/provider/openai/images"
provider := images.New(images.WithAPIKey("sk-..."))
model := provider.GenerationModel("gpt-image-1")
result, err := sdk.GenerateImage(ctx,
sdk.WithImageGenerationModel(model),
sdk.WithImagePrompt("A sunset over mountains, oil painting style"),
sdk.WithImageSize("1024x1024"),
)
// result.Data[0].B64JSON contains the base64-encoded imageEdit existing images with inpainting or extensions:
model := provider.EditModel("gpt-image-1")
result, err := sdk.EditImage(ctx,
sdk.WithImageEditModel(model),
sdk.WithEditPrompt("Add a rainbow in the sky"),
sdk.WithEditImages(sdk.ImageInput{
Data: pngBytes,
Filename: "photo.png",
}),
)Alibaba Cloud Model Studio (DashScope) image models work through the same API:
import "github.com/felinics/twilight/provider/alibabacloud/images"
provider := images.New(images.WithAPIKey("sk-..."))
model := provider.GenerationModel("qwen-image-max")
result, err := sdk.GenerateImage(ctx,
sdk.WithImageGenerationModel(model),
sdk.WithImagePrompt("A sunset over mountains, oil painting style"),
sdk.WithImageSize("1024x1024"),
)
// result.Data[0].URL contains the generated image URLThe DashScope provider routes Qwen-Image and Wan models to the right endpoint automatically and transparently polls async generation tasks. See Images for details.
Generate vector embeddings for text using OpenAI or Google:
import "github.com/felinics/twilight/provider/openai/embedding"
provider := embedding.New(embedding.WithAPIKey("sk-..."))
model := provider.EmbeddingModel("text-embedding-3-small")
// Single value
vec, err := sdk.Embed(ctx, "Hello world", sdk.WithEmbeddingModel(model))
// vec is []float64
// Multiple values
result, err := sdk.EmbedMany(ctx, []string{"Hello", "World"},
sdk.WithEmbeddingModel(model),
sdk.WithDimensions(256),
)
// result.Embeddings is [][]float64
// result.Usage.Tokens reports token consumptionGoogle Gemini embeddings:
import "github.com/felinics/twilight/provider/google/embedding"
provider := embedding.New(
embedding.WithAPIKey("AIza..."),
embedding.WithTaskType("RETRIEVAL_DOCUMENT"),
)
model := provider.EmbeddingModel("gemini-embedding-001")
vec, err := sdk.Embed(ctx, "Hello world", sdk.WithEmbeddingModel(model))Generate speech audio from text using Edge TTS (free, no API key required):
import "github.com/felinics/twilight/provider/edge/speech"
provider := speech.New()
model := provider.SpeechModel("edge-read-aloud")
// Generate complete audio
result, err := sdk.GenerateSpeech(ctx,
sdk.WithSpeechModel(model),
sdk.WithText("Hello, world!"),
sdk.WithSpeechConfig(map[string]any{
"voice": "en-US-EmmaMultilingualNeural",
"speed": 1.0,
}),
)
// result.Audio is []byte, result.ContentType is "audio/mpeg"Stream audio chunks for low-latency playback:
sr, err := sdk.StreamSpeech(ctx,
sdk.WithSpeechModel(model),
sdk.WithText("δ½ ε₯½οΌθΏζ―ζ΅εΌθ―ι³εζγ"),
sdk.WithSpeechConfig(map[string]any{
"voice": "zh-CN-XiaoxiaoNeural",
}),
)
for chunk := range sr.Stream {
// write chunk to audio player or file
}Test connectivity and discover available models before making generation requests:
provider := completions.New(completions.WithAPIKey("sk-..."))
// Check provider connectivity
result := provider.Test(context.Background())
switch result.Status {
case sdk.ProviderStatusOK:
fmt.Println("Provider is healthy")
case sdk.ProviderStatusUnhealthy:
fmt.Println("Connected but unhealthy:", result.Message)
case sdk.ProviderStatusUnreachable:
fmt.Println("Cannot connect:", result.Message)
}
// List all available models
models, err := provider.ListModels(context.Background())
for _, m := range models {
fmt.Println(m.ID)
}
// Check if a specific model is supported
model := provider.ChatModel("gpt-4o")
testResult, err := model.Test(context.Background())
if testResult.Supported {
fmt.Println("Model is supported")
}| Document | Description |
|---|---|
| Getting Started | Installation, setup, and first request |
| Providers | Provider interface, OpenAI, Anthropic, and Google Gemini |
| Images | Generate and edit images with OpenAI and Alibaba Cloud DashScope image models |
| Embeddings | Generate vector embeddings with OpenAI and Google |
| Speech | Speech synthesis with Edge TTS and custom providers |
| Tool Calling | Tool definitions, typed arguments and outputs, replaying a step |
| Streaming | Model.Stream, the ModelStream and its StreamPart types |
| API Reference | Complete type and function reference |
| Provider | Constructor | API | Status |
|---|---|---|---|
| OpenAI Chat Completions | completions.New() |
/chat/completions |
β Stable |
| OpenAI Responses | responses.New() |
/responses |
β Stable |
| OpenAI Codex | codex.New() |
/codex/responses |
β Stable |
| OpenAI-compatible (DeepSeek, Groq, etc.) | completions.New() + WithBaseURL |
/chat/completions |
β Stable |
| OpenRouter Responses | responses.New() + WithBaseURL |
/responses |
β Stable |
| Anthropic | messages.New() |
/messages |
β Stable |
| Google Gemini | generativeai.New() |
Generative AI API | β Stable |
| OpenAI Images | images.New() |
/images/generations, /images/edits |
β Stable |
| Alibaba Cloud DashScope Images | images.New() |
DashScope text2image / multimodal-generation | β Stable |
| OpenAI Embeddings | embedding.New() |
/embeddings |
β Stable |
| Google Embeddings | embedding.New() |
embedContent / batchEmbedContents |
β Stable |
| Edge TTS | speech.New() |
Bing WebSocket | β Stable |
| OpenAI / compatible TTS | speech.New() |
/audio/speech |
β Stable |
| Deepgram TTS | speech.New() |
/v1/speak |
β Stable |
| ElevenLabs TTS | speech.New() |
/v1/text-to-speech/{voice_id} |
β Stable |
| MiniMax TTS | speech.New() |
/v1/t2a_v2 |
β Stable |
| MiMo TTS | speech.New() |
/chat/completions + audio output |
β Stable |
| Alibaba Cloud CosyVoice | speech.New() |
DashScope WebSocket | β Stable |
| Volcengine SAMI TTS | speech.New() |
/api/v1/invoke |
β Stable |