Repository navigation
Media
Beyond chat, model abilities are optional interfaces on the client: llm.ImageGenerator, llm.SpeechGenerator, llm.VideoGenerator, llm.Decider. Code that holds an llm.Client type-asserts; *openrouter.Client and *llmtest.StubClient implement all four, *codex.Client none. Transcription runs on any client whose provider accepts audio input. Every snippet below is from llm/example_test.go, where a StubClient stands in for OpenRouter.
Two paths, by model:
-
Images endpoint (
POST /api/v1/images):openai/gpt-image-2.5-flareand friends answer only there and are missing fromGET /models. UseGenerateImage. -
Chat completions: models that answer there (the Gemini image family,
openai/gpt-5.4-image-2) useCompletewithModalities: []string{"image", "text"}, the only path that also carries a conversation. NotStream: one base64 image exceeds the SSE line cap.
From ExampleImageGenerator:
var model llm.Client = client
generator, ok := model.(llm.ImageGenerator)
if !ok {
fmt.Println("no images endpoint")
return
}
response, err := generator.GenerateImage(context.Background(), llm.ImageRequest{
Model: "openai/gpt-image-2.5-flare",
Prompt: "A pixel-art fox in tall grass.",
AspectRatio: "16:9",
ImageSize: "2K",
Quality: "high",
})From ExampleRequest_imageOutput:
response, err := client.Complete(context.Background(), llm.Request{
Model: "google/gemini-3.1-flash-image-preview",
Messages: []llm.Message{{Role: llm.RoleUser, Content: "A pixel-art fox in tall grass."}},
Modalities: []string{"image", "text"},
ImageConfiguration: &llm.ImageConfiguration{AspectRatio: "16:9", ImageSize: "2K"},
})-
ImageRequest:AspectRatio(1:1,3:2,16:9, ...; OpenAI images models refuse4:5),ImageSize(1K,2K,4K), orSizeas explicit"WIDTHxHEIGHT"(both sides divisible by 16, long edge at most 3840 on OpenAI) instead of the two;Qualityautotomax(about 16x the cost ofautoon OpenAI);Referencesare input images asdata:orhttpsURLs. -
ImagePart.Dataholds inline bytes (decoded from adata:URI),URLa remote link. Agent runs surface images asEventImage, andDrainputs them on the assistant message. - Complete program: examples/image-generation; the CLI picks the path and frame for you.
GenerateVideo submits a job, polls it and downloads the clip in one blocking call, minutes on a real provider; bound it with ctx. From ExampleVideoGenerator:
// The call blocks until the clip is downloaded, minutes on a real
// provider: bound it with the context.
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
withoutAudio := false
response, err := generator.GenerateVideo(ctx, llm.VideoRequest{
Model: "google/veo-3.1-lite",
Prompt: "A paper boat drifts across a calm pond at sunrise, static camera.",
AspectRatio: "16:9",
Resolution: "720p",
DurationSeconds: 4,
GenerateAudio: &withoutAudio,
})Every model accepts a closed list of durations, resolutions, ratios and sizes and rejects the rest; openrouter.Client.VideoModels(ctx) lists them, and unset fields stay off the wire. FirstFrame/LastFrame anchor the clip, References guide style. VideoResponse has Video, ContentType (sniffed), JobID (also set on errors after the submit) and Usage.CostUSD. A refused prompt, and a job that completes without a clip (Veo's silent rejection), is llm.ErrContentFiltered.
Speak returns the whole clip; OpenRouter does not stream it, so speak sentence by sentence for early playback (Voice does). From ExampleSpeechGenerator:
response, err := speaker.Speak(context.Background(), llm.SpeechRequest{
Model: "elevenlabs/eleven-v4-turbo",
Input: "The shower goes by the window.",
Voice: "sarah",
Format: "mp3",
})Format is mp3 (audio/mpeg) or pcm (16-bit little-endian mono, OpenRouter's default). OpenRouter reports no cost on speech: SpeechResponse has no Usage, the price is on GET /api/v1/generation?id=<GenerationID> some seconds later, or computed per character (Voice).
llm.Transcribe(ctx, client, llm.TranscribeRequest{...}) turns a clip into text with one chat call on client.Complete, temperature 0, default model llm.DefaultTranscribeModel (google/gemini-3.1-flash-lite). Format is the container (wav, mp3, m4a, webm, ogg, flac, aiff); llm.AudioFormat("audio/webm;codecs=opus") derives it from a browser MIME type. A clip without speech returns "", not an error. TranscribeDetailed returns Raw and corrected Text. From ExampleTranscribe:
text, _, err := llm.Transcribe(context.Background(), client, llm.TranscribeRequest{
Audio: []byte("RIFF...."),
Format: "wav",
Hint: "The speaker speaks German.",
Vocabulary: &llm.Vocabulary{Terms: []string{"Waschtisch"}},
})Keep example sentences and amounts out of Hint: for a silent clip the model repeats them as the transcript.
llm.Vocabulary fixes words a speech model gets wrong (brand names, article numbers, trade words): Terms go into the prompt (at most llm.MaximumVocabularyPromptTerms, 200, and MaximumVocabularyPromptBytes, 3000), then Correct rewrites the transcript: Replacements (heard variant to term), exact term matches ignoring spaces, punctuation and case, and with Fuzzy near misses of terms with at least 6 letters (edit distance 1, 2 from 9 letters on, same first letter, every heard word at least 4 letters, so short common words are never touched). From ExampleVocabulary_Correct:
vocabulary := &llm.Vocabulary{
Terms: []string{"LIV60G", "Duschtasse"},
Replacements: map[string]string{"pfeifer und mai": "PFEIFFER & MAY"},
Fuzzy: true,
}
fmt.Println(vocabulary.Correct("Die Duschtase LIV 60 g von Pfeifer und Mai."))
// Output:
// Die Duschtasse LIV60G von PFEIFFER & MAY.llm/codex rejects audio.
Decision models (typesafe/jev-1.14, perplexity/pplx-decider-v1.2, ...) answer typed questions about a state with probabilities instead of prose, on OpenRouter's alpha Decisions API; chat completions rejects them. From ExampleDecider:
var model llm.Client = client
decider, ok := model.(llm.Decider)
if !ok {
fmt.Println("no decisions endpoint")
return
}
response, err := decider.Decide(context.Background(), llm.DecisionRequest{
Model: "typesafe/jev-1.14",
State: "WIN A FREE CRUISE!!! Click here.",
Questions: map[string]llm.DecisionQuestion{"spam": llm.NoulQuestion("Is this message spam?")},
})- Questions:
llm.NoulQuestion(yes/no, answerNoul= probability of yes),llm.ChoiceQuestion(instructions, options)(Choice,Probabilities,Confidence),llm.ScoreQuestion(instructions, levels...)(Score, 0 = first level,Legend),llm.BoxQuestion(aBoxin fractions of the image; always returns one, so pair it with a noul on presence). -
Stateis a string or any JSON-marshalable value; images go into a[]anystate asllm.ImagePartvalues next to text. - A question left unanswered is
llm.ErrServerError, returned with the answers that did arrive. Provider routing applies.openrouter.Client.DecisionModels(ctx)lists the models.
Mirror of docs/wiki at v1.2.0 (dd94d84). Edit the repository, not this wiki.