Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/jcode-harness-api-server/src/translate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2032,6 +2032,9 @@ impl BridgeState {
.then_some((id, path))
})
.collect();
if candidates.is_empty() {
return Vec::new();
}
// `stat` is the dominant cost with 100k+ sessions. Match the TUI picker
// by doing those independent filesystem calls concurrently rather than
// serially blocking the API reply long enough for clients to time out.
Expand Down
30 changes: 30 additions & 0 deletions crates/jcode-harness-api-server/src/translate_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1530,6 +1530,36 @@ fn session_records_are_read_from_the_instance_home() {
);
}

#[test]
fn unattached_list_sessions_handles_no_session_candidates() {
let home = ScopedJcodeHome::new("empty-session-discovery");
let sessions_dir = home.path.join("sessions");
let mut state = BridgeState::default();

// Missing directory, empty directory, and entries that are all filtered out.
for layout in 0..3 {
if layout == 1 {
std::fs::create_dir(&sessions_dir).unwrap();
} else if layout == 2 {
std::fs::write(sessions_dir.join("not-a-session.txt"), "ignored").unwrap();
std::fs::create_dir(sessions_dir.join("directory.json")).unwrap();
}
for limit in [None, Some(0), Some(10)] {
let event = only_reply_event(state.api_request_to_legacy(&json!({
"req": "list_sessions", "id": 1, "limit": limit,
})));
assert_eq!(event, ApiEvent::Sessions { sessions: vec![] });
assert_eq!(
only_reply_event(state.api_request_to_legacy(&json!({"req": "ping", "id": 2}))),
ApiEvent::Pong,
);
}
}

write_session_record(&home.path, "first_session", &home.path);
assert_eq!(BridgeState::stored_session_ids(None), ["first_session"]);
}

