diff --git a/mateclaw-server/src/main/java/vip/mate/llm/model/ModelConfigEntity.java b/mateclaw-server/src/main/java/vip/mate/llm/model/ModelConfigEntity.java index 09bbe302..746ee6a5 100644 --- a/mateclaw-server/src/main/java/vip/mate/llm/model/ModelConfigEntity.java +++ b/mateclaw-server/src/main/java/vip/mate/llm/model/ModelConfigEntity.java @@ -77,6 +77,16 @@ public class ModelConfigEntity { */ private String modalities; + /** + * Transient, request-scoped flag set by {@link vip.mate.llm.service.ModelConfigService#listByType} + * when a modality filter is supplied: {@code true} when this row's declared or + * heuristically-resolved capabilities cover the requested modality. Lets the + * sidecar selector list every enabled chat model while still highlighting the + * ones already known to support the modality. Never persisted. + */ + @TableField(exist = false) + private Boolean modalityCapable; + @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; diff --git a/mateclaw-server/src/main/java/vip/mate/llm/routing/MultimodalRouter.java b/mateclaw-server/src/main/java/vip/mate/llm/routing/MultimodalRouter.java index 9a51d46c..b4ca2273 100644 --- a/mateclaw-server/src/main/java/vip/mate/llm/routing/MultimodalRouter.java +++ b/mateclaw-server/src/main/java/vip/mate/llm/routing/MultimodalRouter.java @@ -125,11 +125,19 @@ public class MultimodalRouter { } /** - * Resolve the configured sidecar model for a modality. Returns null when: + * Resolve the configured sidecar model for a modality. Returns null only when: * - the setting is empty / blank; - * - the referenced row no longer exists or has been disabled; - * - the row's resolved capability set does not actually contain the modality. + * - the referenced row no longer exists or has been disabled. * The caller treats null as "ask the user to configure one." + *
+ * An explicit sidecar selection is treated as the user's own capability + * declaration: a provider-compatible model can be vision-capable in practice + * even when the built-in heuristics don't recognize its name and it carries no + * declared {@code modalities}. Rejecting such a model here made it impossible to + * use a perfectly good compatible-mode vision model as the sidecar. We therefore + * honour the explicit choice and only emit a diagnostic when the heuristics + * can't confirm it — a wrong pick degrades gracefully (the caption call fails and + * the attachment is reported as un-processed) rather than being silently ignored. */ private ModelConfigEntity resolveSidecar(Modality modality) { SystemSettingsDTO settings = systemSettingService.getSettings(); @@ -148,9 +156,9 @@ public class MultimodalRouter { } if (model == null || !Boolean.TRUE.equals(model.getEnabled())) return null; if (!capabilityService.supports(model.getModelName(), model.getModalities(), modality)) { - log.warn("Configured sidecar model {}/{} does not actually support {} — ignoring", - model.getProvider(), model.getModelName(), modality); - return null; + log.info("Configured sidecar model {}/{} is not recognized as {}-capable by the " + + "built-in heuristics; honouring the explicit selection anyway", + model.getProvider(), model.getModelName(), modality.name().toLowerCase()); } return model; } diff --git a/mateclaw-server/src/main/java/vip/mate/llm/service/ModelConfigService.java b/mateclaw-server/src/main/java/vip/mate/llm/service/ModelConfigService.java index 983cd49e..24be318b 100644 --- a/mateclaw-server/src/main/java/vip/mate/llm/service/ModelConfigService.java +++ b/mateclaw-server/src/main/java/vip/mate/llm/service/ModelConfigService.java @@ -11,6 +11,7 @@ import vip.mate.llm.event.ModelConfigChangedEvent; import vip.mate.llm.model.ModelConfigEntity; import vip.mate.llm.repository.ModelConfigMapper; +import java.util.Comparator; import java.util.List; import org.springframework.context.ApplicationEventPublisher; @@ -73,9 +74,18 @@ public class ModelConfigService { /** * Optional modality filter (case-insensitive: {@code "vision" / "video" / "audio"}). - * When non-null, only enabled rows whose resolved capability set contains the - * requested modality survive — used by the multimodal sidecar settings UI to - * populate "default vision model" / "default video model" dropdowns. + * Used by the multimodal sidecar settings UI to populate "default vision model" / + * "default video model" dropdowns. + *
+ * The filter does not hide models the built-in heuristics fail to recognize:
+ * a provider-compatible model (e.g. a DashScope OpenAI-compatible vision model with
+ * a custom name) is vision-capable in practice even though its name matches no
+ * built-in prefix and it carries no declared {@code modalities}. Hard-filtering
+ * those out left them un-selectable as a sidecar. Instead every enabled
+ * chat model is returned; each row's transient {@link ModelConfigEntity#getModalityCapable()}
+ * flag records whether its declared / heuristic capabilities already cover the
+ * requested modality, and known-capable rows are sorted to the top so the UI can
+ * highlight them while still letting the user pick any model.
*/
public List