auditkit 1.0.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.
Files changed (81) hide show
  1. auditkit/README.md +99 -0
  2. auditkit/__init__.py +177 -0
  3. auditkit/__main__.py +3 -0
  4. auditkit/_bootstrap.py +77 -0
  5. auditkit/_identity_guard.py +99 -0
  6. auditkit/adapter.py +264 -0
  7. auditkit/annotator.py +339 -0
  8. auditkit/api.py +502 -0
  9. auditkit/assets/auditkit_logo.png +0 -0
  10. auditkit/cache.py +47 -0
  11. auditkit/cli.py +417 -0
  12. auditkit/comparison.py +563 -0
  13. auditkit/diff.py +265 -0
  14. auditkit/errors.py +54 -0
  15. auditkit/evaluator.py +20 -0
  16. auditkit/experiment.py +145 -0
  17. auditkit/hf_publish.py +262 -0
  18. auditkit/lmeval_engine.py +550 -0
  19. auditkit/loaders.py +121 -0
  20. auditkit/logs.py +18 -0
  21. auditkit/metric.py +199 -0
  22. auditkit/metrics/README.md +15 -0
  23. auditkit/metrics/__init__.py +0 -0
  24. auditkit/metrics/code.py +222 -0
  25. auditkit/metrics/embedding.py +131 -0
  26. auditkit/metrics/encoder_judge.py +423 -0
  27. auditkit/metrics/generation.py +331 -0
  28. auditkit/metrics/guard.py +412 -0
  29. auditkit/metrics/hallucination.py +45 -0
  30. auditkit/metrics/judge.py +547 -0
  31. auditkit/metrics/pairwise.py +153 -0
  32. auditkit/metrics/perf.py +53 -0
  33. auditkit/metrics/rag.py +149 -0
  34. auditkit/metrics/security.py +64 -0
  35. auditkit/metrics/toxicity.py +238 -0
  36. auditkit/model/README.md +16 -0
  37. auditkit/model/__init__.py +485 -0
  38. auditkit/model/anthropic.py +94 -0
  39. auditkit/model/api_gen.py +133 -0
  40. auditkit/model/groq_gen.py +121 -0
  41. auditkit/model/hf_gen.py +385 -0
  42. auditkit/model/lexsi.py +155 -0
  43. auditkit/model/litellm_gen.py +65 -0
  44. auditkit/model/openai.py +90 -0
  45. auditkit/model/openrouter_gen.py +152 -0
  46. auditkit/model/vllm_gen.py +316 -0
  47. auditkit/model_compare.py +655 -0
  48. auditkit/redteam/README.md +9 -0
  49. auditkit/redteam/__init__.py +26 -0
  50. auditkit/redteam/detector.py +37 -0
  51. auditkit/redteam/detectors/README.md +5 -0
  52. auditkit/redteam/detectors/builtin.py +126 -0
  53. auditkit/redteam/probe.py +39 -0
  54. auditkit/redteam/probes/README.md +5 -0
  55. auditkit/redteam/probes/builtin.py +85 -0
  56. auditkit/redteam/runner.py +206 -0
  57. auditkit/registry.py +65 -0
  58. auditkit/report.py +278 -0
  59. auditkit/report_format.py +52 -0
  60. auditkit/router.py +54 -0
  61. auditkit/runner.py +575 -0
  62. auditkit/runspec.py +159 -0
  63. auditkit/sample.py +40 -0
  64. auditkit/scenario.py +88 -0
  65. auditkit/scenarios/README.md +10 -0
  66. auditkit/scenarios/__init__.py +4 -0
  67. auditkit/scenarios/arc.py +33 -0
  68. auditkit/scenarios/gsm8k.py +32 -0
  69. auditkit/scenarios/hellaswag.py +33 -0
  70. auditkit/scenarios/humaneval.py +32 -0
  71. auditkit/scenarios/mmlu.py +34 -0
  72. auditkit/scenarios/truthfulqa.py +33 -0
  73. auditkit/score.py +165 -0
  74. auditkit/scorers.py +117 -0
  75. auditkit/scoring.py +79 -0
  76. auditkit/types.py +69 -0
  77. auditkit-1.0.0.dist-info/METADATA +396 -0
  78. auditkit-1.0.0.dist-info/RECORD +81 -0
  79. auditkit-1.0.0.dist-info/WHEEL +4 -0
  80. auditkit-1.0.0.dist-info/entry_points.txt +2 -0
  81. auditkit-1.0.0.dist-info/licenses/LICENSE.md +92 -0
