Skip to content

Add observability to codebase#23

Open
WilliamAvHolmberg wants to merge 13 commits into
mainfrom
claude/add-observability-011CUsHJ9qYro3haDsqX2vxW
Open

Add observability to codebase#23
WilliamAvHolmberg wants to merge 13 commits into
mainfrom
claude/add-observability-011CUsHJ9qYro3haDsqX2vxW

Conversation

@WilliamAvHolmberg

@WilliamAvHolmberg WilliamAvHolmberg commented Nov 7, 2025

Copy link
Copy Markdown
Owner

Add full-featured observability to OpenRouter.NET using OpenTelemetry, enabling tracking of LLM API calls, token usage, performance metrics, and tool execution with platforms like Arize Phoenix, Jaeger, or any OTLP-compatible backend.

Key Features

  • Opt-in telemetry: All telemetry disabled by default for privacy
  • Request/Response tracking: Capture prompts, completions, raw JSON
  • Token usage: Track input/output tokens for cost monitoring
  • Streaming metrics: TTFT, tokens/sec, chunk count, duration
  • Tool execution spans: Child spans for tool calls with args/results
  • Error tracking: Automatic exception recording with stack traces
  • Privacy controls: Sanitization callbacks for PII redaction
  • Zero overhead: No performance impact when disabled
  • Standards compliant: Follows OpenTelemetry GenAI semantic conventions

Implementation Details

New Components

  • OpenRouterActivitySource: Singleton ActivitySource for tracing
  • GenAiSemanticConventions: Constants for OTel GenAI attributes
  • OpenRouterTelemetryOptions: Comprehensive opt-in configuration
  • TelemetryHelper: Enricher methods for spans and events
  • OpenTelemetryExtensions: Extension method for easy setup

Instrumented Classes

  • OpenRouterClient.CreateChatCompletionAsync(): Chat completion spans
  • StreamingHandler: Streaming metrics (TTFT, throughput)
  • ToolManager.ExecuteTool(): Tool execution child spans
  • ObjectGenerator.GenerateObjectAsync(): Schema validation tracking
  • HttpRequestHandler: HTTP-level attributes

Configuration Options

  • EnableTelemetry: Master switch (default: false)
  • CaptureRawRequests/Responses: Log full HTTP payloads
  • CapturePrompts/Completions: Log structured messages
  • CaptureStreamChunks: Log individual chunks (high volume)
  • CaptureToolDetails: Log tool arguments and results
  • MaxEventBodySize: Size limit before truncation (32KB default)
  • SanitizePrompt/Completion/ToolArguments: PII redaction callbacks

Presets

  • OpenRouterTelemetryOptions.Default: All disabled (opt-in)
  • OpenRouterTelemetryOptions.Production: Metrics only, no content
  • OpenRouterTelemetryOptions.Debug: Full capture for debugging
  • OpenRouterTelemetryOptions.FullTelemetry: Everything enabled

Documentation

  • Comprehensive observability guide: docs/OBSERVABILITY.md
  • Setup examples for Arize Phoenix, Jaeger, console exporter
  • Privacy best practices and PII sanitization examples
  • ASP.NET Core integration examples
  • Performance impact details and sampling strategies

Dependencies Added

  • OpenTelemetry.Api 1.10.0 (lightweight, core instrumentation)
  • OpenTelemetry 1.10.0 (for TracerProviderBuilder extension)
  • System.Diagnostics.DiagnosticSource 9.0.0 (Activity support)

Usage Example

// Configure OpenTelemetry
var tracerProvider = Sdk.CreateTracerProviderBuilder()
    .AddOpenRouterInstrumentation()
    .AddOtlpExporter(options =>
    {
        options.Endpoint = new Uri("http://localhost:4317");
    })
    .Build();

// Enable telemetry in client
var client = new OpenRouterClient(new OpenRouterClientOptions
{
    ApiKey = "your-api-key",
    Telemetry = new OpenRouterTelemetryOptions
    {
        EnableTelemetry = true,
        CapturePrompts = true,
        CaptureCompletions = true,
        CaptureToolDetails = true
    }
});

// All API calls are now automatically traced!

Breaking Changes

None - all changes are additive and opt-in by default.

Resolves: #[issue-number] (if applicable)

Summary by CodeRabbit

  • New Features

    • Built-in OpenTelemetry observability with configurable presets (Default, Production, Debug, FullTelemetry), spans for requests/streams/tool calls/tokens/errors, optional raw captures with sanitization, and zero runtime overhead when disabled; per-client/per-endpoint telemetry controls.
  • Documentation

    • README observability section, Quick Start example, and full Observability Guide added.
  • Samples

    • Sample app updated with telemetry-enabled setup, exporter examples (console/OTLP) and backend guidance.

Add full-featured observability to OpenRouter.NET using OpenTelemetry,
enabling tracking of LLM API calls, token usage, performance metrics,
and tool execution with platforms like Arize Phoenix, Jaeger, or any
OTLP-compatible backend.

## Key Features

- **Opt-in telemetry**: All telemetry disabled by default for privacy
- **Request/Response tracking**: Capture prompts, completions, raw JSON
- **Token usage**: Track input/output tokens for cost monitoring
- **Streaming metrics**: TTFT, tokens/sec, chunk count, duration
- **Tool execution spans**: Child spans for tool calls with args/results
- **Error tracking**: Automatic exception recording with stack traces
- **Privacy controls**: Sanitization callbacks for PII redaction
- **Zero overhead**: No performance impact when disabled
- **Standards compliant**: Follows OpenTelemetry GenAI semantic conventions

## Implementation Details

### New Components

- `OpenRouterActivitySource`: Singleton ActivitySource for tracing
- `GenAiSemanticConventions`: Constants for OTel GenAI attributes
- `OpenRouterTelemetryOptions`: Comprehensive opt-in configuration
- `TelemetryHelper`: Enricher methods for spans and events
- `OpenTelemetryExtensions`: Extension method for easy setup

### Instrumented Classes

- `OpenRouterClient.CreateChatCompletionAsync()`: Chat completion spans
- `StreamingHandler`: Streaming metrics (TTFT, throughput)
- `ToolManager.ExecuteTool()`: Tool execution child spans
- `ObjectGenerator.GenerateObjectAsync()`: Schema validation tracking
- `HttpRequestHandler`: HTTP-level attributes

### Configuration Options

- `EnableTelemetry`: Master switch (default: false)
- `CaptureRawRequests/Responses`: Log full HTTP payloads
- `CapturePrompts/Completions`: Log structured messages
- `CaptureStreamChunks`: Log individual chunks (high volume)
- `CaptureToolDetails`: Log tool arguments and results
- `MaxEventBodySize`: Size limit before truncation (32KB default)
- `SanitizePrompt/Completion/ToolArguments`: PII redaction callbacks

### Presets

- `OpenRouterTelemetryOptions.Default`: All disabled (opt-in)
- `OpenRouterTelemetryOptions.Production`: Metrics only, no content
- `OpenRouterTelemetryOptions.Debug`: Full capture for debugging
- `OpenRouterTelemetryOptions.FullTelemetry`: Everything enabled

## Documentation

- Comprehensive observability guide: `docs/OBSERVABILITY.md`
- Setup examples for Arize Phoenix, Jaeger, console exporter
- Privacy best practices and PII sanitization examples
- ASP.NET Core integration examples
- Performance impact details and sampling strategies

## Dependencies Added

- OpenTelemetry.Api 1.10.0 (lightweight, core instrumentation)
- OpenTelemetry 1.10.0 (for TracerProviderBuilder extension)
- System.Diagnostics.DiagnosticSource 9.0.0 (Activity support)

## Usage Example

```csharp
// Configure OpenTelemetry
var tracerProvider = Sdk.CreateTracerProviderBuilder()
    .AddOpenRouterInstrumentation()
    .AddOtlpExporter(options =>
    {
        options.Endpoint = new Uri("http://localhost:4317");
    })
    .Build();

// Enable telemetry in client
var client = new OpenRouterClient(new OpenRouterClientOptions
{
    ApiKey = "your-api-key",
    Telemetry = new OpenRouterTelemetryOptions
    {
        EnableTelemetry = true,
        CapturePrompts = true,
        CaptureCompletions = true,
        CaptureToolDetails = true
    }
});

// All API calls are now automatically traced!
```

## Breaking Changes

None - all changes are additive and opt-in by default.

Resolves: #[issue-number] (if applicable)
@coderabbitai

coderabbitai Bot commented Nov 7, 2025

Copy link
Copy Markdown

Walkthrough

Adds OpenTelemetry-based observability: new telemetry types, ActivitySource, semantic conventions, helpers and extensions; injects telemetry into OpenRouterClient and internal components, updates constructors to accept telemetry options, adds NuGet telemetry dependencies, and expands docs and samples with observability guides and examples.

Changes

Cohort / File(s) Summary
Docs: Observability guides
README.md, docs/OBSERVABILITY.md, samples/StreamingWebApiSample/OBSERVABILITY.md
New observability documentation, quick-start, configuration presets, privacy/sanitization guidance and sample instructions.
Project dependencies
packages/dotnet-sdk/OpenRouter.NET.csproj, samples/StreamingWebApiSample/samples.csproj
Added OpenTelemetry and related NuGet package references (OpenTelemetry API/SDK, exporters, AspNetCore/Http instrumentation, DiagnosticSource).
Observability core (new types)
packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs, .../OpenRouterActivitySource.cs, .../OpenRouterTelemetryOptions.cs, .../OpenTelemetryExtensions.cs, .../TelemetryHelper.cs
New semantic constants, ActivitySource, telemetry options/presets and sanitizers, OpenTelemetry integration helpers, and TelemetryHelper to enrich Activities and record events/exceptions.
Client options
packages/dotnet-sdk/src/OpenRouterClientOptions.cs
Added public Telemetry property of type OpenRouterTelemetryOptions (default preset).
Client integration
packages/dotnet-sdk/src/OpenRouterClient.cs
Wired telemetry options through client and subcomponents; starts/enriches chat activities, records HTTP/response data and exceptions; passes telemetry options into internal components.
Internal handlers & generators
packages/dotnet-sdk/src/Internal/HttpRequestHandler.cs, .../StreamingHandler.cs, .../ObjectGenerator.cs
Constructors updated to accept OpenRouterTelemetryOptions; added telemetry activity scaffolding for requests, streaming, validation, retry loops, metrics and error recording.
Tools
packages/dotnet-sdk/src/Tools/ToolManager.cs
ToolManager accepts telemetry options; tool execution spans tag sanitized args/results, durations, and exceptions.
Sample app instrumentation
samples/StreamingWebApiSample/Program.cs
Added global OpenTelemetry setup (resource, ASP.NET/HTTP instrumentation, OTLP/Console exporters) and per-endpoint OpenRouterClient instances configured with telemetry options.
Sample docs/build
samples/StreamingWebApiSample/samples.csproj
Sample project updated with OpenTelemetry package references and example exporter configuration.
Tests (formatting)
tests/OpenRouter.NET.Tests/SchemaValidationTests.cs
Minor formatting change (added blank line).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant App
  participant Client as OpenRouterClient
  participant ActSrc as OpenRouterActivitySource
  participant Http as HttpClient
  participant API as OpenRouter API

  App->>Client: CreateChatCompletionAsync(request)
  Note over Client,ActSrc `#E8F5E9`: If telemetry enabled → Start "gen_ai.client.chat" activity
  Client->>ActSrc: StartActivity() + add request attrs
  Client->>Http: POST /chat (serialized request)
  activate Http
  Http-->>Client: HTTP response
  deactivate Http
  Client->>ActSrc: EnrichWithHttp + EnrichWithResponse
  alt Success
    Client-->>App: ChatCompletionResponse
  else Error
    Client->>ActSrc: RecordException(), set status
    Client-->>App: throw error
  end
  Note over Client,ActSrc `#E8F5E9`: Stop Activity
