fa45d8aa5f
- health_checklist.json: 192.168.1.122→node122
- ocr_client.py: docstring IP→node122
- docs/market-data-requirements.md: IP→node122
- 所有API调用通过ProxyHandler({})绕过系统代理
Privoxy对node122:18003返回500,直连正常
129 lines
4.7 KiB
Python
129 lines
4.7 KiB
Python
"""OpenInference attribute mapper (Arize + Arize-Phoenix shared vocabulary).
|
|
|
|
Spec: https://github.com/Arize-ai/openinference/tree/main/spec — the standard
|
|
both Arize and Phoenix consume. Composing this mapper after ``GenAIMapper``
|
|
gives the same span both vocabularies, so a single trace lights up Arize +
|
|
Phoenix + any other OpenInference-aware backend simultaneously.
|
|
"""
|
|
|
|
import json
|
|
from typing import Callable, Sequence
|
|
|
|
from litellm.integrations.otel.mappers.base import AttributeMap, AttrValue, SpanData
|
|
from litellm.integrations.otel.mappers.utils import (
|
|
collect,
|
|
drop_none,
|
|
json_if,
|
|
message_content,
|
|
output_messages,
|
|
)
|
|
from litellm.integrations.otel.model.payloads import (
|
|
LLMCallSpanData,
|
|
LLMRequestParams,
|
|
ToolDefinition,
|
|
)
|
|
|
|
|
|
class OpenInferenceMapper:
|
|
"""Emits OpenInference attributes for LLM_CALL spans.
|
|
|
|
Key families (per the OpenInference spec):
|
|
- ``openinference.span.kind`` — discriminator (``"LLM"`` here)
|
|
- ``llm.model_name`` / ``llm.provider`` / ``llm.invocation_parameters``
|
|
- ``llm.input_messages.{i}.message.role`` / ``...content``
|
|
- ``llm.output_messages.{i}.message.role`` / ``...content``
|
|
- ``llm.token_count.prompt`` / ``...completion`` / ``...total``
|
|
- ``input.value`` / ``output.value`` — JSON-serialized request / response
|
|
"""
|
|
|
|
_LLM_CALL_ATTRS: dict[str, Callable[[LLMCallSpanData], AttrValue | None]] = {
|
|
"openinference.span.kind": lambda d: "LLM",
|
|
"llm.model_name": lambda d: d.request_model or None,
|
|
"llm.provider": lambda d: d.provider or None,
|
|
"llm.token_count.prompt": lambda d: d.usage.input_tokens,
|
|
"llm.token_count.completion": lambda d: d.usage.output_tokens,
|
|
"llm.token_count.total": lambda d: d.usage.total_tokens,
|
|
}
|
|
|
|
# Folded into the ``llm.invocation_parameters`` JSON blob.
|
|
_INVOCATION_PARAMS: dict[str, Callable[[LLMRequestParams], AttrValue | None]] = {
|
|
"temperature": lambda rp: rp.temperature,
|
|
"top_p": lambda rp: rp.top_p,
|
|
"top_k": lambda rp: rp.top_k,
|
|
"max_tokens": lambda rp: rp.max_tokens,
|
|
"frequency_penalty": lambda rp: rp.frequency_penalty,
|
|
"presence_penalty": lambda rp: rp.presence_penalty,
|
|
"seed": lambda rp: rp.seed,
|
|
}
|
|
|
|
# Per-tool extractors, keyed by the ``llm.tools.{idx}.*`` suffix.
|
|
_TOOL_ATTRS: dict[str, Callable[[ToolDefinition], AttrValue | None]] = {
|
|
"tool.name": lambda t: t.name,
|
|
"tool.description": lambda t: t.description or None,
|
|
"tool.json_schema": lambda t: t.parameters_json or None,
|
|
}
|
|
|
|
# JSON-payload attributes: each builder returns the serialized blob or None.
|
|
_BLOB_ATTRS: dict[str, Callable[[LLMCallSpanData], AttrValue | None]] = {
|
|
"llm.invocation_parameters": lambda d: json_if(
|
|
collect(OpenInferenceMapper._INVOCATION_PARAMS, d.request_params)
|
|
),
|
|
}
|
|
|
|
def map(self, data: SpanData) -> AttributeMap:
|
|
match data:
|
|
case LLMCallSpanData():
|
|
return self._llm_call(data)
|
|
case _:
|
|
return {}
|
|
|
|
@classmethod
|
|
def _llm_call(cls, data: LLMCallSpanData) -> AttributeMap:
|
|
return {
|
|
**collect(cls._LLM_CALL_ATTRS, data),
|
|
**collect(cls._BLOB_ATTRS, data),
|
|
**cls._messages("llm.input_messages", "input.value", data.messages_in),
|
|
**cls._messages(
|
|
"llm.output_messages", "output.value", output_messages(data)
|
|
),
|
|
**cls._tools(data),
|
|
}
|
|
|
|
@staticmethod
|
|
def _messages(
|
|
prefix: str, value_key: str, messages: Sequence[object]
|
|
) -> AttributeMap:
|
|
"""Per-message ``{prefix}.{idx}.message.*`` keys + the ``value_key`` blob."""
|
|
parsed = [
|
|
(m.get("role") if isinstance(m, dict) else None, message_content(m))
|
|
for m in messages
|
|
]
|
|
attrs = drop_none(
|
|
{
|
|
key: value
|
|
for idx, (role, content) in enumerate(parsed)
|
|
for key, value in (
|
|
(
|
|
f"{prefix}.{idx}.message.role",
|
|
role if isinstance(role, str) else None,
|
|
),
|
|
(f"{prefix}.{idx}.message.content", content),
|
|
)
|
|
}
|
|
)
|
|
if parsed:
|
|
attrs[value_key] = json.dumps(
|
|
[{"role": role, "content": content} for role, content in parsed]
|
|
)
|
|
return attrs
|
|
|
|
@classmethod
|
|
def _tools(cls, data: LLMCallSpanData) -> AttributeMap:
|
|
return drop_none(
|
|
{
|
|
f"llm.tools.{idx}.{suffix}": extract(tool)
|
|
for idx, tool in enumerate(data.tools)
|
|
for suffix, extract in cls._TOOL_ATTRS.items()
|
|
}
|
|
)
|