mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(wiki): add Zhipu GLM-V and Volcano Doubao vision providers
This commit is contained in:
parent
af5670f2f7
commit
ac654f3957
@ -1,62 +1,30 @@
|
||||
package vip.mate.tool.image.vision.provider;
|
||||
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import vip.mate.agent.prompt.PromptLoader;
|
||||
import vip.mate.exception.MateClawException;
|
||||
import vip.mate.llm.model.ModelProviderEntity;
|
||||
import vip.mate.llm.service.ModelProviderService;
|
||||
import vip.mate.system.model.SystemSettingsDTO;
|
||||
import vip.mate.tool.image.ImageCapability;
|
||||
import vip.mate.tool.image.vision.ImageVisionProvider;
|
||||
import vip.mate.tool.image.vision.VisionContext;
|
||||
import vip.mate.tool.image.vision.VisionRequest;
|
||||
import vip.mate.tool.image.vision.VisionResult;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* DashScope vision provider — uses qwen-vl-max via the OpenAI-compatible
|
||||
* endpoint at {@code /compatible-mode/v1/chat/completions}.
|
||||
* DashScope vision provider — uses {@code qwen-vl-max} via the
|
||||
* OpenAI-compatible endpoint at {@code /compatible-mode/v1/chat/completions}.
|
||||
*
|
||||
* <p>Default for the regional production rollout: API keys are typically
|
||||
* available (DASHSCOPE_API_KEY is mandatory for the rest of the platform)
|
||||
* and per-image cost is the lowest of the supported vendors.
|
||||
*
|
||||
* <p>Implementation deliberately uses Hutool {@code HttpRequest} rather
|
||||
* than building a Spring AI {@code ChatClient} — the call shape is a
|
||||
* one-shot request/response that doesn't benefit from the heavyweight
|
||||
* agent-style observability / retry / cache machinery in
|
||||
* {@code AgentOpenAiCompatibleChatModelBuilder}.
|
||||
*
|
||||
* @author MateClaw Team
|
||||
* <p>Default for the Chinese cloud rollout: API keys are typically
|
||||
* available (DASHSCOPE_API_KEY is mandatory for the rest of the
|
||||
* platform) and per-image cost is the lowest of the supported vendors,
|
||||
* so this provider sits at the front of the auto-detect chain.
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class DashScopeVisionProvider implements ImageVisionProvider {
|
||||
public class DashScopeVisionProvider extends OpenAiCompatibleVisionProvider {
|
||||
|
||||
private static final String PROVIDER_ID = "dashscope-vision";
|
||||
private static final String DASHSCOPE_PROVIDER_KEY = "dashscope";
|
||||
private static final String DEFAULT_MODEL = "qwen-vl-max";
|
||||
private static final String DEFAULT_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1";
|
||||
private static final int CALL_TIMEOUT_MS = 60_000;
|
||||
private static final Pattern OFF_TOPIC_PREFIX = Pattern.compile("^\\s*\\[OFF-TOPIC\\]\\s*", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private final ModelProviderService modelProviderService;
|
||||
private final ObjectMapper objectMapper;
|
||||
public DashScopeVisionProvider(ModelProviderService modelProviderService,
|
||||
ObjectMapper objectMapper) {
|
||||
super(modelProviderService, objectMapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String id() {
|
||||
@ -68,154 +36,23 @@ public class DashScopeVisionProvider implements ImageVisionProvider {
|
||||
return "DashScope qwen-vl";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresCredential() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int autoDetectOrder() {
|
||||
return 10;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ImageCapability> capabilities() {
|
||||
return Set.of(ImageCapability.IMAGE_TO_TEXT);
|
||||
protected String providerKey() {
|
||||
return DASHSCOPE_PROVIDER_KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable(SystemSettingsDTO settings) {
|
||||
try {
|
||||
return modelProviderService.isProviderConfigured(DASHSCOPE_PROVIDER_KEY);
|
||||
} catch (Exception e) {
|
||||
log.debug("[Vision][dashscope] availability check failed: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
protected String defaultBaseUrl() {
|
||||
return DEFAULT_BASE_URL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VisionResult caption(VisionRequest request, SystemSettingsDTO settings) {
|
||||
ModelProviderEntity provider;
|
||||
try {
|
||||
provider = modelProviderService.getProviderConfig(DASHSCOPE_PROVIDER_KEY);
|
||||
} catch (Exception e) {
|
||||
throw new MateClawException("err.wiki.vision.no_provider",
|
||||
"DashScope provider is not configured");
|
||||
}
|
||||
String apiKey = provider.getApiKey();
|
||||
if (apiKey == null || apiKey.isBlank()) {
|
||||
throw new MateClawException("err.wiki.vision.no_provider",
|
||||
"DashScope API key is missing");
|
||||
}
|
||||
String baseUrl = (provider.getBaseUrl() == null || provider.getBaseUrl().isBlank())
|
||||
? DEFAULT_BASE_URL
|
||||
: provider.getBaseUrl();
|
||||
String model = (request.getPreferModel() == null || request.getPreferModel().isBlank())
|
||||
? DEFAULT_MODEL
|
||||
: request.getPreferModel();
|
||||
|
||||
String prompt = buildPrompt(request.getContext());
|
||||
String imageDataUrl = buildDataUrl(request.getMimeType(), request.getImageBytes());
|
||||
|
||||
ObjectNode body = objectMapper.createObjectNode();
|
||||
body.put("model", model);
|
||||
body.put("temperature", 0.0);
|
||||
body.put("max_tokens", 4096);
|
||||
|
||||
ArrayNode messages = body.putArray("messages");
|
||||
ObjectNode userMsg = messages.addObject();
|
||||
userMsg.put("role", "user");
|
||||
ArrayNode content = userMsg.putArray("content");
|
||||
content.addObject().put("type", "text").put("text", prompt);
|
||||
ObjectNode imagePart = content.addObject();
|
||||
imagePart.put("type", "image_url");
|
||||
imagePart.putObject("image_url").put("url", imageDataUrl);
|
||||
|
||||
long startNanos = System.nanoTime();
|
||||
HttpResponse response;
|
||||
try {
|
||||
response = HttpRequest.post(baseUrl + "/chat/completions")
|
||||
.header("Authorization", "Bearer " + apiKey)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(body.toString())
|
||||
.timeout(CALL_TIMEOUT_MS)
|
||||
.execute();
|
||||
} catch (Exception e) {
|
||||
throw new MateClawException("err.wiki.vision.provider_failed",
|
||||
"DashScope vision call failed: " + e.getMessage());
|
||||
}
|
||||
long durationMs = Duration.ofNanos(System.nanoTime() - startNanos).toMillis();
|
||||
|
||||
if (response.getStatus() != 200) {
|
||||
String errBody = response.body();
|
||||
log.warn("[Vision][dashscope] HTTP {} body={}", response.getStatus(), truncate(errBody, 400));
|
||||
throw new MateClawException("err.wiki.vision.provider_failed",
|
||||
"DashScope vision returned HTTP " + response.getStatus());
|
||||
}
|
||||
|
||||
String text;
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(response.body());
|
||||
text = root.path("choices").path(0).path("message").path("content").asText("").trim();
|
||||
} catch (Exception e) {
|
||||
throw new MateClawException("err.wiki.vision.provider_failed",
|
||||
"DashScope vision response was unparseable: " + e.getMessage());
|
||||
}
|
||||
if (text.isEmpty()) {
|
||||
throw new MateClawException("err.wiki.vision.provider_failed",
|
||||
"DashScope vision returned an empty caption");
|
||||
}
|
||||
|
||||
Matcher offTopic = OFF_TOPIC_PREFIX.matcher(text);
|
||||
boolean isOffTopic = offTopic.find();
|
||||
String caption = isOffTopic ? offTopic.replaceFirst("").trim() : text;
|
||||
|
||||
return VisionResult.builder()
|
||||
.caption(caption)
|
||||
.visibleText(extractVisibleTextHeuristic(caption))
|
||||
.offTopic(isOffTopic)
|
||||
.providerId(PROVIDER_ID)
|
||||
.model(model)
|
||||
.capturedAt(Instant.now())
|
||||
.durationMs(durationMs)
|
||||
.build();
|
||||
}
|
||||
|
||||
private String buildPrompt(VisionContext context) {
|
||||
if (context != null && context.hasContext()) {
|
||||
String template = PromptLoader.loadPrompt("wiki/vision-caption-context-aware");
|
||||
String before = (context.getBeforeText() == null || context.getBeforeText().isBlank())
|
||||
? "(none)" : context.getBeforeText().trim();
|
||||
String after = (context.getAfterText() == null || context.getAfterText().isBlank())
|
||||
? "(none)" : context.getAfterText().trim();
|
||||
return template.replace("{before}", before).replace("{after}", after);
|
||||
}
|
||||
return PromptLoader.loadPrompt("wiki/vision-caption-factual");
|
||||
}
|
||||
|
||||
private static String buildDataUrl(String mimeType, byte[] bytes) {
|
||||
String mime = (mimeType == null || mimeType.isBlank()) ? "image/png" : mimeType;
|
||||
return "data:" + mime + ";base64," + Base64.getEncoder().encodeToString(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Heuristic: pull obvious quoted strings as "visible text". This is a
|
||||
* placeholder until a stronger OCR-grade extraction lands; for the
|
||||
* factual prompt the model already inlines text verbatim into the
|
||||
* caption, so the cache row's caption alone is enough for retrieval.
|
||||
*/
|
||||
private static String extractVisibleTextHeuristic(String caption) {
|
||||
Matcher m = Pattern.compile("\"([^\"]{3,})\"").matcher(caption);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
while (m.find()) {
|
||||
if (sb.length() > 0) sb.append('\n');
|
||||
sb.append(m.group(1));
|
||||
}
|
||||
return sb.length() == 0 ? null : sb.toString();
|
||||
}
|
||||
|
||||
private static String truncate(String s, int max) {
|
||||
if (s == null) return "";
|
||||
return s.length() > max ? s.substring(0, max) + "…" : s;
|
||||
protected String defaultModel() {
|
||||
return DEFAULT_MODEL;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
package vip.mate.tool.image.vision.provider;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
import vip.mate.llm.service.ModelProviderService;
|
||||
|
||||
/**
|
||||
* Volcengine Ark Doubao vision provider — uses
|
||||
* {@code doubao-seed-1-8-251228} (the seeded multimodal flagship)
|
||||
* via the OpenAI-compatible endpoint at {@code /api/v3/chat/completions}.
|
||||
*
|
||||
* <p>Attaches to the {@code volcengine} model provider; the
|
||||
* {@code volcengine-plan} sibling (coding subscription tier) is
|
||||
* intentionally not consumed here because the multimodal model
|
||||
* isn't part of that plan.
|
||||
*/
|
||||
@Component
|
||||
public class DoubaoVisionProvider extends OpenAiCompatibleVisionProvider {
|
||||
|
||||
private static final String PROVIDER_ID = "doubao-vision";
|
||||
private static final String VOLCENGINE_PROVIDER_KEY = "volcengine";
|
||||
private static final String DEFAULT_MODEL = "doubao-seed-1-8-251228";
|
||||
private static final String DEFAULT_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3";
|
||||
|
||||
public DoubaoVisionProvider(ModelProviderService modelProviderService,
|
||||
ObjectMapper objectMapper) {
|
||||
super(modelProviderService, objectMapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String id() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String label() {
|
||||
return "Volcano Doubao Vision";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int autoDetectOrder() {
|
||||
return 30;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String providerKey() {
|
||||
return VOLCENGINE_PROVIDER_KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String defaultBaseUrl() {
|
||||
return DEFAULT_BASE_URL;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String defaultModel() {
|
||||
return DEFAULT_MODEL;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,221 @@
|
||||
package vip.mate.tool.image.vision.provider;
|
||||
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import vip.mate.agent.prompt.PromptLoader;
|
||||
import vip.mate.exception.MateClawException;
|
||||
import vip.mate.llm.model.ModelProviderEntity;
|
||||
import vip.mate.llm.service.ModelProviderService;
|
||||
import vip.mate.system.model.SystemSettingsDTO;
|
||||
import vip.mate.tool.image.ImageCapability;
|
||||
import vip.mate.tool.image.vision.ImageVisionProvider;
|
||||
import vip.mate.tool.image.vision.VisionContext;
|
||||
import vip.mate.tool.image.vision.VisionRequest;
|
||||
import vip.mate.tool.image.vision.VisionResult;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Shared base for vision providers that hit OpenAI-compatible
|
||||
* {@code /chat/completions} endpoints with image-content blocks.
|
||||
*
|
||||
* <p>DashScope (qwen-vl), Zhipu (glm-v) and Volcengine Ark (doubao
|
||||
* vision) all accept the same request shape — a {@code messages} array
|
||||
* whose user message contains a {@code text} part and an
|
||||
* {@code image_url} part with a base64 data URL. Extracting that
|
||||
* shape into one place lets each concrete provider supply only its
|
||||
* identity and routing constants.
|
||||
*
|
||||
* <p>Subclass contract: implement {@link #id()}, {@link #label()},
|
||||
* {@link #autoDetectOrder()}, {@link #providerKey()},
|
||||
* {@link #defaultBaseUrl()}, and {@link #defaultModel()}. Override
|
||||
* {@link #completionsPath()} when the endpoint differs from the
|
||||
* default {@code /chat/completions}.
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class OpenAiCompatibleVisionProvider implements ImageVisionProvider {
|
||||
|
||||
protected static final int CALL_TIMEOUT_MS = 60_000;
|
||||
private static final Pattern OFF_TOPIC_PREFIX =
|
||||
Pattern.compile("^\\s*\\[OFF-TOPIC\\]\\s*", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
protected final ModelProviderService modelProviderService;
|
||||
protected final ObjectMapper objectMapper;
|
||||
|
||||
protected OpenAiCompatibleVisionProvider(ModelProviderService modelProviderService,
|
||||
ObjectMapper objectMapper) {
|
||||
this.modelProviderService = modelProviderService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
/** {@code mate_model_provider.provider_id} this vision provider attaches to. */
|
||||
protected abstract String providerKey();
|
||||
|
||||
protected abstract String defaultBaseUrl();
|
||||
|
||||
protected abstract String defaultModel();
|
||||
|
||||
/** Override when the endpoint isn't the OpenAI-standard suffix. */
|
||||
protected String completionsPath() {
|
||||
return "/chat/completions";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresCredential() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ImageCapability> capabilities() {
|
||||
return Set.of(ImageCapability.IMAGE_TO_TEXT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable(SystemSettingsDTO settings) {
|
||||
try {
|
||||
return modelProviderService.isProviderConfigured(providerKey());
|
||||
} catch (Exception e) {
|
||||
log.debug("[Vision][{}] availability check failed: {}", id(), e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public VisionResult caption(VisionRequest request, SystemSettingsDTO settings) {
|
||||
ModelProviderEntity provider = loadProvider();
|
||||
String apiKey = provider.getApiKey();
|
||||
if (apiKey == null || apiKey.isBlank()) {
|
||||
throw new MateClawException("err.wiki.vision.no_provider",
|
||||
label() + " API key is missing");
|
||||
}
|
||||
String baseUrl = (provider.getBaseUrl() == null || provider.getBaseUrl().isBlank())
|
||||
? defaultBaseUrl()
|
||||
: provider.getBaseUrl();
|
||||
String model = (request.getPreferModel() == null || request.getPreferModel().isBlank())
|
||||
? defaultModel()
|
||||
: request.getPreferModel();
|
||||
|
||||
String prompt = buildPrompt(request.getContext());
|
||||
String imageDataUrl = buildDataUrl(request.getMimeType(), request.getImageBytes());
|
||||
|
||||
ObjectNode body = objectMapper.createObjectNode();
|
||||
body.put("model", model);
|
||||
body.put("temperature", 0.0);
|
||||
body.put("max_tokens", 4096);
|
||||
|
||||
ArrayNode messages = body.putArray("messages");
|
||||
ObjectNode userMsg = messages.addObject();
|
||||
userMsg.put("role", "user");
|
||||
ArrayNode content = userMsg.putArray("content");
|
||||
content.addObject().put("type", "text").put("text", prompt);
|
||||
ObjectNode imagePart = content.addObject();
|
||||
imagePart.put("type", "image_url");
|
||||
imagePart.putObject("image_url").put("url", imageDataUrl);
|
||||
|
||||
long startNanos = System.nanoTime();
|
||||
HttpResponse response;
|
||||
try {
|
||||
response = HttpRequest.post(baseUrl + completionsPath())
|
||||
.header("Authorization", "Bearer " + apiKey)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(body.toString())
|
||||
.timeout(CALL_TIMEOUT_MS)
|
||||
.execute();
|
||||
} catch (Exception e) {
|
||||
throw new MateClawException("err.wiki.vision.provider_failed",
|
||||
label() + " vision call failed: " + e.getMessage());
|
||||
}
|
||||
long durationMs = Duration.ofNanos(System.nanoTime() - startNanos).toMillis();
|
||||
|
||||
if (response.getStatus() != 200) {
|
||||
String errBody = response.body();
|
||||
log.warn("[Vision][{}] HTTP {} body={}", id(), response.getStatus(), truncate(errBody, 400));
|
||||
throw new MateClawException("err.wiki.vision.provider_failed",
|
||||
label() + " vision returned HTTP " + response.getStatus());
|
||||
}
|
||||
|
||||
String text;
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(response.body());
|
||||
text = root.path("choices").path(0).path("message").path("content").asText("").trim();
|
||||
} catch (Exception e) {
|
||||
throw new MateClawException("err.wiki.vision.provider_failed",
|
||||
label() + " vision response was unparseable: " + e.getMessage());
|
||||
}
|
||||
if (text.isEmpty()) {
|
||||
throw new MateClawException("err.wiki.vision.provider_failed",
|
||||
label() + " vision returned an empty caption");
|
||||
}
|
||||
|
||||
Matcher offTopic = OFF_TOPIC_PREFIX.matcher(text);
|
||||
boolean isOffTopic = offTopic.find();
|
||||
String caption = isOffTopic ? offTopic.replaceFirst("").trim() : text;
|
||||
|
||||
return VisionResult.builder()
|
||||
.caption(caption)
|
||||
.visibleText(extractVisibleTextHeuristic(caption))
|
||||
.offTopic(isOffTopic)
|
||||
.providerId(id())
|
||||
.model(model)
|
||||
.capturedAt(Instant.now())
|
||||
.durationMs(durationMs)
|
||||
.build();
|
||||
}
|
||||
|
||||
private ModelProviderEntity loadProvider() {
|
||||
try {
|
||||
return modelProviderService.getProviderConfig(providerKey());
|
||||
} catch (Exception e) {
|
||||
throw new MateClawException("err.wiki.vision.no_provider",
|
||||
label() + " provider is not configured");
|
||||
}
|
||||
}
|
||||
|
||||
private String buildPrompt(VisionContext context) {
|
||||
if (context != null && context.hasContext()) {
|
||||
String template = PromptLoader.loadPrompt("wiki/vision-caption-context-aware");
|
||||
String before = (context.getBeforeText() == null || context.getBeforeText().isBlank())
|
||||
? "(none)" : context.getBeforeText().trim();
|
||||
String after = (context.getAfterText() == null || context.getAfterText().isBlank())
|
||||
? "(none)" : context.getAfterText().trim();
|
||||
return template.replace("{before}", before).replace("{after}", after);
|
||||
}
|
||||
return PromptLoader.loadPrompt("wiki/vision-caption-factual");
|
||||
}
|
||||
|
||||
private static String buildDataUrl(String mimeType, byte[] bytes) {
|
||||
String mime = (mimeType == null || mimeType.isBlank()) ? "image/png" : mimeType;
|
||||
return "data:" + mime + ";base64," + Base64.getEncoder().encodeToString(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Heuristic: pull obvious quoted strings as "visible text". This is a
|
||||
* placeholder until a stronger OCR-grade extraction lands; for the
|
||||
* factual prompt the model already inlines text verbatim into the
|
||||
* caption, so the cache row's caption alone is enough for retrieval.
|
||||
*/
|
||||
private static String extractVisibleTextHeuristic(String caption) {
|
||||
Matcher m = Pattern.compile("\"([^\"]{3,})\"").matcher(caption);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
while (m.find()) {
|
||||
if (sb.length() > 0) sb.append('\n');
|
||||
sb.append(m.group(1));
|
||||
}
|
||||
return sb.length() == 0 ? null : sb.toString();
|
||||
}
|
||||
|
||||
private static String truncate(String s, int max) {
|
||||
if (s == null) return "";
|
||||
return s.length() > max ? s.substring(0, max) + "…" : s;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
package vip.mate.tool.image.vision.provider;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
import vip.mate.llm.service.ModelProviderService;
|
||||
|
||||
/**
|
||||
* Zhipu vision provider — uses {@code glm-5v-turbo} via the
|
||||
* OpenAI-compatible endpoint at {@code /api/paas/v4/chat/completions}.
|
||||
*
|
||||
* <p>Attaches to the {@code zhipu-cn} model provider. Operators with
|
||||
* the international tier should configure the {@code zhipu-cn} entry
|
||||
* with the international base URL or wait for an explicit intl
|
||||
* sibling provider — the seeded {@code zhipu-intl} differs only in
|
||||
* base URL.
|
||||
*/
|
||||
@Component
|
||||
public class ZhipuVisionProvider extends OpenAiCompatibleVisionProvider {
|
||||
|
||||
private static final String PROVIDER_ID = "zhipu-vision";
|
||||
private static final String ZHIPU_PROVIDER_KEY = "zhipu-cn";
|
||||
private static final String DEFAULT_MODEL = "glm-5v-turbo";
|
||||
private static final String DEFAULT_BASE_URL = "https://open.bigmodel.cn/api/paas/v4";
|
||||
|
||||
public ZhipuVisionProvider(ModelProviderService modelProviderService,
|
||||
ObjectMapper objectMapper) {
|
||||
super(modelProviderService, objectMapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String id() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String label() {
|
||||
return "Zhipu GLM-V";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int autoDetectOrder() {
|
||||
return 20;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String providerKey() {
|
||||
return ZHIPU_PROVIDER_KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String defaultBaseUrl() {
|
||||
return DEFAULT_BASE_URL;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String defaultModel() {
|
||||
return DEFAULT_MODEL;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user