Tech13 min read

Qwen enable_search works in chat.completions, but URLs need the Responses API

IkesanContents

The Qwen behind my M5Stack voice assistant (async-voice-chat) is a single chat.completions.create call with no search involved. Before thinking about how it should handle questions that need fresh information, I decided to check whether my current Qwen setup can call both 3.7 and 3.8 at all, and what actually comes back when search is turned on.

For connections I reused what I already had: the ModelScope route to Qwen3.7 Plus I set up earlier and the Qwen3.8 Max preview access from the Qwen ambassador program.

Test environment

ItemDetails
ClientApple M1 Max / 64GB / macOS
Language / runnerPython 3.10 / uv
SDKopenai 2.41
Models AQwen3.7 Plus / Qwen3.7 Max (OpenAI-compatible API via ModelScope)
Models BQwen3.8 Max preview (dedicated workspace endpoint on Alibaba Cloud Model Studio)
Call stylesBoth chat.completions.create and responses.create from the openai package

Plain calls first

The simplest possible call to start with, asking each of the three models to introduce itself in one sentence.

from openai import OpenAI

client = OpenAI(api_key=..., base_url=...)
resp = client.chat.completions.create(
    model=MODEL,
    messages=[{"role": "user", "content": "自己紹介を1文でしてください。"}],
)

3.7 Plus, 3.7 Max, and 3.8 all responded normally, and the response message carried reasoning_content. All three are think-then-answer models. For now, that confirmed 3.7 and 3.8 can share the same calling code.

A question that needs search, starting with a simple one. The prompt asks for today’s date plus one concrete piece of generative AI news from the past week.

resp = client.chat.completions.create(
    model=MODEL,
    messages=[{"role": "user", "content": "今日の日付を教えてください。また、直近1週間以内の生成AI関連ニュースを1つ具体的に教えてください。"}],
    extra_body={"enable_search": True},
)

Both 3.7 Plus and 3.7 Max came back with “July 24, 2026” and a concrete story about the 2026 World Artificial Intelligence Conference (WAIC) held in Shanghai. The date matched the actual date. Peeking into reasoning_content, the Chinese word “知识库” (knowledge base) kept appearing while the model narrowed down the date and the news from what looked like search result snippets. Adding enable_search: True through extra_body on the plain OpenAI-compatible chat.completions was enough to get search results reflected in the answer.

Meanwhile message.annotations stayed null every time. Nothing that looks like a search result URL shows up anywhere. The date and the news were real, but there was no way to check from the API response which articles it had read.

A messier question

Next I stuffed different kinds of requests into one question: today’s weather in Tokyo, the current USD/JPY exchange rate, and one concrete update to ComfyUI or a Stable Diffusion tool from the past week.

東京の今日の天気と、今日のドル円の為替レートを教えてください。
そのうえで、直近1週間以内に発表されたComfyUIまたはStable Diffusion系ツールの
アップデートを1つ具体的に教えてください。

Without enable_search, both 3.7 and 3.8 honestly declined, saying they have no real-time search capability.

With enable_search: True, 3.7 Plus honestly answered that “the knowledge base contains no concrete numeric data” for the weather and the exchange rate, and instead picked up a real GitHub project, “Multi-Platform Package Manager for Stable Diffusion” (updated July 14, 2026). The search results appear to be a pile of text like news articles and GitHub metadata, with no structured data from weather or forex APIs, so the numbers themselves never come out.

flowchart TD
    Q[Send question] --> P{extra_body settings}
    P -->|3.7: enable_search only| S1{Searched?}
    P -->|3.8: enable_search + forced_search| S2[Searches]
    S1 -->|Yes| R1[Answers with real info]
    S1 -->|No| R2[Says it cannot tell]
    S2 --> R3[Answers with real info]

enable_search alone did nothing on 3.8

Sending the same extra_body={"enable_search": True} to 3.8, it did not search. Its reasoning_content assumed values like “the current system date is 2026-06-22”, off from the actual date (July 24, 2026), and built the answer on stale information. The assumed date also drifted between calls (2026-06-15 one time, 2026-06-22 another), so instead of consulting search results it was patching a story together on its own and hallucinating.

Adding DashScope’s search_options parameter got 3.8 searching too.

extra_body = {
    "enable_search": True,
    "search_options": {"forced_search": True, "enable_source": True},
}

With this, 3.8 also returned “July 25, 2026” (the actual date was July 24) plus concrete news. Even with enable_source: True, annotations stayed null and no URLs came back.

Naming specific sites

Next I named four sites, HuggingFace, the Qwen official blog, the OpenAI blog, and the Anthropic blog, and asked what had been posted there in the past week, telling the model to honestly say it didn’t know if it didn’t.

