diff --git a/docs-site/src/content/docs/fr/guides/providers.md b/docs-site/src/content/docs/fr/guides/providers.md index 798862c525..378dc215d2 100644 --- a/docs-site/src/content/docs/fr/guides/providers.md +++ b/docs-site/src/content/docs/fr/guides/providers.md @@ -363,6 +363,11 @@ Le préréglage DeepSeek intégré route également `deepseek-v4-flash` par son et conserve le streaming SSE en amont. Si ce modèle termine tous les éléments de sortie mais omet l'événement Responses final, opencodex applique une réparation après un délai de grâce de cinq secondes, limitée à ce modèle ; les flux mal formés ou partiels sont fermés comme incomplets, et non déclarés réussis. +Le modèle DeepSeek de première partie `deepseek-flash` déclare nativement les entrées `text` et `image` ; +les requêtes contenant une image sont donc envoyées directement à DeepSeek par défaut sans passer par le +sidecar de vision. Les déclarations explicites `noVisionModels` ou texte seul restent prioritaires. Les modèles +de première partie `deepseek-chat`, `deepseek-reasoner` et `deepseek-v4-flash` restent desservis par le sidecar +par défaut ; les routes Zen sont inchangées et n'ont pas été sondées dans cette mise à jour. > **Trois routes de facturation Volcengine :** `volcengine` correspond à l'API Ark facturée à l'usage, > `volcengine-coding-plan` consomme le quota Coding Plan et `volcengine-agent-plan` le quota Agent Plan. diff --git a/docs-site/src/content/docs/guides/codex-app-models.md b/docs-site/src/content/docs/guides/codex-app-models.md index 8219dd2281..209469f41d 100644 --- a/docs-site/src/content/docs/guides/codex-app-models.md +++ b/docs-site/src/content/docs/guides/codex-app-models.md @@ -73,13 +73,15 @@ the resulting list; otherwise the native default is used when present, then the choice. Stored custom configuration is unchanged, and repeated syncs do not add `max` back to a narrow custom list. -This requires the exact provider, destination, and capability-backed model identity. An arbitrary -gateway such as `YYLJ/gpt-6-astra` does not inherit native capabilities from its name. Its explicit -custom ladder continues to override discovered provider metadata under the normal routed rules. +The same catalog bound applies when the custom model id has pinned native capability metadata, +including an arbitrary gateway such as `YYLJ/gpt-6-astra`. Desktop validates the model id, so +`none` and `minimal` are stripped from that catalog row. Full native identity still requires the +exact provider, destination, and capability-backed model identity; a gateway does not inherit +Responses Lite, multi-agent, or native windows from its name. Codex's native Astra `ultra` choice is retained: it is a client delegation mode converted to a supported wire effort, distinct from the [API model's effort list](https://developers.openai.com/api/docs/models/gpt-6-astra). -Catalog normalization does not rewrite existing thread settings or establish support for a -particular installed Desktop version. +Catalog normalization does not rewrite existing thread settings. Request-time native effort +clamps remain canonical-forward only. When the `codexAccountNamespaces` map is empty, account-qualified picker rows are off. If `codexAccountPickerEnabled` is omitted with a non-empty map, they are treated as enabled for diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index 7deb669c24..f45f653c39 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -524,6 +524,11 @@ The built-in DeepSeek preset also routes `deepseek-v4-flash` over its native Res keeps upstream SSE streaming enabled. If that model finishes every output item but omits the final Responses event, opencodex applies a five-second model-scoped grace repair; malformed or partial streams close as incomplete rather than being reported as successful. +The first-party `deepseek-flash` model advertises native `text` and `image` input, so image requests +are sent directly to DeepSeek by default instead of through the vision sidecar. Explicit +`noVisionModels` or text-only declarations remain authoritative. First-party `deepseek-chat`, +`deepseek-reasoner`, and `deepseek-v4-flash` remain sidecar-backed by default. Zen routes are +unchanged and were not probed in this update. > **Three Volcengine billing routes:** `volcengine` is the pay-as-you-go Ark API, > `volcengine-coding-plan` consumes Coding Plan quota, and `volcengine-agent-plan` consumes Agent diff --git a/docs-site/src/content/docs/guides/sidecars.md b/docs-site/src/content/docs/guides/sidecars.md index cee0da3f07..d87c5b9513 100644 --- a/docs-site/src/content/docs/guides/sidecars.md +++ b/docs-site/src/content/docs/guides/sidecars.md @@ -135,6 +135,10 @@ allow attachments instead of blocking them before the sidecar runs. When use the `gpt-5.6-luna` fallback. Startup still migrates an explicitly persisted legacy `gpt-5.4-mini` value to `gpt-5.6-luna`; that migration applies to a stored value, not to an absent model field. +The first-party DeepSeek `deepseek-flash` model is native multimodal (`text` and `image`) and does +not use this sidecar by default. Explicit `noVisionModels` or text-only declarations remain +authoritative. First-party `deepseek-chat`, `deepseek-reasoner`, and `deepseek-v4-flash` remain +sidecar-backed by default; Zen routes are unchanged and were not probed in this update. - Images can come from user, developer, and tool-result messages, including Codex's `view_image`. - On the OpenAI path (ChatGPT-login passthrough), each image is sent to the configured vision model diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index a80eab897f..752c13b395 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -231,11 +231,12 @@ provider request is sent. Changing away and back also ends that continuation. St use the new selection. Selection changes before the first provider send retain normal reselection. Custom-model `reasoningEfforts` normally override discovered provider metadata. The bounded -exception is an explicit Astra or Daybreak custom row on the canonical `openai` Codex-forward -destination: its advertised list is intersected with that model's pinned native capabilities. -An explicit empty list remains empty with no default; a nonempty incompatible list falls back -to the native default as a single choice. Defaults must belong to the final list. This changes -the catalog projection, not stored configuration or arbitrary gateway models sharing a GPT name. +exception is an explicit custom row whose model id has pinned native Codex capabilities, +including Astra or Daybreak on an arbitrary gateway: its advertised list is intersected with +that model's pinned native capabilities. Full native identity still requires the canonical +`openai` Codex-forward destination. An explicit empty list remains empty with no default; a +nonempty incompatible list falls back to the native default as a single choice. Defaults must +belong to the final list. This changes the catalog projection, not stored configuration. See [custom native catalog examples](/guides/codex-app-models/). ### Operator-pinned reasoning effort diff --git a/docs-site/src/content/docs/tr/guides/providers.md b/docs-site/src/content/docs/tr/guides/providers.md index b6b35b96a5..b7914c4f42 100644 --- a/docs-site/src/content/docs/tr/guides/providers.md +++ b/docs-site/src/content/docs/tr/guides/providers.md @@ -409,6 +409,11 @@ yönlendirir ve yukarı akış SSE akışını etkin tutar. Bu model tüm çıkt bitirir ancak son Responses olayını atlarsa opencodex beş saniyelik model kapsamlı bir yetkisiz kullanım onarımı uygular; hatalı biçimlendirilmiş veya kısmi akışlar başarılı olarak bildirilmek yerine tamamlanmamış olarak kapanır. +Birinci taraf `deepseek-flash` modeli yerel olarak `text` ve `image` girdilerini bildirir; bu nedenle +görüntü içeren istekler varsayılan olarak vision sidecar üzerinden geçmeden doğrudan DeepSeek'e gönderilir. +Açık `noVisionModels` veya yalnızca metin bildirimleri önceliğini korur. Birinci taraf `deepseek-chat`, +`deepseek-reasoner` ve `deepseek-v4-flash` varsayılan olarak sidecar üzerinden çalışmaya devam eder; Zen +rotaları değişmedi ve bu güncellemede yoklanmadı. > **Üç Volcengine faturalandırma rotası:** `volcengine` kullandıkça öde Ark API'sidir, `volcengine-coding-plan` Coding Plan kotasını tüketir ve `volcengine-agent-plan` Agent Plan kotasını tüketir. Aynı ürün için verilen anahtarı ve uç noktayı kullanın; sıradan `/api/v3` uç noktası bir Plan aboneliği mevcut olduğunda bile kullandıkça öde ücretlerine neden olabilir. Önayarlar özenle seçilmiş statik model katalogları kullanır çünkü Ark'ın `/models` yanıtı yerleştirme, görsel, video ve 3D kaynaklarını da içerir, Coding ağ geçidi aynı geniş kataloğu döndürür ve Agent Plan ağ geçidinin `/models` kaynağı yoktur. Kullandıkça öde varsayılan olarak `doubao-seed-2-1-pro-260628`'dir; seçilmiş kataloğu güncel DeepSeek ve GLM metin modellerini de içerir. Coding Plan varsayılan olarak `ark-code-latest`, Agent Plan ise varsayılan olarak `deepseek-v4-flash`'dur. diff --git a/docs-site/src/content/docs/zh-cn/guides/providers.md b/docs-site/src/content/docs/zh-cn/guides/providers.md index 07e693ce73..ade0047960 100644 --- a/docs-site/src/content/docs/zh-cn/guides/providers.md +++ b/docs-site/src/content/docs/zh-cn/guides/providers.md @@ -235,6 +235,10 @@ Cline IDE/CLI 中提供,不能通过 API 使用;`minimax/minimax-m2.5` 是 内置 DeepSeek preset 同样会让 `deepseek-v4-flash` 使用原生 Responses 端点,并保留上游 SSE 流式输出。如果该模型已经完成全部输出项却缺少最终 Responses 事件,opencodex 会应用模型级 5 秒宽限修复;不完整或格式异常的流会以 incomplete 结束,不会被误报为成功。 +第一方 `deepseek-flash` 模型原生声明支持 `text` 和 `image` 输入,因此图像请求默认会直接发送给 +DeepSeek,不经过 vision sidecar。显式的 `noVisionModels` 或纯文本声明仍然优先。第一方 +`deepseek-chat`、`deepseek-reasoner` 和 `deepseek-v4-flash` 默认仍使用 sidecar;Zen 路由保持不变, +本次更新未进行探测。 > **三条火山方舟计费线路:**`volcengine` 是按量付费方舟 API,`volcengine-coding-plan` > 消耗 Coding Plan 额度,`volcengine-agent-plan` 消耗 Agent Plan 额度。密钥与端点需要属于 diff --git a/docs-site/src/content/docs/zh-tw/guides/providers.md b/docs-site/src/content/docs/zh-tw/guides/providers.md index 8a38d30988..e1db617b1f 100644 --- a/docs-site/src/content/docs/zh-tw/guides/providers.md +++ b/docs-site/src/content/docs/zh-tw/guides/providers.md @@ -315,6 +315,10 @@ provider,例如 **Xiaomi MiMo**,使用 `anthropic` adapter(`x-api-key`) 原生 Responses endpoint,並保持上游 SSE streaming。若該模型完成所有 output item 卻省略最後的 Responses event,opencodex 會套用 5 秒、model-scoped 的 grace repair;malformed 或 partial stream 會以 incomplete 關閉,不會被誤報為成功。 +第一方 `deepseek-flash` 模型原生宣告支援 `text` 與 `image` 輸入,因此圖片請求預設會直接送往 +DeepSeek,不經過 vision sidecar。明確的 `noVisionModels` 或純文字宣告仍然優先。第一方 +`deepseek-chat`、`deepseek-reasoner` 與 `deepseek-v4-flash` 預設仍使用 sidecar;Zen 路由維持不變, +本次更新未進行探測。 > **三條 Volcengine 計費路徑:** `volcengine` 是 pay-as-you-go Ark API, > `volcengine-coding-plan` 消耗 Coding Plan quota,`volcengine-agent-plan` 消耗 Agent Plan quota。請使用 diff --git a/src/codex/catalog/provider-fetch.ts b/src/codex/catalog/provider-fetch.ts index f4560a03b8..f76e0b98fb 100644 --- a/src/codex/catalog/provider-fetch.ts +++ b/src/codex/catalog/provider-fetch.ts @@ -2321,7 +2321,7 @@ async function gatherRoutedModelsWithAuth( return models; } -/** Bound a proven Codex-forward custom row without changing its stored configuration. */ +/** Bound a custom row whose model id has pinned native Codex metadata, without changing stored configuration. */ function boundCustomNativeReasoning( model: CatalogModel, allowed: readonly string[], @@ -2625,8 +2625,8 @@ async function gatherRoutedModelsUncached( : {}), // Explicit custom-row ladder wins over the inherited provider row below: the merge only // gap-fills, so a stored `[]` (explicit "no reasoning") or a declared ladder is kept - // instead of being replaced by that row's metadata. Only proven native aliases are - // bounded against their own capability source after the merge. + // instead of being replaced by that row's metadata. Capability-backed native model ids + // are bounded against their own pinned ladder after the merge, including gateways. ...(Array.isArray(cm.reasoningEfforts) ? { reasoningEfforts: [...cm.reasoningEfforts] } : {}), ...(cm.defaultReasoningEffort ? { defaultReasoningEffort: cm.defaultReasoningEffort } : {}), ...(typeof supportsServiceTier === "boolean" ? { supportsServiceTier } : {}), @@ -2679,8 +2679,16 @@ async function gatherRoutedModelsUncached( ...(base.codexToolMode === undefined && replaced.codexToolMode !== undefined ? { codexToolMode: replaced.codexToolMode } : {}), ...(base.capabilities === undefined && replaced.capabilities !== undefined ? { capabilities: replaced.capabilities } : {}), } : base; - const reasoningBounded = codexForwardNativeCapabilityAlias - ? boundCustomNativeReasoning(merged, nativeReasoningEfforts(cm.modelId), nativeAliasDefaultEffort) + // Catalog-advertised efforts are bounded whenever the model id is a pinned native + // slug. Desktop validates that id, so a gateway such as YYLJ/gpt-6-astra still cannot + // advertise none/minimal. Full native identity stays behind the alias predicate. + const nativeEffortSource = hasNativeOpenAiCapabilityMetadata(cm.modelId); + const reasoningBounded = nativeEffortSource + ? boundCustomNativeReasoning( + merged, + nativeReasoningEfforts(cm.modelId), + nativeAliasDefaultEffort ?? nativeDefaultReasoningEffort(cm.modelId), + ) : merged; // Vision-sidecar coverage only: when the enriched provider's shared predicate matches // noVisionModels or text-without-image modelInputModalities, advertise image input so the diff --git a/src/codex/catalog/sync.ts b/src/codex/catalog/sync.ts index edf41e74c9..d3380020b0 100644 --- a/src/codex/catalog/sync.ts +++ b/src/codex/catalog/sync.ts @@ -49,7 +49,7 @@ import { codexAccountLogLabel, fallbackCodexAccountLogLabel } from "../account-l import { CODEX_CUSTOM_MODEL_CATALOG_KIND, CODEX_PROVIDER_MODEL_CATALOG_KIND, activeCodexModelsCachePath, applyCatalogMetadata, applyMultiAgentMode, applyNativeOpenAiContextOverride, applyRoutedCodexToolMode, catalogBackupPathFor, catalogHasRoutedEntries, catalogModelSlug, ensureStrictCatalogFields, findNativeTemplate, findSupportedNativeTemplate, isDefaultCatalogPath, isRoutedModelCompatibilityExcluded, legacyCatalogBackupPath, normalizeRoutedCatalogEntry, normalizeServiceTiers, readCatalog, readCatalogBackup, readCodexCatalogPath, readCodexCatalogPathForHome, readConfiguredAutoReviewModel, readNativeBaseline } from "./parsing"; import type { CatalogModel, MultiAgentMode, RawCatalog, RawEntry } from "./parsing"; -import { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, applyNativeVisibility, CODEX_NATIVE_ALIAS_CATALOG_KIND, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, isNativeAliasCatalogEntry, isUnsupportedOpenAiNativeSlug, NATIVE_OPENAI_MODELS, RETIRED_NATIVE_OPENAI_MODELS, nativeContextLimits, observedAccountBoundNativeEntries, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, shouldUpgradeToUpstreamEntry, SUPPORTED_NATIVE_OPENAI_SLUGS, upstreamNativeEntry, type NativeContextLimitsInput } from "./metadata"; +import { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, applyNativeVisibility, CODEX_NATIVE_ALIAS_CATALOG_KIND, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, hasNativeOpenAiCapabilityMetadata, isNativeAliasCatalogEntry, isUnsupportedOpenAiNativeSlug, NATIVE_OPENAI_MODELS, RETIRED_NATIVE_OPENAI_MODELS, nativeContextLimits, observedAccountBoundNativeEntries, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, shouldUpgradeToUpstreamEntry, SUPPORTED_NATIVE_OPENAI_SLUGS, upstreamNativeEntry, type NativeContextLimitsInput } from "./metadata"; import { bundledCatalogCacheState, loadBundledCodexCatalog, @@ -316,6 +316,13 @@ function routedDisplayName(slug: string, model?: CatalogModel, config?: Pick [id, deepseekReasoningMapFor(id)])), modelSupportsReasoningSummaries: Object.fromEntries(DEEPSEEK_NATIVE_THINKING_MODELS.map(id => [id, true])), preserveReasoningContentModels: DEEPSEEK_NATIVE_THINKING_MODELS, - // Issue #88: every DeepSeek API model is text-only input (no image support upstream) — the - // vision sidecar describes attached images for them, and the catalog advertises image input - // on their behalf (same treatment as opencode-go's DeepSeek V4 entries above). - noVisionModels: ["deepseek-chat", "deepseek-reasoner", ...DEEPSEEK_NATIVE_THINKING_MODELS], + // #4436: first-party deepseek-flash accepts native images on Chat and Responses. + // Keep unprobed compatibility aliases on the #88 sidecar path. This must be fixed + // here: router enrichment unions this list with saved config, so config cannot remove it. + noVisionModels: ["deepseek-chat", "deepseek-reasoner", "deepseek-v4-flash"], }, // llama-3.3-70b was deprecated by Cerebras on 2026-02-16. Evidence: devlog/_plan/260710_provider_hardening/003_research_aggregators.md. { id: "cerebras", label: "Cerebras", baseUrl: "https://api.cerebras.ai/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://cloud.cerebras.ai/platform/apikeys", defaultModel: "gpt-oss-120b" }, diff --git a/structure/catalog.md b/structure/catalog.md index 153b52f111..8cce6268b0 100644 --- a/structure/catalog.md +++ b/structure/catalog.md @@ -45,13 +45,16 @@ custom catalog remains the native metadata/template authority even when a bundle warm. Both paths may use an admitted matching bundled memo only as installed-runtime capability evidence to remove unsupported reasoning efforts; convergence never probes Codex itself. -Custom Astra and Daybreak rows acquire native reasoning capability only through the existing -canonical `openai` forward destination and explicit capability-source predicate. The shared -custom-row producer bounds their merged effort lists against pinned per-model Codex metadata, -preserves an explicit empty list without a default, and recovers an incompatible nonempty list -to the native default singleton. A default must belong to the projected list. Other custom rows -keep their declaration precedence; a GPT model name, display alias, or arbitrary gateway is not -native provenance. Stored configuration and native capability maps are unchanged. +Custom Astra and Daybreak rows acquire native identity -- Responses Lite, multi-agent, context +windows, display names -- only through the canonical `openai` forward destination and explicit +capability-source predicate. Catalog-advertised reasoning lists are a narrower bound: when a +custom row's model id has pinned native capability metadata, the shared producer intersects an +explicit declared ladder with that pinned list even on an arbitrary gateway such as +`YYLJ/gpt-6-astra`. Desktop validates the model id, so `none` and `minimal` must not survive on +those catalog rows. An explicit empty list remains empty; a nonempty incompatible list falls back +to the native default singleton. A default must belong to the projected list. Full native identity +is still not inferred from a GPT name. Stored configuration and native capability maps are +unchanged. Request-time native effort clamps remain canonical-forward only. The observed-state merge tracks the current invocation's freshly generated custom row objects after detaching its inputs. Those rows already own their complete reasoning projection, so the @@ -60,9 +63,7 @@ ordinary retained provider rows still receive the existing mock-tier policy. A p marker alone never grants this exemption. Both gather entry points, retained sync, management convergence and direct Codex model discovery use the same producer. The legacy runtime effort union clamp remains separate; it is not a per-model or per-client-version grammar oracle. -Existing thread settings and the reported Desktop 0.153.4 gateway rejection require separate -runtime evidence. Codex's native `ultra` mode is preserved and is not a literal API wire promise. - +Codex's native `ultra` mode is preserved and is not a literal API wire promise. When account selectors are enabled, the sync path may also observe exact, visible, API-supported OpenAI-family ids from Codex's user-owned catalog/cache. Only rows with native catalog provenance are trusted; unknown ids are carried through startup cache invalidation as hidden observations and diff --git a/structure/providers/xai-grok.md b/structure/providers/xai-grok.md index 64de0e514c..6213ce1449 100644 --- a/structure/providers/xai-grok.md +++ b/structure/providers/xai-grok.md @@ -96,6 +96,10 @@ Devin CLI credential path composition in `src/oauth/devin/cli-import.ts` follows Provider-scoped catalog hints remain isolated by provider in `src/providers/registry.ts`. The OpenCode Go `deepseek-v4.1-flash` 1,048,576-token context hint does not change xAI model metadata or transport behavior. +The first-party DeepSeek `deepseek-flash` native `text`/`image` declaration is likewise scoped to +the DeepSeek provider and does not alter xAI metadata or transport behavior; explicit capability +overrides remain authoritative. First-party `deepseek-chat`, `deepseek-reasoner`, and +`deepseek-v4-flash` remain sidecar-backed by default. Zen routes are unchanged and unprobed here. Native Chat applies qualifying effort ceilings independently of model pins; pin selection precedes the cap and only pins or cap rewrites enter wire mapping. The [catalog effort contract](../catalog.md#ultra-reasoning-level) records the V1/compaction exemptions and caller-preservation boundary. diff --git a/structure/runtime.md b/structure/runtime.md index de83f3734c..4aff69172a 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -201,6 +201,10 @@ Provider-scoped capability hints remain authoritative when discovery returns an capabilities. In particular, `src/providers/registry.ts` assigns OpenCode Go's live `deepseek-v4.1-flash` route the official 1,048,576-token window instead of the conservative 128k routed-model fallback. +The same registry declares the first-party `deepseek-flash` model with `text` and `image` input, +so it bypasses the vision sidecar by default; explicit `noVisionModels` or text-only declarations +remain authoritative. First-party `deepseek-chat`, `deepseek-reasoner`, and `deepseek-v4-flash` +remain sidecar-backed by default. Zen routes are unchanged and unprobed in this update. The BigModel Coding Plan Responses preset uses the separately documented `https://open.bigmodel.cn/api/v1` transport and a static catalog. Its provider row diff --git a/structure/subagents.md b/structure/subagents.md index 98c827ffa7..ffa3bdd9ef 100644 --- a/structure/subagents.md +++ b/structure/subagents.md @@ -131,6 +131,10 @@ featured or picker rank. Canonical `opencode-go` rows retain their configured re and provider-scoped context metadata both when generated and when merged from retained catalog state; `deepseek-v4.1-flash` therefore keeps its 1,048,576-token window, while synthetic max/ultra choices are not added to that provider's declared ladder. +The first-party DeepSeek `deepseek-flash` row declares native `text` and `image` input and therefore +does not require the vision sidecar by default; explicit `noVisionModels` or text-only declarations +remain authoritative. First-party `deepseek-chat`, `deepseek-reasoner`, and `deepseek-v4-flash` +remain sidecar-backed by default. Zen routes are unchanged and unprobed in this update. Full derivation with per-line citations: `devlog/_plan/260816_codexrs_multiagent_v2_and_history_perf/013_five_cap_v1_vs_v2.md`. diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index e344f73126..10b5be6a6b 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -33,6 +33,11 @@ surface is listed here so a maintainer can find the owner without grepping: | Alibaba regions | `src/providers/alibaba-region-backup.ts`, `src/providers/alibaba-region-migration.ts`, `src/providers/alibaba-region-startup.ts` | Region migration backs up before rewriting and is idempotent across restarts. | | Discovery and quota | `src/providers/model-discovery.ts`, `src/providers/quota.ts`, `src/providers/registry.ts` | Discovery rejects a response over 4 MiB or past 2,000 raw rows before caching it. Provider-scoped hints fill capabilities omitted by live rosters; OpenCode Go's `deepseek-v4.1-flash` keeps its 1,048,576-token context window. Codex quota DTOs suppress retired Spark evidence under the [OpenAI scope contract](../providers/openai-tiers.md#public-provider-contract), retaining ordinary custom windows. | +The registry's first-party `deepseek-flash` row declares native `text` and `image` input, so image +requests bypass the vision sidecar by default; explicit `noVisionModels` or text-only declarations +remain authoritative. First-party `deepseek-chat`, `deepseek-reasoner`, and `deepseek-v4-flash` +remain sidecar-backed by default. Zen routes are unchanged and unprobed in this update. + > Decision record: [ADR-0072](../decisions/ADR-0072-transport-inventory.md) Cursor external-model continuations attach data-URL screenshots from the contiguous active diff --git a/tests/claude-integration/claude-models-discovery.test.ts b/tests/claude-integration/claude-models-discovery.test.ts index 5c29fd7ef0..e0cdc22d0e 100644 --- a/tests/claude-integration/claude-models-discovery.test.ts +++ b/tests/claude-integration/claude-models-discovery.test.ts @@ -141,7 +141,7 @@ test("per-surface id style: ?ids= wins, claude-code UA gets readable, unknown UA } }); -test("Codex discovery bounds proven custom Astra before any disk sync and preserves a gateway namesake", async () => { +test("Codex discovery bounds proven custom Astra before any disk sync including a gateway namesake", async () => { const config = configWithStaticModels(); config.providers.openai = { adapter: "openai-responses", @@ -164,8 +164,8 @@ test("Codex discovery bounds proven custom Astra before any disk sync and preser expect(canonical?.supported_reasoning_levels.map(level => level.effort)).toEqual(["low"]); expect(canonical?.default_reasoning_level).toBe("low"); const gateway = catalog.models.find(row => row.slug === "YYLJ/gpt-6-astra"); - expect(gateway?.supported_reasoning_levels.map(level => level.effort)).toEqual(["none", "minimal", "low", "max", "ultra"]); - expect(gateway?.default_reasoning_level).toBe("minimal"); + expect(gateway?.supported_reasoning_levels.map(level => level.effort)).toEqual(["low"]); + expect(gateway?.default_reasoning_level).toBe("low"); } finally { await server.stop(true); } diff --git a/tests/codex-integration/codex-catalog.test.ts b/tests/codex-integration/codex-catalog.test.ts index cae7a2d00f..fe46b224b2 100644 --- a/tests/codex-integration/codex-catalog.test.ts +++ b/tests/codex-integration/codex-catalog.test.ts @@ -4062,14 +4062,14 @@ describe("Codex catalog routed normalization", () => { }); test.each([ - { name: "YYLJ", adapter: "openai-responses", baseUrl: "https://gateway.example.test/v1", authMode: "key", modelId: "gpt-6-astra" }, - { name: "openai", adapter: "openai-responses", baseUrl: "https://gateway.example.test/v1", authMode: "forward", modelId: "gpt-6-astra" }, - { name: "openai", adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api/codex", authMode: "key", modelId: "gpt-6-astra" }, - { name: "openai", adapter: "openai-chat", baseUrl: "https://chatgpt.com/backend-api/codex", authMode: "key", modelId: "gpt-6-astra" }, - { name: "openai-apikey", adapter: "openai-responses", baseUrl: "https://api.openai.com/v1", authMode: "key", modelId: "gpt-6-astra" }, - { name: "openai", adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api/codex", authMode: "forward", modelId: "gpt-unproven" }, - ] satisfies Array<{ name: string; adapter: OcxProviderConfig["adapter"]; baseUrl: string; authMode: OcxProviderConfig["authMode"]; modelId: string }>)( - "custom $name/$modelId does not infer native effort capability from $baseUrl / $authMode / $adapter", + { name: "YYLJ", adapter: "openai-responses", baseUrl: "https://gateway.example.test/v1", authMode: "key", modelId: "gpt-6-astra", efforts: ["low"], defaultEffort: "low", catalogEfforts: ["low"] }, + { name: "openai", adapter: "openai-responses", baseUrl: "https://gateway.example.test/v1", authMode: "forward", modelId: "gpt-6-astra", efforts: ["low"], defaultEffort: "low", catalogEfforts: ["low"] }, + { name: "openai", adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api/codex", authMode: "key", modelId: "gpt-6-astra", efforts: ["low"], defaultEffort: "low", catalogEfforts: ["low"] }, + { name: "openai", adapter: "openai-chat", baseUrl: "https://chatgpt.com/backend-api/codex", authMode: "key", modelId: "gpt-6-astra", efforts: ["low"], defaultEffort: "low", catalogEfforts: ["low"] }, + { name: "openai-apikey", adapter: "openai-responses", baseUrl: "https://api.openai.com/v1", authMode: "key", modelId: "gpt-6-astra", efforts: ["low"], defaultEffort: "low", catalogEfforts: ["low"] }, + { name: "openai", adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api/codex", authMode: "forward", modelId: "gpt-unproven", efforts: ["none", "minimal", "low"], defaultEffort: "minimal", catalogEfforts: ["none", "minimal", "low", "max", "ultra"] }, + ] satisfies Array<{ name: string; adapter: OcxProviderConfig["adapter"]; baseUrl: string; authMode: OcxProviderConfig["authMode"]; modelId: string; efforts: string[]; defaultEffort: string; catalogEfforts: string[] }>)( + "custom $name/$modelId does not inherit native identity from $baseUrl / $authMode / $adapter", async fixture => { const models = await gatherRoutedModels({ port: 10100, @@ -4079,15 +4079,42 @@ describe("Codex catalog routed normalization", () => { }); const custom = models.find(row => row.provider === fixture.name && row.id === fixture.modelId); expect(custom?.codexForwardNativeCapabilityAlias).toBeUndefined(); - expect(custom?.reasoningEfforts).toEqual(["none", "minimal", "low"]); - expect(custom?.defaultReasoningEffort).toBe("minimal"); + expect(custom?.reasoningEfforts).toEqual(fixture.efforts); + expect(custom?.defaultReasoningEffort).toBe(fixture.defaultEffort); const entries = buildCatalogEntries(nativeTemplate(), [], models); const row = entries.find(entry => entry.slug === `${fixture.name}/${fixture.modelId}`); - expect(row ? catalogEntryEfforts(row) : undefined) - .toEqual(["none", "minimal", "low", "max", "ultra"]); + expect(row ? catalogEntryEfforts(row) : undefined).toEqual(fixture.catalogEfforts); + expect(row?.use_responses_lite).toBeUndefined(); + expect(row?.multi_agent_version).toBeUndefined(); }, ); + test("gateway custom Astra bounds catalog efforts without native identity (#3775)", async () => { + const config = { + port: 10100, + defaultProvider: "YYLJ", + providers: { YYLJ: { adapter: "openai-responses" as const, baseUrl: "https://gateway.example.test/v1", authMode: "key" as const, liveModels: false, models: ["gpt-6-astra"] } }, + customModels: [{ + id: "yylj-astra", + provider: "YYLJ", + modelId: "gpt-6-astra", + reasoningEfforts: ["none", "minimal", "low", "medium", "high", "xhigh", "max"], + defaultReasoningEffort: "minimal", + }], + }; + const beforeConfig = JSON.stringify(config); + const models = await gatherRoutedModels(config); + const custom = models.find(row => row.provider === "YYLJ" && row.id === "gpt-6-astra"); + expect(custom?.codexForwardNativeCapabilityAlias).toBeUndefined(); + expect(custom?.reasoningEfforts).toEqual(["low", "medium", "high", "xhigh", "max"]); + expect(custom?.defaultReasoningEffort).toBe("low"); + const row = buildCatalogEntries(nativeTemplate(), [], models).find(entry => entry.slug === "YYLJ/gpt-6-astra"); + expect(row ? catalogEntryEfforts(row) : undefined).toEqual(["low", "medium", "high", "xhigh", "max"]); + expect(row?.default_reasoning_level).toBe("low"); + expect(row?.use_responses_lite).toBeUndefined(); + expect(JSON.stringify(config)).toBe(beforeConfig); + }); + test("fresh none-only custom rows keep their ladder while retained provider rows still gain max", async () => { const models = await gatherRoutedModels({ port: 10100, diff --git a/tests/providers/provider-registry-parity.test.ts b/tests/providers/provider-registry-parity.test.ts index 8911bc4a1f..46579009ce 100644 --- a/tests/providers/provider-registry-parity.test.ts +++ b/tests/providers/provider-registry-parity.test.ts @@ -20,6 +20,7 @@ import { FREE_PROVIDER_DIRECTORY } from "../../src/providers/free-directory"; import { applyProviderConfigHints } from "../../src/codex/catalog"; import { routeModel } from "../../src/router"; import { resolveAdapter } from "../../src/server"; +import { isModelVisionSidecarConsumer } from "../../src/vision/eligibility"; import type { OcxConfig, OcxProviderConfig } from "../../src/types"; function nativeTemplate(): Record { @@ -112,7 +113,7 @@ describe("provider registry parity", () => { expect(Object.keys(map ?? {})).toContain("deepseek-flash"); } expect(nativeDeepseek?.preserveReasoningContentModels).toContain("deepseek-flash"); - expect(nativeDeepseek?.noVisionModels).toContain("deepseek-flash"); + expect(nativeDeepseek?.noVisionModels).not.toContain("deepseek-flash"); // The new id keeps the Flash ladder, not the Pro one, through isDeepseekFlashModel. expect(nativeDeepseek?.modelReasoningEfforts?.["deepseek-flash"]) .toEqual(nativeDeepseek?.modelReasoningEfforts?.["deepseek-v4-flash"]); @@ -239,9 +240,9 @@ describe("provider registry parity", () => { expect(KEY_LOGIN_PROVIDERS.deepseek.modelReasoningEffortMap?.["deepseek-v4-flash"]?.max).toBe("max"); expect(KEY_LOGIN_PROVIDERS.deepseek.preserveReasoningContentModels) .toEqual(["deepseek-flash", "deepseek-v4-flash"]); - // Issue #88: every DeepSeek API model is text-only input — the vision sidecar covers them. + // #4436: first-party Flash accepts images; unprobed compatibility aliases keep the sidecar. expect(KEY_LOGIN_PROVIDERS.deepseek.noVisionModels).toEqual([ - "deepseek-chat", "deepseek-reasoner", "deepseek-flash", "deepseek-v4-flash", + "deepseek-chat", "deepseek-reasoner", "deepseek-v4-flash", ]); }); @@ -456,6 +457,53 @@ describe("provider registry parity", () => { expect(neuralwatt?.preserveReasoningContentModels).not.toContain("moonshotai/Kimi-K2.5"); }); + test("first-party DeepSeek Flash advertises native images without widening gateway aliases (#4436)", () => { + const provider = providerConfigSeed(PROVIDER_REGISTRY.find(entry => entry.id === "deepseek")!); + expect(KEY_LOGIN_PROVIDERS.deepseek.modelInputModalities?.["deepseek-flash"]).toEqual(["text", "image"]); + expect(provider.modelInputModalities?.["deepseek-flash"]).toEqual(["text", "image"]); + expect(isModelVisionSidecarConsumer(provider, "deepseek-flash")).toBe(false); + expect(isModelVisionSidecarConsumer(provider, "deepseek-v4-flash-vision-exp")).toBe(false); + for (const model of ["deepseek-chat", "deepseek-reasoner", "deepseek-v4-flash"]) { + expect(isModelVisionSidecarConsumer(provider, model)).toBe(true); + } + for (const id of ["opencode-go", "opencode-zen"]) { + const gateway = providerConfigSeed(PROVIDER_REGISTRY.find(entry => entry.id === id)!); + expect(isModelVisionSidecarConsumer(gateway, "deepseek-v4.1-flash")).toBe(true); + expect(isModelVisionSidecarConsumer(gateway, "deepseek-v4-flash")).toBe(true); + } + const free = providerConfigSeed(PROVIDER_REGISTRY.find(entry => entry.id === "opencode-free")!); + expect(isModelVisionSidecarConsumer(free, "deepseek-v4-flash-free")).toBe(true); + // Saved providers without explicit modality overrides inherit the fix during routing. + const config: OcxConfig = { + port: 0, defaultProvider: "deepseek", + providers: { deepseek: { adapter: "openai-chat", baseUrl: "https://api.deepseek.com", authMode: "key" } }, + }; + const route = routeModel(config, "deepseek/deepseek-flash"); + expect(isModelVisionSidecarConsumer(route.provider, route.modelId)).toBe(false); + const model = applyProviderConfigHints("deepseek", route.provider, { provider: "deepseek", id: route.modelId }); + expect(model.inputModalities).toEqual(["text", "image"]); + const catalog = buildCatalogEntries(nativeTemplate(), [], [model]); + expect(catalog.find(entry => entry.slug === "deepseek/deepseek-flash")?.input_modalities).toEqual(["text", "image"]); + + // Existing saved providers that previously persisted the old seed continue using the sidecar + // until deepseek-flash is removed from their saved noVisionModels list. + const legacyConfig: OcxConfig = { + port: 0, defaultProvider: "deepseek", + providers: { + deepseek: { + adapter: "openai-chat", baseUrl: "https://api.deepseek.com", authMode: "key", + noVisionModels: ["deepseek-chat", "deepseek-reasoner", "deepseek-flash", "deepseek-v4-flash"], + }, + }, + }; + const legacyRoute = routeModel(legacyConfig, "deepseek/deepseek-flash"); + expect(isModelVisionSidecarConsumer(legacyRoute.provider, legacyRoute.modelId)).toBe(true); + // Once deepseek-flash is removed from saved config, native vision is unlocked. + legacyConfig.providers.deepseek.noVisionModels = ["deepseek-chat", "deepseek-reasoner", "deepseek-v4-flash"]; + const upgradedRoute = routeModel(legacyConfig, "deepseek/deepseek-flash"); + expect(isModelVisionSidecarConsumer(upgradedRoute.provider, upgradedRoute.modelId)).toBe(false); + }); + test("Z.AI and Kimi context aliases route with bracket-suffix stripping", () => { const zai = PROVIDER_REGISTRY.find(entry => entry.id === "zai"); const optedInProviders = PROVIDER_REGISTRY diff --git a/tests/routing/router.test.ts b/tests/routing/router.test.ts index 9b6eb68d4d..d8b92dfdc7 100644 --- a/tests/routing/router.test.ts +++ b/tests/routing/router.test.ts @@ -431,7 +431,7 @@ describe("routeModel registry effort defaults", () => { expect(route.provider.modelReasoningEfforts?.["umans-kimi-k2.7"]).toEqual(["low", "medium", "high", "xhigh", "max"]); }); - test("minimal persisted DeepSeek config inherits the registry text-only classification (issue #88)", () => { + test("minimal persisted DeepSeek config inherits registry vision classification (#4436)", () => { const config: OcxConfig = { port: 10100, defaultProvider: "deepseek", @@ -447,7 +447,7 @@ describe("routeModel registry effort defaults", () => { const route = routeModel(config, "deepseek/deepseek-v4-flash"); expect(route.provider.noVisionModels).toEqual([ - "deepseek-chat", "deepseek-reasoner", "deepseek-flash", "deepseek-v4-flash", + "deepseek-chat", "deepseek-reasoner", "deepseek-v4-flash", ]); }); diff --git a/tests/vision/vision-sidecar-e2e.test.ts b/tests/vision/vision-sidecar-e2e.test.ts index d88b0070df..29c06c4fe9 100644 --- a/tests/vision/vision-sidecar-e2e.test.ts +++ b/tests/vision/vision-sidecar-e2e.test.ts @@ -413,6 +413,52 @@ describe("vision sidecar fallback (issue #88, end-to-end)", () => { } }); + test.each(["openai-chat", "openai-responses"] as const)("DeepSeek Flash preserves native images on the %s wire without a sidecar call (#4436)", async adapter => { + let upstreamBody = ""; + let sidecarHits = 0; + upstream = adapter === "openai-chat" + ? serveUpstream(b => { upstreamBody = b; }) + : serveResponsesUpstream(b => { upstreamBody = b; }); + sidecar = serveResponsesUpstream(() => { sidecarHits += 1; }); + const deepseek = PROVIDER_REGISTRY.find(entry => entry.id === "deepseek")!; + const config: OcxConfig = { + port: 0, hostname: "127.0.0.1", defaultProvider: "deepseeklike", + providers: { + // Carry the real registry classification to a loopback fixture on each wire. + deepseeklike: { + adapter, authMode: "key", baseUrl: upstream.url.toString().replace(/\/$/, ""), + allowPrivateNetwork: true, apiKey: "key-alpha-000111222333", + noVisionModels: deepseek.noVisionModels, + modelInputModalities: deepseek.modelInputModalities, + }, + helper: { + adapter: "openai-responses", authMode: "key", baseUrl: sidecar.url.toString().replace(/\/$/, ""), + allowPrivateNetwork: true, apiKey: "key-alpha-000111222333", + modelInputModalities: { "vision-model": ["text", "image"] }, + }, + }, + visionSidecar: { enabled: true, backend: "routed", model: "helper/vision-model" }, + }; + saveConfig(config); + const server = startServer(0); + try { + const res = await fetch(new URL("/v1/responses", server.url), { + method: "POST", headers: { "content-type": "application/json" }, + body: JSON.stringify(baseRequest("deepseeklike/deepseek-flash")), + }); + expect(res.status).toBe(200); + expect(sidecarHits).toBe(0); + const body = JSON.parse(upstreamBody); + const content = adapter === "openai-chat" ? body.messages[0].content : body.input[0].content; + expect(content).toContainEqual(adapter === "openai-chat" + ? expect.objectContaining({ type: "image_url", image_url: expect.objectContaining({ url: PNG_DATA_URL }) }) + : expect.objectContaining({ type: "input_image", image_url: PNG_DATA_URL })); + expect(upstreamBody).not.toContain("[image omitted"); + } finally { + await server.stop(true); + } + }); + /* * #1043 activation evidence. The registry classification is only useful if the * strip actually fires for a Zen model, so this drives the real path with the