#[test]
fn unattached_list_sessions_discovers_all_persisted_records() {
let home = ScopedJcodeHome::new("persisted-discovery");
Expand Down
13 changes: 2 additions & 11 deletions crates/jcode-provider-anthropic/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,9 +406,10 @@ pub fn format_content_blocks(blocks: &[ContentBlock], is_oauth: bool) -> Vec<Api
/// while the handler requires `task` + `wake_in_minutes`/`wake_at`), so every
/// call failed with "task is required for action=create" (#706). Forwarding the
/// real schema under the remapped name keeps the two in sync by construction.
/// `bash` is likewise forwarded: its curated schema omitted timeout units and
/// execution options (#1223). Only its OAuth name changes, not its definition.
const OAUTH_BUILTIN_LOCAL_TOOLS: &[&str] = &[
"subagent",
"bash",
"edit",
"glob",
"grep",
Expand Down Expand Up @@ -459,16 +460,6 @@ pub fn format_tools(tools: &[ToolDefinition], is_oauth: bool, cache_ttl_1h: bool
cache_control: None,
},
),
(
&["bash"],
ApiTool {
name: "Bash".to_string(),
description: "Executes a given bash command and returns its output."
.to_string(),
input_schema: json!({"type":"object","properties":{"command":{"type":"string"},"timeout":{"type":"integer"},"run_in_background":{"type":"boolean"},"justification":{"type":"string","description":"Only when re-issuing a command the destructive gate refused; explain which user request it serves."}},"required":["command"],"additionalProperties":false}),
cache_control: None,
},
),
(
&["edit"],
ApiTool {
Expand Down
56 changes: 50 additions & 6 deletions crates/jcode-provider-anthropic/src/oauth_tool_schema_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,61 @@
//! so `format_tools` hand-maintains a curated definition for a few of them.
//! Hand-maintained schemas drift from the real tools they stand in for, and the
//! failure is invisible until a model calls the tool and the handler rejects
//! the arguments. These tests pin the two drifts that reached users.
//! the arguments. These tests pin the drifts that reached users.

use super::*;
use jcode_message_types::ToolDefinition;
use serde_json::json;

fn tool_def(name: &str) -> ToolDefinition {
fn bash_registry_definition() -> ToolDefinition {
ToolDefinition {
name: name.to_string(),
description: format!("{name} description"),
input_schema: json!({"type":"object","properties":{}}),
name: "bash".to_string(),
description: "Run a bash command with the registered execution options.".to_string(),
input_schema: json!({
"type": "object",
"properties": {
"command": {"type": "string"},
"timeout": {
"type": "integer",
"description": "Timeout in MILLISECONDS (not seconds), e.g. 600000 = 10min; kills with exit 124. Omit for no timeout."
},
"run_in_background": {"type": "boolean"},
"intent": {"type": "string"},
"notify": {"type": "boolean"},
"wake": {"type": "boolean"},
"stall_wake_seconds": {"type": "integer"},
"justification": {
"type": "string",
"description": "Explain why the refused command serves the user request."
}
},
"required": ["command"]
}),
}
}

#[test]
fn oauth_bash_forwards_registered_schema_and_timeout_units() {
assert!(format_tools(&[], true, false).is_empty());
let mut registry_bash = bash_registry_definition();
// A new registry property must survive without another provider-side edit.
registry_bash.input_schema["properties"]["future_execution_option"] =
json!({"type": "boolean", "description": "A newly registered option."});

for is_oauth in [false, true] {
let formatted = format_tools(std::slice::from_ref(&registry_bash), is_oauth, false);
assert_eq!(formatted.len(), 1, "Bash must be advertised exactly once");
let bash = &formatted[0];
assert_eq!(bash.name, if is_oauth { "Bash" } else { "bash" });
assert_eq!(bash.input_schema, registry_bash.input_schema);
assert_eq!(bash.description, registry_bash.description);
assert!(
bash.input_schema["properties"]["timeout"]["description"]
.as_str()
.unwrap()
.contains("MILLISECONDS (not seconds)")
);
assert!(bash.cache_control.is_some());
}
}

Expand Down Expand Up @@ -66,7 +110,7 @@ fn oauth_schedule_wakeup_forwards_the_real_schedule_schema() {
fn oauth_bash_schema_advertises_the_justification_escape_hatch() {
// Regression for #722: the destructive gate consumes `justification`,
// so it has to be discoverable in the advertised schema.
let formatted = format_tools(&[tool_def("bash")], true, false);
let formatted = format_tools(&[bash_registry_definition()], true, false);
let bash = formatted
.iter()
.find(|t| t.name == "Bash")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,19 @@ impl Provider for OpenRouterProvider {
if let Some(supports_images) = self.static_image_input_support.get(&model_id) {
return *supports_images;
}
// The direct DeepSeek Flash aliases accept image_url parts (#1221).
// Keep Pro and unverified models text-only, and let explicit per-model
// input configuration above override this narrow built-in allowlist.
if self
.profile_id
.as_deref()
.is_some_and(|id| id.eq_ignore_ascii_case("deepseek"))
{
return matches!(
model_id.as_str(),
"deepseek-flash" | "deepseek-v4-flash" | "deepseek-v4-flash-vision-exp"
);
}
if Self::profile_rejects_image_input(self.profile_id.as_deref()) {
return false;
}
Expand Down
155 changes: 154 additions & 1 deletion crates/jcode-provider-openrouter-runtime/src/openrouter_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ fn named_openai_compatible_model_with_empty_input_preserves_image_support() {
}

#[test]
fn direct_deepseek_profile_does_not_advertise_image_input_support() {
fn direct_deepseek_profile_unknown_model_does_not_advertise_image_input_support() {
let provider = OpenRouterProvider {
profile_id: Some("deepseek".to_string()),
supports_provider_features: false,
Expand All @@ -414,6 +414,159 @@ fn direct_deepseek_profile_does_not_advertise_image_input_support() {
assert!(!provider.supports_image_input());
}

#[test]
fn deepseek_image_input_capability_matrix() {
for (profile, model, provider_features, expected) in [
("deepseek", "deepseek-flash", false, true),
("deepseek", "deepseek-v4-flash", false, true),
("deepseek", "deepseek-v4-flash-vision-exp", false, true),
("DeepSeek", "DEEPSEEK-FLASH", false, true),
("deepseek", "deepseek:deepseek-flash", false, true),
("deepseek", "deepseek-v4-pro", false, false),
("deepseek", "deepseek-pro", false, false),
("deepseek", "deepseek-chat", false, false),
("deepseek", "deepseek-reasoner", false, false),
("deepseek", "unknown", false, false),
("deepseek", "deepseek-v4-flash-free", false, false),
("deepseek", "deepseek-flash-future", false, false),
("deepseek", "deepseek/deepseek-flash", false, false),
("zai", "deepseek-flash", false, false),
("zai", "glm-5", false, false),
("openrouter", "deepseek-flash", true, false),
("custom", "unknown", false, true),
] {
let provider = OpenRouterProvider {
profile_id: Some(profile.to_string()),
model: Arc::new(RwLock::new(model.to_string())),
supports_provider_features: provider_features,
..make_custom_compatible_provider()
};
assert_eq!(
provider.supports_image_input(),
expected,
"profile={profile}, model={model}"
);
}
}

#[test]
fn deepseek_image_input_explicit_model_inputs_take_precedence() {
let _lock = ENV_LOCK.lock();
let _namespace = EnvVarGuard::remove("JCODE_OPENROUTER_CACHE_NAMESPACE");
for profile_id in ["deepseek", "zai"] {
let profile = jcode_base::config::NamedProviderConfig {
base_url: "http://localhost:1234/v1".to_string(),
auth: jcode_base::config::NamedProviderAuth::None,
default_model: Some("deepseek-flash".to_string()),
models: vec![
jcode_base::config::NamedProviderModelConfig {
id: "deepseek-flash".to_string(),
input: vec!["text".to_string()],
..Default::default()
},
jcode_base::config::NamedProviderModelConfig {
id: "deepseek-v4-pro".to_string(),
input: vec!["text".to_string(), "image".to_string()],
..Default::default()
},
],
..Default::default()
};
let provider = OpenRouterProvider::new_named_openai_compatible(profile_id, &profile)
.expect("named profile should initialize without auth");
assert!(
!provider.supports_image_input(),
"{profile_id}: explicit text-only Flash"
);
provider.set_model("deepseek-v4-pro").unwrap();
assert!(
provider.supports_image_input(),
"{profile_id}: explicit image-capable Pro"
);
}
}

#[test]
fn deepseek_image_input_captured_requests_preserve_only_allowed_pixels() {
let _lock = ENV_LOCK.lock();
// A real 1x1 PNG, retained byte-for-byte in the outbound data URL.
let pixels = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aK1cAAAAASUVORK5CYII=";
let messages = vec![Message {
role: Role::User,
content: vec![
ContentBlock::Text {
text: "describe this".to_string(),
cache_control: None,
},
ContentBlock::Image {
media_type: "image/png".to_string(),
data: pixels.to_string(),
},
],
timestamp: None,
tool_duration_ms: None,
}];
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime");
for (profile, model, override_support, expected) in [
("deepseek", "deepseek-flash", None, true),
("deepseek", "deepseek-v4-flash", None, true),
("deepseek", "deepseek-v4-flash-vision-exp", None, true),
("deepseek", "deepseek-v4-pro", None, false),
("deepseek", "unknown", None, false),
("zai", "deepseek-flash", None, false),
("deepseek", "deepseek-flash", Some(false), false),
("deepseek", "deepseek-v4-pro", Some(true), true),
] {
let (api_base, request_rx) = spawn_single_response_chat_server();
let provider = OpenRouterProvider {
api_base,
model: Arc::new(RwLock::new(model.to_string())),
profile_id: Some(profile.to_string()),
supports_provider_features: false,
supports_model_catalog: false,
static_image_input_support: override_support
.map(|value| HashMap::from([(model.to_string(), value)]))
.unwrap_or_default(),
..make_custom_compatible_provider()
};
rt.block_on(async {
let mut stream = provider.complete(&messages, &[], "", None).await.unwrap();
while let Some(event) = stream.next().await {
event.expect("local fixture stream should succeed");
}
});
let request = request_rx.recv_timeout(Duration::from_secs(2)).unwrap();
let body = parse_captured_request_body(&request);
assert_eq!(body["model"], model);
let parts = body["messages"]
.as_array()
.unwrap()
.iter()
.filter_map(|message| message["content"].as_array())
.flatten()
.filter(|part| part["type"] == "image_url")
.collect::<Vec<_>>();
assert_eq!(
parts.len(),
usize::from(expected),
"{profile}/{model}: {body}"
);
if expected {
assert_eq!(
parts[0]["image_url"]["url"],
format!("data:image/png;base64,{pixels}")
);
assert!(!request.contains("Image omitted"), "{body}");
} else {
assert!(request.contains("Image omitted"), "{body}");
assert!(!request.contains(pixels), "{body}");
}
}
}

#[test]
fn direct_zai_profile_does_not_advertise_image_input_support() {
let provider = OpenRouterProvider {
Expand Down
Loading