Skip to content

fix(core): capture provider-reported OpenRouter costs#1155

Merged
omeraplak merged 5 commits intomainfrom
fix/openrouter-cost-observability
Mar 16, 2026
Merged

fix(core): capture provider-reported OpenRouter costs#1155
omeraplak merged 5 commits intomainfrom
fix/openrouter-cost-observability

Conversation

@omeraplak
Copy link
Copy Markdown
Member

@omeraplak omeraplak commented Mar 16, 2026

PR Checklist

Please check if your PR fulfills the following requirements:

Bugs / Features

What is the current behavior?

OpenRouter provider-reported billed costs are not forwarded into VoltAgent observability spans, so downstream consumers can only fall back to token and model-based pricing. The observability docs also do not explain provider-supplied costs or custom hook-based cost reporting.

What is the new behavior?

VoltAgent now extracts OpenRouter usage cost metadata from provider responses and records it on both the LLM span and the root agent span. The observability docs now cover provider-supplied OpenRouter costs and custom cost integration through onEnd hooks.

fixes (issue)

N/A

Notes for reviewers

  • Added an observability test that verifies usage.cost and usage.cost_details.upstream_inference_* on both span levels.
  • Added docs for OpenRouter usage accounting and custom cost attributes written from VoltAgent hooks.
  • Companion backend changes that consume these attributes live in api/voltagent-api and are not part of this repo.
  • Validation: pnpm -C packages/core test:single src/agent/agent-observability.spec.ts

Summary by cubic

Capture OpenRouter’s provider-reported costs in @voltagent/core observability spans and prefer them over static pricing to improve VoltOps cost accuracy, especially for router/BYOK flows.

  • Bug Fixes

    • Record OpenRouter cost metadata from providerMetadata on LLM and root spans: usage.cost, usage.is_byok, and usage.cost_details.{upstream_inference_cost, upstream_inference_input_cost, upstream_inference_output_cost}.
    • Prefer upstream_inference_cost > usage.cost; fall back to model/token pricing (supports camelCase and snake_case). Preserve root span cost even if post-processing fails after a successful model call.
    • Add ai.model.provider derived from ai.model.name for better grouping.
    • Expand tests for spans and error paths; docs cover cost precedence, OpenRouter setup (usage: { include: true }), custom cost hooks, and when to safely set custom cost attributes.
  • New Features

    • Add examples/with-openrouter showing integration with @openrouter/ai-sdk-provider, usage: { include: true }, Hono server via @voltagent/server-hono, and LibSQL memory via @voltagent/libsql.

Written for commit ef2311b. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Provider-reported OpenRouter costs and model provider info are captured and surfaced on LLM and root observability spans (detailed cost attributes recorded).
  • Documentation

    • Expanded LLM usage & cost docs: provider-supplied costs precedence, OpenRouter guidance, custom cost hooks, additional telemetry attributes, and updated usage display behavior.
  • Tests

    • Added observability tests verifying provider cost attributes are recorded and preserved on spans.
  • Examples

    • New OpenRouter example with env sample, README, and project setup demonstrating integration.

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 16, 2026

🦋 Changeset detected

Latest commit: ef2311b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@voltagent/core Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@joggrbot

This comment has been minimized.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 16, 2026

📝 Walkthrough

Walkthrough

Parse OpenRouter provider usage from providerMetadata, extract cost and cost details, thread providerMetadata through generation and streaming finalizers, record provider-reported costs on LLM and root agent spans, and document provider-priority and custom cost hooks.

Changes

