usegradient 1.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,263 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any, Literal
5
+
6
+ from ..tools import Tool, execute_tool_calls
7
+ from ..tracing import get_tracer
8
+
9
+
10
+ def generate(
11
+ *,
12
+ provider: Literal["openai", "anthropic"],
13
+ model: str,
14
+ interface: Literal["responses", "chat", "messages"] | str | None = None,
15
+ messages: list[dict[str, Any]] | str,
16
+ tools: list[Tool] | None = None,
17
+ client: Any | None = None,
18
+ **kwargs: Any,
19
+ ) -> dict[str, Any]:
20
+ """Run a traced provider call and return a normalized response."""
21
+
22
+ tracer = get_tracer()
23
+ with tracer.start_span(f"llm.{provider}.{model}", kind="LLM") as span:
24
+ span.set_attribute("llm.provider", provider)
25
+ span.set_attribute("llm.model_name", model)
26
+ span.set_attribute(
27
+ "input.value",
28
+ messages if isinstance(messages, str) else json.dumps(messages, default=str),
29
+ )
30
+ if provider == "openai":
31
+ result = _generate_openai(
32
+ model=model,
33
+ interface=interface or "responses",
34
+ messages=messages,
35
+ tools=tools,
36
+ client=client,
37
+ **kwargs,
38
+ )
39
+ else:
40
+ result = _generate_anthropic(model=model, messages=messages, tools=tools, client=client, **kwargs)
41
+ usage = result.get("usage", {})
42
+ span.set_attribute("llm.token_count.prompt", int(usage.get("prompt_tokens", 0)))
43
+ span.set_attribute("llm.token_count.completion", int(usage.get("completion_tokens", 0)))
44
+ span.set_attribute("llm.token_count.total", int(usage.get("total_tokens", 0)))
45
+ span.set_output(result.get("text", ""))
46
+ return result
47
+
48
+
49
+ def run_agent_loop(
50
+ *,
51
+ provider: Literal["openai", "anthropic"],
52
+ model: str,
53
+ messages: list[dict[str, Any]],
54
+ tools: list[Tool],
55
+ client: Any | None = None,
56
+ max_turns: int = 8,
57
+ **kwargs: Any,
58
+ ) -> dict[str, Any]:
59
+ """Run a traced tool-calling loop similar to LangChain AgentExecutor."""
60
+
61
+ tracer = get_tracer()
62
+ with tracer.start_span("agent.loop", kind="AGENT") as span:
63
+ span.set_attribute("llm.provider", provider)
64
+ span.set_attribute("llm.model_name", model)
65
+ span.set_attribute("input.value", json.dumps(messages, default=str))
66
+ history = list(messages)
67
+ final_text = ""
68
+ usage = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
69
+ tool_results: list[dict[str, Any]] = []
70
+ for _ in range(max_turns):
71
+ result = generate(
72
+ provider=provider,
73
+ model=model,
74
+ messages=history,
75
+ tools=tools,
76
+ client=client,
77
+ **kwargs,
78
+ )
79
+ _merge_usage(usage, result.get("usage", {}))
80
+ tool_calls = result.get("tool_calls") or []
81
+ final_text = result.get("text", "")
82
+ if not tool_calls:
83
+ break
84
+ history.append({"role": "assistant", "content": final_text, "tool_calls": tool_calls})
85
+ executed = execute_tool_calls(tools, tool_calls)
86
+ tool_results.extend(executed)
87
+ for item in executed:
88
+ history.append(
89
+ {
90
+ "role": "tool",
91
+ "tool_call_id": item.get("tool_call_id"),
92
+ "name": item.get("name"),
93
+ "content": json.dumps(item.get("output", item.get("error")), default=str),
94
+ }
95
+ )
96
+ payload = {"text": final_text, "messages": history, "usage": usage, "tool_results": tool_results}
97
+ span.set_output(payload)
98
+ return payload
99
+
100
+
101
+ def _merge_usage(target: dict[str, int], incoming: dict[str, int]) -> None:
102
+ for key in ("prompt_tokens", "completion_tokens", "total_tokens"):
103
+ target[key] = int(target.get(key, 0)) + int(incoming.get(key, 0))
104
+
105
+
106
+ def _generate_openai(
107
+ *,
108
+ model: str,
109
+ interface: str,
110
+ messages: list[dict[str, Any]] | str,
111
+ tools: list[Tool] | None,
112
+ client: Any | None,
113
+ **kwargs: Any,
114
+ ) -> dict[str, Any]:
115
+ if client is None:
116
+ from openai import OpenAI
117
+
118
+ client = OpenAI()
119
+ if interface == "responses" and hasattr(client, "responses"):
120
+ payload: dict[str, Any] = {"model": model, "input": messages, **kwargs}
121
+ if tools:
122
+ payload["tools"] = [_openai_responses_tool(tool) for tool in tools]
123
+ response = client.responses.create(**payload)
124
+ return _parse_openai_responses_response(response)
125
+
126
+ payload = {"model": model, "messages": messages, **kwargs}
127
+ if tools:
128
+ payload["tools"] = [tool.to_openai() for tool in tools]
129
+ response = client.chat.completions.create(**payload)
130
+ text = ""
131
+ tool_calls: list[dict[str, Any]] = []
132
+ if response.choices:
133
+ message = response.choices[0].message
134
+ text = message.content or ""
135
+ for call in getattr(message, "tool_calls", None) or []:
136
+ tool_calls.append(
137
+ {
138
+ "id": call.id,
139
+ "name": call.function.name,
140
+ "arguments": call.function.arguments,
141
+ }
142
+ )
143
+ usage = getattr(response, "usage", None)
144
+ prompt_tokens = int(getattr(usage, "prompt_tokens", 0) or 0) if usage else 0
145
+ completion_tokens = int(getattr(usage, "completion_tokens", 0) or 0) if usage else 0
146
+ return {
147
+ "text": text,
148
+ "tool_calls": tool_calls,
149
+ "usage": {
150
+ "prompt_tokens": prompt_tokens,
151
+ "completion_tokens": completion_tokens,
152
+ "total_tokens": prompt_tokens + completion_tokens,
153
+ },
154
+ }
155
+
156
+
157
+ def _parse_openai_responses_response(response: Any) -> dict[str, Any]:
158
+ text = str(_get(response, "output_text", "") or "")
159
+ tool_calls: list[dict[str, Any]] = []
160
+ for item in _get(response, "output", []) or []:
161
+ item_type = _get(item, "type", "")
162
+ if item_type == "message" and not text:
163
+ text = _extract_response_message_text(item)
164
+ if item_type in {"function_call", "tool_call"}:
165
+ tool_calls.append(
166
+ {
167
+ "id": _get(item, "id", _get(item, "call_id", None)),
168
+ "name": _get(item, "name", ""),
169
+ "arguments": _get(item, "arguments", _get(item, "input", {})),
170
+ }
171
+ )
172
+ usage = _get(response, "usage", {}) or {}
173
+ prompt_tokens = int(_get(usage, "input_tokens", _get(usage, "prompt_tokens", 0)) or 0)
174
+ completion_tokens = int(_get(usage, "output_tokens", _get(usage, "completion_tokens", 0)) or 0)
175
+ total_tokens = int(_get(usage, "total_tokens", prompt_tokens + completion_tokens) or 0)
176
+ return {
177
+ "text": text,
178
+ "tool_calls": tool_calls,
179
+ "usage": {
180
+ "prompt_tokens": prompt_tokens,
181
+ "completion_tokens": completion_tokens,
182
+ "total_tokens": total_tokens,
183
+ },
184
+ }
185
+
186
+
187
+ def _openai_responses_tool(tool: Tool) -> dict[str, Any]:
188
+ raw = tool.to_openai()
189
+ fn = raw.get("function", {})
190
+ return {
191
+ "type": "function",
192
+ "name": fn.get("name", tool.name),
193
+ "description": fn.get("description", tool.description),
194
+ "parameters": fn.get("parameters", tool.parameters),
195
+ }
196
+
197
+
198
+ def _extract_response_message_text(item: Any) -> str:
199
+ parts: list[str] = []
200
+ for content in _get(item, "content", []) or []:
201
+ value = _get(content, "text", None)
202
+ if value is None:
203
+ value = _get(content, "output_text", "")
204
+ if value:
205
+ parts.append(str(value))
206
+ return "".join(parts)
207
+
208
+
209
+ def _generate_anthropic(
210
+ *,
211
+ model: str,
212
+ messages: list[dict[str, Any]] | str,
213
+ tools: list[Tool] | None,
214
+ client: Any | None,
215
+ **kwargs: Any,
216
+ ) -> dict[str, Any]:
217
+ if client is None:
218
+ from anthropic import Anthropic
219
+
220
+ client = Anthropic()
221
+ if isinstance(messages, str):
222
+ anthropic_messages = [{"role": "user", "content": messages}]
223
+ else:
224
+ anthropic_messages = messages
225
+ payload: dict[str, Any] = {
226
+ "model": model,
227
+ "max_tokens": kwargs.pop("max_tokens", 1024),
228
+ "messages": anthropic_messages,
229
+ **kwargs,
230
+ }
231
+ if tools:
232
+ payload["tools"] = [tool.to_anthropic() for tool in tools]
233
+ response = client.messages.create(**payload)
234
+ content = _get(response, "content", []) or []
235
+ text = "".join(str(_get(block, "text", "")) for block in content if _get(block, "type", "") == "text")
236
+ tool_calls: list[dict[str, Any]] = []
237
+ for block in content:
238
+ if _get(block, "type", "") == "tool_use":
239
+ tool_calls.append(
240
+ {
241
+ "id": _get(block, "id", None),
242
+ "name": _get(block, "name", ""),
243
+ "input": _get(block, "input", {}),
244
+ }
245
+ )
246
+ usage = _get(response, "usage", {}) or {}
247
+ prompt_tokens = int(_get(usage, "input_tokens", 0) or 0)
248
+ completion_tokens = int(_get(usage, "output_tokens", 0) or 0)
249
+ return {
250
+ "text": text,
251
+ "tool_calls": tool_calls,
252
+ "usage": {
253
+ "prompt_tokens": prompt_tokens,
254
+ "completion_tokens": completion_tokens,
255
+ "total_tokens": prompt_tokens + completion_tokens,
256
+ },
257
+ }
258
+
259
+
260
+ def _get(value: Any, key: str, default: Any = None) -> Any:
261
+ if isinstance(value, dict):
262
+ return value.get(key, default)
263
+ return getattr(value, key, default)
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ from ..tracing import get_tracer
7
+
8
+
9
+ def wrap_anthropic(client: Any) -> Any:
10
+ """Wrap an Anthropic client so messages.create calls are traced automatically."""
11
+
12
+ class MessagesProxy:
13
+ def __init__(self, messages: Any) -> None:
14
+ self._messages = messages
15
+
16
+ def create(self, *args: Any, **kwargs: Any) -> Any:
17
+ model = kwargs.get("model", "unknown")
18
+ prompt = kwargs.get("messages", args[0] if args else "")
19
+ response = self._messages.create(*args, **kwargs)
20
+ text = "".join(
21
+ getattr(block, "text", "")
22
+ for block in getattr(response, "content", [])
23
+ if getattr(block, "type", "") == "text"
24
+ )
25
+ usage = getattr(response, "usage", None)
26
+ prompt_tokens = int(getattr(usage, "input_tokens", 0) or 0) if usage else 0
27
+ completion_tokens = int(getattr(usage, "output_tokens", 0) or 0) if usage else 0
28
+ tracer = get_tracer()
29
+ with tracer.start_span(f"anthropic.messages.{model}", kind="LLM") as span:
30
+ span.set_attribute("llm.provider", "anthropic")
31
+ span.set_attribute("llm.model_name", model)
32
+ span.set_attribute(
33
+ "input.value",
34
+ prompt if isinstance(prompt, str) else json.dumps(prompt, default=str),
35
+ )
36
+ span.set_attribute("llm.token_count.prompt", prompt_tokens)
37
+ span.set_attribute("llm.token_count.completion", completion_tokens)
38
+ span.set_attribute("llm.token_count.total", prompt_tokens + completion_tokens)
39
+ span.set_output(text)
40
+ return response
41
+
42
+ class AnthropicProxy:
43
+ def __init__(self, inner: Any) -> None:
44
+ self._inner = inner
45
+ if hasattr(inner, "messages"):
46
+ self.messages = MessagesProxy(inner.messages)
47
+
48
+ def __getattr__(self, name: str) -> Any:
49
+ return getattr(self._inner, name)
50
+
51
+ return AnthropicProxy(client)
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ from ..tracing import get_tracer
7
+
8
+
9
+ def _record_llm_span(
10
+ *,
11
+ name: str,
12
+ provider: str,
13
+ model: str,
14
+ input_value: Any,
15
+ output_value: Any,
16
+ prompt_tokens: int = 0,
17
+ completion_tokens: int = 0,
18
+ ) -> None:
19
+ tracer = get_tracer()
20
+ with tracer.start_span(name, kind="LLM") as span:
21
+ span.set_attribute("llm.provider", provider)
22
+ span.set_attribute("llm.model_name", model)
23
+ span.set_attribute("input.value", input_value if isinstance(input_value, str) else json.dumps(input_value, default=str))
24
+ span.set_attribute("llm.token_count.prompt", prompt_tokens)
25
+ span.set_attribute("llm.token_count.completion", completion_tokens)
26
+ span.set_attribute("llm.token_count.total", prompt_tokens + completion_tokens)
27
+ span.set_output(output_value)
28
+
29
+
30
+ def wrap_openai(client: Any) -> Any:
31
+ """Wrap an OpenAI client so chat/responses calls are traced automatically."""
32
+
33
+ class ChatCompletionsProxy:
34
+ def __init__(self, completions: Any) -> None:
35
+ self._completions = completions
36
+
37
+ def create(self, *args: Any, **kwargs: Any) -> Any:
38
+ model = kwargs.get("model", "unknown")
39
+ messages = kwargs.get("messages", kwargs.get("input", args[0] if args else ""))
40
+ response = self._completions.create(*args, **kwargs)
41
+ text, prompt_tokens, completion_tokens = _extract_openai_chat_response(response)
42
+ _record_llm_span(
43
+ name=f"openai.chat.{model}",
44
+ provider="openai",
45
+ model=model,
46
+ input_value=messages,
47
+ output_value=text,
48
+ prompt_tokens=prompt_tokens,
49
+ completion_tokens=completion_tokens,
50
+ )
51
+ return response
52
+
53
+ class ResponsesProxy:
54
+ def __init__(self, responses: Any) -> None:
55
+ self._responses = responses
56
+
57
+ def create(self, *args: Any, **kwargs: Any) -> Any:
58
+ model = kwargs.get("model", "unknown")
59
+ prompt = kwargs.get("input", args[0] if args else "")
60
+ response = self._responses.create(*args, **kwargs)
61
+ text = getattr(response, "output_text", str(response))
62
+ usage = getattr(response, "usage", None)
63
+ prompt_tokens = int(getattr(usage, "input_tokens", 0) or 0) if usage else 0
64
+ completion_tokens = int(getattr(usage, "output_tokens", 0) or 0) if usage else 0
65
+ _record_llm_span(
66
+ name=f"openai.responses.{model}",
67
+ provider="openai",
68
+ model=model,
69
+ input_value=prompt,
70
+ output_value=text,
71
+ prompt_tokens=prompt_tokens,
72
+ completion_tokens=completion_tokens,
73
+ )
74
+ return response
75
+
76
+ class ChatProxy:
77
+ def __init__(self, chat: Any) -> None:
78
+ self._chat = chat
79
+ self.completions = ChatCompletionsProxy(getattr(chat, "completions", chat))
80
+
81
+ class OpenAIProxy:
82
+ def __init__(self, inner: Any) -> None:
83
+ self._inner = inner
84
+ if hasattr(inner, "chat"):
85
+ self.chat = ChatProxy(inner.chat)
86
+ if hasattr(inner, "responses"):
87
+ self.responses = ResponsesProxy(inner.responses)
88
+
89
+ def __getattr__(self, name: str) -> Any:
90
+ return getattr(self._inner, name)
91
+
92
+ return OpenAIProxy(client)
93
+
94
+
95
+ def _extract_openai_chat_response(response: Any) -> tuple[str, int, int]:
96
+ text = ""
97
+ choices = getattr(response, "choices", None) or []
98
+ if choices:
99
+ message = getattr(choices[0], "message", None)
100
+ if message is not None:
101
+ text = getattr(message, "content", "") or ""
102
+ usage = getattr(response, "usage", None)
103
+ prompt_tokens = int(getattr(usage, "prompt_tokens", 0) or 0) if usage else 0
104
+ completion_tokens = int(getattr(usage, "completion_tokens", 0) or 0) if usage else 0
105
+ return text, prompt_tokens, completion_tokens
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+ from typing import Any
6
+
7
+
8
+ class Model(str, Enum):
9
+ GPT_5_6 = "openai:gpt-5.6"
10
+ GPT_5_6_SOL = "openai:gpt-5.6-sol"
11
+ GPT_5_6_TERRA = "openai:gpt-5.6-terra"
12
+ GPT_5_6_LUNA = "openai:gpt-5.6-luna"
13
+ GPT_5_5 = "openai:gpt-5.5"
14
+ GPT_5_5_PRO = "openai:gpt-5.5-pro"
15
+ GPT_5_4 = "openai:gpt-5.4"
16
+ GPT_5_4_PRO = "openai:gpt-5.4-pro"
17
+ GPT_5_4_MINI = "openai:gpt-5.4-mini"
18
+ GPT_5_4_NANO = "openai:gpt-5.4-nano"
19
+ GPT_5_3_CODEX = "openai:gpt-5.3-codex"
20
+ GPT_5_2 = "openai:gpt-5.2"
21
+ GPT_5_2_PRO = "openai:gpt-5.2-pro"
22
+ GPT_5_1 = "openai:gpt-5.1"
23
+ GPT_5 = "openai:gpt-5"
24
+ GPT_5_MINI = "openai:gpt-5-mini"
25
+ GPT_5_NANO = "openai:gpt-5-nano"
26
+ GPT_5_PRO = "openai:gpt-5-pro"
27
+ GPT_4_1 = "openai:gpt-4.1"
28
+ GPT_4_1_MINI = "openai:gpt-4.1-mini"
29
+ GPT_4O_MINI = "openai:gpt-4o-mini"
30
+
31
+ CLAUDE_FABLE_5 = "anthropic:claude-fable-5"
32
+ CLAUDE_OPUS_4_8 = "anthropic:claude-opus-4-8"
33
+ CLAUDE_SONNET_5 = "anthropic:claude-sonnet-5"
34
+ CLAUDE_HAIKU_4_5 = "anthropic:claude-haiku-4-5"
35
+ CLAUDE_HAIKU_4_5_20251001 = "anthropic:claude-haiku-4-5-20251001"
36
+
37
+
38
+ @dataclass(frozen=True, slots=True)
39
+ class ModelSpec:
40
+ provider: str
41
+ model: str
42
+ interface: str
43
+
44
+
45
+ MODEL_SPECS: dict[Model, ModelSpec] = {
46
+ item: ModelSpec(
47
+ provider=item.value.split(":", 1)[0],
48
+ model=item.value.split(":", 1)[1],
49
+ interface="messages" if item.value.startswith("anthropic:") else "responses",
50
+ )
51
+ for item in Model
52
+ }
53
+
54
+ MODEL_ALIASES: dict[str, Model] = {}
55
+ for item in Model:
56
+ MODEL_ALIASES[item.value] = item
57
+ MODEL_ALIASES[item.value.split(":", 1)[1]] = item
58
+
59
+
60
+ def resolve_model(value: Model | str | Any) -> ModelSpec | None:
61
+ if isinstance(value, Model):
62
+ return MODEL_SPECS[value]
63
+ if not isinstance(value, str):
64
+ return None
65
+ model = MODEL_ALIASES.get(value)
66
+ if model is None:
67
+ supported = ", ".join(item.value for item in Model)
68
+ raise ValueError(
69
+ f"unsupported model {value!r}; use one of Model.* or these ids: {supported}"
70
+ )
71
+ return MODEL_SPECS[model]
72
+
73
+
74
+ def supported_models() -> list[str]:
75
+ return [item.value for item in Model]