Skip to content

feat: add Cerebras integration to the Python SDK - #7342

Merged
alexkuzmik merged 2 commits into
comet-ml:mainfrom
NishchayMahor:feat/cerebras-integration
Sep 7, 2026
Merged

alexkuzmik merged 2 commits into
comet-ml:mainfrom
NishchayMahor:feat/cerebras-integration

Conversation

@NishchayMahor

Copy link
Copy Markdown
Contributor

Summary

Adds a Cerebras integration to the Opik Python SDK, so calls made with the native cerebras-cloud-sdk client are automatically logged as Opik LLM spans:

from cerebras.cloud.sdk import Cerebras
from opik.integrations.cerebras import track_cerebras

client = track_cerebras(Cerebras())
client.chat.completions.create(
    model="llama-3.3-70b",
    messages=[{"role": "user", "content": "Why is the sky blue?"}],
)

Cerebras (very fast inference for Llama and other open models) has a distinct native SDK, so the OpenAI wrapper (track_openai) can't instrument it — this adds first-class support, matching the existing standalone integrations.

Details

track_cerebras() follows the existing OpenAI/Anthropic pattern:

  • Wraps chat.completions.create() on both Cerebras and AsyncCerebras, including stream=True (sync + async streaming via aggregated chunks).
  • Records the span provider as "cerebras"; Cerebras returns OpenAI-shaped usage, so token usage is parsed with the OpenAI converter (the comment notes this denotes payload format, not the span provider).
  • Graceful span/trace finalization with error_info on failures.

Files:

  • src/opik/integrations/cerebras/ — track_cerebras, decorator, chunk aggregator, stream patchers.
  • tests/library_integration/cerebras/ — mock-based unit tests + requirements.txt.
  • CI: lib-cerebras-tests.yml + registration in lib-integration-tests-runner.yml.

Testing

pytest tests/library_integration/cerebras/ → 4 passed (sync happy-path with full trace-tree assertion, async, and error handling), using the fake_backend fixture and a mocked transport so no Cerebras API key is required. ruff and mypy clean.

Happy to add a documentation/cookbook page as a follow-up if preferred.

