Skip to content

Commit 09ddf58

Browse files
committed
fix: address vertex review findings
1 parent 19e7bed commit 09ddf58

3 files changed

Lines changed: 139 additions & 54 deletions

File tree

crates/openshell-cli/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ fn resolve_sandbox_name(name: Option<String>, gateway: &str) -> Result<String> {
184184
let last = load_last_sandbox(gateway).ok_or_else(|| {
185185
miette::miette!(
186186
"No sandbox name provided and no last-used sandbox.\n\
187-
Specify a sandbox name or connect to one first: nav sandbox connect <name>"
187+
Specify a sandbox name or connect to one first: openshell sandbox connect <name>"
188188
)
189189
})?;
190190
eprintln!("{} Using sandbox '{}' (last used)", "→".bold(), last.bold());
@@ -3490,7 +3490,7 @@ mod tests {
34903490
let err = resolve_sandbox_name(None, "unknown-gateway").unwrap_err();
34913491
let msg = err.to_string();
34923492
assert!(
3493-
msg.contains("nav sandbox connect"),
3493+
msg.contains("openshell sandbox connect"),
34943494
"expected helpful hint in error, got: {msg}"
34953495
);
34963496
});

crates/openshell-cli/src/run.rs

Lines changed: 102 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ use openshell_providers::{
6060
};
6161
use owo_colors::OwoColorize;
6262
use std::collections::{HashMap, HashSet};
63-
use std::io::{IsTerminal, Read, Write};
63+
use std::io::{ErrorKind, IsTerminal, Read, Write};
6464
use std::path::{Path, PathBuf};
6565
use std::process::Command;
6666
use std::time::{Duration, Instant};
@@ -76,6 +76,15 @@ pub use openshell_core::forward::{
7676
find_forward_by_port, list_forwards, stop_forward, stop_forwards_for_sandbox,
7777
};
7878

