#17289

fix(providers): [3.8.52] Capability gate rejects combos that DO have a working vision member, and hides actionable upstream errors behind generic 400/404

Openmanjughosaopened 2d ago

Summary

Two related problems in the 3.8.52 capability gate:

  1. False negative — a combo is rejected with capability_mismatch even when one of its members demonstrably handles the image request (200 when called directly).
  2. Error surface — the gate's generic messages replace the upstream's actionable ones, and any 404 renders as the literal string Model not found.

Net effect: image requests that used to fail with a clear "this profile cannot handle images" now fail with "No target has confirmed vision support" or "Model not found", which reads like the gateway is broken.

Environment

  • OmniRoute Desktop 3.8.52, Windows 11
  • Upstream is a local OpenAI-compatible bridge network (JetHub pools on 127.0.0.1:64646, plus a separate WorkBuddy bridge on 127.0.0.1:47825)

Repro A — combo rejects a member that works

Combo deepseek v4.1 flash (strategy=priority), members in order:

  1. workbuddy/global:deepseek-v4.1-flash
  2. workbuddy/cn:deepseek-v4.1-flash
  3. codearts/deepseek-v4.1-flash
  4. raccoon/sn-deepseek-v4-1-flash
  5. lobster/deepseek-flash
  6. loomy/DeepSeek-V4.1-Flash
  7. trae_cn/deepseek-v4.1-flash

Direct call to member 1 with a 1×1 PNG (image_url, data:image/png;base64,…):

POST /v1/chat/completions   model=workbuddy/global:deepseek-v4.1-flash
→ 200

Same image, through the combo:

POST /v1/chat/completions   model=deepseek v4.1 flash
→ 400 {"error":{"message":"No target in combo deepseek v4.1 flash has confirmed vision support for this image request","type":"invalid_request_error","code":"capability_mismatch"}}

The member that works is filtered out.

Compare combo glm-5.3 flash, whose first member is workbuddy/cn:glm-5.3-flash — same shape, same image:

POST /v1/chat/completions   model=glm-5.3 flash
→ 200

The only difference between the two working members is the global: vs cn: prefix on the workbuddy model, i.e. one resolves a vision capability and the other does not.

Repro B — the gate hides the actionable upstream message

Direct call to codearts/deepseek-v4.1-flash with an image (bypassing the combo gate) returns the upstream's own, actionable text:

400  unsupported_content
当前 profile 未装附件处理器 @deepseek-ai/dsh-attachment-local,无法处理图片
("this profile has no attachment handler installed; cannot process images")

Through the combo, the operator never sees that — the gate answers first.

Before 3.8.52 the same request surfaced the upstream message directly. From this instance's log, five days before the upgrade:

2026-10-15T08:19:12.805Z  400  loomy/MiniMax-M3  key=loomy
当前 profile 未装附件处理器 @deepseek-ai/dsh-attachment-local,无法处理图片

The underlying upstream gap is old. What changed is that the operator can no longer see it.

Repro C — 404 renders as a fixed string

The status→message table is literal:

r = { 400:"Bad request", 401:"Invalid API key provided", 402:"Payment required",
      403:"Permission denied", 404:"Model not found", 406:"Model not supported",
      410:"Model has been shut down", 429:"Rate limit exceeded",
      499:"Client disconnected", 500:"Internal server error",
      502:"Bad gateway - upstream provider error", 503:"Service temporarily unavailable", … }

with

404: { type: "invalid_request_error", code: "model_not_found" }

So any 404 from any cause — upstream 404, catalog miss, retired model, routing miss — surfaces as:

404: {"message":"Model not found","type":"invalid_request_error","code":"model_not_found"}

which points the operator at the wrong thing entirely.

Requests

  1. Do not filter out a target that can actually serve the request. When vision cannot be confirmed from metadata but the target would accept the image, prefer attempting it (or keep it as a last-resort candidate) over rejecting the whole combo. A metadata gap should degrade to "try it", not "refuse it".
  2. Let the upstream error through when the gate rejects for capability. If the request is going to fail anyway, the upstream's own message (unsupported_content, naming the missing handler) is strictly more useful than capability_mismatch.
  3. Make 404 say what was actually missing — the model id, the combo, and which lookup failed — instead of the constant "Model not found".
  4. Register vision for workbuddy/global:* the way workbuddy/cn:* is registered. 3.8.52 already did this for DeepSeek API Flash models (#16865) and Command Code; on this instance workbuddy/global:deepseek-v4.1-flash handles images fine but is not advertised as vision-capable.

Why A matters most

A turns a per-member limitation into a whole-combo outage. 5 of the 7 members genuinely lack image support upstream (pre-existing — their profile has no @deepseek-ai/dsh-attachment-local), but the 2 members that do work are excluded as well, so the operator gets a hard failure where a working target existed all along.

Activity

1
diegosouzapw2d ago

Thanks for the very detailed report. Findings on release/v3.8.53. A: reproduced. workbuddy/global:deepseek-v4.1-flash and workbuddy/cn:deepseek-v4.1-flash both resolve vision=null, and the combo gate treats null as unsupported, so the whole combo is rejected. The global:/cn: prefix is not the difference. The glm-5.3 combo passes because the generic vision heuristic recognises the glm-5.3 id and has no entry for deepseek-v4.1-flash. This is not covered by PR #17243, which only aliases the official DeepSeek provider's model. We will fix it so that an unconfirmed (unknown) vision target is tried as a last resort instead of failing the whole combo, and so that the registered capability for WorkBuddy-served deepseek-v4.1-flash is explicit (not a global id fragment, since codearts/loomy serve the same id without image support). B: this follows from A, because the gate runs before the upstream is called; the plan is for the gate not to reject a pool that still has unknown-capability targets, so the upstream's own unsupported_content message reaches you. C: the literal "Model not found" is only the fallback when the upstream 404 has an empty message. If you can share the raw log line of a 404 you saw (provider, model, whether the body was empty), we will make it name the model id and lookup. Workaround meanwhile: mark the model as "Vision capable" in the custom model settings.