@NishchayMahor
NishchayMahor requested review from a team as code owners July 5, 2026 06:33
@github-actions github-actions Bot added dependencies Pull requests that update a dependency file python Pull requests that update Python code Infrastructure tests Including test files, or tests related like configuration. Python SDK labels Jul 5, 2026
Comment on lines +26 to +30
aggregated_response = {
"choices": [{"index": 0, "message": {"role": "", "content": ""}}],
"created": first_chunk.created,
"id": first_chunk.id,
"model": first_chunk.model,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate chunk aggregation

aggregate() is a near copy of opik/integrations/openai/chat_completion_chunks_aggregator.py:22-63, so OpenAI and Cerebras chunk stitching will drift if this logic changes; should we extract it into a reusable helper?

Severity

Want Baz to fix this for you? Activate Fixer

Comment on lines +48 to +49
if delta.content:
text_chunks.append(delta.content)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drops streamed reasoning output

delta.reasoning is dropped when the reducer aggregates streamed choices, so reasoning-enabled completions log a synthetic choices[0].message without the reasoning text and the span output is incomplete — should we preserve it too, as Cerebras’ official reasoning docs say it’s delivered in delta.reasoning?

Severity web_search

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
sdks/python/src/opik/integrations/cerebras/chat_completion_chunks_aggregator.py around
lines 48-49 inside the `aggregate` function’s loop, the code currently appends only
`delta.content` into `text_chunks` and never copies `delta.reasoning` from the streamed
chunks. Refactor the aggregation to also collect `delta.reasoning` tokens (e.g., a
separate buffer) and set them onto the synthetic
`aggregated_response['choices'][0]['message']` in the appropriate field so downstream
span/log output includes reasoning-enabled text. Ensure this works when reasoning is
present across multiple streamed chunks by concatenating all reasoning tokens in order,
similar to how `content` is concatenated.

Comment on lines +33 to +37
def Stream__iter__decorator(dunder_iter_func: Callable) -> Callable:
@functools.wraps(dunder_iter_func)
def wrapper(self: cerebras.Stream) -> Iterator[StreamItem]:
try:
accumulated_items: List[StreamItem] = []

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate stream patching

patch_sync_stream/patch_async_stream duplicate the opik/integrations/openai/stream_patchers.py:30‑157 logic almost verbatim for cerebras.Stream, so any fix to generations_aggregator or finally_callback has to be mirrored in both places and can drift — should we parameterize the shared patcher instead?

Severity

Want Baz to fix this for you? Activate Fixer

Comment on lines +43 to +47
LOGGER.debug(
"Exception raised from cerebras.Stream.",
str(exception),
exc_info=True,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logging call raises formatting error

In both the sync and async stream wrappers, LOGGER.debug("Exception raised from cerebras.Stream.", str(exception), exc_info=True) passes str(exception) to a message with no %s, so logging raises a TypeError before the debug line is emitted and can interrupt exception handling while the span is still open — should we change it to LOGGER.debug("Exception raised from cerebras.Stream: %s", exception, exc_info=True)?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
sdks/python/src/opik/integrations/cerebras/stream_patchers.py around lines 43-47 in
patch_sync_stream’s Stream__iter__decorator exception handler, the LOGGER.debug call
passes str(exception) as a positional arg to a message string with no formatting
placeholders, which can raise TypeError during logging and prevent cleanup. Refactor
that logging line to use standard logging formatting with a “%s” placeholder (e.g.,
append “: %s” to the message and pass the exception as the corresponding argument)
while keeping exc_info=True. Do the same for the analogous LOGGER.debug call in
patch_async_stream around lines 96-100 inside AsyncStream__aiter__decorator.

Comment on lines +4 to +8
import cerebras.cloud.sdk as cerebras

from opik.types import LLMProvider

from . import cerebras_chat_completions_decorator, chat_completion_chunks_aggregator

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional import crashes package load

sdks/python/src/opik/integrations/cerebras/__init__.py imports cerebras.cloud.sdk and the decorator helpers at module load, so import opik.integrations.cerebras raises ModuleNotFoundError in environments without cerebras-cloud-sdk before track_cerebras() is reached — should we move those imports behind the public function boundary or guard them like the other optional integrations, as .agents/skills/python-sdk/SKILL.md requires?

Severity

Want Baz to fix this for you? Activate Fixer You can also update your AI coding guidelines based on this comment by apply pr to [branch name]

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
sdks/python/src/opik/integrations/cerebras/opik_tracker.py around lines 1-70,
`track_cerebras()` relies on `import cerebras.cloud.sdk as cerebras` and module-scope
imports of the Cerebras decorator modules, which execute during package import and can
raise ModuleNotFoundError when the optional `cerebras-cloud-sdk` isn’t installed.
Refactor by removing the top-level Cerebras SDK import and any other Cerebras-runtime
imports from module scope; instead, perform them inside `track_cerebras()` (and
`_extract_metadata_from_client` if needed) with a clear error message if Cerebras
isn’t installed. Keep typing aliases (e.g., `TypeVar`) behind `typing.TYPE_CHECKING`
or adjust them so they don’t require the Cerebras SDK at import time.

@NishchayMahor

Copy link
Copy Markdown
Contributor Author

Same as #7341 (Groq) — these three points are intentional consistency with the existing OpenAI/Anthropic integrations:

  1. The aggregator's try/except → None mirrors integrations/openai/chat_completion_chunks_aggregator.py.
  2. stream_patchers deliberately follows the OpenAI integration's structure (Cerebras's SDK is OpenAI-shaped; only Stream/AsyncStream, a subset). A shared base for OpenAI/Groq/Cerebras would be a separate refactor.
  3. import cerebras.cloud.sdk at module top matches import anthropic in the anthropic integration; it's opt-in, so import opik doesn't pull it. Happy to adjust any of these.

Add track_cerebras() to wrap Cerebras / AsyncCerebras clients so calls to
chat.completions.create() (including stream=True) are logged as Opik LLM
spans, following the existing OpenAI/Anthropic integration pattern. Cerebras
returns OpenAI-shaped payloads, so usage is parsed with the OpenAI converter
while the span provider is recorded as 'cerebras'. Adds mock-based unit tests
and wires the cerebras suite into the integration runner.
@NishchayMahor
NishchayMahor force-pushed the feat/cerebras-integration branch from 729ed0d to b89405f Compare August 16, 2026 20:35
@NishchayMahor

Copy link
Copy Markdown
Contributor Author

Rebased onto latest main — this is MERGEABLE again.

Note for anyone comparing SHAs: a plain rebase wasn't possible (git merge-base between the branch and current main is empty, so history has been rewritten upstream since the branch was cut). I recreated the branch from current main and cherry-picked the single commit instead, so the diff is unchanged.

The only conflict was in lib-integration-tests-runner.yml, where mistral and groq landed in the same lists this PR adds cerebras to — resolved by keeping all three, in the choice options, the job list, the notify-slack needs, and the Slack results payload. Verified the resolved file parses and all three jobs are wired.

Also re-checked the decorator API against current main: _start_span_inputs_preprocessor, _end_span_inputs_preprocessor and _streams_handler all still match the OpenAI reference implementation, so nothing drifted.

@andrescrz

Copy link
Copy Markdown
Member

Hi @NishchayMahor

Thanks for the contribution and apologies about the delay!

We're going to proceed with this review and potentially add this Cerebras integration if once all is ready in the PR.

When you have a chance, could you take a look at the automated review comments from Baz? For each comment, please either address it with a code change or reply with a brief explanation if you believe the current implementation is the right approach.

Thanks!

Comment on lines +9 to +10
env:
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR tests inherit Cerebras credential

CEREBRAS_API_KEY is exposed at workflow scope, so PR-controlled code run by pip install . or pytest can read and transmit the real secret even though the tests use a fake key and monkeypatch _post — should we remove the workflow-wide env entry or scope credentials to the specific test step that needs Cerebras access?

Severity web_search

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`.github/workflows/lib-cerebras-tests.yml` around lines 9-10, remove the workflow-wide
`CEREBRAS_API_KEY` environment variable because it exposes the secret to package
installation and PR-controlled test code, while these tests use fake credentials and
mocked requests. If a future test genuinely requires the key, scope it only to that
specific step rather than defining it at workflow scope.

Comment on lines +69 to +70
def _extract_metadata_from_client(client: CerebrasClient) -> Dict[str, Any]:
return {"base_url": str(client.base_url)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Secrets in Cerebras URL reach Opik telemetry

_extract_metadata_from_client stores str(client.base_url) verbatim, so caller-supplied userinfo, query, and fragment components reach metadata["base_url"] and the span/trace payloads — should we record only the validated host/origin or strip those components first?

Severity web_search

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`sdks/python/src/opik/integrations/cerebras/opik_tracker.py` around lines 69-70, update
`_extract_metadata_from_client` so `metadata["base_url"]` cannot contain credentials,
query parameters, or fragments from the caller-provided Cerebras `base_url`. Parse the
URL and record only its validated scheme and host/origin (including a non-sensitive port
when needed), with safe handling for malformed or missing values before the metadata is
merged into spans and traces.

Comment on lines +68 to +70
cerebras.Stream.__iter__ = Stream__iter__decorator(original_stream_iter_method)

stream.opik_tracked_instance = True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

patch_sync_stream replaces cerebras.Stream.__iter__ at the class level with per-call finally_callback and accumulated_items state, so concurrent track_cerebras() calls share state and can finalize the wrong span/trace or make untracked streams accumulate chunks; patch_async_stream has the same issue with cerebras.AsyncStream.__aiter__ — should we wrap each tracked stream instance or use a thread-safe per-stream dispatcher?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`sdks/python/src/opik/integrations/cerebras/stream_patchers.py` around lines 68-70 and
121-125, refactor `patch_sync_stream` and `patch_async_stream` so they no longer replace
`cerebras.Stream.__iter__` or `cerebras.AsyncStream.__aiter__` at the class level with a
closure holding call-specific state. Instead, attach the iterator wrapper only to the
tracked stream instance (or use a stable per-instance/thread-safe dispatcher that reads
finalization state from that stream), ensuring: (1) concurrent `track_cerebras()` calls
finalize the correct span, trace, and callback without racing; (2) untracked streams
never accumulate chunks or otherwise change SDK behavior; and (3) successive tracked
streams don't stack wrappers around each other — while preserving the original SDK
methods and existing finalization/error handling.

Comment on lines +95 to +106
except Exception as exception:
LOGGER.debug(
"Exception raised from cerebras.AsyncStream.",
str(exception),
exc_info=True,
)
error_info = error_info_collector.collect(exception)
raise exception
finally:
if hasattr(self, "opik_tracked_instance"):
delattr(self, "opik_tracked_instance")
output = (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interrupted streams recorded as successful

The sync and async stream wrappers catch only Exception, so cancellations and generator termination leave error_info=None; their finally blocks pass partial aggregation to _after_call, which records interrupted streams as successful output — should we capture these terminations, pass output=None, and re-raise the original interruption unchanged?

Severity web_search

Want Baz to fix this for you? Activate Fixer You can also update your AI coding guidelines based on this comment by apply pr to [branch name]

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`sdks/python/src/opik/integrations/cerebras/stream_patchers.py` around lines 42-64 and
95-117, update the sync and async stream wrapper exception handling so cancellation,
`GeneratorExit`, and other relevant `BaseException` interruptions are collected into
`error_info` before finalization. Ensure these paths re-raise the original termination
unchanged, while the `finally` logic passes `output=None` instead of aggregating partial
items; preserve normal `Exception` handling and successful aggregation for genuinely
completed streams.

@alexkuzmik

Copy link
Copy Markdown
Collaborator

Thanks for the contribution @NishchayMahor!

@alexkuzmik
alexkuzmik merged commit 87e5dc1 into comet-ml:main Sep 7, 2026
55 of 59 checks passed
JetoPistola pushed a commit that referenced this pull request Sep 15, 2026
…rror paths (#8238)

The two LOGGER.debug calls in the Cerebras stream wrappers pass
str(exception) positionally to a message with no %s. logging defers the
interpolation to the handler, so with debug logging on it raises inside
Handler.emit, handleError prints a "--- Logging error ---" block to
stderr, and the record — including the exc_info traceback — is dropped.

Same fix as #7850, which covered the twelve call sites that existed when
it merged. The Cerebras integration landed afterwards in #7342, so these
two were never in its scope. An AST pass over sdks/python/src/opik for
LOGGER calls whose message carries fewer %-placeholders than positional
args now reports none.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file Infrastructure Python SDK python Pull requests that update Python code 🔴 size/XL tests Including test files, or tests related like configuration.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants