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
16 changes: 16 additions & 0 deletions src/openhuman/inference/provider/factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -978,6 +978,22 @@ pub(crate) fn chat_model_from_provider(
)
}

/// Build an `Arc<dyn ChatModel>` for `role`, pinned to an explicit `model` id
/// rather than the role's default (issue #4249, Motion B).
///
/// Used by completion callers (the flows `agent` node) that resolve the role's
/// provider but pin a node-specified raw/BYOK model id verbatim (issue #4598).
/// The provider is the role's provider; only the baked model differs.
pub fn create_chat_model_pinned(
role: &str,
config: &Config,
model: &str,
temperature: f64,
) -> anyhow::Result<Arc<dyn ChatModel<()>>> {
let (provider, _default_model) = create_chat_provider(role, config)?;
Ok(chat_model_from_provider(provider, model.to_string(), temperature))
}

/// Build a local-runtime provider without applying the custom-provider session gate.
///
/// Used by setup/probe flows that need to validate an endpoint before the
Expand Down
27 changes: 27 additions & 0 deletions src/openhuman/tinyagents/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,33 @@ pub(super) fn ta_call_to_oh_call(
}
}

/// Convert openhuman history into harness messages for a one-shot `ChatModel`
/// request. Exposed (issue #4249, Motion B) for callers outside the seam that
/// build a crate model request from openhuman [`ChatMessage`]s (e.g. the flows
/// `agent` node).
pub(crate) fn chat_messages_to_model_messages(
history: &[ChatMessage],
) -> Vec<tinyagents::harness::message::Message> {
history_to_messages(history)
}

/// Convert a harness [`ModelResponse`](tinyagents::harness::model::ModelResponse)
/// back into an openhuman [`ChatResponse`] — text + tool calls + reasoning +
/// usage — for one-shot callers that consume the full response envelope (e.g. the
/// flows `agent` node round-tripping into its JSON output). Issue #4249, Motion B.
pub(crate) fn model_response_to_chat_response(
response: &tinyagents::harness::model::ModelResponse,
) -> crate::openhuman::inference::provider::ChatResponse {
let assistant = &response.message;
let text = response.text();
crate::openhuman::inference::provider::ChatResponse {
text: (!text.is_empty()).then_some(text),
tool_calls: assistant.tool_calls.iter().map(ta_call_to_oh_call).collect(),
usage: super::model::usage_info_from_response(response),
reasoning_content: reasoning_from_content(&assistant.content),
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
3 changes: 3 additions & 0 deletions src/openhuman/tinyagents/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@

mod abort_guard;
mod convert;
// One-shot response/message converters for callers outside the seam that drive a
// crate `ChatModel` directly (issue #4249, Motion B — e.g. the flows agent node).
pub(crate) use convert::{chat_messages_to_model_messages, model_response_to_chat_response};
pub(crate) mod delegation;
mod embeddings;
pub(crate) mod journal;
Expand Down
50 changes: 33 additions & 17 deletions src/openhuman/tinyflows/caps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ use crate::openhuman::config::{Config, HttpRequestConfig};
use crate::openhuman::credentials::{HttpCredential, HttpCredentialsStore};
use crate::openhuman::flows;
use crate::openhuman::inference::provider::{
create_chat_provider, is_raw_passthrough_model, role_for_model_tier, ChatMessage, ChatRequest,
UsageInfo,
is_raw_passthrough_model, role_for_model_tier, ChatMessage, UsageInfo,
};
use crate::openhuman::sandbox::{execute_in_sandbox, resolve_sandbox_policy};
use crate::openhuman::security::{
Expand Down Expand Up @@ -516,25 +515,42 @@ impl LlmProvider for OpenHumanLlm {
"[flows] llm.complete: dispatching agent-node completion"
);

let (provider, model) = create_chat_provider(role, &self.config)
// Build the completion model on the crate `ChatModel` interface (issue
// #4249, Motion B — replaces the raw `provider.chat`). `create_chat_model_*`
// resolves the role's provider + default model; if the node pinned a
// raw/BYOK id, rebuild the model pinned to it verbatim (issue #4598).
let (chat, factory_model) =
crate::openhuman::inference::provider::create_chat_model_with_model_id(
role,
&self.config,
temperature,
)
.map_err(|e| EngineError::Capability(e.to_string()))?;
// `create_chat_provider` handed back the role's default model. If the node
// pinned a raw/BYOK id, forward it verbatim instead (issue #4598).
let model = resolve_completion_model(node_model, model);

let response = provider
.chat(
ChatRequest {
messages: &messages,
tools: None,
stream: None,
max_tokens,
},
let model = resolve_completion_model(node_model, factory_model.clone());
let chat = if model == factory_model {
chat
} else {
crate::openhuman::inference::provider::factory::create_chat_model_pinned(
role,
&self.config,
&model,
temperature,
)
.await
.map_err(|e| EngineError::Capability(e.to_string()))?;
.map_err(|e| EngineError::Capability(e.to_string()))?
};

let mut model_request = tinyagents::harness::model::ModelRequest::new(
crate::openhuman::tinyagents::chat_messages_to_model_messages(&messages),
);
if let Some(cap) = max_tokens {
model_request = model_request.with_max_tokens(cap);
}
let response = crate::openhuman::tinyagents::model_response_to_chat_response(
&chat
.invoke(&(), model_request)
Comment on lines +548 to +550

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve literal tool-call markup in no-tools completions

Routing this no-tools completion through ProviderModel::invoke makes the adapter run the harness tool-call parser on every text response before model_response_to_chat_response sees it. Because llm.complete does not populate ModelRequest.tools (old ChatRequest.tools was None), prompts that legitimately ask the agent node to emit examples such as <tool_call>{"name":"x","arguments":{}}</tool_call> now have that text removed and surfaced as tool_calls, so the structured parser below sees truncated/empty text instead of the model's answer. The direct provider path returned that text verbatim; skip tool parsing for this one-shot/no-tools path or preserve the raw text.

Useful? React with 👍 / 👎.

.await
.map_err(|e| EngineError::Capability(e.to_string()))?,
);

// Structured mode: surface the parsed object itself so downstream
// `=item.<field>` / `=nodes.<id>.item.<field>` bindings work. The
Expand Down
Loading