Commit Graph

25 Commits

Author SHA1 Message Date
Hamza Kyamanywa
cf08ef0a29 fix(openrouter): support default_headers for custom HTTP header injection (#36582)
Fixes #36581

## Problem

`ChatOpenRouter` had no way to set custom HTTP headers on requests to
OpenRouter. Passing `default_headers` to the constructor silently
misfired: `build_extra` treated it as an unrecognized kwarg, emitted a
"transferred to model_kwargs" warning, and dumped the header into the
request body instead of the HTTP layer. This blocked any feature that
needs per-request header injection — for example xAI's `x-grok-conv-id`
for sticky-routing prompt cache hits.

## What changed

- `default_headers` is now a first-class field on `ChatOpenRouter`
(`Mapping[str, str] | None`). Because headers may carry credentials, the
field is excluded from serialization.
- User-supplied headers are merged with the built-in app-attribution
headers (`HTTP-Referer`, `X-Title`, `X-OpenRouter-Categories`). On
collision the user value wins; because HTTP header names are
case-insensitive, the merge drops any built-in whose name
case-insensitively matches a user header before applying, so
`http-referer` replaces `HTTP-Referer` rather than producing a doubled
header.
- Corrected the documented `session_id` length limit from 128 to 256
characters.

Example:

    ChatOpenRouter(
        model="x-ai/grok-4",
        default_headers={"x-grok-conv-id": "session-abc123"},
    )

---------

Co-authored-by: Mason Daugherty <github@mdrxy.com>
2026-07-05 16:40:25 -04:00
Mason Daugherty
7725ccec03 fix(openrouter): deduplicate repeated finish metadata (#38552)
Closes #38226

---

OpenRouter can emit more than one terminal streaming chunk for a single
response. Before this change, LangChain treated each terminal chunk as
independent final metadata, so repeated string fields could be merged
into corrupted values like `stopstop`.

This updates `ChatOpenRouter` streaming so repeated terminal chunks are
interpreted as parts of the same response ending. Usage metadata is
still captured, later chunks can fill in terminal details that were
missing from earlier chunks, and already-seen terminal fields are not
duplicated.

The regression coverage exercises sync and async generation with
duplicate finish chunks, including the case where usage and additional
terminal metadata arrive on the later chunk.
2026-06-29 15:39:57 -04:00
Mason Daugherty
57c83d44bc fix(openrouter): strip Responses reasoning IDs (#38383)
Closes #37777

---

OpenRouter can return OpenAI Responses reasoning item IDs such as `rs_*`
in assistant reasoning details. Those IDs are not reliably resolvable on
a later OpenRouter turn, so replaying them can make otherwise-valid
multi-turn conversations fail with a provider 404.

This keeps the useful reasoning payload while removing only the
ephemeral Responses item IDs before serializing `reasoning_details` back
into request history. Non-Responses IDs and reasoning text are left
intact.
2026-06-23 12:26:24 -04:00
Mason Daugherty
6188e9270e chore(openrouter): bump openrouter floor to 0.9.2, drop file workaround (#38216)
`ChatOpenRouter` users sending PDF/file content blocks previously relied
on a workaround: the OpenRouter Python SDK's `ChatContentItems`
discriminated union didn't include a `file` tag, so file parts failed
Pydantic validation. To work around it, `langchain-openrouter` wrapped
message dicts with `model_construct()` to bypass SDK validation while
still producing correct JSON.

The SDK fixed this in `openrouter` 0.9.2 by adding `file` to the
`ChatContentItems` union (verified against
OpenRouterTeam/python-sdk#38). This PR raises the dependency floor to
`openrouter>=0.9.2` and removes the now-unnecessary
`_wrap_messages_for_sdk` / `_has_file_content_blocks` helpers, passing
plain message dicts straight to the SDK so file parts go through the
normal validated path.

The old workaround's unit tests are replaced with a test asserting the
SDK now validates a `file` content part natively, guarding against a
regression if the floor is ever lowered. The existing `file`-block
conversion helpers and their tests are unchanged.

_This change was authored with the help of an AI agent._

Made by [Open
SWE](https://openswe.vercel.app/agents/9f95ffe6-2298-1045-9316-935f55d00fa7)

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-22 23:38:28 -04:00
Mason Daugherty
237bb61692 test(openrouter): cover cache_control passthrough on tool defs (#38215)
The OpenRouter SDK preserves `cache_control` on tool definitions, and
`ChatOpenRouter` forwards full tool dicts unchanged. This adds a
regression test confirming a top-level `cache_control` on a tool dict
passed to `bind_tools` survives into the SDK request payload.

Made by [Open
SWE](https://openswe.vercel.app/agents/5059cee2-c2de-1ca4-200d-c3fdb8c5814e)

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-22 23:09:00 -04:00
Mason Daugherty
dae4e5de9d feat(openrouter): surface parallel_tool_calls on bind_tools (#38214)
`ChatOpenRouter.bind_tools()` now accepts a `parallel_tool_calls`
argument to disable parallel tool use.

---

Users binding tools to `ChatOpenRouter` previously had to know that
arbitrary kwargs were forwarded to the OpenRouter SDK in order to
disable parallel tool use (e.g.
`model.bind(parallel_tool_calls=False)`). This made the option hard to
discover.

This exposes `parallel_tool_calls` as an explicit keyword-only argument
on `ChatOpenRouter.bind_tools()`, matching the ergonomics of
`langchain-openai`. When set, it is forwarded to the request exactly as
before; when left as `None` it is omitted, preserving existing behavior.
No change to the underlying request payload or SDK floor — purely an
API/discoverability improvement.

Made by [Open
SWE](https://openswe.vercel.app/agents/053b59c8-baf4-84e1-c003-425e349e014d)

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-22 23:01:15 -04:00
Mason Daugherty
8837163917 fix(core,partners): rename package version trace metadata (#38110)
Package-version trace metadata now uses the LangChain-owned
`metadata["lc_versions"]` convention instead of the user-owned
`metadata["versions"]` key. Metadata merging is narrowed so only
`lc_versions` accumulates nested package-version entries, while generic
nested metadata keeps normal last-writer-wins behavior.

## Changes
- Renamed `BaseLanguageModel._add_version()` trace metadata from
`versions` to `lc_versions`, including docstrings and the non-dict
replacement warning.
- Scoped `_merge_metadata_dicts()` nested-map accumulation to only
`lc_versions`; duplicate package entries remain last-writer-wins and
`lc_versions` mappings are copied defensively.
- Preserved user-owned `metadata["versions"]` semantics by keeping it
out of package-version tracking and generic nested metadata merging.
- Updated runnable snapshots and partner package metadata assertions
across Anthropic, DeepSeek, Fireworks, Groq, Hugging Face, MistralAI,
Ollama, OpenAI, OpenRouter, Perplexity, and xAI to expect `lc_versions`.

## Testing
- Added/adjusted core tests for `lc_versions` accumulation, duplicate
package overwrite behavior, non-dict `lc_versions` replacement,
defensive copying, and `metadata["versions"]` last-writer-wins behavior.
- Ran focused core and partner metadata tests plus Ruff checks for
changed areas.
2026-06-12 14:26:32 -04:00
Mason Daugherty
5d20596d73 style(core,langchain,langchain-classic,partners): replace double backticks in docstrings (#38095)
Standardizes inline code markup in Python docstrings and comments by
replacing Sphinx-style double backticks with single-backtick Markdown.
The cleanup keeps existing code fences intact while aligning inline
references with the repo's docstring convention.

## Changes
- Converted inline code references in core prompt-loading docs and
LangSmith tracer comments, including `..`, `allow_dangerous_paths`, and
inheritable metadata keys.
- Normalized agent-related docstrings and comments around
`wrap_model_call`, `ExtendedModelResponse`, `Command`,
`create_structured_chat_agent`, and `DockerExecutionPolicy`.
- Updated partner package docstrings for inline references such as
`json_schema`, `ToolCall`, `apply_patch_call_output`, OpenRouter content
block keys, and Perplexity tool-call serialization.
- Cleaned test and helper docstrings that referenced command separators,
fake `resource` modules, stream event names, and xdist rate-limit
environment variables.
2026-06-12 00:17:26 -04:00
Mason Daugherty
948f6cc58c feat(core,partners): add package version tracking to tracing metadata (#35295)
Following on the heels of #35293

TODO:
- Packages outside of this repo (e.g. LiteLLM, Nvidia, Google, AWS)

---

## Summary

Surface partner package versions in `metadata.versions` on LangSmith
traces. Mirrors the JS SDK's `_addVersion()` pattern
([langchainjs#10106](https://github.com/langchain-ai/langchainjs/pull/10106)).

Each model constructor records its package version via `_add_version()`
on `BaseLanguageModel`. The version dict accumulates through the class
hierarchy — `langchain-core` is added in
`BaseLanguageModel.model_post_init`, `langchain-openai` in
`BaseChatOpenAI._set_openai_chat_version`, and each leaf partner in its
uniquely-named `model_validator`. Traces end up with:

```json
{
  "metadata": {
    "versions": {
      "langchain-core": "1.4.5",
      "langchain-openai": "1.3.0",
      "langchain-xai": "1.2.2"
    }
  }
}
```

### Changes

- `BaseLanguageModel._add_version(pkg, version)` — appends to
`self.metadata["versions"]`; accepts any `Mapping` type; emits a warning
if a non-mapping value is found and replaced
- `BaseLanguageModel.model_post_init` — adds `langchain-core` version;
calls `super()` for MRO safety
- `_merge_metadata_dicts` — one-level-deep (non-recursive) merge for
nested dict metadata keys
- `CallbackManager.add_metadata` — uses `_merge_metadata_dicts` instead
of flat `dict.update()` so nested metadata dicts (like `versions`)
coexist rather than clobber
- `merge_configs` — uses `_merge_metadata_dicts` for config merging

**Partners:**
- Each now calls `self._add_version("langchain-<pkg>", __version__)`

### Design decisions

- **Constructor-based, not `_get_ls_params`-based** — versions flow
through `self.metadata` (local metadata on traces), not through
`LangSmithParams`. This matches JS and makes child-class version
inheritance automatic (no merge/clobber issues).
- **`versions` is local (non-inheritable) metadata** — `self.metadata`
is passed to `CallbackManager.configure` as `local_metadata`
(`add_metadata(..., inherit=False)`), so `versions` is attached **once
per chat-model run** and is **not** propagated to child runs or
duplicated onto every streaming chunk. This is intentionally the
opposite of the inheritable-per-chunk metadata that #36588 was reducing
for performance — `versions` does not regress that path.
- **`add_metadata` deep-merge is a correctness fix, not just for
versions** — previously `add_metadata`/`merge_configs` did a flat
top-level `dict.update`/spread, so any nested metadata dict baked into a
config (e.g. via `.with_config({"metadata": {...}})`) would be wholly
replaced when a caller also passed `metadata`. `_merge_metadata_dicts`
merges one level deep so user-provided `config.metadata.versions` and
model-set `versions` coexist instead of clobbering. The merge runs once
per `configure` (not per chunk), so it is off the streaming hot path.
- **One level deep only** — `_merge_metadata_dicts` is deliberately
*not* a recursive deep merge; values nested more than one level are
last-writer-wins. This covers the `versions` case without the
ambiguity/cost of arbitrary-depth merging.
- **Warn on non-dict `metadata["versions"]`** — if a user sets
`metadata={"versions": "some-string"}`, `_add_version` emits a warning
and replaces the value with the version dict rather than silently
discarding user data or crashing. This is a soft breaking change for
anyone who previously stored non-dict values at this key.

### Follow-ups (tracked separately, out of scope here)

- JS `mergeConfigs` still flat-spreads nested metadata, so
`metadata.versions` can still clobber on the JS side until an equivalent
deep-merge lands.

---

Made by [Open SWE](https://openswe.vercel.app)

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-11 02:23:19 -04:00
Mason Daugherty
6b9e22dbbc fix(langchain): tighten structured output model fallbacks (#38042)
Provider-native structured output fallback detection now uses bounded
model-name patterns instead of broad substring checks, reducing false
positives for unrelated model IDs. The model examples and test fixtures
across OpenAI/OpenRouter-facing code were refreshed around current
OpenAI model families while preserving shipped defaults.

## Changes
- Tightened `FALLBACK_MODELS_WITH_STRUCTURED_OUTPUT` from loose string
fragments to regex patterns, with `_supports_provider_strategy` matching
full model-name segments instead of arbitrary substrings.
- Expanded structured-output fallback coverage for newer OpenAI,
Anthropic, and xAI/Grok model families, including `gpt-5.x`, newer
Claude 4/5-style names, and `grok-build`.
- Reused `_attempt_infer_model_provider` in provider tool search routing
so `_provider_from_model_name` follows the same provider inference
behavior as `init_chat_model`.
- Suppressed irrelevant provider-inference deprecation warnings during
provider tool search registry lookup.
- Refreshed OpenAI, Azure OpenAI, OpenRouter, core metadata, and example
model references from older fixtures like `gpt-4`, `gpt-4o`, `o1`, and
`o4-mini` to current test/profile models such as `gpt-5.5`,
`gpt-5-nano`, and `gpt-4.1-mini`.
- Removed outdated OpenAI test assumptions around legacy `o1` behavior
and narrowed legacy structured-output checks to explicitly legacy model
names.
2026-06-10 21:18:14 -04:00
Mason Daugherty
21eeadf274 test(partners): account for warning behavior in partner tests (#38046)
Partner unit tests now reflect the warning behavior emitted by updated
`langchain-core` serialization and model initialization paths.
Warning-strict runs can stay focused on the behavior under test rather
than expected framework warnings.
2026-06-10 19:50:52 -04:00
Xi Zhang
70f5626579 fix(openrouter): merge fragmented reasoning_details in streaming (#36401)
## Description

Fixes #36400

During streaming, `AIMessageChunk.__add__` list-concatenates
`reasoning_details` in `additional_kwargs`, fragmenting a single entry
into many. When `_convert_message_to_dict()` serializes conversation
history back to the OpenRouter API for the next turn, these fragmented
entries cause `BadRequestResponseError`.

### Changes

- Add `_merge_reasoning_details()` helper that merges consecutive
entries sharing the same `type` and `index` (streaming fragments) while
preserving distinct entries (legitimate non-streaming data)
- Metadata from later fragments (e.g. `signature`) is preserved in the
merged result
- Entries without `index` are never merged (safe for non-streaming
responses)
- Call `_merge_reasoning_details()` in `_convert_message_to_dict()`
before serializing `reasoning_details`

### Why merge instead of drop?

Non-streaming users (`invoke()`) rely on `reasoning_details` for
structured metadata (`type`, `signature`, `format`, `index`). Dropping
it entirely would be a regression. This approach fixes streaming while
preserving non-streaming functionality, similar to `langchain-openai`'s
`_implode_reasoning_blocks()`.

## Test plan

- [x] Fragmented entries (same type + same index) are merged into one
- [x] Distinct entries (different index) are preserved separately
- [x] Entries without index are never merged
- [x] Metadata from later fragments (e.g. signature) is preserved
- [x] Single-entry lists pass through unchanged
- [x] Round-trip (dict → message → dict) works correctly
- [x] All 210 unit tests pass

---------

Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2026-05-01 17:57:00 -04:00
Mason Daugherty
1e38d59d41 feat(openrouter): session_id and trace fields (#37137)
Add first-class `session_id` and `trace` constructor fields on
`ChatOpenRouter`, plumbed into the request payload alongside the
existing `route` / `plugins` / `openrouter_provider` knobs. Both
correspond to the OpenRouter
[broadcast](https://openrouter.ai/docs/guides/features/broadcast/overview)
feature for grouping requests under one logical workflow and attaching
per-request observability metadata. Previously these were only reachable
by stuffing them through `model_kwargs` or `.bind()`, neither of which
is discoverable.

[Docs](https://github.com/langchain-ai/docs/pull/3840)

## Changes
- New `session_id: str | None` field with a
`from_env("OPENROUTER_SESSION_ID", default=None)` factory, so a process
can group all requests via env var without threading the value through
application code. Truthy-guarded in `_default_params` so an explicit or
env-sourced empty string is treated as unset.
- New `trace: dict[str, Any] | None` field for per-request trace
metadata (`trace_id`, `trace_name`, `span_name`, `generation_name`,
`parent_span_id`, plus arbitrary extras forwarded as custom metadata).
No env fallback — set per-call or on the constructor.
- Per-call override (`model.invoke(..., session_id=..., trace=...)`)
works through the existing `**kwargs` flow into `_generate` / `_stream`,
with the constructor value preserved across calls.
- Updated the "Key init args — client params" docstring table on
`ChatOpenRouter` to surface both fields.
2026-05-01 14:14:04 -04:00
Mason Daugherty
8c15649127 fix(openai,groq,openrouter): use is-not-None checks in usage metadata token extraction (#36500)
Python's `or` operator treats `0` as falsy, so
`token_usage.get("total_tokens") or fallback` silently replaces a
provider-reported `total_tokens=0` with the computed sum of input +
output tokens. Providers can legitimately report zero tokens (e.g.,
cached responses, empty completions).

The same pattern exists in the dual-key lookups for
`input_tokens`/`output_tokens` in Groq and OpenRouter. While current
APIs don't return both key formats simultaneously (making the `or`-chain
functionally correct today), the semantics are still wrong; `0` should
not fall through to a fallback.

## Changes

- Replace `x.get(key) or fallback` with explicit `is not None` checks in
`_create_usage_metadata` across `langchain-openai`, `langchain-groq`,
and `langchain-openrouter` for `input_tokens`, `output_tokens`, and
`total_tokens`
- Fix a concrete bug in the `total_tokens` path: a provider-reported `0`
was silently replaced by the computed sum
- Harden dual-key lookups in Groq and OpenRouter to correctly preserve
zero values from the preferred key, should both key formats ever coexist
- Update OpenAI's single-key extraction for consistency — the old `or 0`
pattern happened to produce correct results (`0 or 0 == 0`) but was
semantically wrong
2026-04-03 11:46:36 -04:00
Mason Daugherty
342d8bdef2 release(openrouter): 0.2.1 (#36348) 2026-03-29 20:50:14 -04:00
Mason Daugherty
4d9842da67 fix(openrouter): pass attribution headers via httpx default_headers (#36347)
Fixes #36339

---

The `openrouter` SDK v0.8.0 renamed `x_title` to `x_open_router_title`,
breaking `ChatOpenRouter` instantiation with the default `app_title`.

Rather than chasing SDK parameter renames across versions, all three
attribution headers are now injected via httpx `default_headers` —
version-agnostic and consistent with how `app_categories` was already
handled.

## Changes
- Pass `HTTP-Referer`, `X-Title`, and `X-OpenRouter-Categories` as httpx
client default headers in `_build_client` instead of SDK constructor
kwargs (`http_referer`, `x_title`), making the integration compatible
across `openrouter>=0.7.11,<1.0.0`
- Move `_build_client()` inside the `try/except ImportError` in
`validate_environment` so a version-mismatch `ImportError` from
`openrouter.utils` gets the friendly install message instead of a raw
traceback
- Add `warnings.warn` in `_wrap_messages_for_sdk` for two previously
silent fallbacks: failed `openrouter.components` import (file blocks
sent as raw dicts) and unknown message roles passed through to the API
- Clarify `max_retries` docstring to explain the ~150s-per-unit backoff
mapping; drop stale `(v0.6.0)` version reference in
`_wrap_messages_for_sdk`
2026-03-28 17:48:55 -04:00
Mason Daugherty
2a16ee9b73 feat(openrouter): add app_categories field for marketplace attribution (#36205)
Add support for the `X-OpenRouter-Categories` header via a new
`app_categories` field on `ChatOpenRouter`, and extract inline client
construction into a dedicated `_build_client` method.
2026-03-25 10:58:17 -04:00
Mason Daugherty
839f1df333 chore(openrouter): update URL/title (#36204) 2026-03-24 21:11:46 -04:00
Mason Daugherty
2f64d80cc6 fix(core,model-profiles): add missing ModelProfile fields, warn on schema drift (#36129)
PR #35788 added 7 new fields to the `langchain-profiles` CLI output
(`name`, `status`, `release_date`, `last_updated`, `open_weights`,
`attachment`, `temperature`) but didn't update `ModelProfile` in
`langchain-core`. Partner packages like `langchain-aws` that set
`extra="forbid"` on their Pydantic models hit `extra_forbidden`
validation errors when Pydantic encountered undeclared TypedDict keys at
construction time. This adds the missing fields, makes `ModelProfile`
forward-compatible, provides a base-class hook so partners can stop
duplicating model-profile validator boilerplate, migrates all in-repo
partners to the new hook, and adds runtime + CI-time warnings for schema
drift.

## Changes

### `langchain-core`
- Add `__pydantic_config__ = ConfigDict(extra="allow")` to
`ModelProfile` so unknown profile keys pass Pydantic validation even on
models with `extra="forbid"` — forward-compatibility for when the CLI
schema evolves ahead of core
- Declare the 7 missing fields on `ModelProfile`: `name`, `status`,
`release_date`, `last_updated`, `open_weights` (metadata) and
`attachment`, `temperature` (capabilities)
- Add `_warn_unknown_profile_keys()` in `model_profile.py` — emits a
`UserWarning` when a profile dict contains keys not in `ModelProfile`,
suggesting a core upgrade. Wrapped in a bare `except` so introspection
failures never crash model construction
- Add `BaseChatModel._resolve_model_profile()` hook that returns `None`
by default. Partners can override this single method instead of
redefining the full `_set_model_profile` validator — the base validator
calls it automatically
- Add `BaseChatModel._check_profile_keys` as a separate
`model_validator` that calls `_warn_unknown_profile_keys`. Uses a
distinct method name so partner overrides of `_set_model_profile` don't
inadvertently suppress the check

### `langchain-profiles` CLI
- Add `_warn_undeclared_profile_keys()` to the CLI (`cli.py`), called
after merging augmentations in `refresh()` — warns at profile-generation
time (not just runtime) when emitted keys aren't declared in
`ModelProfile`. Gracefully skips if `langchain-core` isn't installed
- Add guard test
`test_model_data_to_profile_keys_subset_of_model_profile` in
model-profiles — feeds a fully-populated model dict to
`_model_data_to_profile()` and asserts every emitted key exists in
`ModelProfile.__annotations__`. CI fails before any release if someone
adds a CLI field without updating the TypedDict

### Partner packages
- Migrate all 10 in-repo partners to the `_resolve_model_profile()`
hook, replacing duplicated `@model_validator` / `_set_model_profile`
overrides: anthropic, deepseek, fireworks, groq, huggingface, mistralai,
openai (base + azure), openrouter, perplexity, xai
- Anthropic retains custom logic (context-1m beta → `max_input_tokens`
override); all others reduce to a one-liner
- Add `pr_lint.yml` scope for the new `model-profiles` package
2026-03-23 00:44:27 -04:00
Mason Daugherty
e91da86efe feat(openrouter): add streaming token usage support (#35559)
Streaming token usage was silently dropped for `ChatOpenRouter`. Both
`_stream` and `_astream` skipped any SSE chunk without a `choices` array
— which is exactly the shape OpenRouter uses for the final
usage-reporting chunk. This meant `usage_metadata` was never populated
on streamed responses, causing downstream consumers (like the Deep
Agents CLI) to show "unknown" model with 0 tokens.

## Changes
- Add `stream_usage: bool = True` field to `ChatOpenRouter`, which
passes `stream_options: {"include_usage": True}` to the OpenRouter API
when streaming — matching the pattern already established in
`langchain-openai`'s `BaseChatOpenAI`
- Handle usage-only chunks (no `choices`, just `usage`) in both
`_stream` and `_astream` by emitting a `ChatGenerationChunk` with
`usage_metadata` via `_create_usage_metadata`, instead of silently
`continue`-ing past them
2026-03-04 15:35:30 -05:00
Mason Daugherty
e50625e7c3 feat(fireworks,groq,openrouter): add standard model property (#35542)
Add a `model` property to `ChatFireworks`, `ChatGroq`, and
`ChatOpenRouter` that returns `model_name`. These partners use
Pydantic's `Field(alias="model")` on `model_name`, which means
`instance.model` doesn't work as a read accessor after construction — it
raises an `AttributeError` or returns the field descriptor. `ChatOpenAI`
already has this property; this brings the remaining in-repo partners to
parity.
2026-03-03 17:28:46 -05:00
Hamza Kyamanywa
f84b534248 feat(openrouter): surface cost and cost_details in response_metadata (#35461)
## Description

OpenRouter returns `cost` and `cost_details` in its API response `usage`
object, providing the actual cost of each API call. Currently,
`_create_usage_metadata()` only extracts token counts and drops these
cost fields.

This PR surfaces both `cost` and `cost_details` in `response_metadata`
for both non-streaming and streaming paths, allowing users to access
actual API costs directly from the response without manual estimation
from token counts.

**Example response from OpenRouter:**
```json
{
  "usage": {
    "prompt_tokens": 100,
    "completion_tokens": 50,
    "cost": 0.000075,
    "cost_details": {
      "upstream_inference_cost": 0.00007745,
      "upstream_inference_prompt_cost": 0.00000895,
      "upstream_inference_completions_cost": 0.0000685
    }
  }
}
```

**After this change:**
```python
result = chat.invoke("hello")
result.response_metadata["cost"]          # 0.000075
result.response_metadata["cost_details"]  # {...}
```

## Changes

- **`_create_chat_result`**: Surface `cost` and `cost_details` from
`token_usage` into `response_metadata` (non-streaming)
- **`_convert_chunk_to_message_chunk`**: Same for streaming
`AIMessageChunk`
- Added `PLR0912` to `noqa` comments (new branches pushed count over
threshold)
- Added two unit tests: one verifying cost fields are present when
returned, one verifying they're absent when not in usage

## Issue

N/A — discovered while integrating OpenRouter in a production pipeline.
The cost data is already returned by the API but was being silently
dropped.

## Dependencies

None.

## Twitter handle

@hamza_kyamanywa

---------

Co-authored-by: Mason Daugherty <mason@langchain.dev>
2026-03-01 18:47:19 -05:00
Mason Daugherty
be1c3fcc1d feat(openrouter): default headers (#35369) 2026-02-20 14:44:06 -05:00
Mason Daugherty
04ec7ff8ff chore(openrouter): bump core ver, silence warning (#35231) 2026-02-15 02:42:36 -05:00
Mason Daugherty
f9fd7be695 feat(openrouter): add langchain-openrouter provider package (#35211)
Add a first-party `langchain-openrouter` partner package
(`ChatOpenRouter`) that wraps the official `openrouter` Python SDK,
providing native support for OpenRouter-specific features that
`ChatOpenAI` intentionally does not handle.

Also adds scope-clarifying docstrings to `ChatOpenAI` / `BaseChatOpenAI`
warning users away from using `base_url` overrides with third-party
providers.

---

Closes #31325
Closes #32967
Closes #32977
Closes #32981
Closes #33643
Closes #33757
Closes #34056
Closes #34797
Closes #34962

Supersedes #33902, #34867 (thank you @elonfeng and @okamototk for your
initial work on this!)

---

Bugs with upstream sdk:
- https://github.com/OpenRouterTeam/python-sdk/issues/38
- https://github.com/OpenRouterTeam/python-sdk/issues/51
- https://github.com/OpenRouterTeam/python-sdk/issues/52
2026-02-15 02:09:13 -05:00