LangChain and LangGraph
LangChain agents and LangGraph graphs run on Anyray with a base URL and a client key. There is no package to install.
Console Users > Agents > Add agent. Name it, then open the LangChain tab, or LangGraph for a graph.
Copy the tab's prompt and give it to the coding agent working on your app. The code it writes
reads ANYRAY_GATEWAY_URL and ANYRAY_CLIENT_KEY. Put the key in the app's environment
yourself; never paste it into the chat. The prompt ends by polling /connect/self-check until
problems is 0.
The prompts are also served at https://app.anyray.ai/agents/langchain.md and
https://app.anyray.ai/agents/langgraph.md, where the agent asks you for the gateway URL.
The prompt always wires the Anyray MCP server. Trimming in your app's tool loop is the gateway
setting harnessRetrieval, on by default
(Console → Optimizer → Harness retrieval). While it is on, the gateway trims tool output
behind handles the model reads back. An admin can turn it off; the gateway then trims nothing in
the loop and keeps the retrieval tools it declares away from the model. An app that reads local
files and takes input only from trusted people gets the anyray-connect stdio server, which adds
batch reads (anyray_read_batch).
Open Wire it manually on the same tab. It prints the code below with your gateway and key filled in. Use the tab for the package your app already uses.
- Anthropic Python
- OpenAI Python
- Anthropic TypeScript
- OpenAI TypeScript
import asyncio
from langchain.agents import create_agent
from langchain.tools import tool
from langchain_anthropic import ChatAnthropic
@tool
def ping(text: str) -> str:
"""Echo the text back."""
return text
tools = [ping]
model = ChatAnthropic(
model="claude-sonnet-5-5",
base_url="https://gateway.example.com",
api_key="ark_synthetic-123456",
default_headers={"x-anyray-metadata": '{"tool":"langchain"}'},
)
agent = create_agent(model, tools=tools)
# In a notebook, await the call instead of asyncio.run.
result = asyncio.run(agent.ainvoke({"messages": [{"role": "user", "content": "Ping hello"}]}))
print(result["messages"][-1].content)
import asyncio
from langchain.agents import create_agent
from langchain.tools import tool
from langchain_openai import ChatOpenAI
@tool
def ping(text: str) -> str:
"""Echo the text back."""
return text
tools = [ping]
model = ChatOpenAI(
model="gpt-6.1-sol",
base_url="https://gateway.example.com/v1",
api_key="ark_synthetic-123456",
default_headers={"x-anyray-metadata": '{"tool":"langchain"}'},
)
agent = create_agent(model, tools=tools)
# In a notebook, await the call instead of asyncio.run.
result = asyncio.run(agent.ainvoke({"messages": [{"role": "user", "content": "Ping hello"}]}))
print(result["messages"][-1].content)
import { createAgent, tool } from 'langchain';
import { ChatAnthropic } from '@langchain/anthropic';
import { z } from 'zod';
const ping = tool(({ text }) => text, {
name: 'ping',
description: 'Echo the text back.',
schema: z.object({ text: z.string() }),
});
const tools = [ping];
const model = new ChatAnthropic({
model: "claude-sonnet-5-5",
apiKey: "ark_synthetic-123456",
anthropicApiUrl: "https://gateway.example.com",
clientOptions: {
defaultHeaders: { 'x-anyray-metadata': '{"tool":"langchain"}' },
},
});
const agent = createAgent({ model, tools });
const result = await agent.invoke({ messages: [{ role: 'user', content: 'Ping hello' }] });
console.log(result.messages.at(-1)?.content);
import { createAgent, tool } from 'langchain';
import { ChatOpenAI } from '@langchain/openai';
import { z } from 'zod';
const ping = tool(({ text }) => text, {
name: 'ping',
description: 'Echo the text back.',
schema: z.object({ text: z.string() }),
});
const tools = [ping];
const model = new ChatOpenAI({
model: "gpt-6.1-sol",
apiKey: "ark_synthetic-123456",
configuration: {
baseURL: "https://gateway.example.com/v1",
defaultHeaders: { 'x-anyray-metadata': '{"tool":"langchain"}' },
},
});
const agent = createAgent({ model, tools });
const result = await agent.invoke({ messages: [{ role: 'user', content: 'Ping hello' }] });
console.log(result.messages.at(-1)?.content);
The Anthropic package takes the bare gateway origin; the OpenAI package needs /v1. The header
labels the traffic in the console. The key already carries the agent's name and team.
Within a minute the dialog's last step reads Connected, and the agent's row on the
Agents card reads Active · forwarded unchanged, or Active · no tool calls for a
chain without tools. An app with the Anyray MCP server reads Active, unless an admin turned
harnessRetrieval off.
LangGraph
The LangGraph tab prints this graph. The model is built as in step 3, with "tool":"langgraph"
in the header.
- Python
- TypeScript
import asyncio
from langchain_core.tools import tool
from langchain_anthropic import ChatAnthropic
from langgraph.graph import START, MessagesState, StateGraph
from langgraph.prebuilt import ToolNode, tools_condition
@tool
def ping(text: str) -> str:
"""Echo the text back."""
return text
tools = [ping]
model = ChatAnthropic(
model="claude-sonnet-5-5",
base_url="https://gateway.example.com",
api_key="ark_synthetic-123456",
default_headers={"x-anyray-metadata": '{"tool":"langgraph"}'},
).bind_tools(tools)
async def agent(state: MessagesState):
return {"messages": [await model.ainvoke(state["messages"])]}
builder = StateGraph(MessagesState)
builder.add_node("agent", agent)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", tools_condition)
builder.add_edge("tools", "agent")
graph = builder.compile()
# In a notebook, await the call instead of asyncio.run.
result = asyncio.run(graph.ainvoke({"messages": [{"role": "user", "content": "Ping hello"}]}))
print(result["messages"][-1].content)
import { tool } from '@langchain/core/tools';
import { ChatAnthropic } from '@langchain/anthropic';
import { MessagesAnnotation, START, StateGraph } from '@langchain/langgraph';
import { ToolNode, toolsCondition } from '@langchain/langgraph/prebuilt';
import { z } from 'zod';
const ping = tool(({ text }) => text, {
name: 'ping',
description: 'Echo the text back.',
schema: z.object({ text: z.string() }),
});
const tools = [ping];
const model = new ChatAnthropic({
model: "claude-sonnet-5-5",
apiKey: "ark_synthetic-123456",
anthropicApiUrl: "https://gateway.example.com",
clientOptions: {
defaultHeaders: { 'x-anyray-metadata': '{"tool":"langgraph"}' },
},
}).bindTools(tools);
const graph = new StateGraph(MessagesAnnotation)
.addNode('agent', async (state) => ({ messages: [await model.invoke(state.messages)] }))
.addNode('tools', new ToolNode(tools))
.addEdge(START, 'agent')
.addConditionalEdges('agent', toolsCondition)
.addEdge('tools', 'agent')
.compile();
const result = await graph.invoke({ messages: [{ role: 'user', content: 'Ping hello' }] });
console.log(result.messages.at(-1)?.content);
| In your graph | What happens |
|---|---|
A fixed sessionId in each node's header, such as "sessionId":"ping-graph.agent" | That node's calls share one row on the Sessions page, run after run. On the OpenAI package it is the only way they group |
| A node whose model binds no tools | Its requests are never trimmed, even after earlier nodes trimmed theirs |
Send fan-out | Branches that open with the same system prompt and first message share one session until each makes its first tool call |
| An interrupt and resume | The resumed run keeps its session |
LangGraph Platform (langgraph.json) | The model options stay in your graph code; put the key in the deployment's environment |
thread_id as the sessionIdA thread_id or any other per-run id changes every run. On OpenAI models sessionId also becomes
the prompt-cache key, so a per-run value sends every run to a cold cache.
Why, and what to send instead.
Tools and retrieval
Tool calls go through unchanged, so there is nothing to add. The agent row reads Active · forwarded unchanged, or Active · no tool calls for a chain with no tools (RAG, summarization).
Add the gateway's MCP tools with langchain-mcp-adapters when you want to:
- call your organization's shared connectors (keep just the
anyray-connectorsentry below), - read back trimmed output, which
harnessRetrievalallows by default, or - fix an MCP disconnected row after an admin opted tool loops into gateway-served retrieval.
On a served deployment the gateway answers retrieval itself, except in three setups:
| Your app uses | Why the gateway stands aside |
|---|---|
thinking on ChatAnthropic | A thinking block's signature binds to rounds your app never sees |
ChatOpenAI with use_responses_api=True | That lane is served only with store=False |
A forced tool choice, which includes most structured output (with_structured_output, TypeScript responseFormat) | The model may not pick the retrieval tool |
Replace the tools line in the script with:
- Python
- TypeScript
# Replace the tools line above with these.
from langchain_mcp_adapters.client import MultiServerMCPClient
mcp = MultiServerMCPClient({
"anyray": {
"transport": "streamable_http",
"url": "https://gateway.example.com/mcp",
"headers": {
"x-anyray-api-key": "ark_synthetic-123456",
"x-anyray-metadata": "{\"tool\":\"langchain\"}"
}
},
"anyray-connectors": {
"transport": "streamable_http",
"url": "https://gateway.example.com/mcp/org",
"headers": {
"x-anyray-api-key": "ark_synthetic-123456",
"x-anyray-metadata": "{\"tool\":\"langchain\"}"
}
}
})
# In a notebook, await the call instead of asyncio.run.
tools = [ping, *asyncio.run(mcp.get_tools())]
// Replace the tools line above with these.
import { MCPAdapter } from '@langchain/mcp-adapters';
const mcp = new MCPAdapter({
"servers": {
"anyray": {
"transport": "http",
"url": "https://gateway.example.com/mcp",
"headers": {
"x-anyray-api-key": "ark_synthetic-123456",
"x-anyray-metadata": "{\"tool\":\"langchain\"}"
}
},
"anyray-connectors": {
"transport": "http",
"url": "https://gateway.example.com/mcp/org",
"headers": {
"x-anyray-api-key": "ark_synthetic-123456",
"x-anyray-metadata": "{\"tool\":\"langchain\"}"
}
}
}
});
const tools = [ping, ...(await mcp.listTools())];
// Reuse mcp for the life of the process; await mcp.close() when it shuts down.
In a graph the header names langgraph. The adapter's Python tools are async-only, which is why
the scripts call ainvoke. /mcp reads trimmed content back and /mcp/org serves your
organization's shared connectors. Both sit on the gateway origin, not under /v1. Rarer cases are in the
full list.
Streaming
.stream(), .astream() and LangGraph's stream_mode values work unchanged. On a deployment that
serves retrieval for tool loops, a reply's tool calls arrive together when the model finishes
instead of token by token. Token usage comes once, on the last chunk, even if your client did not
ask for it.
Set it up with environment variables
export ANTHROPIC_BASE_URL="https://gateway.example.com"
export ANTHROPIC_API_KEY="ark_synthetic-123456"
export OPENAI_BASE_URL="https://gateway.example.com/v1"
export OPENAI_API_KEY="ark_synthetic-123456"
ChatAnthropic and ChatOpenAI read these when you pass no base URL or key. A variable cannot
carry the x-anyray-metadata header, so the console lists this traffic as Shell / SDK
rather than LangChain or LangGraph.
Deep Agents
A Deep Agents app takes the model from step 3. A subagent that names its own model needs the same base URL, key and header. It does not need Anyray's built-in skill, which is for coding tools on enrolled machines.
LangSmith tracing
With LANGSMITH_TRACING=true, LangChain sends each run's prompts and responses to LangSmith
under your LangSmith key, whether or not you route through Anyray. Leave it unset if prompt
content must stay in your network.