Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ not statically link the other vendors' SDKs:
|---|---|---|---|
| OpenAI | `pkg/llm/openai` | `openai.New(key, model)` | gpt-4o, gpt-4o-mini, o1, ... |
| Anthropic | `pkg/llm/anthropic` | `anthropic.New(key, model)` | claude-sonnet, claude-opus, ... |
| Google Gemini | `pkg/llm/gemini` | `gemini.New(key, model)` | gemini-2.5-flash, gemini-2.5-pro, ... |
| Google Gemini | `pkg/llm/gemini` | `gemini.New(key, model)` | gemini-3.8-flash (default), gemini-2.5-pro, ... |
| Vertex AI (Gemini) | `pkg/llm/gemini` | `gemini.NewVertex(project, location, model)` | Vertex-hosted Gemini via ADC |
| OpenAI-compatible | `pkg/llm/openai` | `openai.NewCompat(key, model, baseURL)` | OpenRouter, Ollama, Groq, vLLM, Together, ... |

Expand Down
2 changes: 1 addition & 1 deletion examples/demo/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ ANTHROPIC_API_KEY=sk-ant-...
ANTHROPIC_MODEL=claude-sonnet-4-5

GEMINI_API_KEY=AIza...
GEMINI_MODEL=gemini-2.5-flash
GEMINI_MODEL=gemini-3.8-flash

# ── Tools ────────────────────────────────────────────────────────────────────
TAVILY_API_KEY=tvly-...
Expand Down
3 changes: 3 additions & 0 deletions pkg/audio/audio.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ var extByMIME = map[string]string{
"audio/opus": "ogg",
"audio/flac": "flac",
"audio/x-flac": "flac",
"audio/aac": "aac",
"audio/x-aac": "aac",
"audio/amr": "amr",
}

