Skip to content

Commit 484c449

Browse files
authored
feat: make gcp auth work seamlessly from vscode (#1860)
Implement loading GCP oauth token via a JS callback using the gcp node sdk, saving users from having to create a service account and put the secret key in an env var to use the playground. Also convert from using a DI-esque pattern where things are wired through RuntimeContext to instead using a singleton pattern which uses channels to mimic RPCs across the wasm/js boundary to drive the callbacks (necessary because of the `Send` requirements that tokio imposes on `js_sys::Value` which is not `Send`). DONE: - [x] - test the errors - what happens when the user is not gcloud auth'd? is the error message good? - [x] - restore the vertex wasm_auth path when env vars are explicitly set - [x] - fix this error (explicit env var should never resolve to file in wasm) "Failed to auth - cannot load credentials from a file in WASM (path='$INTE...', path.len=51)", - [x] - fix this error (explicit env var should have a useful error) "Failed to auth - cannot load credentials from a file in WASM (path='$INTE...', path.len=51)", - [x] - try to drop the custom result enums - [x] - rename aws* to js-callback-bridge* - [x] - update docs (both this and the vertex docs saying that claude is not supported) Fixes #1857 <!-- ELLIPSIS_HIDDEN --> ---- > [!IMPORTANT] > Implement seamless GCP OAuth token loading from VSCode using JS callbacks, refactor AWS credential handling, and update documentation. > > - **Behavior**: > - Implement GCP OAuth token loading via JS callback using GCP Node SDK, removing need for service account keys in env vars. > - Refactor AWS credential handling to use JS callback bridge pattern. > - Update `VertexAuth` in `vertex/std_auth.rs` and `vertex/wasm_auth.rs` to support new auth strategies. > - **JS Callback Bridge**: > - Introduce `JsCallbackProvider` in `js_callback_provider.rs` for AWS and GCP credential requests. > - Implement `init_js_callback_bridge()` in `js_callback_bridge.rs` to set up JS callbacks for credential loading. > - **Documentation**: > - Update `vertex.mdx` to reflect new GCP auth method and deprecate `credentials_content`. > - **Misc**: > - Rename `AuthStrategy::JsonString` and `AuthStrategy::JsonFile` to `StringContainingJson` and `MaybeFilePath` respectively. > - Add `derive-new` dependency in `Cargo.toml` and `Cargo.lock`. > > <sup>This description was created by </sup>[<img alt="Ellipsis" src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=BoundaryML%2Fbaml&utm_source=github&utm_medium=referral)<sup> for daee78a. You can [customize](https://app.ellipsis.dev/BoundaryML/settings/summaries) this summary. It will automatically update as commits are pushed.</sup> <!-- ELLIPSIS_HIDDEN -->
1 parent 8801482 commit 484c449

28 files changed

Lines changed: 815 additions & 371 deletions

File tree

engine/Cargo.lock

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

engine/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ bytes = "1.6.0"
5050
cfg-if = "1.0.0"
5151
clap = { version = "4.4.6", features = ["cargo", "derive"] }
5252
dashmap = "5.5.3"
53+
derive-new = "0.7"
5354
derive_builder = "0.20.0"
5455
derive_more = { version = "0.99.18", features = ["constructor"] }
5556
either = "1.8.1"

engine/baml-lib/llm-client/src/clients/vertex.rs

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ use anyhow::Result;
99
use baml_types::{GetEnvVar, StringOr, UnresolvedValue};
1010
use either::Either;
1111
use indexmap::IndexMap;
12-
use secrecy::SecretString;
13-
use serde::Deserialize;
1412

1513
use super::helpers::{Error, PropertyHandler, UnresolvedUrl};
1614

@@ -26,19 +24,12 @@ enum UnresolvedGcpAuthStrategy<Meta> {
2624
SystemDefault,
2725
}
2826

29-
#[derive(Debug, Deserialize)]
30-
pub struct ServiceAccount {
31-
pub token_uri: String,
32-
pub project_id: String,
33-
pub client_email: String,
34-
pub private_key: SecretString,
35-
}
36-
3727
pub enum ResolvedGcpAuthStrategy {
38-
/// This is the mechanism that GCP SDKs usually support for GOOGLE_APPLICATION_CREDENTIALS
39-
FilePath(String),
28+
/// GCP SDKs usually support passing in GOOGLE_APPLICATION_CREDENTIALS as a file path
29+
/// In WASM, however, we treat both StringContainingJson and MaybeFilePath as a string
30+
MaybeFilePath(String),
4031
/// Because the WASM playground needs a way to pass in credentials.
41-
JsonString(String),
32+
StringContainingJson(String),
4233
/// JsonObject was implemented for a user: https://github.com/BoundaryML/baml/issues/1001
4334
JsonObject(IndexMap<String, String>),
4435
/// The normal GCP application default credentials flow, after checking
@@ -94,8 +85,8 @@ impl<Meta> UnresolvedGcpAuthStrategy<Meta> {
9485
UnresolvedGcpAuthStrategy::CredentialsString(s) => {
9586
let s = s.resolve(ctx)?;
9687
match serde_json::from_str::<serde_json::Value>(&s) {
97-
Ok(_) => ResolvedGcpAuthStrategy::JsonString(s),
98-
Err(_) => ResolvedGcpAuthStrategy::FilePath(s),
88+
Ok(_) => ResolvedGcpAuthStrategy::StringContainingJson(s),
89+
Err(_) => ResolvedGcpAuthStrategy::MaybeFilePath(s),
9990
}
10091
}
10192
UnresolvedGcpAuthStrategy::CredentialsJsonObject(m) => {
@@ -107,7 +98,7 @@ impl<Meta> UnresolvedGcpAuthStrategy<Meta> {
10798
}
10899
UnresolvedGcpAuthStrategy::CredentialsContentString(s) => {
109100
let s = s.resolve(ctx)?;
110-
ResolvedGcpAuthStrategy::JsonString(s)
101+
ResolvedGcpAuthStrategy::StringContainingJson(s)
111102
}
112103
UnresolvedGcpAuthStrategy::SystemDefault => {
113104
log::debug!("Neither options.credentials nor options.credentials_content are set, falling back to env vars");
@@ -125,16 +116,16 @@ impl<Meta> UnresolvedGcpAuthStrategy<Meta> {
125116
log::warn!("Resolving GOOGLE_APPLICATION_CREDENTIALS from env, but it is an empty string");
126117
}
127118
match serde_json::from_str::<serde_json::Value>(&credentials) {
128-
Ok(_) => ResolvedGcpAuthStrategy::JsonString(credentials),
129-
Err(_) => ResolvedGcpAuthStrategy::FilePath(credentials),
119+
Ok(_) => ResolvedGcpAuthStrategy::StringContainingJson(credentials),
120+
Err(_) => ResolvedGcpAuthStrategy::MaybeFilePath(credentials),
130121
}
131122
}
132123
(None, Some(credentials_content)) => {
133124
log::debug!("Using GOOGLE_APPLICATION_CREDENTIALS_CONTENT from env");
134125
if credentials_content.is_empty() {
135126
log::warn!("Resolving GOOGLE_APPLICATION_CREDENTIALS_CONTENT from env, but it is an empty string");
136127
}
137-
ResolvedGcpAuthStrategy::JsonString(credentials_content)
128+
ResolvedGcpAuthStrategy::StringContainingJson(credentials_content)
138129
}
139130
(None, None) => {
140131
log::debug!("Using UseSystemDefault strategy");

engine/baml-runtime/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ cfg-if.workspace = true
2626
clap.workspace = true
2727
colored = "2.1.0"
2828
dashmap.workspace = true
29+
derive-new.workspace = true
2930
derive_more.workspace = true
3031
dunce = "1.0.4"
3132
either.workspace = true

engine/baml-runtime/src/cli/serve/mod.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -311,8 +311,7 @@ Tip: test that the server is up using `curl http://localhost:{}/_debug/ping`
311311
Err(e) => return e.into_response(),
312312
};
313313

314-
let ctx_mgr =
315-
RuntimeContextManager::new_from_env_vars(std::env::vars().collect(), None, None);
314+
let ctx_mgr = RuntimeContextManager::new_from_env_vars(std::env::vars().collect(), None);
316315
let client_registry = b_options.and_then(|options| options.client_registry);
317316

318317
let locked = self.b.read().await;
@@ -400,7 +399,7 @@ Tip: test that the server is up using `curl http://localhost:{}/_debug/ping`
400399

401400
tokio::spawn(async move {
402401
let ctx_mgr =
403-
RuntimeContextManager::new_from_env_vars(std::env::vars().collect(), None, None);
402+
RuntimeContextManager::new_from_env_vars(std::env::vars().collect(), None);
404403

405404
let result_stream = self.b.read().await.stream_function(
406405
b_fn,

engine/baml-runtime/src/internal/llm_client/primitive/aws/aws_client.rs

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ use crate::internal::llm_client::{
5151
ModelFeatures, ResolveMediaUrls,
5252
};
5353
use crate::tracingv2::storage::storage::BAML_TRACER;
54-
use crate::{json_body, AwsCredProvider, JsonBodyInput, RenderCurlSettings, RuntimeContext};
54+
use crate::{json_body, JsonBodyInput, RenderCurlSettings, RuntimeContext};
5555
// See https://github.com/awslabs/aws-sdk-rust/issues/169
5656
use super::custom_http_client;
5757
#[cfg(target_arch = "wasm32")]
@@ -307,7 +307,6 @@ impl AwsClient {
307307
&self,
308308
span_id: Option<Uuid>,
309309
http_request_id: &HttpRequestId,
310-
aws_cred_provider: AwsCredProvider,
311310
) -> Result<bedrock::Client> {
312311
#[cfg(target_arch = "wasm32")]
313312
let loader = super::wasm::load_aws_config();
@@ -324,7 +323,6 @@ impl AwsClient {
324323
#[cfg(target_arch = "wasm32")]
325324
{
326325
loader.credentials_provider(WasmAwsCreds {
327-
aws_cred_provider: aws_cred_provider.clone(),
328326
profile: self.properties.profile.clone(),
329327
})
330328
}
@@ -564,11 +562,7 @@ impl WithStreamChat for AwsClient {
564562
let prompt = internal_baml_jinja::RenderedPrompt::Chat(chat_messages.to_vec());
565563

566564
let aws_client = match self
567-
.client_anyhow(
568-
ctx.span_id.clone(),
569-
&http_request_id,
570-
ctx.aws_cred_provider.clone(),
571-
)
565+
.client_anyhow(ctx.span_id.clone(), &http_request_id)
572566
.await
573567
{
574568
Ok(c) => c,
@@ -864,11 +858,7 @@ impl WithChat for AwsClient {
864858
let prompt = internal_baml_jinja::RenderedPrompt::Chat(chat_messages.to_vec());
865859

866860
let aws_client = match self
867-
.client_anyhow(
868-
ctx.span_id.clone(),
869-
&http_request_id,
870-
ctx.aws_cred_provider.clone(),
871-
)
861+
.client_anyhow(ctx.span_id.clone(), &http_request_id)
872862
.await
873863
{
874864
Ok(c) => c,

engine/baml-runtime/src/internal/llm_client/primitive/aws/wasm.rs

Lines changed: 19 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ use std::sync::Arc;
3838
use std::time::SystemTime;
3939
use time::OffsetDateTime;
4040

41-
use crate::{AwsCredProvider, AwsCredProviderImpl, AwsCredResult};
41+
use crate::{js_callback_provider::get_js_callback_provider, AwsCredResult, JsCallbackProvider};
4242

4343
pub fn load_aws_config() -> ConfigLoader {
4444
log::debug!("Loading AWS config for wasm specifically");
@@ -160,8 +160,6 @@ impl HttpClient for BrowserHttp2 {
160160
}
161161

162162
pub(super) struct WasmAwsCreds {
163-
// pub default_chain: aws_config::default_provider::credentials::DefaultCredentialsChain,
164-
pub aws_cred_provider: AwsCredProvider,
165163
pub profile: Option<String>,
166164
}
167165

@@ -176,59 +174,25 @@ impl std::fmt::Debug for WasmAwsCreds {
176174

177175
impl WasmAwsCreds {
178176
async fn provide_credentials_impl(&self) -> aws_credential_types::provider::Result {
179-
match self.aws_cred_provider.clone() {
180-
Some(AwsCredProviderImpl {
181-
req_tx,
182-
mut resp_rx,
183-
}) => {
184-
if let Err(e) = req_tx.send(self.profile.clone()).await {
185-
log::error!(
186-
"Failed to send AWS cred request across WASM bridge: {:?}",
187-
e
188-
);
189-
return Err(CredentialsError::unhandled(e));
190-
};
191-
let creds = match resp_rx.recv().await {
192-
Ok(Ok(creds)) => creds,
193-
Ok(Err(e)) => {
194-
log::error!("Error in AWS cred provider: {:?}", e);
195-
return Err(CredentialsError::unhandled(e));
196-
}
197-
Err(e) => {
198-
log::error!(
199-
"Failed to recv AWS cred response across WASM bridge: {:?}",
200-
e
201-
);
202-
return Err(CredentialsError::unhandled(e));
203-
}
204-
};
205-
206-
match creds {
207-
AwsCredResult::Ok {
208-
access_key_id,
209-
secret_access_key,
210-
session_token,
211-
expiration,
212-
..
213-
} => Ok(Credentials::new(
214-
access_key_id,
215-
secret_access_key,
216-
session_token,
217-
match expiration {
218-
Some(expiration) => match expiration.parse::<DateTime<Utc>>() {
219-
Ok(dt) => Some(dt.into()),
220-
Err(_) => None,
221-
},
222-
None => None,
223-
},
224-
"baml-playground-wasm-bridge",
225-
)),
226-
AwsCredResult::Err { name, message } => Err(CredentialsError::unhandled(
227-
format!("{}: {}", name, message),
228-
)),
229-
}
177+
let cred_provider = get_js_callback_provider().map_err(CredentialsError::unhandled)?;
178+
match cred_provider.aws_req(self.profile.clone()).await {
179+
Err(e) => {
180+
log::error!("Error calling AWS cred provider: {:?}", e);
181+
return Err(CredentialsError::unhandled(e));
230182
}
231-
None => Err(CredentialsError::not_loaded_no_source()),
183+
Ok(aws_creds) => Ok(Credentials::new(
184+
aws_creds.access_key_id,
185+
aws_creds.secret_access_key,
186+
aws_creds.session_token,
187+
match aws_creds.expiration {
188+
Some(expiration) => match expiration.parse::<DateTime<Utc>>() {
189+
Ok(dt) => Some(dt.into()),
190+
Err(_) => None,
191+
},
192+
None => None,
193+
},
194+
"baml-playground-wasm-bridge",
195+
)),
232196
}
233197
}
234198
}

engine/baml-runtime/src/internal/llm_client/primitive/vertex/std_auth.rs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,30 @@ pub enum VertexAuth {
1414
impl VertexAuth {
1515
pub async fn new(auth_strategy: &ResolvedGcpAuthStrategy) -> Result<VertexAuth> {
1616
match auth_strategy {
17-
ResolvedGcpAuthStrategy::FilePath(path) => {
17+
ResolvedGcpAuthStrategy::MaybeFilePath(path) => {
1818
log::debug!("Attempting to auth using JsonFile strategy");
19-
let authz_user = gcp_auth::CustomServiceAccount::from_file(&path)?;
19+
let authz_user =
20+
gcp_auth::CustomServiceAccount::from_file(&path).context(format!(
21+
"Failed to parse credentials as JSON file: {}",
22+
serde_json::to_string(&path)
23+
.expect("Serialization of string should always succeed")
24+
))?;
2025
Ok(VertexAuth::CustomServiceAccount(authz_user))
2126
}
22-
ResolvedGcpAuthStrategy::JsonString(s) => {
27+
ResolvedGcpAuthStrategy::StringContainingJson(s) => {
2328
log::debug!("Attempting to auth using JsonString strategy");
24-
let authz_user = gcp_auth::CustomServiceAccount::from_json(&s)?;
29+
let authz_user = gcp_auth::CustomServiceAccount::from_json(&s).context(format!(
30+
"Failed to parse credentials as JSON string: {}",
31+
{
32+
let s = serde_json::to_string(&s)
33+
.expect("Serialization of string should always succeed");
34+
if s.len() > 8 {
35+
format!("{}...{}", &s[..4], &s[s.len() - 4..])
36+
} else {
37+
s
38+
}
39+
}
40+
))?;
2541
Ok(VertexAuth::CustomServiceAccount(authz_user))
2642
}
2743
ResolvedGcpAuthStrategy::JsonObject(o) => {

engine/baml-runtime/src/internal/llm_client/primitive/vertex/vertex_client.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,7 @@ use chrono::{Duration, Utc};
3030
use futures::StreamExt;
3131
#[cfg(not(target_arch = "wasm32"))]
3232
use gcp_auth::TokenProvider;
33-
use internal_llm_client::vertex::{
34-
BaseUrlOrLocation, ResolvedGcpAuthStrategy, ResolvedVertex, ServiceAccount,
35-
};
33+
use internal_llm_client::vertex::{BaseUrlOrLocation, ResolvedGcpAuthStrategy, ResolvedVertex};
3634
use internal_llm_client::{
3735
AllowedRoleMetadata, ClientProvider, ResolvedClientProperty, UnresolvedClientProperty,
3836
};

0 commit comments

Comments
 (0)