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,直连正常
181 lines
8.0 KiB
Python
181 lines
8.0 KiB
Python
"""Canonical OpenTelemetry GenAI semantic-convention mapper (always active).
|
|
|
|
Owns the attribute schema for every span kind the engine emits — LLM call,
|
|
guardrail, and service — so the engine itself never references attribute keys.
|
|
|
|
Each span kind declares its schema as a flat ``attribute key -> extractor``
|
|
table: one lambda per mapping operation, applied against the typed span data.
|
|
"""
|
|
|
|
from typing import Callable
|
|
|
|
from litellm.integrations.otel.mappers.base import AttributeMap, AttrValue, SpanData
|
|
from litellm.integrations.otel.mappers.utils import (
|
|
collect,
|
|
drop_none,
|
|
output_messages,
|
|
serialize_messages,
|
|
)
|
|
from litellm.integrations.otel.model.payloads import (
|
|
GuardrailSpanData,
|
|
LLMCallSpanData,
|
|
MCPToolCallSpanData,
|
|
ServiceSpanData,
|
|
ToolDefinition,
|
|
)
|
|
from litellm.integrations.otel.model.semconv import (
|
|
DB,
|
|
MCP,
|
|
Error,
|
|
GenAI,
|
|
LiteLLM,
|
|
Server,
|
|
)
|
|
from litellm.integrations.otel.model.spans import db_system
|
|
|
|
|
|
class GenAIMapper:
|
|
|
|
_LLM_CALL_ATTRS: dict[str, Callable[[LLMCallSpanData], AttrValue | None]] = {
|
|
GenAI.OPERATION_NAME: lambda d: d.operation.value,
|
|
GenAI.PROVIDER_NAME: lambda d: d.provider or None,
|
|
GenAI.REQUEST_MODEL: lambda d: d.request_model or None,
|
|
GenAI.REQUEST_TEMPERATURE: lambda d: d.request_params.temperature,
|
|
GenAI.REQUEST_TOP_P: lambda d: d.request_params.top_p,
|
|
GenAI.REQUEST_TOP_K: lambda d: d.request_params.top_k,
|
|
GenAI.REQUEST_MAX_TOKENS: lambda d: d.request_params.max_tokens,
|
|
GenAI.REQUEST_FREQUENCY_PENALTY: lambda d: d.request_params.frequency_penalty,
|
|
GenAI.REQUEST_PRESENCE_PENALTY: lambda d: d.request_params.presence_penalty,
|
|
GenAI.REQUEST_STOP_SEQUENCES: lambda d: (
|
|
list(d.request_params.stop_sequences)
|
|
if d.request_params.stop_sequences
|
|
else None
|
|
),
|
|
GenAI.REQUEST_SEED: lambda d: d.request_params.seed,
|
|
GenAI.INPUT_MESSAGES: lambda d: serialize_messages(d.messages_in),
|
|
GenAI.OUTPUT_MESSAGES: lambda d: serialize_messages(output_messages(d)),
|
|
GenAI.RESPONSE_MODEL: lambda d: d.response_model,
|
|
GenAI.RESPONSE_ID: lambda d: d.response_id,
|
|
GenAI.RESPONSE_FINISH_REASONS: lambda d: (
|
|
list(d.finish_reasons) if d.finish_reasons else None
|
|
),
|
|
GenAI.USAGE_INPUT_TOKENS: lambda d: d.usage.input_tokens,
|
|
GenAI.USAGE_OUTPUT_TOKENS: lambda d: d.usage.output_tokens,
|
|
Error.TYPE: lambda d: d.error.error_type if d.error else None,
|
|
Server.ADDRESS: lambda d: d.server.address if d.server else None,
|
|
Server.PORT: lambda d: d.server.port if d.server else None,
|
|
LiteLLM.CALL_ID: lambda d: d.identity.call_id or None,
|
|
# The provider/underlying model is only known once routing has picked a
|
|
# deployment, so it can't ride identity Baggage (seeded at auth, before
|
|
# routing) onto the boundary-born LLM span — stamp it directly here.
|
|
LiteLLM.PROVIDER_MODEL: lambda d: d.identity.provider_model or None,
|
|
f"{LiteLLM.COST_PREFIX}total": lambda d: d.response_cost,
|
|
# Per-component cost breakdown (from the StandardLoggingPayload
|
|
# ``cost_breakdown``). Each component is omitted when the source didn't
|
|
# report it, so spans stay sparse rather than carrying zeros.
|
|
f"{LiteLLM.COST_PREFIX}input": lambda d: d.cost.input,
|
|
f"{LiteLLM.COST_PREFIX}output": lambda d: d.cost.output,
|
|
f"{LiteLLM.COST_PREFIX}cache_read": lambda d: d.cost.cache_read,
|
|
f"{LiteLLM.COST_PREFIX}cache_creation": lambda d: d.cost.cache_creation,
|
|
f"{LiteLLM.COST_PREFIX}tool_usage": lambda d: d.cost.tool_usage,
|
|
f"{LiteLLM.COST_PREFIX}original": lambda d: d.cost.original,
|
|
f"{LiteLLM.COST_PREFIX}discount_amount": lambda d: d.cost.discount_amount,
|
|
f"{LiteLLM.COST_PREFIX}discount_percent": lambda d: d.cost.discount_percent,
|
|
f"{LiteLLM.COST_PREFIX}margin_fixed_amount": lambda d: d.cost.margin_fixed_amount,
|
|
f"{LiteLLM.COST_PREFIX}margin_percent": lambda d: d.cost.margin_percent,
|
|
f"{LiteLLM.COST_PREFIX}margin_total_amount": lambda d: d.cost.margin_total_amount,
|
|
LiteLLM.REQUEST_STREAMING: lambda d: d.is_streaming,
|
|
}
|
|
|
|
_TOOL_ATTRS: dict[str, Callable[[ToolDefinition], AttrValue | None]] = {
|
|
"name": lambda t: t.name,
|
|
"description": lambda t: t.description or None,
|
|
"parameters": lambda t: t.parameters_json or None,
|
|
}
|
|
|
|
_MCP_ATTRS: dict[str, Callable[[MCPToolCallSpanData], AttrValue | None]] = {
|
|
GenAI.OPERATION_NAME: lambda d: d.operation.value,
|
|
MCP.METHOD_NAME: lambda d: d.method,
|
|
MCP.SESSION_ID: lambda d: d.session_id,
|
|
GenAI.TOOL_NAME: lambda d: d.tool_name or None,
|
|
GenAI.TOOL_CALL_ARGUMENTS: lambda d: d.arguments_json,
|
|
GenAI.TOOL_CALL_RESULT: lambda d: d.result_json,
|
|
LiteLLM.MCP_SERVER_NAME: lambda d: d.server_name,
|
|
LiteLLM.CALL_ID: lambda d: d.identity.call_id or None,
|
|
f"{LiteLLM.COST_PREFIX}total": lambda d: d.response_cost,
|
|
}
|
|
|
|
_GUARDRAIL_ATTRS: dict[str, Callable[[GuardrailSpanData], AttrValue | None]] = {
|
|
LiteLLM.GUARDRAIL_NAME: lambda d: d.guardrail_name,
|
|
LiteLLM.GUARDRAIL_MODE: lambda d: d.mode,
|
|
LiteLLM.GUARDRAIL_STATUS: lambda d: d.status,
|
|
LiteLLM.GUARDRAIL_PROVIDER: lambda d: d.provider,
|
|
LiteLLM.GUARDRAIL_ACTION: lambda d: d.action,
|
|
LiteLLM.GUARDRAIL_RESPONSE: lambda d: d.response_json,
|
|
LiteLLM.GUARDRAIL_VIOLATION_CATEGORIES: lambda d: (
|
|
list(d.violation_categories) if d.violation_categories else None
|
|
),
|
|
LiteLLM.GUARDRAIL_CONFIDENCE_SCORE: lambda d: d.confidence_score,
|
|
LiteLLM.GUARDRAIL_RISK_SCORE: lambda d: d.risk_score,
|
|
LiteLLM.GUARDRAIL_MASKED_ENTITY_COUNT: lambda d: d.masked_entity_count,
|
|
LiteLLM.GUARDRAIL_DURATION: lambda d: d.duration,
|
|
LiteLLM.GUARDRAIL_ID: lambda d: d.guardrail_id,
|
|
LiteLLM.GUARDRAIL_POLICY_TEMPLATE: lambda d: d.policy_template,
|
|
LiteLLM.GUARDRAIL_DETECTION_METHOD: lambda d: d.detection_method,
|
|
}
|
|
|
|
_SERVICE_ATTRS: dict[str, Callable[[ServiceSpanData], AttrValue | None]] = {
|
|
LiteLLM.SERVICE_NAME: lambda d: d.service_name,
|
|
LiteLLM.SERVICE_CALL_TYPE: lambda d: d.call_type,
|
|
}
|
|
|
|
def map(self, data: SpanData) -> AttributeMap:
|
|
match data:
|
|
case LLMCallSpanData():
|
|
return self._llm_call(data)
|
|
case MCPToolCallSpanData():
|
|
return collect(self._MCP_ATTRS, data)
|
|
case GuardrailSpanData():
|
|
return self._guardrail(data)
|
|
case ServiceSpanData():
|
|
return self._service(data)
|
|
case _:
|
|
return {}
|
|
|
|
@classmethod
|
|
def _llm_call(cls, data: LLMCallSpanData) -> AttributeMap:
|
|
attrs = collect(cls._LLM_CALL_ATTRS, data)
|
|
attrs.update(
|
|
drop_none(
|
|
{
|
|
f"gen_ai.tool.{idx}.{suffix}": extract(tool)
|
|
for idx, tool in enumerate(data.tools)
|
|
for suffix, extract in cls._TOOL_ATTRS.items()
|
|
}
|
|
)
|
|
)
|
|
return attrs
|
|
|
|
@classmethod
|
|
def _guardrail(cls, data: GuardrailSpanData) -> AttributeMap:
|
|
return collect(cls._GUARDRAIL_ATTRS, data)
|
|
|
|
@classmethod
|
|
def _service(cls, data: ServiceSpanData) -> AttributeMap:
|
|
attrs = collect(cls._SERVICE_ATTRS, data)
|
|
# An outbound datastore call (DB_CALL / CLIENT span) also carries db.*
|
|
# semconv. Internal services (router, budget jobs, …) have no db.system,
|
|
# so they get only the litellm.service.* keys above.
|
|
system = db_system(data.service_name)
|
|
if system is not None:
|
|
attrs[DB.SYSTEM_NAME] = system
|
|
if data.call_type:
|
|
attrs[DB.OPERATION_NAME] = data.call_type
|
|
attrs.update(
|
|
{
|
|
f"{LiteLLM.METADATA_PREFIX}{key}": value
|
|
for key, value in data.event_metadata.items()
|
|
}
|
|
)
|
|
return attrs
|