Cohort / File(s) Summary
Release & Changelog
./.changeset/fair-seahorses-ring.md
Patch release note announcing provider-cost observability for OpenRouter.
Docs: Observability
website/observability/llm-usage-and-costs.md
Document provider-supplied costs precedence, OpenRouter-specific attributes, custom onEnd hook guidance, and raw OpenTelemetry attributes for cost reporting and fallback pricing.
Agent - Cost Extraction & Propagation
packages/core/src/agent/agent.ts
Add OpenRouter parsing helpers and OpenRouterUsageCost type; thread providerMetadata into finalize flows; add recordProviderCost and recordRootSpanUsageAndProviderCost; surface provider cost on LLM and root spans.
Observability: Trace Context
packages/core/src/agent/open-telemetry/trace-context.ts
Extract ai.model.provider from provider/model modelName forms and set span attribute.
Tests: Observability
packages/core/src/agent/agent-observability.spec.ts
New tests simulate providerMetadata.openrouter.usage and assert cost, is_byok, and costDetails on LLM and root spans, including preserving cost after post-processing failure.
Examples: OpenRouter demo
examples/with-openrouter/*
Add a new example project demonstrating OpenRouter integration: README.md, src/index.ts, package.json, tsconfig.json, .env.example, .gitignore.
Examples Index
examples/README.md
Add navigation entry for the OpenRouter example.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Agent
    participant OpenRouter
    participant OT as OpenTelemetry

    Client->>Agent: request generation
    Agent->>OpenRouter: call model (include providerMetadata)
    OpenRouter-->>Agent: response + providerMetadata (cost, costDetails, is_byok)
    Agent->>Agent: extractOpenRouterUsageCost(providerMetadata)
    Agent->>OT: finalizeLLMSpan(with usage + providerMetadata)
    Agent->>OT: recordProviderCost(rootSpan, extractedCost)
    OT-->>Agent: spans recorded with cost attributes
    Agent-->>Client: return result and usage
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through traces, nose to the log,

OpenRouter whispered each penny and cog,
Spans hum the cost in tidy little parts,
I counted the bytes with my rabbit-heart charts,
Cheery hops — observability snug as a bog!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(core): capture provider-reported OpenRouter costs' is concise, specific, and clearly reflects the main change—capturing OpenRouter cost metadata in observability spans.
Description check ✅ Passed The description fulfills all required template sections: commit message convention noted, related items checked, tests/docs/changesets confirmed added, current and new behavior explained, and comprehensive notes provided for reviewers.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/openrouter-cost-observability
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Mar 16, 2026

Deploying voltagent with  Cloudflare Pages  Cloudflare Pages

Latest commit: ef2311b
Status: ✅  Deploy successful!
Preview URL: https://a90b6bc2.voltagent.pages.dev
Branch Preview URL: https://fix-openrouter-cost-observab.voltagent.pages.dev

View logs

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 5 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/core/src/agent/agent.ts">

<violation number="1" location="packages/core/src/agent/agent.ts:1383">
P2: Record root-span provider cost before output middleware, guardrails, or hooks can throw; otherwise successful OpenRouter costs are dropped from the agent span.</violation>
</file>

<file name="packages/core/src/agent/agent-observability.spec.ts">

<violation number="1" location="packages/core/src/agent/agent-observability.spec.ts:201">
P2: The new root-span assertion misses several provider-cost attributes, so regressions there would still pass this test.</violation>
</file>

<file name="website/observability/llm-usage-and-costs.md">

<violation number="1" location="website/observability/llm-usage-and-costs.md:82">
P2: The `onEnd` custom-cost example is too broad: some operations end the root span before `onEnd`, so these `setAttribute` calls are dropped.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
packages/core/src/agent/agent.ts (1)

1346-1359: ⚠️ Potential issue | 🟠 Major

Make generateText write provider cost before onEnd.

generateText is the only path here that records root-span cost after onEnd. That means hook-set usage.cost / usage.cost_details.* can be overwritten here, and an onEnd throw skips root-span provider-cost emission entirely, unlike streamText, generateObject, and streamObject.

Suggested fix
+            this.setTraceContextUsage(oc.traceContext, usageForFinish);
+            this.recordProviderCost(
+              oc.traceContext.getRootSpan(),
+              (result as { providerMetadata?: unknown }).providerMetadata,
+            );
             await this.getMergedHooks(options).onEnd?.({
               conversationId: oc.conversationId || "",
               agent: this,
               output: {
                 text: finalText,
-            // Add usage to span
-            this.setTraceContextUsage(oc.traceContext, usageForFinish);
-            this.recordProviderCost(
-              oc.traceContext.getRootSpan(),
-              (result as { providerMetadata?: unknown }).providerMetadata,
-            );
             oc.traceContext.setOutput(finalText);

Also applies to: 1381-1386

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/agent/agent.ts` around lines 1346 - 1359, generateText
currently invokes getMergedHooks(...).onEnd before recording/writing the
provider cost to the root span, which can cause hooks to see stale/overwritten
usage.cost and can skip emitting provider cost if onEnd throws; move the
provider-cost recording/emission so it happens immediately after computing
provider cost and BEFORE calling onEnd (apply the same change to the other onEnd
invocation around the nearby block referenced in the comment). Concretely: in
the generateText flow, after assembling usageInfo/provider cost (the variables
used in the output object) call the existing root-span provider-cost
writer/emitter routine (the same mechanism used by
streamText/generateObject/streamObject) and set usage.cost and
usage.cost_details.* on the usageInfo object, then call
this.getMergedHooks(options).onEnd?.(...) so hooks see the final cost and
provider-cost emission always runs even if onEnd throws. Ensure you update both
onEnd call sites mentioned.
🧹 Nitpick comments (1)
packages/core/src/agent/open-telemetry/trace-context.ts (1)

582-586: Synchronize model attribute handling between setModelAttributes() and addModelAttributesToSpan().

addModelAttributesToSpan() sets ai.model.provider (lines 582–586), but setModelAttributes() on the AgentTraceContext class does not. While setModelAttributes() is not currently called in production code, it remains part of the public API (exposed via OperationContext.traceContext). For API consistency and to avoid confusion for external callers, either add the provider derivation to setModelAttributes() or document why it differs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/agent/open-telemetry/trace-context.ts` around lines 582 -
586, The addModelAttributesToSpan() logic derives a provider from modelName and
sets ai.model.provider, but AgentTraceContext.setModelAttributes() does not;
update setModelAttributes() (the method on AgentTraceContext exposed via
OperationContext.traceContext) to derive provider the same way (if modelName is
a string and includes '/', take the prefix before '/' ) and store it in the same
trace attributes store (e.g., this.traceAttributes or the attributes object
manipulated by setModelAttributes()) so that ai.model.provider is present for
callers using setModelAttributes(); mirror the same key name and derivation
logic used in addModelAttributesToSpan().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/agent/agent-observability.spec.ts`:
- Around line 180-202: The test currently may pick the LLM child span as the
"rootSpan" because child spans inherit entity.type, and it asserts the fallback
usage.cost (0.0012) instead of the intended upstreamInferenceCost winner; update
the rootSpan selection to find the actual root agent span (e.g., require
span.parent_id === undefined/null or no parent and
span.attributes["entity.type"] === "agent") and then assert
rootSpan.attributes["usage.cost"] equals the upstreamInferenceCost winner
(0.001) and that
rootSpan.attributes["usage.cost_details.upstream_inference_cost"] is 0.001 to
reflect the PR behavior; keep references to events, endSpans, llmSpan, and
rootSpan when locating and asserting the correct span.

In `@packages/core/src/agent/agent.ts`:
- Around line 274-283: The helper toFiniteNumber currently coerces blank or
whitespace-only strings ("", "   ") to 0 because Number("") === 0; update
toFiniteNumber so that when value is a string you first trim it and if the
trimmed string is empty return undefined, otherwise parse Number(trimmed) and
return it only if Number.isFinite(parsed); reference the toFiniteNumber function
to locate and change the string-handling branch accordingly.

---

Outside diff comments:
In `@packages/core/src/agent/agent.ts`:
- Around line 1346-1359: generateText currently invokes
getMergedHooks(...).onEnd before recording/writing the provider cost to the root
span, which can cause hooks to see stale/overwritten usage.cost and can skip
emitting provider cost if onEnd throws; move the provider-cost
recording/emission so it happens immediately after computing provider cost and
BEFORE calling onEnd (apply the same change to the other onEnd invocation around
the nearby block referenced in the comment). Concretely: in the generateText
flow, after assembling usageInfo/provider cost (the variables used in the output
object) call the existing root-span provider-cost writer/emitter routine (the
same mechanism used by streamText/generateObject/streamObject) and set
usage.cost and usage.cost_details.* on the usageInfo object, then call
this.getMergedHooks(options).onEnd?.(...) so hooks see the final cost and
provider-cost emission always runs even if onEnd throws. Ensure you update both
onEnd call sites mentioned.

---

Nitpick comments:
In `@packages/core/src/agent/open-telemetry/trace-context.ts`:
- Around line 582-586: The addModelAttributesToSpan() logic derives a provider
from modelName and sets ai.model.provider, but
AgentTraceContext.setModelAttributes() does not; update setModelAttributes()
(the method on AgentTraceContext exposed via OperationContext.traceContext) to
derive provider the same way (if modelName is a string and includes '/', take
the prefix before '/' ) and store it in the same trace attributes store (e.g.,
this.traceAttributes or the attributes object manipulated by
setModelAttributes()) so that ai.model.provider is present for callers using
setModelAttributes(); mirror the same key name and derivation logic used in
addModelAttributesToSpan().

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f6d0bef7-464e-4d3d-8f4e-99430ad677df

📥 Commits

Reviewing files that changed from the base of the PR and between a3aa5f6 and c15b398.

📒 Files selected for processing (5)
  • .changeset/fair-seahorses-ring.md
  • packages/core/src/agent/agent-observability.spec.ts
  • packages/core/src/agent/agent.ts
  • packages/core/src/agent/open-telemetry/trace-context.ts
  • website/observability/llm-usage-and-costs.md

Comment on lines +274 to +283
const toFiniteNumber = (value: unknown): number | undefined => {
if (typeof value === "number") {
return Number.isFinite(value) ? value : undefined;
}
if (typeof value === "string") {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : undefined;
}
return undefined;
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Don't coerce blank cost strings to 0.

Number("") and Number(" ") both become 0, so a missing OpenRouter cost field serialized as a blank string gets recorded as a real zero-cost value and can suppress the pricing fallback.

Suggested fix
 const toFiniteNumber = (value: unknown): number | undefined => {
   if (typeof value === "number") {
     return Number.isFinite(value) ? value : undefined;
   }
   if (typeof value === "string") {
-    const parsed = Number(value);
+    const trimmed = value.trim();
+    if (trimmed.length === 0) {
+      return undefined;
+    }
+    const parsed = Number(trimmed);
     return Number.isFinite(parsed) ? parsed : undefined;
   }
   return undefined;
 };
📝 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
const toFiniteNumber = (value: unknown): number | undefined => {
if (typeof value === "number") {
return Number.isFinite(value) ? value : undefined;
}
if (typeof value === "string") {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : undefined;
}
return undefined;
};
const toFiniteNumber = (value: unknown): number | undefined => {
if (typeof value === "number") {
return Number.isFinite(value) ? value : undefined;
}
if (typeof value === "string") {
const trimmed = value.trim();
if (trimmed.length === 0) {
return undefined;
}
const parsed = Number(trimmed);
return Number.isFinite(parsed) ? parsed : undefined;
}
return undefined;
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/agent/agent.ts` around lines 274 - 283, The helper
toFiniteNumber currently coerces blank or whitespace-only strings ("", "   ") to
0 because Number("") === 0; update toFiniteNumber so that when value is a string
you first trim it and if the trimmed string is empty return undefined, otherwise
parse Number(trimmed) and return it only if Number.isFinite(parsed); reference
the toFiniteNumber function to locate and change the string-handling branch
accordingly.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
examples/with-openrouter/package.json (1)

31-36: Minor formatting inconsistency.

Line 32 has an extra leading space before "build" compared to the other script entries.

📝 Suggested fix
  "scripts": {
-   "build": "tsc",
+    "build": "tsc",
    "dev": "tsx watch --env-file=.env ./src",
    "start": "node dist/index.js",
    "volt": "volt"
  },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/with-openrouter/package.json` around lines 31 - 36, The package.json
"scripts" section has an extra leading space before the "build" key causing
inconsistent formatting; edit the "scripts" object so the "build" entry
("build": "tsc") aligns with the other script keys (dev, start, volt) by
removing the stray leading space before the "build" key name.
examples/with-openrouter/src/index.ts (1)

41-48: Consider deferring the example request until the server is ready.

The async IIFE executes immediately on module load, which runs concurrently with server initialization. For a robust example, consider waiting for a server-ready event or adding a brief delay to ensure the VoltAgent server is fully initialized before making the test request.

💡 Suggested improvement
 new VoltAgent({
   agents: {
     agent,
   },
   logger,
   server: honoServer(),
 });

-(async () => {
-  const result = await agent.generateText("Explain how observability helps with AI cost control.");
-
-  logger.info("OpenRouter example request completed", {
-    text: result.text,
-    usage: result.usage,
-  });
-})();
+// Give the server a moment to initialize before running the example request
+setTimeout(async () => {
+  const result = await agent.generateText("Explain how observability helps with AI cost control.");
+
+  logger.info("OpenRouter example request completed", {
+    text: result.text,
+    usage: result.usage,
+  });
+}, 1000);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/with-openrouter/src/index.ts` around lines 41 - 48, The example
currently invokes the async IIFE at module load which calls agent.generateText
immediately; instead defer the request until the server/agent is ready by moving
the call into a server-ready callback or awaiting a readiness promise (e.g.,
server.on('listening', ...) or await agent.ready() if available) before calling
agent.generateText, then log with logger.info; as a simpler fallback, insert a
short delay (setTimeout) before invoking agent.generateText to ensure the
VoltAgent server is initialized.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/with-openrouter/README.md`:
- Line 3: Change the article before "VoltAgent" in the README sentence "An
[VoltAgent](https://github.com/VoltAgent/voltagent) application using OpenRouter
through `@openrouter/ai-sdk-provider`." to "A" so it reads "A
[VoltAgent](https://github.com/VoltAgent/voltagent) application..." to fix the
grammatical error.
- Around line 57-67: The README.md project tree is out of sync: remove the
nonexistent scripts/ entry (and scripts/dev.mjs) from the documented structure
so it matches the actual setup; update the tree in README.md to reflect that
package.json uses the dev script (e.g., "tsx watch --env-file=.env ./src") and
no scripts/ folder should be listed.

---

Nitpick comments:
In `@examples/with-openrouter/package.json`:
- Around line 31-36: The package.json "scripts" section has an extra leading
space before the "build" key causing inconsistent formatting; edit the "scripts"
object so the "build" entry ("build": "tsc") aligns with the other script keys
(dev, start, volt) by removing the stray leading space before the "build" key
name.

In `@examples/with-openrouter/src/index.ts`:
- Around line 41-48: The example currently invokes the async IIFE at module load
which calls agent.generateText immediately; instead defer the request until the
server/agent is ready by moving the call into a server-ready callback or
awaiting a readiness promise (e.g., server.on('listening', ...) or await
agent.ready() if available) before calling agent.generateText, then log with
logger.info; as a simpler fallback, insert a short delay (setTimeout) before
invoking agent.generateText to ensure the VoltAgent server is initialized.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 20e7ec96-e1c0-4889-9f78-c27765387fee

📥 Commits

Reviewing files that changed from the base of the PR and between c15b398 and fe37f5f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • examples/README.md
  • examples/with-openrouter/.env.example
  • examples/with-openrouter/.gitignore
  • examples/with-openrouter/README.md
  • examples/with-openrouter/package.json
  • examples/with-openrouter/src/index.ts
  • examples/with-openrouter/tsconfig.json
✅ Files skipped from review due to trivial changes (2)
  • examples/with-openrouter/.gitignore
  • examples/with-openrouter/.env.example

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 8 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="examples/with-openrouter/README.md">

<violation number="1" location="examples/with-openrouter/README.md:42">
P2: The README claims `pnpm dev` avoids `tsx`, but the actual `dev` script still uses `tsx watch`. This will send readers to the wrong command when they try to reproduce or work around the loader issue.</violation>

<violation number="2" location="examples/with-openrouter/README.md:62">
P3: The documented project structure includes `scripts/dev.mjs`, but that file is not present in this example.</violation>
</file>

<file name="examples/with-openrouter/src/index.ts">

<violation number="1" location="examples/with-openrouter/src/index.ts:41">
P2: Avoid firing the demo LLM request unconditionally on startup; it creates a paid OpenRouter call on every boot and can crash the example if that request fails.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

server: honoServer(),
});

(async () => {
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 16, 2026

Choose a reason for hiding this comment

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

P2: Avoid firing the demo LLM request unconditionally on startup; it creates a paid OpenRouter call on every boot and can crash the example if that request fails.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/with-openrouter/src/index.ts, line 41:

<comment>Avoid firing the demo LLM request unconditionally on startup; it creates a paid OpenRouter call on every boot and can crash the example if that request fails.</comment>

<file context>
@@ -0,0 +1,48 @@
+  server: honoServer(),
+});
+
+(async () => {
+  const result = await agent.generateText("Explain how observability helps with AI cost control.");
+
</file context>
Fix with Cubic

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="website/observability/llm-usage-and-costs.md">

<violation number="1" location="website/observability/llm-usage-and-costs.md:114">
P3: This warning overstates the limitation for `streamObject`; on successful `streamObject` runs, `onEnd` executes before the root span is ended.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

});
```

Do not treat this as a universal pattern for every operation. Some flows, including `generateObject`, `streamObject`, and cancellation or error paths, can end the root span before `onEnd` runs. If you need custom cost attributes to be recorded reliably across every operation type, set them earlier in your provider integration or emit them directly through OpenTelemetry instrumentation.
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 16, 2026

Choose a reason for hiding this comment

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

P3: This warning overstates the limitation for streamObject; on successful streamObject runs, onEnd executes before the root span is ended.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At website/observability/llm-usage-and-costs.md, line 114:

<comment>This warning overstates the limitation for `streamObject`; on successful `streamObject` runs, `onEnd` executes before the root span is ended.</comment>

<file context>
@@ -111,6 +111,8 @@ const agent = new Agent({
 });

+Do not treat this as a universal pattern for every operation. Some flows, including generateObject, streamObject, and cancellation or error paths, can end the root span before onEnd runs. If you need custom cost attributes to be recorded reliably across every operation type, set them earlier in your provider integration or emit them directly through OpenTelemetry instrumentation.
+
Use these attributes for custom cost reporting:
</file context>


</details>

```suggestion
Do not treat this as a universal pattern for every operation. `generateObject` and cancellation or error paths can end the root span before `onEnd` runs. If you need custom cost attributes to be recorded reliably across every operation type, set them earlier in your provider integration or emit them directly through OpenTelemetry instrumentation.
Fix with Cubic

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
packages/core/src/agent/agent.ts (1)

274-281: ⚠️ Potential issue | 🟡 Minor

Blank strings are still coerced to numeric zero in toFiniteNumber.

At Line 279, Number(value) converts "" and " " to 0, which can record missing provider cost as real zero and suppress fallback pricing.

Suggested fix
 const toFiniteNumber = (value: unknown): number | undefined => {
   if (typeof value === "number") {
     return Number.isFinite(value) ? value : undefined;
   }
   if (typeof value === "string") {
-    const parsed = Number(value);
+    const trimmed = value.trim();
+    if (trimmed.length === 0) {
+      return undefined;
+    }
+    const parsed = Number(trimmed);
     return Number.isFinite(parsed) ? parsed : undefined;
   }
   return undefined;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/agent/agent.ts` around lines 274 - 281, The toFiniteNumber
helper currently treats empty or whitespace-only strings as numeric zero because
Number("") === 0; update toFiniteNumber to detect string inputs that are empty
or only whitespace (e.g., using trim() or a regex) and return undefined for
those cases before calling Number(), so blank strings like "" or "   " do not
coerce to 0 and instead signal missing values; adjust the branch handling string
inputs inside toFiniteNumber accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/core/src/agent/agent.ts`:
- Around line 274-281: The toFiniteNumber helper currently treats empty or
whitespace-only strings as numeric zero because Number("") === 0; update
toFiniteNumber to detect string inputs that are empty or only whitespace (e.g.,
using trim() or a regex) and return undefined for those cases before calling
Number(), so blank strings like "" or "   " do not coerce to 0 and instead
signal missing values; adjust the branch handling string inputs inside
toFiniteNumber accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1666a3e8-55c9-42d2-8e73-254043f0fc5a

📥 Commits

Reviewing files that changed from the base of the PR and between fe37f5f and e49d0a3.

📒 Files selected for processing (4)
  • examples/with-openrouter/package.json
  • packages/core/src/agent/agent-observability.spec.ts
  • packages/core/src/agent/agent.ts
  • website/observability/llm-usage-and-costs.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/with-openrouter/package.json

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
examples/with-openrouter/README.md (1)

53-53: Keep package-manager instructions consistent in the Notes section.

Line 53 only shows pnpm commands, while earlier sections provide npm/yarn/pnpm alternatives. Consider either making Line 53 pnpm-only explicitly or adding equivalent npm/yarn commands for consistency.

📝 Suggested doc tweak
-Use `pnpm build && pnpm start` for the compiled output, or `pnpm dev` during development.
+Use one of the following for compiled output:
+
+```bash
+npm run build && npm run start
+# or
+yarn build && yarn start
+# or
+pnpm build && pnpm start
+```
+
+For development, use `npm run dev`, `yarn dev`, or `pnpm dev`.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/with-openrouter/README.md` at line 53, The README line "Use `pnpm
build && pnpm start` for the compiled output, or `pnpm dev` during development."
is inconsistent with earlier package-manager examples; either make this line
explicitly pnpm-only by prefacing it with "For pnpm:" or add equivalent npm/yarn
alternatives. Update that sentence to include the three variants (e.g., "npm run
build && npm run start", "yarn build && yarn start", "pnpm build && pnpm start")
and similarly list "npm run dev", "yarn dev", "pnpm dev" for development so the
Notes section matches the format used elsewhere.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@examples/with-openrouter/README.md`:
- Line 53: The README line "Use `pnpm build && pnpm start` for the compiled
output, or `pnpm dev` during development." is inconsistent with earlier
package-manager examples; either make this line explicitly pnpm-only by
prefacing it with "For pnpm:" or add equivalent npm/yarn alternatives. Update
that sentence to include the three variants (e.g., "npm run build && npm run
start", "yarn build && yarn start", "pnpm build && pnpm start") and similarly
list "npm run dev", "yarn dev", "pnpm dev" for development so the Notes section
matches the format used elsewhere.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 20f11c7c-2fb4-4ed0-883a-6433bffad52f

📥 Commits

Reviewing files that changed from the base of the PR and between e49d0a3 and ef2311b.

📒 Files selected for processing (1)
  • examples/with-openrouter/README.md

@omeraplak omeraplak merged commit 52bda94 into main Mar 16, 2026
24 checks passed
@omeraplak omeraplak deleted the fix/openrouter-cost-observability branch March 16, 2026 19:57
@coderabbitai coderabbitai bot mentioned this pull request Apr 1, 2026
5 tasks
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.

1 participant