3.7 Plus with only enable_search: True answered “I can’t tell, no real-time access” for all four sites and stopped there. It searched for the simple news question, but did not search for this messier one naming multiple sites.

3.8 with search_options.forced_search searched here as well and returned fairly specific content.

For Hugging Face, it was the July 16, 2026 disclosure that part of the production infrastructure had been compromised by an autonomously operating AI agent. Code execution via a malicious dataset stole credentials and moved laterally into internal clusters, and it also touched on OpenAI’s July 21 statement that “a model of ours under evaluation was the cause.” For the Qwen official blog, it brought up the July 21, 2026 announcement of the image generation model Qwen-Image-3.0.

I checked both stories with an actual web search, and both had really been reported. BleepingComputer and The Hacker News covered the HuggingFace breach around July 20-21, and OpenAI acknowledging its model’s involvement matched an Axios article. Qwen-Image-3.0 was indeed announced on July 21, down to the detail that it was an unusual release with no benchmarks or weights published. The search results coming over the API reflected real events, not just plausible-sounding text.

chat.completions never gave up the search URLs

Everything so far with chat.completions + enable_search clearly reads search result text to build its answers, but message.annotations never became anything other than null. search_options.enable_source: True changed nothing. The answers matched the facts, yet there was no way to check from the response which articles had been read.

Just in case, I also tried hitting the public dashscope.aliyuncs.com endpoint directly with the 3.8 API key, and got rejected with 401 Incorrect API key. That key was issued for the ambassador program’s dedicated workspace, and I couldn’t figure out any other way to call 3.8 to compare against.

I changed the method and tried responses.create with tools=[{"type": "web_search"}] instead of chat.completions. The same “model calls search by itself” kind of built-in tool that OpenAI and Anthropic offer also exists on the Qwen side, via the Responses API.

import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["QWEN38_API_KEY"],
    base_url="https://dashscope-intl.aliyuncs.com/api/v2/apps/protocols/compatible-mode/v1",
)

resp = client.responses.create(
    model="qwen3.7-max",
    input="今日の生成AIニュースを検索して、重要なものを3件教えて",
    tools=[{"type": "web_search"}],
)
print(resp.output_text)

With the 3.8 API key, this endpoint responded whether I specified qwen3.8-max-preview or qwen3.7-max. The ModelScope key (QWEN_API_KEY) got 401 InvalidApiKey on the same endpoint, so ModelScope and Alibaba Cloud Model Studio run on separate auth systems.

The returned text had links to the referenced pages embedded directly in the body.

