Resolve a Vertex AI route given provider config, model, and bearer token.
(
config: &std::collections::HashMap<String, String>,
model_id: &str,
route_name: &str,
api_key: &str,
profile: &openshell_core::inference::InferenceProviderProfile,
)
| 547 | |
| 548 | /// Resolve a Vertex AI route given provider config, model, and bearer token. |
| 549 | fn resolve_vertex_ai_route( |
| 550 | config: &std::collections::HashMap<String, String>, |
| 551 | model_id: &str, |
| 552 | route_name: &str, |
| 553 | api_key: &str, |
| 554 | profile: &openshell_core::inference::InferenceProviderProfile, |
| 555 | ) -> Result<RouterResolvedRoute, Status> { |
| 556 | // Validate model_id early — it appears in URL paths for Anthropic routes |
| 557 | // and in JSON request bodies for all routes. Rejecting path separators, |
| 558 | // traversal segments, and control characters up front is defense-in-depth. |
| 559 | validate_vertex_model_id(model_id)?; |
| 560 | |
| 561 | // Determine if this is an Anthropic model. |
| 562 | // Explicit VERTEX_AI_PUBLISHER=anthropic overrides inference. |
| 563 | // All non-Anthropic models route to the OpenAI-compatible endpoint. |
| 564 | let explicit_publisher = config |
| 565 | .get(VERTEX_AI_PUBLISHER_KEY) |
| 566 | .map(String::as_str) |
| 567 | .filter(|v| !v.trim().is_empty()); |
| 568 | |
| 569 | let is_anthropic = explicit_publisher.map_or_else( |
| 570 | || infer_vertex_publisher(model_id) == Some("anthropic"), |
| 571 | |p| p.eq_ignore_ascii_case("anthropic"), |
| 572 | ); |
| 573 | |
| 574 | // Escape hatch: caller-supplied full base URL still uses the model-derived |
| 575 | // protocol and path contract, but only for the OpenAI-compatible Vertex surface. |
| 576 | // Anthropic-on-Vertex needs model-path shaping and body adaptation that a fully |
| 577 | // caller-controlled URL cannot safely preserve. |
| 578 | if let Some(base_url) = config |
| 579 | .get(profile.base_url_config_keys[0]) |
| 580 | .or_else(|| config.get(profile.base_url_config_keys[1])) |
| 581 | .map(String::as_str) |
| 582 | .filter(|v| !v.trim().is_empty()) |
| 583 | { |
| 584 | if is_anthropic { |
| 585 | return Err(Status::invalid_argument( |
| 586 | "Vertex AI base URL overrides are not supported for Anthropic models. \ |
| 587 | Remove GOOGLE_VERTEX_AI_BASE_URL / VERTEX_AI_BASE_URL and configure \ |
| 588 | VERTEX_AI_PROJECT_ID + VERTEX_AI_REGION instead." |
| 589 | .to_string(), |
| 590 | )); |
| 591 | } |
| 592 | let base_url = validate_vertex_base_url(base_url)?; |
| 593 | |
| 594 | return Ok(build_vertex_route( |
| 595 | route_name, |
| 596 | base_url, |
| 597 | model_id, |
| 598 | api_key, |
| 599 | vec!["openai_chat_completions".to_string()], |
| 600 | profile, |
| 601 | false, |
| 602 | Some("/chat/completions".to_string()), |
| 603 | )); |
| 604 | } |
| 605 | |
| 606 | let project = required_vertex_config(config, VERTEX_AI_PROJECT_ID_KEY)?; |