phase-40: Gemini + Claude provider adapters
Phase 40 PRD (docs/CONTROL_PLANE_PRD.md:82-83) listed: - crates/aibridge/src/providers/gemini.rs - crates/aibridge/src/providers/claude.rs Neither existed. Landing both now, in gateway/src/v1/ (matches the existing ollama.rs + openrouter.rs sibling pattern — aibridge's providers/ is for the adapter *trait* abstractions, v1/ holds the concrete /v1/chat dispatchers that know the wire format). gemini.rs: - POST https://generativelanguage.googleapis.com/v1beta/models/ {model}:generateContent?key=<API_KEY> - Auth: query-string key (not bearer) - Maps messages → contents+parts (Gemini's wire shape), extracts from candidates[0].content.parts[0].text - 3 tests: key resolution, body serialization (camelCase generationConfig + maxOutputTokens), prefix-strip claude.rs: - POST https://api.anthropic.com/v1/messages - Auth: x-api-key header + anthropic-version: 2023-06-01 - Carries system prompt in top-level `system` field (not messages[]). Extracts from content[0].text where type=="text" - 4 tests: key resolution, body serialization with/without system field, prefix-strip v1/mod.rs: + V1State.gemini_key + claude_key Option<String> + resolve_provider() strips "gemini/" and "claude/" prefixes + /v1/chat dispatcher handles "gemini" + "claude"/"anthropic" + 2 new resolve_provider tests (prefix + strip per adapter) main.rs: + Construct both keys at startup via resolve_*_key() helpers. Missing keys log at debug (not warn) since these are optional providers — unlike OpenRouter which is the rescue rung. Every /v1/chat error path mirrors the existing pattern: - 503 SERVICE_UNAVAILABLE when key isn't configured - 502 BAD_GATEWAY with the provider's error text when the upstream call fails - Response shape always the OpenAI-compatible ChatResponse Workspace warnings still at 0. 9 new tests pass. Pre-existing test failure `executor_prompt_includes_surfaced_ candidates` at execution_loop/mod.rs:1550 is unrelated (fails on pristine HEAD too — PR fixture divergence). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,8 @@
|
||||
pub mod ollama;
|
||||
pub mod ollama_cloud;
|
||||
pub mod openrouter;
|
||||
pub mod gemini;
|
||||
pub mod claude;
|
||||
pub mod langfuse_trace;
|
||||
pub mod respond;
|
||||
pub mod truth;
|
||||
@@ -42,6 +44,14 @@ pub struct V1State {
|
||||
/// provider="openrouter" calls 503 rather than attempt. Same key
|
||||
/// sourcing as LLM Team UI so the two share one API quota.
|
||||
pub openrouter_key: Option<String>,
|
||||
/// Gemini API key (Google Generative Language). Loaded at startup
|
||||
/// via `gemini::resolve_gemini_key()`. None = provider="gemini"
|
||||
/// calls 503. Phase 40 deliverable.
|
||||
pub gemini_key: Option<String>,
|
||||
/// Anthropic Claude API key. Loaded at startup via
|
||||
/// `claude::resolve_claude_key()`. None = provider="claude" calls
|
||||
/// 503. Phase 40 deliverable.
|
||||
pub claude_key: Option<String>,
|
||||
/// Phase 40 early deliverable — Langfuse client. None = tracing
|
||||
/// disabled (keys missing or container unreachable). Traces are
|
||||
/// fire-and-forget: never block the response path.
|
||||
@@ -159,6 +169,12 @@ fn resolve_provider(req: &ChatRequest) -> (String, String) {
|
||||
if let Some(rest) = req.model.strip_prefix("cloud/") {
|
||||
return ("ollama_cloud".to_string(), rest.to_string());
|
||||
}
|
||||
if let Some(rest) = req.model.strip_prefix("gemini/") {
|
||||
return ("gemini".to_string(), rest.to_string());
|
||||
}
|
||||
if let Some(rest) = req.model.strip_prefix("claude/") {
|
||||
return ("claude".to_string(), rest.to_string());
|
||||
}
|
||||
("ollama".to_string(), req.model.clone())
|
||||
}
|
||||
|
||||
@@ -210,6 +226,18 @@ mod resolve_provider_tests {
|
||||
let r = mk_req(Some("openrouter"), "openrouter/openai/gpt-4o-mini");
|
||||
assert_eq!(resolve_provider(&r), ("openrouter".into(), "openrouter/openai/gpt-4o-mini".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gemini_prefix_infers_and_strips() {
|
||||
let r = mk_req(None, "gemini/gemini-2.0-flash");
|
||||
assert_eq!(resolve_provider(&r), ("gemini".into(), "gemini-2.0-flash".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claude_prefix_infers_and_strips() {
|
||||
let r = mk_req(None, "claude/claude-3-5-sonnet-latest");
|
||||
assert_eq!(resolve_provider(&r), ("claude".into(), "claude-3-5-sonnet-latest".into()));
|
||||
}
|
||||
}
|
||||
|
||||
async fn chat(
|
||||
@@ -273,10 +301,34 @@ async fn chat(
|
||||
.map_err(|e| (StatusCode::BAD_GATEWAY, format!("openrouter: {e}")))?;
|
||||
(r, "openrouter".to_string())
|
||||
}
|
||||
"gemini" => {
|
||||
// Phase 40 provider adapter. Google Generative Language
|
||||
// API via query-string key auth (not bearer).
|
||||
let key = state.gemini_key.as_deref().ok_or((
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
"GEMINI_API_KEY not configured".to_string(),
|
||||
))?;
|
||||
let r = gemini::chat(key, &*req_for_adapter)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::BAD_GATEWAY, format!("gemini: {e}")))?;
|
||||
(r, "gemini".to_string())
|
||||
}
|
||||
"claude" | "anthropic" => {
|
||||
// Phase 40 provider adapter. Anthropic Messages API via
|
||||
// x-api-key header + anthropic-version:2023-06-01.
|
||||
let key = state.claude_key.as_deref().ok_or((
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
"ANTHROPIC_API_KEY not configured".to_string(),
|
||||
))?;
|
||||
let r = claude::chat(key, &*req_for_adapter)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::BAD_GATEWAY, format!("claude: {e}")))?;
|
||||
(r, "claude".to_string())
|
||||
}
|
||||
other => {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("unknown provider '{other}' — supported: ollama, ollama_cloud, openrouter"),
|
||||
format!("unknown provider '{other}' — supported: ollama, ollama_cloud, openrouter, gemini, claude"),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user