@@ -0,0 +1,155 @@
1
+ """Lexsi platform model backend with optional MLflow auto-logging."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ from typing import Any
6
+
7
+ from . import (
8
+ Model, Request, Result_, Generated, resolve_params, DEFAULT_TEMPERATURE,
9
+ reject_generation_kwargs, split_session_kwargs,
10
+ )
11
+ from ..errors import ExtraNotInstalled
12
+
13
+ # The Lexsi platform's real accepted parameter set beyond temperature/
14
+ # max_tokens isn't documented in this codebase -- kept deliberately
15
+ # conservative (only what was already hardcoded here) rather than guessing
16
+ # at unverified field names for a proprietary API.
17
+ _KEY_MAP = {
18
+ "temperature": "temperature",
19
+ "max_tokens": "max_tokens",
20
+ }
21
+
22
+
23
+ class LexsiModel(Model):
24
+ """Call the Lexsi platform API with optional MLflow tracking.
25
+
26
+ Parameters
27
+ ----------
28
+ model
29
+ Model name on the Lexsi platform.
30
+ api_key
31
+ Lexsi API key. Falls back to ``LEXSI_API_KEY`` env var.
32
+ api_base
33
+ Base URL for the Lexsi API.
34
+ mlflow_tracking_uri
35
+ If set, auto-log every run to MLflow.
36
+ mlflow_experiment
37
+ MLflow experiment name (defaults to "auditkit-lexsi").
38
+ """
39
+
40
+ name = "lexsi"
41
+
42
+ def __init__(
43
+ self,
44
+ model: str = "lexsi-3.5",
45
+ api_key: str | None = None,
46
+ api_base: str = "https://api.lexsi.ai/v1",
47
+ mlflow_tracking_uri: str | None = None,
48
+ mlflow_experiment: str | None = None,
49
+ name: str = "lexsi",
50
+ **kwargs: Any,
51
+ ) -> None:
52
+ reject_generation_kwargs(kwargs, "LexsiModel")
53
+ self._model_name = model
54
+ self._api_key = api_key or os.environ.get("LEXSI_API_KEY")
55
+ self._api_base = api_base.rstrip("/")
56
+ self._mlflow_tracking_uri = mlflow_tracking_uri
57
+ self._mlflow_experiment = mlflow_experiment or "auditkit-lexsi"
58
+ # verify=/proxies=/cert=/timeout= are real requests.Session concerns,
59
+ # not API request-body fields -- split them out so e.g.
60
+ # LexsiModel(verify=False) actually disables TLS verification
61
+ # instead of silently becoming an extra, ignored JSON field.
62
+ self._session_kwargs, self._extra_kwargs = split_session_kwargs(kwargs)
63
+ self._session = None
64
+ self.name = name
65
+ self._total_requests = 0
66
+ self._total_latency_ms = 0.0
67
+
68
+ def _ensure_session(self) -> None:
69
+ if self._session is not None:
70
+ return
71
+ try:
72
+ import requests
73
+ except ImportError:
74
+ raise ExtraNotInstalled("requests", "pip install requests")
75
+ self._session = requests.Session()
76
+ self._session.headers.update({
77
+ "Authorization": f"Bearer {self._api_key}",
78
+ "Content-Type": "application/json",
79
+ })
80
+ for attr in ("verify", "proxies", "cert"):
81
+ if attr in self._session_kwargs:
82
+ setattr(self._session, attr, self._session_kwargs[attr])
83
+
84
+ def generate(self, requests: list[Request]) -> list[Result_]:
85
+ self._ensure_session()
86
+ if self._api_key is None:
87
+ raise ValueError("Lexsi API key required (pass api_key= or set LEXSI_API_KEY)")
88
+
89
+ mlflow_active = False
90
+ if self._mlflow_tracking_uri:
91
+ try:
92
+ import mlflow
93
+ mlflow.set_tracking_uri(self._mlflow_tracking_uri)
94
+ mlflow.set_experiment(self._mlflow_experiment)
95
+ mlflow_active = True
96
+ except ImportError:
97
+ pass
98
+
99
+ results = []
100
+ run_metrics = {}
101
+ defaults = {"temperature": DEFAULT_TEMPERATURE, "max_tokens": 1024}
102
+ last_gen_kwargs = dict(defaults)
103
+
104
+ for r in requests:
105
+ prompt = r.prompt if isinstance(r.prompt, str) else str(r.prompt)
106
+ last_gen_kwargs = resolve_params(r, defaults, _KEY_MAP)
107
+ import time
108
+ t0 = time.monotonic()
109
+ resp = self._session.post(
110
+ f"{self._api_base}/completions",
111
+ json={
112
+ "model": self._model_name,
113
+ "prompt": prompt,
114
+ **last_gen_kwargs,
115
+ **self._extra_kwargs,
116
+ },
117
+ timeout=self._session_kwargs.get("timeout", 120),
118
+ )
119
+ latency_ms = (time.monotonic() - t0) * 1000
120
+ self._total_requests += 1
121
+ self._total_latency_ms += latency_ms
122
+ try:
123
+ resp.raise_for_status()
124
+ except Exception as e:
125
+ # requests' default HTTPError message drops the response
126
+ # body, which is where the real API actually explains what's
127
+ # wrong. Surface it instead of a bare "400 Client Error".
128
+ raise type(e)(f"{e} -- response body: {resp.text}") from None
129
+ data = resp.json()
130
+ text = data.get("choices", [{}])[0].get("text", "")
131
+ results.append(Result_(completions=[Generated(text=text)], latency_ms=latency_ms))
132
+
133
+ if mlflow_active:
134
+ import mlflow
135
+ with mlflow.start_run(run_name=f"lexsi-batch-{self._total_requests}"):
136
+ mlflow.log_param("model", self._model_name)
137
+ mlflow.log_param("temperature", last_gen_kwargs.get("temperature", DEFAULT_TEMPERATURE))
138
+ mlflow.log_param("max_tokens", last_gen_kwargs.get("max_tokens", 1024))
139
+ mlflow.log_param("batch_size", len(requests))
140
+ mlflow.log_metric("total_requests", self._total_requests)
141
+ mlflow.log_metric("avg_latency_ms", self._total_latency_ms / max(self._total_requests, 1))
142
+ if run_metrics:
143
+ for k, v in run_metrics.items():
144
+ mlflow.log_metric(k, v)
145
+
146
+ return results
147
+
148
+ def log_mlflow_params(self, **params: Any) -> None:
149
+ """Manually log parameters to the active MLflow run."""
150
+ try:
151
+ import mlflow
152
+ for k, v in params.items():
153
+ mlflow.log_param(k, v)
154
+ except ImportError:
155
+ pass
@@ -0,0 +1,65 @@
1
+ """LiteLLM unified API backend (optional extra: auditkit[litellm])."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from . import (
7
+ Model, Request, Result_, Generated, resolve_params, resolve_messages,
8
+ DEFAULT_TEMPERATURE, reject_generation_kwargs,
9
+ )
10
+ from ..errors import ExtraNotInstalled
11
+
12
+ # LiteLLM normalizes most providers to OpenAI-style kwarg names.
13
+ _KEY_MAP = {
14
+ "temperature": "temperature",
15
+ "top_p": "top_p",
16
+ "max_tokens": "max_tokens",
17
+ "stop_sequences": "stop",
18
+ "presence_penalty": "presence_penalty",
19
+ "frequency_penalty": "frequency_penalty",
20
+ "num_completions": "n",
21
+ "seed": "seed",
22
+ }
23
+
24
+
25
+ class LiteLLMModel(Model):
26
+ """Call any LLM through the LiteLLM unified interface."""
27
+
28
+ name = "litellm"
29
+ threadsafe = True
30
+
31
+ def __init__(self, model: str = "gpt-4o-mini",
32
+ name: str = "litellm", **kwargs: Any) -> None:
33
+ reject_generation_kwargs(kwargs, "LiteLLMModel")
34
+ self._model_name = model
35
+ self._extra_kwargs = kwargs
36
+ self.name = name
37
+
38
+ def generate(self, requests: list[Request]) -> list[Result_]:
39
+ try:
40
+ from litellm import completion
41
+ except ImportError:
42
+ raise ExtraNotInstalled("litellm", "pip install auditkit[litellm]")
43
+ results = []
44
+ for r in requests:
45
+ defaults = {"temperature": DEFAULT_TEMPERATURE, "max_tokens": 1024}
46
+ gen_kwargs = resolve_params(r, defaults, _KEY_MAP)
47
+ resp = completion(
48
+ model=self._model_name,
49
+ messages=resolve_messages(r),
50
+ **gen_kwargs,
51
+ **self._extra_kwargs,
52
+ )
53
+ text = resp.choices[0].message.content or ""
54
+ usage = {}
55
+ resp_usage = getattr(resp, "usage", None)
56
+ if resp_usage is not None:
57
+ # LiteLLM normalizes every provider's response to this
58
+ # OpenAI-shaped usage object regardless of backend.
59
+ usage = {
60
+ "prompt_tokens": resp_usage.prompt_tokens,
61
+ "completion_tokens": resp_usage.completion_tokens,
62
+ "total_tokens": resp_usage.total_tokens,
63
+ }
64
+ results.append(Result_(completions=[Generated(text=text)], usage=usage))
65
+ return results
@@ -0,0 +1,90 @@
1
+ """OpenAI model backend (optional extra: auditkit[openai])."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from . import (
7
+ Model, Request, Result_, Generated, resolve_params, resolve_messages,
8
+ DEFAULT_TEMPERATURE, reject_generation_kwargs,
9
+ )
10
+ from ..errors import ExtraNotInstalled
11
+
12
+ # AuditKit-internal RunConfig key -> OpenAI chat.completions.create() kwarg name.
13
+ # Only these keys are ever pulled from Request.params; timeout/max_retries/
14
+ # top_k/best_of are either Runner-only or unsupported by this API, so they're
15
+ # deliberately absent and can never reach the real API call.
16
+ _KEY_MAP = {
17
+ "temperature": "temperature",
18
+ "top_p": "top_p",
19
+ "max_tokens": "max_tokens",
20
+ "stop_sequences": "stop",
21
+ "presence_penalty": "presence_penalty",
22
+ "frequency_penalty": "frequency_penalty",
23
+ "num_completions": "n",
24
+ "seed": "seed",
25
+ }
26
+
27
+ # Real openai.OpenAI() CLIENT-construction kwargs -- must reach the client
28
+ # constructor in _ensure_client(), not chat.completions.create(). Previously
29
+ # blindly spread into **self._extra_kwargs on the per-request call instead,
30
+ # where max_retries/default_headers/default_query/organization/project
31
+ # don't exist at all and raise TypeError (timeout happens to be valid on
32
+ # both, so it alone never surfaced this). Anything else in **kwargs is
33
+ # assumed to be a real chat.completions.create()-level kwarg and keeps
34
+ # going there, unaffected. api_key/base_url are already handled via their
35
+ # own explicit constructor params, not this set.
36
+ _CLIENT_KWARGS = frozenset({
37
+ "timeout", "max_retries", "default_headers", "default_query",
38
+ "organization", "project",
39
+ })
40
+
41
+
42
+ class OpenAIModel(Model):
43
+ """Call the OpenAI chat completions API."""
44
+
45
+ name = "openai"
46
+
47
+ def __init__(self, model: str = "gpt-4o-mini", api_key: str | None = None,
48
+ api_base: str | None = None, name: str = "openai", **kwargs: Any) -> None:
49
+ reject_generation_kwargs(kwargs, "OpenAIModel")
50
+ self._model_name = model
51
+ self._extra_kwargs = kwargs
52
+ self._client = None
53
+ self._api_key = api_key
54
+ self._api_base = api_base
55
+ self.name = name
56
+
57
+ def _ensure_client(self) -> None:
58
+ if self._client is not None:
59
+ return
60
+ try:
61
+ import openai
62
+ except ImportError:
63
+ raise ExtraNotInstalled("openai", "pip install auditkit[openai]")
64
+ client_kwargs = {k: v for k, v in self._extra_kwargs.items() if k in _CLIENT_KWARGS}
65
+ self._client = openai.OpenAI(api_key=self._api_key, base_url=self._api_base, **client_kwargs)
66
+
67
+ def generate(self, requests: list[Request]) -> list[Result_]:
68
+ self._ensure_client()
69
+ results = []
70
+ for r in requests:
71
+ defaults = {"temperature": DEFAULT_TEMPERATURE, "max_tokens": 1024}
72
+ gen_kwargs = resolve_params(r, defaults, _KEY_MAP)
73
+ call_kwargs = {k: v for k, v in self._extra_kwargs.items() if k not in _CLIENT_KWARGS}
74
+ resp = self._client.chat.completions.create(
75
+ model=self._model_name,
76
+ messages=resolve_messages(r),
77
+ **gen_kwargs,
78
+ **call_kwargs,
79
+ )
80
+ text = resp.choices[0].message.content or ""
81
+ usage = {}
82
+ resp_usage = getattr(resp, "usage", None)
83
+ if resp_usage is not None:
84
+ usage = {
85
+ "prompt_tokens": resp_usage.prompt_tokens,
86
+ "completion_tokens": resp_usage.completion_tokens,
87
+ "total_tokens": resp_usage.total_tokens,
88
+ }
89
+ results.append(Result_(completions=[Generated(text=text)], usage=usage))
90
+ return results
@@ -0,0 +1,152 @@
1
+ """OpenRouter model backend (optional extra: auditkit[requests]).
2
+
3
+ OpenRouter's API is a documented OpenAI-compatible endpoint
4
+ (https://openrouter.ai/api/v1, standard Bearer auth, /chat/completions
5
+ request/response shape) -- this backend is a thin, pre-configured wrapper so
6
+ you don't have to pass api_base=/api_key= through the generic api: backend
7
+ by hand every time. Same shape as GroqModel; OpenRouter itself is a router
8
+ in front of many providers (OpenAI, Anthropic, Meta, Mistral, Google, ...),
9
+ selected via a "provider/model" name (e.g. "openai/gpt-4o",
10
+ "anthropic/claude-3.5-sonnet", "meta-llama/llama-3.1-70b-instruct") rather
11
+ than one fixed model family.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ import time
17
+ from typing import Any
18
+
19
+ from . import (
20
+ Model, Request, Result_, Generated, resolve_params, resolve_messages,
21
+ DEFAULT_TEMPERATURE, reject_generation_kwargs, split_session_kwargs,
22
+ )
23
+ from ..errors import ExtraNotInstalled
24
+
25
+ _KEY_MAP = {
26
+ "temperature": "temperature",
27
+ "top_p": "top_p",
28
+ "max_tokens": "max_tokens",
29
+ "stop_sequences": "stop",
30
+ "presence_penalty": "presence_penalty",
31
+ "frequency_penalty": "frequency_penalty",
32
+ "num_completions": "n",
33
+ "seed": "seed",
34
+ }
35
+
36
+
37
+ class OpenRouterModel(Model):
38
+ """Call the OpenRouter chat completions API.
39
+
40
+ Parameters
41
+ ----------
42
+ model
43
+ Model name on OpenRouter, in ``"provider/model"`` form (e.g.
44
+ ``"openai/gpt-4o"``, ``"anthropic/claude-3.5-sonnet"``,
45
+ ``"meta-llama/llama-3.1-70b-instruct"``). The provider prefix is
46
+ OpenRouter's own routing syntax, unrelated to AuditKit's
47
+ ``"openrouter:"`` spec prefix -- ``"openrouter:openai/gpt-4o"``
48
+ parses correctly since only the text before the *first* colon is
49
+ AuditKit's own prefix.
50
+ api_key
51
+ OpenRouter API key. Falls back to the ``OPENROUTER_API_KEY`` env var.
52
+ api_base
53
+ Base URL (default: ``https://openrouter.ai/api/v1``).
54
+ site_url, app_name
55
+ Optional -- OpenRouter's own leaderboard/analytics attribution
56
+ headers (``HTTP-Referer``/``X-Title``). Not required for the API
57
+ to work; harmless to omit.
58
+ """
59
+
60
+ name = "openrouter"
61
+ threadsafe = False
62
+
63
+ def __init__(
64
+ self,
65
+ model: str = "openai/gpt-4o-mini",
66
+ api_key: str | None = None,
67
+ api_base: str = "https://openrouter.ai/api/v1",
68
+ site_url: str | None = None,
69
+ app_name: str | None = None,
70
+ name: str = "openrouter",
71
+ **kwargs: Any,
72
+ ) -> None:
73
+ reject_generation_kwargs(kwargs, "OpenRouterModel")
74
+ self._model_name = model
75
+ self._api_base = api_base.rstrip("/")
76
+ self._api_key = api_key or os.environ.get("OPENROUTER_API_KEY")
77
+ self._site_url = site_url
78
+ self._app_name = app_name
79
+ # verify=/proxies=/cert=/timeout= are real requests.Session concerns,
80
+ # not API request-body fields -- split them out so e.g.
81
+ # OpenRouterModel(verify=False) actually disables TLS verification
82
+ # instead of silently becoming an extra, ignored JSON field.
83
+ self._session_kwargs, self._extra_kwargs = split_session_kwargs(kwargs)
84
+ self._session = None
85
+ self.name = name
86
+
87
+ def _ensure_session(self) -> None:
88
+ if self._session is not None:
89
+ return
90
+ try:
91
+ import requests
92
+ except ImportError:
93
+ raise ExtraNotInstalled("requests", "pip install auditkit[requests]")
94
+ self._session = requests.Session()
95
+ headers = {
96
+ "Authorization": f"Bearer {self._api_key}" if self._api_key else "",
97
+ "Content-Type": "application/json",
98
+ }
99
+ if self._site_url:
100
+ headers["HTTP-Referer"] = self._site_url
101
+ if self._app_name:
102
+ headers["X-Title"] = self._app_name
103
+ self._session.headers.update(headers)
104
+ for attr in ("verify", "proxies", "cert"):
105
+ if attr in self._session_kwargs:
106
+ setattr(self._session, attr, self._session_kwargs[attr])
107
+
108
+ def generate(self, requests: list[Request]) -> list[Result_]:
109
+ self._ensure_session()
110
+ results = []
111
+ for r in requests:
112
+ defaults = {"temperature": DEFAULT_TEMPERATURE, "max_tokens": 1024}
113
+ gen_kwargs = resolve_params(r, defaults, _KEY_MAP)
114
+ body = {
115
+ "model": self._model_name,
116
+ "messages": resolve_messages(r),
117
+ **gen_kwargs,
118
+ **self._extra_kwargs,
119
+ }
120
+ timeout = self._session_kwargs.get("timeout", 120)
121
+ t0 = time.monotonic()
122
+ resp = self._session.post(f"{self._api_base}/chat/completions", json=body, timeout=timeout)
123
+ try:
124
+ resp.raise_for_status()
125
+ except Exception as e:
126
+ # requests' default HTTPError message drops the response body,
127
+ # which is where OpenRouter (and the underlying provider it
128
+ # routed to) actually explains what's wrong -- an invalid
129
+ # model slug, an unsupported param for that specific model,
130
+ # rate limiting, etc. Surface it instead of a bare "400
131
+ # Client Error: Bad Request".
132
+ raise type(e)(f"{e} -- response body: {resp.text}") from None
133
+ latency_ms = (time.monotonic() - t0) * 1000
134
+ data = resp.json()
135
+ message = data.get("choices", [{}])[0].get("message", {}) or {}
136
+ # Reasoning models (e.g. some OpenRouter-routed models) can return
137
+ # an explicit `"content": null` -- not a missing key -- when
138
+ # max_tokens is hit while the model is still inside its internal
139
+ # `reasoning` field. dict.get(key, "") only falls back to "" when
140
+ # the key is *absent*, so a present-but-null value passes through
141
+ # as None otherwise; `or ""` catches both cases.
142
+ text = message.get("content") or ""
143
+ raw_usage = data.get("usage") or {}
144
+ usage = {}
145
+ if raw_usage:
146
+ usage = {
147
+ "prompt_tokens": raw_usage.get("prompt_tokens", 0),
148
+ "completion_tokens": raw_usage.get("completion_tokens", 0),
149
+ "total_tokens": raw_usage.get("total_tokens", 0),
150
+ }
151
+ results.append(Result_(completions=[Generated(text=text)], usage=usage, latency_ms=latency_ms))
152
+ return results