1mod acp_bridge;
10mod cache;
11mod emit;
12mod inprocess;
13mod metrics;
14mod models;
15mod tiers;
16mod translate;
17
18use std::collections::HashMap;
19use std::future::Future;
20use std::pin::Pin;
21use std::sync::{Arc, Mutex};
22use std::time::{Duration, Instant};
23
24use serde::{Deserialize, Serialize};
25use serde_json::{Map, Value, json};
26use tokio::sync::mpsc;
27use tokio_util::sync::CancellationToken;
28
29pub use acp_bridge::{acp_generate, acp_generate_stream, acp_messages};
30pub use cache::PromptCacheOptions;
31pub use emit::{UNSUPPORTED_PART_AS_ERROR, UNSUPPORTED_PART_AS_TEXT, validate_part};
32pub use inprocess::{
33 EmitFn, GenerateFn, GenerateStreamFn, InProcessOptions, InProcessRequest, InProcessResponse,
34 InProcessToolCall, InProcessUsage, create_in_process_client,
35};
36pub use metrics::MetricEvent;
37pub use models::{ModelInfo, list_models};
38pub use tiers::{CountTokensFn, ToolTier, ToolTierOptions, UsageCountFn};
39pub use translate::{TranslateRequest, TranslateResult, TranslatedToolCall};
40
41use crate::store::{ConversationStore, InMemoryConversationStore};
42use crate::tool::{Answer, ContentPart, Request, ToolContext, ToolHints, ToolResult, pending_of};
43use crate::toolkit::Toolkit;
44use crate::{to_anthropic, to_openai};
45
46#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "lowercase")]
53pub enum Style {
54 #[default]
55 #[serde(rename = "openai")]
56 OpenAI,
57 Anthropic,
58}
59
60pub type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
62pub type BeforeLlmHook =
63 Arc<dyn Fn(BeforeLlmEvent) -> BoxFuture<Result<Option<LlmOverride>, String>> + Send + Sync>;
64pub type AfterLlmHook = Arc<dyn Fn(AfterLlmEvent) -> BoxFuture<Result<(), String>> + Send + Sync>;
65pub type BeforeToolHook =
66 Arc<dyn Fn(BeforeToolEvent) -> BoxFuture<Result<Option<ToolOverride>, String>> + Send + Sync>;
67pub type AfterToolHook =
68 Arc<dyn Fn(AfterToolEvent) -> BoxFuture<Result<Option<ToolOverride>, String>> + Send + Sync>;
69pub type WaitForFn = Arc<dyn Fn(Request) -> BoxFuture<Result<Answer, String>> + Send + Sync>;
71pub type WaitForCtxFn =
73 Arc<dyn Fn(CancellationToken, Request) -> BoxFuture<Result<Answer, String>> + Send + Sync>;
74pub type OnMetricFn = Arc<dyn Fn(&MetricEvent) + Send + Sync>;
75pub type OnErrorFn = Arc<dyn Fn(&ErrorInfo) -> ErrorTier + Send + Sync>;
76pub type BodyTransformFn = Arc<dyn Fn(Value) -> Option<Value> + Send + Sync>;
77
78#[derive(Clone, Default)]
80pub struct Hooks {
81 pub before_llm: Option<BeforeLlmHook>,
82 pub after_llm: Option<AfterLlmHook>,
83 pub before_tool: Option<BeforeToolHook>,
84 pub after_tool: Option<AfterToolHook>,
85}
86
87#[derive(Debug, Clone)]
88pub struct BeforeLlmEvent {
89 pub messages: Vec<Value>,
90 pub tools: Vec<Value>,
91 pub model: String,
92 pub turn: usize,
93}
94
95#[derive(Debug, Clone)]
96pub struct AfterLlmEvent {
97 pub response: Value,
98 pub model: String,
99 pub turn: usize,
100}
101
102#[derive(Debug, Clone)]
103pub struct BeforeToolEvent {
104 pub name: String,
105 pub args: Value,
106 pub id: String,
107 pub turn: usize,
108 pub hints: Option<ToolHints>,
110}
111
112#[derive(Debug, Clone)]
113pub struct AfterToolEvent {
114 pub name: String,
115 pub args: Value,
116 pub result: ToolResult,
117 pub id: String,
118 pub turn: usize,
119}
120
121#[derive(Debug, Clone, Default)]
123pub struct LlmOverride {
124 pub messages: Option<Vec<Value>>,
125 pub tools: Option<Vec<Value>>,
126 pub model: Option<String>,
127}
128
129#[derive(Debug, Clone, Default)]
131pub struct ToolOverride {
132 pub args: Option<Value>,
133 pub result: Option<ToolResult>,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum ErrorTier {
139 Retry,
140 Fail,
141}
142
143#[derive(Debug, Clone)]
145pub struct ErrorInfo {
146 pub error: String,
147 pub status: u16,
149 pub attempt: u32,
150 pub retryable: bool,
152}
153
154#[derive(Clone, Default)]
156pub struct ClientOptions {
157 pub base_url: String,
158 pub style: Style,
159 pub model: String,
160 pub api_key: String,
162 pub api_key_env: String,
164 pub headers: HashMap<String, String>,
165 pub system_prompt: String,
166 pub max_turns: usize,
168 pub max_tool_concurrency: usize,
172 pub hooks: Hooks,
173 pub retries: i32,
175 pub retry_base_ms: u64,
177 pub retryable_statuses: Vec<u16>,
179 pub timeout_ms: u64,
181 pub store: Option<Arc<dyn ConversationStore>>,
183 pub on_metric: Option<OnMetricFn>,
184 pub wait_for: Option<WaitForFn>,
185 pub wait_for_ctx: Option<WaitForCtxFn>,
187 pub request_params: Map<String, Value>,
189 pub body_transform: Option<BodyTransformFn>,
191 pub prompt_cache: Option<PromptCacheOptions>,
192 pub tool_tiers: Option<ToolTierOptions>,
193 pub http_client: Option<reqwest::Client>,
194 pub on_error: Option<OnErrorFn>,
195 pub max_part_bytes: usize,
197 pub on_unsupported_part: String,
199}
200
201#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
207pub struct ToolCall {
208 pub name: String,
209 pub args: Value,
210 pub output: String,
211 #[serde(rename = "isError")]
212 pub is_error: bool,
213 #[serde(skip_serializing_if = "Option::is_none")]
214 pub metadata: Option<Map<String, Value>>,
215 #[serde(default, skip_serializing_if = "Vec::is_empty")]
217 pub parts: Vec<ContentPart>,
218}
219
220#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
222pub struct RunFile {
223 #[serde(rename = "type")]
224 pub kind: String,
225 #[serde(rename = "mimeType", default, skip_serializing_if = "String::is_empty")]
226 pub mime_type: String,
227 #[serde(default, skip_serializing_if = "String::is_empty")]
228 pub url: String,
229 #[serde(default, skip_serializing_if = "String::is_empty")]
230 pub data: String,
231 #[serde(default, skip_serializing_if = "String::is_empty")]
233 pub name: String,
234 pub from: String,
236 #[serde(rename = "callId")]
238 pub call_id: String,
239}
240
241pub fn run_files_of(call: &ToolCall, call_id: &str) -> Vec<RunFile> {
243 call.parts
244 .iter()
245 .filter(|p| p.kind != "text")
246 .map(|p| RunFile {
247 kind: p.kind.clone(),
248 mime_type: p.mime_type.clone(),
249 url: p.url.clone(),
250 data: p.data.clone(),
251 name: p.name.clone(),
252 from: call.name.clone(),
253 call_id: call_id.to_string(),
254 })
255 .collect()
256}
257
258#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
260pub struct Usage {
261 #[serde(rename = "promptTokens")]
262 pub prompt_tokens: i64,
263 #[serde(rename = "completionTokens")]
264 pub completion_tokens: i64,
265 #[serde(rename = "totalTokens")]
266 pub total_tokens: i64,
267 #[serde(rename = "cacheReadTokens", skip_serializing_if = "is_zero", default)]
268 pub cache_read_tokens: i64,
269 #[serde(rename = "cacheWriteTokens", skip_serializing_if = "is_zero", default)]
270 pub cache_write_tokens: i64,
271}
272
273fn is_zero(n: &i64) -> bool {
274 *n == 0
275}
276
277#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
279pub struct RunResult {
280 pub text: String,
281 pub messages: Vec<Value>,
282 #[serde(rename = "toolCalls")]
283 pub tool_calls: Vec<ToolCall>,
284 #[serde(default)]
286 pub files: Vec<RunFile>,
287 #[serde(rename = "toolCallCount")]
288 pub tool_call_count: usize,
289 pub turns: usize,
290 pub usage: Usage,
291 pub model: String,
292 pub status: String,
293 #[serde(skip_serializing_if = "String::is_empty", default)]
294 pub limit: String,
295 #[serde(skip_serializing_if = "Option::is_none", default)]
296 pub pending: Option<Request>,
297}
298
299#[derive(Debug, Clone, Default)]
303pub struct StreamEvent {
304 pub kind: String,
305 pub delta: String,
306 pub name: String,
307 pub via: String,
309 pub id: String,
310 pub args: Value,
311 pub output: String,
312 pub is_error: bool,
313 pub metadata: Option<Map<String, Value>>,
314 pub usage: Option<Usage>,
315 pub result: Option<Box<RunResult>>,
316 pub request: Option<Request>,
317 pub error: Option<ClientError>,
318}
319
320impl StreamEvent {
321 fn of(kind: &str) -> Self {
322 Self {
323 kind: kind.into(),
324 ..Default::default()
325 }
326 }
327 pub fn text(delta: impl Into<String>) -> Self {
329 Self {
330 kind: "text".into(),
331 delta: delta.into(),
332 ..Default::default()
333 }
334 }
335}
336
337#[derive(Debug, Clone, PartialEq)]
340pub struct ProviderError {
341 pub status: u16,
342 pub body: String,
343 pub retry_after: String,
344}
345
346pub const REDACTED_MARKER: &str = "«redacted»";
348
349pub fn redact_provider_body(status: u16, body: &str) -> String {
351 if status == 401 || status == 403 {
352 return String::new();
353 }
354 let s = body.trim();
355 if s.is_empty() {
356 return String::new();
357 }
358 static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
359 let re = RE.get_or_init(|| {
360 regex::Regex::new(
361 r#"("(?:user_id|account_id|org_id|organization)"\s*:\s*)("(?:[^"\\]|\\.)*"|[^,}\s]+)"#,
362 )
363 .expect("static regex")
364 });
365 re.replace_all(s, format!("${{1}}\"{REDACTED_MARKER}\"").as_str())
366 .into_owned()
367}
368
369impl std::fmt::Display for ProviderError {
370 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
371 if self.body.is_empty() {
372 return write!(f, "LLM {}", self.status);
373 }
374 let b = &self.body;
375 if b.len() > 200 {
376 let mut n = 200;
377 while !b.is_char_boundary(n) {
378 n -= 1;
379 }
380 write!(f, "LLM {}: {}…", self.status, &b[..n])
381 } else {
382 write!(f, "LLM {}: {}", self.status, b)
383 }
384 }
385}
386
387#[derive(Debug, Clone, thiserror::Error)]
389pub enum ClientError {
390 #[error("{0}")]
391 NoApiKey(String),
392 #[error("{0}")]
393 Provider(ProviderError),
394 #[error("{0}")]
395 Transport(String),
396 #[error("decode LLM response: {0}")]
397 Decode(String),
398 #[error("{0}")]
399 Hook(String),
400 #[error("waitFor: {0}")]
401 WaitFor(String),
402 #[error("{0}")]
403 Part(String),
404 #[error("conversation store: {0}")]
405 Store(String),
406 #[error("context canceled")]
407 Cancelled,
408 #[error("run timeout after {timeout_ms}ms")]
411 RunTimeout {
412 timeout_ms: u64,
413 partial: Box<RunResult>,
414 },
415 #[error("{0}")]
416 Unsupported(String),
417 #[error("{0}")]
419 Resume(String),
420}
421
422#[derive(Debug, Clone)]
424pub enum Prompt {
425 Text(String),
426 Parts(Vec<ContentPart>),
428}
429
430impl From<&str> for Prompt {
431 fn from(s: &str) -> Self {
432 Prompt::Text(s.into())
433 }
434}
435impl From<String> for Prompt {
436 fn from(s: String) -> Self {
437 Prompt::Text(s)
438 }
439}
440impl From<Vec<ContentPart>> for Prompt {
441 fn from(p: Vec<ContentPart>) -> Self {
442 Prompt::Parts(p)
443 }
444}
445
446fn user_message(p: &Prompt) -> Value {
447 match p {
448 Prompt::Text(s) => json!({"role": "user", "content": s}),
449 Prompt::Parts(parts) => json!({"role": "user", "content": parts}),
450 }
451}
452
453const DEFAULT_RETRYABLE: [u16; 6] = [429, 500, 502, 503, 504, 529];
458
459fn is_retryable_status(status: u16, extra: &[u16]) -> bool {
460 DEFAULT_RETRYABLE.contains(&status) || extra.contains(&status)
461}
462
463pub fn retry_after_seconds(v: &str) -> Option<Duration> {
465 let s = v.trim();
466 if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
467 return None;
468 }
469 let secs: i64 = s.parse().ok()?;
470 if secs > i32::MAX as i64 {
471 return None;
472 }
473 Some(Duration::from_secs(secs as u64))
474}
475
476pub(crate) enum Transport {
477 Http(reqwest::Client),
478 InProcess(Arc<inprocess::InProcess>),
479}
480
481pub(crate) enum RespBody {
482 Http(reqwest::Response),
483 Full(Vec<u8>),
484 Chunks(mpsc::UnboundedReceiver<Result<Vec<u8>, String>>),
485}
486
487pub(crate) struct RawResponse {
488 pub status: u16,
489 pub retry_after: String,
490 pub body: RespBody,
491}
492
493impl RawResponse {
494 async fn read_all(self) -> Result<Vec<u8>, ClientError> {
495 match self.body {
496 RespBody::Full(b) => Ok(b),
497 RespBody::Http(r) => r
498 .bytes()
499 .await
500 .map(|b| b.to_vec())
501 .map_err(|e| ClientError::Transport(e.to_string())),
502 RespBody::Chunks(mut rx) => {
503 let mut out = Vec::new();
504 while let Some(c) = rx.recv().await {
505 out.extend(c.map_err(ClientError::Transport)?);
506 }
507 Ok(out)
508 }
509 }
510 }
511}
512
513pub(crate) struct TurnGate {
517 sem: Arc<tokio::sync::Semaphore>,
518 counts: Mutex<(usize, usize)>,
519}
520
521pub(crate) struct TurnSlot<'a> {
522 gate: &'a TurnGate,
523 _permit: tokio::sync::OwnedSemaphorePermit,
524}
525
526impl Drop for TurnSlot<'_> {
527 fn drop(&mut self) {
528 let mut c = self.gate.counts.lock().unwrap_or_else(|e| e.into_inner());
529 c.0 -= 1;
530 }
531}
532
533impl TurnGate {
534 pub(crate) fn new(n: usize) -> Self {
535 TurnGate {
536 sem: Arc::new(tokio::sync::Semaphore::new(n.max(1))),
537 counts: Mutex::new((0, 0)),
538 }
539 }
540 async fn acquire(&self) -> TurnSlot<'_> {
541 let permit = self
542 .sem
543 .clone()
544 .acquire_owned()
545 .await
546 .expect("turn gate semaphore is never closed");
547 let mut c = self.counts.lock().unwrap_or_else(|e| e.into_inner());
548 c.0 += 1;
549 if c.0 > c.1 {
550 c.1 = c.0;
551 }
552 TurnSlot {
553 gate: self,
554 _permit: permit,
555 }
556 }
557 pub(crate) fn max_observed(&self) -> usize {
559 self.counts.lock().unwrap_or_else(|e| e.into_inner()).1
560 }
561}
562
563struct Inner {
564 opts: ClientOptions,
565 transport: Transport,
566 gate: Option<Arc<TurnGate>>,
567 store: Arc<dyn ConversationStore>,
568 registry: metrics::Registry,
569 plans: Mutex<HashMap<usize, Arc<tiers::Plan>>>,
570}
571
572#[derive(Clone)]
574pub struct Client {
575 inner: Arc<Inner>,
576}
577
578impl std::fmt::Debug for Client {
579 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
580 f.debug_struct("Client")
581 .field("base_url", &self.inner.opts.base_url)
582 .field("style", &self.inner.opts.style)
583 .field("model", &self.inner.opts.model)
584 .finish_non_exhaustive()
585 }
586}
587
588pub fn create_client(opts: ClientOptions) -> Client {
590 let transport = Transport::Http(opts.http_client.clone().unwrap_or_default());
591 Client::with_transport(opts, transport)
592}
593
594#[derive(Clone)]
595struct Call {
596 id: String,
597 name: String,
598 args: Value,
599}
600
601#[derive(Default)]
602struct RunState {
603 messages: Vec<Value>,
604 tool_calls: Vec<ToolCall>,
605 files: Vec<RunFile>,
606 usage: Usage,
607 turns: usize,
608 last_model: String,
609 started: bool,
611}
612
613enum TurnEnd {
614 Empty,
616 Final(String),
617 Calls(Vec<Call>),
618}
619
620type Tx = mpsc::UnboundedSender<StreamEvent>;
621
622fn send(tx: Option<&Tx>, ev: StreamEvent) {
623 if let Some(tx) = tx {
624 let _ = tx.send(ev);
625 }
626}
627
628fn ms_since(t: Instant) -> i64 {
629 t.elapsed().as_millis() as i64
630}
631
632fn num(raw: &Value, k: &str) -> i64 {
633 raw.get(k).and_then(Value::as_f64).unwrap_or(0.0) as i64
634}
635
636fn add_usage(acc: &mut Usage, raw: &Value, style: Style) {
637 if !raw.is_object() {
638 return;
639 }
640 if style == Style::Anthropic {
641 let (i, o) = (num(raw, "input_tokens"), num(raw, "output_tokens"));
642 acc.prompt_tokens += i;
643 acc.completion_tokens += o;
644 acc.total_tokens += i + o;
645 acc.cache_read_tokens += num(raw, "cache_read_input_tokens");
646 acc.cache_write_tokens += num(raw, "cache_creation_input_tokens");
647 return;
648 }
649 if let Some(d) = raw.get("prompt_tokens_details") {
650 acc.cache_read_tokens += num(d, "cached_tokens");
651 }
652 let (p, c) = (num(raw, "prompt_tokens"), num(raw, "completion_tokens"));
653 acc.prompt_tokens += p;
654 acc.completion_tokens += c;
655 let total = num(raw, "total_tokens");
656 acc.total_tokens += if total != 0 { total } else { p + c };
657}
658
659fn per_call(raw: &Value, style: Style) -> (i64, i64) {
660 if style == Style::Anthropic {
661 (num(raw, "input_tokens"), num(raw, "output_tokens"))
662 } else {
663 (num(raw, "prompt_tokens"), num(raw, "completion_tokens"))
664 }
665}
666
667fn safe_json_args(s: &str) -> Value {
668 match serde_json::from_str::<Value>(s) {
669 Ok(Value::Object(m)) => Value::Object(m),
670 _ => Value::Object(Map::new()),
671 }
672}
673
674fn obj_or_empty(v: Option<&Value>) -> Value {
675 match v {
676 Some(Value::Object(m)) => Value::Object(m.clone()),
677 _ => Value::Object(Map::new()),
678 }
679}
680
681fn last_text(messages: &[Value]) -> String {
682 for m in messages.iter().rev() {
683 if m.get("role").and_then(Value::as_str) != Some("assistant") {
684 continue;
685 }
686 if let Some(s) = m.get("content").and_then(Value::as_str) {
687 return s.into();
688 }
689 }
690 String::new()
691}
692
693const INTERRUPTED_OUTPUT: &str = "interrupted: the turn ended before this tool returned. Whether its side effects happened is unknown. Do not re-run it automatically; check first.";
694
695fn seal_interrupted(mut messages: Vec<Value>) -> Vec<Value> {
698 let Some(last) = messages
699 .iter()
700 .rposition(|m| m.get("role").and_then(Value::as_str) == Some("assistant"))
701 else {
702 return messages;
703 };
704 let asst = messages[last].clone();
705 if let Some(calls) = asst
706 .get("tool_calls")
707 .and_then(Value::as_array)
708 .filter(|c| !c.is_empty())
709 {
710 let have: std::collections::HashSet<String> = messages[last + 1..]
711 .iter()
712 .filter(|m| m.get("role").and_then(Value::as_str) == Some("tool"))
713 .filter_map(|m| {
714 m.get("tool_call_id")
715 .and_then(Value::as_str)
716 .map(String::from)
717 })
718 .collect();
719 for tc in calls {
720 let id = tc.get("id").and_then(Value::as_str).unwrap_or("");
721 if id.is_empty() || have.contains(id) {
722 continue;
723 }
724 messages
725 .push(json!({"role": "tool", "tool_call_id": id, "content": INTERRUPTED_OUTPUT}));
726 }
727 return messages;
728 }
729 let ids: Vec<String> = asst
730 .get("content")
731 .and_then(Value::as_array)
732 .map(|bs| {
733 bs.iter()
734 .filter(|b| b.get("type").and_then(Value::as_str) == Some("tool_use"))
735 .filter_map(|b| b.get("id").and_then(Value::as_str))
736 .filter(|s| !s.is_empty())
737 .map(String::from)
738 .collect()
739 })
740 .unwrap_or_default();
741 if ids.is_empty() {
742 return messages;
743 }
744 let mut blocks: Vec<Value> = Vec::new();
745 let mut have = std::collections::HashSet::new();
746 let mut has_user = false;
747 if let Some(um) = messages.get(last + 1)
748 && um.get("role").and_then(Value::as_str) == Some("user")
749 && let Some(bs) = um.get("content").and_then(Value::as_array)
750 {
751 has_user = true;
752 blocks.extend(bs.iter().cloned());
753 for b in bs {
754 if b.get("type").and_then(Value::as_str) == Some("tool_result")
755 && let Some(id) = b.get("tool_use_id").and_then(Value::as_str)
756 {
757 have.insert(id.to_owned());
758 }
759 }
760 }
761 let mut added = false;
762 for id in ids {
763 if !have.contains(&id) {
764 blocks.push(json!({"type": "tool_result", "tool_use_id": id, "content": INTERRUPTED_OUTPUT, "is_error": true}));
765 added = true;
766 }
767 }
768 if !added {
769 return messages;
770 }
771 let user = json!({"role": "user", "content": blocks});
772 if has_user {
773 messages[last + 1] = user;
774 } else {
775 messages.push(user);
776 }
777 messages
778}
779
780struct LineReader {
782 buf: Vec<u8>,
783}
784
785impl LineReader {
786 fn push(&mut self, chunk: &[u8]) -> Vec<String> {
787 self.buf.extend_from_slice(chunk);
788 let mut out = Vec::new();
789 while let Some(i) = self.buf.iter().position(|b| *b == b'\n') {
790 let line: Vec<u8> = self.buf.drain(..=i).collect();
791 let s = String::from_utf8_lossy(&line[..line.len() - 1]);
792 out.push(s.trim_end_matches('\r').to_owned());
793 }
794 out
795 }
796 fn finish(&mut self) -> Option<String> {
797 if self.buf.is_empty() {
798 return None;
799 }
800 let s = String::from_utf8_lossy(&self.buf)
801 .trim_end_matches('\r')
802 .to_owned();
803 self.buf.clear();
804 Some(s)
805 }
806}
807
808impl Client {
809 pub(crate) fn with_transport(opts: ClientOptions, transport: Transport) -> Client {
810 Self::with_transport_gate(opts, transport, None)
811 }
812
813 pub(crate) fn gated(
816 opts: ClientOptions,
817 in_process: Option<GenerateFn>,
818 gate: Arc<TurnGate>,
819 ) -> Client {
820 let transport = match in_process {
821 Some(g) => inprocess::transport_of(g),
822 None => Transport::Http(opts.http_client.clone().unwrap_or_default()),
823 };
824 Self::with_transport_gate(opts, transport, Some(gate))
825 }
826
827 fn with_transport_gate(
828 opts: ClientOptions,
829 transport: Transport,
830 gate: Option<Arc<TurnGate>>,
831 ) -> Client {
832 let store = opts
833 .store
834 .clone()
835 .unwrap_or_else(|| Arc::new(InMemoryConversationStore::new()));
836 Client {
837 inner: Arc::new(Inner {
838 opts,
839 transport,
840 gate,
841 store,
842 registry: metrics::Registry::default(),
843 plans: Mutex::new(HashMap::new()),
844 }),
845 }
846 }
847
848 fn opts(&self) -> &ClientOptions {
849 &self.inner.opts
850 }
851
852 pub fn metrics(&self) -> String {
854 self.inner.registry.render()
855 }
856
857 pub fn record_metric(&self, ev: MetricEvent) {
860 self.inner.registry.record(&ev);
861 if let Some(f) = &self.opts().on_metric {
862 f(&ev);
863 }
864 }
865
866 pub fn conversation_store(&self) -> Arc<dyn ConversationStore> {
868 self.inner.store.clone()
869 }
870
871 fn emit_llm(&self, model: &str, status: &str, start: Instant, p: i64, c: i64) {
872 self.record_metric(MetricEvent {
873 event: "llm".into(),
874 model: model.into(),
875 status: status.into(),
876 ms: ms_since(start),
877 prompt_tokens: p,
878 completion_tokens: c,
879 ..Default::default()
880 });
881 }
882
883 fn emit_run(&self, st: &RunState, start: Instant, error: String) {
884 self.record_metric(MetricEvent {
885 event: "run".into(),
886 model: st.last_model.clone(),
887 turns: st.turns as i64,
888 tool_calls: st.tool_calls.len() as i64,
889 total_tokens: st.usage.total_tokens,
890 ms: ms_since(start),
891 error,
892 ..Default::default()
893 });
894 }
895
896 fn emit_tool(&self, name: &str, source: &str, r: &ToolResult, start: Instant) {
897 let pending = pending_of(r).is_some();
898 self.record_metric(MetricEvent {
899 event: "tool".into(),
900 tool: name.into(),
901 source: source.into(),
902 is_error: !pending && r.is_error,
903 pending,
904 ms: ms_since(start),
905 ..Default::default()
906 });
907 }
908
909 fn max_turns(&self) -> usize {
910 if self.opts().max_turns > 0 {
911 self.opts().max_turns
912 } else {
913 10
914 }
915 }
916
917 fn retries(&self) -> u32 {
918 match self.opts().retries {
919 -1 => 0,
920 n if n > 0 => n as u32,
921 _ => 2,
922 }
923 }
924
925 fn retry_base_ms(&self) -> u64 {
926 if self.opts().retry_base_ms > 0 {
927 self.opts().retry_base_ms
928 } else {
929 500
930 }
931 }
932
933 pub fn backoff(&self, attempt: u32, retry_after: &str) -> Duration {
936 if let Some(d) = retry_after_seconds(retry_after) {
937 return d;
938 }
939 let jitter = (std::time::SystemTime::now()
940 .duration_since(std::time::UNIX_EPOCH)
941 .map(|d| d.subsec_nanos())
942 .unwrap_or(0)
943 % 100) as u64;
944 Duration::from_millis(self.retry_base_ms() * (1u64 << attempt.min(30)) + jitter)
945 }
946
947 pub fn resolve_key(&self) -> Result<String, ClientError> {
951 let o = self.opts();
952 if !o.api_key.is_empty() {
953 return Ok(o.api_key.clone());
954 }
955 let env_name = if o.api_key_env.is_empty() {
956 key_env_for_base_url(&o.base_url).to_owned()
957 } else {
958 o.api_key_env.clone()
959 };
960 if env_name.is_empty() {
961 return Err(ClientError::NoApiKey(format!(
962 "no API key: set APIKey or APIKeyEnv (provider env vars are only used for https://api.openai.com, https://api.anthropic.com and https://openrouter.ai, never for {:?})",
963 o.base_url
964 )));
965 }
966 match std::env::var(&env_name) {
967 Ok(k) if !k.is_empty() => Ok(k),
968 _ => Err(ClientError::NoApiKey(format!(
969 "no API key: set APIKey or {env_name}"
970 ))),
971 }
972 }
973
974 fn system(&self, tk: &Toolkit) -> String {
975 let mut parts = Vec::new();
976 if !self.opts().system_prompt.is_empty() {
977 parts.push(self.opts().system_prompt.clone());
978 }
979 let sp = tk.skills_prompt();
980 if !sp.is_empty() {
981 parts.push(sp);
982 }
983 parts.join("\n\n")
984 }
985
986 fn plan(&self, tk: &Toolkit) -> Option<Arc<tiers::Plan>> {
989 let o = self.opts().tool_tiers.as_ref()?;
990 let mut plans = self.inner.plans.lock().unwrap_or_else(|e| e.into_inner());
991 Some(
992 plans
993 .entry(tk.identity())
994 .or_insert_with(|| Arc::new(tiers::build_plan(o, &tk.tools())))
995 .clone(),
996 )
997 }
998
999 pub fn tool_tier_plan(&self, tk: &Toolkit) -> Option<HashMap<String, ToolTier>> {
1001 self.plan(tk)
1002 .map(|p| p.tier.iter().map(|(k, v)| (k.clone(), *v)).collect())
1003 }
1004
1005 pub fn reset_tool_tiers(&self, tk: &Toolkit) {
1007 self.inner
1008 .plans
1009 .lock()
1010 .unwrap_or_else(|e| e.into_inner())
1011 .remove(&tk.identity());
1012 }
1013
1014 pub fn request_tools(&self, tk: &Toolkit) -> Vec<crate::tool::Tool> {
1017 let Some(p) = self.plan(tk) else {
1018 return tk.tools();
1019 };
1020 let mut out: Vec<_> = p.core.iter().filter_map(|n| tk.get(n)).collect();
1021 out.push(p.search.clone());
1022 out.push(p.call.clone());
1023 out
1024 }
1025
1026 pub fn tool_search(&self, tk: &Toolkit, args: &Value) -> ToolResult {
1028 tiers::tool_search(tk, args)
1029 }
1030
1031 fn call_safe(&self, tk: &Toolkit, call: &Call) -> bool {
1033 if self.opts().tool_tiers.is_some() && call.name == "tool_search" {
1034 return true;
1035 }
1036 let (name, args, _) = self.unwrap_call(&call.name, &call.args);
1037 tk.get(&name)
1038 .map(|t| crate::tool::is_concurrency_safe(&t, &args))
1039 .unwrap_or(false)
1040 }
1041
1042 fn unwrap_call(&self, name: &str, args: &Value) -> (String, Value, bool) {
1043 if self.opts().tool_tiers.is_none() || name != "call_tool" {
1044 return (name.into(), args.clone(), false);
1045 }
1046 let real = args
1047 .get("name")
1048 .and_then(Value::as_str)
1049 .unwrap_or("")
1050 .to_owned();
1051 (real, obj_or_empty(args.get("arguments")), true)
1052 }
1053
1054 fn finalize_body(&self, mut body: Map<String, Value>) -> Value {
1057 let o = self.opts();
1058 if let Some(pc) = &o.prompt_cache {
1059 cache::apply(&mut body, o.style, pc);
1060 }
1061 for (k, v) in &o.request_params {
1062 if k == "messages" || k == "tools" || k == "stream" {
1063 eprintln!(
1064 "[toolnexus] requestParams key {k:?} is not allowed — ignored (use BodyTransform)"
1065 );
1066 continue;
1067 }
1068 body.insert(k.clone(), v.clone());
1069 }
1070 let body = Value::Object(body);
1071 if let Some(t) = &o.body_transform
1072 && let Some(out) = t(body.clone())
1073 {
1074 return out;
1075 }
1076 body
1077 }
1078
1079 async fn send_once(
1080 &self,
1081 endpoint: &str,
1082 headers: &[(String, String)],
1083 raw: &[u8],
1084 cancel: &CancellationToken,
1085 side: Option<&Tx>,
1086 ) -> Result<RawResponse, String> {
1087 let _slot = match &self.inner.gate {
1088 Some(g) => Some(g.acquire().await),
1089 None => None,
1090 };
1091 match &self.inner.transport {
1092 Transport::InProcess(ip) => ip.respond(raw, cancel, side).await,
1093 Transport::Http(http) => {
1094 let mut req = http
1095 .post(endpoint)
1096 .header("Content-Type", "application/json");
1097 for (k, v) in headers {
1098 req = req.header(k, v);
1099 }
1100 for (k, v) in &self.opts().headers {
1101 req = req.header(k, v);
1102 }
1103 let resp = req
1104 .body(raw.to_vec())
1105 .send()
1106 .await
1107 .map_err(|e| e.to_string())?;
1108 let retry_after = resp
1109 .headers()
1110 .get("retry-after")
1111 .and_then(|v| v.to_str().ok())
1112 .unwrap_or("")
1113 .to_owned();
1114 Ok(RawResponse {
1115 status: resp.status().as_u16(),
1116 retry_after,
1117 body: RespBody::Http(resp),
1118 })
1119 }
1120 }
1121 }
1122
1123 async fn sleep(&self, d: Duration, cancel: &CancellationToken) -> Result<(), ClientError> {
1124 tokio::select! {
1125 _ = cancel.cancelled() => Err(ClientError::Cancelled),
1126 _ = tokio::time::sleep(d) => Ok(()),
1127 }
1128 }
1129
1130 async fn llm_fetch(
1133 &self,
1134 endpoint: &str,
1135 headers: &[(String, String)],
1136 raw: &[u8],
1137 cancel: &CancellationToken,
1138 side: Option<&Tx>,
1139 ) -> Result<RawResponse, ClientError> {
1140 let retries = self.retries();
1141 let classify = |info: &ErrorInfo| match &self.opts().on_error {
1142 Some(f) => f(info),
1143 None => {
1144 if info.retryable {
1145 ErrorTier::Retry
1146 } else {
1147 ErrorTier::Fail
1148 }
1149 }
1150 };
1151 let mut attempt = 0;
1152 loop {
1153 let r = tokio::select! {
1154 _ = cancel.cancelled() => return Err(ClientError::Cancelled),
1155 r = self.send_once(endpoint, headers, raw, cancel, side) => r,
1156 };
1157 match r {
1158 Err(e) => {
1159 if cancel.is_cancelled() {
1160 return Err(ClientError::Cancelled);
1161 }
1162 let tier = classify(&ErrorInfo {
1163 error: e.clone(),
1164 status: 0,
1165 attempt,
1166 retryable: true,
1167 });
1168 if tier == ErrorTier::Fail || attempt == retries {
1169 return Err(ClientError::Transport(e));
1170 }
1171 self.sleep(self.backoff(attempt, ""), cancel).await?;
1172 }
1173 Ok(resp) => {
1174 if resp.status < 300 {
1175 return Ok(resp);
1176 }
1177 let retryable =
1178 is_retryable_status(resp.status, &self.opts().retryable_statuses);
1179 let tier = classify(&ErrorInfo {
1180 error: format!("LLM {}", resp.status),
1181 status: resp.status,
1182 attempt,
1183 retryable,
1184 });
1185 if tier == ErrorTier::Fail || attempt == retries {
1186 return Ok(resp);
1187 }
1188 let ra = resp.retry_after.clone();
1189 drop(resp);
1190 self.sleep(self.backoff(attempt, &ra), cancel).await?;
1191 }
1192 }
1193 attempt += 1;
1194 }
1195 }
1196
1197 async fn provider_error(resp: RawResponse) -> ClientError {
1198 let (status, ra) = (resp.status, resp.retry_after.clone());
1199 let body = resp.read_all().await.unwrap_or_default();
1200 ClientError::Provider(ProviderError {
1201 status,
1202 body: redact_provider_body(status, &String::from_utf8_lossy(&body)),
1203 retry_after: ra,
1204 })
1205 }
1206
1207 fn endpoint(&self) -> String {
1208 let base = self.opts().base_url.trim_end_matches('/');
1209 match self.opts().style {
1210 Style::OpenAI => format!("{base}/chat/completions"),
1211 Style::Anthropic => {
1212 if base.ends_with("/v1") {
1213 format!("{base}/messages")
1214 } else {
1215 format!("{base}/v1/messages")
1216 }
1217 }
1218 }
1219 }
1220
1221 fn auth_headers(&self, key: &str) -> Vec<(String, String)> {
1222 match self.opts().style {
1223 Style::OpenAI => vec![("Authorization".into(), format!("Bearer {key}"))],
1224 Style::Anthropic => vec![
1225 ("x-api-key".into(), key.into()),
1226 ("anthropic-version".into(), "2023-06-01".into()),
1227 ],
1228 }
1229 }
1230
1231 fn wire(&self, messages: &[Value]) -> Result<Vec<Value>, ClientError> {
1232 if !emit::any_parts(messages) {
1233 return Ok(messages.to_vec());
1234 }
1235 let mut e = emit::Emitter {
1236 style: self.opts().style,
1237 max_bytes: self.opts().max_part_bytes,
1238 mode: self.opts().on_unsupported_part.clone(),
1239 warned: false,
1240 };
1241 e.emit(messages).map_err(ClientError::Part)
1242 }
1243
1244 #[allow(clippy::too_many_arguments)]
1249 async fn call_model(
1250 &self,
1251 st: &mut RunState,
1252 key: &str,
1253 model: &str,
1254 system: &str,
1255 tools: &[Value],
1256 turn: usize,
1257 cancel: &CancellationToken,
1258 tx: Option<&Tx>,
1259 ) -> Result<(Option<Value>, TurnEnd), ClientError> {
1260 let style = self.opts().style;
1261 let streaming = tx.is_some();
1262 let wire = self.wire(&st.messages)?;
1263 let mut body = Map::new();
1264 body.insert("model".into(), json!(model));
1265 if style == Style::Anthropic {
1266 body.insert("max_tokens".into(), json!(4096));
1267 }
1268 body.insert("messages".into(), Value::Array(wire));
1269 if streaming {
1270 body.insert("stream".into(), json!(true));
1271 if style == Style::OpenAI {
1272 body.insert("stream_options".into(), json!({"include_usage": true}));
1273 }
1274 }
1275 if style == Style::Anthropic && !system.is_empty() {
1276 body.insert("system".into(), json!(system));
1277 }
1278 if !tools.is_empty() {
1279 body.insert("tools".into(), Value::Array(tools.to_vec()));
1280 if style == Style::OpenAI {
1281 body.insert("tool_choice".into(), json!("auto"));
1282 }
1283 }
1284 let body = self.finalize_body(body);
1285 let raw = serde_json::to_vec(&body).map_err(|e| ClientError::Decode(e.to_string()))?;
1286 let t0 = Instant::now();
1287 st.last_model = model.into();
1288 let before = st.usage;
1289 let resp = match self
1290 .llm_fetch(&self.endpoint(), &self.auth_headers(key), &raw, cancel, tx)
1291 .await
1292 {
1293 Ok(r) => r,
1294 Err(e) => {
1295 self.emit_llm(model, "error", t0, 0, 0);
1296 return Err(e);
1297 }
1298 };
1299 if !(200..300).contains(&resp.status) {
1300 self.emit_llm(model, "error", t0, 0, 0);
1301 return Err(Self::provider_error(resp).await);
1302 }
1303 let parsed = if streaming {
1304 self.read_stream(st, resp, cancel, tx).await
1305 } else {
1306 self.read_full(st, resp).await
1307 };
1308 let (assistant, end, response) = match parsed {
1309 Ok(x) => x,
1310 Err(e) => {
1311 self.emit_llm(model, "error", t0, 0, 0);
1312 return Err(e);
1313 }
1314 };
1315 if streaming {
1316 self.emit_llm(
1317 model,
1318 "ok",
1319 t0,
1320 st.usage.prompt_tokens - before.prompt_tokens,
1321 st.usage.completion_tokens - before.completion_tokens,
1322 );
1323 }
1324 if let Some(h) = &self.opts().hooks.after_llm {
1325 h(AfterLlmEvent {
1326 response,
1327 model: model.into(),
1328 turn,
1329 })
1330 .await
1331 .map_err(ClientError::Hook)?;
1332 }
1333 Ok((assistant, end))
1334 }
1335
1336 async fn read_full(
1337 &self,
1338 st: &mut RunState,
1339 resp: RawResponse,
1340 ) -> Result<(Option<Value>, TurnEnd, Value), ClientError> {
1341 let style = self.opts().style;
1342 let model = st.last_model.clone();
1343 let t0 = Instant::now();
1344 let raw = resp.read_all().await?;
1345 let data: Value =
1346 serde_json::from_slice(&raw).map_err(|e| ClientError::Decode(e.to_string()))?;
1347 if !data.is_object() {
1348 return Err(ClientError::Decode("response is not an object".into()));
1349 }
1350 let usage = data.get("usage").cloned().unwrap_or(Value::Null);
1351 let (p, c) = per_call(&usage, style);
1352 self.emit_llm(&model, "ok", t0, p, c);
1353 add_usage(&mut st.usage, &usage, style);
1354 if style == Style::OpenAI {
1355 let Some(msg) = data
1356 .get("choices")
1357 .and_then(Value::as_array)
1358 .and_then(|c| c.first())
1359 .map(|c| c.get("message").cloned().unwrap_or(Value::Null))
1360 else {
1361 return Ok((None, TurnEnd::Empty, data));
1362 };
1363 let content = msg
1364 .get("content")
1365 .and_then(Value::as_str)
1366 .unwrap_or("")
1367 .to_owned();
1368 let mut asst = json!({"role": "assistant", "content": content});
1369 let calls: Vec<Call> = msg
1370 .get("tool_calls")
1371 .and_then(Value::as_array)
1372 .map(|tcs| {
1373 tcs.iter()
1374 .map(|tc| Call {
1375 id: tc.get("id").and_then(Value::as_str).unwrap_or("").into(),
1376 name: tc
1377 .pointer("/function/name")
1378 .and_then(Value::as_str)
1379 .unwrap_or("")
1380 .into(),
1381 args: safe_json_args(
1382 tc.pointer("/function/arguments")
1383 .and_then(Value::as_str)
1384 .unwrap_or(""),
1385 ),
1386 })
1387 .collect()
1388 })
1389 .unwrap_or_default();
1390 if !calls.is_empty() {
1391 let raw_calls: Vec<Value> = msg["tool_calls"]
1392 .as_array()
1393 .into_iter()
1394 .flatten()
1395 .map(|tc| {
1396 json!({
1397 "id": tc.get("id").and_then(Value::as_str).unwrap_or(""),
1398 "type": "function",
1399 "function": {
1400 "name": tc.pointer("/function/name").and_then(Value::as_str).unwrap_or(""),
1401 "arguments": tc.pointer("/function/arguments").and_then(Value::as_str).unwrap_or(""),
1402 }
1403 })
1404 })
1405 .collect();
1406 asst["tool_calls"] = Value::Array(raw_calls);
1407 return Ok((Some(asst), TurnEnd::Calls(calls), data));
1408 }
1409 return Ok((Some(asst), TurnEnd::Final(content), data));
1410 }
1411 let mut content = Vec::new();
1412 let mut text = String::new();
1413 let mut calls = Vec::new();
1414 for b in data
1415 .get("content")
1416 .and_then(Value::as_array)
1417 .into_iter()
1418 .flatten()
1419 {
1420 match b.get("type").and_then(Value::as_str) {
1421 Some("text") => {
1422 let t = b.get("text").and_then(Value::as_str).unwrap_or("");
1423 content.push(json!({"type": "text", "text": t}));
1424 text.push_str(t);
1425 }
1426 Some("tool_use") => {
1427 let id = b.get("id").and_then(Value::as_str).unwrap_or("");
1428 let name = b.get("name").and_then(Value::as_str).unwrap_or("");
1429 let input = b.get("input").cloned().unwrap_or(Value::Null);
1430 content
1431 .push(json!({"type": "tool_use", "id": id, "name": name, "input": input}));
1432 calls.push(Call {
1433 id: id.into(),
1434 name: name.into(),
1435 args: obj_or_empty(Some(&input)),
1436 });
1437 }
1438 _ => {}
1439 }
1440 }
1441 let asst = json!({"role": "assistant", "content": content});
1442 if calls.is_empty() {
1443 return Ok((Some(asst), TurnEnd::Final(text), data));
1444 }
1445 Ok((Some(asst), TurnEnd::Calls(calls), data))
1446 }
1447
1448 async fn read_stream(
1449 &self,
1450 st: &mut RunState,
1451 resp: RawResponse,
1452 cancel: &CancellationToken,
1453 tx: Option<&Tx>,
1454 ) -> Result<(Option<Value>, TurnEnd, Value), ClientError> {
1455 let style = self.opts().style;
1456 let mut reader = LineReader { buf: Vec::new() };
1457 let mut content = String::new();
1459 let mut acc: Vec<(i64, Call, String)> = Vec::new(); #[derive(Default)]
1462 struct Block {
1463 typ: String,
1464 text: String,
1465 id: String,
1466 name: String,
1467 json: String,
1468 }
1469 let mut blocks: Vec<(i64, Block)> = Vec::new();
1470 let mut stop_reason = String::new();
1471 let mut done = false;
1472
1473 let mut handle = |line: &str, st: &mut RunState| -> bool {
1474 let Some(payload) = line.strip_prefix("data:") else {
1475 return true;
1476 };
1477 let payload = payload.trim();
1478 if style == Style::OpenAI && payload == "[DONE]" {
1479 return false;
1480 }
1481 let Ok(j) = serde_json::from_str::<Value>(payload) else {
1482 return true;
1483 };
1484 if !j.is_object() {
1485 return true;
1486 }
1487 if style == Style::OpenAI {
1488 if let Some(u) = j.get("usage").filter(|u| u.is_object()) {
1489 add_usage(&mut st.usage, u, style);
1490 }
1491 let Some(d) = j
1492 .get("choices")
1493 .and_then(Value::as_array)
1494 .and_then(|c| c.first())
1495 .and_then(|c| c.get("delta"))
1496 .filter(|d| d.is_object())
1497 else {
1498 return true;
1499 };
1500 if let Some(t) = d.get("content").and_then(Value::as_str)
1501 && !t.is_empty()
1502 {
1503 content.push_str(t);
1504 send(tx, StreamEvent::text(t));
1505 }
1506 for tc in d
1507 .get("tool_calls")
1508 .and_then(Value::as_array)
1509 .into_iter()
1510 .flatten()
1511 {
1512 if !tc.is_object() {
1513 continue;
1514 }
1515 let idx = tc.get("index").and_then(Value::as_f64).unwrap_or(0.0) as i64;
1516 let pos = match acc.iter().position(|(i, _, _)| *i == idx) {
1517 Some(p) => p,
1518 None => {
1519 acc.push((
1520 idx,
1521 Call {
1522 id: String::new(),
1523 name: String::new(),
1524 args: Value::Null,
1525 },
1526 String::new(),
1527 ));
1528 acc.len() - 1
1529 }
1530 };
1531 let slot = &mut acc[pos];
1532 if let Some(id) = tc.get("id").and_then(Value::as_str)
1533 && !id.is_empty()
1534 {
1535 slot.1.id = id.into();
1536 }
1537 if let Some(n) = tc.pointer("/function/name").and_then(Value::as_str) {
1538 slot.1.name.push_str(n);
1539 }
1540 if let Some(a) = tc.pointer("/function/arguments").and_then(Value::as_str) {
1541 slot.2.push_str(a);
1542 }
1543 }
1544 return true;
1545 }
1546 match j.get("type").and_then(Value::as_str) {
1547 Some("message_start") => {
1548 if let Some(u) = j.pointer("/message/usage").filter(|u| u.is_object()) {
1549 add_usage(&mut st.usage, u, style);
1550 }
1551 }
1552 Some("content_block_start") => {
1553 let idx = j.get("index").and_then(Value::as_f64).unwrap_or(0.0) as i64;
1554 let cb = j.get("content_block");
1555 let s = |k: &str| {
1556 cb.and_then(|c| c.get(k))
1557 .and_then(Value::as_str)
1558 .unwrap_or("")
1559 .to_owned()
1560 };
1561 let b = Block {
1562 typ: s("type"),
1563 id: s("id"),
1564 name: s("name"),
1565 ..Default::default()
1566 };
1567 blocks.retain(|(i, _)| *i != idx);
1568 blocks.push((idx, b));
1569 }
1570 Some("content_block_delta") => {
1571 let idx = j.get("index").and_then(Value::as_f64).unwrap_or(0.0) as i64;
1572 let Some((_, b)) = blocks.iter_mut().rev().find(|(i, _)| *i == idx) else {
1573 return true;
1574 };
1575 match j.pointer("/delta/type").and_then(Value::as_str) {
1576 Some("text_delta") => {
1577 if let Some(t) = j.pointer("/delta/text").and_then(Value::as_str) {
1578 b.text.push_str(t);
1579 send(tx, StreamEvent::text(t));
1580 }
1581 }
1582 Some("input_json_delta") => {
1583 if let Some(p) =
1584 j.pointer("/delta/partial_json").and_then(Value::as_str)
1585 {
1586 b.json.push_str(p);
1587 }
1588 }
1589 _ => {}
1590 }
1591 }
1592 Some("message_delta") => {
1593 if let Some(sr) = j.pointer("/delta/stop_reason").and_then(Value::as_str)
1594 && !sr.is_empty()
1595 {
1596 stop_reason = sr.into();
1597 }
1598 if let Some(u) = j.get("usage").filter(|u| u.is_object()) {
1599 add_usage(&mut st.usage, u, style);
1600 }
1601 }
1602 _ => {}
1603 }
1604 true
1605 };
1606
1607 match resp.body {
1608 RespBody::Full(b) => {
1609 for line in reader.push(&b).into_iter().chain(reader.finish()) {
1610 if !handle(&line, st) {
1611 break;
1612 }
1613 }
1614 }
1615 RespBody::Http(mut r) => loop {
1616 let chunk = tokio::select! {
1617 _ = cancel.cancelled() => return Err(ClientError::Cancelled),
1618 c = r.chunk() => c.map_err(|e| ClientError::Transport(e.to_string()))?,
1619 };
1620 let lines = match chunk {
1621 Some(c) => reader.push(&c),
1622 None => {
1623 done = true;
1624 reader.finish().into_iter().collect()
1625 }
1626 };
1627 let mut stop = false;
1628 for line in lines {
1629 if !handle(&line, st) {
1630 stop = true;
1631 break;
1632 }
1633 }
1634 if stop || done {
1635 break;
1636 }
1637 },
1638 RespBody::Chunks(mut rx) => loop {
1639 let chunk = tokio::select! {
1640 _ = cancel.cancelled() => return Err(ClientError::Cancelled),
1641 c = rx.recv() => c,
1642 };
1643 let lines = match chunk {
1644 Some(Ok(c)) => reader.push(&c),
1645 Some(Err(e)) => return Err(ClientError::Transport(e)),
1646 None => {
1647 done = true;
1648 reader.finish().into_iter().collect()
1649 }
1650 };
1651 let mut stop = false;
1652 for line in lines {
1653 if !handle(&line, st) {
1654 stop = true;
1655 break;
1656 }
1657 }
1658 if stop || done {
1659 break;
1660 }
1661 },
1662 }
1663 if cancel.is_cancelled() {
1664 return Err(ClientError::Cancelled);
1665 }
1666 let streamed = json!({"streamed": true});
1667 if style == Style::OpenAI {
1668 if acc.is_empty() {
1669 let asst = json!({"role": "assistant", "content": content});
1670 return Ok((Some(asst), TurnEnd::Final(content), streamed));
1671 }
1672 let raw_calls: Vec<Value> = acc
1673 .iter()
1674 .map(|(_, c, a)| json!({"id": c.id, "type": "function", "function": {"name": c.name, "arguments": a}}))
1675 .collect();
1676 let asst = json!({
1677 "role": "assistant",
1678 "tool_calls": raw_calls,
1679 "content": if content.is_empty() { Value::Null } else { json!(content) },
1680 });
1681 let calls = acc
1682 .into_iter()
1683 .map(|(_, mut c, a)| {
1684 c.args = safe_json_args(&a);
1685 c
1686 })
1687 .collect();
1688 return Ok((Some(asst), TurnEnd::Calls(calls), streamed));
1689 }
1690 let mut content_blocks = Vec::new();
1691 let mut text = String::new();
1692 let mut calls = Vec::new();
1693 for (_, b) in blocks {
1694 if b.typ == "tool_use" {
1695 let input = safe_json_args(&b.json);
1696 content_blocks
1697 .push(json!({"type": "tool_use", "id": b.id, "name": b.name, "input": input}));
1698 calls.push(Call {
1699 id: b.id,
1700 name: b.name,
1701 args: input,
1702 });
1703 } else {
1704 content_blocks.push(json!({"type": "text", "text": b.text}));
1705 text.push_str(&b.text);
1706 }
1707 }
1708 let asst = json!({"role": "assistant", "content": content_blocks});
1709 if stop_reason != "tool_use" || calls.is_empty() {
1710 return Ok((Some(asst), TurnEnd::Final(text), streamed));
1711 }
1712 Ok((Some(asst), TurnEnd::Calls(calls), streamed))
1713 }
1714
1715 async fn run_tool(
1718 &self,
1719 tk: &Toolkit,
1720 call: &Call,
1721 turn: usize,
1722 cancel: &CancellationToken,
1723 ) -> Result<(Value, ToolResult), ClientError> {
1724 if self.opts().tool_tiers.is_some() && call.name == "tool_search" {
1725 let start = Instant::now();
1726 let r = tiers::tool_search(tk, &call.args);
1727 self.emit_tool(&call.name, "builtin", &r, start);
1728 return Ok((call.args.clone(), r));
1729 }
1730 let (real, inner, wrapped) = self.unwrap_call(&call.name, &call.args);
1731 let (a, r) = self
1732 .run_tool_direct(tk, &real, inner, &call.id, turn, cancel)
1733 .await?;
1734 if wrapped {
1735 return Ok((json!({"name": real, "arguments": a}), r));
1736 }
1737 Ok((a, r))
1738 }
1739
1740 fn source_of(tk: &Toolkit, name: &str) -> (String, Option<ToolHints>) {
1741 match tk.get(name) {
1742 Some(t) => (
1743 serde_json::to_value(t.source)
1744 .ok()
1745 .and_then(|v| v.as_str().map(String::from))
1746 .unwrap_or_else(|| "custom".into()),
1747 t.hints,
1748 ),
1749 None => ("custom".into(), None),
1750 }
1751 }
1752
1753 async fn run_tool_direct(
1754 &self,
1755 tk: &Toolkit,
1756 name: &str,
1757 args: Value,
1758 id: &str,
1759 turn: usize,
1760 cancel: &CancellationToken,
1761 ) -> Result<(Value, ToolResult), ClientError> {
1762 let hooks = &self.opts().hooks;
1763 let (source, hints) = Self::source_of(tk, name);
1764 let start = Instant::now();
1765 let mut a = args;
1766 if let Some(h) = &hooks.before_tool {
1767 let ov = h(BeforeToolEvent {
1768 name: name.into(),
1769 args: a.clone(),
1770 id: id.into(),
1771 turn,
1772 hints,
1773 })
1774 .await
1775 .map_err(ClientError::Hook)?;
1776 if let Some(ov) = ov {
1777 if let Some(r) = ov.result {
1778 self.emit_tool(name, &source, &r, start);
1779 return Ok((a, r));
1780 }
1781 if let Some(na) = ov.args {
1782 a = na;
1783 }
1784 }
1785 }
1786 let ctx = ToolContext {
1787 cancel: cancel.clone(),
1788 call_id: Some(id.into()),
1789 ..Default::default()
1790 };
1791 let mut result = tk.execute(name, a.clone(), ctx).await;
1792 if let Some(h) = &hooks.after_tool
1793 && pending_of(&result).is_none()
1794 {
1795 let ov = h(AfterToolEvent {
1796 name: name.into(),
1797 args: a.clone(),
1798 result: result.clone(),
1799 id: id.into(),
1800 turn,
1801 })
1802 .await
1803 .map_err(ClientError::Hook)?;
1804 if let Some(r) = ov.and_then(|o| o.result) {
1805 result = r;
1806 }
1807 }
1808 self.emit_tool(name, &source, &result, start);
1809 Ok((a, result))
1810 }
1811
1812 #[allow(clippy::too_many_arguments)]
1815 async fn resolve_pending(
1816 &self,
1817 tk: &Toolkit,
1818 name: &str,
1819 args: &Value,
1820 request: Request,
1821 id: &str,
1822 turn: usize,
1823 cancel: &CancellationToken,
1824 ) -> Result<(ToolResult, Option<Request>), ClientError> {
1825 let (name, args, _) = self.unwrap_call(name, args);
1826 let o = self.opts();
1827 let answer = if let Some(w) = &o.wait_for_ctx {
1828 w(cancel.clone(), request.clone()).await
1829 } else if let Some(w) = &o.wait_for {
1830 w(request.clone()).await
1831 } else {
1832 return Ok((ToolResult::error(request.prompt.clone()), Some(request)));
1833 };
1834 let answer = answer.map_err(ClientError::WaitFor)?;
1835 if !answer.ok {
1836 return Ok((
1837 ToolResult::error(format!("declined/expired: {}", request.prompt)),
1838 None,
1839 ));
1840 }
1841 let start = Instant::now();
1842 let ctx = ToolContext {
1843 cancel: cancel.clone(),
1844 call_id: Some(id.into()),
1845 answer: Some(answer),
1846 ..Default::default()
1847 };
1848 let mut result = tk.execute(&name, args.clone(), ctx).await;
1849 if let Some(h) = &o.hooks.after_tool {
1850 let ov = h(AfterToolEvent {
1851 name: name.clone(),
1852 args: args.clone(),
1853 result: result.clone(),
1854 id: id.into(),
1855 turn,
1856 })
1857 .await
1858 .map_err(ClientError::Hook)?;
1859 if let Some(r) = ov.and_then(|o| o.result) {
1860 result = r;
1861 }
1862 }
1863 let (source, _) = Self::source_of(tk, &name);
1864 self.emit_tool(&name, &source, &result, start);
1865 if pending_of(&result).is_some() {
1866 result = ToolResult::error(format!("unresolved: {}", request.prompt));
1867 }
1868 Ok((result, None))
1869 }
1870
1871 fn call_event(&self, c: &Call) -> StreamEvent {
1872 let (real, inner, wrapped) = self.unwrap_call(&c.name, &c.args);
1873 StreamEvent {
1874 kind: "tool_call".into(),
1875 id: c.id.clone(),
1876 name: real,
1877 args: inner,
1878 via: if wrapped {
1879 "call_tool".into()
1880 } else {
1881 String::new()
1882 },
1883 ..Default::default()
1884 }
1885 }
1886
1887 fn result_event(&self, c: &Call, r: &ToolResult) -> StreamEvent {
1888 let (real, _, wrapped) = self.unwrap_call(&c.name, &c.args);
1889 StreamEvent {
1890 kind: "tool_result".into(),
1891 id: c.id.clone(),
1892 name: real,
1893 output: r.output.clone(),
1894 is_error: r.is_error,
1895 metadata: r.metadata.clone(),
1896 via: if wrapped {
1897 "call_tool".into()
1898 } else {
1899 String::new()
1900 },
1901 ..Default::default()
1902 }
1903 }
1904
1905 fn result(&self, st: &RunState, text: String) -> RunResult {
1908 RunResult {
1909 text,
1910 messages: st.messages.clone(),
1911 tool_calls: st.tool_calls.clone(),
1912 files: st.files.clone(),
1913 tool_call_count: st.tool_calls.len(),
1914 turns: st.turns,
1915 usage: st.usage,
1916 model: st.last_model.clone(),
1917 status: "done".into(),
1918 ..Default::default()
1919 }
1920 }
1921
1922 async fn run_loop(
1923 &self,
1924 st: &mut RunState,
1925 prompt: &Prompt,
1926 tk: &Toolkit,
1927 history: Vec<Value>,
1928 cancel: &CancellationToken,
1929 tx: Option<&Tx>,
1930 ) -> Result<RunResult, ClientError> {
1931 let key = self.resolve_key()?;
1932 let style = self.opts().style;
1933 let system = self.system(tk);
1934 if !history.is_empty() {
1935 st.messages = history;
1936 } else if style == Style::OpenAI && !system.is_empty() {
1937 st.messages
1938 .push(json!({"role": "system", "content": system}));
1939 }
1940 if !matches!(prompt, Prompt::Text(t) if t == crate::relay::RESUME_NO_PROMPT) {
1942 st.messages.push(user_message(prompt));
1943 }
1944 st.started = true;
1945 let req_tools = self.request_tools(tk);
1946 let mut tools = match style {
1947 Style::OpenAI => to_openai(&req_tools),
1948 Style::Anthropic => to_anthropic(&req_tools),
1949 };
1950 for turn in 0..self.max_turns() {
1951 st.turns += 1;
1952 let mut turn_model = self.opts().model.clone();
1953 if let Some(h) = &self.opts().hooks.before_llm {
1954 let ov = h(BeforeLlmEvent {
1955 messages: st.messages.clone(),
1956 tools: tools.clone(),
1957 model: self.opts().model.clone(),
1958 turn,
1959 })
1960 .await
1961 .map_err(ClientError::Hook)?;
1962 if let Some(ov) = ov {
1963 if let Some(m) = ov.messages {
1964 st.messages = m;
1965 }
1966 if let Some(t) = ov.tools {
1967 tools = t;
1968 }
1969 if let Some(m) = ov.model.filter(|m| !m.is_empty()) {
1970 turn_model = m;
1971 }
1972 }
1973 }
1974 let (asst, end) = self
1975 .call_model(st, &key, &turn_model, &system, &tools, turn, cancel, tx)
1976 .await?;
1977 if let Some(a) = asst {
1978 st.messages.push(a);
1979 }
1980 let calls = match end {
1981 TurnEnd::Empty => return Ok(self.end_run(st, String::new(), tx)),
1982 TurnEnd::Final(text) => return Ok(self.end_run(st, text, tx)),
1983 TurnEnd::Calls(c) => c,
1984 };
1985 for c in &calls {
1986 send(tx, self.call_event(c));
1987 }
1988 let futs = calls.iter().map(|c| async move {
1989 let (args, mut result) = self.run_tool(tk, c, turn, cancel).await?;
1990 let mut halted = None;
1991 if let Some(mut req) = pending_of(&result) {
1992 crate::relay::stamp_relay_call_id(&mut req, &c.id);
1994 send(
1995 tx,
1996 StreamEvent {
1997 request: Some(req.clone()),
1998 ..StreamEvent::of("pending")
1999 },
2000 );
2001 let (r, h) = self
2002 .resolve_pending(tk, &c.name, &args, req, &c.id, turn, cancel)
2003 .await?;
2004 result = r;
2005 halted = h;
2006 }
2007 if cancel.is_cancelled() {
2008 return Err(ClientError::Cancelled);
2009 }
2010 Ok::<_, ClientError>((args, result, halted))
2011 });
2012 let safe: Vec<bool> = calls.iter().map(|c| self.call_safe(tk, c)).collect();
2013 let cap = crate::tool::resolve_max_tool_concurrency(self.opts().max_tool_concurrency);
2014 let futs: Vec<_> = futs.collect();
2015 let outcomes = crate::tool::schedule_calls(futs, &safe, cap).await;
2016 let entry = |c: &Call, r: &ToolResult| match style {
2017 Style::OpenAI => emit::with_tool_parts(
2018 json!({"role": "tool", "tool_call_id": c.id, "content": r.output}),
2019 &c.name,
2020 &r.parts,
2021 ),
2022 Style::Anthropic => emit::with_tool_parts(
2023 json!({"type": "tool_result", "tool_use_id": c.id, "content": r.output, "is_error": r.is_error}),
2024 &c.name,
2025 &r.parts,
2026 ),
2027 };
2028 let record = |c: &Call, args: &Value, r: &ToolResult| {
2032 let (name, args, _) = self.unwrap_call(&c.name, args);
2033 ToolCall {
2034 name,
2035 args,
2036 output: r.output.clone(),
2037 is_error: r.is_error,
2038 metadata: r.metadata.clone(),
2039 parts: r.parts.clone(),
2040 }
2041 };
2042 let push = |st: &mut RunState, tc: ToolCall, id: &str| {
2043 st.files.extend(run_files_of(&tc, id));
2044 st.tool_calls.push(tc);
2045 };
2046 if let Some(err) = outcomes.iter().find_map(|o| o.as_ref().err()).cloned() {
2047 let mut blocks = Vec::new();
2049 for (c, o) in calls.iter().zip(&outcomes) {
2050 if let Ok((args, r, _)) = o {
2051 match style {
2052 Style::OpenAI => {
2053 push(st, record(c, args, r), &c.id);
2054 st.messages.push(entry(c, r));
2055 }
2056 Style::Anthropic => blocks.push(entry(c, r)),
2057 }
2058 }
2059 }
2060 if !blocks.is_empty() {
2061 st.messages.push(json!({"role": "user", "content": blocks}));
2062 }
2063 return Err(err);
2064 }
2065 let mut blocks = Vec::new();
2066 let mut halt: Option<Request> = None;
2067 let mut done = Vec::with_capacity(outcomes.len());
2068 let mut halts = Vec::with_capacity(outcomes.len());
2069 for o in outcomes {
2070 let (args, r, h) = o?;
2071 done.push((args, r));
2072 halts.push(h);
2073 }
2074 crate::relay::merge_relay_calls(&mut halts);
2077 for ((c, (args, r)), h) in calls.iter().zip(done).zip(halts) {
2078 push(st, record(c, &args, &r), &c.id);
2079 match style {
2080 Style::OpenAI => st.messages.push(entry(c, &r)),
2081 Style::Anthropic => blocks.push(entry(c, &r)),
2082 }
2083 if h.is_some() {
2084 halt = h;
2085 break;
2086 }
2087 send(tx, self.result_event(c, &r));
2088 }
2089 if style == Style::Anthropic {
2090 st.messages.push(json!({"role": "user", "content": blocks}));
2091 }
2092 if let Some(req) = halt {
2093 return Ok(self.pending_run(st, req, tx));
2094 }
2095 }
2096 let text = if style == Style::OpenAI {
2097 last_text(&st.messages)
2098 } else {
2099 String::new()
2100 };
2101 let mut res = self.end_run(st, text.clone(), None);
2102 if text.is_empty() {
2103 res.status = "incomplete".into();
2104 res.limit = "maxTurns".into();
2105 }
2106 if tx.is_some() {
2107 send(
2108 tx,
2109 StreamEvent {
2110 usage: Some(res.usage),
2111 result: Some(Box::new(res.clone())),
2112 ..StreamEvent::of("done")
2113 },
2114 );
2115 }
2116 Ok(res)
2117 }
2118
2119 fn end_run(&self, st: &RunState, text: String, tx: Option<&Tx>) -> RunResult {
2120 if let Some(tx) = tx {
2121 let _ = tx.send(StreamEvent {
2122 usage: Some(st.usage),
2123 ..StreamEvent::of("usage")
2124 });
2125 }
2126 self.record_metric(MetricEvent {
2127 event: "run".into(),
2128 model: st.last_model.clone(),
2129 turns: st.turns as i64,
2130 tool_calls: st.tool_calls.len() as i64,
2131 total_tokens: st.usage.total_tokens,
2132 ..Default::default()
2133 });
2134 let res = self.result(st, text);
2135 send(
2136 tx,
2137 StreamEvent {
2138 usage: Some(res.usage),
2139 result: Some(Box::new(res.clone())),
2140 ..StreamEvent::of("done")
2141 },
2142 );
2143 res
2144 }
2145
2146 fn pending_run(&self, st: &RunState, req: Request, tx: Option<&Tx>) -> RunResult {
2147 self.record_metric(MetricEvent {
2148 event: "run".into(),
2149 model: st.last_model.clone(),
2150 turns: st.turns as i64,
2151 tool_calls: st.tool_calls.len() as i64,
2152 total_tokens: st.usage.total_tokens,
2153 ..Default::default()
2154 });
2155 let mut res = self.result(st, req.prompt.clone());
2156 res.status = "pending".into();
2157 res.pending = Some(req);
2158 send(
2159 tx,
2160 StreamEvent {
2161 usage: Some(res.usage),
2162 result: Some(Box::new(res.clone())),
2163 ..StreamEvent::of("done")
2164 },
2165 );
2166 res
2167 }
2168
2169 async fn drive(
2172 &self,
2173 prompt: &Prompt,
2174 tk: &Toolkit,
2175 history: Vec<Value>,
2176 cancel: CancellationToken,
2177 tx: Option<&Tx>,
2178 ) -> (Result<RunResult, ClientError>, Option<Vec<Value>>) {
2179 let run_cancel = cancel.child_token();
2180 let start = Instant::now();
2181 let mut st = RunState {
2182 last_model: self.opts().model.clone(),
2183 ..Default::default()
2184 };
2185 let timeout_ms = self.opts().timeout_ms;
2186 let r = {
2187 let fut = self.run_loop(&mut st, prompt, tk, history, &run_cancel, tx);
2188 if timeout_ms > 0 {
2189 match tokio::time::timeout(Duration::from_millis(timeout_ms), fut).await {
2190 Ok(r) => r,
2191 Err(_) => {
2192 run_cancel.cancel();
2193 Err(ClientError::RunTimeout {
2194 timeout_ms,
2195 partial: Box::default(),
2196 })
2197 }
2198 }
2199 } else {
2200 fut.await
2201 }
2202 };
2203 match r {
2204 Ok(res) => (Ok(res), None),
2205 Err(e) => {
2206 let e = if timeout_ms > 0
2207 && matches!(e, ClientError::Cancelled)
2208 && start.elapsed() >= Duration::from_millis(timeout_ms)
2209 {
2210 ClientError::RunTimeout {
2211 timeout_ms,
2212 partial: Box::default(),
2213 }
2214 } else {
2215 e
2216 };
2217 self.emit_run(&st, start, e.to_string());
2218 let partial = if st.started {
2219 st.messages = seal_interrupted(std::mem::take(&mut st.messages));
2220 Some(st.messages.clone())
2221 } else {
2222 None
2223 };
2224 let e = match e {
2225 ClientError::RunTimeout { timeout_ms, .. } => {
2226 let mut res = self.result(&st, String::new());
2227 res.status = "incomplete".into();
2228 res.limit = "timeout".into();
2229 ClientError::RunTimeout {
2230 timeout_ms,
2231 partial: Box::new(res),
2232 }
2233 }
2234 e => e,
2235 };
2236 (Err(e), partial)
2237 }
2238 }
2239 }
2240
2241 pub async fn run(
2245 &self,
2246 prompt: impl Into<Prompt>,
2247 tk: &Toolkit,
2248 ) -> Result<RunResult, ClientError> {
2249 self.run_ctx(CancellationToken::new(), prompt, tk, Vec::new())
2250 .await
2251 }
2252
2253 pub async fn run_with_history(
2255 &self,
2256 prompt: impl Into<Prompt>,
2257 tk: &Toolkit,
2258 history: Vec<Value>,
2259 ) -> Result<RunResult, ClientError> {
2260 self.run_ctx(CancellationToken::new(), prompt, tk, history)
2261 .await
2262 }
2263
2264 pub async fn run_ctx(
2267 &self,
2268 cancel: CancellationToken,
2269 prompt: impl Into<Prompt>,
2270 tk: &Toolkit,
2271 history: Vec<Value>,
2272 ) -> Result<RunResult, ClientError> {
2273 let prompt = prompt.into();
2274 if let Prompt::Parts(parts) = &prompt {
2275 for p in parts {
2276 validate_part(p, self.opts().max_part_bytes).map_err(ClientError::Part)?;
2277 }
2278 }
2279 self.drive(&prompt, tk, history, cancel, None).await.0
2280 }
2281
2282 pub async fn run_with_answer(
2289 &self,
2290 cancel: CancellationToken,
2291 tk: &Toolkit,
2292 history: Vec<Value>,
2293 pending: &Request,
2294 answer: &Answer,
2295 ) -> Result<RunResult, ClientError> {
2296 if answer.id != pending.id {
2297 return Err(ClientError::Resume(format!(
2298 "toolnexus: cannot resume — answer id {:?} does not echo the pending request id {:?}",
2299 answer.id, pending.id
2300 )));
2301 }
2302 if history.is_empty() {
2303 return Err(ClientError::Resume(
2304 "toolnexus: cannot resume — no transcript supplied".into(),
2305 ));
2306 }
2307 let repaired = crate::relay::repair_halted_turn(&history, pending, answer)
2308 .map_err(ClientError::Resume)?;
2309 self.run_ctx(cancel, crate::relay::RESUME_NO_PROMPT, tk, repaired)
2310 .await
2311 }
2312
2313 pub async fn ask_with_answer(
2316 &self,
2317 cancel: CancellationToken,
2318 tk: &Toolkit,
2319 id: &str,
2320 pending: &Request,
2321 answer: &Answer,
2322 ) -> Result<RunResult, ClientError> {
2323 if id.is_empty() {
2324 return Err(ClientError::Resume(
2325 "toolnexus: AskWithAnswer needs a conversation id".into(),
2326 ));
2327 }
2328 let history = self
2329 .inner
2330 .store
2331 .get(id)
2332 .map_err(ClientError::Store)?
2333 .unwrap_or_default();
2334 let res = self
2335 .run_with_answer(cancel, tk, history, pending, answer)
2336 .await?;
2337 self.inner
2338 .store
2339 .save(id, &res.messages)
2340 .map_err(ClientError::Store)?;
2341 Ok(res)
2342 }
2343
2344 pub async fn ask(
2347 &self,
2348 prompt: impl Into<Prompt>,
2349 tk: &Toolkit,
2350 id: &str,
2351 ) -> Result<RunResult, ClientError> {
2352 self.ask_ctx(CancellationToken::new(), prompt, tk, id).await
2353 }
2354
2355 pub async fn ask_ctx(
2356 &self,
2357 cancel: CancellationToken,
2358 prompt: impl Into<Prompt>,
2359 tk: &Toolkit,
2360 id: &str,
2361 ) -> Result<RunResult, ClientError> {
2362 let prompt = prompt.into();
2363 if id.is_empty() {
2364 return self.run_ctx(cancel, prompt, tk, Vec::new()).await;
2365 }
2366 let history = self
2367 .inner
2368 .store
2369 .get(id)
2370 .map_err(ClientError::Store)?
2371 .unwrap_or_default();
2372 let (r, partial) = self.drive(&prompt, tk, history.clone(), cancel, None).await;
2373 match r {
2374 Ok(res) => {
2375 self.inner
2376 .store
2377 .save(id, &res.messages)
2378 .map_err(ClientError::Store)?;
2379 Ok(res)
2380 }
2381 Err(e) => {
2382 self.save_partial(id, history, &prompt, tk, partial);
2383 Err(e)
2384 }
2385 }
2386 }
2387
2388 pub async fn ask_stream(
2390 &self,
2391 prompt: impl Into<Prompt>,
2392 tk: &Toolkit,
2393 id: &str,
2394 mut on_text: impl FnMut(&str) + Send,
2395 ) -> Result<RunResult, ClientError> {
2396 let mut rx = self.stream_with_id(prompt, tk, id)?;
2397 let mut result = None;
2398 let mut err = None;
2399 while let Some(ev) = rx.recv().await {
2400 match ev.kind.as_str() {
2401 "text" => on_text(&ev.delta),
2402 "done" => result = ev.result.map(|r| *r),
2403 "error" => err = ev.error,
2404 _ => {}
2405 }
2406 }
2407 if let Some(e) = err {
2408 return Err(e);
2409 }
2410 Ok(result.unwrap_or_default())
2411 }
2412
2413 fn save_partial(
2414 &self,
2415 id: &str,
2416 history: Vec<Value>,
2417 prompt: &Prompt,
2418 tk: &Toolkit,
2419 partial: Option<Vec<Value>>,
2420 ) {
2421 let msgs = match partial {
2422 Some(m) => m,
2423 None => {
2424 let empty = history.is_empty();
2425 let mut m = history;
2426 if empty && self.opts().style != Style::Anthropic {
2427 let sys = self.system(tk);
2428 if !sys.is_empty() {
2429 m.push(json!({"role": "system", "content": sys}));
2430 }
2431 }
2432 m.push(user_message(prompt));
2433 m
2434 }
2435 };
2436 let _ = self.inner.store.save(id, &msgs);
2437 }
2438
2439 pub fn stream(
2442 &self,
2443 prompt: impl Into<Prompt>,
2444 tk: &Toolkit,
2445 ) -> Result<mpsc::UnboundedReceiver<StreamEvent>, ClientError> {
2446 self.stream_ctx(CancellationToken::new(), prompt, tk, "")
2447 }
2448
2449 pub fn stream_with_id(
2451 &self,
2452 prompt: impl Into<Prompt>,
2453 tk: &Toolkit,
2454 id: &str,
2455 ) -> Result<mpsc::UnboundedReceiver<StreamEvent>, ClientError> {
2456 self.stream_ctx(CancellationToken::new(), prompt, tk, id)
2457 }
2458
2459 pub fn stream_ctx(
2460 &self,
2461 cancel: CancellationToken,
2462 prompt: impl Into<Prompt>,
2463 tk: &Toolkit,
2464 id: &str,
2465 ) -> Result<mpsc::UnboundedReceiver<StreamEvent>, ClientError> {
2466 let prompt = prompt.into();
2467 let history = if id.is_empty() {
2468 Vec::new()
2469 } else {
2470 self.inner
2471 .store
2472 .get(id)
2473 .map_err(ClientError::Store)?
2474 .unwrap_or_default()
2475 };
2476 let (tx, rx) = mpsc::unbounded_channel();
2477 let (this, tk, id) = (self.clone(), tk.clone(), id.to_owned());
2478 tokio::spawn(async move {
2479 let (r, partial) = this
2480 .drive(&prompt, &tk, history.clone(), cancel, Some(&tx))
2481 .await;
2482 match r {
2483 Ok(res) => {
2484 if !id.is_empty()
2485 && let Err(e) = this.inner.store.save(&id, &res.messages)
2486 {
2487 let _ = tx.send(StreamEvent {
2488 error: Some(ClientError::Store(e)),
2489 ..StreamEvent::of("error")
2490 });
2491 }
2492 }
2493 Err(e) => {
2494 if !id.is_empty() {
2495 this.save_partial(&id, history, &prompt, &tk, partial);
2496 }
2497 let _ = tx.send(StreamEvent {
2498 error: Some(e),
2499 ..StreamEvent::of("error")
2500 });
2501 }
2502 }
2503 });
2504 Ok(rx)
2505 }
2506
2507 pub fn conversation(&self, tk: &Toolkit) -> Conversation {
2509 Conversation {
2510 client: self.clone(),
2511 tk: tk.clone(),
2512 messages: Vec::new(),
2513 }
2514 }
2515}
2516
2517#[derive(Debug, Clone)]
2519pub struct Conversation {
2520 client: Client,
2521 tk: Toolkit,
2522 pub messages: Vec<Value>,
2523}
2524
2525impl Conversation {
2526 pub async fn send(&mut self, prompt: impl Into<Prompt>) -> Result<RunResult, ClientError> {
2527 let res = self
2528 .client
2529 .run_with_history(prompt, &self.tk, self.messages.clone())
2530 .await?;
2531 self.messages = res.messages.clone();
2532 Ok(res)
2533 }
2534 pub fn reset(&mut self) {
2535 self.messages.clear();
2536 }
2537}
2538
2539pub fn key_env_for_base_url(base_url: &str) -> &'static str {
2541 let h = host_of(base_url);
2542 if h == "api.openai.com" {
2543 "OPENAI_API_KEY"
2544 } else if h == "api.anthropic.com" {
2545 "ANTHROPIC_API_KEY"
2546 } else if h == "openrouter.ai" || h.ends_with(".openrouter.ai") {
2547 "OPENROUTER_API_KEY"
2548 } else {
2549 ""
2550 }
2551}
2552
2553fn host_of(base_url: &str) -> String {
2554 let Some(rest) = base_url.strip_prefix("https://").or_else(|| {
2555 if base_url.len() > 8 && base_url[..8].eq_ignore_ascii_case("https://") {
2557 Some(&base_url[8..])
2558 } else {
2559 None
2560 }
2561 }) else {
2562 return String::new();
2563 };
2564 let authority = rest.split(['/', '?', '#']).next().unwrap_or("");
2565 let hostport = authority.rsplit('@').next().unwrap_or("");
2566 let host = if let Some(stripped) = hostport.strip_prefix('[') {
2567 stripped.split(']').next().unwrap_or("")
2568 } else {
2569 hostport.split(':').next().unwrap_or("")
2570 };
2571 host.to_ascii_lowercase()
2572}