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
10 changes: 10 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,16 @@

# Google Gemini
# GEMINI_API_KEY=...
# Use Gemini's native generateContent API for chat/responses (default: true).
# Set to false to use Gemini's OpenAI-compatible API for chat/responses.
# Native mode supports inline image data via data: URLs, but GoModel does not
# fetch remote image URLs or upload them through Gemini Files API yet. Set this
# to false when you need OpenAI-compatible image_url pass-through behavior.
# USE_GOOGLE_GEMINI_NATIVE_API=true
# Gemini base URL. The official defaults are:
# - native chat/models: https://generativelanguage.googleapis.com/v1beta
# - OpenAI-compatible API: https://generativelanguage.googleapis.com/v1beta/openai
# If this ends in /openai, GoModel derives the native base by stripping /openai.
# GEMINI_BASE_URL=https://generativelanguage.googleapis.com/v1beta/openai

# xAI (Grok)
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,5 +122,5 @@ Full reference: `.env.template` and `config/config.yaml`
- **Resilience:** Configured via `config/config.yaml` - global `resilience.retry.*` and `resilience.circuit_breaker.*` defaults with optional per-provider overrides under `providers.<name>.resilience.retry.*` and `providers.<name>.resilience.circuit_breaker.*`. Retry defaults: `max_retries` (3), `initial_backoff` (1s), `max_backoff` (30s), `backoff_factor` (2.0), `jitter_factor` (0.1). Circuit breaker defaults: `failure_threshold` (5), `success_threshold` (2), `timeout` (30s)
- **Metrics:** `METRICS_ENABLED` (false), `METRICS_ENDPOINT` (/metrics)
- **Guardrails:** Configured via `config/config.yaml` only (except `GUARDRAILS_ENABLED` env var)
- **Providers:** `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, `XAI_API_KEY`, `GROQ_API_KEY`, `OPENROUTER_API_KEY`, `ZAI_API_KEY`, `ZAI_BASE_URL` (optional Z.ai endpoint override), `MINIMAX_API_KEY`, `MINIMAX_BASE_URL` (optional MiniMax endpoint override), `AZURE_API_KEY`, `AZURE_BASE_URL` (Azure OpenAI deployment base URL), `AZURE_API_VERSION` (optional Azure API version), `ORACLE_API_KEY` (Oracle API key), `ORACLE_BASE_URL` (Oracle OpenAI-compatible base URL), `<PROVIDER>[_SUFFIX]_MODELS` (comma-separated configured model list for any provider type), `OLLAMA_BASE_URL`, `VLLM_BASE_URL`, `VLLM_API_KEY` (optional upstream vLLM bearer token)
- **Providers:** `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, `USE_GOOGLE_GEMINI_NATIVE_API` (true by default; false uses Gemini's OpenAI-compatible chat API), `XAI_API_KEY`, `GROQ_API_KEY`, `OPENROUTER_API_KEY`, `ZAI_API_KEY`, `ZAI_BASE_URL` (optional Z.ai endpoint override), `MINIMAX_API_KEY`, `MINIMAX_BASE_URL` (optional MiniMax endpoint override), `AZURE_API_KEY`, `AZURE_BASE_URL` (Azure OpenAI deployment base URL), `AZURE_API_VERSION` (optional Azure API version), `ORACLE_API_KEY` (Oracle API key), `ORACLE_BASE_URL` (Oracle OpenAI-compatible base URL), `<PROVIDER>[_SUFFIX]_MODELS` (comma-separated configured model list for any provider type), `OLLAMA_BASE_URL`, `VLLM_BASE_URL`, `VLLM_API_KEY` (optional upstream vLLM bearer token)
- **Provider model metadata:** `providers.<name>.models` accepts either model IDs (strings) or `{id, metadata}` objects. When `metadata` is supplied (`display_name`, `context_window`, `max_output_tokens`, `modes`, `capabilities`, `pricing`, …) it is merged onto the remote ai-model-list entry during enrichment, with operator values winning per-field. Primary use case: advertising context windows, capabilities, and pricing for local models (Ollama) and other custom endpoints whose IDs are not in the upstream registry.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ Key settings:
| `ENABLE_PASSTHROUGH_ROUTES` | `true` | Enable provider-native passthrough routes under `/p/{provider}/...` |
| `ALLOW_PASSTHROUGH_V1_ALIAS` | `true` | Allow `/p/{provider}/v1/...` aliases while keeping `/p/{provider}/...` canonical |
| `ENABLED_PASSTHROUGH_PROVIDERS` | `openai,anthropic,openrouter,zai,vllm` | Comma-separated list of enabled passthrough providers |
| `USE_GOOGLE_GEMINI_NATIVE_API` | `true` | Use Gemini native `generateContent` for chat/responses; set `false` for Gemini's OpenAI-compatible API and image_url pass-through behavior |
| `STORAGE_TYPE` | `sqlite` | Storage backend (`sqlite`, `postgresql`, `mongodb`) |
| `METRICS_ENABLED` | `false` | Enable Prometheus metrics (experimental) |
| `LOGGING_ENABLED` | `false` | Enable audit logging |
Expand Down
3 changes: 3 additions & 0 deletions config/config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,9 @@ providers:
gemini:
type: gemini
api_key: "..."
# Chat/responses use Gemini's native generateContent API by default.
# Set USE_GOOGLE_GEMINI_NATIVE_API=false to use Gemini's OpenAI-compatible API instead.
# Native mode accepts image data URLs but does not fetch remote image URLs yet.

xai:
type: xai
Expand Down
15 changes: 11 additions & 4 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,23 @@
"icon": "compass",
"pages": [
"guides/openclaw",
"guides/oracle",
"guides/deepseek",
"guides/vllm",
"guides/multiple-ollama",
"guides/claude-code",
"guides/codex",
"guides/opencode-and-other-agents",
"guides/prometheus-metrics"
]
},
{
"tab": "Providers",
"icon": "plug",
"pages": [
"guides/gemini",
"guides/oracle",
"guides/deepseek",
"guides/vllm",
"guides/multiple-ollama"
]
},
{
"tab": "API Reference",
"icon": "braces",
Expand Down
111 changes: 111 additions & 0 deletions docs/guides/gemini.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
---
title: "GoModel & Google Gemini"
description: "Configure Google Gemini in GoModel, choose native or OpenAI-compatible routing, and understand image_url behavior."
icon: "sparkles"
---

GoModel routes Gemini chat and Responses API requests through Gemini's native
`generateContent` API by default. You can switch those requests back to
Gemini's OpenAI-compatible API when you need compatibility behavior that the
native adapter does not implement yet.

## Configure Gemini

Env-only configuration is enough:

```bash
export GEMINI_API_KEY="..."
```

Or in `config.yaml`:

```yaml
providers:
gemini:
type: gemini
api_key: "${GEMINI_API_KEY}"
```

## Native versus OpenAI-compatible mode

Gemini native mode is enabled by default:

```bash
export USE_GOOGLE_GEMINI_NATIVE_API=true
```

Set it to `false` to route chat and Responses API requests through Gemini's
OpenAI-compatible `/chat/completions` endpoint:

```bash
export USE_GOOGLE_GEMINI_NATIVE_API=false
```

`GEMINI_BASE_URL` configures the Gemini base. GoModel keeps separate internal
bases for native Gemini and the OpenAI-compatible API:

- native chat/models default: `https://generativelanguage.googleapis.com/v1beta`
- OpenAI-compatible default: `https://generativelanguage.googleapis.com/v1beta/openai`

When `GEMINI_BASE_URL` ends in `/openai`, GoModel uses that value for the
OpenAI-compatible client and derives the native base by stripping `/openai`.
Gemini embeddings, files, and batches still use the OpenAI-compatible surface.

```bash
export GEMINI_BASE_URL="https://generativelanguage.googleapis.com/v1beta/openai"
```

<Note>
`USE_GOOGLE_GEMINI_NATIVE_API` decides whether chat and Responses API calls
use native Gemini or the OpenAI-compatible API. `GEMINI_BASE_URL` only
configures the upstream base URLs.
</Note>

## Image URL behavior

Gemini models support image input, but the two GoModel routing modes handle
OpenAI-style `image_url` values differently.

In native mode, GoModel converts OpenAI-compatible messages to Gemini
`generateContent` requests. That adapter currently supports inline image data
only:

```json
{
"type": "image_url",
"image_url": {
"url": "data:image/jpeg;base64,..."
}
}
```

Remote image URLs such as `https://example.com/image.png` are rejected in native
mode. Google's native Gemini API supports inline image data and Files API
references; for URL-hosted images, Google's examples fetch the URL first and
send the bytes to `generateContent`.

Set `USE_GOOGLE_GEMINI_NATIVE_API=false` when you need GoModel to pass the
OpenAI-compatible `image_url` request shape through to Gemini's
OpenAI-compatible endpoint instead. Google documents image input for that
endpoint using the OpenAI `image_url` field.

## Current support

Integrated:

- chat completions and streaming
- Responses API and streaming
- model listing through Gemini's native `/models`
- usage metadata normalization for native responses
- tool calls and function-call results
- inline image data via `data:` URLs in native mode

Not integrated in native mode yet:

- fetching remote `image_url` values
- uploading remote images through the Gemini Files API before a chat request

References:

- [Gemini image understanding](https://ai.google.dev/gemini-api/docs/image-understanding)
- [Gemini OpenAI compatibility](https://ai.google.dev/gemini-api/docs/openai)
1 change: 1 addition & 0 deletions helm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ helm install gomodel ./helm \
| `providers.openai.enabled` | Enable OpenAI | `false` |
| `providers.anthropic.enabled` | Enable Anthropic | `false` |
| `providers.gemini.enabled` | Enable Gemini | `false` |
| `providers.gemini.useNativeApi` | Use Gemini native generateContent for chat/responses; set false for Gemini OpenAI compatibility | `true` |
| `providers.groq.enabled` | Enable Groq | `false` |
| `providers.xai.enabled` | Enable xAI | `false` |
| `providers.zai.enabled` | Enable Z.ai | `false` |
Expand Down
4 changes: 4 additions & 0 deletions helm/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ Generate provider environment variables for the Deployment.
value: {{ $config.baseUrl | quote }}
{{- end }}
{{- end }}
{{- if and (eq $name "gemini") (hasKey $config "useNativeApi") }}
- name: USE_GOOGLE_GEMINI_NATIVE_API
value: {{ $config.useNativeApi | quote }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
1 change: 1 addition & 0 deletions helm/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@
"properties": {
"enabled": { "type": "boolean" },
"apiKey": { "type": "string" },
"useNativeApi": { "type": "boolean" },
"baseUrl": { "type": "string" }
}
},
Expand Down
2 changes: 2 additions & 0 deletions helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ providers:
enabled: false
# -- Gemini API key (ignored if providers.existingSecret is set)
apiKey: ""
# -- Use Gemini native generateContent API for chat/responses. Set false to use Gemini's OpenAI-compatible API.
useNativeApi: true
# -- Optional: Override Gemini base URL
baseUrl: ""

Expand Down
2 changes: 2 additions & 0 deletions internal/core/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ type ModelPricing struct {

// ModelPricingTier represents a volume-based pricing tier.
type ModelPricingTier struct {
UpToTokens *float64 `json:"up_to_tokens,omitempty" yaml:"up_to_tokens,omitempty"`
UpToMtok *float64 `json:"up_to_mtok,omitempty" yaml:"up_to_mtok,omitempty"`
InputPerMtok *float64 `json:"input_per_mtok,omitempty" yaml:"input_per_mtok,omitempty"`
OutputPerMtok *float64 `json:"output_per_mtok,omitempty" yaml:"output_per_mtok,omitempty"`
Expand Down Expand Up @@ -318,6 +319,7 @@ func (p *ModelPricing) Clone() *ModelPricing {
tiers := make([]ModelPricingTier, len(p.Tiers))
for i, t := range p.Tiers {
tiers[i] = ModelPricingTier{
UpToTokens: cloneFloatPtr(t.UpToTokens),
UpToMtok: cloneFloatPtr(t.UpToMtok),
InputPerMtok: cloneFloatPtr(t.InputPerMtok),
OutputPerMtok: cloneFloatPtr(t.OutputPerMtok),
Expand Down
36 changes: 36 additions & 0 deletions internal/core/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,42 @@ import (
"testing"
)

func TestModelPricingTierUnmarshalUpToTokens(t *testing.T) {
var pricing ModelPricing
if err := json.Unmarshal([]byte(`{
"currency": "USD",
"tiers": [
{"up_to_tokens": 200000, "input_per_mtok": 1.25, "output_per_mtok": 10.0}
]
}`), &pricing); err != nil {
t.Fatalf("json.Unmarshal() error = %v, want nil", err)
}
if len(pricing.Tiers) != 1 {
t.Fatalf("len(Tiers) = %d, want 1", len(pricing.Tiers))
}
if pricing.Tiers[0].UpToTokens == nil || *pricing.Tiers[0].UpToTokens != 200000 {
t.Fatalf("UpToTokens = %#v, want 200000", pricing.Tiers[0].UpToTokens)
}

cloned := pricing.Clone()
if cloned == nil {
t.Fatal("Clone() = nil, want pricing copy")
}
if len(cloned.Tiers) != 1 {
t.Fatalf("len(cloned.Tiers) = %d, want 1", len(cloned.Tiers))
}
if cloned.Tiers[0].UpToTokens == nil || *cloned.Tiers[0].UpToTokens != 200000 {
t.Fatalf("cloned UpToTokens = %#v, want 200000", cloned.Tiers[0].UpToTokens)
}
if cloned.Tiers[0].UpToTokens == pricing.Tiers[0].UpToTokens {
t.Fatal("Clone() reused UpToTokens pointer, want deep copy")
}
*pricing.Tiers[0].UpToTokens = 123
if *cloned.Tiers[0].UpToTokens != 200000 {
t.Fatalf("cloned UpToTokens after source mutation = %v, want 200000", *cloned.Tiers[0].UpToTokens)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func TestMessageUnmarshalJSON_AllowsNullContent(t *testing.T) {
payload := []byte(`{
"role":"assistant",
Expand Down
Loading
Loading