Skip to content

Latest commit

Β 

History

97 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Twilight AI

A lightweight, idiomatic AI SDK for Go β€” inspired by Vercel AI SDK.

Go Reference License

Features

  • One call, one result β€” Model.Generate and Model.Stream take an sdk.Request and return a ModelResult or a stream of typed parts. Embed, EmbedMany, GenerateImage, EditImage, GenerateVideo, GenerateSpeech and StreamSpeech cover the other modalities
  • Provider-agnostic β€” swap between OpenAI, Anthropic, Google, GitHub Copilot, Edge TTS, or any OpenAI-compatible endpoint
  • Model discovery β€” ListModels fetches available models, Test checks provider connectivity and model support
  • Tool calling β€” describe tools with ToolDefinition (or infer the schema from a Go struct with NewToolDefinition[T]); the model's calls come back as typed ToolCalls with ToolArguments
  • Streaming β€” first-class channel-based streaming with fine-grained StreamPart types
  • 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

Installation

go get github.com/felinics/twilight

Requires Go 1.25+.

Quick Start

Generate Text (Chat Completions API)

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)
}

Generate Text (Responses API)

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.

Anthropic

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,
    }),
)

Google Gemini

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)

GitHub Copilot Agent

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 Text

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()

Tool Calling

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(&params); 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.

Image Generation

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 image

Edit 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 URL

The DashScope provider routes Qwen-Image and Wan models to the right endpoint automatically and transparently polls async generation tasks. See Images for details.

Embeddings

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 consumption

Google 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))

Speech Synthesis

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
}

Provider Health Check & Model Discovery

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")
}

Documentation

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

Supported Providers

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

License

Apache License 2.0

About

πŸŒ… A lightweight, idiomatic AI SDK for Go

Resources

Stars

48 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages