159 lines
8.4 KiB
Markdown
159 lines
8.4 KiB
Markdown
|
|
# Model Integration Issues
|
||
|
|
|
||
|
|
## Issue 1: What problems arise from using ChatOpenAI directly?
|
||
|
|
|
||
|
|
For OpenAI-compatible models, the simplest approach is to reuse `ChatOpenAI` from `langchain-openai`. Using Qwen as an example:
|
||
|
|
|
||
|
|
```python
|
||
|
|
from langchain_openai import ChatOpenAI
|
||
|
|
|
||
|
|
llm = ChatOpenAI(
|
||
|
|
model="qwen-max",
|
||
|
|
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||
|
|
api_key=os.getenv("DASHSCOPE_API_KEY"),
|
||
|
|
)
|
||
|
|
```
|
||
|
|
|
||
|
|
This is the fastest way to get started, but in production it exposes the following issues:
|
||
|
|
|
||
|
|
- **`reasoning_content` (the thinking process) is silently dropped** — `_create_chat_result` and `_convert_chunk_to_generation_chunk` don't recognize this field, so neither streaming nor non-streaming responses surface the reasoning content.
|
||
|
|
- **The reasoning context is broken across multi-turn conversations** — `reasoning_content` in an AIMessage can't be sent back to the model. `_get_request_payload` doesn't handle `additional_kwargs["reasoning_content"]`, so subsequent turns lose the previous turn's reasoning chain.
|
||
|
|
- **An empty `tools: []` triggers provider errors** — some providers strictly validate empty arrays, but `ChatOpenAI` still sends the field even when no tools are bound.
|
||
|
|
|
||
|
|
So `ChatOpenAI` is suitable only for **quick verification** and **simple conversations without needing to display reasoning**. Once reasoning models or multi-turn reasoning chains are involved, a deeper adapter is needed.
|
||
|
|
|
||
|
|
## Issue 2: How to robustly integrate OpenAI-compatible reasoning models
|
||
|
|
|
||
|
|
To address the issues above, two approaches are recommended. Ask the user which they prefer before proceeding:
|
||
|
|
|
||
|
|
<!-- query
|
||
|
|
type: choice
|
||
|
|
question: "Which approach do you prefer for integrating reasoning models?"
|
||
|
|
options:
|
||
|
|
- label: "Code generation"
|
||
|
|
description: "Generate a custom integration class in your repo via the CN Model Integration Guide — full control, easy to customize"
|
||
|
|
- label: "Third-party library"
|
||
|
|
description: "Install langchain-dev-utils and use its built-in adapters — zero hand-written code"
|
||
|
|
default: 1
|
||
|
|
-->
|
||
|
|
|
||
|
|
- **Approach 1 — Code generation via guide**: use the [CN Model Integration Guide](cn-models/README.md) to generate an integration class into the project repo. Best for teams that want full control and easy customization.
|
||
|
|
- **Approach 2 — Third-party library**: install `langchain-dev-utils` and use its built-in adapters. Best for teams that prefer zero hand-written adapter code.
|
||
|
|
|
||
|
|
### Approach 1: Use the CN Model Integration Guide to generate integration classes
|
||
|
|
|
||
|
|
Follow the [CN Model Integration Guide](cn-models/README.md) to generate (via AI Coding) an integration class that subclasses `BaseChatOpenAI` and fixes all critical methods in one shot:
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Using Qwen as an example, the generated class fixes all the issues above:
|
||
|
|
from models.qwen import ChatQwen
|
||
|
|
|
||
|
|
llm = ChatQwen(model="qwen-max")
|
||
|
|
# reasoning_content automatically preserved, empty tools automatically removed, JSONDecodeError friendly hints
|
||
|
|
```
|
||
|
|
|
||
|
|
The generated integration class covers:
|
||
|
|
|
||
|
|
- `_get_request_payload`: reasoning_content round-tripping + removal of empty tools
|
||
|
|
- `_create_chat_result`: extracting reasoning_content from non-streaming responses
|
||
|
|
- `_convert_chunk_to_generation_chunk`: extracting reasoning_content from streaming deltas
|
||
|
|
- `_stream` / `_astream` / `_generate` / `_agenerate`: unified JSONDecodeError handling
|
||
|
|
|
||
|
|
**DeepSeek special path**: if `langchain-deepseek` is installed, the skill generates a subclass of the official class, only adding reasoning_content round-tripping while reusing the official implementation. Providers like Qwen with no official integration class inherit `BaseChatOpenAI` directly.
|
||
|
|
|
||
|
|
Suitable for: teams that want the adapter logic to live in their code repo, easy to read and customize.
|
||
|
|
|
||
|
|
### Approach 2: Use the third-party community library langchain-dev-utils
|
||
|
|
|
||
|
|
`langchain-dev-utils` is a third-party `langchain` ecosystem toolkit with built-in deep adapters for OpenAI-compatible models, removing the cost of hand-writing subclasses.
|
||
|
|
|
||
|
|
First install the standard version:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
pip install -U langchain-dev-utils[standard]
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2.1 Dynamically generate integration classes via a factory function
|
||
|
|
|
||
|
|
Use the `create_openai_compatible_model` factory function to dynamically generate a chat model class at runtime:
|
||
|
|
|
||
|
|
```python
|
||
|
|
from langchain_dev_utils.chat_models.adapters import create_openai_compatible_model
|
||
|
|
|
||
|
|
ChatQwen = create_openai_compatible_model(
|
||
|
|
model_provider="qwen",
|
||
|
|
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||
|
|
chat_model_cls_name="ChatQwen",
|
||
|
|
compatibility_options={
|
||
|
|
"supported_tool_choice": ["auto","none", "specific"],
|
||
|
|
"supported_response_format": ["json_schema"], # When enabled, with_structured_output defaults to json_schema
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
model = ChatQwen(model="qwen3-max", reasoning_keep_policy="current")
|
||
|
|
```
|
||
|
|
|
||
|
|
Environment variables follow the `${PROVIDER_NAME}_API_BASE` / `${PROVIDER_NAME}_API_KEY` naming convention; omitting `base_url` reads them automatically.
|
||
|
|
|
||
|
|
The function is built on `BaseChatOpenAI`, with the main enhancements being:
|
||
|
|
|
||
|
|
- **Reasoning field extraction and round-tripping**: automatically parses `reasoning_content` / `reasoning`, with the `reasoning_keep_policy` (`never` / `current` / `all`) controlling how reasoning is retained in historical messages — fits Interleaved Thinking and Preserved Thinking.
|
||
|
|
- **tool_choice differential adaptation**: use `supported_tool_choice` to declare which strategies the provider supports; unsupported values are filtered out instead of being forwarded and triggering errors.
|
||
|
|
- **Dynamic structured-output selection**: based on `supported_response_format`, automatically pick the best strategy between `json_schema` and `function_calling`; declaring `json_schema` automatically sets `model.profile.structured_output` to `True`, integrated with `create_agent`.
|
||
|
|
- **video content_block support**: fills in the video-type multimodal capability missing from `ChatOpenAI`.
|
||
|
|
- **Model profiles**: pass a `profile` at creation or instantiation, so higher-level components like `create_agent` can sense model capabilities.
|
||
|
|
|
||
|
|
> **Note**: under the hood it uses pydantic `create_model`, which has dynamic-creation overhead, and the profiles dict is global. Create integration classes once at project startup to avoid repeated runtime regeneration.
|
||
|
|
|
||
|
|
#### 2.2 The registration-based style aligned with `init_chat_model`
|
||
|
|
|
||
|
|
The factory function in 2.1 requires the business side to explicitly hold a concrete class like `ChatQwen`. If you prefer LangChain's native `init_chat_model("provider:model")` "model by string" initialization style, `langchain-dev-utils` provides an equivalent experience — just use `register_model_provider` to register an OpenAI-compatible model under a unified entry point and set `chat_model` to `"openai-compatible"`. Internally it calls the `create_openai_compatible_model` above to build the integration class, and the business side no longer needs to reference the model class directly.
|
||
|
|
|
||
|
|
**Method 1: Pass arguments explicitly**
|
||
|
|
|
||
|
|
```python
|
||
|
|
from langchain_dev_utils.chat_models import register_model_provider
|
||
|
|
|
||
|
|
register_model_provider(
|
||
|
|
provider_name="qwen",
|
||
|
|
chat_model="openai-compatible",
|
||
|
|
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||
|
|
)
|
||
|
|
```
|
||
|
|
|
||
|
|
**Method 2: Via environment variables (recommended for config management)**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
export QWEN_API_BASE=https://dashscope.aliyuncs.com/compatible-mode/v1
|
||
|
|
export QWEN_API_KEY=sk-xxx
|
||
|
|
```
|
||
|
|
|
||
|
|
```python
|
||
|
|
from langchain_dev_utils.chat_models import register_model_provider
|
||
|
|
|
||
|
|
register_model_provider(
|
||
|
|
provider_name="qwen",
|
||
|
|
chat_model="openai-compatible",
|
||
|
|
# Auto-reads QWEN_API_BASE / QWEN_API_KEY
|
||
|
|
)
|
||
|
|
```
|
||
|
|
|
||
|
|
Parameters like `base_url`, `compatibility_options`, and `model_profiles` from `create_openai_compatible_model` are also passed through — usage is identical to calling the factory function directly:
|
||
|
|
|
||
|
|
```python
|
||
|
|
from langchain_dev_utils.chat_models import register_model_provider
|
||
|
|
|
||
|
|
register_model_provider(
|
||
|
|
provider_name="qwen",
|
||
|
|
chat_model="openai-compatible",
|
||
|
|
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||
|
|
compatibility_options={
|
||
|
|
"supported_tool_choice": ["auto", "none","specific"],
|
||
|
|
"supported_response_format": ["json_schema"],
|
||
|
|
},
|
||
|
|
model_profiles=model_profiles,
|
||
|
|
)
|
||
|
|
```
|
||
|
|
|
||
|
|
After registration, business code can initialize models through the unified entry point `load_chat_model("qwen:qwen3-max")` — this is the usage pattern aligned with `init_chat_model()`. The call sites no longer couple to specific class names, `base_url`, or `api_key`.
|