94 Commits

Author SHA1 Message Date
huidongchen148
612140f183
feat(models): add managed Kimi K3.1 Pro capabilities (#7566)
Declare required thinking, native reasoning effort and image support for both
K3.1 Pro IDs. Normalize fixed parameters at the final Moonshot request boundary
and cover real SDK serialization with offline regression tests.

Fixes #7425
2026-10-27 14:57:29 +08:00
lukyturnflow
aa6fb7b67f
fix(models): preserve Moonshot cache_write_tokens in usage metadata (#7428)
* fix(models): preserve Moonshot cache_write_tokens in usage metadata

langchain-openai 1.2.1's _create_usage_metadata reads
prompt_tokens_details.cached_tokens but not cache_write_tokens, so the
PatchedChatDeepSeek conversion dropped the cache-creation count that Kimi K3.1
reports, and DeerFlow's usage journal never saw it (#7426).

PatchedChatDeepSeek now overrides _create_chat_result to restore the raw SDK
cache_write_tokens as input_token_details["cache_creation"].

Offline regression: a real OpenAI ChatCompletion carrying
prompt_tokens_details {cached_tokens: 100, cache_write_tokens: 256} converts to
input_token_details {cache_read: 100, cache_creation: 256} (red before, green
after); the existing 14 patched_deepseek tests and the managed-provider suites
stay green.

* fix(models): restore cache_write_tokens on the streaming path too

Streamed runs (web UI messages-tuple) build usage metadata in
_convert_chunk_to_generation_chunk -> _create_usage_metadata, which ignores
cache_write_tokens, so the non-streaming-only override still lost the count on
the path streamed runs actually take (review follow-up on #7428).

_convert_chunk_to_generation_chunk now mirrors _create_chat_result's restore
for the final usage chunk. Note chunk["usage"] is JSON null on non-final
chunks, so the lookup uses `or {}` instead of a .get default (a None usage
would otherwise raise AttributeError inside every chunk and break
test_model's connection probe).
2026-10-25 19:04:13 +08:00
CodeAlex
4f9a76d5ef
fix(mindie): decode escaped newlines correctly inside tilde-fenced code blocks (#7285)
* fix(stream): tolerate ~~~-fenced code and \-prefixed closer when decoding escaped newlines

_decode_escaped_newlines_outside_fences only recognised ``` fences, so replies
that used ~~~ fences (e.g. code blocks rendered by the model with
~~~python ...) kept literal backslash-n sequences escaped instead of decoding
the ones outside fences, and a fence whose closer was split as backslash+tilde
across chunks was misread mid-stream. Fixes #7262.

- detect the opening fence with (?:```|~~~) and remember which delimiter the
  fence was opened with, so closing requires the same delimiter
- while inside a fence, hold back a closer that begins with a backslash so a
  fence ending in backslash+tilde mid-stream is not misread; the held tail is
  revisited on the next push and normalised on flush
- keep backtick hold semantics unchanged

* fix(mindie): preserve unclosed fences during newline decoding

Match one-shot decoding to streaming by choosing the earliest backtick or tilde opener and preserving the remainder when its matching closer is absent. Correct the string-return docstring and cover mixed delimiters across every two-chunk split and character-by-character streaming.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-24 11:41:35 +08:00
Tsai Yuan
9f1fd2fc05
fix(models): preserve escapes in MindIE tilde fences (#7264) 2026-10-23 20:59:38 +08:00
derrickLzw
1278c5192b
fix(models): honor HTTP-date Retry-After hints in Claude retries (#7295)
* fix(models): honor HTTP-date Retry-After hints in Claude retries

* refactor(models): share bounded HTTP-date retry parsing

* test(agents): move retry date regression to middleware suite
2026-10-23 15:21:44 +08:00
Totoro
0baa99ca90
feat(models): add scoped model controls to Web chats (#7249)
* test: stabilize reporter evidence and guard missing chat nodes

* feat(models): scope Web model choices to tasks, turns and conversations

* test(e2e): mock model-control preview for ordinary chat submissions

* fix(models): harden scoped choices and stale Agent submissions

---------

Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
2026-10-22 08:08:23 +08:00
Starchaser1024
a83aa36309
fix(models): fall through an unusable credential source instead of ending the lookup (#7140)
* fix(models): fall through an unusable credential source instead of ending the lookup

load_claude_code_credential's env branch returned None even when
_credential_from_direct_token rejected the value, so a valid
~/.claude/.credentials.json was never consulted for a blank token.

load_codex_cli_credential's 'or' chain stops at the first truthy layout,
so a blank or non-string top-level access_token hid a usable 'token' or
'tokens.access_token' in the same file -- and made the loader disagree
with scripts/doctor.py, which validates all three layouts.

Fixes #7139

* fix(models): try both env token sources and log the rejected one

A whitespace-only CLAUDE_CODE_OAUTH_TOKEN is rejected by _credential_from_direct_token, but os.getenv(A) or os.getenv(B) never reads ANTHROPIC_AUTH_TOKEN while A is truthy, so the blank first variable hid a usable second one. Test each source with the same non-blank rule, and log the rejected variable at debug (no token material) so the resolution outcome stays observable.

Also refresh the doctor.py mirror comment: the loader rejects whitespace-only tokens in every layout now, so the documented strictness divergence is gone.

* style(tests): format credential fallback log assertion

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-20 10:30:52 +08:00
OUAO-FRANK
5e5913f4cf
fix(codex): honor Retry-After in the provider retry loop (#7151)
* fix(codex): honor Retry-After in the provider retry loop

* style(codex): format retry-after regression helper

* fix(codex): honor HTTP-date Retry-After hints

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-20 10:01:47 +08:00
yangfucheng123
3aa144a377
fix(managed-models): map DeepSeek "minimal" effort to "low" (#6717)
The official DeepSeek Chat Completions endpoint rejects the generic
"minimal" effort token (issue #4514). The declared effort contract for
DeepSeek models now advertises the provider's real vocabulary
(low/medium/high/max/xhigh), defaults to "low", and aliases the generic
"minimal" preset onto "low" so the "flash" dialogue mode serializes a
value the endpoint accepts.

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-19 15:45:56 +08:00
哈基米
12e619c233
fix(models): tolerate non-mapping when_thinking_enabled.extra_body on the reasoning-contract path (#7039)
* fix(models): tolerate non-mapping when_thinking_enabled.extra_body on the reasoning-contract path

* docs: keep frontend guidance below the hard size limit

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-18 20:27:17 +08:00
kuku
a6b3552362
fix(models): preserve Codex tool_choice in requests (#6898)
* fix(models): preserve Codex tool_choice in requests

Signed-off-by: kuku <186267668+xbzz1018@users.noreply.github.com>

* fix(models): validate Codex tool selection before HTTP

Signed-off-by: kuku <186267668+xbzz1018@users.noreply.github.com>

---------

Signed-off-by: kuku <186267668+xbzz1018@users.noreply.github.com>
Co-authored-by: kuku <186267668+xbzz1018@users.noreply.github.com>
2026-10-15 09:58:16 +08:00
万家良
11979fdb57
fix(models): route gpt-6 tool calls through the OpenAI Responses API by default (#6863)
* fix(models): route gpt-6 tool calls through the OpenAI Responses API by default

The locked langchain-openai==1.2.1 routes gpt-6 models configured with
langchain_openai:ChatOpenAI through /v1/chat/completions, where OpenAI
rejects function tools for the gpt-6 family with an HTTP 400. Upstream
only added the gpt-6 + tools -> Responses API branch in 1.7.x, so
backport that routing decision at the model factory instead of bumping
the dependency (large uv.lock churn, wider regression surface).

_apply_openai_responses_api_default() injects use_responses_api: true +
output_version: responses/v1 for BaseChatOpenAI subclasses whose model
starts with gpt-6, unless the operator configured an explicit value.

Fixes #6841

* fix(models): narrow gpt-6 Responses API default per review

Addresses review findings on #6863:

- Custom endpoints (base_url/openai_api_base) are never touched, so
  OpenAI-compatible gateways/proxies (LiteLLM, vLLM, one-api) keep their
  chat-completions routing instead of being silently re-pointed at /responses.
- AzureChatOpenAI is skipped too (MRO-based, so env-configured Azure and any
  subclass are covered): Azure's Responses API requires api-version >=
  2025-04-01-preview.
- An explicit use_responses_api still always wins; a lone explicit
  output_version is preserved while use_responses_api: true is still injected,
  so gpt-6 no longer silently falls back to /v1/chat/completions.
- The default injection is logged so operators can trace the /responses switch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(models): treat env-var-wired gateways as custom endpoints for gpt-6

Addresses the remaining review findings on #6863:

- A gateway wired purely via OPENAI_BASE_URL/OPENAI_API_BASE (the openai SDK
  fallback when no base_url is passed) is now treated like an explicit
  endpoint, so a gpt-6 model behind an env-var-wired gateway is not silently
  re-pointed at <gateway>/responses. The known api_base-subclass blind spot is
  documented in the comment block.

- Regression tests pin the env-var endpoint gate and the explicit-wins
  precedence (an explicit use_responses_api still wins on a custom endpoint).

* test(models): isolate Responses routing from endpoint environment

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-15 09:53:19 +08:00
哈基米
631c524e4b
fix(mindie): degrade unserializable tool args to str() in XML replay (#6839)
* fix(mindie): degrade unserializable tool args to str() in XML replay

The XML parser's literal_eval fallback puts values json.dumps rejects
(e.g. Python sets from set-literal arguments) into AIMessage tool_calls
args. Replaying that message from history hit the unguarded json.dumps
in _fix_messages' args join, raising TypeError and failing the whole
request — and every later turn, since the poisoned message rides the
history. Mirror the tool-result json block handling already in the same
function: degrade the value to str() so one unserializable argument
cannot fail the request.

* fix(mindie): reject non-finite floats in literal sets and tuples

* fix(mindie): validate literal dictionary keys recursively

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-14 09:14:20 +08:00
哈基米
a4be043aec
fix(models): tolerate null template keys in the legacy thinking disable path (#6838)
* fix(models): tolerate null template keys in the legacy thinking disable path

`ModelConfig.when_thinking_enabled` is a plain `dict | None` with no
per-key validation, so a template key left blank in config.yaml
(`extra_body:`, `thinking:`) loads as an explicit None. The legacy
disable path read those keys with `effective_wte.get(key, {})`, whose
default only applies to an absent key — so it called `.get` on None and
every create_chat_model for the profile crashed with
AttributeError: 'NoneType' object has no attribute 'get', on the
default thinking_enabled=False path (title generation, summarization,
subagent and lead-agent builds).

Guard the three reads with the same `or {}` idiom the sibling accesses
in this module already use (`_infer_dialect`, the contract path), so a
null value falls through the dialect heuristics like an absent one and
the legacy path matches the contract path, which already tolerates the
same profile shape.

* fix(models): tolerate null nested thinking templates

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-14 08:44:59 +08:00
Eilen Shin
e98179a9d3
feat(models): register managed models with provider presets (#6757)
* feat(models): register managed models with provider presets

* docs: add PR 6757 UI comparison screenshots

* test: fix model management CI checks

* docs: remove PR 6757 screenshots

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-13 08:14:23 +08:00
Dragon ze
6709c58256
refactor(models): give reasoning capture plumbing a single owner (#6639) 2026-10-11 10:53:00 +08:00
Daoyuan Li
215758818e
fix(models): keep Claude retries behind request admission (#6545)
* fix(models): keep Claude retries behind request admission

* fix(models): keep Codex retries behind admission and log overrides

* fix(models): retry paced Codex HTTP 529 errors through middleware

* test(jina): allow independent HTTPX options in compatibility checks

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-09 21:55:42 +08:00
kuku
845af59301
fix(models): preserve MiniMax reasoning_content responses (#6071)
* fix(models): preserve MiniMax reasoning_content responses

* fix(models): preserve whitespace MiniMax reasoning deltas

* test(models): pin MiniMax reasoning precedence

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-09 16:00:32 +08:00
Daoyuan Li
61ce6d2b96
fix(vllm): normalize thinking switches without mutating request bodies (#6496)
* fix(vllm): preserve caller-owned thinking request bodies

* test(vllm): cover reused thinking bodies in sync and async streams

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-08 20:41:40 +08:00
Daoyuan Li
46eecf2950
fix(models): align MindIE synchronous streaming compatibility (#6497) 2026-10-08 19:30:44 +08:00
uhhgxg
9472579ef0
fix(middleware): guard OverflowError when parsing non-finite Retry-After hints (#6455)
* fix(middleware): guard OverflowError when parsing non-finite Retry-After hints

* fix(middleware): guard OverflowError when parsing non-finite Retry-After hints

* fix(retry): bound provider retry hints across model paths

Share a 24-hour ceiling for middleware numeric/date hints and Claude's
integer-second hints. Non-finite or oversized values use local backoff;
valid hints above the jitter cap remain unchanged and negatives clamp to zero.

Parameterize unusable hints at both middleware test levels, including nan.
Cover numeric/date boundaries and sync/async recovery through both retry paths.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-08 18:16:21 +08:00
xbzz1018
340a5b63fa
fix(models): validate Claude extended-thinking budgets (#6060)
* fix(models): validate Claude extended thinking budgets

* fix(models): preserve Claude thinking budget semantics

* fix(models): isolate Claude auto thinking budgets

* fix(models): preserve interleaved Claude thinking budgets

Consolidate the additional thinking-budget boundary and type coverage from PR #6482 into #6060. Keep copied automatic budgets and preserve valid manual interleaved budgets with supported models, tools, and the effective beta header. Document the validation scope and cover native payload construction and SDK header precedence.

* docs(models): avoid upstream Claude setup overlap

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-08 10:24:16 +08:00
哈基米
3772f7c7c3
fix(models): keep escaped newlines inside fences in MindIE native stream (#6478)
The no-tool native stream applied _decode_escaped_newlines_outside_fences
to each streamed chunk independently, but a fenced code block almost never
opens and closes within one token-sized chunk, so the per-chunk fence regex
never saw a complete ``` pair and decoded every literal \n inside the fence.
Code like print("a\nb") reached the streamed answer and the persisted
message content with a real newline, while the non-streaming path and the
tool-mode simulated stream preserved it.

Decode the native stream with a fence-stateful decoder instead: it carries
whether the stream is inside a fence across chunks and holds back a
trailing partial ```/`\n` sequence, so the joined stream equals the one-shot
decode of the same reply. A fence left unclosed by a truncated reply keeps
its escapes, matching how an unterminated fence still renders as code.

Document native no-tool streaming in the MindIE decode contract.
2026-10-08 08:56:20 +08:00
cangen185-sudo
4918bea98b
fix(models): preserve escaped newlines in MindIE XML JSON arguments (#6334)
* fix(models): preserve escaped newlines in MindIE XML JSON arguments

* fix(models): restore multiline raw MindIE tool arguments
2026-10-07 14:44:53 +08:00
Guo-Yixin
d95da069a6
fix(models): preserve explicit Codex function tool strictness (#6360)
* fix(models): preserve explicit Codex function tool strictness

* fix(models): omit null Codex strictness and share BaseTool conversion
2026-10-06 16:53:45 +08:00
xbzz1018
d85f5bf6cc
fix(models): parse numeric MindIE XML tool arguments (#6030)
* fix(models): parse numeric MindIE XML tool arguments

Signed-off-by: lovett55 <xbzz101800@163.com>

* chore(models): narrow MindIE numeric parsing change

Signed-off-by: lovett55 <xbzz101800@163.com>

* fix(models): reject unsafe MindIE numeric literals

* fix(models): preserve nested MindIE JSON numbers on conversion errors

Keep the entire XML argument as raw text when JSON numeric conversion fails.
Reserve Python-literal fallback for JSON syntax errors so nested underflow
cannot silently become zero.

Port the malformed-number regression tests from PR #6207 and cover nested
underflow, representable tiny values, and the existing literal fallback.

Co-authored-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>

---------

Signed-off-by: lovett55 <xbzz101800@163.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
2026-10-06 16:47:21 +08:00
Yufeng He
e325c90b24
fix(models): preserve and budget structured MindIE tool results (#6208)
* fix(models): preserve structured MindIE tool results

Signed-off-by: lovett55 <xbzz101800@163.com>

* fix(models): redact and bound structured MindIE tool results

* fix(models): keep structured MindIE ToolMessage content visible

_fix_messages flattened list content keeping only string and type=text
blocks, so a JSON tool result became an empty <tool_response> and the
model could not see the tool output on the next turn. Serialize type=json
blocks onto the same text channel; the existing XML escaping applies to
the serialized text unchanged.

Fixes #6010

Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>

* fix(models): harden the MindIE json channel from review

Review follow-ups on the structured-content fix:

- json.dumps can also raise ValueError (circular reference); catch it
  alongside TypeError so the payload degrades to str() instead of
  failing the whole request normalization.
- A {"type": "json"} block with no json key keeps the old drop
  behavior instead of emitting a literal "null" into the frame.
- PiiRedactionMiddleware now redacts json block payloads leaf-wise
  (nested dict/list strings scanned, keys and non-string leaves kept),
  so the newly serialized channel participates in the configured
  redaction policy for user messages and remote tool results alike.
  Without it, redaction-enabled deployments would ship structured PII
  raw once the provider serializes the block.

Tests: fallback degrade for both exception paths, missing-key drop,
leaf-wise redaction on the model-call and tool boundaries, and a
middleware-to-MindIE regression pinning redaction through provider
serialization. Red/green verified: the 6 new redaction/json tests
fail without the source changes, 155 pass with them.

Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>

* fix(models): close MindIE structured tool redaction gaps

* fix(agents): budget structured json tool output before provider normalization

Willem's P2 on #6208: _message_text() returned None for {"type": "json"}
blocks, so a large structured ToolMessage slipped past both the tool-call
and history budget gates and reached MindIE's serializer at full size,
ignoring the externalize threshold and the fallback cap.

Render those blocks with the same json.dumps(..., ensure_ascii=False)
serialization the MindIE adapter applies (str() fallback included), so
they count toward the budget everywhere _message_text is consulted.
Images and other non-textual blocks still skip enforcement.

Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>

* fix(mindie): render json blocks for tool messages only

InputSanitizationMiddleware neutralizes strings and text blocks, so a
{"type": "json"} block smuggled into a genuine user message reaches
_fix_messages with its payload untouched. Rendering it into the text
channel handed the model an unescaped, unframed injection (e.g. a
literal <system-reminder>), bypassing the guardrail entirely.

Gate the json-block serialization on ToolMessage: structured tool
results keep riding the text channel (and still get html.escape plus
the <tool_response> framing), while user/assistant json blocks go back
to the old drop behavior. Adds unit coverage for the drop paths plus a
chain regression that runs a genuine user message through the
sanitization middleware and then _fix_messages, asserting the smuggled
payload never reaches the model-bound text.

Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>

* fix(models): preserve configured budgets in combined MindIE fix

---------

Signed-off-by: lovett55 <xbzz101800@163.com>
Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
Co-authored-by: lovett55 <xbzz101800@163.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-06 12:16:18 +08:00
Guo-Yixin
52edef4f12
fix(models): exclude thinking blocks from Claude cache breakpoints (#6339)
* fix(models): exclude thinking blocks from Claude cache breakpoints

* fix(models): declare Claude test SDK and clarify caching docs

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-06 09:05:55 +08:00
Guo-Yixin
d8bdd7552c
fix(models): tolerate null Codex response usage (#6335) 2026-10-06 08:11:05 +08:00
Guo-Yixin
9f8a59da80
fix(models): skip undecodable Claude descriptor handoffs (#6323) 2026-10-05 20:21:31 +08:00
Guo-Yixin
57a2d7fa70
fix(models): decode CLI credential files as UTF-8 (#6277)
* fix(models): decode CLI credential files as UTF-8

* fix(doctor): accept BOM CLI credentials consistently
2026-10-05 14:47:14 +08:00
destire-mio
73a305cd87
fix(models): omit Codex tool results with blank call IDs (#6234)
* fix(models): omit Codex tool results with blank call IDs

* fix(models): validate Codex call and result IDs symmetrically

Use one strip-based validity check for assistant calls and tool results while preserving non-blank IDs verbatim. Add paired regressions for ordinary and invalid tool calls, plus non-blank correlation controls.

Validation: 126 focused tests passed; 210 blocking-I/O tests passed; backend lint and format checks passed. Paired regression fails on the previous implementation for whitespace-only IDs.

* fix(models): log dropped Codex tool results

---------

Co-authored-by: destire-mio <248462155+destire-mio@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-04 12:44:05 +08:00
Insecshin
759136e074
fix(config): correct Gemini official endpoint reasoning example (#6161)
* fix(config): correct Gemini official endpoint reasoning example

* docs: clarify official Gemini endpoint heading
2026-10-02 22:32:21 +08:00
Guo-Yixin
d2cfe4a986
fix(models): honor Codex SSE terminal events (#6155)
* fix(models): honor Codex SSE terminal events

* fix(models): preserve malformed Codex error diagnostics

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-01 19:32:49 +08:00
inchang-ing
4ced9dddf5
fix(vllm): fall back to legacy reasoning_content field (#6048)
* fix(vllm): fall back to legacy reasoning_content field

* fix(vllm): document the reasoning_content fallback and pin precedence

The previous change set left the docs describing the provider as
preserving only `reasoning`:

- the module docstring now mentions the legacy `reasoning_content`
  wire field, and the vLLM Provider bullet in models/AGENTS.md is
  extended to match the repo's documentation-update policy;

- _create_chat_result evaluated choice["message"] twice, once per key;
  hoist it into a single local and read both keys from it;

- two regression tests pin that a payload carrying both fields keeps
  `reasoning`, so a future refactor cannot silently invert the
  precedence (the existing tests only covered the absent-`reasoning`
  case).

Reported-by: willem-bd

* refactor(vllm): share reasoning fallback and cover follow-up payloads

---------

Co-authored-by: inchang-ing <197932532+inchang-ing@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-01 08:12:32 +08:00
xbzz1018
f56d276122
fix(models): reject blank Codex CLI tokens (#5959)
* fix(models): reject blank Codex CLI tokens

Signed-off-by: xbzz1018 <xbzz1018@users.noreply.github.com>

* test(models): cover Codex token normalization path

Signed-off-by: xbzz1018 <xbzz1018@users.noreply.github.com>

---------

Signed-off-by: xbzz1018 <xbzz1018@users.noreply.github.com>
Co-authored-by: xbzz1018 <xbzz1018@users.noreply.github.com>
2026-09-28 16:16:15 +08:00
Hyeonsang Cho
931d2eeb81
fix(models): deep-merge when_thinking_enabled on the legacy path (#5894)
* fix(models): deep-merge when_thinking_enabled on the legacy path

The legacy thinking path (profiles without a `reasoning:` block, i.e. every
existing config.yaml) applied `when_thinking_enabled` with a shallow
`dict.update`, so a template such as `extra_body.thinking.type=enabled`
replaced the profile's whole `extra_body` and silently dropped sibling keys
like GLM's `extra_body.tool_stream` — but only while thinking was on. The
disable branch of the same function already deep-merged into `extra_body`,
and the contract path added in #5780 deep-merges through `_merge_settings`
for exactly this reason (its docstring names `tool_stream`). The on/off
asymmetry cannot be intended: the same profile sent `tool_stream` with
thinking off and lost it with thinking on.

Route the enable template through `_merge_settings` as well. Precedence is
unchanged (template values win on conflicts); the only observable
difference is that keys present only in the profile now survive a
thinking-on request. Everything else on the legacy path — the synthesized
`reasoning_effort=minimal`, the disable payloads, the supports_thinking
guard — is untouched.

Tests pin both directions for a base extra_body beside a thinking template,
recursive merging of nested chat_template_kwargs, and template precedence.

* docs(changelog): reference #5894 in the when_thinking_enabled entries

* fix(models): keep the template's vLLM switch spelling authoritative when merging

Review follow-up. Deep-merging the thinking template into a profile keeps
both spellings of vLLM's toggle when they differ — base
`chat_template_kwargs.enable_thinking: false` plus a template using the
legacy `thinking: true` alias — and VllmChatModel's normalizer maps the
alias onto `enable_thinking` only when the latter is absent, so the
profile's `false` won and thinking stayed off despite thinking_enabled=True.
The previous shallow replacement never hit this because it discarded the
base switch. The contract path had the same trap since it already
deep-merged, and both disable branches had the mirror image (a base
`enable_thinking: true` outlived a template `thinking: false`).

Route every thinking on/off merge through _merge_thinking_payload: it drops
the profile's *other* spelling of the switch before the deep merge, so the
payload's spelling decides in both directions on both paths, and unrelated
chat_template_kwargs still survive.

Tests cover both spelling combinations, both directions, and both paths,
asserting the switch as the provider will send it after normalization.

* fix(models): deep-merge the legacy disable template and mirror the vLLM switch instead of pruning

Review of the previous head found two regressions. The legacy
when_thinking_disabled branch was still dict.update-ed, so a profile that
declares both templates (the shape of most extra_body examples) kept
tool_stream while thinking was on and lost it while off. And the merge
pruned the profile's other vLLM switch spelling for every model class,
although only VllmChatModel maps `thinking` onto `enable_thinking`: on a
plain ChatOpenAI profile the prune removed the only key vLLM reads, so
thinking-off could no longer switch reasoning off.

Route when_thinking_disabled through _merge_thinking_payload too, and
replace the prune with mirroring: after the deep merge, the value the
payload declares for its switch spelling is copied onto the profile's
other spelling when the profile carries it. VllmChatModel collapses the
pair, plain OpenAI-compatible classes send both, and either way the
server reads the template's intent. No spelling is invented, a payload
that spells both is left alone, and non-mapping template values are
forwarded unchanged instead of raising. _VLLM_THINKING_SWITCHES is now
the single definition used by the disable, toggle and dialect helpers.

Docs state the merge semantics (keys are never removed; a template can
only add or override) and drop the "byte-for-byte" wording; tests cover
the disable template on/off, nested inheritance, non-mapping values, the
raw payload for a non-normalizing class, and the no-invention rule.

* fix(models): copy thinking templates on merge and document the never-remove rule

Review follow-up. Templates merged into the constructor settings were
stored by reference wherever the profile lacked the key, so the kwargs
handed to the model class aliased the cached ModelConfig; the mirroring
step now writes into that mapping, so make _merge_settings and
_deep_merge_dicts deep-copy every template mapping they store. Pinned at
every depth by a test that also checks the profile is unchanged.

Deep-merging the legacy when_thinking_disabled template means it can no
longer clear a key the profile's base extra_body sets: a base
extra_body.thinking {type: enabled, budget_tokens} reaches the provider
as {type: disabled, budget_tokens}, which Anthropic-style APIs reject.
The synthesized disable payloads and the contract path already behaved
this way. State the rule and the migration (enable-only keys belong in
when_thinking_enabled) in the changelog, models/AGENTS.md and
configuration.mdx, and pin it with a test for both shapes.

Route the legacy synthesized OpenAI disable through
_merge_thinking_payload too (identical output), narrow the docstring and
guidance wording about the native-Anthropic disable, and make the two
vLLM switch suites resolve the real VllmChatModel / ChatOpenAI classes
so a class-gated merge would fail the case it is named after.
2026-09-27 20:52:13 +08:00
Grapette.L
e2feca1e56
fix(models): skip credential files whose access token is not a string (#5864)
* fix(models): skip credential files whose access token is not a string

* fix(models): normalize the file-route access token and guard the refresh token
2026-09-27 15:18:55 +08:00
Totoro
4028b63647
fix(models): keep Claude prompt-cache markers out of thread messages (#5892)
* fix(models): keep Claude prompt-cache markers out of thread messages

ClaudeChatModel._apply_prompt_caching set cache_control on payload blocks in
place. langchain-anthropic forwards Claude-native blocks (image or document
blocks with a source, search results) by reference, so the marker landed in
the thread's own message and was checkpointed with it. Each later request
carried those stale markers on top of its own four, and from the third turn
of a thread with such blocks the API rejected every request (400, "A maximum
of 4 blocks with cache_control may be provided").

Breakpoints are now placed on copies, and markers already in the request are
dropped first, so threads whose checkpoints already carry them recover
without a migration. The OAuth path's _strip_cache_control popped markers
from the same shared objects; it now copies too, and prompt caching reuses it.

* fix(models): drop stored Claude cache markers with caching off too

The strip only ran inside _apply_prompt_caching, so with
enable_prompt_caching: false a thread whose checkpoint already carried five
or more markers kept failing: exactly the switch an operator would flip in
response to these 400s. _get_request_payload now strips on the non-caching
path as well, so recovery no longer depends on the flag.

The stored-checkpoint recovery is now pinned through _get_request_payload
with real messages, for caching on and off. The docstring and the AGENTS.md
note now say which objects are really shared (blocks, list-form system
blocks, a reused binding's tool dicts), that the message-dict and list
copies are defensive, and that _apply_prompt_caching must strip first.

---------

Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
2026-09-27 08:29:56 +08:00
Nan Gao
bcaf6db36a
feat(models): add declarative reasoning capability contract (#5780)
* feat(models): add declarative reasoning capability contract (#5073)

Introduce an optional per-model `reasoning:` block beside the legacy
`supports_thinking` / `supports_reasoning_effort` booleans. It declares
thinking availability (unsupported | optional | required), the accepted
effort values with aliases, default, and serialization path, the payload
dialect, and the reasoning-history requirement. When present the booleans
are derived from it and contradictory profiles fail at config load.

`deerflow.models.reasoning` normalizes legacy and declared profiles into
one immutable contract and resolves every caller's generic request against
it. `create_chat_model` is the single enforcement point: required-thinking
models never receive a synthesized disable payload, unsupported effort
values never reach the provider, and the dialect synthesizes the on/off
payload when no template exists. Legacy profiles keep the historical path
byte-for-byte. The lead agent and subagent descriptor resolve the same
policy first so run metadata reports effective values.

`/api/models` and `DeerFlowClient` project the contract as `reasoning`;
the frontend derives mode clamping and effort choices from it through
`core/models/reasoning.ts`, and user preferences accept provider-specific
effort tokens. The setup wizard's Z.AI GLM-5.3-Flash profile migrates to
the contract, restoring its low/high/max effort control.

* docs(models): move the reasoning contract guidance into models/AGENTS.md

backend/AGENTS.md sits within 46 bytes of the 96 KiB effective-chain hard
limit for the middlewares, runtime, sandbox, and subagents guides, so the
new section pushed scripts/check_agent_guidance.py over it. The models
guide is the nearest file for the factory and the contract module, and
its chain has ample room.

* fix(models): address review on the reasoning contract

- vLLM dialect: the enable path now mirrors whichever chat_template_kwargs
  switch the template declares, like the disable path already did, so a
  template using the older `thinking` key no longer also grows
  `enable_thinking`.
- Validate operator-supplied effort values at `effort.path` inside
  `when_thinking_enabled`, `when_thinking_disabled`, and the `thinking`
  shortcut against the contract, not only the profile-level value; they are
  forwarded when the caller chooses nothing, so the "unknown values never
  reach the provider" guarantee now holds for them too.
- Constrain `effort.path` to dotted identifiers and reject single-segment
  paths that would replace a whole mapping (`extra_body`, `thinking`,
  `model_kwargs`, `default_headers`, `default_query`).
- Z.AI GLM-5.3-Flash profile: `default: high` instead of `max`, because the
  default also governs summarization, title generation, and subagent calls
  that never choose an effort; `max` stays selectable. Documented in the
  config example, the configuration manual, and the design note.

* fix(models): close reasoning contract review gaps

* fix(models): preserve native Ollama reasoning config

* fix(models): keep native reasoning strings and project reasoning into the descriptor

- `ModelConfig.reasoning` also accepts the level string ChatOllama takes
  for gpt-oss style models (`low` / `medium` / `high`); the factory forwards
  both the boolean and the string on the legacy path, so profiles that
  passed `reasoning: high` through before the contract field existed keep
  loading and keep sending it.
- The assembly descriptor no longer excludes `reasoning` from
  `model_parameters`: the native ChatOllama value and a declared contract's
  dialect/history/effort change the request payload, so they now move the
  assembly fingerprint.
- e2e: the sidecar spec pinned the pre-contract body where a model without
  effort control still received `reasoning_effort: minimal`; the preset is
  now dropped for such models (the Gateway discarded it anyway), so the
  assertion expects the key to be absent.
2026-09-24 20:39:30 +08:00
YxinMiracle
b8ab097ca2
fix(models): support official DeepSeek managed profiles (#5718)
Co-authored-by: YxinMiracle <“939157765@qq.com”>
2026-09-22 17:53:21 +08:00
Xuehao Xu
8ef58eaa90
feat(models): manage shared models from Settings (#5596)
* feat(models): add admin UI for shared model management

* docs(gateway): keep model guidance within size budget
2026-09-20 16:13:58 +08:00
哈基米
4e8e2ce691
fix(models): ignore a non-string account_id in the Codex auth file (#5601)
load_codex_cli_credential copied tokens.account_id straight into CodexCliCredential.account_id with no type check. A Codex auth file whose account_id is JSON null therefore propagated None into CodexChatModel._account_id, and model_post_init raised TypeError: 'NoneType' object is not subscriptable on the account prefix in its log line. A numeric or boolean account_id slipped through the same way, reaching the ChatGPT-Account-ID header as a non-string.

Treat any non-string account_id as absent and fall back to the credential's empty-string default, matching how a missing account_id already behaves.
2026-09-20 16:08:27 +08:00
哈基米
5051709343
fix(models): skip Claude credentials sources with a non-numeric expiresAt (#5591)
`_extract_claude_code_credential` copied `expiresAt` straight into
`ClaudeCodeCredential.expires_at`, so a credentials file whose `expiresAt` is a
string, null, list or object reached `is_expired` and raised
`TypeError: '<=' not supported between instances of 'str' and 'int'`. That
aborted the whole lookup instead of skipping the malformed source and moving on
down the documented order, the way the rest of the loader already behaves for a
malformed `claudeAiOauth` container.

Validate the field the way the sibling branches validate their input: log a
debug line and skip the source so the next candidate is tried.
2026-09-20 14:46:12 +08:00
哈基米
2b8c6a970a
fix(models): degrade a non-object Codex auth file to no credential (#5584)
load_codex_cli_credential called .get on the parsed ~/.codex/auth.json
(and $CODEX_AUTH_PATH) without checking that the top level is an object.
_load_json_file returns any valid JSON value, so an array or scalar payload
raised AttributeError out of CodexChatModel.model_post_init instead of the
documented 'Codex CLI credential not found' error. Guard the top level the
same way the sibling Claude loader and its own nested tokens guard do.
2026-09-20 07:43:06 +08:00
0xzkslr-ai
e89b128157
fix(runtime): harden model response recovery at provider boundaries (#5080)
* fix(models): preserve DeepSeek thinking tool history

* fix(runtime): harden model response recovery

* fix(runtime): tighten model response recovery

* fix(runtime): protect run-scoped retry state

* fix(runtime): complete model recovery review fixes

* fix(runtime): preserve empty-response diagnostics

* fix(runtime): strip native tool calls on length caps

* docs(middleware): fit recovery guidance within inherited size limit

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-18 07:29:34 +08:00
buleboy
bec4231886
fix(models): guard Claude credential loader against malformed claudeAiOauth (#5473) (#5494)
Co-authored-by: cyberspace-cs <cyberspace-cs@users.noreply.github.com>
2026-09-17 22:10:52 +08:00
哈基米
51bd002df9
fix(models): pair Codex invalid tool calls with their tool results (#5509)
* fix(models): pair Codex invalid tool calls with their tool results

`_parse_response` parks a function_call whose `arguments` are not valid JSON
on `AIMessage.invalid_tool_calls`, keeping its id and name. `_convert_messages`
serialized only `msg.tool_calls`, so the placeholder ToolMessage that
`DanglingToolCallMiddleware` injects to answer that call was emitted as a
`function_call_output` whose call_id had no matching `function_call` item in
the same request. Responses requires that pairing, turning a recoverable
malformed call into a hard provider error.

Emit `invalid_tool_calls` alongside `tool_calls` as `function_call` items.

* fix(models): drop invalid tool calls that lack a name or call_id

InvalidToolCall fields are nullable, and serializing every invalid call as a
function_call item sends name: null and call_id: null for one that is missing
them, which the Responses schema does not accept. For a caller that reaches
_convert_messages without DanglingToolCallMiddleware, that turned a call the
old serializer dropped into a rejected request.

A call missing either field is now skipped, and its arguments fall back to
"{}" when they are neither an object nor a string. Skipping cannot orphan the
placeholder ToolMessage that this branch pairs the call with: the middleware
mints a synthetic id and a fallback name for exactly these calls before
serialization, so a call still missing them here has no placeholder.
2026-09-17 21:10:18 +08:00
yeejhyang
53dde30d4a
fix(models): preserve FIFO across request-admission handoff (#5459)
* fix(models): make request admission enqueue atomic

* test(models): cover atomic request admission FIFO entry

* docs(models): record atomic admission FIFO invariant

* fix(models): address request-admission review follow-ups
2026-09-16 22:07:42 +08:00
yang rui
3dc895df4d
feat(models): pace shared RPM budgets before dispatch (#5432)
* feat(models): add shared RPM admission queues

* fix(models): address admission pacing review feedback

* docs: simplify request admission quick start guidance

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-15 07:14:35 +08:00