feat(cost-map): load LITELLM_MODEL_COST_MAP_URL from gs:// with Google application default credentials (#51108)

* feat(cost-map): load LITELLM_MODEL_COST_MAP_URL from gs:// with Google application default credentials

* fix(cost-map): bound the gs:// token wait by the fetch timeout and accept a bare STORAGE_EMULATOR_HOST

* fix(cost-map): retry a gs:// token that is not ready and send no credentials to a storage emulator

* fix(cost-map): spend one timeout across the gs:// token wait and the object request
This commit is contained in:
jesus-berri 2026-11-12 15:22:23 -08:00 • committed by GitHub
parent f298400060
commit f87d0a98df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 567 additions and 12 deletions

View file

@ -4370,10 +4370,14 @@ dependencies = [
"aws-credential-types",
"backon",
"bytes",
"litellm-auth-gcp",
"litellm-auth-types",
"litellm-error",
"litellm-http",
"litellm-s3",
"litellm-secret-value",
"litellm-tracing",
"percent-encoding",
"reqwest",
"rstest",
"thiserror 2.0.19",

View file

@ -1,7 +1,8 @@
# Model catalog refresh
- `fetch` fetches one document over http(s) or `s3://` and never parses it
- `fetch` fetches one document over http(s), `s3://` or `gs://` and never parses it
- Only 429, 500, 502, 503, 504 and transport errors are retried. S3 errors are final
- `gs://` reuses the http attempt with a Google application default credentials bearer token, so its statuses retry like http. A token that cannot be acquired in time retries too, since a new GKE pod's metadata server can refuse the first requests. With `STORAGE_EMULATOR_HOST` set it sends no credentials, like Google's SDKs. The token never reaches a reason or a log line
- `Failed.retryable` means only the attempt limit ended the fetch
- A server's Retry-After overrides the backoff, capped at `max_delay`
- `Start::After(failed)` resumes the backoff schedule where another caller's first attempt left off

View file

@ -8,10 +8,13 @@ repository.workspace = true
[dependencies]
backon.workspace = true
bytes.workspace = true
litellm-auth-gcp.workspace = true
litellm-auth-types.workspace = true
litellm-error.workspace = true
litellm-http.workspace = true
litellm-s3.workspace = true
litellm-tracing.workspace = true
percent-encoding.workspace = true
reqwest.workspace = true
thiserror.workspace = true
tokio = { workspace = true, features = ["time"] }
@ -20,5 +23,6 @@ url.workspace = true
[dev-dependencies]
aws-credential-types = "1.3.0"
litellm-http = { workspace = true, features = ["test-support"] }
litellm-secret-value.workspace = true
rstest.workspace = true
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] }

View file