Loading
sequenceDiagram
  autonumber
  participant Client
  participant ToolMgr as ToolManager
  participant ActSrc as OpenRouterActivitySource
  participant Tool as ExternalTool

  Client->>ToolMgr: ExecuteTool(name, args)
  Note over ToolMgr,ActSrc `#FFF3E0`: If telemetry enabled → Start "gen_ai.client.tool" activity
  ToolMgr->>ActSrc: Tag tool name/mode/sanitized args
  ToolMgr->>Tool: Invoke(args)
  Tool-->>ToolMgr: Result / Error
  alt Success
    ToolMgr->>ActSrc: Enrich result, mark success
    ToolMgr-->>Client: Result
  else Failure
    ToolMgr->>ActSrc: RecordException(), mark failure
    ToolMgr-->>Client: throw
  end
  Note over ToolMgr,ActSrc `#FFF3E0`: Stop Activity
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Areas to focus review on:
    • TelemetryHelper.cs — serialization, truncation, sanitization, attribute/event size safeguards.
    • OpenRouterClient.cs — activity lifecycle, HTTP enrichment, JSON parsing/error mapping, and constructor wiring to internals.
    • StreamingHandler.cs & ObjectGenerator.cs — streaming metrics aggregation, per-chunk events, validation/retry tagging.
    • ToolManager.cs — tool span semantics, sanitized arguments/results handling.
    • Project files — NuGet version compatibility between SDK and samples.

Possibly related PRs

Poem

I hop through spans and trace each token,
Prompts tucked safe where bytes are spoken.
I tag each tool and time each stream,
I count the chunks and guard your scheme.
A rabbit's hop — observability awoken. 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add observability to codebase' directly and accurately captures the primary objective of this PR, which introduces comprehensive OpenTelemetry-based observability across the SDK.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/add-observability-011CUsHJ9qYro3haDsqX2vxW

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/dotnet-sdk/src/Internal/ObjectGenerator.cs (1)

84-161: Clamp retries before using lastException

If GenerateObjectOptions.MaxRetries is 0 (a valid public configuration), the loop never runs, lastException stays null, and the subsequent TelemetryHelper.RecordException(activity, lastException!) will throw a NullReferenceException before we emit the intended OpenRouterException. We also never even attempt a single call. Please ensure we always execute at least one attempt (e.g., clamp to Math.Max(1, options.MaxRetries) and reuse that value for logging/throw) and only record telemetry when there is an exception instance.

Apply this diff:

-        options ??= new GenerateObjectOptions();
-
-        var schemaJson = schema.GetRawText();
+        options ??= new GenerateObjectOptions();
+        var maxRetries = Math.Max(1, options.MaxRetries);
+
+        var schemaJson = schema.GetRawText();
@@
-        for (int attempt = 0; attempt < options.MaxRetries; attempt++)
+        for (int attempt = 0; attempt < maxRetries; attempt++)
@@
-                Log($"GenerateObject attempt {attempt + 1}/{options.MaxRetries}");
+                Log($"GenerateObject attempt {attempt + 1}/{maxRetries}");
@@
-            activity.SetTag(GenAiSemanticConventions.AttributeValidationSuccess, false);
-            TelemetryHelper.RecordException(activity, lastException!);
+            activity.SetTag(GenAiSemanticConventions.AttributeValidationSuccess, false);
+            if (lastException != null)
+            {
+                TelemetryHelper.RecordException(activity, lastException);
+            }
         }
 
         throw new OpenRouterException(
-            $"Failed to generate structured object after {options.MaxRetries} attempts. Last error: {lastException?.Message}",
-            lastException!);
+            $"Failed to generate structured object after {maxRetries} attempts. Last error: {lastException?.Message}",
+            lastException ?? new OpenRouterException("No generation attempts were executed because MaxRetries was less than 1."));
🧹 Nitpick comments (7)
docs/OBSERVABILITY.md (1)

235-245: Add a language hint to the fenced block