1️⃣ DeepSeek V4 安定版が本日リリース、旧モデルは退役
中国のDeepSeekが、V4の安定版を本日7月24日に正式リリース...
🔗 出典: [DeepSeek API Docs](https://api-docs.deepseek.com/news/news260424/) / [buildfastwithai.com](https://www.buildfastwithai.com/blogs/ai-news-today-july-23-2026)

2️⃣ ChatGPTとClaude、そろって音声機能を大幅強化
(ITmedia / Impress Watch ほか、7月24日)
Claude:音声モードが大幅強化され、Opus・Sonnetなど上位モデルにも対応...
🔗 [ITmedia記事](https://www.itmedia.co.jp/news/articles/2607/24/news078.html) / [Impress Watch記事](https://www.watch.impress.co.jp/docs/news/2127532.html)

The real URLs that never appeared on the chat.completions side came embedded right in the body with the Responses API.

Checking the raw response structure, the search trail itself was preserved as separate elements.

type: reasoning        # thinking before the search
type: web_search_call  # the actual queries and the URLs the search hit
type: reasoning        # thinking after reading the results
type: message          # final answer

Inside web_search_call, queries held the actual search queries (multiple patterns in English and Japanese, like "ComfyUI update July 2026"), and sources held a list of real URLs like forum.comfy.org, docs.comfy.org/changelog, and github.com/comfy-org/ComfyUI/releases. Unlike the chat.completions case, where only the vague words “knowledge base” surfaced in reasoning_content, what was searched and what was consulted is separated out as objects. Here the final message’s annotations was an empty array, so the URLs apparently can end up on the web_search_call side rather than in message.annotations.

OpenAI-compatible chat.completions vs the separate Responses API

As far as I could confirm, there are two routes to get Qwen searching.

RouteInterfaceHow search behavesSearch URLs
chat.completions + extra_bodyOpenAI-compatible3.7 searches with enable_search alone but may skip messier questions. 3.8 needs search_options.forced_searchNot available (annotations always null)
responses.create + tools=[{"type":"web_search"}]Responses API (built-in tool)Queries and result URLs preserved as separate structured elementsReal URLs in the body and in web_search_call.action.sources

I had initially assumed you can’t search through the OpenAI-compatible API and need something else entirely, and that turned out half right and half wrong. Search itself worked fine through chat.completions, but never in a form where the URLs were visible. What actually existed as a separate build was the Responses API’s built-in web_search tool, which carries the queries and the consulted URLs as objects.

The design where enable_search only permits search and leaves the decision to the model or the harness resembles the case where Gemini skipped searching because of a “Do NOT issue search queries” instruction I looked into before. Qwen’s chat.completions behaved the same way here: even with the flag up, it searched or didn’t depending on the question.

For the M5Stack voice assistant, rather than adding enable_search to chat.completions, it looks like the Responses API with the built-in web_search is the route it would take. I don’t have to write my own code to judge whether a question needs search; with tools always attached, the model calls it only when it needs to.

Mixing search into ongoing small talk

In the Qwen3.8 Max preview article I wrote that preserve_thinking is on by default, and that when it’s on, the conversation history’s reasoning_content must be sent back intact with the next request. First I checked this with a simple two-turn conversation, “give me one 5-digit prime” and then “what’s double that number”, once keeping reasoning_content and once dropping it. Both answered “20014” correctly. At least in this simple case, dropping reasoning_content caused no error.

Next I wanted to see whether this can keep running as a small-talk voice assistant. I mixed a search-needing question into chatter about the weather and favorite foods, keeping reasoning_content every turn across a six-turn conversation. extra_body had enable_search plus search_options.forced_search fixed on for every turn.

TurnContentprompt_tokensreasoning_tokens
1Small talk: “hot today, isn’t it”4386156
2Small talk: “do you use ComfyUI?“1624234
3Small talk: “name one favorite food”4864141
4A recipe for that food5055190
5Today’s date plus generative AI news from the past week67102439
6More detail on that news90771619

The small-talk-only turns (1-4) stayed at 150-240 reasoning_tokens, while the search-needing news question (turn 5) shot up to 2439. More than 10x the cost of small talk. prompt_tokens doesn’t grow monotonically either, dropping from 4386 on turn 1 to 1624 on turn 2. Since forced_search is forced every turn, the amount of search text injected varies per call, and token counts go up and down for reasons separate from history accumulation.

On top of that, turn 5 answered today’s date as “June 22, 2026”, off from the correct date (around July 24, 2026) that the standalone search tests had produced. This turn, with piled-up small-talk history plus search, was less accurate on the date than the standalone questions.

Keeping forced_search on for the whole conversation is a no, given this kind of cost and accuracy. If I were to build it, it would end up as the setup I originally had in mind: plain chat.completions during small talk, enabling search only on turns where the question looks like it needs it, with my side making that call.

Image input (VLM), too

The Qwen3.8 Max preview article described it as a multimodal model that handles images, video, and documents, so I tried image input while I was at it. I base64-encoded a Kana image used in a published article and sent it as image_url content.

The Kana standing image used for this test
messages=[{
    "role": "user",
    "content": [
        {"type": "text", "text": "この画像には何が写っていますか。髪型、服装、ポーズを具体的に説明してください。"},
        {"type": "image_url", "image_url": {"url": f"data:image/webp;base64,{b64}"}},
    ],
}]

3.7 Plus and 3.8 both described the image accurately. Side ponytail with a blue scrunchie, an ahoge on top, white shirt with a red necktie, navy pleated skirt, black knee socks, brown loafers. All of it matched the actual image.

3.7 Max, on the other hand, crashed through the openai SDK with TypeError: 'NoneType' object is not subscriptable. Looking at the raw response via requests, it was an empty response: HTTP 200 but choices was null and usage was all zeros.

I had assumed 3.7 Max was also VLM-capable, so I converted the image to JPEG and retried, and also tried the same qwen3.7-max on the dashscope-intl Responses API. That one returned a clear error message.

InvalidParameter: The current model only supports text modality and
does not support image input. Please remove the image content from
your messages and retry.

At least the qwen3.7-max reachable through the ambassador program does not accept image input. How the public API’s qwen3.7-max behaves is unknown (my key doesn’t even get through, so I can’t test it). The empty responses via ModelScope look less like a bug and more like the result of receiving input the model doesn’t support.

I then pulled the ModelScope model list with client.models.list() and found Qwen/Qwen3-VL-235B-A22B-Instruct and Qwen/Qwen3-VL-8B-Instruct, dedicated models with VL (Vision-Language) in their names, sitting outside the ambassador program. Throwing the same image at these two, both described it accurately.

I’m not sure whether I’ll be able to keep using 3.8 after this, so basically 3.7 Plus will be handling things, and for what I currently have in mind that looks fine.