---
title: "chat - 模型的响应与计费"
---

在 Solon AI 中，模型调用有两种返回方式：`call()` 返回一个 `ChatResponse`，适用于同步调用；`stream()` 返回 `Flux<ChatEvent>`，适用于流式订阅。流式事件中的 `RESPONSE_END` 是一次响应的正常终态，调用 `getResponse()` 可取得完整的最终 `ChatResponse`（其 `getMessage()` 等结果访问器对应完整聚合结果）。ChatResponse 还负责追踪 Token 的消耗情况。

### 1、响应接口 (ChatResponse)

ChatResponse 统一了承载同步调用结果和流式终态结果的结构。针对目前流行的“推理模型”（如 DeepSeek-R1, OpenAI o1），它提供了专门的方法来区分思维链（Thinking）和最终答案。

提示： 对于支持深度思考的模型，推荐使用 getText() 获取最终文本，getContent() 可用于获取消息原始内容或日志记录。

以下接口代码与当前 `solon-ai-core/src/main/java/org/noear/solon/ai/chat/ChatResponse.java` 一致：

```java
package org.noear.solon.ai.chat;

import org.noear.solon.ai.AiUsage;
import org.noear.solon.ai.chat.content.ContentBlock;
import org.noear.solon.ai.chat.event.ChatEvent;
import org.noear.solon.ai.chat.event.ChatEventType;
import org.noear.solon.ai.chat.message.AssistantMessage;
import org.noear.solon.ai.chat.tool.ToolCall;
import org.noear.solon.lang.Nullable;
import org.noear.solon.lang.Preview;

import java.util.List;

/**
 * 聊天响应（模型调用的结果，不可变）
 */
@Preview("3.1")
public interface ChatResponse {
    /**
     * 获取原始响应数据
     */
    @Nullable
    String getFrameRaw();

    /**
     * 是否为终态
     */
    boolean isTerminal();

    /**
     * 获取模型
     */
    String getModel();

    /**
     * 获取错误
     */
    @Nullable
    ChatException getError();

    /**
     * 获取消息（终态；流式下即完整聚合结果）
     */
    @Nullable
    AssistantMessage getMessage();

    /**
     * 是否为空（没有内容，也没有工具调用）
     */
    boolean isEmpty();

    /**
     * 是否有消息内容
     */
    boolean hasContent();

    /**
     * 获取消息原始内容
     */
    String getContent();

    /**
     * 获取文本
     */
    String getText();

    /**
     * 获取思考
     */
    String getThinking();

    /**
     * 获取工具调用（没有时为空集合，不为 null）
     *
     * @since 4.1
     */
    List<ToolCall> getToolCalls();

    /**
     * 获取内容块（多模态；没有时为空集合，不为 null）
     *
     * @since 4.1
     */
    List<ContentBlock> getBlocks();

    /**
     * 获取完成原因（已归一化：工具调用为 {@code "tool"}、正常结束为 {@code "stop"}，
     * 其余如 {@code "length"} / {@code "content_filter"} 原样透传）
     *
     * @since 4.1
     */
    String getFinishReason();

    /**
     * 获取使用情况（完成时，才会有使用情况）
     */
    @Nullable
    AiUsage getUsage();

    /**
     * 本次响应的语义事件（仅非流式）
     *
     * <p>非流式 {@code call()} 没有事件流可供投递，但方言同样会解析出引用、服务端工具结果、
     * 思考签名、拒答等语义——这些信息并非流式独有，不能静默丢弃。因此非流式路径改为把事件
     * 收集到结果上。</p>
     *
     * <p>流式路径下事件由 {@code stream()} 直接投递，此处为空列表（不会为 null）。
     * 自动工具调用产生多轮时，与聚合消息一致——只携带末轮。</p>
     *
     * @since 4.1
     */
    List<ChatEvent> getEvents();
}
```

调用与流式订阅示例：

```java
// 同步调用：直接取得最终响应
ChatResponse response = chatModel.prompt("hello").call();
System.out.println(response.getText());

// 流式调用：RESPONSE_END 的 getResponse() 是完整终态
chatModel.prompt("hello").stream().subscribe(event -> {
    if (event.is(ChatEventType.RESPONSE_END)) {
        ChatResponse terminal = event.getResponse();
        System.out.println(terminal.getText());
    }
});
```

### 2、计费与使用统计 (AiUsage)

AiUsage 用于记录单次对话消耗的 Token 资源。不同的模型服务商提供的原始 JSON 结构差异很大，Solon AI 将其标准化为三个核心指标。


```java
package org.noear.solon.ai;

import org.noear.snack4.ONode;

public class AiUsage {
    private final long promptTokens;
    private final long thinkTokens;
    private final long completionTokens;
    private final long totalTokens;
    private final long cacheCreationInputTokens;
    private final long cacheReadInputTokens;
    private final ONode source;

    public AiUsage(long promptTokens, long thinkTokens, long completionTokens, long totalTokens, ONode source) {
        this(promptTokens, thinkTokens, completionTokens, totalTokens, 0L, 0L, source);
    }

    public AiUsage(long promptTokens, long thinkTokens, long completionTokens, long totalTokens,
                   long cacheCreationInputTokens, long cacheReadInputTokens, ONode source) {
        this.promptTokens = promptTokens;
        this.thinkTokens = thinkTokens;
        this.completionTokens = completionTokens;
        this.totalTokens = totalTokens;
        this.cacheCreationInputTokens = cacheCreationInputTokens;
        this.cacheReadInputTokens = cacheReadInputTokens;
        this.source = source;
    }

    /**
     * 获取提示语消耗词元数
     */
    public long promptTokens() {
        return promptTokens;
    }

    /**
     * 获取思考消耗词元数
     */
    public long thinkTokens() {
        return thinkTokens;
    }

    /**
     * 获取完成消耗词元数
     */
    public long completionTokens() {
        return completionTokens;
    }

    /**
     * 获取总消耗词元数
     */
    public long totalTokens() {
        return totalTokens;
    }

    /**
     * 获取缓存创建输入词元数 (Claude Prompt Caching)
     */
    public long cacheCreationInputTokens() {
        return cacheCreationInputTokens;
    }

    /**
     * 获取缓存读取输入词元数 (Claude Prompt Caching)
     */
    public long cacheReadInputTokens() {
        return cacheReadInputTokens;
    }

    /**
     * 源数据
     */
    public ONode getSource() {
        return source;
    }
}
```


### 3、代码示例

获取清理后的内容


```java
ChatResponse resp = chatModel.call(prompt);

// 自动处理思考过程，只打印结果
System.out.println("Result: " + resp.getText());

// 打印计费信息
AiUsage usage = resp.getUsage();
if (usage != null) {
    System.out.println("Cost Tokens: " + usage.totalTokens());
}
```


处理原始数据
如果你需要获取厂商特有的计费细节（例如 DeepSeek 的 prompt_cache_hit_tokens）：

```java
long cacheHit = resp.getUsage().getSource()
                    .get("usage")
                    .get("prompt_cache_hit_tokens").getLong();
```