@ -2,14 +2,24 @@ use std::time::Duration;
use bytes::Bytes;
use litellm_auth_gcp::{CredentialSource, GcpAuth};
use litellm_http::Client;
use litellm_s3::S3Uri;
use litellm_s3::sdk::config::retry::RetryConfig;
use litellm_s3::sdk::config::timeout::TimeoutConfig;
use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, utf8_percent_encode};
use reqwest::header::{ETAG, HeaderMap, RETRY_AFTER};
use url::Url;
use crate::{Error, RETRYABLE_STATUSES, S3Access};
use crate::{Error, GcsAccess, GcsUri, RETRYABLE_STATUSES, S3Access};
const GCS_ENDPOINT: &str = "https://storage.googleapis.com";
const OBJECT_NAME_ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC
.remove(b'-')
.remove(b'_')
.remove(b'.')
.remove(b'~');
pub(crate) const MAX_BODY_BYTES: u64 = 100 * 1024 * 1024;
@ -46,14 +56,19 @@ impl Rejected {
pub(crate) async fn http(
client: &Client,
url: Url,
bearer: Option<&str>,
timeout: Duration,
) -> Result<Fetched, Rejected> {
let request = match bearer {
Some(token) => client.get(url).bearer_auth(token),
None => client.get(url),
};
let transport = |error: litellm_http::Error| Rejected {
cause: Cause::Transport(Error::from(error)),
retryable: true,
retry_after: None,
};
let mut response = tokio::time::timeout(timeout, client.get(url).send())
let mut response = tokio::time::timeout(timeout, request.send())
.await
.map_err(litellm_http::Error::read_timeout)
.and_then(|sent| sent.map_err(litellm_http::Error::from))
@ -135,6 +150,78 @@ pub(crate) async fn s3(
fetched.await.map_err(Rejected::fatal)
}
pub(crate) async fn gcs(
client: &Client,
gcs: &GcsAccess,
object: &GcsUri,
timeout: Duration,
) -> Result<Fetched, Rejected> {
let started = tokio::time::Instant::now();
let (endpoint, token) = match gcs {
GcsAccess::Environment => {
match emulator_endpoint(std::env::var("STORAGE_EMULATOR_HOST").ok()) {
Some(emulator) => (emulator, None),
None => (
GCS_ENDPOINT.to_owned(),
Some(
bounded_token(
timeout,
GcpAuth::default().access_token(CredentialSource::application_default(
&|name| std::env::var(name).ok(),
)),
)
.await?,
),
),
}
}
GcsAccess::Config {
endpoint,
token_source,
} => (
endpoint.clone(),
Some(bounded_token(timeout, token_source.token()).await?),
),
};
let url = Url::parse(&format!(
"{}/storage/v1/b/{}/o/{}?alt=media",
endpoint.trim_end_matches('/'),
object.bucket,
utf8_percent_encode(&object.object, OBJECT_NAME_ENCODE_SET),
))
.map_err(|error| Rejected::fatal(Error::from(error)))?;
http(
client,
url,
token.as_ref().map(|token| token.expose()),
timeout.saturating_sub(started.elapsed()),
)
.await
}
async fn bounded_token<T>(
timeout: Duration,
token: impl Future<Output = Result<T, litellm_auth_types::Error>>,
) -> Result<T, Rejected> {
let retryable = |error| Rejected {
cause: Cause::Transport(error),
retryable: true,
retry_after: None,
};
tokio::time::timeout(timeout, token)
.await
.map_err(|elapsed| retryable(Error::gcs_token_timeout(elapsed)))?
.map_err(|error| retryable(Error::gcs_token(error)))
}
fn emulator_endpoint(host: Option<String>) -> Option<String> {
match host {
Some(host) if host.contains("://") => Some(host),
Some(host) if !host.is_empty() => Some(format!("http://{host}")),
_ => None,
}
}
fn append(body: &mut Vec<u8>, chunk: &Bytes) -> Result<(), Error> {
if (body.len() + chunk.len()) as u64 > MAX_BODY_BYTES {
return Err(Error::body_too_large(MAX_BODY_BYTES));

View file

@ -25,6 +25,12 @@ enum Repr {
S3Body(#[source] ByteStreamError),
#[error("S3 GetObject timed out")]
S3Timeout(#[source] Elapsed),
#[error("invalid GCS URL")]
GcsUri,
#[error("Google access token unavailable")]
GcsToken(#[source] litellm_auth_types::Error),
#[error("Google access token timed out")]
GcsTokenTimeout(#[source] Elapsed),
#[error("response body is over the {limit} byte limit")]
BodyTooLarge { limit: u64 },
}
@ -34,6 +40,18 @@ impl Error {
Self(Box::new(Repr::UnsupportedScheme { scheme }))
}
pub(crate) fn invalid_gcs_url() -> Self {
Self(Box::new(Repr::GcsUri))
}
pub(crate) fn gcs_token(cause: litellm_auth_types::Error) -> Self {
Self(Box::new(Repr::GcsToken(cause)))
}
pub(crate) fn gcs_token_timeout(cause: Elapsed) -> Self {
Self(Box::new(Repr::GcsTokenTimeout(cause)))
}
pub(crate) fn body_too_large(limit: u64) -> Self {
Self(Box::new(Repr::BodyTooLarge { limit }))
}

View file

@ -1,8 +1,10 @@
use std::borrow::Cow;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration;
use backon::Retryable;
use litellm_auth_gcp::GcpTokenSource;
use litellm_error::Report;
use litellm_http::Client;
use url::Url;
@ -20,6 +22,15 @@ pub enum S3Access {
Config(Box<litellm_s3::sdk::Config>),
}
#[derive(Clone)]
pub enum GcsAccess {
Environment,
Config {
endpoint: String,
token_source: Arc<dyn GcpTokenSource>,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Failed {
pub reason: String,
@ -38,6 +49,7 @@ pub enum Start {
pub async fn fetch(
client: &Client,
s3: &S3Access,
gcs: &GcsAccess,
url: &str,
policy: RetryPolicy,
start: Start,
@ -58,8 +70,11 @@ pub async fn fetch(
made.fetch_add(1, Ordering::Relaxed);
async {
match &source {
Source::Http(http) => attempt::http(client, http.clone(), policy.timeout).await,
Source::Http(http) => {
attempt::http(client, http.clone(), None, policy.timeout).await
}
Source::S3(object) => attempt::s3(client, s3, object, policy.timeout).await,
Source::Gcs(object) => attempt::gcs(client, gcs, object, policy.timeout).await,
}
}
};

View file

@ -8,6 +8,34 @@ use crate::Error;
pub enum Source {
Http(Url),
S3(S3Uri),
Gcs(GcsUri),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GcsUri {
pub bucket: String,
pub object: String,
}
impl TryFrom<&str> for GcsUri {
type Error = Error;
fn try_from(uri: &str) -> Result<Self, Error> {
let rest = uri.split_once("://").map_or(uri, |(_, rest)| rest);
let (bucket, path) = rest.split_at(rest.find(['/', '?', '#']).unwrap_or(rest.len()));
let object = path
.split(['?', '#'])
.next()
.unwrap_or_default()
.trim_start_matches('/');
if bucket.is_empty() || object.is_empty() {
return Err(Error::invalid_gcs_url());
}
Ok(Self {
bucket: bucket.to_owned(),
object: object.to_owned(),
})
}
}
impl Source {
@ -16,6 +44,7 @@ impl Source {
match parsed.scheme() {
"http" | "https" => Ok(Self::Http(parsed)),
"s3" => Ok(Self::S3(S3Uri::try_from(url)?)),
"gs" => Ok(Self::Gcs(GcsUri::try_from(url)?)),
scheme => Err(Error::unsupported_scheme(scheme.to_owned())),
}
}
@ -48,8 +77,30 @@ mod tests {
);
}
#[rstest]
#[case::object("gs://bucket/cost.json", "bucket", "cost.json")]
#[case::nested_object("gs://bucket/prices/cost.json", "bucket", "prices/cost.json")]
#[case::space_kept_raw("gs://bucket/my prices.json", "bucket", "my prices.json")]
#[case::query_dropped("gs://bucket/cost.json?generation=1", "bucket", "cost.json")]
fn gs_urls_name_a_bucket_and_object(
#[case] url: &str,
#[case] bucket: &str,
#[case] object: &str,
) {
assert_eq!(
Source::parse(url).unwrap(),
Source::Gcs(GcsUri {
bucket: bucket.into(),
object: object.into(),
})
);
}
#[rstest]
#[case::s3_without_a_key("s3://bucket/")]
#[case::gs_without_an_object("gs://bucket/")]
#[case::gs_without_a_path("gs://bucket")]
#[case::gs_without_a_bucket("gs:///cost.json")]
#[case::unsupported("ftp://example.com/cost.json")]
#[case::file("file:///tmp/cost.json")]
#[case::not_a_url("not a url")]

View file

@ -1,13 +1,19 @@
use std::io::{BufRead, BufReader, Write};
use std::net::TcpListener;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use aws_credential_types::Credentials;
use aws_credential_types::provider::error::CredentialsError;
use aws_credential_types::provider::{ProvideCredentials, future};
use litellm_auth_gcp::{GcpAuthFuture, GcpTokenSource};
use litellm_auth_types::{Error, ErrorDetail};
use litellm_http::Client;
use litellm_model_catalog_refresh::{Failed, Fetched, RetryPolicy, S3Access, Start, fetch};
use litellm_model_catalog_refresh::{
Failed, Fetched, GcsAccess, RetryPolicy, S3Access, Start, fetch,
};
use litellm_secret_value::SecretValue;
use rstest::rstest;
const POLICY: RetryPolicy = RetryPolicy {
@ -140,6 +146,7 @@ async fn fetched(url: &str, policy: RetryPolicy, start: Start) -> Result<Fetched
fetch(
&Client::for_test(),
&S3Access::Environment { region: None },
&GcsAccess::Environment,
url,
policy,
start,
@ -181,6 +188,7 @@ async fn a_fetch_runs_as_a_task_on_a_multi_thread_runtime() {
fetch(
&Client::for_test(),
&S3Access::Environment { region: None },
&GcsAccess::Environment,
&url,
POLICY,
Start::Fresh,
@ -372,6 +380,7 @@ async fn fetched_from_s3(
fetch(
&Client::for_test(),
&s3(upstream, credentials),
&GcsAccess::Environment,
url,
POLICY,
Start::Fresh,
@ -417,6 +426,7 @@ async fn an_s3_body_that_stalls_after_the_headers_times_out() {
fetch(
&Client::for_test(),
&s3(&upstream, keys()),
&GcsAccess::Environment,
url,
policy,
Start::Fresh,
@ -450,7 +460,15 @@ async fn a_body_that_keeps_arriving_finishes_after_the_timeout(
..POLICY
};
assert_eq!(
fetch(&Client::for_test(), &access, &url, policy, Start::Fresh).await,
fetch(
&Client::for_test(),
&access,
&GcsAccess::Environment,
&url,
policy,
Start::Fresh
)
.await,
Ok(Fetched {
body: SLOW_BODY.to_vec(),
etag: Some("\"v1\"".into()),
@ -497,6 +515,261 @@ async fn an_s3_fetch_without_credentials_fails_without_a_request() {
assert!(upstream.heads().is_empty());
}
const GCS_TOKEN: &str = "ya29.model-cost-token";
struct Token(Result<&'static str, &'static str>);
impl GcpTokenSource for Token {
fn project_id(&self) -> GcpAuthFuture<'_, String> {
Box::pin(async { Ok("model-cost-project".into()) })
}
fn token(&self) -> GcpAuthFuture<'_, SecretValue> {
Box::pin(async move {
self.0.map(SecretValue::new).map_err(|subject| {
Error::CredentialAcquisition(ErrorDetail::Unresolved { subject })
})
})
}
}
struct StalledToken;
impl GcpTokenSource for StalledToken {
fn project_id(&self) -> GcpAuthFuture<'_, String> {
Box::pin(std::future::pending())
}
fn token(&self) -> GcpAuthFuture<'_, SecretValue> {
Box::pin(std::future::pending())
}
}
async fn fetched_from_gcs(
url: &str,
upstream: &Upstream,
token: impl GcpTokenSource + 'static,
) -> Result<Fetched, Failed> {
fetch(
&Client::for_test(),
&S3Access::Environment { region: None },
&GcsAccess::Config {
endpoint: format!("{}/", upstream.base),
token_source: Arc::new(token),
},
url,
POLICY,
Start::Fresh,
)
.await
}
#[rstest]
#[tokio::test]
async fn a_gcs_fetch_sends_the_bearer_token_to_the_object_media_url() {
let upstream = Upstream::serve(vec![Answer::Raw(OK)]);
assert_eq!(
fetched_from_gcs(
"gs://prices/maps/cost.json",
&upstream,
Token(Ok(GCS_TOKEN))
)
.await,
Ok(Fetched {
body: b"{}".to_vec(),
etag: Some("\"v1\"".into()),
})
);
let heads = upstream.heads();
let [head] = heads.as_slice() else {
panic!("expected one request, got {heads:?}");
};
assert!(
head.starts_with("GET /storage/v1/b/prices/o/maps%2Fcost.json?alt=media HTTP/1.1"),
"{head}"
);
assert_eq!(
header(head, "authorization"),
Some(format!("Bearer {GCS_TOKEN}").as_str())
);
}
#[rstest]
#[case::unavailable(UNAVAILABLE, " (after 3 attempts)", true, 3)]
#[case::rate_limited(
"HTTP/1.1 429 Too Many Requests\r\nRetry-After: 0\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
" (after 3 attempts)",
true,
3
)]
#[case::unauthorized(
"HTTP/1.1 401 Unauthorized\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
"",
false,
1
)]
#[case::denied(
"HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
"",
false,
1
)]
#[case::missing(NOT_FOUND, "", false, 1)]
#[tokio::test]
async fn a_gcs_status_retries_only_when_an_http_status_would(
#[case] response: &'static str,
#[case] suffix: &str,
#[case] retryable: bool,
#[case] requests: usize,
) {
let upstream = Upstream::serve(vec![Answer::Raw(response)]);
let url = "gs://prices/cost.json";
let failed = fetched_from_gcs(url, &upstream, Token(Ok(GCS_TOKEN)))
.await
.unwrap_err();
let status = &response[9..12];
assert_eq!(failed.reason, format!("HTTP {status} from {url}{suffix}"));
assert_eq!(failed.retryable, retryable);
assert_eq!(upstream.heads().len(), requests);
}
#[rstest]
#[tokio::test]
async fn a_gcs_fetch_without_a_token_retries_without_a_request() {
let upstream = Upstream::serve(vec![Answer::Raw(OK)]);
let url = "gs://prices/cost.json";
let failed = fetched_from_gcs(url, &upstream, Token(Err("Google access token")))
.await
.unwrap_err();
assert!(
failed.reason.starts_with(&format!(
"error fetching {url}: Google access token unavailable"
)),
"{}",
failed.reason
);
assert!(failed.retryable);
assert!(upstream.heads().is_empty());
}
struct TokenAfterOneRefusal(AtomicU32);
impl GcpTokenSource for TokenAfterOneRefusal {
fn project_id(&self) -> GcpAuthFuture<'_, String> {
Box::pin(async { Ok("model-cost-project".into()) })
}
fn token(&self) -> GcpAuthFuture<'_, SecretValue> {
Box::pin(async move {
match self.0.fetch_add(1, Ordering::Relaxed) {
0 => Err(Error::CredentialAcquisition(ErrorDetail::Unresolved {
subject: "Google access token",
})),
_ => Ok(SecretValue::new(GCS_TOKEN)),
}
})
}
}
#[rstest]
#[tokio::test]
async fn a_gcs_token_refused_once_is_retried_into_a_fetch() {
let upstream = Upstream::serve(vec![Answer::Raw(OK)]);
let fetched = fetched_from_gcs(
"gs://prices/cost.json",
&upstream,
TokenAfterOneRefusal(AtomicU32::new(0)),
)
.await
.unwrap();
assert_eq!(fetched.body, b"{}");
assert_eq!(upstream.heads().len(), 1);
}
#[rstest]
#[tokio::test]
async fn a_gcs_token_that_never_arrives_fails_within_the_attempt_timeout() {
let upstream = Upstream::serve(vec![Answer::Raw(OK)]);
let url = "gs://prices/cost.json";
let once = RetryPolicy {
max_attempts: 1,
..POLICY
};
let failed = tokio::time::timeout(
once.timeout * 2,
fetch(
&Client::for_test(),
&S3Access::Environment { region: None },
&GcsAccess::Config {
endpoint: format!("{}/", upstream.base),
token_source: Arc::new(StalledToken),
},
url,
once,
Start::Fresh,
),
)
.await
.expect("the token wait outlived the attempt timeout")
.unwrap_err();
assert!(
failed.reason.starts_with(&format!(
"error fetching {url}: Google access token timed out"
)),
"{}",
failed.reason
);
assert!(failed.retryable);
assert!(upstream.heads().is_empty());
}
struct SlowToken(Duration);
impl GcpTokenSource for SlowToken {
fn project_id(&self) -> GcpAuthFuture<'_, String> {
Box::pin(async { Ok("model-cost-project".into()) })
}
fn token(&self) -> GcpAuthFuture<'_, SecretValue> {
Box::pin(async move {
tokio::time::sleep(self.0).await;
Ok(SecretValue::new(GCS_TOKEN))
})
}
}
#[rstest]
#[tokio::test]
async fn a_gcs_attempt_spends_one_timeout_across_the_token_and_the_request() {
let upstream = Upstream::serve(vec![Answer::Stall]);
let once = RetryPolicy {
max_attempts: 1,
timeout: Duration::from_secs(2),
..POLICY
};
let started = std::time::Instant::now();
let failed = fetch(
&Client::for_test(),
&S3Access::Environment { region: None },
&GcsAccess::Config {
endpoint: format!("{}/", upstream.base),
token_source: Arc::new(SlowToken(once.timeout / 2)),
},
"gs://prices/cost.json",
once,
Start::Fresh,
)
.await
.unwrap_err();
assert!(failed.retryable, "{}", failed.reason);
assert!(
started.elapsed() < once.timeout + once.timeout / 4,
"the attempt took {:?}, over its {:?} timeout",
started.elapsed(),
once.timeout
);
}
const DECLARED_OVERSIZED: &str =
"HTTP/1.1 200 OK\r\nContent-Length: 104857601\r\nConnection: close\r\n\r\n";

View file

@ -1,13 +1,14 @@
use std::time::Duration;
use litellm_http::{ClientVariant, HttpClientConfig};
use litellm_model_catalog_refresh::{Failed, Fetched, RetryPolicy, S3Access, Start};
use litellm_model_catalog_refresh::{Failed, Fetched, GcsAccess, RetryPolicy, S3Access, Start};
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::PyBytes;
/// A cost map URL and how to connect to it. An `s3://` URL reaches S3 as an AWS SDK finds it in the environment,
/// with LiteLLM's `AWS_REGION_NAME` winning over the region it finds
/// with LiteLLM's `AWS_REGION_NAME` winning over the region it finds. A `gs://` URL reaches Cloud Storage with Google
/// application default credentials
#[pyclass(
frozen,
name = "NativeCostMapSource",
@ -42,8 +43,15 @@ impl NativeCostMapSource {
let policy = policy.0;
crate::execution::run_async_value(py, async move {
Ok(Outcome(
litellm_model_catalog_refresh::fetch(&client, &s3(), &url, policy, Start::Fresh)
.await,
litellm_model_catalog_refresh::fetch(
&client,
&s3(),
&GcsAccess::Environment,
&url,
policy,
Start::Fresh,
)
.await,
))
})
}
@ -68,6 +76,7 @@ impl NativeCostMapSource {
litellm_host_python::run_isolated(litellm_model_catalog_refresh::fetch(
&client,
&s3(),
&GcsAccess::Environment,
&url,
policy,
start,

View file

@ -6,8 +6,8 @@ import os
import ssl
import sys
import threading
from collections.abc import Generator, Iterator, Mapping, Sequence
from contextlib import contextmanager
from collections.abc import Callable, Generator, Iterator, Mapping, Sequence
from contextlib import AbstractContextManager, contextmanager
from dataclasses import dataclass, field
from pathlib import Path
from typing import Final, Literal
@ -15,6 +15,7 @@ from urllib.parse import urlsplit
import pytest
from integration._support.tls import server_context, write_self_signed_cert
from integration._support.vertex import service_account_json
from integration._support.wire import Reply, Request, wire_server
import litellm
@ -532,6 +533,98 @@ def test_an_s3_load_signs_for_the_region_and_reaches_the_endpoint_the_environmen
assert get_model_cost_map_source_info()["source"] == "remote"
_GCS_URL: Final = "gs://lit-prices/maps/cost.json"
_GCS_OBJECT_TARGET: Final = "/storage/v1/b/lit-prices/o/maps%2Fcost.json?alt=media"
_GCS_TOKEN: Final = "ya29.lit-scripted-gcs-token"
@pytest.fixture
def gcs(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Callable[[_Reply], AbstractContextManager[_Server]]:
@contextmanager
def serving(reply: _Reply) -> Generator[_Server]:
received: Final[list[Request]] = [] # mutable-ok: respond records every request the peer answers
def respond(request: Request) -> Reply:
received.append(request)
if request.target == "/_oauth/token":
return Reply(body=json.dumps({"access_token": _GCS_TOKEN, "expires_in": 3600}).encode())
return Reply(status=reply.status, body=reply.body, headers=reply.headers)
with wire_server(respond) as wire:
credentials: Final = tmp_path / "application_default_credentials.json"
credentials.write_text(service_account_json("lit-prices-project", wire.url))
monkeypatch.setenv("GOOGLE_APPLICATION_CREDENTIALS", str(credentials))
monkeypatch.setenv("STORAGE_EMULATOR_HOST", wire.url)
yield _Server(url=_GCS_URL, received=received)
return serving
@pytest.mark.usefixtures("remote_source", "own_model_cost")
@pytest.mark.parametrize("load", ["boot", "reload", "register_model"])
def test_every_load_reads_a_gs_url_from_the_emulator_without_credentials(
gcs: Callable[[_Reply], AbstractContextManager[_Server]], load: Literal["boot", "reload", "register_model"]
):
body: Final = _document_with(**{"lit-gcs-model": {"litellm_provider": "openai", "mode": "chat"}})
with gcs(_Reply(body=body)) as server:
rows: Final = _rows_loaded_through(load, server.url)
assert server.targets == [_GCS_OBJECT_TARGET]
assert server.authorizations == [""]
assert "lit-gcs-model" in rows
@pytest.mark.usefixtures("remote_source", "own_model_cost")
def test_a_gs_load_reaches_an_emulator_host_given_without_a_scheme(
gcs: Callable[[_Reply], AbstractContextManager[_Server]], monkeypatch: pytest.MonkeyPatch
):
with gcs(_Reply()) as server:
emulator: Final = os.environ["STORAGE_EMULATOR_HOST"]
monkeypatch.setenv("STORAGE_EMULATOR_HOST", emulator.removeprefix("http://"))
litellm.get_model_cost_map(url=server.url, max_attempts=1)
assert server.targets == [_GCS_OBJECT_TARGET]
assert get_model_cost_map_source_info()["source"] == "remote"
@pytest.mark.usefixtures("remote_source")
def test_a_gs_boot_load_records_the_remote_source(gcs: Callable[[_Reply], AbstractContextManager[_Server]]):
with gcs(_Reply()) as server:
litellm.get_model_cost_map(url=server.url)
source: Final = get_model_cost_map_source_info()
assert (source["source"], source["url"], source["fallback_reason"]) == ("remote", _GCS_URL, None)
@pytest.mark.usefixtures("remote_source")
@pytest.mark.parametrize("status", [403, 404], ids=["denied", "missing"])
def test_a_gs_boot_load_refused_by_storage_falls_back_without_a_retry(
gcs: Callable[[_Reply], AbstractContextManager[_Server]], status: int
):
with gcs(_Reply(status=status, body=b"refused")) as server:
litellm.get_model_cost_map(url=server.url, max_attempts=3)
assert server.requests == 1
source: Final = get_model_cost_map_source_info()
assert source["source"] == "local"
assert source["fallback_reason"] == f"Remote fetch failed: HTTP {status} from {_GCS_URL}"
@pytest.mark.usefixtures("remote_source", "own_model_cost")
@pytest.mark.parametrize("status", [403, 404], ids=["denied", "missing"])
def test_a_gs_reload_refused_by_storage_keeps_the_installed_map(
gcs: Callable[[_Reply], AbstractContextManager[_Server]], status: int
):
installed: Final = litellm.model_cost
with gcs(_Reply(status=status, body=b"refused")) as server:
result: Final = asyncio.run(refetch_model_cost_map(url=server.url, max_attempts=3))
assert isinstance(result, ModelCostMapReloadUnavailable)
assert result.reason == f"HTTP {status} from {_GCS_URL}"
assert server.requests == 1
assert litellm.model_cost is installed
@pytest.mark.usefixtures("remote_source", "own_model_cost")
@pytest.mark.parametrize("load", ["boot", "reload"])
def test_both_loads_fetch_through_the_proxy_the_environment_names(