### 1、支持哪些聊天模型？

支持聊天模型，其实是支持接口风格。比如 DeepSeek-V3 官网的接口兼容 openai；在 ollama 平台是另一种接口风格；在阿里百炼则有两种接口风格，一种兼容 openai，另一种则是百炼专属风格；在模力方舟（ai.gitee）则是兼容 openai。


聊天模型的这种接口风格，称为聊天方言（简称，方言）。ChatConfig 通过 `provider` 或 `apiUrl`识别模型服务是由谁提供的。并自动选择对应的聊天方言适配。


框架内置的方言适配有：

| 言方               | 配置要求 | 描述 |
| ----------- | ------------------ | -------- |
| openai                   |   默认 <br/>或者<br/> `apiUrl=.../v1/chat/completions`                                | 兼容 openai 的接口规范（默认）     |
| openai-responses   | `provider=openai-responses` <br/>或者<br/> `apiUrl=.../v1/responses`         | 兼容 openai-responses 的接口规范      |
| ollama                   | `provider=ollama`          | 兼容 ollama 的接口规范      |
| gemini                   | `provider=gemini` <br/>或者<br/> `apiUrl=.../v1beta/models/*`         | 兼容 google gemini 的接口规范（v3.8.1 后可试用）      |
| anthropic               | `provider=anthropic` <br/>或者<br/> `apiUrl=.../v1/messages`         | 兼容 anthropic claude 的接口规范（v3.9.1 后可试用）。<br/><mark>claude 还有个 openai 的兼容模式（使用 openai 方言）<mark/>      |
| dashscope     |  `provider=dashscope` <br/>或者<br/> `apiUrl=.../v1/services/*`                                  | 兼容 dashscope （阿里云的平台百炼）的接口规范。<br/><mark>dashscope 还有个 openai 的兼容模式（使用 openai 方言）</mark>    |


那支持哪些聊天模型？

* 所有兼容 openai 的模型或平台服务（比如："DeepSeek"、"QWen"、"GLM"、"Kimi"、"MiniMax"、“Claude（openai 兼容模式）”、"Gemini（openai 兼容模式）"、"DashScope（openai 兼容模式）"、"GPT"、“模力方舟”、“硅基流动”、“魔搭社区（魔力空间）”、“Xinference”、“火山引擎”、“智谱”、“讯飞火星”、“百度千帆”、“阿里百炼”、"MiniMax" 等），都兼容
* 所有 ollama 平台上的模型，都兼容 
* 所有 gemini、 claude 相关模型，都兼容 
* 所有 阿里百炼 平台上的模型（同时提供有 “百炼” 和 “openai” 两套接口），都兼容


构建示例：

```java
ChatModel chatModel = ChatModel.of("http://127.0.0.1:11434/api/chat") //使用完整地址（而不是 api_base）
                .headerSet("x-demo", "demo1")
                .provider("ollama")
                .model("llama3.2")
                .build();
```

### 2、自带的方言依赖包



| 方言依赖包                                          | 描述                                                     | 
| ------------------------------- | -------------------------------- | 
| org.noear:solon-ai                               | 包含 solon-ai-core 和下面所有的方言包。一般引用这个     |
| org.noear:solon-ai-dialect-openai         | 兼容 openai 的方言包                              | 
| org.noear:solon-ai-dialect-ollama         | 兼容 ollama 的方言包                              | 
| org.noear:solon-ai-dialect-dashscope    | 兼容 dashscope 的方言包                        | 
| org.noear:solon-ai-dialect-gemini          | 兼容 gemini 的方言包                        | 

提醒：一般匹配不到方言时？要么是 provider 配置有问题，要么是 pom 缺少相关的依赖包。


### 3、聊天方言接口定义

```java
public interface ChatDialect extends AiModelDialect {
    /**
     * 是否为默认
     */
    default boolean isDefault() {
        return false;
    }

    /**
     * 匹配检测
     *
     * @param config 聊天配置
     */
    boolean matched(ChatConfig config);

    /**
     * 创建 http 工具
     *
     * @param config 聊天配置
     */
    HttpUtils createHttpUtils(ChatConfig config);

    /**
     * 创建 http 工具
     *
     * @param config   聊天配置
     * @param isStream 是否流式获取
     */
    default HttpUtils createHttpUtils(ChatConfig config, boolean isStream) {
        return createHttpUtils(config);
    }

    /**
     * 构建请求数据
     *
     * @param config   聊天配置
     * @param options  聊天选项
     * @param messages 消息
     * @param isStream 是否流式获取
     */
    String buildRequestJson(ChatConfig config, ChatOptions options, List<ChatMessage> messages, boolean isStream);

    /**
     * 构建助理消息节点
     *
     * @param toolCallBuilders 工具调用构建器集合
     */
    ONode buildAssistantMessageNode(Map<String, ToolCallBuilder> toolCallBuilders);

    /**
     * 构建助理消息根据直接返回的工具消息
     *
     * @param toolMessages 直接返回的工具消息
     */
    AssistantMessage buildAssistantMessageByToolMessages(List<ToolMessage> toolMessages);

    /**
     * 分析响应数据
     *
     * @param config   聊天配置
     * @param resp     响应体
     * @param respJson 响应数据
     */
    boolean parseResponseJson(ChatConfig config, ChatResponseDefault resp, String respJson);

    /**
     * 分析工具调用
     *
     * @param resp     响应体
     * @param oMessage 消息节点
     */
    List<AssistantMessage> parseAssistantMessage(ChatResponseDefault resp, ONode oMessage);
}
```


