Skip to content

Standard usage object empty when usage data is in providerMetadata #419

@konard

Description

@konard

Description

When using the OpenRouter AI SDK provider with certain API endpoints (e.g., Kilo Gateway at https://api.kilo.ai/api/openrouter), the standard usage object in the finish-step event contains undefined/NaN values, while the actual usage data is correctly populated in providerMetadata.openrouter.usage.

Steps to Reproduce

  1. Configure the OpenRouter provider with a custom baseURL (e.g., Kilo Gateway)
  2. Make a streaming request to a model like z-ai/glm-5:free
  3. Observe the finish-step event

Expected Behavior

The standard usage object should contain valid token counts:

{
  "inputTokens": 12004,
  "outputTokens": 40,
  "cachedInputTokens": 10880,
  "reasoningTokens": 20
}

Actual Behavior

The standard usage object has undefined values:

{
  "inputTokens": undefined,
  "outputTokens": undefined,
  "inputTokenDetails": {},
  "outputTokenDetails": {}
}

While providerMetadata.openrouter.usage correctly contains:

{
  "promptTokens": 12004,
  "completionTokens": 40,
  "promptTokensDetails": { "cachedTokens": 10880 },
  "completionTokensDetails": { "reasoningTokens": 20 },
  "cost": 0.0001714,
  "totalTokens": 12044
}

Analysis

Looking at the SDK source code:

  1. In streaming mode, usage is initialized with Number.NaN values (around line 1923):
const usage = {
  inputTokens: Number.NaN,
  outputTokens: Number.NaN,
  ...
};
  1. The SDK expects streaming chunks to contain value.usage.prompt_tokens (snake_case, line 1971-1999)

  2. The SDK also populates openrouterUsage separately and includes it in providerMetadata

  3. On flush(), both objects are emitted:

controller.enqueue({
  type: "finish",
  finishReason,
  usage,  // May still be NaN if never populated
  providerMetadata: {
    openrouter: { usage: openrouterUsage }  // Correctly populated
  }
});

The issue appears to be that the Kilo Gateway API (or potentially other OpenRouter-compatible APIs) sends usage data in a format or timing that doesn't trigger the population of the standard usage object, but does populate openrouterUsage.

Suggested Fix

The SDK could copy data from openrouterUsage to the standard usage object in the flush() handler if the standard usage values are still NaN:

flush(controller) {
  // Before emitting, ensure standard usage is populated from openrouterUsage if needed
  if (Number.isNaN(usage.inputTokens) && openrouterUsage.promptTokens != null) {
    usage.inputTokens = openrouterUsage.promptTokens;
    usage.outputTokens = openrouterUsage.completionTokens ?? 0;
    usage.cachedInputTokens = openrouterUsage.promptTokensDetails?.cachedTokens ?? 0;
    usage.reasoningTokens = openrouterUsage.completionTokensDetails?.reasoningTokens ?? 0;
    usage.totalTokens = openrouterUsage.totalTokens ?? (usage.inputTokens + usage.outputTokens);
  }
  // ... rest of flush
}

Environment

  • @openrouter/ai-sdk-provider version: 1.5.4
  • Node.js/Bun runtime
  • Custom baseURL: https://api.kilo.ai/api/openrouter

Workaround

For now, consumers can fall back to extracting usage from providerMetadata.openrouter.usage when the standard usage is empty. See: link-assistant/agent#188

Related Issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions