fix(core): capture provider-reported OpenRouter costs#1155
Conversation
🦋 Changeset detectedLatest commit: ef2311b The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
This comment has been minimized.
This comment has been minimized.
📝 WalkthroughWalkthroughParse 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
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. Comment |
Deploying voltagent with
|
| Latest commit: |
ef2311b
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://a90b6bc2.voltagent.pages.dev |
| Branch Preview URL: | https://fix-openrouter-cost-observab.voltagent.pages.dev |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 | 🟠 MajorMake
generateTextwrite provider cost beforeonEnd.
generateTextis the only path here that records root-span cost afteronEnd. That means hook-setusage.cost/usage.cost_details.*can be overwritten here, and anonEndthrow skips root-span provider-cost emission entirely, unlikestreamText,generateObject, andstreamObject.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 betweensetModelAttributes()andaddModelAttributesToSpan().
addModelAttributesToSpan()setsai.model.provider(lines 582–586), butsetModelAttributes()on theAgentTraceContextclass does not. WhilesetModelAttributes()is not currently called in production code, it remains part of the public API (exposed viaOperationContext.traceContext). For API consistency and to avoid confusion for external callers, either add the provider derivation tosetModelAttributes()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
📒 Files selected for processing (5)
.changeset/fair-seahorses-ring.mdpackages/core/src/agent/agent-observability.spec.tspackages/core/src/agent/agent.tspackages/core/src/agent/open-telemetry/trace-context.tswebsite/observability/llm-usage-and-costs.md
| 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; | ||
| }; |
There was a problem hiding this comment.
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.
| 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.
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (7)
examples/README.mdexamples/with-openrouter/.env.exampleexamples/with-openrouter/.gitignoreexamples/with-openrouter/README.mdexamples/with-openrouter/package.jsonexamples/with-openrouter/src/index.tsexamples/with-openrouter/tsconfig.json
✅ Files skipped from review due to trivial changes (2)
- examples/with-openrouter/.gitignore
- examples/with-openrouter/.env.example
There was a problem hiding this comment.
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 () => { |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
packages/core/src/agent/agent.ts (1)
274-281:⚠️ Potential issue | 🟡 MinorBlank strings are still coerced to numeric zero in
toFiniteNumber.At Line 279,
Number(value)converts""and" "to0, 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
📒 Files selected for processing (4)
examples/with-openrouter/package.jsonpackages/core/src/agent/agent-observability.spec.tspackages/core/src/agent/agent.tswebsite/observability/llm-usage-and-costs.md
🚧 Files skipped from review as they are similar to previous changes (1)
- examples/with-openrouter/package.json
There was a problem hiding this comment.
🧹 Nitpick comments (1)
examples/with-openrouter/README.md (1)
53-53: Keep package-manager instructions consistent in the Notes section.Line 53 only shows
pnpmcommands, while earlier sections provide npm/yarn/pnpm alternatives. Consider either making Line 53pnpm-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
📒 Files selected for processing (1)
examples/with-openrouter/README.md
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
onEndhooks.fixes (issue)
N/A
Notes for reviewers
usage.costandusage.cost_details.upstream_inference_*on both span levels.api/voltagent-apiand are not part of this repo.pnpm -C packages/core test:single src/agent/agent-observability.spec.tsSummary by cubic
Capture OpenRouter’s provider-reported costs in
@voltagent/coreobservability spans and prefer them over static pricing to improve VoltOps cost accuracy, especially for router/BYOK flows.Bug Fixes
providerMetadataon LLM and root spans:usage.cost,usage.is_byok, andusage.cost_details.{upstream_inference_cost, upstream_inference_input_cost, upstream_inference_output_cost}.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.ai.model.providerderived fromai.model.namefor better grouping.usage: { include: true }), custom cost hooks, and when to safely set custom cost attributes.New Features
examples/with-openroutershowing 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
Documentation
Tests
Examples