symbolicai 1.5.0__py3-none-any.whl → 1.7.0__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.
- symai/__init__.py +21 -71
- symai/backend/base.py +0 -26
- symai/backend/engines/drawing/engine_gemini_image.py +101 -0
- symai/backend/engines/embedding/engine_openai.py +11 -8
- symai/backend/engines/neurosymbolic/__init__.py +8 -0
- symai/backend/engines/neurosymbolic/engine_anthropic_claudeX_chat.py +1 -0
- symai/backend/engines/neurosymbolic/engine_anthropic_claudeX_reasoning.py +48 -1
- symai/backend/engines/neurosymbolic/engine_cerebras.py +1 -0
- symai/backend/engines/neurosymbolic/engine_google_geminiX_reasoning.py +14 -1
- symai/backend/engines/neurosymbolic/engine_openrouter.py +294 -0
- symai/backend/mixin/__init__.py +4 -0
- symai/backend/mixin/anthropic.py +37 -16
- symai/backend/mixin/openrouter.py +2 -0
- symai/components.py +203 -13
- symai/extended/interfaces/nanobanana.py +23 -0
- symai/interfaces.py +2 -0
- symai/ops/primitives.py +0 -18
- symai/shellsv.py +2 -7
- symai/strategy.py +44 -4
- {symbolicai-1.5.0.dist-info → symbolicai-1.7.0.dist-info}/METADATA +3 -10
- {symbolicai-1.5.0.dist-info → symbolicai-1.7.0.dist-info}/RECORD +25 -48
- {symbolicai-1.5.0.dist-info → symbolicai-1.7.0.dist-info}/WHEEL +1 -1
- symai/backend/driver/webclient.py +0 -217
- symai/backend/engines/crawler/engine_selenium.py +0 -94
- symai/backend/engines/drawing/engine_dall_e.py +0 -131
- symai/backend/engines/embedding/engine_plugin_embeddings.py +0 -12
- symai/backend/engines/experiments/engine_bard_wrapper.py +0 -131
- symai/backend/engines/experiments/engine_gptfinetuner.py +0 -32
- symai/backend/engines/experiments/engine_llamacpp_completion.py +0 -142
- symai/backend/engines/neurosymbolic/engine_openai_gptX_completion.py +0 -277
- symai/collect/__init__.py +0 -8
- symai/collect/dynamic.py +0 -117
- symai/collect/pipeline.py +0 -156
- symai/collect/stats.py +0 -434
- symai/extended/crawler.py +0 -21
- symai/extended/interfaces/selenium.py +0 -18
- symai/extended/interfaces/vectordb.py +0 -21
- symai/extended/personas/__init__.py +0 -3
- symai/extended/personas/builder.py +0 -105
- symai/extended/personas/dialogue.py +0 -126
- symai/extended/personas/persona.py +0 -154
- symai/extended/personas/research/__init__.py +0 -1
- symai/extended/personas/research/yann_lecun.py +0 -62
- symai/extended/personas/sales/__init__.py +0 -1
- symai/extended/personas/sales/erik_james.py +0 -62
- symai/extended/personas/student/__init__.py +0 -1
- symai/extended/personas/student/max_tenner.py +0 -51
- symai/extended/strategies/__init__.py +0 -1
- symai/extended/strategies/cot.py +0 -40
- {symbolicai-1.5.0.dist-info → symbolicai-1.7.0.dist-info}/entry_points.txt +0 -0
- {symbolicai-1.5.0.dist-info → symbolicai-1.7.0.dist-info}/licenses/LICENSE +0 -0
- {symbolicai-1.5.0.dist-info → symbolicai-1.7.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
|
|
5
|
+
import openai
|
|
6
|
+
|
|
7
|
+
from ....components import SelfPrompt
|
|
8
|
+
from ....core_ext import retry
|
|
9
|
+
from ....utils import UserMessage
|
|
10
|
+
from ...base import Engine
|
|
11
|
+
from ...settings import SYMAI_CONFIG
|
|
12
|
+
|
|
13
|
+
logging.getLogger("openai").setLevel(logging.ERROR)
|
|
14
|
+
logging.getLogger("requests").setLevel(logging.ERROR)
|
|
15
|
+
logging.getLogger("urllib").setLevel(logging.ERROR)
|
|
16
|
+
logging.getLogger("httpx").setLevel(logging.ERROR)
|
|
17
|
+
logging.getLogger("httpcore").setLevel(logging.ERROR)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_NON_VERBOSE_OUTPUT = (
|
|
21
|
+
"<META_INSTRUCTION/>\n"
|
|
22
|
+
"You do not output anything else, like verbose preambles or post explanation, such as "
|
|
23
|
+
'"Sure, let me...", "Hope that was helpful...", "Yes, I can help you with that...", etc. '
|
|
24
|
+
"Consider well formatted output, e.g. for sentences use punctuation, spaces etc. or for code use "
|
|
25
|
+
"indentation, etc. Never add meta instructions information to your output!\n\n"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class OpenRouterEngine(Engine):
|
|
30
|
+
def __init__(self, api_key: str | None = None, model: str | None = None):
|
|
31
|
+
super().__init__()
|
|
32
|
+
self.config = deepcopy(SYMAI_CONFIG)
|
|
33
|
+
# In case we use EngineRepository.register to inject the api_key and model => dynamically change the engine at runtime
|
|
34
|
+
if api_key is not None and model is not None:
|
|
35
|
+
self.config["NEUROSYMBOLIC_ENGINE_API_KEY"] = api_key
|
|
36
|
+
self.config["NEUROSYMBOLIC_ENGINE_MODEL"] = model
|
|
37
|
+
if self.id() != "neurosymbolic":
|
|
38
|
+
return # do not initialize if not neurosymbolic; avoids conflict with llama.cpp check in EngineRepository.register_from_package
|
|
39
|
+
openai.api_key = self.config["NEUROSYMBOLIC_ENGINE_API_KEY"]
|
|
40
|
+
self.model = self.config["NEUROSYMBOLIC_ENGINE_MODEL"]
|
|
41
|
+
self.seed = None
|
|
42
|
+
self.name = self.__class__.__name__
|
|
43
|
+
self._last_prompt_tokens = None
|
|
44
|
+
self._last_messages = None
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
self.client = openai.OpenAI(
|
|
48
|
+
api_key=openai.api_key, base_url="https://openrouter.ai/api/v1"
|
|
49
|
+
)
|
|
50
|
+
except Exception as exc:
|
|
51
|
+
UserMessage(
|
|
52
|
+
f"Failed to initialize OpenRouter client. Please check your OpenAI library version. Caused by: {exc}",
|
|
53
|
+
raise_with=ValueError,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def id(self) -> str:
|
|
57
|
+
model_name = self.config.get("NEUROSYMBOLIC_ENGINE_MODEL")
|
|
58
|
+
if model_name and model_name.startswith("openrouter"):
|
|
59
|
+
return "neurosymbolic"
|
|
60
|
+
return super().id()
|
|
61
|
+
|
|
62
|
+
def command(self, *args, **kwargs):
|
|
63
|
+
super().command(*args, **kwargs)
|
|
64
|
+
if "NEUROSYMBOLIC_ENGINE_API_KEY" in kwargs:
|
|
65
|
+
openai.api_key = kwargs["NEUROSYMBOLIC_ENGINE_API_KEY"]
|
|
66
|
+
if "NEUROSYMBOLIC_ENGINE_MODEL" in kwargs:
|
|
67
|
+
self.model = kwargs["NEUROSYMBOLIC_ENGINE_MODEL"]
|
|
68
|
+
if "seed" in kwargs:
|
|
69
|
+
self.seed = kwargs["seed"]
|
|
70
|
+
|
|
71
|
+
def compute_required_tokens(self, messages):
|
|
72
|
+
if self._last_prompt_tokens is not None and self._last_messages == messages:
|
|
73
|
+
return self._last_prompt_tokens
|
|
74
|
+
UserMessage(
|
|
75
|
+
"Token counting not implemented for this engine.", raise_with=NotImplementedError
|
|
76
|
+
)
|
|
77
|
+
return 0
|
|
78
|
+
|
|
79
|
+
def compute_remaining_tokens(self, _prompts: list) -> int:
|
|
80
|
+
UserMessage(
|
|
81
|
+
"Token counting not implemented for this engine.", raise_with=NotImplementedError
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def _handle_prefix(self, model_name: str) -> str:
|
|
85
|
+
"""Handle prefix for model name."""
|
|
86
|
+
return model_name.replace("openrouter:", "")
|
|
87
|
+
|
|
88
|
+
def _extract_thinking_content(self, output: list[str]) -> tuple[str | None, list[str]]:
|
|
89
|
+
"""Extract thinking content from textual output using <think>...</think> tags if present."""
|
|
90
|
+
if not output or not output[0]:
|
|
91
|
+
return None, output
|
|
92
|
+
|
|
93
|
+
content = output[0]
|
|
94
|
+
start = content.find("<think>")
|
|
95
|
+
if start == -1:
|
|
96
|
+
return None, output
|
|
97
|
+
|
|
98
|
+
end = content.find("</think>", start + 7)
|
|
99
|
+
if end == -1:
|
|
100
|
+
return None, output
|
|
101
|
+
|
|
102
|
+
thinking_content = content[start + 7 : end].strip() or None
|
|
103
|
+
cleaned_content = (content[:start] + content[end + 8 :]).strip()
|
|
104
|
+
cleaned_output = [cleaned_content, *output[1:]]
|
|
105
|
+
|
|
106
|
+
return thinking_content, cleaned_output
|
|
107
|
+
|
|
108
|
+
# cumulative wait time is < 30s
|
|
109
|
+
@retry(tries=8, delay=0.5, backoff=1.5, max_delay=5, jitter=(0, 0.5))
|
|
110
|
+
def forward(self, argument):
|
|
111
|
+
kwargs = argument.kwargs
|
|
112
|
+
messages = argument.prop.prepared_input
|
|
113
|
+
payload = self._prepare_request_payload(messages, argument)
|
|
114
|
+
except_remedy = kwargs.get("except_remedy")
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
res = self.client.chat.completions.create(**payload)
|
|
118
|
+
except Exception as exc:
|
|
119
|
+
if openai.api_key is None or openai.api_key == "":
|
|
120
|
+
msg = (
|
|
121
|
+
"OpenRouter API key is not set. Please set it in the config file or "
|
|
122
|
+
"pass it as an argument to the command method."
|
|
123
|
+
)
|
|
124
|
+
UserMessage(msg)
|
|
125
|
+
if (
|
|
126
|
+
self.config["NEUROSYMBOLIC_ENGINE_API_KEY"] is None
|
|
127
|
+
or self.config["NEUROSYMBOLIC_ENGINE_API_KEY"] == ""
|
|
128
|
+
):
|
|
129
|
+
UserMessage(msg, raise_with=ValueError)
|
|
130
|
+
openai.api_key = self.config["NEUROSYMBOLIC_ENGINE_API_KEY"]
|
|
131
|
+
|
|
132
|
+
callback = self.client.chat.completions.create
|
|
133
|
+
kwargs["model"] = (
|
|
134
|
+
self._handle_prefix(kwargs["model"])
|
|
135
|
+
if "model" in kwargs
|
|
136
|
+
else self._handle_prefix(self.model)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if except_remedy is not None:
|
|
140
|
+
res = except_remedy(self, exc, callback, argument)
|
|
141
|
+
else:
|
|
142
|
+
UserMessage(f"Error during generation. Caused by: {exc}", raise_with=ValueError)
|
|
143
|
+
|
|
144
|
+
prompt_tokens = getattr(res.usage, "prompt_tokens", None)
|
|
145
|
+
if prompt_tokens is None:
|
|
146
|
+
prompt_tokens = getattr(res.usage, "input_tokens", None)
|
|
147
|
+
self._last_prompt_tokens = prompt_tokens
|
|
148
|
+
self._last_messages = messages
|
|
149
|
+
|
|
150
|
+
metadata = {"raw_output": res}
|
|
151
|
+
if payload.get("tools"):
|
|
152
|
+
metadata = self._process_function_calls(res, metadata)
|
|
153
|
+
|
|
154
|
+
output = [r.message.content for r in res.choices]
|
|
155
|
+
thinking, output = self._extract_thinking_content(output)
|
|
156
|
+
if thinking:
|
|
157
|
+
metadata["thinking"] = thinking
|
|
158
|
+
|
|
159
|
+
return output, metadata
|
|
160
|
+
|
|
161
|
+
def _prepare_raw_input(self, argument):
|
|
162
|
+
if not argument.prop.processed_input:
|
|
163
|
+
UserMessage(
|
|
164
|
+
"Need to provide a prompt instruction to the engine if raw_input is enabled.",
|
|
165
|
+
raise_with=ValueError,
|
|
166
|
+
)
|
|
167
|
+
value = argument.prop.processed_input
|
|
168
|
+
if not isinstance(value, list):
|
|
169
|
+
if not isinstance(value, dict):
|
|
170
|
+
value = {"role": "user", "content": str(value)}
|
|
171
|
+
value = [value]
|
|
172
|
+
return value
|
|
173
|
+
|
|
174
|
+
def prepare(self, argument):
|
|
175
|
+
if argument.prop.raw_input:
|
|
176
|
+
argument.prop.prepared_input = self._prepare_raw_input(argument)
|
|
177
|
+
return
|
|
178
|
+
self._validate_response_format(argument)
|
|
179
|
+
|
|
180
|
+
system = self._build_system_message(argument)
|
|
181
|
+
user_content = self._build_user_content(argument)
|
|
182
|
+
user_prompt = {"role": "user", "content": user_content}
|
|
183
|
+
system, user_prompt = self._apply_self_prompt_if_needed(argument, system, user_prompt)
|
|
184
|
+
|
|
185
|
+
argument.prop.prepared_input = [
|
|
186
|
+
{"role": "system", "content": system},
|
|
187
|
+
user_prompt,
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
def _validate_response_format(self, argument) -> None:
|
|
191
|
+
if argument.prop.response_format:
|
|
192
|
+
response_format = argument.prop.response_format
|
|
193
|
+
assert response_format.get("type") is not None, (
|
|
194
|
+
'Expected format `{ "type": "json_object" }`! We are using the OpenAI compatible '
|
|
195
|
+
"API for OpenRouter."
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def _build_system_message(self, argument) -> str:
|
|
199
|
+
system: str = ""
|
|
200
|
+
if argument.prop.suppress_verbose_output:
|
|
201
|
+
system += _NON_VERBOSE_OUTPUT
|
|
202
|
+
if system:
|
|
203
|
+
system = f"{system}\n"
|
|
204
|
+
|
|
205
|
+
ref = argument.prop.instance
|
|
206
|
+
static_ctxt, dyn_ctxt = ref.global_context
|
|
207
|
+
if len(static_ctxt) > 0:
|
|
208
|
+
system += f"<STATIC CONTEXT/>\n{static_ctxt}\n\n"
|
|
209
|
+
|
|
210
|
+
if len(dyn_ctxt) > 0:
|
|
211
|
+
system += f"<DYNAMIC CONTEXT/>\n{dyn_ctxt}\n\n"
|
|
212
|
+
|
|
213
|
+
if argument.prop.payload:
|
|
214
|
+
system += f"<ADDITIONAL CONTEXT/>\n{argument.prop.payload!s}\n\n"
|
|
215
|
+
|
|
216
|
+
examples = argument.prop.examples
|
|
217
|
+
if examples and len(examples) > 0:
|
|
218
|
+
system += f"<EXAMPLES/>\n{examples!s}\n\n"
|
|
219
|
+
|
|
220
|
+
if argument.prop.prompt is not None and len(argument.prop.prompt) > 0:
|
|
221
|
+
val = str(argument.prop.prompt)
|
|
222
|
+
system += f"<INSTRUCTION/>\n{val}\n\n"
|
|
223
|
+
|
|
224
|
+
if argument.prop.template_suffix:
|
|
225
|
+
system += (
|
|
226
|
+
" You will only generate content for the placeholder "
|
|
227
|
+
f"`{argument.prop.template_suffix!s}` following the instructions and the provided context "
|
|
228
|
+
"information.\n\n"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
return system
|
|
232
|
+
|
|
233
|
+
def _build_user_content(self, argument) -> str:
|
|
234
|
+
return str(argument.prop.processed_input)
|
|
235
|
+
|
|
236
|
+
def _apply_self_prompt_if_needed(self, argument, system, user_prompt):
|
|
237
|
+
if argument.prop.instance._kwargs.get("self_prompt", False) or argument.prop.self_prompt:
|
|
238
|
+
self_prompter = SelfPrompt()
|
|
239
|
+
res = self_prompter({"user": user_prompt["content"], "system": system})
|
|
240
|
+
if res is None:
|
|
241
|
+
UserMessage("Self-prompting failed!", raise_with=ValueError)
|
|
242
|
+
return res["system"], {"role": "user", "content": res["user"]}
|
|
243
|
+
return system, user_prompt
|
|
244
|
+
|
|
245
|
+
def _process_function_calls(self, res, metadata):
|
|
246
|
+
hit = False
|
|
247
|
+
if (
|
|
248
|
+
hasattr(res, "choices")
|
|
249
|
+
and res.choices
|
|
250
|
+
and hasattr(res.choices[0], "message")
|
|
251
|
+
and res.choices[0].message
|
|
252
|
+
and hasattr(res.choices[0].message, "tool_calls")
|
|
253
|
+
and res.choices[0].message.tool_calls
|
|
254
|
+
):
|
|
255
|
+
for tool_call in res.choices[0].message.tool_calls:
|
|
256
|
+
if hasattr(tool_call, "function") and tool_call.function:
|
|
257
|
+
if hit:
|
|
258
|
+
UserMessage(
|
|
259
|
+
"Multiple function calls detected in the response but only the first one will be processed."
|
|
260
|
+
)
|
|
261
|
+
break
|
|
262
|
+
try:
|
|
263
|
+
args_dict = json.loads(tool_call.function.arguments)
|
|
264
|
+
except json.JSONDecodeError:
|
|
265
|
+
args_dict = {}
|
|
266
|
+
metadata["function_call"] = {
|
|
267
|
+
"name": tool_call.function.name,
|
|
268
|
+
"arguments": args_dict,
|
|
269
|
+
}
|
|
270
|
+
hit = True
|
|
271
|
+
return metadata
|
|
272
|
+
|
|
273
|
+
# TODO: requires updates for reasoning
|
|
274
|
+
def _prepare_request_payload(self, messages, argument):
|
|
275
|
+
kwargs = argument.kwargs
|
|
276
|
+
max_tokens = kwargs.get("max_tokens")
|
|
277
|
+
if max_tokens is None:
|
|
278
|
+
max_tokens = kwargs.get("max_completion_tokens")
|
|
279
|
+
return {
|
|
280
|
+
"messages": messages,
|
|
281
|
+
"model": self._handle_prefix(kwargs.get("model", self.model)),
|
|
282
|
+
"seed": kwargs.get("seed", self.seed),
|
|
283
|
+
"max_tokens": max_tokens,
|
|
284
|
+
"stop": kwargs.get("stop"),
|
|
285
|
+
"temperature": kwargs.get("temperature", 1),
|
|
286
|
+
"frequency_penalty": kwargs.get("frequency_penalty", 0),
|
|
287
|
+
"presence_penalty": kwargs.get("presence_penalty", 0),
|
|
288
|
+
"top_p": kwargs.get("top_p", 1),
|
|
289
|
+
"n": kwargs.get("n", 1),
|
|
290
|
+
"tools": kwargs.get("tools"),
|
|
291
|
+
"tool_choice": kwargs.get("tool_choice"),
|
|
292
|
+
"response_format": kwargs.get("response_format"),
|
|
293
|
+
"stream": kwargs.get("stream", False),
|
|
294
|
+
}
|
symai/backend/mixin/__init__.py
CHANGED
|
@@ -11,6 +11,8 @@ from .groq import SUPPORTED_REASONING_MODELS as GROQ_REASONING_MODELS
|
|
|
11
11
|
from .openai import SUPPORTED_CHAT_MODELS as OPENAI_CHAT_MODELS
|
|
12
12
|
from .openai import SUPPORTED_REASONING_MODELS as OPENAI_REASONING_MODELS
|
|
13
13
|
from .openai import SUPPORTED_RESPONSES_MODELS as OPENAI_RESPONSES_MODELS
|
|
14
|
+
from .openrouter import SUPPORTED_CHAT_MODELS as OPENROUTER_CHAT_MODELS
|
|
15
|
+
from .openrouter import SUPPORTED_REASONING_MODELS as OPENROUTER_REASONING_MODELS
|
|
14
16
|
|
|
15
17
|
__all__ = [
|
|
16
18
|
"ANTHROPIC_CHAT_MODELS",
|
|
@@ -26,4 +28,6 @@ __all__ = [
|
|
|
26
28
|
"OPENAI_CHAT_MODELS",
|
|
27
29
|
"OPENAI_REASONING_MODELS",
|
|
28
30
|
"OPENAI_RESPONSES_MODELS",
|
|
31
|
+
"OPENROUTER_CHAT_MODELS",
|
|
32
|
+
"OPENROUTER_REASONING_MODELS",
|
|
29
33
|
]
|
symai/backend/mixin/anthropic.py
CHANGED
|
@@ -10,6 +10,7 @@ SUPPORTED_CHAT_MODELS = [
|
|
|
10
10
|
"claude-3-haiku-20240307",
|
|
11
11
|
]
|
|
12
12
|
SUPPORTED_REASONING_MODELS = [
|
|
13
|
+
"claude-opus-4-6",
|
|
13
14
|
"claude-opus-4-5",
|
|
14
15
|
"claude-opus-4-1",
|
|
15
16
|
"claude-opus-4-0",
|
|
@@ -19,29 +20,49 @@ SUPPORTED_REASONING_MODELS = [
|
|
|
19
20
|
"claude-sonnet-4-5",
|
|
20
21
|
]
|
|
21
22
|
|
|
23
|
+
LONG_CONTEXT_1M_TOKENS = 1_000_000
|
|
24
|
+
LONG_CONTEXT_1M_BETA_HEADER = "context-1m-2025-08-07"
|
|
25
|
+
LONG_CONTEXT_1M_MODELS = {
|
|
26
|
+
"claude-opus-4-6",
|
|
27
|
+
"claude-sonnet-4-5",
|
|
28
|
+
}
|
|
29
|
+
|
|
22
30
|
|
|
23
31
|
class AnthropicMixin:
|
|
24
|
-
def
|
|
32
|
+
def supports_long_context_1m(self, model: str) -> bool:
|
|
33
|
+
return model in LONG_CONTEXT_1M_MODELS
|
|
34
|
+
|
|
35
|
+
def long_context_beta_header(self) -> str:
|
|
36
|
+
return LONG_CONTEXT_1M_BETA_HEADER
|
|
37
|
+
|
|
38
|
+
def api_max_context_tokens(self, long_context_1m: bool = False, model: str | None = None):
|
|
39
|
+
selected_model = self.model if model is None else model
|
|
40
|
+
if long_context_1m and self.supports_long_context_1m(selected_model):
|
|
41
|
+
return LONG_CONTEXT_1M_TOKENS
|
|
25
42
|
if (
|
|
26
|
-
|
|
27
|
-
or
|
|
28
|
-
or
|
|
29
|
-
or
|
|
30
|
-
or
|
|
31
|
-
or
|
|
32
|
-
or
|
|
33
|
-
or
|
|
34
|
-
or
|
|
35
|
-
or
|
|
36
|
-
or
|
|
37
|
-
or
|
|
38
|
-
or
|
|
39
|
-
or
|
|
43
|
+
selected_model == "claude-opus-4-6"
|
|
44
|
+
or selected_model == "claude-opus-4-5"
|
|
45
|
+
or selected_model == "claude-opus-4-1"
|
|
46
|
+
or selected_model == "claude-opus-4-0"
|
|
47
|
+
or selected_model == "claude-sonnet-4-0"
|
|
48
|
+
or selected_model == "claude-3-7-sonnet-latest"
|
|
49
|
+
or selected_model == "claude-haiku-4-5"
|
|
50
|
+
or selected_model == "claude-sonnet-4-5"
|
|
51
|
+
or selected_model == "claude-3-5-sonnet-latest"
|
|
52
|
+
or selected_model == "claude-3-5-sonnet-20241022"
|
|
53
|
+
or selected_model == "claude-3-5-sonnet-20240620"
|
|
54
|
+
or selected_model == "claude-3-opus-latest"
|
|
55
|
+
or selected_model == "claude-3-opus-20240229"
|
|
56
|
+
or selected_model == "claude-3-sonnet-20240229"
|
|
57
|
+
or selected_model == "claude-3-haiku-20240307"
|
|
40
58
|
):
|
|
41
59
|
return 200_000
|
|
42
60
|
return None
|
|
43
61
|
|
|
44
62
|
def api_max_response_tokens(self):
|
|
63
|
+
if self.model == "claude-opus-4-6":
|
|
64
|
+
return 128_000
|
|
65
|
+
|
|
45
66
|
if (
|
|
46
67
|
self.model == "claude-opus-4-5"
|
|
47
68
|
or self.model == "claude-sonnet-4-0"
|
|
@@ -61,7 +82,7 @@ class AnthropicMixin:
|
|
|
61
82
|
if (
|
|
62
83
|
self.model == "claude-3-5-sonnet-20240620"
|
|
63
84
|
or self.model == "claude-3-opus-latest"
|
|
64
|
-
or self.model == "
|
|
85
|
+
or self.model == "claude-3-opus-20240229"
|
|
65
86
|
or self.model == "claude-3-sonnet-20240229"
|
|
66
87
|
or self.model == "claude-3-haiku-20240307"
|
|
67
88
|
):
|
symai/components.py
CHANGED
|
@@ -1229,6 +1229,7 @@ class MetadataTracker(Expression):
|
|
|
1229
1229
|
and frame.f_code.co_name == "forward"
|
|
1230
1230
|
and "self" in frame.f_locals
|
|
1231
1231
|
and isinstance(frame.f_locals["self"], Engine)
|
|
1232
|
+
and arg is not None # Ensure arg is not None to avoid unpacking error on exceptions
|
|
1232
1233
|
):
|
|
1233
1234
|
_, metadata = arg # arg contains return value on 'return' event
|
|
1234
1235
|
engine_name = frame.f_locals["self"].__class__.__name__
|
|
@@ -1350,6 +1351,116 @@ class MetadataTracker(Expression):
|
|
|
1350
1351
|
token_details[(engine_name, model_name)]["completion_breakdown"][
|
|
1351
1352
|
"reasoning_tokens"
|
|
1352
1353
|
] += 0
|
|
1354
|
+
elif engine_name in ("ClaudeXChatEngine", "ClaudeXReasoningEngine"):
|
|
1355
|
+
raw_output = metadata["raw_output"]
|
|
1356
|
+
usage = self._extract_claude_usage(raw_output)
|
|
1357
|
+
if usage is None:
|
|
1358
|
+
# Skip if we can't extract usage (shouldn't happen normally)
|
|
1359
|
+
logger.warning(f"Could not extract usage from {engine_name} response.")
|
|
1360
|
+
token_details[(engine_name, model_name)]["usage"]["total_calls"] += 1
|
|
1361
|
+
token_details[(engine_name, model_name)]["prompt_breakdown"][
|
|
1362
|
+
"cached_tokens"
|
|
1363
|
+
] += 0
|
|
1364
|
+
token_details[(engine_name, model_name)]["completion_breakdown"][
|
|
1365
|
+
"reasoning_tokens"
|
|
1366
|
+
] += 0
|
|
1367
|
+
continue
|
|
1368
|
+
input_tokens = getattr(usage, "input_tokens", 0) or 0
|
|
1369
|
+
output_tokens = getattr(usage, "output_tokens", 0) or 0
|
|
1370
|
+
token_details[(engine_name, model_name)]["usage"]["prompt_tokens"] += (
|
|
1371
|
+
input_tokens
|
|
1372
|
+
)
|
|
1373
|
+
token_details[(engine_name, model_name)]["usage"]["completion_tokens"] += (
|
|
1374
|
+
output_tokens
|
|
1375
|
+
)
|
|
1376
|
+
# Calculate total tokens
|
|
1377
|
+
total = input_tokens + output_tokens
|
|
1378
|
+
token_details[(engine_name, model_name)]["usage"]["total_tokens"] += total
|
|
1379
|
+
token_details[(engine_name, model_name)]["usage"]["total_calls"] += 1
|
|
1380
|
+
# Track cache tokens if available
|
|
1381
|
+
cache_creation = getattr(usage, "cache_creation_input_tokens", 0) or 0
|
|
1382
|
+
cache_read = getattr(usage, "cache_read_input_tokens", 0) or 0
|
|
1383
|
+
token_details[(engine_name, model_name)]["prompt_breakdown"][
|
|
1384
|
+
"cache_creation_tokens"
|
|
1385
|
+
] += cache_creation
|
|
1386
|
+
token_details[(engine_name, model_name)]["prompt_breakdown"][
|
|
1387
|
+
"cache_read_tokens"
|
|
1388
|
+
] += cache_read
|
|
1389
|
+
# For backward compatibility, also track as cached_tokens
|
|
1390
|
+
token_details[(engine_name, model_name)]["prompt_breakdown"][
|
|
1391
|
+
"cached_tokens"
|
|
1392
|
+
] += cache_read
|
|
1393
|
+
# Track reasoning/thinking tokens for ClaudeXReasoningEngine
|
|
1394
|
+
if engine_name == "ClaudeXReasoningEngine":
|
|
1395
|
+
thinking_output = metadata.get("thinking", "")
|
|
1396
|
+
# Store thinking content if available
|
|
1397
|
+
if thinking_output:
|
|
1398
|
+
if "thinking_content" not in token_details[(engine_name, model_name)]:
|
|
1399
|
+
token_details[(engine_name, model_name)]["thinking_content"] = []
|
|
1400
|
+
token_details[(engine_name, model_name)]["thinking_content"].append(
|
|
1401
|
+
thinking_output
|
|
1402
|
+
)
|
|
1403
|
+
# Note: Anthropic doesn't break down reasoning tokens separately in usage,
|
|
1404
|
+
# but extended thinking is included in output_tokens
|
|
1405
|
+
token_details[(engine_name, model_name)]["completion_breakdown"][
|
|
1406
|
+
"reasoning_tokens"
|
|
1407
|
+
] += 0
|
|
1408
|
+
elif engine_name == "GeminiXReasoningEngine":
|
|
1409
|
+
usage = metadata["raw_output"].usage_metadata
|
|
1410
|
+
token_details[(engine_name, model_name)]["usage"]["prompt_tokens"] += (
|
|
1411
|
+
usage.prompt_token_count
|
|
1412
|
+
)
|
|
1413
|
+
token_details[(engine_name, model_name)]["usage"]["completion_tokens"] += (
|
|
1414
|
+
usage.candidates_token_count
|
|
1415
|
+
)
|
|
1416
|
+
token_details[(engine_name, model_name)]["usage"]["total_tokens"] += (
|
|
1417
|
+
usage.total_token_count
|
|
1418
|
+
)
|
|
1419
|
+
token_details[(engine_name, model_name)]["usage"]["total_calls"] += 1
|
|
1420
|
+
# Track cache tokens if available
|
|
1421
|
+
cache_read = getattr(usage, "cached_content_token_count", 0) or 0
|
|
1422
|
+
token_details[(engine_name, model_name)]["prompt_breakdown"][
|
|
1423
|
+
"cached_tokens"
|
|
1424
|
+
] += cache_read
|
|
1425
|
+
# Track thinking content if available
|
|
1426
|
+
thinking_output = metadata.get("thinking", "")
|
|
1427
|
+
if thinking_output:
|
|
1428
|
+
if "thinking_content" not in token_details[(engine_name, model_name)]:
|
|
1429
|
+
token_details[(engine_name, model_name)]["thinking_content"] = []
|
|
1430
|
+
token_details[(engine_name, model_name)]["thinking_content"].append(
|
|
1431
|
+
thinking_output
|
|
1432
|
+
)
|
|
1433
|
+
# Note: Gemini reasoning tokens are part of candidates_token_count
|
|
1434
|
+
token_details[(engine_name, model_name)]["completion_breakdown"][
|
|
1435
|
+
"reasoning_tokens"
|
|
1436
|
+
] += 0
|
|
1437
|
+
elif engine_name == "DeepSeekXReasoningEngine":
|
|
1438
|
+
usage = metadata["raw_output"].usage
|
|
1439
|
+
token_details[(engine_name, model_name)]["usage"]["completion_tokens"] += (
|
|
1440
|
+
usage.completion_tokens
|
|
1441
|
+
)
|
|
1442
|
+
token_details[(engine_name, model_name)]["usage"]["prompt_tokens"] += (
|
|
1443
|
+
usage.prompt_tokens
|
|
1444
|
+
)
|
|
1445
|
+
token_details[(engine_name, model_name)]["usage"]["total_tokens"] += (
|
|
1446
|
+
usage.total_tokens
|
|
1447
|
+
)
|
|
1448
|
+
token_details[(engine_name, model_name)]["usage"]["total_calls"] += 1
|
|
1449
|
+
# Track thinking content if available
|
|
1450
|
+
thinking_output = metadata.get("thinking", "")
|
|
1451
|
+
if thinking_output:
|
|
1452
|
+
if "thinking_content" not in token_details[(engine_name, model_name)]:
|
|
1453
|
+
token_details[(engine_name, model_name)]["thinking_content"] = []
|
|
1454
|
+
token_details[(engine_name, model_name)]["thinking_content"].append(
|
|
1455
|
+
thinking_output
|
|
1456
|
+
)
|
|
1457
|
+
# Note: DeepSeek reasoning tokens might be in completion_tokens_details
|
|
1458
|
+
reasoning_tokens = 0
|
|
1459
|
+
if hasattr(usage, "completion_tokens_details") and usage.completion_tokens_details:
|
|
1460
|
+
reasoning_tokens = getattr(usage.completion_tokens_details, "reasoning_tokens", 0) or 0
|
|
1461
|
+
token_details[(engine_name, model_name)]["completion_breakdown"][
|
|
1462
|
+
"reasoning_tokens"
|
|
1463
|
+
] += reasoning_tokens
|
|
1353
1464
|
else:
|
|
1354
1465
|
logger.warning(f"Tracking {engine_name} is not supported.")
|
|
1355
1466
|
continue
|
|
@@ -1361,8 +1472,60 @@ class MetadataTracker(Expression):
|
|
|
1361
1472
|
# Convert to normal dict
|
|
1362
1473
|
return {**token_details}
|
|
1363
1474
|
|
|
1475
|
+
def _extract_claude_usage(self, raw_output):
|
|
1476
|
+
"""Extract usage information from Claude response (handles both streaming and non-streaming).
|
|
1477
|
+
|
|
1478
|
+
For non-streaming responses, raw_output is a Message object with a .usage attribute.
|
|
1479
|
+
For streaming responses, raw_output is a list of stream events. Usage info is in:
|
|
1480
|
+
- RawMessageStartEvent.message.usage (input_tokens)
|
|
1481
|
+
- RawMessageDeltaEvent.usage (output_tokens)
|
|
1482
|
+
"""
|
|
1483
|
+
# Non-streaming: raw_output is a Message with .usage
|
|
1484
|
+
if hasattr(raw_output, "usage"):
|
|
1485
|
+
return raw_output.usage
|
|
1486
|
+
|
|
1487
|
+
# Streaming: raw_output is a list of events
|
|
1488
|
+
if isinstance(raw_output, list):
|
|
1489
|
+
# Accumulate usage from stream events
|
|
1490
|
+
input_tokens = 0
|
|
1491
|
+
output_tokens = 0
|
|
1492
|
+
cache_creation = 0
|
|
1493
|
+
cache_read = 0
|
|
1494
|
+
|
|
1495
|
+
for event in raw_output:
|
|
1496
|
+
# RawMessageStartEvent contains initial usage with input_tokens
|
|
1497
|
+
if hasattr(event, "message") and hasattr(event.message, "usage"):
|
|
1498
|
+
msg_usage = event.message.usage
|
|
1499
|
+
input_tokens += getattr(msg_usage, "input_tokens", 0) or 0
|
|
1500
|
+
cache_creation += getattr(msg_usage, "cache_creation_input_tokens", 0) or 0
|
|
1501
|
+
cache_read += getattr(msg_usage, "cache_read_input_tokens", 0) or 0
|
|
1502
|
+
# RawMessageDeltaEvent contains usage with output_tokens
|
|
1503
|
+
elif hasattr(event, "usage") and event.usage is not None:
|
|
1504
|
+
evt_usage = event.usage
|
|
1505
|
+
output_tokens += getattr(evt_usage, "output_tokens", 0) or 0
|
|
1506
|
+
|
|
1507
|
+
# Create a simple object-like dict to hold usage (using Box for attribute access)
|
|
1508
|
+
return Box({
|
|
1509
|
+
"input_tokens": input_tokens,
|
|
1510
|
+
"output_tokens": output_tokens,
|
|
1511
|
+
"cache_creation_input_tokens": cache_creation,
|
|
1512
|
+
"cache_read_input_tokens": cache_read,
|
|
1513
|
+
})
|
|
1514
|
+
|
|
1515
|
+
return None
|
|
1516
|
+
|
|
1364
1517
|
def _can_accumulate_engine(self, engine_name: str) -> bool:
|
|
1365
|
-
supported_engines = (
|
|
1518
|
+
supported_engines = (
|
|
1519
|
+
"GPTXChatEngine",
|
|
1520
|
+
"GPTXReasoningEngine",
|
|
1521
|
+
"GPTXSearchEngine",
|
|
1522
|
+
"ClaudeXChatEngine",
|
|
1523
|
+
"ClaudeXReasoningEngine",
|
|
1524
|
+
"GeminiXReasoningEngine",
|
|
1525
|
+
"DeepSeekXReasoningEngine",
|
|
1526
|
+
"GroqEngine",
|
|
1527
|
+
"CerebrasEngine",
|
|
1528
|
+
)
|
|
1366
1529
|
return engine_name in supported_engines
|
|
1367
1530
|
|
|
1368
1531
|
def _track_parallel_usage_items(self, token_details, engine_name, metadata):
|
|
@@ -1388,21 +1551,48 @@ class MetadataTracker(Expression):
|
|
|
1388
1551
|
|
|
1389
1552
|
metadata_raw_output = metadata["raw_output"]
|
|
1390
1553
|
accumulated_raw_output = accumulated["raw_output"]
|
|
1391
|
-
if not hasattr(metadata_raw_output, "usage") or not hasattr(
|
|
1392
|
-
accumulated_raw_output, "usage"
|
|
1393
|
-
):
|
|
1394
|
-
return
|
|
1395
1554
|
|
|
1396
|
-
|
|
1397
|
-
|
|
1555
|
+
# Handle both OpenAI/Anthropic-style (usage) and Gemini-style (usage_metadata)
|
|
1556
|
+
current_usage = getattr(metadata_raw_output, "usage", None) or getattr(
|
|
1557
|
+
metadata_raw_output, "usage_metadata", None
|
|
1558
|
+
)
|
|
1559
|
+
accumulated_usage = getattr(accumulated_raw_output, "usage", None) or getattr(
|
|
1560
|
+
accumulated_raw_output, "usage_metadata", None
|
|
1561
|
+
)
|
|
1398
1562
|
|
|
1399
|
-
|
|
1563
|
+
if not current_usage or not accumulated_usage:
|
|
1564
|
+
return
|
|
1565
|
+
|
|
1566
|
+
# Handle both OpenAI-style (completion_tokens, prompt_tokens),
|
|
1567
|
+
# Anthropic-style (output_tokens, input_tokens),
|
|
1568
|
+
# and Gemini-style (candidates_token_count, prompt_token_count) fields
|
|
1569
|
+
token_attrs = [
|
|
1570
|
+
"completion_tokens",
|
|
1571
|
+
"prompt_tokens",
|
|
1572
|
+
"total_tokens",
|
|
1573
|
+
"input_tokens",
|
|
1574
|
+
"output_tokens",
|
|
1575
|
+
"candidates_token_count",
|
|
1576
|
+
"prompt_token_count",
|
|
1577
|
+
"total_token_count",
|
|
1578
|
+
]
|
|
1579
|
+
for attr in token_attrs:
|
|
1400
1580
|
if hasattr(current_usage, attr) and hasattr(accumulated_usage, attr):
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1581
|
+
current_val = getattr(current_usage, attr) or 0
|
|
1582
|
+
accumulated_val = getattr(accumulated_usage, attr) or 0
|
|
1583
|
+
setattr(accumulated_usage, attr, accumulated_val + current_val)
|
|
1584
|
+
|
|
1585
|
+
# Handle Anthropic cache tokens and Gemini cached tokens
|
|
1586
|
+
cache_attrs = [
|
|
1587
|
+
"cache_creation_input_tokens",
|
|
1588
|
+
"cache_read_input_tokens",
|
|
1589
|
+
"cached_content_token_count",
|
|
1590
|
+
]
|
|
1591
|
+
for attr in cache_attrs:
|
|
1592
|
+
if hasattr(current_usage, attr) and hasattr(accumulated_usage, attr):
|
|
1593
|
+
current_val = getattr(current_usage, attr) or 0
|
|
1594
|
+
accumulated_val = getattr(accumulated_usage, attr) or 0
|
|
1595
|
+
setattr(accumulated_usage, attr, accumulated_val + current_val)
|
|
1406
1596
|
|
|
1407
1597
|
for detail_attr in ["completion_tokens_details", "prompt_tokens_details"]:
|
|
1408
1598
|
if not hasattr(current_usage, detail_attr) or not hasattr(
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from ... import core
|
|
2
|
+
from ...backend.engines.drawing.engine_gemini_image import GeminiImageResult
|
|
3
|
+
from ...symbol import Expression
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class nanobanana(Expression):
|
|
7
|
+
def __init__(self, *args, **kwargs):
|
|
8
|
+
super().__init__(*args, **kwargs)
|
|
9
|
+
self.name = self.__class__.__name__
|
|
10
|
+
|
|
11
|
+
def __call__(
|
|
12
|
+
self,
|
|
13
|
+
prompt: str | None = None,
|
|
14
|
+
operation: str = "create",
|
|
15
|
+
**kwargs,
|
|
16
|
+
) -> list:
|
|
17
|
+
prompt = self._to_symbol(prompt)
|
|
18
|
+
|
|
19
|
+
@core.draw(operation=operation, **kwargs)
|
|
20
|
+
def _func(_) -> GeminiImageResult:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
return _func(prompt).value
|