markdownlint (MD040) is flagging the “Example Trace Hierarchy” block because it lacks a language identifier. Please add something like ```text (or another appropriate label) to keep the ASCII tree while satisfying the linter.

packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs (6)

21-35: Complete request/response attributes (optional).

Consider adding seed, choice count, and encoding formats to cover common knobs. (opentelemetry.io)

     public const string AttributeRequestPresencePenalty = "gen_ai.request.presence_penalty";
     public const string AttributeRequestSizeBytes = "gen_ai.request.size_bytes";
+    public const string AttributeRequestSeed = "gen_ai.request.seed";
+    public const string AttributeRequestChoiceCount = "gen_ai.request.choice.count";
+    public const string AttributeRequestEncodingFormats = "gen_ai.request.encoding_formats";

40-45: HTTP attributes look good; add server.port for completeness.

server.port is commonly recommended alongside server.address. (opentelemetry.io)

     public const string AttributeHttpRetryAfter = "http.response.retry_after";
+    public const string AttributeServerPort = "server.port";

47-52: Streaming fields are custom; prefer metrics for TTFT.

Spec models TTFT as metric gen_ai.server.time_to_first_token (seconds). Keep attributes if useful, but also emit the metric and use seconds for consistency. (opentelemetry.io)


63-68: Non‑standard schema/validation keys—prefix as extension or document.

Either prefix with openrouter.* or document as extensions to avoid confusion with spec keys. (opentelemetry.netlify.app)


76-80: Event attribute names OK; consider adding input/output messages.

Add gen_ai.input.messages and gen_ai.output.messages for structured capture when enabled. (opentelemetry.io)

     public const string EventAttributeRawResponse = "gen_ai.response.raw_json";
+    public const string EventAttributeInputMessages = "gen_ai.input.messages";
+    public const string EventAttributeOutputMessages = "gen_ai.output.messages";

7-92: Visibility nit.

Class is internal, so public const fields are effectively internal. For clarity, consider making them internal const.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eb54522 and 161ad83.

📒 Files selected for processing (14)
  • README.md (2 hunks)
  • docs/OBSERVABILITY.md (1 hunks)
  • packages/dotnet-sdk/OpenRouter.NET.csproj (1 hunks)
  • packages/dotnet-sdk/src/Internal/HttpRequestHandler.cs (2 hunks)
  • packages/dotnet-sdk/src/Internal/ObjectGenerator.cs (6 hunks)
  • packages/dotnet-sdk/src/Internal/StreamingHandler.cs (2 hunks)
  • packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs (1 hunks)
  • packages/dotnet-sdk/src/Observability/OpenRouterActivitySource.cs (1 hunks)
  • packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1 hunks)
  • packages/dotnet-sdk/src/Observability/OpenTelemetryExtensions.cs (1 hunks)
  • packages/dotnet-sdk/src/Observability/TelemetryHelper.cs (1 hunks)
  • packages/dotnet-sdk/src/OpenRouterClient.cs (5 hunks)
  • packages/dotnet-sdk/src/OpenRouterClientOptions.cs (2 hunks)
  • packages/dotnet-sdk/src/Tools/ToolManager.cs (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (8)
packages/dotnet-sdk/src/OpenRouterClientOptions.cs (1)
packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
  • OpenRouterTelemetryOptions (7-122)
packages/dotnet-sdk/src/Internal/StreamingHandler.cs (3)
packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
  • OpenRouterTelemetryOptions (7-122)
packages/dotnet-sdk/src/Internal/HttpRequestHandler.cs (2)
  • HttpRequestHandler (11-108)
  • HttpRequestHandler (21-37)
packages/dotnet-sdk/src/Tools/ToolManager.cs (2)
  • ToolManager (11-174)
  • ToolManager (19-23)
packages/dotnet-sdk/src/Internal/ObjectGenerator.cs (6)
packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
  • OpenRouterTelemetryOptions (7-122)
packages/dotnet-sdk/src/OpenRouterClient.cs (7)
  • Task (99-193)
  • Task (205-228)
  • Task (230-260)
  • Task (262-292)
  • Task (315-323)
  • Task (328-335)
  • Log (337-340)
packages/dotnet-sdk/src/Observability/OpenRouterActivitySource.cs (1)
  • OpenRouterActivitySource (9-26)
packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs (1)
  • GenAiSemanticConventions (7-92)
packages/dotnet-sdk/src/Models/GenerateObjectModels.cs (1)
  • GenerateObjectOptions (42-48)
packages/dotnet-sdk/src/Observability/TelemetryHelper.cs (2)
  • TelemetryHelper (11-314)
  • RecordException (214-226)
packages/dotnet-sdk/src/Observability/OpenTelemetryExtensions.cs (1)
packages/dotnet-sdk/src/Observability/OpenRouterActivitySource.cs (1)
  • OpenRouterActivitySource (9-26)
packages/dotnet-sdk/src/Tools/ToolManager.cs (4)
packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
  • OpenRouterTelemetryOptions (7-122)
packages/dotnet-sdk/src/Observability/OpenRouterActivitySource.cs (1)
  • OpenRouterActivitySource (9-26)
packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs (1)
  • GenAiSemanticConventions (7-92)
packages/dotnet-sdk/src/Observability/TelemetryHelper.cs (2)
  • TelemetryHelper (11-314)
  • RecordException (214-226)
packages/dotnet-sdk/src/OpenRouterClient.cs (9)
packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
  • OpenRouterTelemetryOptions (7-122)
packages/dotnet-sdk/src/Tools/ToolManager.cs (2)
  • ToolManager (11-174)
  • ToolManager (19-23)
packages/dotnet-sdk/src/Internal/StreamingHandler.cs (3)
  • StreamingHandler (15-596)
  • StreamingHandler (22-32)
  • Log (592-595)
packages/dotnet-sdk/src/Internal/ObjectGenerator.cs (3)
  • ObjectGenerator (15-265)
  • ObjectGenerator (23-33)
  • Log (261-264)
packages/dotnet-sdk/src/Observability/OpenRouterActivitySource.cs (1)
  • OpenRouterActivitySource (9-26)
packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs (1)
  • GenAiSemanticConventions (7-92)
packages/dotnet-sdk/src/Observability/TelemetryHelper.cs (5)
  • TelemetryHelper (11-314)
  • EnrichWithRequest (16-88)
  • EnrichWithHttp (168-185)
  • EnrichWithResponse (93-163)
  • RecordException (214-226)
packages/dotnet-sdk/src/Internal/HttpRequestHandler.cs (1)
  • AddHeaders (42-56)
packages/dotnet-sdk/src/Exceptions.cs (4)
  • OpenRouterException (3-21)
  • OpenRouterException (8-10)
  • OpenRouterException (12-14)
  • OpenRouterException (16-20)
packages/dotnet-sdk/src/Observability/TelemetryHelper.cs (4)
packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
  • OpenRouterTelemetryOptions (7-122)
packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs (1)
  • GenAiSemanticConventions (7-92)
packages/dotnet-sdk/src/Models/ChatCompletionResponse.cs (1)
  • ChatCompletionResponse (5-27)
packages/dotnet-sdk/src/Models/Message.cs (1)
  • ContentPart (48-52)
packages/dotnet-sdk/src/Internal/HttpRequestHandler.cs (1)
packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
  • OpenRouterTelemetryOptions (7-122)
🪛 markdownlint-cli2 (0.18.1)
docs/OBSERVABILITY.md

107-107: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

Comment on lines +1 to +92
namespace OpenRouter.NET.Observability;

/// <summary>
/// OpenTelemetry semantic conventions for Generative AI operations.
/// Based on: https://opentelemetry.io/docs/specs/semconv/gen-ai/
/// </summary>
internal static class GenAiSemanticConventions
{
// ========== Span Names ==========
public const string SpanNameChat = "gen_ai.client.chat";
public const string SpanNameStream = "gen_ai.client.stream";
public const string SpanNameGenerateObject = "gen_ai.client.generate_object";
public const string SpanNameToolCall = "gen_ai.tool.call";
public const string SpanNameHttp = "http";

// ========== General Attributes ==========
public const string AttributeSystem = "gen_ai.system";
public const string AttributeOperationName = "gen_ai.operation.name";
public const string AttributeServerAddress = "server.address";

// ========== Request Attributes ==========
public const string AttributeRequestModel = "gen_ai.request.model";
public const string AttributeRequestMaxTokens = "gen_ai.request.max_tokens";
public const string AttributeRequestTemperature = "gen_ai.request.temperature";
public const string AttributeRequestTopP = "gen_ai.request.top_p";
public const string AttributeRequestFrequencyPenalty = "gen_ai.request.frequency_penalty";
public const string AttributeRequestPresencePenalty = "gen_ai.request.presence_penalty";
public const string AttributeRequestSizeBytes = "gen_ai.request.size_bytes";

// ========== Response Attributes ==========
public const string AttributeResponseId = "gen_ai.response.id";
public const string AttributeResponseModel = "gen_ai.response.model";
public const string AttributeResponseFinishReasons = "gen_ai.response.finish_reasons";
public const string AttributeResponseSizeBytes = "gen_ai.response.size_bytes";

// ========== Usage/Token Attributes ==========
public const string AttributeUsageInputTokens = "gen_ai.usage.input_tokens";
public const string AttributeUsageOutputTokens = "gen_ai.usage.output_tokens";

// ========== HTTP Attributes ==========
public const string AttributeHttpStatusCode = "http.response.status_code";
public const string AttributeHttpMethod = "http.request.method";
public const string AttributeHttpUrl = "url.full";
public const string AttributeHttpRetryAfter = "http.response.retry_after";

// ========== Streaming Attributes ==========
public const string AttributeStreamTimeToFirstToken = "gen_ai.stream.time_to_first_token_ms";
public const string AttributeStreamTotalChunks = "gen_ai.stream.total_chunks";
public const string AttributeStreamDuration = "gen_ai.stream.duration_ms";
public const string AttributeStreamTokensPerSecond = "gen_ai.stream.tokens_per_second";
public const string AttributeStreamFinishReason = "gen_ai.stream.finish_reason";

// ========== Tool Attributes ==========
public const string AttributeToolName = "gen_ai.tool.name";
public const string AttributeToolId = "gen_ai.tool.id";
public const string AttributeToolExecutionMode = "gen_ai.tool.execution_mode";
public const string AttributeToolArguments = "gen_ai.tool.arguments";
public const string AttributeToolResult = "gen_ai.tool.result";
public const string AttributeToolError = "gen_ai.tool.error";
public const string AttributeToolDuration = "gen_ai.tool.duration_ms";
public const string AttributeToolLoopIteration = "gen_ai.tool.loop_iteration";

// ========== Schema/Validation Attributes ==========
public const string AttributeSchemaSizeBytes = "gen_ai.schema.size_bytes";
public const string AttributeValidationAttempt = "gen_ai.validation.attempt";
public const string AttributeValidationSuccess = "gen_ai.validation.success";
public const string AttributeValidationError = "gen_ai.validation.error";

// ========== Event Names ==========
public const string EventPrompt = "gen_ai.client.input";
public const string EventCompletion = "gen_ai.client.output";
public const string EventStreamChunk = "gen_ai.client.stream.chunk";
public const string EventException = "exception";

// ========== Event Attribute Names ==========
public const string EventAttributePrompt = "gen_ai.prompt";
public const string EventAttributeCompletion = "gen_ai.completion";
public const string EventAttributeRawRequest = "gen_ai.request.raw_json";
public const string EventAttributeRawResponse = "gen_ai.response.raw_json";

// ========== Exception Attributes ==========
public const string AttributeExceptionType = "exception.type";
public const string AttributeExceptionMessage = "exception.message";
public const string AttributeExceptionStacktrace = "exception.stacktrace";

// ========== Constant Values ==========
public const string SystemValue = "openrouter";
public const string OperationChat = "chat";
public const string OperationStream = "stream";
public const string OperationGenerateObject = "generate_object";
public const string ServerAddressValue = "openrouter.ai";
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Repo‑wide verification.

Search and update call sites for renamed keys/events/span names.

Run:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Find non‑standard or deprecated keys/usages to update
rg -n --hidden -S -e 'gen_ai\.client\.input|gen_ai\.client\.output|gen_ai\.tool\.arguments|gen_ai\.tool\.result|gen_ai\.tool\.id|SpanNameToolCall|SpanNameChat|SpanNameStream|OperationStream|OperationGenerateObject'
# Check presence of required provider attribute usage
rg -n --hidden -S -e 'gen_ai\.provider\.name'

Length of output: 2299


Add missing gen_ai.provider.name semantic attribute and set it on all GenAI spans
The OpenTelemetry GenAI spec mandates gen_ai.provider.name on every GenAI span (chat, stream, generate_object, tool calls) (opentelemetry.io). Define

public const string AttributeProviderName = "gen_ai.provider.name";

in GenAiSemanticConventions.cs and update OpenRouterClient.cs, ToolManager.cs, and ObjectGenerator.cs to set this tag to the appropriate provider identifier (e.g. "openrouter").

🤖 Prompt for AI Agents
In packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs around
lines 1-92, add a new constant string AttributeProviderName =
"gen_ai.provider.name"; then in packages/dotnet-sdk/src/OpenRouterClient.cs,
ToolManager.cs, and ObjectGenerator.cs update the GenAI-related span creation
logic to set the attribute "gen_ai.provider.name" to the provider identifier
(e.g. "openrouter") on every GenAI span (chat, stream, generate_object,
tool.call) so the provider name is included per the OpenTelemetry GenAI semantic
conventions.

Comment on lines +10 to +15
public const string SpanNameChat = "gen_ai.client.chat";
public const string SpanNameStream = "gen_ai.client.stream";
public const string SpanNameGenerateObject = "gen_ai.client.generate_object";
public const string SpanNameToolCall = "gen_ai.tool.call";
public const string SpanNameHttp = "http";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Align span naming with OTel guidance; avoid fixed names.

Client span name SHOULD be "{gen_ai.operation.name} {gen_ai.request.model}". Tool span SHOULD be "execute_tool {gen_ai.tool.name}". Fixed names like "gen_ai.client.chat" and "gen_ai.tool.call" deviate. Replace with a formatter in TelemetryHelper and drop these constants. (opentelemetry.io)

Apply:

-    public const string SpanNameChat = "gen_ai.client.chat";
-    public const string SpanNameStream = "gen_ai.client.stream";
-    public const string SpanNameGenerateObject = "gen_ai.client.generate_object";
-    public const string SpanNameToolCall = "gen_ai.tool.call";
-    public const string SpanNameHttp = "http";
+    // Span names should be constructed dynamically:
+    // "{gen_ai.operation.name} {gen_ai.request.model}" and "execute_tool {gen_ai.tool.name}"
+    // Use TelemetryHelper to format; no fixed constants needed.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public const string SpanNameChat = "gen_ai.client.chat";
public const string SpanNameStream = "gen_ai.client.stream";
public const string SpanNameGenerateObject = "gen_ai.client.generate_object";
public const string SpanNameToolCall = "gen_ai.tool.call";
public const string SpanNameHttp = "http";
- public const string SpanNameChat = "gen_ai.client.chat";
- public const string SpanNameStream = "gen_ai.client.stream";
- public const string SpanNameGenerateObject = "gen_ai.client.generate_object";
- public const string SpanNameToolCall = "gen_ai.tool.call";
// Span names should be constructed dynamically:
// "{gen_ai.operation.name} {gen_ai.request.model}"
// and for tools:
// "execute_tool {gen_ai.tool.name}"
// Use TelemetryHelper to format these at runtime instead of fixed constants.
🤖 Prompt for AI Agents
In packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs around
lines 10-15, the file currently defines fixed span name constants (SpanNameChat,
SpanNameStream, SpanNameGenerateObject, SpanNameToolCall, SpanNameHttp) which
conflict with OTel guidance; remove these fixed constants and instead implement
span-name formatting in TelemetryHelper so client spans are generated as
"{gen_ai.operation.name} {gen_ai.request.model}" and tool spans as "execute_tool
{gen_ai.tool.name}". Update TelemetryHelper to expose methods that accept
operation name, request model, and tool name and return the correctly formatted
span names, and refactor call sites to use those formatter methods rather than
the removed constants.

Comment on lines +16 to +21
// ========== General Attributes ==========
public const string AttributeSystem = "gen_ai.system";
public const string AttributeOperationName = "gen_ai.operation.name";
public const string AttributeServerAddress = "server.address";

// ========== Request Attributes ==========

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add required provider attribute.

gen_ai.provider.name is Required for GenAI client spans. Add a constant. (opentelemetry.io)

     public const string AttributeSystem = "gen_ai.system";
     public const string AttributeOperationName = "gen_ai.operation.name";
     public const string AttributeServerAddress = "server.address";
+    public const string AttributeProviderName = "gen_ai.provider.name";
🤖 Prompt for AI Agents
In packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs around
lines 16 to 21, the OpenTelemetry GenAI required provider attribute is missing;
add a new public const string for "gen_ai.provider.name" (e.g., public const
string AttributeProviderName = "gen_ai.provider.name";) alongside the other
general attributes so spans include the required provider attribute.

Comment on lines +53 to +62
// ========== Tool Attributes ==========
public const string AttributeToolName = "gen_ai.tool.name";
public const string AttributeToolId = "gen_ai.tool.id";
public const string AttributeToolExecutionMode = "gen_ai.tool.execution_mode";
public const string AttributeToolArguments = "gen_ai.tool.arguments";
public const string AttributeToolResult = "gen_ai.tool.result";
public const string AttributeToolError = "gen_ai.tool.error";
public const string AttributeToolDuration = "gen_ai.tool.duration_ms";
public const string AttributeToolLoopIteration = "gen_ai.tool.loop_iteration";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use spec tool call keys and standard error.

Rename to gen_ai.tool.call.arguments/result/id and use error.type for failures. Execution duration should be the span duration, not a custom tag. (opentelemetry.io)

-    public const string AttributeToolName = "gen_ai.tool.name";
-    public const string AttributeToolId = "gen_ai.tool.id";
-    public const string AttributeToolExecutionMode = "gen_ai.tool.execution_mode";
-    public const string AttributeToolArguments = "gen_ai.tool.arguments";
-    public const string AttributeToolResult = "gen_ai.tool.result";
-    public const string AttributeToolError = "gen_ai.tool.error";
-    public const string AttributeToolDuration = "gen_ai.tool.duration_ms";
-    public const string AttributeToolLoopIteration = "gen_ai.tool.loop_iteration";
+    public const string AttributeToolName = "gen_ai.tool.name";
+    public const string AttributeToolCallId = "gen_ai.tool.call.id";
+    public const string AttributeToolExecutionMode = "gen_ai.tool.execution_mode"; // extension
+    public const string AttributeToolCallArguments = "gen_ai.tool.call.arguments";
+    public const string AttributeToolCallResult = "gen_ai.tool.call.result";
+    public const string AttributeErrorType = "error.type";
+    // Prefer span duration over a duration attribute
+    public const string AttributeToolLoopIteration = "gen_ai.tool.loop_iteration"; // extension
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// ========== Tool Attributes ==========
public const string AttributeToolName = "gen_ai.tool.name";
public const string AttributeToolId = "gen_ai.tool.id";
public const string AttributeToolExecutionMode = "gen_ai.tool.execution_mode";
public const string AttributeToolArguments = "gen_ai.tool.arguments";
public const string AttributeToolResult = "gen_ai.tool.result";
public const string AttributeToolError = "gen_ai.tool.error";
public const string AttributeToolDuration = "gen_ai.tool.duration_ms";
public const string AttributeToolLoopIteration = "gen_ai.tool.loop_iteration";
// ========== Tool Attributes ==========
public const string AttributeToolName = "gen_ai.tool.name";
public const string AttributeToolCallId = "gen_ai.tool.call.id";
public const string AttributeToolExecutionMode = "gen_ai.tool.execution_mode"; // extension
public const string AttributeToolCallArguments = "gen_ai.tool.call.arguments";
public const string AttributeToolCallResult = "gen_ai.tool.call.result";
public const string AttributeErrorType = "error.type";
// Prefer span duration over a duration attribute
public const string AttributeToolLoopIteration = "gen_ai.tool.loop_iteration"; // extension

Comment on lines +69 to +74
// ========== Event Names ==========
public const string EventPrompt = "gen_ai.client.input";
public const string EventCompletion = "gen_ai.client.output";
public const string EventStreamChunk = "gen_ai.client.stream.chunk";
public const string EventException = "exception";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use standard content event names.

Rename events to gen_ai.content.prompt and gen_ai.content.completion. Keep exception as is. (hexdocs.pm)

-    public const string EventPrompt = "gen_ai.client.input";
-    public const string EventCompletion = "gen_ai.client.output";
+    public const string EventPrompt = "gen_ai.content.prompt";
+    public const string EventCompletion = "gen_ai.content.completion";
     public const string EventStreamChunk = "gen_ai.client.stream.chunk";
     public const string EventException = "exception";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// ========== Event Names ==========
public const string EventPrompt = "gen_ai.client.input";
public const string EventCompletion = "gen_ai.client.output";
public const string EventStreamChunk = "gen_ai.client.stream.chunk";
public const string EventException = "exception";
// ========== Event Names ==========
public const string EventPrompt = "gen_ai.content.prompt";
public const string EventCompletion = "gen_ai.content.completion";
public const string EventStreamChunk = "gen_ai.client.stream.chunk";
public const string EventException = "exception";
🤖 Prompt for AI Agents
In packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs around
lines 69 to 74, the event constant names and values should follow the standard
content event names; rename the two constants so their identifiers and string
values reflect "gen_ai.content.prompt" and "gen_ai.content.completion" (keep the
EventException constant and its value "exception" unchanged). Update any
references in the project to use the new constant names if you change the
identifiers, or if you only change the string values, replace the literal values
accordingly so the exported semantic convention matches the referenced spec.

Comment on lines +86 to +92
// ========== Constant Values ==========
public const string SystemValue = "openrouter";
public const string OperationChat = "chat";
public const string OperationStream = "stream";
public const string OperationGenerateObject = "generate_object";
public const string ServerAddressValue = "openrouter.ai";
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Normalize operation names.

"stream" and "generate_object" aren’t standard operation names; prefer chat/generate_content/etc., and use events/metrics to indicate streaming or JSON output. Also add execute_tool/invoke_agent constants if used. (opentelemetry.io)

     public const string SystemValue = "openrouter";
     public const string OperationChat = "chat";
-    public const string OperationStream = "stream";
-    public const string OperationGenerateObject = "generate_object";
+    public const string OperationGenerateContent = "generate_content";
+    public const string OperationExecuteTool = "execute_tool";
+    public const string OperationInvokeAgent = "invoke_agent";
     public const string ServerAddressValue = "openrouter.ai";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// ========== Constant Values ==========
public const string SystemValue = "openrouter";
public const string OperationChat = "chat";
public const string OperationStream = "stream";
public const string OperationGenerateObject = "generate_object";
public const string ServerAddressValue = "openrouter.ai";
}
// ========== Constant Values ==========
public const string SystemValue = "openrouter";
public const string OperationChat = "chat";
public const string OperationGenerateContent = "generate_content";
public const string OperationExecuteTool = "execute_tool";
public const string OperationInvokeAgent = "invoke_agent";
public const string ServerAddressValue = "openrouter.ai";
}
🤖 Prompt for AI Agents
In packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs around
lines 86 to 92, normalize the operation name constants to follow the Gen‑AI
semantic conventions: rename OperationStream to OperationGenerateContent (or
OperationGenerate if your project prefers "generate_content") and rename
OperationGenerateObject to OperationGenerateContent/Generate as appropriate so
operation names use chat/generate_content conventions; keep OperationChat as
"chat"; add new public const string OperationExecuteTool = "execute_tool" and
public const string OperationInvokeAgent = "invoke_agent" if those actions are
used in the codebase, and update any references throughout the code to use the
new constant names. Ensure the string values match the OpenTelemetry spec (e.g.,
"generate_content", "execute_tool", "invoke_agent") and run a find/replace to
update usages.

Comment thread packages/dotnet-sdk/src/Observability/OpenTelemetryExtensions.cs
Add comprehensive OpenTelemetry observability to the streaming web API sample,
demonstrating real-world usage of the observability features with console exporter.

## Changes

### OpenTelemetry Setup (Program.cs)
- Added OpenTelemetry with service name "openrouter-streaming-api"
- Configured ASP.NET Core instrumentation (HTTP requests)
- Configured HttpClient instrumentation (outgoing HTTP)
- Added OpenRouter instrumentation with .AddOpenRouterInstrumentation()
- Using ConsoleExporter for easy local debugging

### Telemetry-Enabled Endpoints

**`/api/stream`** - Main streaming chat
- Full telemetry: prompts, completions, tool details, stream chunks
- CaptureStreamChunks = true (demonstrates high-volume logging)

**`/api/dashboard/stream`** - Dashboard builder
- Full telemetry: prompts, completions, tool details

**`/api/generate-object`** - Structured output
- Telemetry: prompts, completions
- Tracks schema validation

**`/api/triage-bug`** - Bug analysis
- Telemetry: prompts, completions

**`/api/demo-typed-tools`** - Typed tools demo
- Full telemetry: prompts, completions, tool details

### Documentation

Created OBSERVABILITY.md with:
- What's being logged and where
- How to view traces in console output
- Switching to Phoenix/Jaeger instructions
- Configuration options and presets
- Performance impact notes
- Privacy considerations
- Example trace output

### Dependencies Added

- OpenTelemetry 1.10.0
- OpenTelemetry.Exporter.Console 1.10.0
- OpenTelemetry.Extensions.Hosting 1.10.0
- OpenTelemetry.Instrumentation.AspNetCore 1.10.0

## Usage

```bash
export OPENROUTER_API_KEY="your-key"
cd samples/StreamingWebApiSample
dotnet run