### 3、OllamaChatDialect 定制参考



如果方言有组件注解，会自动注册。否则，需要手动注册：

```java
ChatDialectManager.register(new OllamaChatDialect());
```

方言定制参考：

```java
public class OllamaChatDialect extends AbstractChatDialect {
    private static OllamaChatDialect instance = new OllamaChatDialect();

    public static OllamaChatDialect getInstance() {
        return instance;
    }

    /**
     * 匹配检测
     *
     * @param config 聊天配置
     */
    @Override
    public boolean matched(ChatConfig config) {
        return "ollama".equals(config.getProvider());
    }

    @Override
    protected void buildUserMessageNodeDo(ChatConfig config, ONode oNode, UserMessage msg) {
        oNode.set("role", msg.getRole().name().toLowerCase());
        if (msg.isMultiModal() == false) {
            //单模态
            oNode.set("content", msg.getContent());
        } else {
            //多模态
            oNode.set("content", msg.getContent());

            for (ContentBlock block1 : msg.getBlocks()) {
                if (block1 instanceof ImageBlock) {
                    oNode.getOrNew("images").add(block1.toDataString(false));
                } else if (block1 instanceof AudioBlock) {
                    oNode.getOrNew("audios").add(block1.toDataString(false));
                } else if (block1 instanceof VideoBlock) {
                    oNode.getOrNew("videos").add(block1.toDataString(false));
                }
            }
        }
    }

    @Override
    public ONode buildAssistantToolCallMessageNode(ChatResponseDefault resp, Map<String, ToolCallBuilder> toolCallBuilders) {
        ONode oNode = new ONode();
        oNode.set("role", "assistant");
        oNode.set("content", resp.getAggregationContent());
        oNode.getOrNew("tool_calls").asArray().then(n1 -> {
            for (Map.Entry<String, ToolCallBuilder> kv : toolCallBuilders.entrySet()) {
                //有可能没有
                n1.addNew().set("id", kv.getValue().idBuilder.toString())
                        .set("type", "function")
                        .getOrNew("function").then(n2 -> {
                            n2.set("name", kv.getValue().nameBuilder.toString());
                            n2.set("arguments", ONode.ofJson(kv.getValue().argumentsBuilder.toString()));
                        });
            }
        });

        return oNode;
    }

    @Override
    public boolean parseResponseJson(ChatConfig config, ChatResponseDefault resp, String json) {
        //解析
        ONode oResp = ONode.ofJson(json);

        if (oResp.isObject() == false) {
            return false;
        }

        if (oResp.hasKey("error")) {
            resp.setError(new ChatException(oResp.get("error").getString()));
        } else {
            resp.setModel(oResp.get("model").getString());
            resp.setFinished(oResp.get("done").getBoolean());
            String done_reason = oResp.get("done_reason").getString();

            String createdStr = oResp.get("created_at").getString();
            if (createdStr != null) {
                createdStr = createdStr.substring(0, createdStr.indexOf(".") + 4);
            }
            Date created = DateUtil.parseTry(createdStr);
            List<AssistantMessage> messageList = parseAssistantMessage(resp, oResp.get("message"));
            for (AssistantMessage msg1 : messageList) {
                resp.addChoice(new ChatChoice(0, created, done_reason, msg1));
            }

            if (Utils.isNotEmpty(done_reason)) {
                resp.lastFinishReason = done_reason;
            }

            if (resp.isFinished()) {
                long promptTokens = oResp.get("prompt_eval_count").getLong();
                long completionTokens = oResp.get("eval_count").getLong();
                long totalTokens = promptTokens + completionTokens;

                resp.setUsage(new AiUsage(promptTokens, completionTokens, totalTokens, oResp));

                if (resp.hasChoices() == false) {
                    resp.addChoice(new ChatChoice(0, created, resp.getLastFinishReasonNormalized(), new AssistantMessage("")));
                }
            }
        }

        return true;
    }

    @Override
    protected ToolCall parseToolCall(ChatResponseDefault resp, ONode n1) {
        String callId = n1.get("id").getString();//可能是空的

        ONode n1f = n1.get("function");
        String name = n1f.get("name").getString();
        ONode n1fArgs = n1f.get("arguments");
        String argStr = n1fArgs.getString();

        String index = name;

        if (n1fArgs.isValue()) {
            //有可能是 json string
            if (hasNestedJsonBlock(argStr)) {
                n1fArgs = ONode.ofJson(argStr);
            }
        }

        Map<String, Object> argMap = null;
        if (n1fArgs.isObject()) {
            argMap = n1fArgs.toBean(Map.class);
        }
        return new ToolCall(index, callId, name, argStr, argMap);
    }
}
```