// Ext returns the file extension for a media type, or "" when the type is not
Expand Down
3 changes: 3 additions & 0 deletions pkg/audio/audio_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ func TestExtStripsParametersAndCase(t *testing.T) {
{"audio/x-m4a", "m4a"},
{"audio/flac", "flac"},
{"audio/opus", "ogg"},
{"audio/aac", "aac"},
{"audio/x-aac", "aac"},
{"audio/amr", "amr"},
{"text/plain", ""},
{"", ""},
} {
Expand Down
28 changes: 28 additions & 0 deletions pkg/llm/gemini/default_model_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package gemini

import "testing"

// A retired default fails every fresh signup on its first call while old keys
// keep passing, so pin the value and check every constructor honors it.
func TestConstructorsUseDefaultModel(t *testing.T) {
if defaultModel != "gemini-3.8-flash" {
t.Fatalf("defaultModel = %q, want gemini-3.8-flash", defaultModel)
}
p, err := New("test-key", "")
if err != nil {
t.Fatalf("New: %v", err)
}
tr, err := NewTranscriber("test-key", "")
if err != nil {
t.Fatalf("NewTranscriber: %v", err)
}
ma, err := NewMediaAnalyzer("test-key", "")
if err != nil {
t.Fatalf("NewMediaAnalyzer: %v", err)
}
for name, got := range map[string]string{"New": p.model, "NewTranscriber": tr.model, "NewMediaAnalyzer": ma.model} {
if got != defaultModel {
t.Fatalf("%s model = %q, want %q", name, got, defaultModel)
}
}
}
10 changes: 8 additions & 2 deletions pkg/llm/gemini/gemini.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,13 @@ func WithSeed(n int64) Option {
return func(p *Provider) { p.seed = &n }
}

// New creates a wrapper over Google Gemini's API.
// defaultModel is the model every constructor in this package uses when
// given "". gemini-2.5-flash is rejected with a 404 for API keys created after
// its retirement, so a fresh signup would fail on its first call.
const defaultModel = "gemini-3.8-flash"

// New creates a wrapper over Google Gemini's API. Empty model defaults to
// "gemini-3.8-flash".
func New(apiKey string, model string, opts ...Option) (*Provider, error) {
if apiKey == "" {
apiKey = os.Getenv("GEMINI_API_KEY")
Expand All @@ -65,7 +71,7 @@ func New(apiKey string, model string, opts ...Option) (*Provider, error) {
return nil, errors.New("GEMINI_API_KEY is not set in environment")
}
if model == "" {
model = "gemini-2.5-flash"
model = defaultModel
}

client, err := genai.NewClient(context.Background(), &genai.ClientConfig{
Expand Down
4 changes: 2 additions & 2 deletions pkg/llm/gemini/gemini_vision.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type MediaAnalyzer struct {
}

// NewMediaAnalyzer builds an analyzer. apiKey defaults to GEMINI_API_KEY;
// model defaults to "gemini-2.5-flash".
// model defaults to "gemini-3.8-flash".
func NewMediaAnalyzer(apiKey, model string) (*MediaAnalyzer, error) {
if apiKey == "" {
apiKey = os.Getenv("GEMINI_API_KEY")
Expand All @@ -29,7 +29,7 @@ func NewMediaAnalyzer(apiKey, model string) (*MediaAnalyzer, error) {
return nil, fmt.Errorf("gemini: GEMINI_API_KEY not set")
}
if model == "" {
model = "gemini-2.5-flash"
model = defaultModel
}
client, err := genai.NewClient(context.Background(), &genai.ClientConfig{
APIKey: apiKey,
Expand Down
4 changes: 2 additions & 2 deletions pkg/llm/gemini/transcriber.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ type Transcriber struct {
var _ audio.Transcriber = (*Transcriber)(nil)

// NewTranscriber builds a transcriber. apiKey defaults to GEMINI_API_KEY;
// model defaults to "gemini-2.5-flash".
// model defaults to "gemini-3.8-flash".
func NewTranscriber(apiKey, model string) (*Transcriber, error) {
if apiKey == "" {
apiKey = os.Getenv("GEMINI_API_KEY")
Expand All @@ -56,7 +56,7 @@ func NewTranscriber(apiKey, model string) (*Transcriber, error) {
return nil, fmt.Errorf("gemini: NewTranscriber: GEMINI_API_KEY not set")
}
if model == "" {
model = "gemini-2.5-flash"
model = defaultModel
}
client, err := genai.NewClient(context.Background(), &genai.ClientConfig{APIKey: apiKey})
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions pkg/llm/gemini/vertex.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
//
// Empty projectID / location fall back to GOOGLE_CLOUD_PROJECT and
// GOOGLE_CLOUD_LOCATION respectively. Empty model defaults to
// "gemini-2.5-flash".
// "gemini-3.8-flash".
//
// The returned *Provider reuses the same GenerateStream implementation
// as the public-API backend — only the transport differs.
Expand All @@ -35,7 +35,7 @@ func NewVertex(projectID, location, model string, opts ...Option) (*Provider, er
location = "us-central1"
}
if model == "" {
model = "gemini-2.5-flash"
model = defaultModel
}

client, err := genai.NewClient(context.Background(), &genai.ClientConfig{
Expand Down
9 changes: 9 additions & 0 deletions pkg/llm/openai/transcriber.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ import (
// instead of after transferring the payload.
const maxClipBytes = 25 << 20

// unsupportedExts are containers pkg/audio accepts (Gemini decodes them) but
// the OpenAI audio endpoint does not list. Rejecting them here fails fast with
// ErrUnsupportedFormat instead of after a full upload.
var unsupportedExts = map[string]bool{"aac": true, "amr": true}

// Transcriber implements audio.Transcriber against OpenAI's audio
// transcription endpoint.
//
Expand Down Expand Up @@ -52,6 +57,10 @@ func (t *Transcriber) Transcribe(ctx context.Context, clip audio.Clip, opts audi
if err := clip.Validate(); err != nil {
return audio.Transcript{}, fmt.Errorf("openai: Transcriber: %w", err)
}
if ext := audio.Ext(clip.MIME); unsupportedExts[ext] {
return audio.Transcript{}, fmt.Errorf("openai: Transcriber: %w: %s is not accepted by the OpenAI audio endpoint",
audio.ErrUnsupportedFormat, ext)
}
if len(clip.Data) > maxClipBytes {
return audio.Transcript{}, fmt.Errorf("openai: Transcriber: %w: %d bytes exceeds %d",
audio.ErrTooLarge, len(clip.Data), maxClipBytes)
Expand Down
2 changes: 2 additions & 0 deletions pkg/llm/openai/transcriber_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ func TestTranscribeRejectsInvalidClip(t *testing.T) {
}{
{"empty", audio.Clip{MIME: "audio/wav"}, audio.ErrNoAudio},
{"unsupported", audio.Clip{MIME: "application/pdf", Data: []byte("x")}, audio.ErrUnsupportedFormat},
{"aac not accepted by endpoint", audio.Clip{MIME: "audio/aac", Data: []byte("x")}, audio.ErrUnsupportedFormat},
{"amr not accepted by endpoint", audio.Clip{MIME: "audio/amr", Data: []byte("x")}, audio.ErrUnsupportedFormat},
} {
t.Run(tc.name, func(t *testing.T) {
if _, err := tr.Transcribe(context.Background(), tc.clip, audio.Options{}); !errors.Is(err, tc.want) {
Expand Down
Loading