Skip to content
RU

LangChain

Connect LangChain to Mixen — pay from your balance in roubles, a catalog of 105+ models through the familiar ChatOpenAI.

Terminal window
pip install -U langchain-openai

Mixen is OpenAI-protocol compatible — point ChatOpenAI at our base URL and pass your key:

from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="openai/gpt-5.6-sol",
base_url="https://api.mixen.ai/v1",
api_key="your-api-key",
)
print(llm.invoke("What is vector search?").content)

Streaming — deltas arrive as the model generates:

for chunk in llm.stream("Explain RAG in three paragraphs"):
print(chunk.content, end="", flush=True)

Key points:

  • In current langchain-openai versions the parameters are base_url and api_key; openai_api_base and openai_api_key are legacy aliases.
  • Instead of arguments you can set the OPENAI_BASE_URL and OPENAI_API_KEY environment variables — explicit arguments take priority.
  • Tools work: llm.bind_tools([...]) — our /v1/chat/completions supports tool calling.

Scenario: a chain with a system prompt and history

Section titled “Scenario: a chain with a system prompt and history”

A bare invoke is fine for checking the key. A product gets an LCEL chain: a prompt template, the model, and an output parser joined with the | operator:

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.output_parsers import StrOutputParser
from langchain_core.messages import HumanMessage, AIMessage
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="anthropic/claude-sonnet-5",
base_url="https://api.mixen.ai/v1",
api_key="your-api-key",
)
prompt = ChatPromptTemplate.from_messages([
("system",
"You are a tea shop consultant. Answer briefly: "
"the variety, its taste, how to brew it. Never invent inventory items."),
MessagesPlaceholder("history"),
("human", "{input}"),
])
chain = prompt | llm | StrOutputParser()
history: list = []
def ask(question: str) -> str:
answer = chain.invoke({"history": history, "input": question})
history.extend([HumanMessage(content=question), AIMessage(content=answer)])
return answer
print(ask("Suggest a tea for an evening session"))
print(ask("Is there one like it but less astringent?")) # "like it" comes from history

What matters here:

  • StrOutputParser unwraps the AIMessage — the output is a plain string.
  • MessagesPlaceholder("history") inserts the message list between the system prompt and the question; this example tracks history by hand — swap it for RunnableWithMessageHistory when you outgrow that.
  • The system prompt comes first and never changes — that’s deliberate: a stable prefix lands in the prompt cache (see economics).

The same result streamed — straight on the chain:

for chunk in chain.astream({"history": history, "input": question}):
print(chunk, end="", flush=True)

And a fully async version — for services on asyncio:

import asyncio
async def main() -> None:
answer = await chain.ainvoke({"history": history, "input": question})
print(answer)
asyncio.run(main())

Our streaming is real: tokens leave the model as they are generated, so time-to-first-token doesn’t depend on the answer’s length.

Embeddings and reranking over the same key are covered in the separate RAG guide: an index on POST /v1/embeddings, retrieval, LLM-as-judge reranking, and citations in the answer. The answer model there is the same ChatOpenAI with our base_url.

Task Model Price per 1M tokens
Bulk calls: classification, extraction, drafts z-ai/glm-5.3-flash 8.4 ₽ input / 27.9 ₽ output
Hard tasks, long context, careful code anthropic/claude-sonnet-5 213.9 / 1069.5 ₽

The prompt cache is the main savings lever in chains with history: every next turn repeats the system prompt and past messages, and that repeating prefix is billed at ~10% of the input price (for claude-sonnet-5 — 21.4 ₽ per 1M instead of 213.9 ₽, for glm-5.3-flash — 1.7 ₽). The cache lives 5 minutes with an extension from every hit, so a dense dialog gets cheaper turn by turn. The rule is the same as in the example above: the unchanging parts at the start of the prompt, the new content at the end.

Reasoning depth is regulated by the API’s reasoning_effort parameter (from off to max: off, low, medium, high, xhigh, max). The allowed levels of a particular model are in GET /v1/modelscapabilities.reasoning_efforts. Remember that reasoning eats into max_tokens: set a generous ceiling for long structured output.

Model ID
GPT-5.6 Sol openai/gpt-5.6-sol
Claude Opus 5 anthropic/claude-opus-5
GLM 5.3 z-ai/glm-5.3
DeepSeek V4 Pro deepseek/deepseek-v4-pro
Kimi K3 moonshotai/kimi-k3

Full list — in the catalog.

  • 401 / Invalid API Key — the key was copied incompletely or is not a Mixen API key; issue a new one in the dashboard, it starts with mxn-.
  • “model not found” — pass the model ID from the catalog verbatim, including the vendor prefix (openai/…, anthropic/…).
  • Requests go out with the wrong key — if api_key is not passed, the SDK falls back to the OPENAI_API_KEY environment variable (e.g. a real OpenAI key), and Mixen replies with 401.
  • The answer got cut off mid-sentence — on reasoning models the thinking part spends max_tokens; raise the ceiling and check finish_reason ("length" means truncation, not a finished thought).
  • Streaming delivers everything at once — check that the request goes through stream()/astream(), not an invoke() with manual slicing of the finished string.