Otel: llm.request client spans + traceparent on LLM calls (#130)
Add OpenTelemetry tracing to every LLM client call so an insight/chat turn is a single trace from HTTP entry to each downstream inference. - LlmClient trait: thread a &opentelemetry::Context through all 5 methods (generate, chat_with_tools, chat_with_tools_stream, generate_embeddings, describe_image), matching the DAO convention of explicit context threading rather than ambient Context::current(). - otel::start_llm_request_span: single shared helper that every provider (openrouter / llamacpp / ollama) funnels through, so the span shape is identical: name llm.request, SpanKind::Client, llm.provider / llm.operation / llm.model. - Per-method spans in all three clients with the spec's operation enum (chat | chat_stream | embeddings | describe_image | generate); stream spans are owned and moved into the async_stream generator so they end when the response drains, not when headers arrive. - llm.tokens.input / llm.tokens.output recorded on non-stream and stream completion; llm.cost_usd from usage.cost (OpenRouter); http.status_code + error status on failure. - W3C traceparent injected on outgoing LLM HTTP requests (llama-swap, Ollama, OpenRouter) gated on span_context.is_valid() so debug no-op-tracer builds don't send an all-zero parent. - Turn-level spans (ai.chat.turn.execute, ai.persona_chat.turn.execute) now carry llm.backend + llm.model; with_entry streaming paths thread the turn context (turn_cx) to child describe_image / agentic-loop calls instead of the ambient context. clippy clean; 690 lib + 775 integration + 4 doc tests green.
This commit is contained in:
1 parent
e84ac82f86
commit
696d9fa8f3
17 files changed
+910
-234
No files matched your search
+9
-5
@@ -115,24 +115,26 @@ fn embed_prefix(key: &str) -> String {
|
||||
/// `Instruct: <task>\nQuery: `. Must pair with the document prefix the
|
||||
/// corpus was embedded with or similarity degrades.
|
||||
pub async fn embed_query(
|
||||
cx: &opentelemetry::Context,
|
||||
ollama: &OllamaClient,
|
||||
llamacpp: Option<&LlamaCppClient>,
|
||||
text: &str,
|
||||
) -> anyhow::Result<Vec<f32>> {
|
||||
let prefixed = format!("{}{}", embed_prefix("EMBED_QUERY_PREFIX"), text);
|
||||
embed_one(ollama, llamacpp, &prefixed).await
|
||||
embed_one(cx, ollama, llamacpp, &prefixed).await
|
||||
}
|
||||
|
||||
/// Embed corpus text (the stored side of retrieval). Applies
|
||||
/// `EMBED_DOCUMENT_PREFIX` (default empty; nomic v1.5 wants
|
||||
/// `search_document: `, Qwen3-Embedding wants none).
|
||||
pub async fn embed_document(
|
||||
cx: &opentelemetry::Context,
|
||||
ollama: &OllamaClient,
|
||||
llamacpp: Option<&LlamaCppClient>,
|
||||
text: &str,
|
||||
) -> anyhow::Result<Vec<f32>> {
|
||||
let prefixed = format!("{}{}", embed_prefix("EMBED_DOCUMENT_PREFIX"), text);
|
||||
embed_one(ollama, llamacpp, &prefixed).await
|
||||
embed_one(cx, ollama, llamacpp, &prefixed).await
|
||||
}
|
||||
|
||||
/// Embed a batch of strings via the configured local backend. Routes
|
||||
@@ -140,30 +142,32 @@ pub async fn embed_document(
|
||||
/// configured), else Ollama. See [`local_backend_is_llamacpp`] for the
|
||||
/// rationale on consistency.
|
||||
pub async fn embed_many(
|
||||
cx: &opentelemetry::Context,
|
||||
ollama: &OllamaClient,
|
||||
llamacpp: Option<&LlamaCppClient>,
|
||||
texts: &[&str],
|
||||
) -> anyhow::Result<Vec<Vec<f32>>> {
|
||||
if local_backend_is_llamacpp() {
|
||||
if let Some(lc) = llamacpp {
|
||||
return <LlamaCppClient as LlmClient>::generate_embeddings(lc, texts).await;
|
||||
return <LlamaCppClient as LlmClient>::generate_embeddings(lc, cx, texts).await;
|
||||
}
|
||||
anyhow::bail!(
|
||||
"LLM_BACKEND=llamacpp but LlamaCppClient is unconfigured — \
|
||||
set LLAMA_SWAP_URL or switch to LLM_BACKEND=ollama"
|
||||
);
|
||||
}
|
||||
ollama.generate_embeddings(texts).await
|
||||
ollama.generate_embeddings(cx, texts).await
|
||||
}
|
||||
|
||||
/// Embed one string via the configured local backend. Single-text
|
||||
/// convenience over [`embed_many`].
|
||||
pub async fn embed_one(
|
||||
cx: &opentelemetry::Context,
|
||||
ollama: &OllamaClient,
|
||||
llamacpp: Option<&LlamaCppClient>,
|
||||
text: &str,
|
||||
) -> anyhow::Result<Vec<f32>> {
|
||||
let mut vecs = embed_many(ollama, llamacpp, &[text]).await?;
|
||||
let mut vecs = embed_many(cx, ollama, llamacpp, &[text]).await?;
|
||||
vecs.pop()
|
||||
.ok_or_else(|| anyhow::anyhow!("embedding backend returned no embeddings"))
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user