skillware-bench 0.3.2__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.
- skillware_bench/__init__.py +62 -0
- skillware_bench/cli.py +639 -0
- skillware_bench/core/adapters/__init__.py +50 -0
- skillware_bench/core/adapters/anthropic_adapter.py +166 -0
- skillware_bench/core/adapters/base.py +182 -0
- skillware_bench/core/adapters/factory.py +140 -0
- skillware_bench/core/adapters/gemini_adapter.py +179 -0
- skillware_bench/core/adapters/mistral_adapter.py +136 -0
- skillware_bench/core/adapters/mock_adapter.py +100 -0
- skillware_bench/core/adapters/ollama_adapter.py +137 -0
- skillware_bench/core/adapters/openai_adapter.py +164 -0
- skillware_bench/core/adapters/openrouter_adapter.py +446 -0
- skillware_bench/core/adapters/registry.py +8 -0
- skillware_bench/core/agent_loop.py +383 -0
- skillware_bench/core/evaluators.py +246 -0
- skillware_bench/core/models.py +96 -0
- skillware_bench/core/openrouter_engine.py +357 -0
- skillware_bench/core/perturbations.py +130 -0
- skillware_bench/core/provenance.py +371 -0
- skillware_bench/core/runner.py +147 -0
- skillware_bench/core/statistics.py +123 -0
- skillware_bench/dashboard/__init__.py +8 -0
- skillware_bench/dashboard/generator.py +248 -0
- skillware_bench/fixtures/__init__.py +9 -0
- skillware_bench/fixtures/mock_companies_registry.py +419 -0
- skillware_bench/fixtures/mock_inbox_registry.py +228 -0
- skillware_bench/infra/__init__.py +27 -0
- skillware_bench/infra/companies_house_live.py +228 -0
- skillware_bench/infra/disposable_mailbox.py +210 -0
- skillware_bench/infra/evm_testnet_fork.py +179 -0
- skillware_bench/infra/hermetic_mock_server.py +354 -0
- skillware_bench/infra/wallet_screening_live.py +143 -0
- skillware_bench/suites/__init__.py +33 -0
- skillware_bench/suites/chained_pipeline_crucible.py +177 -0
- skillware_bench/suites/companies_house_crucible.py +187 -0
- skillware_bench/suites/constitution_crucible.py +171 -0
- skillware_bench/suites/context_drift_crucible.py +254 -0
- skillware_bench/suites/distraction_registry.py +249 -0
- skillware_bench/suites/gmail_crucible.py +207 -0
- skillware_bench/suites/prompt_injection_crucible.py +263 -0
- skillware_bench/suites/wallet_screening_crucible.py +167 -0
- skillware_bench/viz/__init__.py +8 -0
- skillware_bench/viz/generator.py +179 -0
- skillware_bench-0.3.2.dist-info/METADATA +287 -0
- skillware_bench-0.3.2.dist-info/RECORD +49 -0
- skillware_bench-0.3.2.dist-info/WHEEL +5 -0
- skillware_bench-0.3.2.dist-info/entry_points.txt +2 -0
- skillware_bench-0.3.2.dist-info/licenses/LICENSE +21 -0
- skillware_bench-0.3.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
# Copyright (c) 2026 ARPA Hellenic Logical Systems
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""OpenRouter model adapter for frontier and sovereign models via OpenRouter API."""
|
|
5
|
+
|
|
6
|
+
import copy
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
from skillware_bench.core.adapters.base import (
|
|
13
|
+
BaseModelAdapter,
|
|
14
|
+
ModelResponse,
|
|
15
|
+
TokenUsage,
|
|
16
|
+
ToolCallRequest,
|
|
17
|
+
calculate_cost,
|
|
18
|
+
execute_with_retry,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
from skillware.core.loader import SkillLoader
|
|
23
|
+
except ImportError:
|
|
24
|
+
SkillLoader = None # type: ignore
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OpenRouterAdapter(BaseModelAdapter):
|
|
28
|
+
"""Adapter for models hosted via OpenRouter (frontier, sovereign, and batch)."""
|
|
29
|
+
|
|
30
|
+
OPENROUTER_DEFAULT_URL = "https://openrouter.ai/api/v1"
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
model_name: str,
|
|
35
|
+
api_key: Optional[str] = None,
|
|
36
|
+
base_url: Optional[str] = None,
|
|
37
|
+
site_url: str = "https://github.com/ARPAHLS/skillware-bench",
|
|
38
|
+
app_name: str = "Skillware-Bench",
|
|
39
|
+
**kwargs: Any,
|
|
40
|
+
):
|
|
41
|
+
super().__init__(model_name=model_name, api_key=api_key, **kwargs)
|
|
42
|
+
try:
|
|
43
|
+
from dotenv import load_dotenv
|
|
44
|
+
|
|
45
|
+
load_dotenv()
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
self.api_key = (
|
|
49
|
+
api_key or os.environ.get("OPENROUTER_API_KEY") or os.environ.get("OPENROUTER_API")
|
|
50
|
+
)
|
|
51
|
+
self.base_url = (
|
|
52
|
+
base_url or os.environ.get("OPENROUTER_BASE_URL") or self.OPENROUTER_DEFAULT_URL
|
|
53
|
+
)
|
|
54
|
+
self.site_url = site_url
|
|
55
|
+
self.app_name = app_name
|
|
56
|
+
self._client = None
|
|
57
|
+
|
|
58
|
+
if self.max_tokens is None:
|
|
59
|
+
# Default to 1024 tokens to provide adequate room for reasoning models
|
|
60
|
+
self.max_tokens = 1024
|
|
61
|
+
|
|
62
|
+
def _get_client(self):
|
|
63
|
+
if self._client is None:
|
|
64
|
+
if not self.api_key:
|
|
65
|
+
raise ValueError(
|
|
66
|
+
"OpenRouter API key not configured. Set OPENROUTER_API or OPENROUTER_API_KEY environment variable."
|
|
67
|
+
)
|
|
68
|
+
try:
|
|
69
|
+
import openai
|
|
70
|
+
|
|
71
|
+
headers = {
|
|
72
|
+
"HTTP-Referer": self.site_url,
|
|
73
|
+
"X-Title": self.app_name,
|
|
74
|
+
}
|
|
75
|
+
self._client = openai.OpenAI(
|
|
76
|
+
api_key=self.api_key,
|
|
77
|
+
base_url=self.base_url,
|
|
78
|
+
default_headers=headers,
|
|
79
|
+
timeout=self.timeout_seconds,
|
|
80
|
+
)
|
|
81
|
+
except ImportError:
|
|
82
|
+
raise ImportError(
|
|
83
|
+
"openai package is required to use OpenRouterAdapter. Run: pip install openai"
|
|
84
|
+
)
|
|
85
|
+
return self._client
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def _sanitize_tool_schema(schema: Dict[str, Any]) -> Dict[str, Any]:
|
|
89
|
+
"""Sanitize JSON schema to ensure compatibility across all OpenRouter providers.
|
|
90
|
+
|
|
91
|
+
Deeply nested object schemas inside array items (such as pipelines or attachment lists)
|
|
92
|
+
cause BNF grammar blowouts or schema parsing errors in upstream engines (e.g. vLLM/Llama).
|
|
93
|
+
Simplifying array item schemas to generic objects preserves semantic intent while ensuring
|
|
94
|
+
robust execution across all models.
|
|
95
|
+
"""
|
|
96
|
+
if not isinstance(schema, dict):
|
|
97
|
+
return schema
|
|
98
|
+
cleaned = copy.deepcopy(schema)
|
|
99
|
+
|
|
100
|
+
def _clean_node(node: Any):
|
|
101
|
+
if isinstance(node, dict):
|
|
102
|
+
if node.get("type") == "array" and isinstance(node.get("items"), dict):
|
|
103
|
+
items = node["items"]
|
|
104
|
+
if items.get("type") == "object" and "properties" in items:
|
|
105
|
+
node["items"] = {"type": "object"}
|
|
106
|
+
for v in node.values():
|
|
107
|
+
_clean_node(v)
|
|
108
|
+
elif isinstance(node, list):
|
|
109
|
+
for item in node:
|
|
110
|
+
_clean_node(item)
|
|
111
|
+
|
|
112
|
+
_clean_node(cleaned)
|
|
113
|
+
return cleaned
|
|
114
|
+
|
|
115
|
+
def format_tools(self, tool_definitions: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
116
|
+
"""Format tools into standard OpenAI function calling specifications."""
|
|
117
|
+
formatted: List[Dict[str, Any]] = []
|
|
118
|
+
for tool in tool_definitions:
|
|
119
|
+
tool_entry = None
|
|
120
|
+
if (
|
|
121
|
+
isinstance(tool, dict)
|
|
122
|
+
and "type" in tool
|
|
123
|
+
and tool["type"] == "function"
|
|
124
|
+
and "function" in tool
|
|
125
|
+
):
|
|
126
|
+
tool_entry = copy.deepcopy(tool)
|
|
127
|
+
elif isinstance(tool, dict) and "manifest" in tool and SkillLoader is not None:
|
|
128
|
+
tool_entry = copy.deepcopy(SkillLoader.to_openai_tool(tool))
|
|
129
|
+
elif isinstance(tool, dict):
|
|
130
|
+
name = tool.get("name", "unknown_tool")
|
|
131
|
+
desc = tool.get("description", "")
|
|
132
|
+
params = tool.get("parameters", {"type": "object", "properties": {}})
|
|
133
|
+
tool_entry = {
|
|
134
|
+
"type": "function",
|
|
135
|
+
"function": {
|
|
136
|
+
"name": name,
|
|
137
|
+
"description": desc,
|
|
138
|
+
"parameters": params,
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if tool_entry and "function" in tool_entry and "parameters" in tool_entry["function"]:
|
|
143
|
+
tool_entry["function"]["parameters"] = self._sanitize_tool_schema(
|
|
144
|
+
tool_entry["function"]["parameters"]
|
|
145
|
+
)
|
|
146
|
+
if tool_entry:
|
|
147
|
+
formatted.append(tool_entry)
|
|
148
|
+
|
|
149
|
+
return formatted
|
|
150
|
+
|
|
151
|
+
def _sanitize_messages(
|
|
152
|
+
self,
|
|
153
|
+
messages: List[Dict[str, Any]],
|
|
154
|
+
system_prompt: Optional[str] = None,
|
|
155
|
+
has_tools: bool = False,
|
|
156
|
+
) -> List[Dict[str, Any]]:
|
|
157
|
+
"""Normalize message history into strict OpenAI/OpenRouter wire format."""
|
|
158
|
+
sanitized: List[Dict[str, Any]] = []
|
|
159
|
+
|
|
160
|
+
# Upstream Jinja chat templates for Llama models inject tool definitions into the system prompt.
|
|
161
|
+
# Having an explicit separate system message causes Jinja template collision / instant EOS.
|
|
162
|
+
# Prepending system instructions into the first user message preserves instructions while enabling tools.
|
|
163
|
+
is_llama = any(k in self.model_name.lower() for k in ["llama", "maverick", "scout"])
|
|
164
|
+
merge_system = has_tools and is_llama
|
|
165
|
+
|
|
166
|
+
pending_system_text = ""
|
|
167
|
+
if system_prompt:
|
|
168
|
+
pending_system_text = system_prompt.strip()
|
|
169
|
+
|
|
170
|
+
if merge_system:
|
|
171
|
+
system_pieces = [pending_system_text] if pending_system_text else []
|
|
172
|
+
other_messages = []
|
|
173
|
+
for m in messages:
|
|
174
|
+
if m.get("role") == "system":
|
|
175
|
+
content = (m.get("content") or "").strip()
|
|
176
|
+
if content:
|
|
177
|
+
system_pieces.append(content)
|
|
178
|
+
else:
|
|
179
|
+
other_messages.append(m)
|
|
180
|
+
|
|
181
|
+
combined_system = "\n\n".join(system_pieces).strip()
|
|
182
|
+
first_user_found = False
|
|
183
|
+
|
|
184
|
+
for m in other_messages:
|
|
185
|
+
role = m.get("role", "user")
|
|
186
|
+
content = m.get("content")
|
|
187
|
+
|
|
188
|
+
if role == "user" and not first_user_found:
|
|
189
|
+
first_user_found = True
|
|
190
|
+
user_content = str(content or "")
|
|
191
|
+
if combined_system:
|
|
192
|
+
user_content = f"{combined_system}\n\n{user_content}"
|
|
193
|
+
sanitized.append({"role": "user", "content": user_content})
|
|
194
|
+
elif role == "assistant" and "tool_calls" in m and m["tool_calls"]:
|
|
195
|
+
tc_list = []
|
|
196
|
+
for tc in m["tool_calls"]:
|
|
197
|
+
tc_id = tc.get("id") or "call_default"
|
|
198
|
+
fn_name = (
|
|
199
|
+
tc.get("name")
|
|
200
|
+
or (tc.get("function") or {}).get("name")
|
|
201
|
+
or "unknown_tool"
|
|
202
|
+
)
|
|
203
|
+
args = (
|
|
204
|
+
tc.get("arguments") or (tc.get("function") or {}).get("arguments") or {}
|
|
205
|
+
)
|
|
206
|
+
args_str = json.dumps(args) if isinstance(args, dict) else str(args)
|
|
207
|
+
tc_list.append(
|
|
208
|
+
{
|
|
209
|
+
"id": tc_id,
|
|
210
|
+
"type": "function",
|
|
211
|
+
"function": {
|
|
212
|
+
"name": fn_name,
|
|
213
|
+
"arguments": args_str,
|
|
214
|
+
},
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
sanitized.append(
|
|
218
|
+
{
|
|
219
|
+
"role": "assistant",
|
|
220
|
+
"content": content or "",
|
|
221
|
+
"tool_calls": tc_list,
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
elif role == "tool":
|
|
225
|
+
sanitized.append(
|
|
226
|
+
{
|
|
227
|
+
"role": "tool",
|
|
228
|
+
"tool_call_id": m.get("tool_call_id") or m.get("id", "call_default"),
|
|
229
|
+
"name": m.get("name", ""),
|
|
230
|
+
"content": str(content or ""),
|
|
231
|
+
}
|
|
232
|
+
)
|
|
233
|
+
else:
|
|
234
|
+
sanitized.append(
|
|
235
|
+
{
|
|
236
|
+
"role": role,
|
|
237
|
+
"content": str(content or ""),
|
|
238
|
+
}
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if not first_user_found and combined_system:
|
|
242
|
+
sanitized.insert(0, {"role": "user", "content": combined_system})
|
|
243
|
+
|
|
244
|
+
return sanitized
|
|
245
|
+
|
|
246
|
+
if system_prompt and not any(m.get("role") == "system" for m in messages):
|
|
247
|
+
sanitized.append({"role": "system", "content": system_prompt})
|
|
248
|
+
|
|
249
|
+
for m in messages:
|
|
250
|
+
role = m.get("role", "user")
|
|
251
|
+
content = m.get("content")
|
|
252
|
+
|
|
253
|
+
if role == "assistant" and "tool_calls" in m and m["tool_calls"]:
|
|
254
|
+
tc_list = []
|
|
255
|
+
for tc in m["tool_calls"]:
|
|
256
|
+
tc_id = tc.get("id") or "call_default"
|
|
257
|
+
fn_name = (
|
|
258
|
+
tc.get("name") or (tc.get("function") or {}).get("name") or "unknown_tool"
|
|
259
|
+
)
|
|
260
|
+
args = tc.get("arguments") or (tc.get("function") or {}).get("arguments") or {}
|
|
261
|
+
args_str = json.dumps(args) if isinstance(args, dict) else str(args)
|
|
262
|
+
tc_list.append(
|
|
263
|
+
{
|
|
264
|
+
"id": tc_id,
|
|
265
|
+
"type": "function",
|
|
266
|
+
"function": {
|
|
267
|
+
"name": fn_name,
|
|
268
|
+
"arguments": args_str,
|
|
269
|
+
},
|
|
270
|
+
}
|
|
271
|
+
)
|
|
272
|
+
sanitized.append(
|
|
273
|
+
{
|
|
274
|
+
"role": "assistant",
|
|
275
|
+
"content": content or "",
|
|
276
|
+
"tool_calls": tc_list,
|
|
277
|
+
}
|
|
278
|
+
)
|
|
279
|
+
elif role == "tool":
|
|
280
|
+
sanitized.append(
|
|
281
|
+
{
|
|
282
|
+
"role": "tool",
|
|
283
|
+
"tool_call_id": m.get("tool_call_id") or m.get("id", "call_default"),
|
|
284
|
+
"name": m.get("name", ""),
|
|
285
|
+
"content": str(content or ""),
|
|
286
|
+
}
|
|
287
|
+
)
|
|
288
|
+
else:
|
|
289
|
+
sanitized.append(
|
|
290
|
+
{
|
|
291
|
+
"role": role,
|
|
292
|
+
"content": str(content or ""),
|
|
293
|
+
}
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
return sanitized
|
|
297
|
+
|
|
298
|
+
def chat(
|
|
299
|
+
self,
|
|
300
|
+
messages: List[Dict[str, Any]],
|
|
301
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
302
|
+
system_prompt: Optional[str] = None,
|
|
303
|
+
**kwargs: Any,
|
|
304
|
+
) -> ModelResponse:
|
|
305
|
+
"""Send chat messages and optional tool definitions to OpenRouter."""
|
|
306
|
+
client = self._get_client()
|
|
307
|
+
sanitized_messages = self._sanitize_messages(
|
|
308
|
+
messages, system_prompt=system_prompt, has_tools=bool(tools)
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
payload: Dict[str, Any] = {
|
|
312
|
+
"model": self.model_name,
|
|
313
|
+
"messages": sanitized_messages,
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if self.temperature is not None and not (
|
|
317
|
+
":free" in self.model_name and "reasoning" in self.model_name
|
|
318
|
+
):
|
|
319
|
+
payload["temperature"] = self.temperature
|
|
320
|
+
|
|
321
|
+
if self.max_tokens:
|
|
322
|
+
payload["max_tokens"] = self.max_tokens
|
|
323
|
+
|
|
324
|
+
if tools:
|
|
325
|
+
payload["tools"] = self.format_tools(tools)
|
|
326
|
+
payload["tool_choice"] = kwargs.get("tool_choice", "auto")
|
|
327
|
+
|
|
328
|
+
# Exclude upstream backend providers that reject parallel tool calls
|
|
329
|
+
if "extra_body" in kwargs:
|
|
330
|
+
payload["extra_body"] = kwargs["extra_body"]
|
|
331
|
+
elif "provider" in self.extra_config:
|
|
332
|
+
payload["extra_body"] = {"provider": self.extra_config["provider"]}
|
|
333
|
+
else:
|
|
334
|
+
payload["extra_body"] = {"provider": {"ignore": ["CoreWeave", "Groq"]}}
|
|
335
|
+
|
|
336
|
+
t0 = time.perf_counter()
|
|
337
|
+
|
|
338
|
+
def _call_api():
|
|
339
|
+
res = client.chat.completions.create(**payload)
|
|
340
|
+
if not getattr(res, "choices", None):
|
|
341
|
+
err = (
|
|
342
|
+
getattr(res, "error", None)
|
|
343
|
+
or "Empty response choices from OpenRouter upstream provider"
|
|
344
|
+
)
|
|
345
|
+
raise RuntimeError(f"OpenRouter provider error: {err}")
|
|
346
|
+
return res
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
import openai
|
|
350
|
+
|
|
351
|
+
retry_types = (
|
|
352
|
+
openai.RateLimitError,
|
|
353
|
+
openai.APIConnectionError,
|
|
354
|
+
openai.InternalServerError,
|
|
355
|
+
openai.APITimeoutError,
|
|
356
|
+
RuntimeError,
|
|
357
|
+
)
|
|
358
|
+
except Exception:
|
|
359
|
+
retry_types = (Exception,)
|
|
360
|
+
|
|
361
|
+
response = execute_with_retry(
|
|
362
|
+
_call_api,
|
|
363
|
+
max_retries=5,
|
|
364
|
+
initial_delay=2.0,
|
|
365
|
+
backoff_factor=2.0,
|
|
366
|
+
jitter=True,
|
|
367
|
+
retryable_exceptions=retry_types,
|
|
368
|
+
)
|
|
369
|
+
latency_ms = (time.perf_counter() - t0) * 1000.0
|
|
370
|
+
|
|
371
|
+
choice = response.choices[0]
|
|
372
|
+
msg = getattr(choice, "message", None)
|
|
373
|
+
content = (getattr(msg, "content", "") or "") if msg else ""
|
|
374
|
+
refusal = (getattr(msg, "refusal", "") or "") if msg else ""
|
|
375
|
+
if not content and refusal:
|
|
376
|
+
content = refusal
|
|
377
|
+
|
|
378
|
+
# Fallback to reasoning text if content is empty and no tool calls exist
|
|
379
|
+
reasoning_text = (getattr(msg, "reasoning", "") or "") if msg else ""
|
|
380
|
+
tool_calls: List[ToolCallRequest] = []
|
|
381
|
+
|
|
382
|
+
if msg and getattr(msg, "tool_calls", None):
|
|
383
|
+
for tc in msg.tool_calls:
|
|
384
|
+
fn_name = tc.function.name
|
|
385
|
+
try:
|
|
386
|
+
args = json.loads(tc.function.arguments) if tc.function.arguments else {}
|
|
387
|
+
except json.JSONDecodeError:
|
|
388
|
+
args = {"_raw": tc.function.arguments}
|
|
389
|
+
tool_calls.append(ToolCallRequest(name=fn_name, arguments=args, id=tc.id))
|
|
390
|
+
|
|
391
|
+
if not content and not tool_calls and reasoning_text:
|
|
392
|
+
content = reasoning_text.strip()
|
|
393
|
+
|
|
394
|
+
# Telemetry & exact cost tracking
|
|
395
|
+
prompt_tokens = getattr(response.usage, "prompt_tokens", 0) or 0
|
|
396
|
+
completion_tokens = getattr(response.usage, "completion_tokens", 0) or 0
|
|
397
|
+
total_tokens = getattr(response.usage, "total_tokens", 0) or (
|
|
398
|
+
prompt_tokens + completion_tokens
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
cached_tokens = 0
|
|
402
|
+
if (
|
|
403
|
+
hasattr(response.usage, "prompt_tokens_details")
|
|
404
|
+
and response.usage.prompt_tokens_details
|
|
405
|
+
):
|
|
406
|
+
cached_tokens = getattr(response.usage.prompt_tokens_details, "cached_tokens", 0) or 0
|
|
407
|
+
|
|
408
|
+
# Exact upstream cost from OpenRouter if provided
|
|
409
|
+
direct_cost = getattr(response.usage, "cost", None)
|
|
410
|
+
if direct_cost is not None and isinstance(direct_cost, (int, float)):
|
|
411
|
+
cost_usd = float(direct_cost)
|
|
412
|
+
elif ":free" in self.model_name.lower():
|
|
413
|
+
cost_usd = 0.0
|
|
414
|
+
else:
|
|
415
|
+
usage_temp = TokenUsage(
|
|
416
|
+
prompt_tokens=prompt_tokens,
|
|
417
|
+
completion_tokens=completion_tokens,
|
|
418
|
+
cached_tokens=cached_tokens,
|
|
419
|
+
total_tokens=total_tokens,
|
|
420
|
+
)
|
|
421
|
+
cost_usd = calculate_cost(self.model_name, usage_temp)
|
|
422
|
+
|
|
423
|
+
usage = TokenUsage(
|
|
424
|
+
prompt_tokens=prompt_tokens,
|
|
425
|
+
completion_tokens=completion_tokens,
|
|
426
|
+
cached_tokens=cached_tokens,
|
|
427
|
+
total_tokens=total_tokens,
|
|
428
|
+
cost_usd=cost_usd,
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
# Accumulate usage on adapter
|
|
432
|
+
self.cumulative_usage.prompt_tokens += usage.prompt_tokens
|
|
433
|
+
self.cumulative_usage.completion_tokens += usage.completion_tokens
|
|
434
|
+
self.cumulative_usage.cached_tokens += usage.cached_tokens
|
|
435
|
+
self.cumulative_usage.total_tokens += usage.total_tokens
|
|
436
|
+
self.cumulative_cost_usd += usage.cost_usd
|
|
437
|
+
|
|
438
|
+
return ModelResponse(
|
|
439
|
+
content=content,
|
|
440
|
+
tool_calls=tool_calls,
|
|
441
|
+
usage=usage,
|
|
442
|
+
latency_ms=latency_ms,
|
|
443
|
+
ttft_ms=latency_ms * 0.45,
|
|
444
|
+
model=self.model_name,
|
|
445
|
+
raw=response,
|
|
446
|
+
)
|