79+
#[derive(Debug, PartialEq, Eq)]
80+
enum SandboxUploadPlan {
81+
GitAware {
82+
base_dir: PathBuf,
83+
files: Vec<String>,
84+
},
85+
Regular,
86+
}
87+
7988
/// Convert a sandbox phase integer to a human-readable string.
8089
fn phase_name(phase: i32) -> &'static str {
8190
match SandboxPhase::try_from(phase) {
@@ -2007,26 +2016,29 @@ pub async fn sandbox_create(
20072016
"\u{2022}".dimmed(),
20082017
);
20092018
let local = Path::new(local_path);
2010-
if *git_ignore && let Ok((base_dir, files)) = git_sync_files(local) {
2011-
sandbox_sync_up_files(
2012-
&effective_server,
2013-
&sandbox_name,
2014-
&base_dir,
2015-
&files,
2016-
local,
2017-
dest,
2018-
&effective_tls,
2019-
)
2020-
.await?;
2021-
} else if local.exists() {
2022-
sandbox_sync_up(
2023-
&effective_server,
2024-
&sandbox_name,
2025-
local,
2026-
dest,
2027-
&effective_tls,
2028-
)
2029-
.await?;
2019+
match sandbox_upload_plan(local, *git_ignore)? {
2020+
SandboxUploadPlan::GitAware { base_dir, files } => {
2021+
sandbox_sync_up_files(
2022+
&effective_server,
2023+
&sandbox_name,
2024+
&base_dir,
2025+
&files,
2026+
local,
2027+
dest,
2028+
&effective_tls,
2029+
)
2030+
.await?;
2031+
}
2032+
SandboxUploadPlan::Regular => {
2033+
sandbox_sync_up(
2034+
&effective_server,
2035+
&sandbox_name,
2036+
local,
2037+
dest,
2038+
&effective_tls,
2039+
)
2040+
.await?;
2041+
}
20302042
}
20312043
eprintln!(" {} Files uploaded", "\u{2713}".green().bold());
20322044
}
@@ -5616,6 +5628,28 @@ pub fn git_sync_files(local_path: &Path) -> Result<(PathBuf, Vec<String>)> {
56165628
Ok((base_dir, files))
56175629
}
56185630

5631+
fn sandbox_upload_plan(local_path: &Path, git_ignore: bool) -> Result<SandboxUploadPlan> {
5632+
let metadata = std::fs::symlink_metadata(local_path).map_err(|err| {
5633+
if err.kind() == ErrorKind::NotFound {
5634+
miette::miette!("local path does not exist: {}", local_path.display())
5635+
} else {
5636+
miette::miette!(
5637+
"failed to inspect local upload path: {}",
5638+
local_path.display()
5639+
)
5640+
}
5641+
})?;
5642+
5643+
if git_ignore
5644+
&& !metadata.file_type().is_symlink()
5645+
&& let Ok((base_dir, files)) = git_sync_files(local_path)
5646+
{
5647+
return Ok(SandboxUploadPlan::GitAware { base_dir, files });
5648+
}
5649+
5650+
Ok(SandboxUploadPlan::Regular)
5651+
}
5652+
56195653
fn scrub_git_env(command: &mut Command) -> &mut Command {
56205654
for key in [
56215655
"GIT_DIR",
@@ -7191,7 +7225,8 @@ mod tests {
71917225
plaintext_gateway_is_remote, progress_step_from_metadata,
71927226
provider_profile_allows_refresh_bootstrap, provisioning_timeout_message,
71937227
ready_false_condition_message, refresh_status_header, refresh_status_row, resolve_from,
7194-
sandbox_should_persist, service_expose_status_error, service_url_for_gateway,
7228+
sandbox_should_persist, sandbox_upload_plan, service_expose_status_error,
7229+
service_url_for_gateway,
71957230
};
71967231
use crate::TEST_ENV_LOCK;
71977232
use hyper::StatusCode;
@@ -7936,6 +7971,51 @@ mod tests {
79367971
assert_eq!(files, vec!["file.txt", "inner/child.txt"]);
79377972
}
79387973

7974+
#[test]
7975+
fn sandbox_upload_plan_errors_for_missing_local_path() {
7976+
let tmpdir = tempfile::tempdir().expect("create tmpdir");
7977+
let missing = tmpdir.path().join("missing");
7978+
7979+
let err = sandbox_upload_plan(&missing, false).expect_err("missing path should error");
7980+
7981+
assert!(
7982+
err.to_string().contains("local path does not exist"),
7983+
"expected missing-path error, got: {err}"
7984+
);
7985+
}
7986+
7987+
#[test]
7988+
fn sandbox_upload_plan_errors_for_missing_local_path_with_git_ignore() {
7989+
let tmpdir = tempfile::tempdir().expect("create tmpdir");
7990+
let repo = tmpdir.path().join("repo");
7991+
fs::create_dir_all(&repo).expect("create repo");
7992+
init_git_repo(&repo);
7993+
let missing = repo.join("missing");
7994+
7995+
let err = sandbox_upload_plan(&missing, true).expect_err("missing path should error");
7996+
7997+
assert!(
7998+
err.to_string().contains("local path does not exist"),
7999+
"expected missing-path error, got: {err}"
8000+
);
8001+
}
8002+
8003+
#[cfg(unix)]
8004+
#[test]
8005+
fn sandbox_upload_plan_uses_regular_upload_for_symlinks() {
8006+
let tmpdir = tempfile::tempdir().expect("create tmpdir");
8007+
let repo = tmpdir.path().join("repo");
8008+
fs::create_dir_all(repo.join("real-dir")).expect("create repo");
8009+
init_git_repo(&repo);
8010+
fs::write(repo.join("real-dir/file.txt"), "file").expect("write file.txt");
8011+
std::os::unix::fs::symlink("real-dir", repo.join("link-dir")).expect("create symlink");
8012+
8013+
let plan = sandbox_upload_plan(&repo.join("link-dir"), true)
8014+
.expect("symlink upload should be planned");
8015+
8016+
assert_eq!(plan, super::SandboxUploadPlan::Regular);
8017+
}
8018+
79398019
#[test]
79408020
fn git_sync_files_ignores_inherited_git_env() {
79418021
let _lock = TEST_ENV_LOCK

crates/openshell-router/src/backend.rs

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -213,38 +213,43 @@ fn prepare_backend_request(
213213
// path) and inject "anthropic_version" (required in the body, not a header).
214214
// Non-JSON bodies pass through unchanged; model rewrite and version injection
215215
// are silently skipped. Such bodies would be rejected by the upstream anyway.
216-
let body = serde_json::from_slice::<serde_json::Value>(&body).map_or(body, |mut json| {
217-
if let Some(obj) = json.as_object_mut() {
218-
// Vertex AI Anthropic endpoints require anthropic_version in the body.
219-
// Standard Anthropic SDK sends it as a header; Vertex AI needs it as a body field.
220-
// We inject it only for the Vertex rawPredict-style route contract used for
221-
// Anthropic publisher endpoints, not for arbitrary model-in-path routes.
222-
let needs_vertex_anthropic_version = is_vertex_anthropic_rawpredict_route(route);
223-
if needs_vertex_anthropic_version {
224-
// Vertex AI rawPredict encodes the model in the URL path, not
225-
// the request body. Clients using the standard Anthropic API
226-
// (e.g. Claude Code via inference.local) always send "model"
227-
// in the body; strip it so Vertex AI does not reject the
228-
// request with "Extra inputs are not permitted".
229-
obj.remove("model");
230-
} else {
231-
obj.insert(
232-
"model".to_string(),
233-
serde_json::Value::String(route.model.clone()),
234-
);
235-
}
236-
if needs_vertex_anthropic_version && !obj.contains_key("anthropic_version") {
237-
obj.insert(
238-
"anthropic_version".to_string(),
239-
serde_json::Value::String(VERTEX_ANTHROPIC_VERSION.to_string()),
240-
);
216+
let body = match serde_json::from_slice::<serde_json::Value>(&body) {
217+
Ok(mut json) => {
218+
if let Some(obj) = json.as_object_mut() {
219+
// Vertex AI Anthropic endpoints require anthropic_version in the body.
220+
// Standard Anthropic SDK sends it as a header; Vertex AI needs it as a body field.
221+
// We inject it only for the Vertex rawPredict-style route contract used for
222+
// Anthropic publisher endpoints, not for arbitrary model-in-path routes.
223+
let needs_vertex_anthropic_version = is_vertex_anthropic_rawpredict_route(route);
224+
if needs_vertex_anthropic_version {
225+
// Vertex AI rawPredict encodes the model in the URL path, not
226+
// the request body. Clients using the standard Anthropic API
227+
// (e.g. Claude Code via inference.local) always send "model"
228+
// in the body; strip it so Vertex AI does not reject the
229+
// request with "Extra inputs are not permitted".
230+
obj.remove("model");
231+
} else {
232+
obj.insert(
233+
"model".to_string(),
234+
serde_json::Value::String(route.model.clone()),
235+
);
236+
}
237+
if needs_vertex_anthropic_version && !obj.contains_key("anthropic_version") {
238+
obj.insert(
239+
"anthropic_version".to_string(),
240+
serde_json::Value::String(VERTEX_ANTHROPIC_VERSION.to_string()),
241+
);
242+
}
241243
}
244+
245+
bytes::Bytes::from(serde_json::to_vec(&json).map_err(|err| {
246+
RouterError::Internal(format!(
247+
"failed to serialize rewritten inference request body: {err}"
248+
))
249+
})?)
242250
}
243-
bytes::Bytes::from(
244-
serde_json::to_vec(&json)
245-
.expect("re-serializing a valid serde_json::Value cannot fail"),
246-
)
247-
});
251+
Err(_) => body,
252+
};
248253
builder = builder.body(body);
249254

250255
Ok((builder, url))

0 commit comments

Comments
 (0)