# Make request, see traces in console
curl -X POST http://localhost:5000/api/stream \
  -H "Content-Type: application/json" \
  -d '{"message":"Calculate 15 * 23"}'
```

## Notes

- Console output shows full OpenTelemetry traces with all attributes and events
- /api/stream has CaptureStreamChunks enabled (high volume) for demonstration
- Easy to switch to Phoenix by changing exporter to OTLP
- All clients use opt-in telemetry configuration
Add OpenTelemetry.Instrumentation.Http 1.10.0 package to resolve build error.
The AddHttpClientInstrumentation() extension method requires this package.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
samples/StreamingWebApiSample/OBSERVABILITY.md (2)

76-103: Specify language identifier for code block.

The code block showing telemetry output lacks a language identifier, which violates markdown linting standards and prevents syntax highlighting.

Apply this diff to add the language identifier:

-```
+```text
 Activity.TraceId:            8a1f3c2e9b7d4a5f6e8c9b0a1d2e3f4g
 Activity.SpanId:             7b6c5d4e3f2a

156-156: Wrap bare URLs in angle brackets or format as markdown links.

Bare URLs at lines 156, 167, 237, and 238 should be either wrapped in angle brackets (<url>) or formatted as markdown links ([text](url)) to comply with markdown standards.

Apply these diffs:

 4. **View traces**: Open http://localhost:6006
+4. **View traces**: Open <http://localhost:6006>
 3. **View traces**: Open http://localhost:16686
+3. **View traces**: Open <http://localhost:16686>
-## Resources
-
-- **Full Guide**: [../../docs/OBSERVABILITY.md](../../docs/OBSERVABILITY.md)
-- **Arize Phoenix**: https://docs.arize.com/phoenix
-- **OpenTelemetry .NET**: https://opentelemetry.io/docs/languages/dotnet/
+## Resources
+
+- **Full Guide**: [../../docs/OBSERVABILITY.md](../../docs/OBSERVABILITY.md)
+- **Arize Phoenix**: <https://docs.arize.com/phoenix>
+- **OpenTelemetry .NET**: <https://opentelemetry.io/docs/languages/dotnet/>

Also applies to: 167-167, 237-237, 238-238

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 161ad83 and 26b2d79.

📒 Files selected for processing (3)
  • samples/StreamingWebApiSample/OBSERVABILITY.md (1 hunks)
  • samples/StreamingWebApiSample/Program.cs (7 hunks)
  • samples/StreamingWebApiSample/samples.csproj (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
samples/StreamingWebApiSample/Program.cs (4)
packages/dotnet-sdk/src/OpenRouterClient.cs (4)
  • OpenRouterClient (14-349)
  • OpenRouterClient (34-36)
  • OpenRouterClient (38-79)
  • OpenRouterClient (81-90)
packages/dotnet-sdk/src/Tools/ToolRegistrationExtensions.cs (8)
  • OpenRouterClient (10-16)
  • OpenRouterClient (18-24)
  • OpenRouterClient (26-32)
  • OpenRouterClient (34-40)
  • OpenRouterClient (42-55)
  • OpenRouterClient (57-66)
  • OpenRouterClient (72-147)
  • OpenRouterClient (149-213)
packages/dotnet-sdk/src/OpenRouterClientOptions.cs (1)
  • OpenRouterClientOptions (5-18)
packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
  • OpenRouterTelemetryOptions (7-122)
🪛 markdownlint-cli2 (0.18.1)
samples/StreamingWebApiSample/OBSERVABILITY.md

76-76: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


156-156: Bare URL used

(MD034, no-bare-urls)


167-167: Bare URL used

(MD034, no-bare-urls)


237-237: Bare URL used

(MD034, no-bare-urls)


238-238: Bare URL used

(MD034, no-bare-urls)

🔇 Additional comments (1)
samples/StreamingWebApiSample/samples.csproj (1)

11-14: Appropriate OpenTelemetry dependencies for the sample.

The added packages are well-chosen for demonstrating observability in an ASP.NET Core context: the console exporter enables quick testing, hosting extensions simplify integration, and ASP.NET Core instrumentation automatically captures HTTP spans. All versions align with the PR objectives.

Comment on lines 63 to +99
app.MapPost("/api/stream", async (ChatRequest chatRequest, HttpContext context) =>
{
var client = new OpenRouterClient(apiKey);
// ✨ Create client with observability enabled
var client = new OpenRouterClient(new OpenRouterClientOptions
{
ApiKey = apiKey,
Telemetry = new OpenRouterTelemetryOptions
{
EnableTelemetry = true,
CapturePrompts = true, // Log input prompts
CaptureCompletions = true, // Log outputs
CaptureToolDetails = true, // Log tool arguments/results
CaptureStreamChunks = true, // Log streaming chunks (high volume!)
MaxEventBodySize = 32_000 // 32KB limit
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Do not emit raw prompts/completions/tool payloads without sanitization.

Enabling CapturePrompts, CaptureCompletions, CaptureStreamChunks, and CaptureToolDetails in these sample clients causes user-entered content, model output, and tool arguments/results (including search queries or other PII) to be exported verbatim via the configured Console exporter. That is a high-risk privacy/compliance issue for anyone running the sample as-is in a shared environment. Please either disable these capture flags by default or wire up the SanitizePrompt / SanitizeCompletion / SanitizeToolArguments callbacks so sensitive data is redacted before it leaves the process.

Also applies to: 201-212, 441-451, 489-499, 523-533

🤖 Prompt for AI Agents
In samples/StreamingWebApiSample/Program.cs around lines 84 to 99 (and also
apply same changes to 201-212, 441-451, 489-499, 523-533): the telemetry options
currently capture raw prompts, completions, stream chunks and tool details which
can leak PII; either set CapturePrompts, CaptureCompletions,
CaptureStreamChunks, and CaptureToolDetails to false by default, or wire the
SanitizePrompt, SanitizeCompletion and SanitizeToolArguments callbacks to redact
sensitive fields before export (implement and reference a sanitizer that strips
PII and truncates large bodies, and ensure MaxEventBodySize is enforced), and
update comments to warn contributors about enabling raw capture in production.

Add full OpenTelemetry tracing to StreamingHandler to capture streaming metrics,
prompts, completions, and tool execution for the /api/stream endpoint.

## Changes

### StreamingHandler.StreamAsync()
- Create parent span 'gen_ai.client.stream' when telemetry enabled
- Set core attributes: operation, system, server, model
- Capture input prompts via TelemetryHelper.EnrichWithRequest()
- Error tracking with TelemetryHelper.RecordException()

### StreamAsyncInternal()
- Track streaming metrics in real-time:
  - Time to first token (TTFT)
  - Total chunks processed
  - Finish reason
  - Response model
  - Input/output token counts
- Optionally log individual stream chunks (if CaptureStreamChunks enabled)
- Calculate and report tokens per second
- Enrich activity with final metrics after streaming completes

### Metrics Captured

**Always (when telemetry enabled):**
- gen_ai.request.model: Requested model
- gen_ai.response.model: Actual model used
- gen_ai.stream.time_to_first_token_ms: TTFT metric
- gen_ai.stream.total_chunks: Number of chunks
- gen_ai.stream.duration_ms: Total streaming time
- gen_ai.stream.tokens_per_second: Throughput
- gen_ai.stream.finish_reason: How stream ended
- gen_ai.usage.input_tokens: Prompt tokens
- gen_ai.usage.output_tokens: Completion tokens
- http.response.status_code: 200

**Opt-in (CaptureStreamChunks):**
- gen_ai.client.stream.chunk events with chunk_index, text_delta, elapsed_ms

**Opt-in (CapturePrompts):**
- gen_ai.client.input event with full conversation history

## Impact

Now when hitting /api/stream, you'll see:

This matches the console output pattern users expect and enables full
observability of streaming LLM operations in Phoenix/Jaeger.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 26b2d79 and 7478265.

📒 Files selected for processing (1)
  • samples/StreamingWebApiSample/samples.csproj (1 hunks)

Comment on lines +11 to +15
<PackageReference Include="OpenTelemetry" Version="1.10.0" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.10.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.10.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.10.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.10.0" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify that System.Diagnostics.DiagnosticSource is accounted for.

The PR objectives mention System.Diagnostics.DiagnosticSource 9.0.0 as a core dependency, but this package is not explicitly referenced in the sample project. Ensure it's either included here, transitively available from the SDK dependencies, or verify it's not needed for this sample.

Additionally, confirm that all OpenTelemetry package versions (1.10.0) are current and aligned with the main SDK package versions.


🏁 Script executed:

#!/bin/bash
# Verify System.Diagnostics.DiagnosticSource is included in the main SDK and check package versions

# Check main SDK csproj for System.Diagnostics.DiagnosticSource and OpenTelemetry versions
fd -e csproj packages/dotnet-sdk -x grep -n "System.Diagnostics.DiagnosticSource\|OpenTelemetry.Api\|<PackageReference" {} +

# List all PackageReferences in main SDK for comparison
echo "=== Main SDK dependencies ==="
fd packages/dotnet-sdk -name "*.csproj" -x cat {} \; | grep -A 1 "<PackageReference"

Length of output: 642


🏁 Script executed:

#!/bin/bash
# Correctly search for csproj files in packages/dotnet-sdk directory
echo "=== Finding .csproj files in packages/dotnet-sdk ==="
fd . packages/dotnet-sdk -name "*.csproj"

echo ""
echo "=== Searching for System.Diagnostics.DiagnosticSource and OpenTelemetry references in main SDK ==="
fd . packages/dotnet-sdk -name "*.csproj" -exec grep -n "System.Diagnostics.DiagnosticSource\|OpenTelemetry" {} +

echo ""
echo "=== Sample project dependencies ==="
cat samples/StreamingWebApiSample/samples.csproj | grep -n "PackageReference"

Length of output: 1168


🏁 Script executed:

#!/bin/bash
# Find all csproj files
echo "=== Finding .csproj files in packages/dotnet-sdk ==="
find packages/dotnet-sdk -name "*.csproj" -type f

echo ""
echo "=== Searching main SDK csproj for System.Diagnostics.DiagnosticSource and OpenTelemetry ==="
find packages/dotnet-sdk -name "*.csproj" -type f -exec grep -H "System.Diagnostics.DiagnosticSource\|OpenTelemetry" {} \;

echo ""
echo "=== Full PackageReference list from main SDK ==="
find packages/dotnet-sdk -name "*.csproj" -type f -exec grep "PackageReference" {} + | head -20

Length of output: 875


🌐 Web query:

OpenTelemetry 1.10.0 latest version 2025

💡 Result:

Short answer: No — OpenTelemetry 1.10.0 is not the latest in 2025.

Details:

  • core 1.10.0 was released Nov 12, 2024. [1]
  • The project published newer releases in 2025 (e.g., core 1.11.x in Jan–Mar 2025), and ecosystem packages reached 1.12/1.13 in 2025 (NuGet shows 1.13.0 published Oct 22, 2025). [2][3]

If you want, I can list the latest stable release for a specific language (Java, .NET, Python, JS) as of today (Nov 8, 2025).


🌐 Web query:

OpenTelemetry.Instrumentation.AspNetCore latest version

💡 Result:

Latest NuGet version: OpenTelemetry.Instrumentation.AspNetCore 1.13.0 — published 2025-10-22. [1][2]

Sources:
[1] NuGet package page (OpenTelemetry.Instrumentation.AspNetCore).
[2] NuGet package page (opentelemetry.instrumentation.aspnetcore).


🌐 Web query:

System.Diagnostics.DiagnosticSource 9.0.0 transitive dependency OpenTelemetry

💡 Result:

Short answer: No — System.Diagnostics.DiagnosticSource 9.0.0 does not transitively depend on OpenTelemetry. The DiagnosticSource NuGet 9.0.0 package has no OpenTelemetry dependency (for net8/net9 targets it lists no dependencies; only older TFMs list System.Memory and Unsafe). [1]

OpenTelemetry .NET packages, conversely, rely on the Activity/DiagnosticSource APIs (they may reference or recommend installing System.Diagnostics.DiagnosticSource), so OpenTelemetry depends on DiagnosticSource, not the other way around. [2][3]

Sources:

  • System.Diagnostics.DiagnosticSource 9.0.0 (NuGet): package details / dependencies. [1]
  • OpenTelemetry .NET docs / guidance stating OpenTelemetry uses the System.Diagnostics Activity APIs and that DiagnosticSource is provided/managed by package dependencies. [2][3]

Update OpenTelemetry packages to current versions (1.13.0).

The OpenTelemetry packages in this sample are outdated. Version 1.10.0 (released Nov 12, 2024) has been superseded by 1.13.0 (released Oct 22, 2025). Update all OpenTelemetry package references from 1.10.0 to 1.13.0 to match the latest stable release.

Regarding System.Diagnostics.DiagnosticSource: This dependency is present in the main SDK and will likely be included transitively through the OpenTelemetry packages, so explicit inclusion in the sample is not required.

🤖 Prompt for AI Agents
In samples/StreamingWebApiSample/samples.csproj around lines 11 to 15, update
all OpenTelemetry PackageReference versions from 1.10.0 to 1.13.0; specifically
change the Version attributes for OpenTelemetry, OpenTelemetry.Exporter.Console,
OpenTelemetry.Extensions.Hosting, OpenTelemetry.Instrumentation.AspNetCore, and
OpenTelemetry.Instrumentation.Http to "1.13.0". Do not add an explicit
System.Diagnostics.DiagnosticSource package reference since it will be provided
transitively by the updated OpenTelemetry packages.

…o 1.11.0

Fix build errors and security vulnerability:

1. Remove try-catch block from StreamAsync() method
   - C# doesn't allow yield return inside try-catch blocks
   - Moved telemetry setup outside of try block
   - Activity will still be disposed properly via using statement

2. Upgrade OpenTelemetry packages from 1.10.0 to 1.11.0
   - Fixes security vulnerability GHSA-8785-wc3w-h8q6
   - Updates all OpenTelemetry packages in both SDK and sample

Resolves build errors:
- CS1626: Cannot yield a value in the body of a try block with a catch clause
Switch the sample from console output to Phoenix OTLP exporter so traces
appear in the Phoenix UI at http://localhost:6006.

## Changes

- Add OpenTelemetry.Exporter.OpenTelemetryProtocol package
- Replace .AddConsoleExporter() with .AddOtlpExporter()
- Configure endpoint: http://localhost:4317 (Phoenix gRPC)
- Protocol: OtlpExportProtocol.Grpc

## Usage

1. Start Phoenix: docker run -p 6006:6006 -p 4317:4317 arizephoenix/phoenix:latest
2. Run sample: dotnet run
3. Make request: curl -X POST http://localhost:5000/api/stream -H "Content-Type: application/json" -d '{"message":"hi"}'
4. View traces at: http://localhost:6006

Traces will now appear in Phoenix UI instead of console output.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (6)
samples/StreamingWebApiSample/samples.csproj (1)

11-16: Update OpenTelemetry packages to latest stable version (1.13.0).

While version 1.11.0 is an improvement over the original 1.10.0, the latest stable OpenTelemetry packages are at version 1.13.0 (released Oct 22, 2025). Update all six PackageReference entries to version 1.13.0 to stay current with the latest stable release and receive recent bug fixes and improvements.

Apply this diff:

-    <PackageReference Include="OpenTelemetry" Version="1.11.0" />
-    <PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.11.0" />
-    <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.11.0" />
-    <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.11.0" />
-    <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.11.0" />
-    <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.11.0" />
+    <PackageReference Include="OpenTelemetry" Version="1.13.0" />
+    <PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.13.0" />
+    <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.13.0" />
+    <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.13.0" />
+    <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.13.0" />
+    <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.13.0" />
samples/StreamingWebApiSample/Program.cs (5)

92-104: Raw capture without sanitization remains a privacy risk.

Enabling CapturePrompts, CaptureCompletions, CaptureToolDetails, and CaptureStreamChunks without sanitization callbacks can expose PII when running the sample. Developers copying this configuration may inadvertently leak sensitive data in production.

Consider either defaulting to a safer preset like OpenRouterTelemetryOptions.Production (which disables content capture) or demonstrating sanitization callbacks in at least one endpoint.

Example with Production preset:

         Telemetry = new OpenRouterTelemetryOptions
         {
             EnableTelemetry = true,
-            CapturePrompts = true,          // Log input prompts
-            CaptureCompletions = true,      // Log outputs
-            CaptureToolDetails = true,      // Log tool arguments/results
-            CaptureStreamChunks = true,     // Log streaming chunks (high volume!)
-            MaxEventBodySize = 32_000       // 32KB limit
+            // Use Production preset for privacy-safe defaults
+            // Captures metrics but not sensitive content
         }
+        // Or copy Production preset settings explicitly for clarity

Or demonstrate sanitization:

         Telemetry = new OpenRouterTelemetryOptions
         {
             EnableTelemetry = true,
             CapturePrompts = true,
             CaptureCompletions = true,
             CaptureToolDetails = true,
             CaptureStreamChunks = true,
-            MaxEventBodySize = 32_000
+            MaxEventBodySize = 32_000,
+            SanitizePrompt = prompt => Regex.Replace(prompt, @"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", "[EMAIL]"),
+            SanitizeCompletion = completion => Regex.Replace(completion, @"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", "[EMAIL]"),
+            SanitizeToolArguments = args => Regex.Replace(args, @"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", "[EMAIL]")
         }

207-217: Same sanitization concern applies here.

This endpoint also captures prompts, completions, and tool details without sanitization. Apply the same recommendations as the previous endpoint.


447-456: Sanitization concern applies to object generation endpoint.

Structured outputs may contain sensitive data. Consider using Production preset or adding sanitization callbacks.


495-504: Bug reports may contain particularly sensitive data.

Bug descriptions often include system internals, stack traces, and potentially user data. Sanitization is especially important for this endpoint.


528-538: Sanitization concern applies to typed tools demo.

Tool arguments and results may contain sensitive data. Apply the same sanitization recommendations.

🧹 Nitpick comments (1)
packages/dotnet-sdk/src/Internal/StreamingHandler.cs (1)

369-370: Consider capturing actual HTTP status code instead of hardcoding.

The status code is hardcoded to 200, which works because errors throw earlier (line 214), but it's fragile. If error handling changes, this hardcoded value could become incorrect.

Consider capturing and passing the actual response.StatusCode from line 213:

         using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
         using var reader = new StreamReader(stream);
+        var httpStatusCode = (int)response.StatusCode;

         System.Diagnostics.Stopwatch? stopwatch = null;
         var chunkIndex = 0;

Then use it at line 369:

-            currentActivity.SetTag(GenAiSemanticConventions.AttributeHttpStatusCode, 200);
+            currentActivity.SetTag(GenAiSemanticConventions.AttributeHttpStatusCode, httpStatusCode);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7478265 and 45d6799.

📒 Files selected for processing (4)
  • packages/dotnet-sdk/OpenRouter.NET.csproj (1 hunks)
  • packages/dotnet-sdk/src/Internal/StreamingHandler.cs (6 hunks)
  • samples/StreamingWebApiSample/Program.cs (7 hunks)
  • samples/StreamingWebApiSample/samples.csproj (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/dotnet-sdk/OpenRouter.NET.csproj
🧰 Additional context used
🧬 Code graph analysis (2)
packages/dotnet-sdk/src/Internal/StreamingHandler.cs (6)
packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
  • OpenRouterTelemetryOptions (7-122)
packages/dotnet-sdk/src/Internal/HttpRequestHandler.cs (2)
  • HttpRequestHandler (11-108)
  • HttpRequestHandler (21-37)
packages/dotnet-sdk/src/Tools/ToolManager.cs (2)
  • ToolManager (11-174)
  • ToolManager (19-23)
packages/dotnet-sdk/src/Observability/OpenRouterActivitySource.cs (1)
  • OpenRouterActivitySource (9-26)
packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs (1)
  • GenAiSemanticConventions (7-92)
packages/dotnet-sdk/src/Observability/TelemetryHelper.cs (2)
  • TelemetryHelper (11-314)
  • EnrichWithRequest (16-88)
samples/StreamingWebApiSample/Program.cs (3)
packages/dotnet-sdk/src/OpenRouterClient.cs (4)
  • OpenRouterClient (14-349)
  • OpenRouterClient (34-36)
  • OpenRouterClient (38-79)
  • OpenRouterClient (81-90)
packages/dotnet-sdk/src/OpenRouterClientOptions.cs (1)
  • OpenRouterClientOptions (5-18)
packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
  • OpenRouterTelemetryOptions (7-122)
🔇 Additional comments (6)
packages/dotnet-sdk/src/Internal/StreamingHandler.cs (4)

1-1: LGTM: Telemetry scaffolding is properly integrated.

The telemetry infrastructure is correctly added as an optional dependency with safe defaults (disabled by default).

Also applies to: 5-5, 20-20, 25-26, 31-31


43-65: LGTM: Span initialization follows OpenTelemetry patterns.

The conditional activity creation ensures zero overhead when telemetry is disabled. The initial enrichment properly captures operation context and request metadata.


203-204: LGTM: Activity context properly captured.

Capturing Activity.Current before streaming ensures consistent enrichment of the same span throughout the async enumeration, avoiding context loss.


224-231: LGTM: Metric tracking variables properly initialized.

The variables capture key streaming metrics (TTFT, chunk count, tokens, finish reason) that are aggregated and added to the span at completion.

samples/StreamingWebApiSample/Program.cs (2)

2-2: LGTM: OpenTelemetry configuration follows best practices.

The setup properly configures OTLP export to Phoenix with appropriate resource attributes and instrumentation. The localhost endpoint is appropriate for a sample application.

Also applies to: 10-12, 16-35


75-76: LGTM: Demonstrates telemetry opt-in pattern.

Using a simple client without telemetry options shows that observability is opt-in, which is the correct default behavior.

Comment on lines +314 to +322
if (_telemetryOptions.CaptureStreamChunks && !string.IsNullOrEmpty(chunkToYield.TextDelta))
{
currentActivity.AddEvent(new ActivityEvent(GenAiSemanticConventions.EventStreamChunk,
tags: new ActivityTagsCollection
{
{ "chunk_index", totalChunks },
{ "text_delta", chunkToYield.TextDelta },
{ "elapsed_ms", (long)chunkToYield.ElapsedTime.TotalMilliseconds }
}));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Apply sanitization to stream chunk text deltas.

When CaptureStreamChunks is enabled, the text delta is logged verbatim without sanitization. This can leak PII or sensitive model outputs. Consider applying the SanitizeCompletion callback before logging.

Apply this diff:

                     // Optionally log stream chunks
                     if (_telemetryOptions.CaptureStreamChunks && !string.IsNullOrEmpty(chunkToYield.TextDelta))
                     {
+                        var sanitizedDelta = _telemetryOptions.SanitizeCompletion?.Invoke(chunkToYield.TextDelta) ?? chunkToYield.TextDelta;
                         currentActivity.AddEvent(new ActivityEvent(GenAiSemanticConventions.EventStreamChunk,
                             tags: new ActivityTagsCollection
                             {
                                 { "chunk_index", totalChunks },
-                                { "text_delta", chunkToYield.TextDelta },
+                                { "text_delta", sanitizedDelta },
                                 { "elapsed_ms", (long)chunkToYield.ElapsedTime.TotalMilliseconds }
                             }));
                     }
🤖 Prompt for AI Agents
In packages/dotnet-sdk/src/Internal/StreamingHandler.cs around lines 314 to 322,
the code logs chunkToYield.TextDelta verbatim which can leak sensitive content;
call the configured sanitize callback before adding the "text_delta" tag: obtain
a sanitized string by invoking _telemetryOptions.SanitizeCompletion (if
non-null) with chunkToYield.TextDelta (handle nulls and exceptions safely,
falling back to an empty or original string), then use that sanitized value in
the ActivityEvent tag instead of the raw TextDelta; do not mutate the original
chunk object and ensure the call is null-safe.

Changes to StreamingWebApiSample/Program.cs:
- Use HTTP endpoint: http://localhost:4318/v1/traces (instead of gRPC port 4317)
- Switch protocol: HttpProtobuf (instead of Grpc)
- Keep ConsoleExporter for debugging alongside OTLP

Phoenix typically expects HTTP protocol on port 4318 for OTLP traces.
This should resolve the issue where traces appeared in console but not in Phoenix UI.
- Switch back to gRPC protocol on port 4317 (Phoenix only exposes this port)
- Add verbose logging for OpenTelemetry to debug OTLP export issues
- Configure batch export processor with explicit settings
- Keep ConsoleExporter for comparison

This will help diagnose why traces appear in console but not Phoenix.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
samples/StreamingWebApiSample/Program.cs (1)

93-105: Privacy issue: Raw prompts/completions/tool payloads emitted without sanitization.

This is the same issue flagged in the previous review. Enabling CapturePrompts, CaptureCompletions, CaptureStreamChunks, and CaptureToolDetails without wiring up the SanitizePrompt, SanitizeCompletion, and SanitizeToolArguments callbacks causes user-entered content, model output, and tool arguments/results to be exported verbatim. This is a high-risk privacy/compliance issue.

Either disable these capture flags by default or implement sanitization callbacks:

     var client = new OpenRouterClient(new OpenRouterClientOptions
     {
         ApiKey = apiKey,
         Telemetry = new OpenRouterTelemetryOptions
         {
             EnableTelemetry = true,
-            CapturePrompts = true,          // Log input prompts
-            CaptureCompletions = true,      // Log outputs
-            CaptureToolDetails = true,      // Log tool arguments/results
-            CaptureStreamChunks = true,     // Log streaming chunks (high volume!)
-            MaxEventBodySize = 32_000       // 32KB limit
+            CapturePrompts = false,         // Disable to avoid PII exposure
+            CaptureCompletions = false,     // Disable to avoid PII exposure
+            CaptureToolDetails = false,     // Disable to avoid PII exposure
+            CaptureStreamChunks = false,    // Disable to avoid PII exposure
+            MaxEventBodySize = 32_000,      // 32KB limit
+            // Or enable with sanitization:
+            // SanitizePrompt = (prompt) => "[REDACTED]",
+            // SanitizeCompletion = (completion) => "[REDACTED]",
+            // SanitizeToolArguments = (args) => "[REDACTED]"
         }
     });

Also applies to: 208-218, 448-457, 496-505, 529-539

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 45d6799 and 8596c66.

📒 Files selected for processing (1)
  • samples/StreamingWebApiSample/Program.cs (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
samples/StreamingWebApiSample/Program.cs (3)
packages/dotnet-sdk/src/OpenRouterClient.cs (4)
  • OpenRouterClient (14-349)
  • OpenRouterClient (34-36)
  • OpenRouterClient (38-79)
  • OpenRouterClient (81-90)
packages/dotnet-sdk/src/OpenRouterClientOptions.cs (1)
  • OpenRouterClientOptions (5-18)
packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
  • OpenRouterTelemetryOptions (7-122)
🔇 Additional comments (2)
samples/StreamingWebApiSample/Program.cs (2)

2-2: LGTM!

The OpenTelemetry and observability imports are appropriate for the telemetry integration being added.

Also applies to: 10-12


16-36: LGTM!

The OpenTelemetry configuration is appropriate for a sample application. The service name is descriptive, instrumentation covers the key layers (ASP.NET Core, HTTP client, OpenRouter), and the exporters (Console for debugging, OTLP for Phoenix) are correctly configured.

Comment on lines +76 to 77
// Simple client without telemetry for models endpoint
var client = new OpenRouterClient(apiKey);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Dispose the client after use.

The OpenRouterClient implements IDisposable and should be disposed to prevent resource leaks. When no custom HttpClient is provided via options, the client creates and owns an HttpClient internally that must be disposed.

Apply this diff to wrap the client in a using statement:

-    // Simple client without telemetry for models endpoint
-    var client = new OpenRouterClient(apiKey);
-    var models = await client.GetModelsAsync();
+    // Simple client without telemetry for models endpoint
+    using var client = new OpenRouterClient(apiKey);
+    var models = await client.GetModelsAsync();

Also applies to: 93-105, 208-218, 448-457, 496-505, 529-539

🤖 Prompt for AI Agents
In samples/StreamingWebApiSample/Program.cs around lines 76-77 (and similarly at
93-105, 208-218, 448-457, 496-505, 529-539), the OpenRouterClient is created but
not disposed; wrap each client creation in a using/using-declaration or
explicitly call Dispose so the internal HttpClient is released when no custom
HttpClient is provided. Replace the direct instantiation with a using (or add
await using if the client is IAsyncDisposable) scope around the code that uses
the client so the client is disposed automatically at the end of the scope.

claude and others added 4 commits November 8, 2025 13:03
- Enable Trace level logging for OpenTelemetry components
- Add console logger for debug output
- Filter specifically for OTLP exporter logs
- Simplify OTLP configuration to defaults

This will reveal any gRPC connection errors or export failures.
Switch from gRPC (port 4317) to HTTP (port 6006/v1/traces) for OTLP export.
Phoenix documentation shows HTTP endpoint: http://localhost:6006/v1/traces
This is often more reliable than gRPC for local development.
Changes:
- TelemetryHelper: Add OpenInference span attributes alongside events
  - Input messages: llm.input_messages.N.message.role/content
  - Output messages: llm.output_messages.N.message.role/content
  - Keep existing events for backward compatibility

- StreamingHandler: Accumulate complete streamed text
  - Track full completion text across all chunks
  - Add OpenInference output message attributes at stream end
  - Enables Phoenix UI to display complete responses

- Make TruncateIfNeeded internal for reuse in StreamingHandler

Phoenix UI displays span attributes but not event attributes. By adding
OpenInference conventions as span attributes, users can now view prompts,
completions, and all LLM interaction data directly in Phoenix.
- Added OpenTelemetry configuration to the StreamingWebApiSample for improved observability.
- Introduced telemetry options in OpenRouterClient initialization to enable detailed tracking of API interactions.
- Updated OpenTelemetryExtensions to include a default sampling method for activity tracing.
- Added necessary OpenTelemetry package references in the project file.

This update allows for better monitoring and debugging capabilities in the sample application.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (7)
samples/StreamingWebApiSample/Program.cs (6)

83-95: Dispose the client after use.

The OpenRouterClient implements IDisposable and should be disposed to prevent resource leaks. When no custom HttpClient is provided, the client creates and owns an HttpClient internally that must be disposed.

Apply this diff to wrap the client in a using statement:

-    // Simple client without telemetry for models endpoint
-    var client = new OpenRouterClient(apiKey);
+    // Simple client without telemetry for models endpoint
+    using var client = new OpenRouterClient(apiKey);

99-112: Do not emit raw prompts/completions/tool payloads without sanitization.

Enabling CapturePrompts, CaptureCompletions, CaptureStreamChunks, and CaptureToolDetails in this sample endpoint causes user-entered content, model output, and tool arguments/results (including search queries or other PII) to be exported verbatim via the configured Console exporter. This is a high-risk privacy/compliance issue. Additionally, the client is not disposed, leading to resource leaks.

Please either:

  1. Disable these capture flags by default, or
  2. Wire up the SanitizePrompt, SanitizeCompletion, and SanitizeToolArguments callbacks to redact sensitive data before export, or
  3. Add prominent warnings in comments about the privacy implications of enabling raw capture in production

Also, wrap the client in a using statement to ensure proper disposal.

Apply this diff to address both issues:

-    // ✨ Create client with observability enabled
-    var client = new OpenRouterClient(new OpenRouterClientOptions
+    // ✨ Create client with observability enabled
+    // ⚠️ WARNING: This sample captures raw prompts/completions for demonstration.
+    // In production, disable these flags or implement sanitization callbacks.
+    using var client = new OpenRouterClient(new OpenRouterClientOptions
     {
         ApiKey = apiKey,
         Telemetry = new OpenRouterTelemetryOptions
         {
             EnableTelemetry = true,
-            CapturePrompts = true,          // Log input prompts
-            CaptureCompletions = true,      // Log outputs
-            CaptureToolDetails = true,      // Log tool arguments/results
-            CaptureStreamChunks = true,     // Log streaming chunks (high volume!)
+            CapturePrompts = false,         // ⚠️ May log PII - enable only with sanitization
+            CaptureCompletions = false,     // ⚠️ May log PII - enable only with sanitization
+            CaptureToolDetails = false,     // ⚠️ May log sensitive data
+            CaptureStreamChunks = false,    // ⚠️ High volume + PII risk
             MaxEventBodySize = 32_000       // 32KB limit
         }
     });

214-225: Do not emit raw prompts/completions/tool payloads without sanitization.

This endpoint captures CapturePrompts, CaptureCompletions, and CaptureToolDetails without sanitization callbacks, risking PII leakage via the Console exporter. Additionally, the client is not disposed properly.

Apply this diff:

-    // ✨ Create client with observability enabled
-    var client = new OpenRouterClient(new OpenRouterClientOptions
+    // ✨ Create client with observability enabled
+    // ⚠️ WARNING: Raw capture disabled by default. Enable only with sanitization in production.
+    using var client = new OpenRouterClient(new OpenRouterClientOptions
     {
         ApiKey = apiKey,
         Telemetry = new OpenRouterTelemetryOptions
         {
             EnableTelemetry = true,
-            CapturePrompts = true,
-            CaptureCompletions = true,
-            CaptureToolDetails = true
+            CapturePrompts = false,
+            CaptureCompletions = false,
+            CaptureToolDetails = false
         }
     });

454-464: Do not emit raw prompts/completions without sanitization.

This endpoint captures CapturePrompts and CaptureCompletions without sanitization, risking PII exposure. The client also lacks proper disposal.

Apply this diff:

-    // ✨ Create client with observability for object generation
-    var client = new OpenRouterClient(new OpenRouterClientOptions
+    // ✨ Create client with observability for object generation
+    using var client = new OpenRouterClient(new OpenRouterClientOptions
     {
         ApiKey = apiKey,
         Telemetry = new OpenRouterTelemetryOptions
         {
             EnableTelemetry = true,
-            CapturePrompts = true,
-            CaptureCompletions = true
+            CapturePrompts = false,
+            CaptureCompletions = false
         }
     });

502-512: Do not emit raw prompts/completions without sanitization.

Bug reports may contain sensitive system information or user data. Capturing prompts and completions without sanitization poses privacy risks. The client also needs proper disposal.

Apply this diff:

-    // ✨ Create client with observability for bug triage
-    var client = new OpenRouterClient(new OpenRouterClientOptions
+    // ✨ Create client with observability for bug triage
+    using var client = new OpenRouterClient(new OpenRouterClientOptions
     {
         ApiKey = apiKey,
         Telemetry = new OpenRouterTelemetryOptions
         {
             EnableTelemetry = true,
-            CapturePrompts = true,
-            CaptureCompletions = true
+            CapturePrompts = false,
+            CaptureCompletions = false
         }
     });

535-546: Do not emit raw prompts/completions/tool payloads without sanitization.

This demo endpoint captures prompts, completions, and tool details without sanitization, creating PII leakage risk. The client also requires proper disposal.

Apply this diff:

-    // ✨ Create client with observability for typed tools demo
-    var client = new OpenRouterClient(new OpenRouterClientOptions
+    // ✨ Create client with observability for typed tools demo
+    using var client = new OpenRouterClient(new OpenRouterClientOptions
     {
         ApiKey = apiKey,
         Telemetry = new OpenRouterTelemetryOptions
         {
             EnableTelemetry = true,
-            CapturePrompts = true,
-            CaptureCompletions = true,
-            CaptureToolDetails = true
+            CapturePrompts = false,
+            CaptureCompletions = false,
+            CaptureToolDetails = false
         }
     });
packages/dotnet-sdk/src/Internal/StreamingHandler.cs (1)

315-324: Apply sanitization to stream chunk text deltas.

The text delta is logged verbatim without applying the SanitizeCompletion callback, which can leak sensitive content.

Apply this diff:

                 // Optionally log stream chunks
                 if (_telemetryOptions.CaptureStreamChunks && !string.IsNullOrEmpty(chunkToYield.TextDelta))
                 {
+                    var sanitizedDelta = _telemetryOptions.SanitizeCompletion?.Invoke(chunkToYield.TextDelta) ?? chunkToYield.TextDelta;
                     currentActivity.AddEvent(new ActivityEvent(GenAiSemanticConventions.EventStreamChunk,
                         tags: new ActivityTagsCollection
                         {
                             { "chunk_index", totalChunks },
-                            { "text_delta", chunkToYield.TextDelta },
+                            { "text_delta", sanitizedDelta },
                             { "elapsed_ms", (long)chunkToYield.ElapsedTime.TotalMilliseconds }
                         }));
                 }
🧹 Nitpick comments (1)
packages/dotnet-sdk/src/Observability/TelemetryHelper.cs (1)

82-82: Optimize repeated GetMessageContent calls.

The current code calls GetMessageContent twice when the sanitization callback is null—once for the sanitize path and once as the fallback. Consider storing the result in a variable first to avoid the redundant call.

Example for line 82:

- var content = options.SanitizePrompt?.Invoke(GetMessageContent(message)) ?? GetMessageContent(message);
+ var rawContent = GetMessageContent(message);
+ var content = options.SanitizePrompt?.Invoke(rawContent) ?? rawContent;

Apply similar changes to lines 174, 276, and 298.

Also applies to: 174-174, 276-276, 298-298

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8596c66 and 04902bc.

📒 Files selected for processing (3)
  • packages/dotnet-sdk/src/Internal/StreamingHandler.cs (6 hunks)
  • packages/dotnet-sdk/src/Observability/TelemetryHelper.cs (1 hunks)
  • samples/StreamingWebApiSample/Program.cs (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
packages/dotnet-sdk/src/Internal/StreamingHandler.cs (5)
packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
  • OpenRouterTelemetryOptions (7-122)
packages/dotnet-sdk/src/Internal/HttpRequestHandler.cs (2)
  • HttpRequestHandler (11-108)
  • HttpRequestHandler (21-37)
packages/dotnet-sdk/src/Observability/OpenRouterActivitySource.cs (1)
  • OpenRouterActivitySource (9-26)
packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs (1)
  • GenAiSemanticConventions (7-92)
packages/dotnet-sdk/src/Observability/TelemetryHelper.cs (3)
  • TelemetryHelper (11-352)
  • EnrichWithRequest (16-103)
  • TruncateIfNeeded (338-351)
samples/StreamingWebApiSample/Program.cs (4)
packages/dotnet-sdk/src/OpenRouterClient.cs (4)
  • OpenRouterClient (14-349)
  • OpenRouterClient (34-36)
  • OpenRouterClient (38-79)
  • OpenRouterClient (81-90)
packages/dotnet-sdk/src/Tools/ToolRegistrationExtensions.cs (8)
  • OpenRouterClient (10-16)
  • OpenRouterClient (18-24)
  • OpenRouterClient (26-32)
  • OpenRouterClient (34-40)
  • OpenRouterClient (42-55)
  • OpenRouterClient (57-66)
  • OpenRouterClient (72-147)
  • OpenRouterClient (149-213)
packages/dotnet-sdk/src/OpenRouterClientOptions.cs (1)
  • OpenRouterClientOptions (5-18)
packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
  • OpenRouterTelemetryOptions (7-122)
packages/dotnet-sdk/src/Observability/TelemetryHelper.cs (4)
packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
  • OpenRouterTelemetryOptions (7-122)
packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs (1)
  • GenAiSemanticConventions (7-92)
packages/dotnet-sdk/src/Models/ChatCompletionResponse.cs (1)
  • ChatCompletionResponse (5-27)
packages/dotnet-sdk/src/Models/Message.cs (1)
  • ContentPart (48-52)
🔇 Additional comments (1)
samples/StreamingWebApiSample/Program.cs (1)

2-2: LGTM: OpenTelemetry setup is well-configured for development.

The observability wiring correctly initializes Phoenix OTLP exporter with HTTP protocol, enables relevant instrumentations (ASP.NET Core, HttpClient, OpenRouter), and includes console output for debugging. The verbose logging levels are appropriate for a sample application.

Also applies to: 10-43

Comment on lines +380 to +392
if (_telemetryOptions.CaptureCompletions && completeTextBuilder.Length > 0)
{
var completeText = completeTextBuilder.ToString();
var truncated = TelemetryHelper.TruncateIfNeeded(completeText, _telemetryOptions.MaxEventBodySize);

currentActivity.SetTag("llm.output_messages.0.message.role", "assistant");
currentActivity.SetTag("llm.output_messages.0.message.content", truncated);

if (!string.IsNullOrEmpty(finishReason))
{
currentActivity.SetTag("llm.output_messages.0.finish_reason", finishReason);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Apply sanitization to complete output text before logging.

The accumulated complete text is added to span attributes without applying the SanitizeCompletion callback. This can leak PII or sensitive model outputs. The truncation on line 383 only limits size but does not sanitize content.

Apply this diff:

             // OpenInference: Add complete output message as span attributes for Phoenix UI
             if (_telemetryOptions.CaptureCompletions && completeTextBuilder.Length > 0)
             {
                 var completeText = completeTextBuilder.ToString();
+                var sanitizedText = _telemetryOptions.SanitizeCompletion?.Invoke(completeText) ?? completeText;
-                var truncated = TelemetryHelper.TruncateIfNeeded(completeText, _telemetryOptions.MaxEventBodySize);
+                var truncated = TelemetryHelper.TruncateIfNeeded(sanitizedText, _telemetryOptions.MaxEventBodySize);
 
                 currentActivity.SetTag("llm.output_messages.0.message.role", "assistant");
                 currentActivity.SetTag("llm.output_messages.0.message.content", truncated);
 
                 if (!string.IsNullOrEmpty(finishReason))
                 {
                     currentActivity.SetTag("llm.output_messages.0.finish_reason", finishReason);
                 }
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (_telemetryOptions.CaptureCompletions && completeTextBuilder.Length > 0)
{
var completeText = completeTextBuilder.ToString();
var truncated = TelemetryHelper.TruncateIfNeeded(completeText, _telemetryOptions.MaxEventBodySize);
currentActivity.SetTag("llm.output_messages.0.message.role", "assistant");
currentActivity.SetTag("llm.output_messages.0.message.content", truncated);
if (!string.IsNullOrEmpty(finishReason))
{
currentActivity.SetTag("llm.output_messages.0.finish_reason", finishReason);
}
}
if (_telemetryOptions.CaptureCompletions && completeTextBuilder.Length > 0)
{
var completeText = completeTextBuilder.ToString();
var sanitizedText = _telemetryOptions.SanitizeCompletion?.Invoke(completeText) ?? completeText;
var truncated = TelemetryHelper.TruncateIfNeeded(sanitizedText, _telemetryOptions.MaxEventBodySize);
currentActivity.SetTag("llm.output_messages.0.message.role", "assistant");
currentActivity.SetTag("llm.output_messages.0.message.content", truncated);
if (!string.IsNullOrEmpty(finishReason))
{
currentActivity.SetTag("llm.output_messages.0.finish_reason", finishReason);
}
}
🤖 Prompt for AI Agents
In packages/dotnet-sdk/src/Internal/StreamingHandler.cs around lines 380 to 392,
the code writes the accumulated completion text to activity tags without calling
the SanitizeCompletion callback, risking PII leakage; update the flow to first
run completeText through the configured SanitizeCompletion callback (or a no-op
if null), then apply TelemetryHelper.TruncateIfNeeded to the sanitized result,
and use that sanitized+truncated string when setting the
llm.output_messages.0.message.content and related tags (keep existing
finish_reason handling).

Comment on lines +338 to +351
internal static string TruncateIfNeeded(string content, int maxSize)
{
if (maxSize <= 0) return content;

var bytes = Encoding.UTF8.GetBytes(content);
if (bytes.Length <= maxSize)
{
return content;
}

// Truncate to maxSize bytes and add indicator
var truncated = Encoding.UTF8.GetString(bytes, 0, Math.Max(0, maxSize - 20));
return truncated + "... [TRUNCATED]";
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

UTF-8 truncation can corrupt multi-byte characters.

Line 349 truncates at an arbitrary byte boundary, which can split UTF-8 multi-byte characters and produce invalid output or replacement characters (�). This corrupts the truncated content.

Apply this diff to use character-safe truncation:

 internal static string TruncateIfNeeded(string content, int maxSize)
 {
     if (maxSize <= 0) return content;
 
     var bytes = Encoding.UTF8.GetBytes(content);
     if (bytes.Length <= maxSize)
     {
         return content;
     }
 
-    // Truncate to maxSize bytes and add indicator
-    var truncated = Encoding.UTF8.GetString(bytes, 0, Math.Max(0, maxSize - 20));
-    return truncated + "... [TRUNCATED]";
+    // Truncate by character count to avoid splitting multi-byte UTF-8 sequences
+    const string suffix = "... [TRUNCATED]";
+    var suffixBytes = Encoding.UTF8.GetByteCount(suffix);
+    var targetSize = Math.Max(0, maxSize - suffixBytes);
+    
+    // Walk back from targetSize to find a safe character boundary
+    var decoder = Encoding.UTF8.GetDecoder();
+    var charCount = decoder.GetCharCount(bytes, 0, targetSize, false);
+    var truncated = content.Substring(0, charCount);
+    return truncated + suffix;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
internal static string TruncateIfNeeded(string content, int maxSize)
{
if (maxSize <= 0) return content;
var bytes = Encoding.UTF8.GetBytes(content);
if (bytes.Length <= maxSize)
{
return content;
}
// Truncate to maxSize bytes and add indicator
var truncated = Encoding.UTF8.GetString(bytes, 0, Math.Max(0, maxSize - 20));
return truncated + "... [TRUNCATED]";
}
internal static string TruncateIfNeeded(string content, int maxSize)
{
if (maxSize <= 0) return content;
var bytes = Encoding.UTF8.GetBytes(content);
if (bytes.Length <= maxSize)
{
return content;
}
// Truncate by character count to avoid splitting multi-byte UTF-8 sequences
const string suffix = "... [TRUNCATED]";
var suffixBytes = Encoding.UTF8.GetByteCount(suffix);
var targetSize = Math.Max(0, maxSize - suffixBytes);
// Walk back from targetSize to find a safe character boundary
var decoder = Encoding.UTF8.GetDecoder();
var charCount = decoder.GetCharCount(bytes, 0, targetSize, false);
var truncated = content.Substring(0, charCount);
return truncated + suffix;
}

This change ensures proper formatting and adherence to coding standards by adding a newline at the end of the file.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (7)
samples/StreamingWebApiSample/Program.cs (7)

91-93: Dispose the client after use.

The OpenRouterClient implements IDisposable and should be disposed to prevent resource leaks. When no custom HttpClient is provided, the client creates and owns an HttpClient internally that must be disposed.

Apply this diff to wrap the client in a using statement:

-    // Simple client without telemetry for models endpoint
-    var client = new OpenRouterClient(apiKey);
+    // Simple client without telemetry for models endpoint
+    using var client = new OpenRouterClient(apiKey);

107-120: Do not emit raw prompts/completions/tool payloads without sanitization.

Enabling CapturePrompts, CaptureCompletions, CaptureStreamChunks, and CaptureToolDetails causes user-entered content, model output, and tool arguments/results to be exported verbatim via the configured Console and OTLP exporters. This is a high-risk privacy/compliance issue. Wire up the SanitizePrompt, SanitizeCompletion, and SanitizeToolArguments callbacks to redact sensitive data before export.

Apply this diff to add sanitization callbacks:

     var client = new OpenRouterClient(new OpenRouterClientOptions
     {
         ApiKey = apiKey,
         Telemetry = new OpenRouterTelemetryOptions
         {
             EnableTelemetry = true,
             CapturePrompts = true,
             CaptureCompletions = true,
             CaptureToolDetails = true,
             CaptureStreamChunks = true,
-            MaxEventBodySize = 32_000
+            MaxEventBodySize = 32_000,
+            SanitizePrompt = prompt => "[REDACTED]",
+            SanitizeCompletion = completion => "[REDACTED]",
+            SanitizeToolArguments = args => "[REDACTED]"
         }
     });

Or disable raw capture for production use:

         Telemetry = new OpenRouterTelemetryOptions
         {
             EnableTelemetry = true,
-            CapturePrompts = true,
-            CaptureCompletions = true,
-            CaptureToolDetails = true,
-            CaptureStreamChunks = true,
+            CapturePrompts = false,
+            CaptureCompletions = false,
+            CaptureToolDetails = false,
+            CaptureStreamChunks = false,
             MaxEventBodySize = 32_000
         }

108-120: Dispose the client after use.

The client is not disposed, which will leak resources. Wrap it in a using statement.

Apply this diff:

-    // ✨ Create client with observability enabled
-    var client = new OpenRouterClient(new OpenRouterClientOptions
+    // ✨ Create client with observability enabled
+    using var client = new OpenRouterClient(new OpenRouterClientOptions

222-233: Apply sanitization and disposal fixes.

This endpoint has the same PII/sanitization and disposal issues as the /api/stream endpoint.

Apply both fixes:

-    // ✨ Create client with observability enabled
-    var client = new OpenRouterClient(new OpenRouterClientOptions
+    // ✨ Create client with observability enabled
+    using var client = new OpenRouterClient(new OpenRouterClientOptions
     {
         ApiKey = apiKey,
         Telemetry = new OpenRouterTelemetryOptions
         {
             EnableTelemetry = true,
-            CapturePrompts = true,
-            CaptureCompletions = true,
-            CaptureToolDetails = true
+            CapturePrompts = false,
+            CaptureCompletions = false,
+            CaptureToolDetails = false
         }
     });

462-472: Apply sanitization and disposal fixes.

This endpoint captures prompts and completions without sanitization and does not dispose the client.

Apply both fixes:

-    // ✨ Create client with observability for object generation
-    var client = new OpenRouterClient(new OpenRouterClientOptions
+    // ✨ Create client with observability for object generation
+    using var client = new OpenRouterClient(new OpenRouterClientOptions
     {
         ApiKey = apiKey,
         Telemetry = new OpenRouterTelemetryOptions
         {
             EnableTelemetry = true,
-            CapturePrompts = true,
-            CaptureCompletions = true
+            CapturePrompts = false,
+            CaptureCompletions = false
         }
     });

510-520: Apply sanitization and disposal fixes.

This endpoint captures prompts and completions without sanitization and does not dispose the client.

Apply both fixes:

-    // ✨ Create client with observability for bug triage
-    var client = new OpenRouterClient(new OpenRouterClientOptions
+    // ✨ Create client with observability for bug triage
+    using var client = new OpenRouterClient(new OpenRouterClientOptions
     {
         ApiKey = apiKey,
         Telemetry = new OpenRouterTelemetryOptions
         {
             EnableTelemetry = true,
-            CapturePrompts = true,
-            CaptureCompletions = true
+            CapturePrompts = false,
+            CaptureCompletions = false
         }
     });

543-554: Apply sanitization and disposal fixes.

This endpoint has the same PII/sanitization and disposal issues as other endpoints.

Apply both fixes:

-    // ✨ Create client with observability for typed tools demo
-    var client = new OpenRouterClient(new OpenRouterClientOptions
+    // ✨ Create client with observability for typed tools demo
+    using var client = new OpenRouterClient(new OpenRouterClientOptions
     {
         ApiKey = apiKey,
         Telemetry = new OpenRouterTelemetryOptions
         {
             EnableTelemetry = true,
-            CapturePrompts = true,
-            CaptureCompletions = true,
-            CaptureToolDetails = true
+            CapturePrompts = false,
+            CaptureCompletions = false,
+            CaptureToolDetails = false
         }
     });
🧹 Nitpick comments (1)
samples/StreamingWebApiSample/Program.cs (1)

16-20: Consider environment-specific logging configuration.

The Trace-level logging for OpenTelemetry is useful for debugging but will generate excessive output in production environments. Consider wrapping this configuration in an environment check.

Apply this diff to enable verbose logging only in Development:

-// Enable verbose logging for OpenTelemetry debugging
-builder.Logging.SetMinimumLevel(LogLevel.Debug);
-builder.Logging.AddConsole();
-builder.Logging.AddFilter("OpenTelemetry", LogLevel.Trace);
-builder.Logging.AddFilter("OpenTelemetry.Exporter.OpenTelemetryProtocol", LogLevel.Trace);
+// Enable verbose logging for OpenTelemetry debugging (Development only)
+if (builder.Environment.IsDevelopment())
+{
+    builder.Logging.SetMinimumLevel(LogLevel.Debug);
+    builder.Logging.AddConsole();
+    builder.Logging.AddFilter("OpenTelemetry", LogLevel.Trace);
+    builder.Logging.AddFilter("OpenTelemetry.Exporter.OpenTelemetryProtocol", LogLevel.Trace);
+}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 04902bc and 0fd2b05.

📒 Files selected for processing (3)
  • packages/dotnet-sdk/src/Observability/OpenTelemetryExtensions.cs (1 hunks)
  • samples/StreamingWebApiSample/Program.cs (7 hunks)
  • tests/OpenRouter.NET.Tests/SchemaValidationTests.cs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/dotnet-sdk/src/Observability/OpenTelemetryExtensions.cs
🧰 Additional context used
🧬 Code graph analysis (1)
samples/StreamingWebApiSample/Program.cs (3)
packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
  • OpenRouterTelemetryOptions (7-122)
packages/dotnet-sdk/src/OpenRouterClient.cs (5)
  • List (97-97)
  • OpenRouterClient (14-349)
  • OpenRouterClient (34-36)
  • OpenRouterClient (38-79)
  • OpenRouterClient (81-90)
packages/dotnet-sdk/src/OpenRouterClientOptions.cs (1)
  • OpenRouterClientOptions (5-18)
🔇 Additional comments (1)
tests/OpenRouter.NET.Tests/SchemaValidationTests.cs (1)

410-410: Standard formatting: newline at end of file.

Adding a newline at the end of the file is a standard practice and aligns with POSIX conventions.

Comment on lines +78 to +84
var telemetryOptions = new OpenRouterTelemetryOptions
{
EnableTelemetry = true,
CapturePrompts = true,
CaptureCompletions = true,
CaptureToolDetails = true
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove or use the unused telemetryOptions variable.

This variable is defined but never referenced. Each endpoint creates its own inline telemetry options instead.

Apply this diff to remove the unused variable:

-var telemetryOptions = new OpenRouterTelemetryOptions
-{
-    EnableTelemetry = true,
-    CapturePrompts = true,
-    CaptureCompletions = true,
-    CaptureToolDetails = true
-};
-
 var conversationStore = new ConcurrentDictionary<string, List<Message>>();

Alternatively, if you intended to reuse this configuration, reference it in the endpoints:

var client = new OpenRouterClient(new OpenRouterClientOptions
{
    ApiKey = apiKey,
    Telemetry = telemetryOptions  // Reuse the shared config
});
🤖 Prompt for AI Agents
In samples/StreamingWebApiSample/Program.cs around lines 78 to 84, the variable
telemetryOptions is defined but never used; either remove this unused variable
or reuse it by passing it into the OpenRouterClient configuration for each
endpoint (set Telemetry = telemetryOptions on new OpenRouterClientOptions).
Update all endpoint client constructions to reference the shared
telemetryOptions if you want reuse, otherwise delete the telemetryOptions
declaration to eliminate the unused variable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants