Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ members = [
"crates/aisix-gateway",
"crates/aisix-provider-openai",
"crates/aisix-provider-anthropic",
"crates/aisix-provider-gemini",
"crates/aisix-provider-google",
"crates/aisix-provider-deepseek",
"crates/aisix-proxy",
"crates/aisix-admin",
Expand Down
2 changes: 1 addition & 1 deletion crates/aisix-admin/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ const OPENAPI_JSON: &str = r##"{
"required": ["display_name"],
"properties": {
"display_name": {"type": "string", "example": "my-gpt4"},
"provider": {"type": "string", "enum": ["openai","anthropic","gemini","deepseek"]},
"provider": {"type": "string", "enum": ["openai","anthropic","google","deepseek"]},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
Comment thread
jarvis9443 marked this conversation as resolved.
Resolved
"model_name": {"type": "string", "example": "gpt-4o"},
"provider_key_id": {"type": "string", "example": "11111111-1111-1111-1111-111111111111"},
"timeout": {"type": "integer", "minimum": 0, "description": "Request timeout in milliseconds. Absent or 0 = no timeout."},
Expand Down
8 changes: 4 additions & 4 deletions crates/aisix-core/src/models/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use crate::resource::Resource;
pub enum Provider {
Openai,
Anthropic,
Comment thread
jarvis9443 marked this conversation as resolved.
Resolved
Gemini,
Google,
Deepseek,
/// Cohere — currently exposed for `/v1/rerank` only (#213 Phase 1).
/// Cohere's chat / generate APIs are not OpenAI-compatible; a
Expand All @@ -43,7 +43,7 @@ impl Provider {
match self {
Self::Openai => "https://api.openai.com",
Self::Anthropic => "https://api.anthropic.com",
Self::Gemini => "https://generativelanguage.googleapis.com/v1beta/openai",
Self::Google => "https://generativelanguage.googleapis.com/v1beta/openai",
Self::Deepseek => "https://api.deepseek.com",
Self::Cohere => "https://api.cohere.com",
Self::Jina => "https://api.jina.ai",
Expand All @@ -54,7 +54,7 @@ impl Provider {
match self {
Self::Openai => "openai",
Self::Anthropic => "anthropic",
Self::Gemini => "gemini",
Self::Google => "google",
Self::Deepseek => "deepseek",
Self::Cohere => "cohere",
Self::Jina => "jina",
Expand Down Expand Up @@ -447,7 +447,7 @@ mod tests {
"https://api.anthropic.com"
);
assert_eq!(
Provider::Gemini.default_base_url(),
Provider::Google.default_base_url(),
"https://generativelanguage.googleapis.com/v1beta/openai"
);
assert_eq!(
Expand Down
2 changes: 1 addition & 1 deletion crates/aisix-core/src/models/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ fn model_schema() -> Value {
"additionalProperties": false,
"properties": {
"display_name": { "type": "string", "minLength": 1 },
"provider": { "type": "string", "enum": ["openai","anthropic","gemini","deepseek","cohere","jina"] },
"provider": { "type": "string", "enum": ["openai","anthropic","google","deepseek","cohere","jina"] },
Comment thread
jarvis9443 marked this conversation as resolved.
Resolved
Comment thread
jarvis9443 marked this conversation as resolved.
Resolved
"model_name": { "type": "string", "minLength": 1 },
"provider_key_id": { "type": "string", "minLength": 1 },
"timeout": { "type": "integer", "minimum": 0 },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[package]
name = "aisix-provider-gemini"
name = "aisix-provider-google"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
//! aisix-provider-gemini — Google Gemini via its OpenAI-compatible endpoint.
//! aisix-provider-google — Google Gemini via its OpenAI-compatible endpoint.
//!
//! Google exposes an OpenAI-shaped `/chat/completions` surface at
//! `generativelanguage.googleapis.com/v1beta/openai`. The wire format is
//! close enough to plain OpenAI that the upstream `OpenAiBridge` covers
//! every field we care about — this crate only relabels the bridge
//! (`name() == "gemini"`) so metrics and logs can distinguish traffic.
//! (`name() == "google"`) so metrics and logs can distinguish traffic.
//!
//! Operators configure Gemini access by setting on the Model:
//!
Expand All @@ -23,14 +23,14 @@

use aisix_provider_openai::OpenAiBridge;

/// Default base for Gemini's OpenAI-compat endpoint. Only used when the
/// Default base for Google's OpenAI-compat endpoint. Only used when the
/// Model doesn't carry an explicit `api_base` — production configs should
/// set one.
pub const GEMINI_DEFAULT_BASE: &str = "https://generativelanguage.googleapis.com/v1beta/openai";
pub const GOOGLE_DEFAULT_BASE: &str = "https://generativelanguage.googleapis.com/v1beta/openai";

/// Build a Bridge that speaks Gemini's OpenAI-compatible chat API.
/// Build a Bridge that speaks Google's OpenAI-compatible chat API.
pub fn gemini_bridge() -> OpenAiBridge {
OpenAiBridge::new().with_name("gemini")
OpenAiBridge::new().with_name("google")
}

#[cfg(test)]
Expand All @@ -43,12 +43,12 @@ mod tests {

#[test]
fn bridge_reports_gemini_name() {
assert_eq!(gemini_bridge().name(), "gemini");
assert_eq!(gemini_bridge().name(), "google");
}

#[test]
fn default_base_targets_v1beta_openai_shim() {
assert!(GEMINI_DEFAULT_BASE.contains("/v1beta/openai"));
assert!(GOOGLE_DEFAULT_BASE.contains("/v1beta/openai"));
}

#[tokio::test]
Expand All @@ -73,7 +73,7 @@ mod tests {
let model: aisix_core::Model = serde_json::from_str(
r#"{
"display_name": "my-gemini",
"provider": "gemini",
"provider": "google",
"model_name": "gemini-2.5-flash",
"provider_key_id": "11111111-1111-1111-1111-111111111111"
}"#,
Expand Down
14 changes: 7 additions & 7 deletions crates/aisix-provider-openai/src/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ pub const OPENAI_DEFAULT_BASE: &str = "https://api.openai.com/v1";
const DEEPSEEK_DEFAULT_BASE: &str = "https://api.deepseek.com";

/// Fallback host for the `gemini`-named variant of this bridge.
Comment thread
jarvis9443 marked this conversation as resolved.
Resolved
Comment thread
jarvis9443 marked this conversation as resolved.
Resolved
/// Mirrors `aisix_provider_gemini::GEMINI_DEFAULT_BASE` so that a
/// `with_name("gemini")` instance without an explicit `api_base`
/// Mirrors `aisix_provider_google::GOOGLE_DEFAULT_BASE` so that a
/// `with_name("google")` instance without an explicit `api_base`
/// dispatches to Google's OpenAI-compatible Gemini endpoint.
const GEMINI_DEFAULT_BASE: &str = "https://generativelanguage.googleapis.com/v1beta/openai";
const GOOGLE_DEFAULT_BASE: &str = "https://generativelanguage.googleapis.com/v1beta/openai";

/// Path suffixes the bridge appends to `api_base` when building upstream
/// URLs. If an operator accidentally pastes the full upstream URL into
Expand Down Expand Up @@ -91,14 +91,14 @@ impl OpenAiBridge {
}

/// Default upstream base for this bridge variant. The bridge factory
/// wraps with `with_name("deepseek")` / `with_name("gemini")` to
/// wraps with `with_name("deepseek")` / `with_name("google")` to
/// retarget; the default base follows the same retargeting so a
/// degenerate config (no `api_base` on the Model) still reaches the
/// right host.
fn default_base(&self) -> &'static str {
match self.name {
"deepseek" => DEEPSEEK_DEFAULT_BASE,
"gemini" => GEMINI_DEFAULT_BASE,
"google" => GOOGLE_DEFAULT_BASE,
_ => OPENAI_DEFAULT_BASE,
}
}
Expand Down Expand Up @@ -852,7 +852,7 @@ data: [DONE]\n\n";
/// path rather than falling through to the OpenAI host.
#[test]
fn gemini_default_base_targets_gemini_v1beta_openai() {
let bridge = OpenAiBridge::new().with_name("gemini");
let bridge = OpenAiBridge::new().with_name("google");
let pk: ProviderKey = serde_json::from_str(r#"{"display_name":"x","secret":"k"}"#).unwrap();
let ctx = BridgeContext::new("rid", sample_model(), Arc::new(pk));
assert_eq!(
Expand All @@ -866,7 +866,7 @@ data: [DONE]\n\n";
/// still strips an accidentally-pasted endpoint suffix.
#[test]
fn gemini_api_base_strips_endpoint_suffix_but_does_not_synthesize_prefix() {
let bridge = OpenAiBridge::new().with_name("gemini");
let bridge = OpenAiBridge::new().with_name("google");

// Canonical form passes through.
let canonical = "https://generativelanguage.googleapis.com/v1beta/openai";
Expand Down
2 changes: 1 addition & 1 deletion crates/aisix-proxy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,5 @@ aisix-provider-openai = { path = "../aisix-provider-openai" }
# aisix-provider-anthropic moved to [dependencies] so the /v1/messages
# handler can use its inbound wire helpers in non-test builds too.
aisix-provider-deepseek = { path = "../aisix-provider-deepseek" }
aisix-provider-gemini = { path = "../aisix-provider-gemini" }
aisix-provider-google = { path = "../aisix-provider-google" }
wiremock.workspace = true
14 changes: 7 additions & 7 deletions crates/aisix-proxy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3115,7 +3115,7 @@ data: [DONE]\n\n";
// → upstream → Bridge response decoder → renderer → wire bytes.

const MATRIX_ANTHROPIC_PK_ID: &str = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
const MATRIX_GEMINI_PK_ID: &str = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb";
const MATRIX_GOOGLE_PK_ID: &str = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb";
const MATRIX_DEEPSEEK_PK_ID: &str = "cccccccc-cccc-cccc-cccc-cccccccccccc";

fn anthropic_model_entry(name: &str) -> ResourceEntry<Model> {
Expand All @@ -3134,9 +3134,9 @@ data: [DONE]\n\n";
let cfg = format!(
r#"{{
"display_name": "{name}",
"provider": "gemini",
"provider": "google",
"model_name": "gemini-2.0-flash",
"provider_key_id": "{MATRIX_GEMINI_PK_ID}"
"provider_key_id": "{MATRIX_GOOGLE_PK_ID}"
}}"#
);
ResourceEntry::new("model-gemini-1", serde_json::from_str(&cfg).unwrap(), 1)
Expand Down Expand Up @@ -3171,7 +3171,7 @@ data: [DONE]\n\n";
}

fn matrix_gemini_pk(api_base: &str) -> ResourceEntry<aisix_core::ProviderKey> {
matrix_pk_entry(MATRIX_GEMINI_PK_ID, "ya29-test", api_base)
matrix_pk_entry(MATRIX_GOOGLE_PK_ID, "ya29-test", api_base)
}

fn matrix_deepseek_pk(api_base: &str) -> ResourceEntry<aisix_core::ProviderKey> {
Expand Down Expand Up @@ -3308,12 +3308,12 @@ event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n";
/// (OpenAI inbound) × (Gemini upstream). Gemini's bridge is a
/// thin wrapper around the OpenAi-compat `/chat/completions`
/// endpoint, so the upstream wire is OpenAI-shape — but the
/// `Hub.get(Provider::Gemini)` lookup must still resolve to the
/// `Hub.get(Provider::Google)` lookup must still resolve to the
/// Gemini-specific bridge instance (different metrics label,
/// different default base URL behavior).
#[tokio::test]
async fn matrix_openai_in_gemini_upstream_non_streaming() {
use aisix_provider_gemini::gemini_bridge;
use aisix_provider_google::gemini_bridge;

let upstream = MockServer::start().await;
Mock::given(method("POST"))
Expand All @@ -3337,7 +3337,7 @@ event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n";
snap.apikeys
.insert(apikey_entry("sk-caller", &["my-gemini"]));
let hub = Arc::new(Hub::new());
hub.register(Provider::Gemini, Arc::new(gemini_bridge()));
hub.register(Provider::Google, Arc::new(gemini_bridge()));
let app = build_router(build_state(snap, hub));

let body = serde_json::json!({
Expand Down
16 changes: 8 additions & 8 deletions crates/aisix-proxy/src/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -691,7 +691,7 @@ mod tests {

const ANTHROPIC_PK_ID: &str = "11111111-1111-1111-1111-111111111111";
const OPENAI_PK_ID: &str = "22222222-2222-2222-2222-222222222222";
const GEMINI_PK_ID: &str = "33333333-3333-3333-3333-333333333333";
const GOOGLE_PK_ID: &str = "33333333-3333-3333-3333-333333333333";
const DEEPSEEK_PK_ID: &str = "44444444-4444-4444-4444-444444444444";

fn anthropic_model(name: &str) -> ResourceEntry<Model> {
Expand Down Expand Up @@ -1072,9 +1072,9 @@ data: [DONE]\n\n";
let cfg = format!(
r#"{{
"display_name": "{name}",
"provider": "gemini",
"provider": "google",
"model_name": "gemini-2.0-flash",
"provider_key_id": "{GEMINI_PK_ID}"
"provider_key_id": "{GOOGLE_PK_ID}"
}}"#
);
ResourceEntry::new("m-3", serde_json::from_str(&cfg).unwrap(), 1)
Expand All @@ -1097,7 +1097,7 @@ data: [DONE]\n\n";
r#"{{"display_name":"gemini-up","secret":"ya29-test","api_base":"{api_base}"}}"#
);
let pk: aisix_core::ProviderKey = serde_json::from_str(&json).unwrap();
ResourceEntry::new(GEMINI_PK_ID, pk, 1)
ResourceEntry::new(GOOGLE_PK_ID, pk, 1)
}

fn deepseek_pk(api_base: &str) -> ResourceEntry<aisix_core::ProviderKey> {
Expand Down Expand Up @@ -1128,7 +1128,7 @@ data: [DONE]\n\n";
/// non-Anthropic Bridge in the workspace.
#[tokio::test]
async fn matrix_anthropic_in_gemini_upstream_non_streaming() {
use aisix_provider_gemini::gemini_bridge;
use aisix_provider_google::gemini_bridge;

let upstream = MockServer::start().await;
Mock::given(method("POST"))
Expand All @@ -1154,7 +1154,7 @@ data: [DONE]\n\n";

let hub = Arc::new(Hub::new());
hub.register(Provider::Anthropic, Arc::new(AnthropicBridge::new()));
hub.register(Provider::Gemini, Arc::new(gemini_bridge()));
hub.register(Provider::Google, Arc::new(gemini_bridge()));
let handle = SnapshotHandle::new(snap);
let app = crate::build_router(crate::ProxyState::new(handle, hub, &cfg()).without_cache());

Expand Down Expand Up @@ -1343,9 +1343,9 @@ data: [DONE]\n\n";

#[tokio::test]
async fn matrix_anthropic_in_gemini_upstream_streaming() {
use aisix_provider_gemini::gemini_bridge;
use aisix_provider_google::gemini_bridge;
assert_anthropic_streams_through_openai_compat_upstream(
Provider::Gemini,
Provider::Google,
Arc::new(gemini_bridge()),
// Placeholder; helper rebuilds with the wiremock uri.
gemini_model("my-claude-via-gemini"),
Expand Down
2 changes: 1 addition & 1 deletion crates/aisix-proxy/src/passthrough.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ fn default_base(provider_prefix: &str) -> Option<&'static str> {
match provider_prefix {
"openai" => Some("https://api.openai.com"),
"anthropic" => Some("https://api.anthropic.com"),
"gemini" => Some("https://generativelanguage.googleapis.com"),
"google" => Some("https://generativelanguage.googleapis.com"),
"deepseek" => Some("https://api.deepseek.com"),
_ => None,
}
Expand Down
2 changes: 1 addition & 1 deletion crates/aisix-proxy/src/rerank.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ fn default_base_for_provider(provider: aisix_core::models::Provider) -> Option<S
// body fields, same `results` array shape, Bearer auth.
Provider::Jina => Some("https://api.jina.ai".to_string()),
Provider::Anthropic => None, // Anthropic doesn't expose a rerank API
Provider::Gemini => None, // Gemini doesn't expose a rerank API
Provider::Google => None, // Gemini doesn't expose a rerank API
Provider::Deepseek => None,
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/aisix-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ aisix-guardrails = { path = "../aisix-guardrails" }
aisix-admin = { path = "../aisix-admin" }
aisix-provider-openai = { path = "../aisix-provider-openai" }
aisix-provider-anthropic = { path = "../aisix-provider-anthropic" }
aisix-provider-gemini = { path = "../aisix-provider-gemini" }
aisix-provider-google = { path = "../aisix-provider-google" }
aisix-provider-deepseek = { path = "../aisix-provider-deepseek" }
aisix-ratelimit = { path = "../aisix-ratelimit" }
aisix-cache = { path = "../aisix-cache", features = ["redis"] }
Expand Down
4 changes: 2 additions & 2 deletions crates/aisix-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ use aisix_gateway::Hub;
use aisix_obs::{init_tracing, install_otlp_tracer, Metrics};
use aisix_provider_anthropic::AnthropicBridge;
use aisix_provider_deepseek::deepseek_bridge;
use aisix_provider_gemini::gemini_bridge;
use aisix_provider_google::gemini_bridge;
use aisix_provider_openai::OpenAiBridge;
use aisix_proxy::background::run_background_model_check_once;
use aisix_proxy::budget::BudgetClient;
Expand Down Expand Up @@ -841,7 +841,7 @@ fn build_hub() -> Hub {
let hub = Hub::new();
hub.register(Provider::Openai, Arc::new(OpenAiBridge::new()));
hub.register(Provider::Anthropic, Arc::new(AnthropicBridge::new()));
hub.register(Provider::Gemini, Arc::new(gemini_bridge()));
hub.register(Provider::Google, Arc::new(gemini_bridge()));
hub.register(Provider::Deepseek, Arc::new(deepseek_bridge()));
hub
}
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ curl -sS -X POST http://127.0.0.1:3001/admin/v1/models \
## Field Notes

- `display_name` is the alias clients send in proxy requests.
- `provider` currently supports `openai`, `anthropic`, `gemini`, and `deepseek`.
- `provider` currently supports `openai`, `anthropic`, `google`, and `deepseek`.
Comment thread
jarvis9443 marked this conversation as resolved.
Resolved
- `provider_key_id` must reference an existing `ProviderKey` resource.
- `timeout` is in milliseconds. `0` or omission means no timeout.
- `cost` stores pricing metadata used by budget and usage accounting paths.
Expand Down
4 changes: 2 additions & 2 deletions docs/configuration/provider-keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Each provider has its own convention — the four current bridges do **not** sha
|---|---|---|---|
| `openai` | include `/v1` | `/chat/completions`, `/embeddings`, `/completions`, `/images/generations`, `/audio/*` | `https://api.openai.com/v1` |
| `deepseek` | bare host (DeepSeek serves OpenAI-compatible paths at the host root) | `/chat/completions` | `https://api.deepseek.com` |
| `gemini` | host plus the OpenAI-compat prefix `/v1beta/openai` | `/chat/completions` | `https://generativelanguage.googleapis.com/v1beta/openai` |
| `google` | host plus the OpenAI-compat prefix `/v1beta/openai` | `/chat/completions` | `https://generativelanguage.googleapis.com/v1beta/openai` |
| `anthropic` | bare host | `/v1/messages` | `https://api.anthropic.com` |

The OpenAI and Anthropic conventions match each upstream's official SDK — `openai-python` initialises `base_url = "https://api.openai.com/v1"`, while `anthropic-sdk-python` initialises `base_url = "https://api.anthropic.com"` and appends `/v1/messages` itself. DeepSeek is OpenAI-compatible but exposes `/chat/completions` directly at the host root, and Gemini's OpenAI-compatible surface lives under a fixed `/v1beta/openai` prefix that the bridge does not synthesize.
Expand All @@ -80,7 +80,7 @@ For `anthropic` (always tolerated):

- the full upstream URL `…/v1/messages` or the `/v1` prefix is stripped — `api_base: "https://api.anthropic.com/v1/messages"`, `…/v1`, and bare host all converge to the bare host at dispatch time.

For `gemini` and any other variant: only suffix stripping; the bridge does not synthesize the `/v1beta/openai` prefix. Operators should paste the full canonical form.
For `google` and any other variant: only suffix stripping; the bridge does not synthesize the `/v1beta/openai` prefix. Operators should paste the full canonical form.

### Outside the canonical hosts

Expand Down
Loading
Loading