Skip to content
Open
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
10 changes: 10 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,16 @@ For library tests only:
cargo test --lib
```

**Build Rust artifacts with the correct targets:**

```bash
cd crates
cargo build --release --target wasm32-wasip1 -p llm_gateway -p prompt_gateway
cargo build --release -p brightstaff -p hermesllm -p common
```

Do not run a blanket workspace-native build such as `cargo build --release` from `crates/`. The `llm_gateway` and `prompt_gateway` crates are Proxy-WASM `cdylib`s and must be built for `wasm32-wasip1`, while `brightstaff`, `hermesllm`, and `common` build natively.

**Run Python CLI tests:**

```bash
Expand Down
8 changes: 5 additions & 3 deletions cli/planoai/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,10 +471,12 @@ def up(
else:
env_file_dict = load_env_file_to_dict(app_env_file)
for access_key in access_keys:
if env_file_dict.get(access_key) is None:
missing_keys.append(access_key)
else:
if env_file_dict.get(access_key) is not None:
env_stage[access_key] = env_file_dict[access_key]
elif env.get(access_key) is not None:
env_stage[access_key] = env.get(access_key)
else:
missing_keys.append(access_key)

if missing_keys:
_print_missing_keys(console, missing_keys)
Expand Down
4 changes: 2 additions & 2 deletions crates/brightstaff/src/handlers/llm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use common::llm_providers::LlmProviders;
use hermesllm::apis::openai::Message;
use hermesllm::apis::openai_responses::InputParam;
use hermesllm::clients::{SupportedAPIsFromClient, SupportedUpstreamAPIs};
use hermesllm::{ProviderRequest, ProviderRequestType};
use hermesllm::{serialize_for_upstream, ProviderRequest, ProviderRequestType};
use http_body_util::combinators::BoxBody;
use http_body_util::BodyExt;
use hyper::header::{self};
Expand Down Expand Up @@ -284,7 +284,7 @@ async fn llm_chat_inner(

// Serialize request for upstream BEFORE router consumes it
let client_request_bytes_for_upstream: Bytes =
match ProviderRequestType::to_bytes(&client_request) {
match serialize_for_upstream(&client_request, provider_id) {
Ok(bytes) => bytes.into(),
Err(err) => {
warn!(error = %err, "failed to serialize request for upstream");
Expand Down
97 changes: 87 additions & 10 deletions crates/hermesllm/src/apis/openai_responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,16 +126,12 @@ pub enum InputParam {
pub enum InputItem {
/// Input message (role + content)
Message(InputMessage),
/// Item reference
ItemReference {
#[serde(rename = "type")]
item_type: String,
id: String,
},
/// Function call emitted by model in prior turn
FunctionCall {
#[serde(rename = "type")]
item_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
id: Option<String>,
name: String,
arguments: String,
call_id: String,
Expand All @@ -147,6 +143,18 @@ pub enum InputItem {
call_id: String,
output: serde_json::Value,
},
/// Item reference
///
/// Keep this after concrete item variants. Some Responses items include an
/// `id` plus additional required fields (`function_call` has `call_id`,
/// `name`, and `arguments`). With serde's untagged enum matching, placing
/// this broad reference shape first silently drops those fields and sends an
/// invalid upstream item.
ItemReference {
#[serde(rename = "type")]
item_type: String,
id: String,
},
}

/// Input message with role and content
Expand Down Expand Up @@ -280,16 +288,31 @@ pub struct ConversationParam {
pub id: Option<String>,
}

/// Tool definitions
/// Tool definitions.
///
/// Supports both the canonical OpenAI Responses flat tool shape:
/// { "type": "function", "name": "...", "description": "...", "parameters": {...} }
/// and the nested chat-completions-compatible shape:
/// { "type": "function", "function": { "name": "...", "description": "...", "parameters": {...} } }
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Tool {
/// Function tool - flat structure in Responses API
/// Function tool — accepts both flat and nested `function` object shapes.
Function {
name: String,
/// Top-level name (flat shape).
name: Option<String>,
/// Top-level description (flat shape).
description: Option<String>,
/// Top-level parameters (flat shape).
parameters: Option<serde_json::Value>,
/// Top-level strict flag (flat shape).
strict: Option<bool>,
/// Nested `function` object (nested/compat shape).
///
/// When present, `name`/`description`/`parameters` from the outer level are
/// ignored in favour of the values inside this object.
#[serde(default, flatten)]
function: Option<FunctionDef>,
},
/// File search tool
FileSearch {
Expand Down Expand Up @@ -321,6 +344,47 @@ pub enum Tool {
},
}

impl Tool {
pub fn name(&self) -> Option<&str> {
match self {
Tool::Function { name, function, .. } => function
.as_ref()
.and_then(|f| f.name.as_ref())
.map(|s| s.as_str())
.or_else(|| name.as_ref().map(|s| s.as_str())),
Tool::Custom { name, .. } => name.as_deref(),
_ => None,
}
}

pub fn description(&self) -> Option<&String> {
match self {
Tool::Function {
description,
function,
..
} => description
.as_ref()
.or_else(|| function.as_ref().and_then(|f| f.description.as_ref())),
Tool::Custom { description, .. } => description.as_ref(),
_ => None,
}
}

pub fn parameters(&self) -> Option<&serde_json::Value> {
match self {
Tool::Function {
parameters,
function,
..
} => parameters
.as_ref()
.or_else(|| function.as_ref().and_then(|f| f.parameters.as_ref())),
_ => None,
}
}
}

/// Ranking options for file search
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize)]
Expand All @@ -343,6 +407,16 @@ pub struct UserLocation {
pub timezone: Option<String>,
}

/// Inner function definition — used inside the nested `function` object.
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FunctionDef {
pub name: Option<String>,
pub description: Option<String>,
pub parameters: Option<serde_json::Value>,
pub strict: Option<bool>,
}

/// Tool choice options
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
Expand Down Expand Up @@ -1158,7 +1232,10 @@ impl ProviderRequest for ResponsesAPIRequest {
tools
.iter()
.filter_map(|tool| match tool {
Tool::Function { name, .. } => Some(name.clone()),
Tool::Function { name, function, .. } => function
.as_ref()
.and_then(|f| f.name.clone())
.or_else(|| name.clone()),
Tool::Custom {
name: Some(name), ..
} => Some(name.clone()),
Expand Down
Loading